Skip to content

Commit aeb7a54

Browse files
authored
fix(proxy): inject stream_options.include_usage for Ollama endpoints (#1592)
Ollama and Ollama Cloud streaming responses returned 0 tokens because they require stream_options.include_usage to emit usage data in the final SSE chunk. Extend the existing injection (added for OpenAI and OpenRouter in #1567) to cover ollama and ollama-cloud endpoint keys. Closes #1585
1 parent 67f8852 commit aeb7a54

File tree

3 files changed

+40
-1
lines changed

3 files changed

+40
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"manifest": patch
3+
---
4+
5+
fix(proxy): capture streaming token usage for Ollama and Ollama Cloud providers

packages/backend/src/routing/proxy/__tests__/provider-client.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,34 @@ describe('ProviderClient', () => {
14141414
expect(sentBody.stream_options).toEqual({ include_usage: true });
14151415
});
14161416

1417+
it('injects stream_options.include_usage for Ollama streaming requests', async () => {
1418+
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
1419+
await client.forward({
1420+
provider: 'ollama',
1421+
apiKey: '',
1422+
model: 'llama3',
1423+
body: { messages: [{ role: 'user', content: 'Hello' }] },
1424+
stream: true,
1425+
});
1426+
1427+
const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body);
1428+
expect(sentBody.stream_options).toEqual({ include_usage: true });
1429+
});
1430+
1431+
it('injects stream_options.include_usage for Ollama Cloud streaming requests', async () => {
1432+
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
1433+
await client.forward({
1434+
provider: 'ollama-cloud',
1435+
apiKey: 'ollama-key',
1436+
model: 'llama3',
1437+
body: { messages: [{ role: 'user', content: 'Hello' }] },
1438+
stream: true,
1439+
});
1440+
1441+
const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body);
1442+
expect(sentBody.stream_options).toEqual({ include_usage: true });
1443+
});
1444+
14171445
it('does not inject stream_options for non-streaming OpenAI requests', async () => {
14181446
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
14191447
await client.forward({

packages/backend/src/routing/proxy/provider-client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@ export class ProviderClient {
123123
// Inject stream_options.include_usage so providers always send token
124124
// usage in streaming responses — needed for both DB logging and
125125
// downstream clients (e.g. OpenClaw context management).
126-
if (stream && (endpointKey === 'openai' || endpointKey === 'openrouter')) {
126+
if (
127+
stream &&
128+
(endpointKey === 'openai' ||
129+
endpointKey === 'openrouter' ||
130+
endpointKey === 'ollama' ||
131+
endpointKey === 'ollama-cloud')
132+
) {
127133
const existing =
128134
typeof sanitized.stream_options === 'object' && sanitized.stream_options !== null
129135
? (sanitized.stream_options as Record<string, unknown>)

0 commit comments

Comments
 (0)