From 4ee01706aa41b720b7fc671270d875b039648b99 Mon Sep 17 00:00:00 2001 From: wsyh4567 Date: Sat, 28 Mar 2026 11:40:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Responses=20API=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E4=BA=8B=E4=BB=B6=E6=A0=BC=E5=BC=8F=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=AF=BC=E8=87=B4=20Codex=20CLI=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=A4=9A=E8=BD=AE=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 6 个问题: 1. requestId 未传入 convertStreamChunk, 每个 chunk 生成新 UUID, 导致 state 不共享, response.completed 时 fullText 为空 2. TOTAL_CONTEXT_TOKENS=1000000 但 Claude 实际上下文只有 200K, token 数虚报 5 倍触发异常压缩 3. 默认模型名硬编码为 gpt-4.1-2025-04-14 4. 工具调用参数事件类型用了 response.custom_tool_call_input.delta, 应为 response.function_call_arguments.delta 5. response.completed 的 output 只有空 message, 缺少 function_call 项, Codex 收不到工具调用结果无法进入多轮 6. 缺少 response.function_call_arguments.done 和工具专属 response.output_item.done 事件 --- src/converters/strategies/ClaudeConverter.js | 89 ++++++++---------- src/providers/claude/claude-kiro.js | 3 +- .../openai/openai-responses-core.mjs | 93 ++++++++++++++----- 3 files changed, 115 insertions(+), 70 deletions(-) diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index b9bc5fc..a27d067 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -28,7 +28,14 @@ import { generateOutputTextDone, generateContentPartDone, generateOutputItemDone, - generateResponseCompleted + generateResponseCompleted, + generateOutputTextDelta, + streamStateManager, + startToolCall, + finishToolCall, + generateFunctionCallArgsDelta, + generateFunctionCallArgsDone, + generateFunctionCallOutputItemDone } from '../../providers/openai/openai-responses-core.mjs'; /** @@ -81,14 +88,14 @@ export class ClaudeConverter extends BaseConverter { /** * 转换流式响应块 */ - convertStreamChunk(chunk, targetProtocol, model) { + convertStreamChunk(chunk, targetProtocol, model, requestId) { switch (targetProtocol) { case MODEL_PROTOCOL_PREFIX.OPENAI: return this.toOpenAIStreamChunk(chunk, model); case MODEL_PROTOCOL_PREFIX.GEMINI: return this.toGeminiStreamChunk(chunk, model); case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES: - return this.toOpenAIResponsesStreamChunk(chunk, model); + return this.toOpenAIResponsesStreamChunk(chunk, model, requestId); case MODEL_PROTOCOL_PREFIX.CODEX: return this.toCodexStreamChunk(chunk, model); default: @@ -1694,9 +1701,11 @@ export class ClaudeConverter extends BaseConverter { // 对于 tool_use 类型,添加工具调用项 if (contentBlock && contentBlock.type === 'tool_use') { + startToolCall(responseId, contentBlock.id, contentBlock.name); events.push({ item: { id: contentBlock.id, + call_id: contentBlock.id, type: "function_call", name: contentBlock.name, arguments: "", @@ -1715,13 +1724,7 @@ export class ClaudeConverter extends BaseConverter { // 处理文本增量 if (delta && delta.type === 'text_delta') { - events.push({ - delta: delta.text || "", - item_id: `msg_${uuidv4().replace(/-/g, '')}`, - output_index: claudeChunk.index || 0, - sequence_number: 3, - type: "response.output_text.delta" - }); + events.push(generateOutputTextDelta(responseId, delta.text || "")); } // 处理推理内容增量 else if (delta && delta.type === 'thinking_delta') { @@ -1735,61 +1738,51 @@ export class ClaudeConverter extends BaseConverter { } // 处理工具调用参数增量 else if (delta && delta.type === 'input_json_delta') { - events.push({ - delta: delta.partial_json || "", - item_id: `call_${uuidv4().replace(/-/g, '')}`, - output_index: claudeChunk.index || 0, - sequence_number: 3, - type: "response.custom_tool_call_input.delta" - }); + const state = streamStateManager.getOrCreateState(responseId); + const itemId = state.currentToolCall ? state.currentToolCall.id : 'unknown'; + events.push(generateFunctionCallArgsDelta( + responseId, itemId, claudeChunk.index || 0, delta.partial_json || "" + )); } } // content_block_stop 事件 if (claudeChunk.type === 'content_block_stop') { - events.push({ - item_id: `msg_${uuidv4().replace(/-/g, '')}`, - output_index: claudeChunk.index || 0, - sequence_number: 4, - type: "response.output_item.done" - }); + const state = streamStateManager.getOrCreateState(responseId); + if (state.currentToolCall) { + const itemId = state.currentToolCall.id; + const outputIdx = claudeChunk.index || 0; + events.push(generateFunctionCallArgsDone(responseId, itemId, outputIdx)); + const finished = finishToolCall(responseId); + if (finished) { + events.push(generateFunctionCallOutputItemDone(responseId, finished, outputIdx)); + } + } } - // message_delta 事件 - 流结束 + // message_delta 事件 - 保存 usage 供 message_stop 使用 if (claudeChunk.type === 'message_delta') { - // events.push( - // generateOutputTextDone(responseId), - // generateContentPartDone(responseId), - // generateOutputItemDone(responseId), - // generateResponseCompleted(responseId) - // ); - - // 如果有 usage 信息,更新最后一个事件 - if (claudeChunk.usage && events.length > 0) { - const lastEvent = events[events.length - 1]; - if (lastEvent.response) { - lastEvent.response.usage = { - input_tokens: claudeChunk.usage.input_tokens || 0, - input_tokens_details: { - cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0 - }, - output_tokens: claudeChunk.usage.output_tokens || 0, - output_tokens_details: { - reasoning_tokens: 0 - }, - total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0) - }; - } + if (claudeChunk.usage) { + const state = streamStateManager.getOrCreateState(responseId); + state.savedUsage = { + input_tokens: claudeChunk.usage.input_tokens || 0, + input_tokens_details: { cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0 }, + output_tokens: claudeChunk.usage.output_tokens || 0, + output_tokens_details: { reasoning_tokens: 0 }, + total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0) + }; } } // message_stop 事件 if (claudeChunk.type === 'message_stop') { + const state = streamStateManager.getOrCreateState(responseId); + const savedUsage = state.savedUsage || null; events.push( generateOutputTextDone(responseId), generateContentPartDone(responseId), generateOutputItemDone(responseId), - generateResponseCompleted(responseId) + generateResponseCompleted(responseId, savedUsage) ); } diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index 70683af..303533d 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -44,7 +44,7 @@ const KIRO_CONSTANTS = { AUTH_METHOD_SOCIAL: 'social', CHAT_TRIGGER_TYPE_MANUAL: 'MANUAL', ORIGIN_AI_EDITOR: 'AI_EDITOR', - TOTAL_CONTEXT_TOKENS: 1000000, // Kiro now supports 1M context window + TOTAL_CONTEXT_TOKENS: 200000, // Claude Sonnet 4.5 actual context is 200K }; // 从 provider-models.js 获取支持的模型列表 @@ -1597,6 +1597,7 @@ async saveCredentialsToFile(filePath, newData) { return this.callApi(method, model, body, isRetry, retryCount + 1); } + if (error.response && error.response.data) { logger.error('[Kiro] 400 Response body:', typeof error.response.data === 'string' ? error.response.data.substring(0, 500) : JSON.stringify(error.response.data).substring(0, 500)); } logger.error(`[Kiro] API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); throw error; } diff --git a/src/providers/openai/openai-responses-core.mjs b/src/providers/openai/openai-responses-core.mjs index 2069785..4cefd3c 100644 --- a/src/providers/openai/openai-responses-core.mjs +++ b/src/providers/openai/openai-responses-core.mjs @@ -16,7 +16,9 @@ class StreamState { sequenceNumber: 0, model: null, status: 'in_progress', - startTime: Math.floor(Date.now() / 1000) + startTime: Math.floor(Date.now() / 1000), + toolCalls: [], + currentToolCall: null }); } return this.states.get(requestId); @@ -73,7 +75,7 @@ function generateResponseCreated(requestId, model) { incomplete_details: null, instructions: '', max_output_tokens: null, - model: state.model || 'gpt-4.1-2025-04-14', + model: state.model || 'claude-sonnet-4-6', output: [], parallel_tool_calls: true, previous_response_id: null, @@ -110,7 +112,7 @@ function generateResponseInProgress(requestId) { incomplete_details: null, instructions: '', max_output_tokens: null, - model: state.model || 'gpt-4.1-2025-04-14', + model: state.model || 'claude-sonnet-4-6', output: [], parallel_tool_calls: true, previous_response_id: null, @@ -269,25 +271,32 @@ function generateResponseCompleted(requestId, usage) { max_output_tokens: null, max_tool_calls: null, metadata: {}, - model: state.model || 'gpt-4.1-2025-04-14', + model: state.model || 'claude-sonnet-4-6', object: 'response', - output: [ - { - id: state.msgId, - summary: [], - type: 'message', - role: 'assistant', - status: 'completed', - content: [ - { - type: 'output_text', - text: state.fullText, - annotations: [], - logprobs: [] - } - ] + output: (() => { + const items = []; + if (state.fullText) { + items.push({ + id: state.msgId, summary: [], type: 'message', role: 'assistant', status: 'completed', + content: [{ type: 'output_text', text: state.fullText, annotations: [], logprobs: [] }] + }); } - ], + if (state.toolCalls && state.toolCalls.length > 0) { + for (const tc of state.toolCalls) { + items.push({ + id: tc.id, call_id: tc.call_id || tc.id, type: 'function_call', + name: tc.name, arguments: tc.arguments || '{}', status: 'completed' + }); + } + } + if (items.length === 0) { + items.push({ + id: state.msgId, summary: [], type: 'message', role: 'assistant', status: 'completed', + content: [{ type: 'output_text', text: '', annotations: [], logprobs: [] }] + }); + } + return items; + })(), parallel_tool_calls: true, previous_response_id: null, prompt_cache_key: null, @@ -322,8 +331,50 @@ function generateResponseCompleted(requestId, usage) { }; } + +function startToolCall(requestId, toolCallId, name) { + const state = streamStateManager.getOrCreateState(requestId); + state.currentToolCall = { id: toolCallId, call_id: toolCallId, name: name, arguments: '' }; +} + +function appendToolCallArgs(requestId, delta) { + const state = streamStateManager.getOrCreateState(requestId); + if (state.currentToolCall) state.currentToolCall.arguments += delta; +} + +function finishToolCall(requestId) { + const state = streamStateManager.getOrCreateState(requestId); + if (state.currentToolCall) { + state.toolCalls.push({...state.currentToolCall}); + const finished = state.currentToolCall; + state.currentToolCall = null; + return finished; + } + return null; +} + +function generateFunctionCallArgsDelta(requestId, itemId, outputIndex, delta) { + appendToolCallArgs(requestId, delta); + return { type: 'response.function_call_arguments.delta', item_id: itemId, output_index: outputIndex, delta: delta }; +} + +function generateFunctionCallArgsDone(requestId, itemId, outputIndex) { + const state = streamStateManager.getOrCreateState(requestId); + const args = state.currentToolCall ? state.currentToolCall.arguments : '{}'; + return { type: 'response.function_call_arguments.done', item_id: itemId, output_index: outputIndex, arguments: args }; +} + +function generateFunctionCallOutputItemDone(requestId, toolCall, outputIndex) { + return { + type: 'response.output_item.done', output_index: outputIndex, + item: { id: toolCall.id, call_id: toolCall.call_id || toolCall.id, type: 'function_call', + name: toolCall.name, arguments: toolCall.arguments || '{}', status: 'completed' } + }; +} + // 导出流式状态管理器以供外部使用 export { streamStateManager, generateResponseCreated, generateResponseInProgress, generateOutputItemAdded, generateContentPartAdded, generateOutputTextDelta, generateOutputTextDone, generateContentPartDone, generateOutputItemDone, - generateResponseCompleted }; \ No newline at end of file + generateResponseCompleted, startToolCall, appendToolCallArgs, finishToolCall, + generateFunctionCallArgsDelta, generateFunctionCallArgsDone, generateFunctionCallOutputItemDone }; \ No newline at end of file