diff --git a/packages/core/test/provider-openai-compatible.test.ts b/packages/core/test/provider-openai-compatible.test.ts new file mode 100644 index 000000000000..62a98e06a86b --- /dev/null +++ b/packages/core/test/provider-openai-compatible.test.ts @@ -0,0 +1,139 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import { expect, test } from "bun:test" + +test("openai-compatible synthesizes missing tool-call IDs while streaming", async () => { + const chunks = [ + { + id: "response-1", + created: 0, + model: "zai-glm-5-2", + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + function: { name: "bash", arguments: '{"command":"ls' }, + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "zai-glm-5-2", + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + function: { name: "", arguments: ' -l"}' }, + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "zai-glm-5-2", + object: "chat.completion.chunk", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }, + ] + const mockFetch = Object.assign( + async () => + new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), { + headers: { "Content-Type": "text/event-stream" }, + }), + { preconnect: fetch.preconnect }, + ) + const provider = createOpenAICompatible({ + apiKey: "test", + baseURL: "https://api.mistral.ai/v1", + name: "mistral", + fetch: mockFetch, + }) + const model = provider("zai-glm-5-2") + const result = await model.doStream({ + prompt: [{ role: "user", content: [{ type: "text", text: "list files" }] }], + }) + const events = [] + for await (const event of result.stream) events.push(event) + + const toolInputStart = events.find((event) => event.type === "tool-input-start") + const toolCall = events.find((event) => event.type === "tool-call") + const errors = events.filter((event) => event.type === "error") + + expect(errors).toEqual([]) + expect(toolInputStart).toBeDefined() + expect(toolCall).toBeDefined() + expect(typeof toolInputStart?.id).toBe("string") + expect(toolCall?.toolCallId).toBe(toolInputStart?.id) + expect(toolCall?.toolName).toBe("bash") + expect(JSON.parse(toolCall?.input ?? "{}")).toEqual({ command: "ls -l" }) +}) + +test("openai-compatible uses provided tool-call IDs while streaming", async () => { + const chunks = [ + { + id: "response-1", + created: 0, + model: "test-model", + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + id: "call_abc123", + index: 0, + function: { name: "bash", arguments: '{"command":"ls"}' }, + }, + ], + }, + }, + ], + }, + { + id: "response-1", + created: 0, + model: "test-model", + object: "chat.completion.chunk", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }, + ] + const mockFetch = Object.assign( + async () => + new Response(chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join(""), { + headers: { "Content-Type": "text/event-stream" }, + }), + { preconnect: fetch.preconnect }, + ) + const provider = createOpenAICompatible({ + apiKey: "test", + baseURL: "https://example.com/v1", + name: "test", + fetch: mockFetch, + }) + const model = provider("test-model") + const result = await model.doStream({ + prompt: [{ role: "user", content: [{ type: "text", text: "list files" }] }], + }) + const events = [] + for await (const event of result.stream) events.push(event) + + const toolCall = events.find((event) => event.type === "tool-call") + expect(toolCall?.toolCallId).toBe("call_abc123") +}) diff --git a/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch index 9f03ec95732a..3b69bbf28073 100644 --- a/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch +++ b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.js b/dist/index.js -index dca128d3a790378c51a24a16d92585178343b278..da75f9d64acd2b607abd15079ce2d03b7a8d675a 100644 +index dca128d3a790378c51a24a16d92585178343b278..5b92d5b308eb26b9209f90a162c74a737a537623 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -696,7 +696,7 @@ var OpenAICompatibleChatLanguageModel = class { +@@ -696,7 +696,7 @@ finishReason = { unified: "error", raw: void 0 }; controller.enqueue({ type: "error", @@ -11,11 +11,39 @@ index dca128d3a790378c51a24a16d92585178343b278..da75f9d64acd2b607abd15079ce2d03b }); return; } +@@ -766,12 +766,7 @@ + for (const toolCallDelta of delta.tool_calls) { + const index = (_c = toolCallDelta.index) != null ? _c : toolCalls.length; + if (toolCalls[index] == null) { +- if (toolCallDelta.id == null) { +- throw new import_provider3.InvalidResponseDataError({ +- data: toolCallDelta, +- message: `Expected 'id' to be a string.` +- }); +- } ++ const toolCallId = (_c = toolCallDelta.id) != null ? _c : (0, import_provider_utils2.generateId)(); + if (((_d = toolCallDelta.function) == null ? void 0 : _d.name) == null) { + throw new import_provider3.InvalidResponseDataError({ + data: toolCallDelta, +@@ -780,11 +775,11 @@ + } + controller.enqueue({ + type: "tool-input-start", +- id: toolCallDelta.id, ++ id: toolCallId, + toolName: toolCallDelta.function.name + }); + toolCalls[index] = { +- id: toolCallDelta.id, ++ id: toolCallId, + type: "function", + function: { + name: toolCallDelta.function.name, diff --git a/dist/index.mjs b/dist/index.mjs -index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..eaffc446f80552ea0b26573b89daa4dfc7776e6e 100644 +index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..0c55b7352d67129814b783f25cb779417fe826af 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -683,7 +683,7 @@ var OpenAICompatibleChatLanguageModel = class { +@@ -683,7 +683,7 @@ finishReason = { unified: "error", raw: void 0 }; controller.enqueue({ type: "error", @@ -24,11 +52,39 @@ index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..eaffc446f80552ea0b26573b89daa4df }); return; } +@@ -753,12 +753,7 @@ + for (const toolCallDelta of delta.tool_calls) { + const index = (_c = toolCallDelta.index) != null ? _c : toolCalls.length; + if (toolCalls[index] == null) { +- if (toolCallDelta.id == null) { +- throw new InvalidResponseDataError({ +- data: toolCallDelta, +- message: `Expected 'id' to be a string.` +- }); +- } ++ const toolCallId = (_c = toolCallDelta.id) != null ? _c : generateId(); + if (((_d = toolCallDelta.function) == null ? void 0 : _d.name) == null) { + throw new InvalidResponseDataError({ + data: toolCallDelta, +@@ -767,11 +762,11 @@ + } + controller.enqueue({ + type: "tool-input-start", +- id: toolCallDelta.id, ++ id: toolCallId, + toolName: toolCallDelta.function.name + }); + toolCalls[index] = { +- id: toolCallDelta.id, ++ id: toolCallId, + type: "function", + function: { + name: toolCallDelta.function.name, diff --git a/src/chat/openai-compatible-chat-language-model.ts b/src/chat/openai-compatible-chat-language-model.ts -index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..643c3db68a6043e097edc1122e0eb53fd13495c5 100644 +index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..58ee6eb913399bef344422fd5cda69c1e0ded65a 100644 --- a/src/chat/openai-compatible-chat-language-model.ts +++ b/src/chat/openai-compatible-chat-language-model.ts -@@ -442,7 +442,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { +@@ -442,7 +442,7 @@ finishReason = { unified: 'error', raw: undefined }; controller.enqueue({ type: 'error', @@ -37,3 +93,32 @@ index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..643c3db68a6043e097edc1122e0eb53f }); return; } +@@ -533,12 +533,7 @@ + const index = toolCallDelta.index ?? toolCalls.length; + + if (toolCalls[index] == null) { +- if (toolCallDelta.id == null) { +- throw new InvalidResponseDataError({ +- data: toolCallDelta, +- message: `Expected 'id' to be a string.`, +- }); +- } ++ const toolCallId = toolCallDelta.id ?? generateId(); + + if (toolCallDelta.function?.name == null) { + throw new InvalidResponseDataError({ +@@ -549,12 +544,12 @@ + + controller.enqueue({ + type: 'tool-input-start', +- id: toolCallDelta.id, ++ id: toolCallId, + toolName: toolCallDelta.function.name, + }); + + toolCalls[index] = { +- id: toolCallDelta.id, ++ id: toolCallId, + type: 'function', + function: { + name: toolCallDelta.function.name,