Files
intelligence-terminal/lib/llm/openai-compatible.mjs

78 lines
2.9 KiB
JavaScript

// 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,
};
}
}