Skip to content

Commit 2597347

Browse files
authored
Use openrouter stream_options include_usage (#1905)
1 parent 92dee31 commit 2597347

File tree

2 files changed

+24
-59
lines changed

2 files changed

+24
-59
lines changed

src/api/providers/__tests__/openrouter.test.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ describe("OpenRouterHandler", () => {
112112
},
113113
],
114114
}
115+
// Add usage information in the stream response
116+
yield {
117+
id: "test-id",
118+
choices: [{ delta: {} }],
119+
usage: {
120+
prompt_tokens: 10,
121+
completion_tokens: 20,
122+
cost: 0.001,
123+
},
124+
}
115125
},
116126
}
117127

@@ -121,17 +131,6 @@ describe("OpenRouterHandler", () => {
121131
completions: { create: mockCreate },
122132
} as any
123133

124-
// Mock axios.get for generation details
125-
;(axios.get as jest.Mock).mockResolvedValue({
126-
data: {
127-
data: {
128-
native_tokens_prompt: 10,
129-
native_tokens_completion: 20,
130-
total_cost: 0.001,
131-
},
132-
},
133-
})
134-
135134
const systemPrompt = "test system prompt"
136135
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
137136

@@ -153,7 +152,6 @@ describe("OpenRouterHandler", () => {
153152
inputTokens: 10,
154153
outputTokens: 20,
155154
totalCost: 0.001,
156-
fullResponseText: "test response",
157155
})
158156

159157
// Verify OpenAI client was called with correct parameters

src/api/providers/openrouter.ts

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
2424
thinking?: BetaThinkingConfigParam
2525
}
2626

27-
// Add custom interface for OpenRouter usage chunk.
28-
interface OpenRouterApiStreamUsageChunk extends ApiStreamUsageChunk {
29-
fullResponseText: string
30-
}
31-
3227
export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler {
3328
protected options: ApiHandlerOptions
3429
private client: OpenAI
@@ -110,7 +105,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
110105
top_p: topP,
111106
messages: openAiMessages,
112107
stream: true,
113-
include_reasoning: true,
108+
stream_options: { include_usage: true },
114109
// Only include provider if openRouterSpecificProvider is not "[default]".
115110
...(this.options.openRouterSpecificProvider &&
116111
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && {
@@ -122,7 +117,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
122117

123118
const stream = await this.client.chat.completions.create(completionParams)
124119

125-
let genId: string | undefined
120+
let lastUsage
126121

127122
for await (const chunk of stream as unknown as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {
128123
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
@@ -132,10 +127,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
132127
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
133128
}
134129

135-
if (!genId && chunk.id) {
136-
genId = chunk.id
137-
}
138-
139130
const delta = chunk.choices[0]?.delta
140131

141132
if ("reasoning" in delta && delta.reasoning) {
@@ -146,47 +137,23 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
146137
fullResponseText += delta.content
147138
yield { type: "text", text: delta.content } as ApiStreamChunk
148139
}
149-
}
150-
151-
const endpoint = `${this.client.baseURL}/generation?id=${genId}`
152140

153-
const config: AxiosRequestConfig = {
154-
headers: { Authorization: `Bearer ${this.options.openRouterApiKey}` },
155-
timeout: 3_000,
141+
if (chunk.usage) {
142+
lastUsage = chunk.usage
143+
}
156144
}
157145

158-
let attempt = 0
159-
let lastError: Error | undefined
160-
const startTime = Date.now()
161-
162-
while (attempt++ < 10) {
163-
await delay(attempt * 100) // Give OpenRouter some time to produce the generation metadata.
164-
165-
try {
166-
const response = await axios.get(endpoint, config)
167-
const generation = response.data?.data
168-
169-
yield {
170-
type: "usage",
171-
inputTokens: generation?.native_tokens_prompt || 0,
172-
outputTokens: generation?.native_tokens_completion || 0,
173-
totalCost: generation?.total_cost || 0,
174-
fullResponseText,
175-
} as OpenRouterApiStreamUsageChunk
176-
177-
break
178-
} catch (error: unknown) {
179-
if (error instanceof Error) {
180-
lastError = error
181-
}
182-
}
146+
if (lastUsage) {
147+
yield this.processUsageMetrics(lastUsage)
183148
}
149+
}
184150

185-
if (lastError) {
186-
console.error(
187-
`Failed to fetch OpenRouter generation details after attempt #${attempt} (${Date.now() - startTime}ms) [${genId}]`,
188-
lastError,
189-
)
151+
processUsageMetrics(usage: any): ApiStreamUsageChunk {
152+
return {
153+
type: "usage",
154+
inputTokens: usage?.prompt_tokens || 0,
155+
outputTokens: usage?.completion_tokens || 0,
156+
totalCost: usage?.cost || 0,
190157
}
191158
}
192159

0 commit comments

Comments
 (0)