| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- /**
- * 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 };
- }
|