/* Anthropic Messages API <-> OpenAI Chat Completions */ import type { ChatCompletionRequest, ChatMessage } from "./responses"; export interface AnthropicMessageRequest { model: string; max_tokens: number; messages: AnthropicMessage[]; system?: string | AnthropicContentBlock[]; stream?: boolean; temperature?: number; top_p?: number; tools?: AnthropicTool[]; tool_choice?: { type: string; name?: string } | string; } interface AnthropicMessage { role: "user" | "assistant"; content: string | AnthropicContentBlock[]; } interface AnthropicContentBlock { type: string; text?: string; source?: { type: string; media_type?: string; data?: string; url?: string }; id?: string; name?: string; input?: unknown; tool_use_id?: string; content?: string | AnthropicContentBlock[]; } interface AnthropicTool { name: string; description?: string; input_schema?: unknown; } export interface OpenAIChatResponse { id: string; model: string; choices: Array<{ message: { role: string; content: string | null; tool_calls?: Array<{ id: string; type: string; function: { name: string; arguments: string }; }>; }; finish_reason: string | null; }>; usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; } function extractSystem(system: AnthropicMessageRequest["system"]): string | undefined { if (!system) return undefined; if (typeof system === "string") return system; return system .filter((b) => b.type === "text" && b.text) .map((b) => b.text!) .join(""); } function anthropicImageToOpenAI(block: AnthropicContentBlock): unknown | null { if (block.type !== "image") return null; const src = block.source; if (!src) return null; if (src.type === "base64" && src.media_type && src.data) { return { type: "image_url", image_url: { url: `data:${src.media_type};base64,${src.data}` }, }; } if (src.type === "url" && src.url) { return { type: "image_url", image_url: { url: src.url } }; } return null; } function anthropicContentToOpenAI( content: string | AnthropicContentBlock[], role: string, ): string | unknown[] { if (typeof content === "string") return content; const parts: unknown[] = []; for (const block of content) { if (block.type === "text" && block.text) { parts.push({ type: "text", text: block.text }); } else if (block.type === "image") { const img = anthropicImageToOpenAI(block); if (img) parts.push(img); } } return parts.length === 1 && parts[0] && (parts[0] as { type: string }).type === "text" ? (parts[0] as { text: string }).text : parts; } function anthropicToolsToOpenAI(tools?: AnthropicTool[]): ChatCompletionRequest["tools"] { if (!tools?.length) return undefined; return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description ?? "", parameters: t.input_schema ?? { type: "object", properties: {} }, }, })); } function anthropicToolChoiceToOpenAI( toolChoice?: AnthropicMessageRequest["tool_choice"], ): unknown { if (!toolChoice) return undefined; if (typeof toolChoice === "string") return toolChoice; if (toolChoice.type === "tool" && toolChoice.name) { return { type: "function", function: { name: toolChoice.name } }; } if (toolChoice.type === "any") return "required"; return toolChoice.type === "auto" ? "auto" : toolChoice; } export function anthropicToOpenAI(req: AnthropicMessageRequest): ChatCompletionRequest { let model = req.model; if (model.startsWith("github_copilot/")) { model = model.slice("github_copilot/".length); } const messages: ChatMessage[] = []; const systemText = extractSystem(req.system); if (systemText) { messages.push({ role: "system", content: systemText }); } for (const msg of req.messages) { if (typeof msg.content === "string") { messages.push({ role: msg.role, content: msg.content }); continue; } const toolResults = msg.content.filter((b) => b.type === "tool_result"); const toolUses = msg.content.filter((b) => b.type === "tool_use"); const other = msg.content.filter((b) => b.type !== "tool_result" && b.type !== "tool_use"); if (msg.role === "assistant" && toolUses.length > 0) { const text = other .filter((b) => b.type === "text" && b.text) .map((b) => b.text) .join(""); messages.push({ role: "assistant", content: text || null, tool_calls: toolUses.map((tu) => ({ id: tu.id ?? `toolu_${crypto.randomUUID().slice(0, 12)}`, type: "function", function: { name: tu.name ?? "", arguments: JSON.stringify(tu.input ?? {}), }, })), }); continue; } if (msg.role === "user" && toolResults.length > 0) { if (other.length > 0) { messages.push({ role: "user", content: anthropicContentToOpenAI(other, "user"), }); } for (const tr of toolResults) { const resultContent = typeof tr.content === "string" ? tr.content : Array.isArray(tr.content) ? tr.content .filter((b) => b.type === "text" && b.text) .map((b) => b.text) .join("") : JSON.stringify(tr.content ?? ""); messages.push({ role: "tool", tool_call_id: tr.tool_use_id ?? "", content: resultContent, }); } continue; } messages.push({ role: msg.role, content: anthropicContentToOpenAI(msg.content, msg.role), }); } return { model, messages, max_tokens: req.max_tokens, stream: req.stream ?? false, temperature: req.temperature, top_p: req.top_p, tools: anthropicToolsToOpenAI(req.tools), tool_choice: anthropicToolChoiceToOpenAI(req.tool_choice), }; } export function openAIToAnthropic( openai: OpenAIChatResponse, model: string, ): Record { const message = openai.choices[0]?.message; const usage = openai.usage; const content: AnthropicContentBlock[] = []; if (message?.content) { content.push({ type: "text", text: message.content }); } if (message?.tool_calls?.length) { for (const tc of message.tool_calls) { let input: unknown = {}; try { input = JSON.parse(tc.function.arguments || "{}"); } catch { input = { raw: tc.function.arguments }; } content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input, }); } } const finishReason = openai.choices[0]?.finish_reason; const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn"; return { id: `msg_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`, type: "message", role: "assistant", model, content: content.length > 0 ? content : [{ type: "text", text: "" }], stop_reason: stopReason, stop_sequence: null, usage: { input_tokens: usage?.prompt_tokens ?? 0, output_tokens: usage?.completion_tokens ?? 0, }, }; } function mapStopReason(reason: string | null | undefined): string { if (reason === "length") return "max_tokens"; if (reason === "tool_calls") return "tool_use"; if (reason === "stop") return "end_turn"; return "end_turn"; } function anthropicError(message: string, type = "api_error"): Response { return new Response( JSON.stringify({ type: "error", error: { type, message }, }), { status: 502, headers: { "content-type": "application/json" } }, ); } function sseEvent(event: string, data: unknown): string { return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; } export function transformOpenAIStreamToAnthropic( openaiBody: ReadableStream, model: string, ): ReadableStream { const encoder = new TextEncoder(); const decoder = new TextDecoder(); let buffer = ""; const messageId = `msg_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`; let blockIndex = 0; let started = false; let outputTokens = 0; let toolBlocksStarted = 0; const startMessage = (controller: ReadableStreamDefaultController) => { if (started) return; started = true; controller.enqueue( encoder.encode( sseEvent("message_start", { type: "message_start", message: { id: messageId, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 }, }, }), ), ); }; const startTextBlock = (controller: ReadableStreamDefaultController) => { controller.enqueue( encoder.encode( sseEvent("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "text", text: "" }, }), ), ); }; return new ReadableStream({ async start(controller) { const reader = openaiBody.getReader(); const pendingTools = new Map(); try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith("data: ")) continue; const payload = trimmed.slice(6); if (payload === "[DONE]") continue; let chunk: { choices?: Array<{ delta?: { content?: string; role?: string; tool_calls?: Array<{ index?: number; id?: string; function?: { name?: string; arguments?: string }; }>; }; finish_reason?: string | null; }>; usage?: { completion_tokens?: number }; }; try { chunk = JSON.parse(payload); } catch { continue; } if (chunk.usage?.completion_tokens) { outputTokens = chunk.usage.completion_tokens; } const delta = chunk.choices?.[0]?.delta; const finishReason = chunk.choices?.[0]?.finish_reason; if (delta?.content || delta?.tool_calls || finishReason) { startMessage(controller); } if (delta?.content) { if (blockIndex === 0 && toolBlocksStarted === 0) { startTextBlock(controller); } controller.enqueue( encoder.encode( sseEvent("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "text_delta", text: delta.content }, }), ), ); } if (delta?.tool_calls) { for (const tc of delta.tool_calls) { const idx = tc.index ?? 0; if (!pendingTools.has(idx)) { pendingTools.set(idx, { id: tc.id ?? `toolu_${idx}`, name: tc.function?.name ?? "", args: "", }); const bi = blockIndex + 1 + toolBlocksStarted; toolBlocksStarted++; controller.enqueue( encoder.encode( sseEvent("content_block_start", { type: "content_block_start", index: bi, content_block: { type: "tool_use", id: pendingTools.get(idx)!.id, name: pendingTools.get(idx)!.name, input: {}, }, }), ), ); } const tool = pendingTools.get(idx)!; if (tc.id) tool.id = tc.id; if (tc.function?.name) tool.name = tc.function.name; if (tc.function?.arguments) { tool.args += tc.function.arguments; let partial: unknown = {}; try { partial = JSON.parse(tool.args); } catch { partial = { partial: tool.args }; } controller.enqueue( encoder.encode( sseEvent("content_block_delta", { type: "content_block_delta", index: blockIndex + toolBlocksStarted, delta: { type: "input_json_delta", partial_json: tc.function.arguments }, }), ), ); } } } if (finishReason) { if (blockIndex === 0 && !delta?.tool_calls && toolBlocksStarted === 0) { startTextBlock(controller); } if (blockIndex === 0) { controller.enqueue( encoder.encode( sseEvent("content_block_stop", { type: "content_block_stop", index: blockIndex, }), ), ); } for (let i = 0; i < toolBlocksStarted; i++) { controller.enqueue( encoder.encode( sseEvent("content_block_stop", { type: "content_block_stop", index: blockIndex + 1 + i, }), ), ); } controller.enqueue( encoder.encode( sseEvent("message_delta", { type: "message_delta", delta: { stop_reason: mapStopReason(finishReason), stop_sequence: null, }, usage: { output_tokens: outputTokens || 1 }, }), ), ); controller.enqueue( encoder.encode(sseEvent("message_stop", { type: "message_stop" })), ); } } } controller.close(); } catch (e) { controller.error(e); } }, }); } export { anthropicError };