fix(core): Support streamed reasoning from custom AI endpoints (no-changelog) (#37875)

This commit is contained in:
Predrag Ristic
2026-10-09 05:45:07 +00:00
committed by GitHub
parent c4f84e2f78
commit 53d33b0f95
3 changed files with 110 additions and 5 deletions
@@ -0,0 +1,70 @@
import { streamText } from 'ai';
import { createModel } from '../model/model-factory';
describe('createModel streaming', () => {
it('streams array-based reasoning and text from a custom endpoint', async () => {
const deltas = [
...['Let me ', 'think.'].map((text) => ({
content: [{ type: 'thinking', thinking: [{ type: 'text', text }], closed: true }],
tool_calls: [],
})),
{ content: [{ type: 'text', text: 'The answer ' }] },
{ content: 'is 391.' },
];
const responseChunks = [
...deltas.map((delta) => ({ choices: [{ index: 0, delta, finish_reason: null }] })),
{ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] },
{
choices: [],
usage: { prompt_tokens: 120, completion_tokens: 30, total_tokens: 150 },
},
];
const body = responseChunks
.map((chunk) => {
const payload = {
id: 'completion-1',
object: 'chat.completion.chunk',
created: 1,
model: 'zai-glm-5-2',
...chunk,
};
return `data: ${JSON.stringify(payload)}\n\n`;
})
.join('');
const fetch = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
new Response(`${body}data: [DONE]\n\n`, {
headers: { 'content-type': 'text/event-stream' },
}),
);
const model = createModel(
{
id: 'custom/zai-glm-5-2',
url: 'https://model.example.com/v1',
apiKey: 'test-key',
},
fetch,
);
const result = streamText({ model, prompt: 'Hello', maxRetries: 0 });
const chunks = [];
for await (const chunk of result.fullStream) chunks.push(chunk);
expect(fetch).toHaveBeenCalledOnce();
expect(fetch.mock.calls[0][0]).toBe('https://model.example.com/v1/chat/completions');
const requestBody = fetch.mock.calls[0][1]?.body;
expect(requestBody).not.toContain('"stream_options"');
expect(chunks.filter((chunk) => chunk.type === 'error')).toEqual([]);
expect(chunks.filter((chunk) => chunk.type.startsWith('reasoning-'))).toEqual([
{ type: 'reasoning-start', id: 'reasoning-0' },
{ type: 'reasoning-delta', id: 'reasoning-0', text: 'Let me ' },
{ type: 'reasoning-delta', id: 'reasoning-0', text: 'think.' },
{ type: 'reasoning-end', id: 'reasoning-0' },
]);
await expect(result.text).resolves.toBe('The answer is 391.');
await expect(result.totalUsage).resolves.toMatchObject({
inputTokens: 120,
outputTokens: 30,
totalTokens: 150,
});
});
});
+39 -4
View File
@@ -207,8 +207,8 @@ catalogs:
specifier: ^4.0.20 specifier: ^4.0.20
version: 4.0.20 version: 4.0.20
'@ai-sdk/openai-compatible': '@ai-sdk/openai-compatible':
specifier: ^3.0.14 specifier: ^3.0.44
version: 3.0.28 version: 3.0.44
'@ai-sdk/otel': '@ai-sdk/otel':
specifier: ^1.0.66 specifier: ^1.0.66
version: 1.0.74 version: 1.0.74
@@ -1209,7 +1209,7 @@ importers:
version: 4.0.20([email protected]) version: 4.0.20([email protected])
'@ai-sdk/openai-compatible': '@ai-sdk/openai-compatible':
specifier: 'catalog:' specifier: 'catalog:'
version: 3.0.28([email protected]) version: 3.0.44([email protected])
'@ai-sdk/otel': '@ai-sdk/otel':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.0.74([email protected]) version: 1.0.74([email protected])
@@ -4723,7 +4723,7 @@ importers:
version: 4.0.39([email protected]) version: 4.0.39([email protected])
'@ai-sdk/openai-compatible': '@ai-sdk/openai-compatible':
specifier: 'catalog:' specifier: 'catalog:'
version: 3.0.28([email protected]) version: 3.0.44([email protected])
'@apidevtools/json-schema-ref-parser': '@apidevtools/json-schema-ref-parser':
specifier: 12.0.2 specifier: 12.0.2
version: 12.0.2 version: 12.0.2
@@ -8772,6 +8772,12 @@ packages:
peerDependencies: peerDependencies:
zod: 3.25.76 zod: 3.25.76
'@ai-sdk/[email protected]':
resolution: {integrity: sha512-pK7mERd/aIJVtyQCe/nV3G3iEJCY1no8GHDOGbgpNC+petjSfsxj+ni0Q8WVVehTUiY9B04F3Xvir+icBgORCg==}
engines: {node: '>=22'}
peerDependencies:
zod: 3.25.76
'@ai-sdk/[email protected]': '@ai-sdk/[email protected]':
resolution: {integrity: sha512-/Hu+btLIO2zuYqCp//vBBAGdM/BeN4gds+NWrlv7Y9WrcTXwnEEwh6P3QZIWh2Tt1I1lswuDIpAJ4t/c/rws3Q==} resolution: {integrity: sha512-/Hu+btLIO2zuYqCp//vBBAGdM/BeN4gds+NWrlv7Y9WrcTXwnEEwh6P3QZIWh2Tt1I1lswuDIpAJ4t/c/rws3Q==}
engines: {node: '>=22'} engines: {node: '>=22'}
@@ -8806,6 +8812,16 @@ packages:
peerDependencies: peerDependencies:
zod: 3.25.76 zod: 3.25.76
'@ai-sdk/[email protected]':
resolution: {integrity: sha512-MFXBn6XDyf37PNQAge/HTatPJE8Vmg/g/w4WPtjSV53jq8FKAzoaN5+43hsdQa9bqgN+/13jxug9ChvsG+godQ==}
engines: {node: '>=22'}
peerDependencies:
zod: 3.25.76
'@ai-sdk/[email protected]':
resolution: {integrity: sha512-fX2ENAc7iDpZ+Wp4+Rk06Usn/Ys7dI9uAkGv0jlF6XVrW13NkRWx5Ou+U6lIM2E1fTLkCs16GGrUAaVvroag7A==}
engines: {node: '>=22'}
'@ai-sdk/[email protected]': '@ai-sdk/[email protected]':
resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==}
engines: {node: '>=22'} engines: {node: '>=22'}
@@ -25311,6 +25327,12 @@ snapshots:
'@ai-sdk/provider-utils': 5.0.25([email protected]) '@ai-sdk/provider-utils': 5.0.25([email protected])
zod: 3.25.76 zod: 3.25.76
'@ai-sdk/[email protected]([email protected])':
dependencies:
'@ai-sdk/provider': 4.0.10
'@ai-sdk/provider-utils': 5.0.36([email protected])
zod: 3.25.76
'@ai-sdk/[email protected]([email protected])': '@ai-sdk/[email protected]([email protected])':
dependencies: dependencies:
'@ai-sdk/provider': 4.0.3 '@ai-sdk/provider': 4.0.3
@@ -25360,6 +25382,19 @@ snapshots:
undici: 7.29.1 undici: 7.29.1
zod: 3.25.76 zod: 3.25.76
'@ai-sdk/[email protected]([email protected])':
dependencies:
'@ai-sdk/provider': 4.0.10
'@standard-schema/spec': 1.1.0
'@workflow/serde': 4.1.0
eventsource-parser: 3.0.8
undici: 7.29.1
zod: 3.25.76
'@ai-sdk/[email protected]':
dependencies:
json-schema: 0.4.0
'@ai-sdk/[email protected]': '@ai-sdk/[email protected]':
dependencies: dependencies:
json-schema: 0.4.0 json-schema: 0.4.0
+1 -1
View File
@@ -65,7 +65,7 @@ catalog:
'@ai-sdk/mistral': ^4.0.14 '@ai-sdk/mistral': ^4.0.14
'@ai-sdk/moonshotai': ^3.0.35 '@ai-sdk/moonshotai': ^3.0.35
'@ai-sdk/openai': ^4.0.20 '@ai-sdk/openai': ^4.0.20
'@ai-sdk/openai-compatible': ^3.0.14 '@ai-sdk/openai-compatible': ^3.0.44
'@ai-sdk/otel': ^1.0.66 '@ai-sdk/otel': ^1.0.66
'@ai-sdk/provider-utils': ^5.0.27 '@ai-sdk/provider-utils': ^5.0.27
'@ai-sdk/xai': ^4.0.18 '@ai-sdk/xai': ^4.0.18