UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

285 lines 11.9 kB
import { BaseProvider } from './base.js'; import { openRouterModelsService } from '../services/openrouter-models.js'; export class OpenRouterProvider extends BaseProvider { name = 'openrouter'; get models() { const models = openRouterModelsService.getModelsByProvider(this.name); if (models.length > 0) { return models.map(m => m.id); } const fallbackModels = { 'openai': ['openai/gpt-4o', 'openai/gpt-4o-mini'], 'anthropic': ['anthropic/claude-opus-4', 'anthropic/claude-sonnet-4'], 'google': ['google/gemini-2.5-pro', 'google/gemini-2.5-flash'], 'xai': ['x-ai/grok-4'], 'deepseek': ['deepseek/deepseek-chat-v3-0324', 'deepseek/deepseek-r1-0528:free'], 'moonshot': ['moonshotai/kimi-k2'] }; return fallbackModels[this.name] || ['openai/gpt-4o']; } supportsStreaming = true; supportsTools = true; baseUrl = 'https://openrouter.ai/api/v1'; constructor(apiKey) { super(apiKey); } async complete(messages, options = {}) { this.validateMessages(messages); const model = options.model || this.models[0]; const formattedMessages = this.formatMessages(messages); const body = { model, messages: formattedMessages, temperature: options.temperature, }; if (options.maxTokens) { body.max_tokens = options.maxTokens; } if (options.tools && options.tools.length > 0) { body.tools = options.tools; body.tool_choice = 'auto'; } const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/capsule-ai-cli', 'X-Title': 'Capsule CLI', }, body: JSON.stringify(body), }); if (!response.ok) { const error = await response.text(); throw new Error(`OpenRouter API error ${response.status}: ${error}`); } const data = await response.json(); return this.parseResponse(data, model); } async *stream(messages, options = {}) { this.validateMessages(messages); const model = options.model || this.models[0]; const formattedMessages = this.formatMessages(messages); const body = { model, messages: formattedMessages, temperature: options.temperature, stream: true, }; if (options.maxTokens) { body.max_tokens = options.maxTokens; } if (options.tools && options.tools.length > 0) { body.tools = options.tools; body.tool_choice = 'auto'; } const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://github.com/capsule-ai-cli', 'X-Title': 'Capsule CLI', }, body: JSON.stringify(body), }); if (!response.ok || !response.body) { const error = await response.text(); throw new Error(`OpenRouter stream error ${response.status}: ${error}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const toolCallsInProgress = new Map(); while (true) { const { value, done } = 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) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') return; try { const chunk = JSON.parse(data); const delta = chunk.choices?.[0]?.delta; if (delta?.content) { yield { delta: delta.content }; } if (delta?.tool_calls) { for (const toolCallDelta of delta.tool_calls) { const index = toolCallDelta.index || 0; if (!toolCallsInProgress.has(index)) { toolCallsInProgress.set(index, {}); } const toolCall = toolCallsInProgress.get(index); if (toolCallDelta.id) { toolCall.id = toolCallDelta.id; } if (toolCallDelta.function?.name) { toolCall.name = toolCallDelta.function.name; } if (toolCallDelta.function?.arguments) { toolCall.arguments = (toolCall.arguments || '') + toolCallDelta.function.arguments; } if (toolCall.id && toolCall.name && toolCall.arguments) { try { const parsedArgs = JSON.parse(toolCall.arguments); yield { delta: '', toolCall: { id: toolCall.id, name: toolCall.name, arguments: parsedArgs, }, }; toolCallsInProgress.delete(index); } catch (e) { } } } } if (chunk.choices?.[0]?.finish_reason === 'tool_calls') { for (const [, toolCall] of toolCallsInProgress) { if (toolCall.id && toolCall.name && toolCall.arguments) { try { const parsedArgs = JSON.parse(toolCall.arguments); yield { delta: '', toolCall: { id: toolCall.id, name: toolCall.name, arguments: parsedArgs, }, }; } catch (e) { } } } toolCallsInProgress.clear(); } if (chunk.usage) { yield { delta: '', usage: { promptTokens: chunk.usage.prompt_tokens, completionTokens: chunk.usage.completion_tokens, totalTokens: chunk.usage.total_tokens, }, }; } } catch (e) { } } } } } calculateCost(usage, model = this.models[0]) { const pricing = openRouterModelsService.getModelPricing(model); let promptCost = 0; let completionCost = 0; if (pricing) { promptCost = usage.promptTokens * pricing.prompt; completionCost = usage.completionTokens * pricing.completion; } else { promptCost = (usage.promptTokens / 1_000_000) * 1; completionCost = (usage.completionTokens / 1_000_000) * 2; } return { amount: promptCost + completionCost, currency: 'USD', breakdown: { prompt: promptCost, completion: completionCost, }, }; } formatMessages(messages) { const formatted = []; for (const msg of messages) { if (msg.role === 'tool_result') { formatted.push({ role: 'tool', content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), tool_call_id: msg.tool_call_id, name: msg.name || 'unknown' }); continue; } const formattedMsg = { role: msg.role, content: '' }; if (msg.role === 'assistant' && msg.tool_calls) { formattedMsg.content = null; formattedMsg.tool_calls = msg.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments) } })); } else if (typeof msg.content === 'string') { formattedMsg.content = msg.content; } else if (Array.isArray(msg.content)) { formattedMsg.content = msg.content.map((part) => { if (part.type === 'text') { return { type: 'text', text: part.text }; } else if (part.type === 'image') { const imagePart = part; return { type: 'image_url', image_url: { url: `data:${imagePart.source.media_type};base64,${imagePart.source.data}`, }, }; } return { type: part.type }; }); } formatted.push(formattedMsg); } return formatted; } parseResponse(data, model) { const choice = data.choices?.[0]; if (!choice) { throw new Error('No response from OpenRouter'); } const message = choice.message; const toolCalls = []; if (message.tool_calls) { for (const tc of message.tool_calls) { toolCalls.push({ id: tc.id, name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}'), }); } } const usage = { promptTokens: data.usage?.prompt_tokens || 0, completionTokens: data.usage?.completion_tokens || 0, totalTokens: data.usage?.total_tokens || 0, }; return { provider: this.name, model, content: message.content || '', usage, toolCalls: toolCalls.length > 0 ? toolCalls : undefined, }; } } //# sourceMappingURL=openrouter.js.map