78 lines
2.9 KiB
JavaScript
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,
|
|
};
|
|
}
|
|
}
|