Skip to content
Open
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
139 changes: 139 additions & 0 deletions packages/core/test/provider-openai-compatible.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
97 changes: 91 additions & 6 deletions patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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',
Expand All @@ -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,
Loading