// OpenAI-compatible provider for OpenRouter, LM Studio, Ollama, Groq, xAI, and local endpoints. import { LLMProvider } from './provider.mjs'; const DEFAULT_BASE_URL = 'https://api.openai.com/v1'; export class OpenAICompatibleProvider extends LLMProvider { constructor(config = {}) { super(config); this.name = config.name || 'openai-compatible'; this.apiKey = config.apiKey || null; this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, ''); this.model = config.model || 'gpt-4o-mini'; this.temperature = config.temperature ?? 0.2; this.maxTokens = config.maxTokens || 2000; this.timeoutMs = config.timeoutMs || 90000; this.extraHeaders = config.extraHeaders || {}; this.requiresApiKey = config.requiresApiKey ?? true; this.useMaxCompletionTokens = Boolean(config.useMaxCompletionTokens); } get isConfigured() { return Boolean(this.model && (!this.requiresApiKey || this.apiKey)); } get status() { if (!this.model) return { state: 'misconfigured', reason: 'LLM_MODEL is required' }; if (this.requiresApiKey && !this.apiKey) return { state: 'misconfigured', reason: 'LLM_API_KEY is required' }; return { state: 'configured', provider: this.name, model: this.model, baseUrl: this.baseUrl }; } async complete(systemPrompt, userMessage, opts = {}) { const maxTokens = opts.maxTokens || this.maxTokens; const timeout = opts.timeout || this.timeoutMs; const body = { model: this.model, temperature: opts.temperature ?? this.temperature, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], }; body[this.useMaxCompletionTokens ? 'max_completion_tokens' : 'max_tokens'] = maxTokens; const headers = { 'Content-Type': 'application/json', ...this.extraHeaders, }; if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`; const res = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(timeout), }); if (!res.ok) { const err = await res.text().catch(() => ''); const detail = err.substring(0, 300); if (res.status === 401 || res.status === 403) throw new Error(`${this.name} auth failed (${res.status}): ${detail}`); if (res.status === 408 || res.status === 429) throw new Error(`${this.name} rate limited or timed out (${res.status}): ${detail}`); throw new Error(`${this.name} API ${res.status}: ${detail}`); } const data = await res.json(); const text = data.choices?.[0]?.message?.content || data.choices?.[0]?.text || ''; return { text, usage: { inputTokens: data.usage?.prompt_tokens || data.usage?.input_tokens || 0, outputTokens: data.usage?.completion_tokens || data.usage?.output_tokens || 0, }, model: data.model || this.model, }; } }