import { register } from "../index.js"; import { FORMATS } from "../formats.js"; // Prefix for Claude OAuth tool names (must match request translator) const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; // Helper: stop thinking block if started function stopThinkingBlock(state, results) { if (!state.thinkingBlockStarted) return; results.push({ type: "content_block_stop", index: state.thinkingBlockIndex }); state.thinkingBlockStarted = false; } // Helper: stop text block if started function stopTextBlock(state, results) { if (!state.textBlockStarted || state.textBlockClosed) return; state.textBlockClosed = true; results.push({ type: "content_block_stop", index: state.textBlockIndex }); state.textBlockStarted = false; } // Convert OpenAI stream chunk to Claude format function openaiToClaudeResponse(chunk, state) { if (!chunk || !chunk.choices?.[0]) return null; const results = []; const choice = chunk.choices[0]; const delta = choice.delta; // First chunk - ALWAYS send message_start first if (!state.messageStartSent) { state.messageStartSent = true; state.messageId = chunk.id?.replace("chatcmpl-", "") || `msg_${Date.now()}`; if (!state.messageId || state.messageId === "chat" || state.messageId.length < 8) { state.messageId = chunk.extend_fields?.requestId || chunk.extend_fields?.traceId || `msg_${Date.now()}`; } state.model = chunk.model || "unknown"; state.nextBlockIndex = 0; results.push({ type: "message_start", message: { id: state.messageId, type: "message", role: "assistant", model: state.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } }); } // Handle reasoning_content (thinking) - GLM, DeepSeek, etc. const reasoningContent = delta?.reasoning_content || delta?.reasoning; if (reasoningContent) { stopTextBlock(state, results); if (!state.thinkingBlockStarted) { state.thinkingBlockIndex = state.nextBlockIndex++; state.thinkingBlockStarted = true; results.push({ type: "content_block_start", index: state.thinkingBlockIndex, content_block: { type: "thinking", thinking: "" } }); } results.push({ type: "content_block_delta", index: state.thinkingBlockIndex, delta: { type: "thinking_delta", thinking: reasoningContent } }); } // Handle regular content if (delta?.content) { stopThinkingBlock(state, results); if (!state.textBlockStarted) { state.textBlockIndex = state.nextBlockIndex++; state.textBlockStarted = true; state.textBlockClosed = false; results.push({ type: "content_block_start", index: state.textBlockIndex, content_block: { type: "text", text: "" } }); } results.push({ type: "content_block_delta", index: state.textBlockIndex, delta: { type: "text_delta", text: delta.content } }); } // Tool calls - accumulate arguments instead of emitting immediately if (delta?.tool_calls) { for (const tc of delta.tool_calls) { const idx = tc.index ?? 0; if (tc.id) { stopThinkingBlock(state, results); stopTextBlock(state, results); const toolBlockIndex = state.nextBlockIndex++; // Strip prefix from tool name for response let toolName = tc.function?.name || ""; if (toolName.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) { toolName = toolName.slice(CLAUDE_OAUTH_TOOL_PREFIX.length); } // Initialize accumulator for this tool state.toolCalls.set(idx, { id: tc.id, name: toolName, blockIndex: toolBlockIndex, arguments: "", // Accumulate arguments here startEmitted: false // Track if content_block_start sent }); } // Accumulate arguments instead of emitting immediately if (tc.function?.arguments) { const toolInfo = state.toolCalls.get(idx); if (toolInfo) { toolInfo.arguments += tc.function.arguments; } } } } // Finish - emit all accumulated tools in correct order if (choice.finish_reason) { stopThinkingBlock(state, results); stopTextBlock(state, results); // STEP 1: Emit all content_block_start for tools (like CLIProxyAPIPlus) const sortedTools = Array.from(state.toolCalls.entries()).sort((a, b) => a[0] - b[0]); for (const [, toolInfo] of sortedTools) { if (!toolInfo.startEmitted) { results.push({ type: "content_block_start", index: toolInfo.blockIndex, content_block: { type: "tool_use", id: toolInfo.id, name: toolInfo.name, input: {} } }); toolInfo.startEmitted = true; } } // STEP 2: Emit input_json_delta + content_block_stop for each tool for (const [, toolInfo] of sortedTools) { if (toolInfo.arguments) { results.push({ type: "content_block_delta", index: toolInfo.blockIndex, delta: { type: "input_json_delta", partial_json: toolInfo.arguments } }); } results.push({ type: "content_block_stop", index: toolInfo.blockIndex }); } results.push({ type: "message_delta", delta: { stop_reason: convertFinishReason(choice.finish_reason) }, usage: { output_tokens: 0 } }); results.push({ type: "message_stop" }); } return results.length > 0 ? results : null; } // Convert OpenAI finish_reason to Claude stop_reason function convertFinishReason(reason) { switch (reason) { case "stop": return "end_turn"; case "length": return "max_tokens"; case "tool_calls": return "tool_use"; default: return "end_turn"; } } // Register register(FORMATS.OPENAI, FORMATS.CLAUDE, null, openaiToClaudeResponse);