capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
285 lines • 11.9 kB
JavaScript
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