import { register } from "../index.js"; import { FORMATS } from "../formats.js"; // Create OpenAI chunk helper function createChunk(state, delta, finishReason = null) { return { id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: state.model, choices: [{ index: 0, delta, finish_reason: finishReason }] }; } // Convert Claude stream chunk to OpenAI format function claudeToOpenAIResponse(chunk, state) { if (!chunk) return null; const results = []; const event = chunk.type; switch (event) { case "message_start": { state.messageId = chunk.message?.id || `msg_${Date.now()}`; state.model = chunk.message?.model; state.toolCallIndex = 0; // Reset tool call counter for OpenAI format console.log("🔍 ----------- toolCallIndex", state.toolCallIndex); results.push(createChunk(state, { role: "assistant" })); break; } case "content_block_start": { const block = chunk.content_block; if (block?.type === "text") { state.textBlockStarted = true; } else if (block?.type === "thinking") { // console.log("🧠 Thinking block started"); state.inThinkingBlock = true; state.currentBlockIndex = chunk.index; results.push(createChunk(state, { content: "" })); } else if (block?.type === "tool_use") { // OpenAI format: tool_calls index must be independent and start from 0 const toolCallIndex = state.toolCallIndex++; const toolCall = { index: toolCallIndex, id: block.id, type: "function", function: { name: block.name, arguments: "" } }; // Map Claude content_block index to OpenAI tool_call index state.toolCalls.set(chunk.index, toolCall); results.push(createChunk(state, { tool_calls: [toolCall] })); } break; } case "content_block_delta": { const delta = chunk.delta; if (delta?.type === "text_delta" && delta.text) { results.push(createChunk(state, { content: delta.text })); } else if (delta?.type === "thinking_delta" && delta.thinking) { // Stream thinking content results.push(createChunk(state, { content: delta.thinking })); } else if (delta?.type === "input_json_delta" && delta.partial_json) { const toolCall = state.toolCalls.get(chunk.index); if (toolCall) { toolCall.function.arguments += delta.partial_json; // Include both index and id for better client compatibility results.push(createChunk(state, { tool_calls: [{ index: toolCall.index, id: toolCall.id, function: { arguments: delta.partial_json } }] })); } } break; } case "content_block_stop": { if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) { // console.log("✅ Thinking block ended"); results.push(createChunk(state, { content: "" })); state.inThinkingBlock = false; } state.textBlockStarted = false; state.thinkingBlockStarted = false; break; } case "message_delta": { if (chunk.delta?.stop_reason) { state.finishReason = convertStopReason(chunk.delta.stop_reason); // Send the final chunk with finish_reason immediately results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: state.model, choices: [{ index: 0, delta: {}, finish_reason: state.finishReason }] }); state.finishReasonSent = true; } // Usage is now extracted in stream.js extractUsage() break; } case "message_stop": { // CLIProxyAPI and OpenAI standard: message_stop should send the final chunk with finish_reason // This ensures proper signaling to the client that the response is complete // Only send a chunk if we haven't already sent the finish_reason in message_delta // In some cases, finish_reason might not have been sent yet if (!state.finishReasonSent) { const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop"); results.push({ id: `chatcmpl-${state.messageId}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: state.model, choices: [{ index: 0, delta: {}, finish_reason: finishReason }], ...(state.usage && { usage: { prompt_tokens: state.usage.input_tokens || 0, completion_tokens: state.usage.output_tokens || 0, total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0) } }) }); state.finishReasonSent = true; } break; } } return results.length > 0 ? results : null; } // 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) { // Stop text block before thinking stopTextBlock(state, results); // Start thinking block if needed if (!state.thinkingBlockStarted) { state.thinkingBlockIndex = state.nextBlockIndex++; state.thinkingBlockStarted = true; results.push({ type: "content_block_start", index: state.thinkingBlockIndex, content_block: { type: "thinking", thinking: "" } }); } // Send thinking delta results.push({ type: "content_block_delta", index: state.thinkingBlockIndex, delta: { type: "thinking_delta", thinking: reasoningContent } }); } // Handle regular content if (delta?.content) { // Stop thinking block before text stopThinkingBlock(state, results); // Start text block if needed 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: "" } }); } // Send text delta results.push({ type: "content_block_delta", index: state.textBlockIndex, delta: { type: "text_delta", text: delta.content } }); } // Tool calls if (delta?.tool_calls) { for (const tc of delta.tool_calls) { const idx = tc.index ?? 0; if (tc.id) { // Stop thinking and text blocks before tool use stopThinkingBlock(state, results); stopTextBlock(state, results); // New tool call const toolBlockIndex = state.nextBlockIndex++; state.toolCalls.set(idx, { id: tc.id, name: tc.function?.name || "", blockIndex: toolBlockIndex }); results.push({ type: "content_block_start", index: toolBlockIndex, content_block: { type: "tool_use", id: tc.id, name: tc.function?.name || "", input: {} } }); } if (tc.function?.arguments) { const toolInfo = state.toolCalls.get(idx); if (toolInfo) { results.push({ type: "content_block_delta", index: toolInfo.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } }); } } } } // Finish if (choice.finish_reason) { // Stop all open blocks stopThinkingBlock(state, results); stopTextBlock(state, results); // Close tool call blocks for (const [, toolInfo] of state.toolCalls) { 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 Claude stop_reason to OpenAI finish_reason function convertStopReason(reason) { switch (reason) { case "end_turn": return "stop"; case "max_tokens": return "length"; case "tool_use": return "tool_calls"; case "stop_sequence": return "stop"; default: return "stop"; } } // 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.CLAUDE, FORMATS.OPENAI, null, claudeToOpenAIResponse); register(FORMATS.OPENAI, FORMATS.CLAUDE, null, openaiToClaudeResponse);