Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/breezy-eagles-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ai-sdk/openai-compatible": patch
---

fix(openai-compatible): buffer tool call deltas until function.name arrives
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,306 @@
`);
});

<<<<<<< HEAD

Check failure on line 1602 in packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

Merge conflict marker encountered.
=======

Check failure on line 1603 in packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

Merge conflict marker encountered.
it('should stream tool deltas when function.name arrives in a later chunk', async () => {
server.urls['https://my.api.com/v1/chat/completions'].response = {
type: 'stream-chunks',
chunks: [
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"role":"assistant","content":null},"finish_reason":null}]}\n\n`,
// First tool_calls delta carries id and (empty) arguments but no function.name
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_late","type":"function","function":{"arguments":""}}]},` +
`"finish_reason":null}]}\n\n`,
// function.name arrives in the next chunk together with the start of the arguments
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_late","type":"function","function":{"name":"test-tool","arguments":"{\\""}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\"hi\\"}"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1729171479,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` +
`"usage":{"prompt_tokens":18,"completion_tokens":10,"total_tokens":28}}\n\n`,
'data: [DONE]\n\n',
],
};

const { stream } = await model.doStream({
tools: [
{
type: 'function',
name: 'test-tool',
inputSchema: {
type: 'object',
properties: { value: { type: 'string' } },
required: ['value'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
},
},
],
prompt: TEST_PROMPT,
includeRawChunks: false,
});

expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
[
{
"type": "stream-start",
"warnings": [],
},
{
"id": "chatcmpl-late-name",
"modelId": "grok-3",
"timestamp": 2024-03-25T09:06:38.000Z,
"type": "response-metadata",
},
{
"id": "call_late",
"toolName": "test-tool",
"type": "tool-input-start",
},
{
"delta": "{"",
"id": "call_late",
"type": "tool-input-delta",
},
{
"delta": "value",
"id": "call_late",
"type": "tool-input-delta",
},
{
"delta": "":"hi"}",
"id": "call_late",
"type": "tool-input-delta",
},
{
"id": "call_late",
"type": "tool-input-end",
},
{
"input": "{"value":"hi"}",
"toolCallId": "call_late",
"toolName": "test-tool",
"type": "tool-call",
},
{
"finishReason": {
"raw": "tool_calls",
"unified": "tool-calls",
},
"providerMetadata": {
"test-provider": {},
},
"type": "finish",
"usage": {
"inputTokens": {
"cacheRead": 0,
"cacheWrite": undefined,
"noCache": 18,
"total": 18,
},
"outputTokens": {
"reasoning": 0,
"text": 10,
"total": 10,
},
"raw": {
"completion_tokens": 10,
"prompt_tokens": 18,
"total_tokens": 28,
},
},
},
]
`);
});

it('should error when streamed tool call never receives a function.name', async () => {
server.urls['https://my.api.com/v1/chat/completions'].response = {
type: 'stream-chunks',
chunks: [
`data: {"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"role":"assistant","content":null},"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_missing","type":"function","function":{"arguments":"{}"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1729171479,"model":"grok-3",` +
`"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` +
`"usage":{"prompt_tokens":18,"completion_tokens":10,"total_tokens":28}}\n\n`,
'data: [DONE]\n\n',
],
};

const { stream } = await model.doStream({
tools: [
{
type: 'function',
name: 'test-tool',
inputSchema: {
type: 'object',
properties: { value: { type: 'string' } },
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
},
},
],
prompt: TEST_PROMPT,
includeRawChunks: false,
});

await expect(
convertReadableStreamToArray(stream),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[AI_InvalidResponseDataError: Expected 'function.name' to be a string.]`,
);
});

it('should stream tool call with thought signature from extra_content', async () => {
server.urls['https://my.api.com/v1/chat/completions'].response = {
type: 'stream-chunks',
chunks: [
// First chunk with tool call start and thought signature in extra_content
`data: {"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
`"tool_calls":[{"index":0,"id":"function-call-1","type":"function","function":{"name":"check_flight","arguments":""},` +
`"extra_content":{"google":{"thought_signature":"<Signature A>"}}}]},` +
`"finish_reason":null}]}\n\n`,
// Subsequent chunks with arguments
`data: {"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"flight\\":"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"AA100\\"}"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` +
`"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}\n\n`,
'data: [DONE]\n\n',
],
};

const { stream } = await model.doStream({
tools: [
{
type: 'function',
name: 'check_flight',
inputSchema: {
type: 'object',
properties: { flight: { type: 'string' } },
required: ['flight'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
},
},
],
prompt: TEST_PROMPT,
includeRawChunks: false,
});

const result = await convertReadableStreamToArray(stream);

// Find the tool-call event and verify it has the thought signature in providerMetadata
const toolCallEvent = result.find(
(event: { type: string }) => event.type === 'tool-call',
);
expect(toolCallEvent).toMatchObject({
type: 'tool-call',
toolCallId: 'function-call-1',
toolName: 'check_flight',
input: '{"flight":"AA100"}',
providerMetadata: {
'test-provider': {
thoughtSignature: '<Signature A>',
},
},
});
});

it('should stream parallel tool calls with signature only on first call', async () => {
server.urls['https://my.api.com/v1/chat/completions'].response = {
type: 'stream-chunks',
chunks: [
// First chunk with two tool calls - only first has thought signature
`data: {"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
`"tool_calls":[` +
`{"index":0,"id":"call-paris","type":"function","function":{"name":"get_weather","arguments":""},` +
`"extra_content":{"google":{"thought_signature":"<Signature A>"}}},` +
`{"index":1,"id":"call-london","type":"function","function":{"name":"get_weather","arguments":""}}` +
`]},` +
`"finish_reason":null}]}\n\n`,
// Arguments for first call
`data: {"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"location\\":\\"Paris\\"}"}}]},` +
`"finish_reason":null}]}\n\n`,
// Arguments for second call
`data: {"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"location\\":\\"London\\"}"}}]},` +
`"finish_reason":null}]}\n\n`,
`data: {"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro",` +
`"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` +
`"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}\n\n`,
'data: [DONE]\n\n',
],
};

const { stream } = await model.doStream({
tools: [
{
type: 'function',
name: 'get_weather',
inputSchema: {
type: 'object',
properties: { location: { type: 'string' } },
required: ['location'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
},
},
],
prompt: TEST_PROMPT,
includeRawChunks: false,
});

const result = await convertReadableStreamToArray(stream);

const toolCallEvents = result.filter(
(event: { type: string }) => event.type === 'tool-call',
);

expect(toolCallEvents).toHaveLength(2);

// First tool call should have thought signature
expect(toolCallEvents[0]).toMatchObject({
type: 'tool-call',
toolCallId: 'call-paris',
toolName: 'get_weather',
providerMetadata: {
'test-provider': {
thoughtSignature: '<Signature A>',
},
},
});

// Second tool call should NOT have thought signature
expect(toolCallEvents[1]).toMatchObject({
type: 'tool-call',
toolCallId: 'call-london',
toolName: 'get_weather',
});
expect(
(toolCallEvents[1] as { providerMetadata?: unknown }).providerMetadata,
).toBeUndefined();
});

>>>>>>> 4d4e1768ea ([v6.0] fix(openai-compatible): buffer tool call deltas until function.name arrives (#16836))

Check failure on line 1901 in packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

Merge conflict marker encountered.
it('should stream tool call deltas when tool call arguments are passed in the first chunk', async () => {
server.urls['https://my.api.com/v1/chat/completions'].response = {
type: 'stream-chunks',
Expand Down
Loading
Loading