import { FORMATS } from "../formats.js"; import { fromOpenAIFinish } from "./finishReason.js"; // Shared OpenAI-Chat → Claude-Messages shape conversion. // // Lives in `concerns/` (not chatCore/) because BOTH the non-streaming handler // and the forced-SSE→JSON handler need it, and importing it from // nonStreamingHandler.js into sseToJsonHandler.js would create a circular // import (nonStreamingHandler already imports parseSSEToOpenAIResponse from // sseToJsonHandler). function parseToolArguments(value) { if (!value) return {}; if (typeof value === "object") return value; try { return JSON.parse(value); } catch { return {}; } } function openAICompletionToClaudeMessage(responseBody) { if (!responseBody?.choices?.[0]) return responseBody; const choice = responseBody.choices[0]; const message = choice.message || {}; const content = []; const reasoning = message.reasoning_content || message.provider_specific_fields?.reasoning_content || ""; if (reasoning) { content.push({ type: "thinking", thinking: reasoning }); } if (typeof message.content === "string" && message.content.length > 0) { content.push({ type: "text", text: message.content }); } for (const toolCall of message.tool_calls || []) { const fn = toolCall.function || {}; content.push({ type: "tool_use", id: toolCall.id || `toolu_${Date.now()}_${content.length}`, name: fn.name || toolCall.name || "", input: parseToolArguments(fn.arguments || toolCall.arguments), }); } if (content.length === 0) content.push({ type: "text", text: "" }); const usage = responseBody.usage || {}; return { id: String(responseBody.id || `msg_${Date.now()}`).replace(/^chatcmpl-/, ""), type: "message", role: "assistant", model: responseBody.model || "unknown", content, stop_reason: fromOpenAIFinish(choice.finish_reason, FORMATS.CLAUDE), stop_sequence: null, usage: { input_tokens: usage.prompt_tokens || usage.input_tokens || 0, output_tokens: usage.completion_tokens || usage.output_tokens || 0, }, }; } /** * Convert an OpenAI Chat Completions body into the Claude Messages shape when * the client speaks Anthropic (sourceFormat=CLAUDE) but the provider replied * OpenAI JSON. Shape-aware guard: Claude-shaped bodies pass through untouched. */ export function toClaudeMessageShape(responseBody) { if (!responseBody || responseBody.type === "message") return responseBody; if (responseBody?.choices) return openAICompletionToClaudeMessage(responseBody); return responseBody; } /** Export the raw converter for tests that want it directly. */ export { openAICompletionToClaudeMessage };