diff --git a/lib/llm/index.mjs b/lib/llm/index.mjs index 5711350..e590b0a 100644 --- a/lib/llm/index.mjs +++ b/lib/llm/index.mjs @@ -2,12 +2,14 @@ import { AnthropicProvider } from './anthropic.mjs'; import { OpenAIProvider } from './openai.mjs'; +import { OpenRouterProvider } from './openrouter.mjs'; import { GeminiProvider } from './gemini.mjs'; import { CodexProvider } from './codex.mjs'; export { LLMProvider } from './provider.mjs'; export { AnthropicProvider } from './anthropic.mjs'; export { OpenAIProvider } from './openai.mjs'; +export { OpenRouterProvider } from './openrouter.mjs'; export { GeminiProvider } from './gemini.mjs'; export { CodexProvider } from './codex.mjs'; @@ -26,6 +28,8 @@ export function createLLMProvider(llmConfig) { return new AnthropicProvider({ apiKey, model }); case 'openai': return new OpenAIProvider({ apiKey, model }); + case 'openrouter': + return new OpenRouterProvider({ apiKey, model }); case 'gemini': return new GeminiProvider({ apiKey, model }); case 'codex': diff --git a/lib/llm/openrouter.mjs b/lib/llm/openrouter.mjs new file mode 100644 index 0000000..4805de8 --- /dev/null +++ b/lib/llm/openrouter.mjs @@ -0,0 +1,52 @@ +// OpenRouter Provider — raw fetch, no SDK + +import { LLMProvider } from './provider.mjs'; + +export class OpenRouterProvider extends LLMProvider { + constructor(config) { + super(config); + this.name = 'openrouter'; + this.apiKey = config.apiKey; + this.model = config.model || 'openrouter/auto'; + } + + get isConfigured() { return !!this.apiKey; } + + async complete(systemPrompt, userMessage, opts = {}) { + const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + 'HTTP-Referer': 'https://github.com/calesthio/Crucix', + 'X-Title': 'Crucix', + }, + body: JSON.stringify({ + model: this.model, + max_tokens: opts.maxTokens || 4096, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userMessage }, + ], + }), + signal: AbortSignal.timeout(opts.timeout || 60000), + }); + + if (!res.ok) { + const err = await res.text().catch(() => ''); + throw new Error(`OpenRouter API ${res.status}: ${err.substring(0, 200)}`); + } + + const data = await res.json(); + const text = data.choices?.[0]?.message?.content || ''; + + return { + text, + usage: { + inputTokens: data.usage?.prompt_tokens || 0, + outputTokens: data.usage?.completion_tokens || 0, + }, + model: data.model || this.model, + }; + } +}