diff --git a/bun.lock b/bun.lock index 35075c1441e2..9b4932552949 100644 --- a/bun.lock +++ b/bun.lock @@ -641,6 +641,7 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", + "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", }, "overrides": { "@types/bun": "catalog:", diff --git a/package.json b/package.json index 9d9207c5ea3e..5862dc117eb5 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "@types/node": "catalog:" }, "patchedDependencies": { + "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" diff --git a/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch new file mode 100644 index 000000000000..b018cbe4dabd --- /dev/null +++ b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch @@ -0,0 +1,398 @@ +diff --git a/dist/index.js b/dist/index.js +index 168bec7..19170c6 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -611,6 +611,14 @@ var OpenAICompatibleChatLanguageModel = class { + }); + } + } ++ if (choice.message.function_call != null && choice.message.tool_calls == null) { ++ content.push({ ++ type: "tool-call", ++ toolCallId: (0, import_provider_utils2.generateId)(), ++ toolName: choice.message.function_call.name, ++ input: choice.message.function_call.arguments ++ }); ++ } + const providerMetadata = { + [metadataKey]: {}, + ...await ((_f = (_e = this.config.metadataExtractor) == null ? void 0 : _e.extractMetadata) == null ? void 0 : _f.call(_e, { +@@ -644,6 +652,14 @@ var OpenAICompatibleChatLanguageModel = class { + async doStream(options) { + var _a; + const { args, warnings, metadataKey } = await this.getArgs({ ...options }); ++ const requestUrl = this.config.url({ ++ path: "/chat/completions", ++ modelId: this.modelId ++ }); ++ const requestHeaders = (0, import_provider_utils2.combineHeaders)(this.config.headers(), options.headers); ++ const fallbackBody = this.transformRequestBody({ ++ ...args ++ }); + const body = this.transformRequestBody({ + ...args, + stream: true, +@@ -651,21 +667,22 @@ var OpenAICompatibleChatLanguageModel = class { + stream_options: this.config.includeUsage ? { include_usage: true } : void 0 + }); + const metadataExtractor = (_a = this.config.metadataExtractor) == null ? void 0 : _a.createStreamExtractor(); ++ const failedResponseHandler = this.failedResponseHandler; ++ const fetch = this.config.fetch; + const { responseHeaders, value: response } = await (0, import_provider_utils2.postJsonToApi)({ +- url: this.config.url({ +- path: "/chat/completions", +- modelId: this.modelId +- }), +- headers: (0, import_provider_utils2.combineHeaders)(this.config.headers(), options.headers), ++ url: requestUrl, ++ headers: requestHeaders, + body, +- failedResponseHandler: this.failedResponseHandler, ++ failedResponseHandler, + successfulResponseHandler: (0, import_provider_utils2.createEventSourceResponseHandler)( + this.chunkSchema + ), + abortSignal: options.abortSignal, +- fetch: this.config.fetch ++ fetch + }); + const toolCalls = []; ++ const pendingToolCallIds = {}; ++ let sawToolCallStart = false; + let finishReason = { + unified: "other", + raw: void 0 +@@ -755,7 +772,12 @@ var OpenAICompatibleChatLanguageModel = class { + delta: delta.content + }); + } +- if (delta.tool_calls != null) { ++ const toolCallDeltas = delta.tool_calls ?? (delta.function_call ? [{ ++ index: 0, ++ id: void 0, ++ function: delta.function_call ++ }] : void 0); ++ if (toolCallDeltas != null) { + if (isActiveReasoning) { + controller.enqueue({ + type: "reasoning-end", +@@ -763,28 +785,23 @@ var OpenAICompatibleChatLanguageModel = class { + }); + isActiveReasoning = false; + } +- for (const toolCallDelta of delta.tool_calls) { ++ for (const toolCallDelta of toolCallDeltas) { + 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 = toolCallDelta.id ?? pendingToolCallIds[index] ?? (0, import_provider_utils2.generateId)(); + if (((_d = toolCallDelta.function) == null ? void 0 : _d.name) == null) { +- throw new import_provider3.InvalidResponseDataError({ +- data: toolCallDelta, +- message: `Expected 'function.name' to be a string.` +- }); ++ pendingToolCallIds[index] = toolCallId; ++ continue; + } ++ delete pendingToolCallIds[index]; + controller.enqueue({ + type: "tool-input-start", +- id: toolCallDelta.id, ++ id: toolCallId, + toolName: toolCallDelta.function.name + }); ++ sawToolCallStart = true; + toolCalls[index] = { +- id: toolCallDelta.id, ++ id: toolCallId, + type: "function", + function: { + name: toolCallDelta.function.name, +@@ -860,8 +877,82 @@ var OpenAICompatibleChatLanguageModel = class { + } + } + }, +- flush(controller) { ++ async flush(controller) { + var _a2, _b, _c, _d, _e; ++ if (finishReason.unified === "tool-calls" && !sawToolCallStart) { ++ try { ++ const { value: fallbackResponse } = await (0, import_provider_utils2.postJsonToApi)({ ++ url: requestUrl, ++ headers: requestHeaders, ++ body: fallbackBody, ++ failedResponseHandler, ++ successfulResponseHandler: (0, import_provider_utils2.createJsonResponseHandler)( ++ OpenAICompatibleChatResponseSchema ++ ), ++ abortSignal: options.abortSignal, ++ fetch ++ }); ++ const fallbackChoice = fallbackResponse.choices[0]; ++ const fallbackToolCalls = fallbackChoice.message.tool_calls ?? (fallbackChoice.message.function_call ? [{ ++ id: void 0, ++ function: fallbackChoice.message.function_call ++ }] : []); ++ if (usage == null && fallbackResponse.usage != null) { ++ usage = fallbackResponse.usage; ++ } ++ for (const fallbackToolCall of fallbackToolCalls) { ++ var _f, _g; ++ const toolName = (_f = fallbackToolCall.function) == null ? void 0 : _f.name; ++ if (toolName == null) { ++ continue; ++ } ++ const toolCallId = fallbackToolCall.id ?? (0, import_provider_utils2.generateId)(); ++ const input = (_g = fallbackToolCall.function.arguments) != null ? _g : ""; ++ controller.enqueue({ ++ type: "tool-input-start", ++ id: toolCallId, ++ toolName ++ }); ++ if (input.length > 0) { ++ controller.enqueue({ ++ type: "tool-input-delta", ++ id: toolCallId, ++ delta: input ++ }); ++ } ++ controller.enqueue({ ++ type: "tool-input-end", ++ id: toolCallId ++ }); ++ controller.enqueue({ ++ type: "tool-call", ++ toolCallId, ++ toolName, ++ input ++ }); ++ sawToolCallStart = true; ++ } ++ } catch (error) { ++ finishReason = { ++ unified: "error", ++ raw: "tool_calls_missing_name" ++ }; ++ controller.enqueue({ ++ type: "error", ++ error: error instanceof Error ? error.message : String(error) ++ }); ++ } ++ if (!sawToolCallStart) { ++ finishReason = { ++ unified: "error", ++ raw: "tool_calls_missing_name" ++ }; ++ controller.enqueue({ ++ type: "error", ++ error: "Provider returned tool_calls without a tool name in streaming or non-stream responses." ++ }); ++ } ++ } + if (isActiveReasoning) { + controller.enqueue({ type: "reasoning-end", id: "reasoning-0" }); + } +diff --git a/dist/index.mjs b/dist/index.mjs +index 1c24da7..e64bf48 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -598,6 +598,14 @@ var OpenAICompatibleChatLanguageModel = class { + }); + } + } ++ if (choice.message.function_call != null && choice.message.tool_calls == null) { ++ content.push({ ++ type: "tool-call", ++ toolCallId: generateId(), ++ toolName: choice.message.function_call.name, ++ input: choice.message.function_call.arguments ++ }); ++ } + const providerMetadata = { + [metadataKey]: {}, + ...await ((_f = (_e = this.config.metadataExtractor) == null ? void 0 : _e.extractMetadata) == null ? void 0 : _f.call(_e, { +@@ -631,6 +639,14 @@ var OpenAICompatibleChatLanguageModel = class { + async doStream(options) { + var _a; + const { args, warnings, metadataKey } = await this.getArgs({ ...options }); ++ const requestUrl = this.config.url({ ++ path: "/chat/completions", ++ modelId: this.modelId ++ }); ++ const requestHeaders = combineHeaders(this.config.headers(), options.headers); ++ const fallbackBody = this.transformRequestBody({ ++ ...args ++ }); + const body = this.transformRequestBody({ + ...args, + stream: true, +@@ -638,21 +654,22 @@ var OpenAICompatibleChatLanguageModel = class { + stream_options: this.config.includeUsage ? { include_usage: true } : void 0 + }); + const metadataExtractor = (_a = this.config.metadataExtractor) == null ? void 0 : _a.createStreamExtractor(); ++ const failedResponseHandler = this.failedResponseHandler; ++ const fetch = this.config.fetch; + const { responseHeaders, value: response } = await postJsonToApi({ +- url: this.config.url({ +- path: "/chat/completions", +- modelId: this.modelId +- }), +- headers: combineHeaders(this.config.headers(), options.headers), ++ url: requestUrl, ++ headers: requestHeaders, + body, +- failedResponseHandler: this.failedResponseHandler, ++ failedResponseHandler, + successfulResponseHandler: createEventSourceResponseHandler( + this.chunkSchema + ), + abortSignal: options.abortSignal, +- fetch: this.config.fetch ++ fetch + }); + const toolCalls = []; ++ const pendingToolCallIds = {}; ++ let sawToolCallStart = false; + let finishReason = { + unified: "other", + raw: void 0 +@@ -742,7 +759,12 @@ var OpenAICompatibleChatLanguageModel = class { + delta: delta.content + }); + } +- if (delta.tool_calls != null) { ++ const toolCallDeltas = delta.tool_calls ?? (delta.function_call ? [{ ++ index: 0, ++ id: void 0, ++ function: delta.function_call ++ }] : void 0); ++ if (toolCallDeltas != null) { + if (isActiveReasoning) { + controller.enqueue({ + type: "reasoning-end", +@@ -750,28 +772,23 @@ var OpenAICompatibleChatLanguageModel = class { + }); + isActiveReasoning = false; + } +- for (const toolCallDelta of delta.tool_calls) { ++ for (const toolCallDelta of toolCallDeltas) { + 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 = toolCallDelta.id ?? pendingToolCallIds[index] ?? generateId(); + if (((_d = toolCallDelta.function) == null ? void 0 : _d.name) == null) { +- throw new InvalidResponseDataError({ +- data: toolCallDelta, +- message: `Expected 'function.name' to be a string.` +- }); ++ pendingToolCallIds[index] = toolCallId; ++ continue; + } ++ delete pendingToolCallIds[index]; + controller.enqueue({ + type: "tool-input-start", +- id: toolCallDelta.id, ++ id: toolCallId, + toolName: toolCallDelta.function.name + }); ++ sawToolCallStart = true; + toolCalls[index] = { +- id: toolCallDelta.id, ++ id: toolCallId, + type: "function", + function: { + name: toolCallDelta.function.name, +@@ -847,8 +864,82 @@ var OpenAICompatibleChatLanguageModel = class { + } + } + }, +- flush(controller) { ++ async flush(controller) { + var _a2, _b, _c, _d, _e; ++ if (finishReason.unified === "tool-calls" && !sawToolCallStart) { ++ try { ++ const { value: fallbackResponse } = await postJsonToApi({ ++ url: requestUrl, ++ headers: requestHeaders, ++ body: fallbackBody, ++ failedResponseHandler, ++ successfulResponseHandler: createJsonResponseHandler( ++ OpenAICompatibleChatResponseSchema ++ ), ++ abortSignal: options.abortSignal, ++ fetch ++ }); ++ const fallbackChoice = fallbackResponse.choices[0]; ++ const fallbackToolCalls = fallbackChoice.message.tool_calls ?? (fallbackChoice.message.function_call ? [{ ++ id: void 0, ++ function: fallbackChoice.message.function_call ++ }] : []); ++ if (usage == null && fallbackResponse.usage != null) { ++ usage = fallbackResponse.usage; ++ } ++ for (const fallbackToolCall of fallbackToolCalls) { ++ var _f, _g; ++ const toolName = (_f = fallbackToolCall.function) == null ? void 0 : _f.name; ++ if (toolName == null) { ++ continue; ++ } ++ const toolCallId = fallbackToolCall.id ?? generateId(); ++ const input = (_g = fallbackToolCall.function.arguments) != null ? _g : ""; ++ controller.enqueue({ ++ type: "tool-input-start", ++ id: toolCallId, ++ toolName ++ }); ++ if (input.length > 0) { ++ controller.enqueue({ ++ type: "tool-input-delta", ++ id: toolCallId, ++ delta: input ++ }); ++ } ++ controller.enqueue({ ++ type: "tool-input-end", ++ id: toolCallId ++ }); ++ controller.enqueue({ ++ type: "tool-call", ++ toolCallId, ++ toolName, ++ input ++ }); ++ sawToolCallStart = true; ++ } ++ } catch (error) { ++ finishReason = { ++ unified: "error", ++ raw: "tool_calls_missing_name" ++ }; ++ controller.enqueue({ ++ type: "error", ++ error: error instanceof Error ? error.message : String(error) ++ }); ++ } ++ if (!sawToolCallStart) { ++ finishReason = { ++ unified: "error", ++ raw: "tool_calls_missing_name" ++ }; ++ controller.enqueue({ ++ type: "error", ++ error: "Provider returned tool_calls without a tool name in streaming or non-stream responses." ++ }); ++ } ++ } + if (isActiveReasoning) { + controller.enqueue({ type: "reasoning-end", id: "reasoning-0" }); + }