/** * Anthropic provider — translates OpenAI chat completion requests * to Anthropic Messages API calls using native fetch(). */ export function createProvider(config) { const { id, apiKey, baseUrl, models } = config; const messagesUrl = baseUrl ? baseUrl.replace(/\/+$/, '') : 'https://api.anthropic.com/v1/messages'; function buildHeaders() { return { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }; } function translateMessages(openAiMessages) { const systemParts = []; const messages = []; for (const msg of openAiMessages) { if (msg.role === 'system') { systemParts.push(msg.content); } else { messages.push({ role: msg.role, content: msg.content }); } } const system = systemParts.length > 0 ? systemParts.join('\n\n') : undefined; return { system, messages }; } function buildBody(openAiMessages, modelId, options, stream) { const { system, messages } = translateMessages(openAiMessages); const body = { model: modelId, messages, max_tokens: options.max_tokens ?? 4096, stream }; if (system !== undefined) { body.system = system; } if (options.temperature !== undefined) { body.temperature = options.temperature; } if (options.top_p !== undefined) { body.top_p = options.top_p; } return body; } async function request(openAiMessages, modelId, options, stream) { const body = buildBody(openAiMessages, modelId, options, stream); const headers = buildHeaders(); let response; try { response = await fetch(messagesUrl, { method: 'POST', headers, body: JSON.stringify(body) }); } catch (error) { throw new Error(`Anthropic provider request failed: ${error.message}`); } if (!response.ok) { let errorBody = ''; try { errorBody = await response.text(); } catch { /* ignore */ } throw new Error( `Anthropic provider returned ${response.status}${errorBody ? `: ${errorBody}` : ''}` ); } return response; } async function chat(openAiMessages, localModelId, options = {}) { const providerModelId = models[localModelId]; const response = await request(openAiMessages, providerModelId, options, false); const anthropicResponse = await response.json(); return { id: anthropicResponse.id, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: localModelId, choices: [ { index: 0, message: { role: 'assistant', content: anthropicResponse.content[0].text }, finish_reason: 'stop' } ], usage: { prompt_tokens: anthropicResponse.usage.input_tokens, completion_tokens: anthropicResponse.usage.output_tokens, total_tokens: anthropicResponse.usage.input_tokens + anthropicResponse.usage.output_tokens } }; } async function* chatStream(openAiMessages, localModelId, options = {}) { const providerModelId = models[localModelId]; const response = await request(openAiMessages, providerModelId, options, true); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; 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(); // keep incomplete line in buffer for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('data:')) continue; const dataStr = trimmed.slice(5).trim(); if (dataStr === '[DONE]') continue; let data; try { data = JSON.parse(dataStr); } catch { continue; } if (data.type === 'content_block_delta' && data.delta?.text) { yield { id: `chunk-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: localModelId, choices: [ { index: 0, delta: { content: data.delta.text }, finish_reason: null } ] }; } else if (data.type === 'message_stop') { yield { id: `chunk-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: localModelId, choices: [ { index: 0, delta: {}, finish_reason: 'stop' } ] }; } } } } finally { reader.releaseLock(); } } return { id, chat, chatStream }; }