const SUPPORTED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); function anthropicError(message, type = 'invalid_request_error') { const error = new Error(message); error.status = 400; error.body = { type: 'error', error: { type, message } }; return error; } function textBlock(block) { if (typeof block === 'string') return block; if (block?.type === 'text' && typeof block.text === 'string') return block.text; throw anthropicError(`Unsupported content block type: ${block?.type || 'unknown'}`); } function imageBlock(block) { const source = block?.source; if (block?.type !== 'image' || source?.type !== 'base64' || !SUPPORTED_IMAGE_TYPES.has(source.media_type)) { throw anthropicError('Only base64 JPEG, PNG, GIF, and WebP image blocks are supported'); } return { type: 'image_url', image_url: { url: `data:${source.media_type};base64,${source.data}` } }; } function toOpenAIContent(content) { if (typeof content === 'string') return content; if (!Array.isArray(content)) throw anthropicError('Message content must be a string or array of content blocks'); return content.map((block) => { if (block?.type === 'text') return { type: 'text', text: textBlock(block) }; if (block?.type === 'image') return imageBlock(block); throw anthropicError(`Unsupported content block type: ${block?.type || 'unknown'}`); }); } function toOpenAITool(tool) { if (!tool || tool.type === 'custom' || !tool.name || !tool.input_schema) { throw anthropicError('Tools must include a name and input_schema'); } return { type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.input_schema }, }; } export function translateAnthropicRequest(body = {}) { if (!body.model) throw anthropicError('model is required'); if (!Number.isFinite(body.max_tokens)) throw anthropicError('max_tokens is required'); if (!Array.isArray(body.messages)) throw anthropicError('messages must be an array'); const messages = []; if (body.system !== undefined) { messages.push({ role: 'system', content: toOpenAIContent(body.system) }); } for (const message of body.messages) { if (message?.role === 'system' || message?.role === 'developer') { messages.push({ role: 'system', content: toOpenAIContent(message.content) }); continue; } if (message?.role === 'tool') { messages.push({ role: 'tool', tool_call_id: message.tool_call_id || message.tool_use_id, content: toOpenAIContent(message.content || '') }); continue; } if (!['user', 'assistant'].includes(message?.role)) throw anthropicError(`Unsupported message role: ${message?.role || 'missing'}`); if (!Array.isArray(message.content)) { messages.push({ role: message.role, content: toOpenAIContent(message.content) }); continue; } const ordinary = message.content.filter((block) => block?.type !== 'tool_result' && block?.type !== 'tool_use' && block?.type !== 'thinking'); const thinking = message.content.filter((block) => block?.type === 'thinking'); if (thinking.length && message.role !== 'assistant') throw anthropicError('thinking blocks are only valid in assistant messages'); if (ordinary.length || thinking.length) { const converted = { role: message.role, content: ordinary.length ? toOpenAIContent(ordinary) : null }; if (thinking.length) { if (thinking.some((block) => typeof block.thinking !== 'string')) throw anthropicError('thinking blocks must include thinking text'); converted.reasoning_content = thinking.map((block) => block.thinking).join(''); } messages.push(converted); } for (const block of message.content) { if (block.type === 'tool_result') { if (!block.tool_use_id) throw anthropicError('tool_result must include tool_use_id'); messages.push({ role: 'tool', tool_call_id: block.tool_use_id, content: toOpenAIContent(block.content || '') }); } else if (block.type === 'tool_use') { if (!block.id || !block.name) throw anthropicError('tool_use must include id and name'); const assistant = messages.at(-1)?.role === 'assistant' ? messages.at(-1) : null; if (assistant) { assistant.tool_calls = assistant.tool_calls || []; assistant.tool_calls.push({ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } }); } else { messages.push({ role: 'assistant', content: null, tool_calls: [{ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } }] }); } } } } const result = { model: body.model, messages, max_tokens: body.max_tokens, }; for (const field of ['temperature', 'top_p']) if (body[field] !== undefined) result[field] = body[field]; if (body.stop_sequences !== undefined) result.stop = body.stop_sequences; if (body.stream !== undefined) result.stream = body.stream; if (body.tools !== undefined) result.tools = body.tools.map(toOpenAITool); if (body.tool_choice !== undefined) { if (body.tool_choice.type === 'auto') result.tool_choice = 'auto'; else if (body.tool_choice.type === 'any') result.tool_choice = 'required'; else if (body.tool_choice.type === 'tool' && body.tool_choice.name) result.tool_choice = { type: 'function', function: { name: body.tool_choice.name } }; else throw anthropicError('Unsupported tool_choice'); } return result; } function usageOf(usage = {}) { return { input_tokens: usage.prompt_tokens ?? 0, output_tokens: usage.completion_tokens ?? 0 }; } export function translateAnthropicResponse(payload, requestedModel) { const choice = payload.choices?.[0] || {}; const message = choice.message || {}; const content = []; if (message.reasoning_content) content.push({ type: 'thinking', thinking: message.reasoning_content }); if (message.content) content.push({ type: 'text', text: message.content }); for (const call of message.tool_calls || []) { let input = {}; try { input = JSON.parse(call.function?.arguments || '{}'); } catch { input = {}; } content.push({ type: 'tool_use', id: call.id, name: call.function?.name, input }); } const finish = choice.finish_reason; return { id: payload.id || `msg_${Date.now()}`, type: 'message', role: 'assistant', model: payload.model || requestedModel, content, stop_reason: finish === 'tool_calls' ? 'tool_use' : finish === 'length' ? 'max_tokens' : finish === 'stop' ? 'end_turn' : null, stop_sequence: null, usage: usageOf(payload.usage), }; } export function translateAnthropicModels(payload) { return { object: 'list', data: (payload.data || []).map((model) => ({ id: model.id, display_name: model.name || model.id, created_at: new Date((model.created || 0) * 1000 || Date.now()).toISOString(), type: 'model', context_length: model.context_length, context_window: model.context_window, max_context_tokens: model.max_context_tokens, })), has_more: false, first_id: payload.data?.[0]?.id, last_id: payload.data?.at(-1)?.id, }; } function event(type, data) { return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`; } export function translateOpenAIChunk(chunk, state) { const choice = chunk.choices?.[0]; if (!choice) return ''; const delta = choice.delta || {}; let output = ''; if (!state.started) { state.started = true; output += event('message_start', { type: 'message_start', message: { id: state.id, type: 'message', role: 'assistant', model: state.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } }); } if (delta.reasoning_content) { if (!state.reasoningStarted) { state.reasoningStarted = true; state.reasoningIndex = state.nextBlockIndex++; output += event('content_block_start', { type: 'content_block_start', index: state.reasoningIndex, content_block: { type: 'thinking', thinking: '' } }); } output += event('content_block_delta', { type: 'content_block_delta', index: state.reasoningIndex, delta: { type: 'thinking_delta', thinking: delta.reasoning_content } }); } if (delta.content) { if (!state.contentStarted) { state.contentStarted = true; state.contentIndex = state.nextBlockIndex++; output += event('content_block_start', { type: 'content_block_start', index: state.contentIndex, content_block: { type: 'text', text: '' } }); } output += event('content_block_delta', { type: 'content_block_delta', index: state.contentIndex, delta: { type: 'text_delta', text: delta.content } }); } if (delta.tool_calls?.length) { const call = delta.tool_calls[0]; if (!state.toolStarted) { state.toolStarted = true; state.toolIndex = state.nextBlockIndex++; output += event('content_block_start', { type: 'content_block_start', index: state.toolIndex, content_block: { type: 'tool_use', id: call.id || 'tool_use', name: call.function?.name || '', input: {} } }); } if (call.function?.arguments) output += event('content_block_delta', { type: 'content_block_delta', index: state.toolIndex, delta: { type: 'input_json_delta', partial_json: call.function.arguments } }); } if (choice.finish_reason) { for (const index of [state.reasoningIndex, state.contentIndex, state.toolIndex]) { if (index !== undefined) output += event('content_block_stop', { type: 'content_block_stop', index }); } output += event('message_delta', { type: 'message_delta', delta: { stop_reason: choice.finish_reason === 'tool_calls' ? 'tool_use' : choice.finish_reason === 'length' ? 'max_tokens' : 'end_turn', stop_sequence: null }, usage: { output_tokens: chunk.usage?.completion_tokens || 0 } }); output += event('message_stop', { type: 'message_stop' }); } return output; } export { anthropicError, event };