From e443d9d891ecc907c9c0fe8d1e80cfca64b3cfc9 Mon Sep 17 00:00:00 2001 From: amarcin Date: Thu, 9 Apr 2026 17:40:22 -0700 Subject: [PATCH] fix: /v1/responses endpoint drops tool calls (function_call) in non-streaming mode Also fixes streaming tool call event type and response status. Non-streaming: - ClaudeConverter.toOpenAIResponsesResponse now handles tool_use content blocks, emitting function_call output items - OpenAIConverter.toOpenAIResponsesResponse now handles message.tool_calls, emitting function_call output items - Both set status to 'requires_action' when tool calls are present Streaming: - OpenAIConverter uses response.function_call_arguments.delta instead of response.custom_tool_call_input.delta - generateResponseCompleted sets status to 'requires_action' when tool calls are tracked in stream state Fixes #471 --- src/converters/strategies/ClaudeConverter.js | 42 ++++++++++++++----- src/converters/strategies/OpenAIConverter.js | 22 +++++++++- .../openai/openai-responses-core.mjs | 2 +- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index 9ebb2b6..fd4e8f2 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -1648,22 +1648,42 @@ export class ClaudeConverter extends BaseConverter { * Claude响应 -> OpenAI Responses响应 */ toOpenAIResponsesResponse(claudeResponse, model) { - const content = this.processClaudeResponseContent(claudeResponse.content); - const textContent = typeof content === 'string' ? content : JSON.stringify(content); + const output = []; + const messageContent = []; + let hasToolUse = false; - let output = []; - output.push({ + // Process Claude content blocks, handling both text and tool_use + if (Array.isArray(claudeResponse.content)) { + for (const block of claudeResponse.content) { + if (block.type === 'text' && block.text) { + messageContent.push({ + annotations: [], + logprobs: [], + text: block.text, + type: "output_text" + }); + } else if (block.type === 'tool_use') { + hasToolUse = true; + output.push({ + type: "function_call", + id: block.id || `fc_${uuidv4().replace(/-/g, '')}`, + call_id: block.id || `call_${uuidv4().replace(/-/g, '')}`, + name: block.name, + arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}), + status: "completed" + }); + } + } + } + + // Always include the message output item (even if content is empty) + output.unshift({ type: "message", id: `msg_${uuidv4().replace(/-/g, '')}`, summary: [], role: "assistant", status: "completed", - content: [{ - annotations: [], - logprobs: [], - text: textContent, - type: "output_text" - }] + content: messageContent }); return { @@ -1684,7 +1704,7 @@ export class ClaudeConverter extends BaseConverter { reasoning: {}, safety_identifier: "user-" + uuidv4().replace(/-/g, ''), service_tier: "default", - status: "completed", + status: hasToolUse ? "requires_action" : "completed", store: false, temperature: 1, text: { diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js index 8eab80b..b77e9c7 100644 --- a/src/converters/strategies/OpenAIConverter.js +++ b/src/converters/strategies/OpenAIConverter.js @@ -1642,11 +1642,29 @@ export class OpenAIConverter extends BaseConverter { content: messageContent }); + // Handle tool calls (function_call output items) + if (message.tool_calls && message.tool_calls.length > 0) { + for (const tc of message.tool_calls) { + if (tc.type === 'function' && tc.function) { + output.push({ + type: 'function_call', + id: tc.id || `fc_${Date.now()}`, + call_id: tc.id || `call_${Date.now()}`, + name: tc.function.name, + arguments: tc.function.arguments || '{}', + status: 'completed' + }); + } + } + } + + const hasToolCalls = message.tool_calls && message.tool_calls.length > 0; + return { id: openaiResponse.id || `resp_${Date.now()}`, object: 'response', created_at: openaiResponse.created || Math.floor(Date.now() / 1000), - status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress', + status: hasToolCalls ? 'requires_action' : (choice.finish_reason === 'stop' ? 'completed' : 'in_progress'), model: model || openaiResponse.model || 'unknown', output: output, usage: openaiResponse.usage ? { @@ -1736,7 +1754,7 @@ export class OpenAIConverter extends BaseConverter { item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`, output_index: outputIndex, sequence_number: 3, - type: "response.custom_tool_call_input.delta" + type: "response.function_call_arguments.delta" }); } } diff --git a/src/providers/openai/openai-responses-core.mjs b/src/providers/openai/openai-responses-core.mjs index 4cefd3c..daee48a 100644 --- a/src/providers/openai/openai-responses-core.mjs +++ b/src/providers/openai/openai-responses-core.mjs @@ -304,7 +304,7 @@ function generateResponseCompleted(requestId, usage) { }, safety_identifier: `user-${uuidv4().replace(/-/g, '')}`, // 随机值 service_tier: "default", - status: "completed", + status: (state.toolCalls && state.toolCalls.length > 0) ? "requires_action" : "completed", store: false, temperature: 1, text: {