tasq/node_modules/agentic-flow/dist/router/providers/openrouter.js

246 lines
8.7 KiB
JavaScript

// OpenRouter provider implementation
import axios from 'axios';
import { mapModelId } from '../model-mapping.js';
export class OpenRouterProvider {
name = 'openrouter';
type = 'openrouter';
supportsStreaming = true;
supportsTools = true;
supportsMCP = false; // Requires translation
client;
config;
constructor(config) {
this.config = config;
if (!config.apiKey) {
throw new Error('OpenRouter API key is required');
}
this.client = axios.create({
baseURL: config.baseUrl || 'https://openrouter.ai/api/v1',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'HTTP-Referer': 'https://github.com/ruvnet/agentic-flow',
'X-Title': 'Agentic Flow Multi-Model Router',
'Content-Type': 'application/json'
},
timeout: config.timeout || 180000
});
}
validateCapabilities(features) {
const supported = ['chat', 'streaming', 'tools'];
return features.every(f => supported.includes(f));
}
async chat(params) {
try {
const requestBody = this.formatRequest(params);
const response = await this.client.post('/chat/completions', requestBody);
return this.formatResponse(response.data, params.model);
}
catch (error) {
throw this.handleError(error);
}
}
async *stream(params) {
try {
const requestBody = this.formatRequest(params, true);
const response = await this.client.post('/chat/completions', requestBody, {
responseType: 'stream'
});
for await (const chunk of response.data) {
const lines = chunk.toString().split('\n').filter((line) => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
yield { type: 'message_stop' };
return;
}
try {
const parsed = JSON.parse(data);
const streamChunk = this.formatStreamChunk(parsed);
if (streamChunk) {
yield streamChunk;
}
}
catch (e) {
// Skip invalid JSON
}
}
}
}
}
catch (error) {
throw this.handleError(error);
}
}
formatRequest(params, stream = false) {
const messages = params.messages.map(msg => ({
role: msg.role,
content: typeof msg.content === 'string'
? msg.content
: msg.content.map(block => {
if (block.type === 'text')
return { type: 'text', text: block.text };
if (block.type === 'tool_use')
return {
type: 'function',
function: {
name: block.name,
arguments: JSON.stringify(block.input)
}
};
if (block.type === 'tool_result')
return {
type: 'function_result',
content: block.content,
is_error: block.is_error
};
return block;
})
}));
// Map model ID to OpenRouter format
const openrouterModel = mapModelId(params.model, 'openrouter');
const body = {
model: openrouterModel,
messages,
temperature: params.temperature ?? 0.7,
max_tokens: params.maxTokens,
stream
};
// Add tools if provided
if (params.tools && params.tools.length > 0) {
body.tools = params.tools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.input_schema
}
}));
if (params.toolChoice) {
if (params.toolChoice === 'auto' || params.toolChoice === 'none') {
body.tool_choice = params.toolChoice;
}
else if (typeof params.toolChoice === 'object') {
body.tool_choice = {
type: 'function',
function: { name: params.toolChoice.name }
};
}
}
}
// Add OpenRouter-specific preferences
if (this.config.preferences) {
if (this.config.preferences.requireParameters) {
body.require_parameters = true;
}
if (this.config.preferences.dataCollection) {
body['X-Data-Collection'] = this.config.preferences.dataCollection;
}
if (this.config.preferences.order) {
body.provider = {
order: this.config.preferences.order
};
}
}
return body;
}
formatResponse(data, model) {
const choice = data.choices[0];
const message = choice.message;
const content = [];
// Handle text content
if (message.content) {
content.push({
type: 'text',
text: message.content
});
}
// Handle tool calls
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: 'tool_use',
id: toolCall.id,
name: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments)
});
}
}
const stopReason = this.mapFinishReason(choice.finish_reason);
return {
id: data.id,
model: data.model || model,
content,
stopReason,
usage: {
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0
},
metadata: {
provider: 'openrouter',
cost: this.calculateCost(data.usage),
latency: 0 // Will be set by router
}
};
}
formatStreamChunk(data) {
const choice = data.choices?.[0];
if (!choice)
return null;
const delta = choice.delta;
if (delta?.content) {
return {
type: 'content_block_delta',
delta: {
type: 'text_delta',
text: delta.content
}
};
}
if (delta?.tool_calls) {
const toolCall = delta.tool_calls[0];
return {
type: 'content_block_delta',
delta: {
type: 'input_json_delta',
partial_json: toolCall.function?.arguments || ''
}
};
}
if (choice.finish_reason) {
return {
type: 'message_stop',
usage: data.usage ? {
inputTokens: data.usage.prompt_tokens || 0,
outputTokens: data.usage.completion_tokens || 0
} : undefined
};
}
return null;
}
mapFinishReason(reason) {
switch (reason) {
case 'stop': return 'end_turn';
case 'length': return 'max_tokens';
case 'tool_calls': return 'tool_use';
default: return 'end_turn';
}
}
calculateCost(usage) {
if (!usage)
return 0;
// OpenRouter pricing varies by model
// Using average pricing: $0.01/1K input, $0.03/1K output
const inputCost = (usage.prompt_tokens || 0) * 0.00001;
const outputCost = (usage.completion_tokens || 0) * 0.00003;
return inputCost + outputCost;
}
handleError(error) {
const providerError = new Error(error.response?.data?.error?.message || error.message || 'OpenRouter request failed');
providerError.provider = 'openrouter';
providerError.statusCode = error.response?.status;
providerError.retryable = error.response?.status >= 500 || error.code === 'ECONNABORTED';
return providerError;
}
}
//# sourceMappingURL=openrouter.js.map