82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
/**
|
|
* OpenAI direct LLM provider.
|
|
*
|
|
* Uses OpenAI REST API with Bearer token authentication.
|
|
* Reads config from OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL.
|
|
*/
|
|
|
|
import type { ChatCompletionRequest, ChatCompletionResponse, LLMProvider } from '../types.js';
|
|
|
|
export interface OpenAIConfig {
|
|
apiKey: string;
|
|
baseUrl?: string;
|
|
model?: string;
|
|
}
|
|
|
|
export class OpenAIProvider implements LLMProvider {
|
|
private config: OpenAIConfig;
|
|
|
|
constructor(config?: Partial<OpenAIConfig>) {
|
|
this.config = {
|
|
apiKey: config?.apiKey || process.env.OPENAI_API_KEY || '',
|
|
baseUrl: config?.baseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
model: config?.model || process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
};
|
|
}
|
|
|
|
isConfigured(): boolean {
|
|
return Boolean(this.config.apiKey);
|
|
}
|
|
|
|
async chatCompletion(req: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
|
if (!this.isConfigured()) {
|
|
throw new Error('OpenAI is not configured (missing OPENAI_API_KEY)');
|
|
}
|
|
|
|
const base = this.config.baseUrl!.replace(/\/+$/, '');
|
|
const url = `${base}/chat/completions`;
|
|
|
|
const body = {
|
|
model: req.model || this.config.model,
|
|
messages: req.messages,
|
|
temperature: req.temperature,
|
|
max_tokens: req.maxTokens,
|
|
top_p: req.topP,
|
|
stop: req.stop,
|
|
response_format: req.responseFormat,
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(`OpenAI error ${response.status}: ${text}`);
|
|
}
|
|
|
|
const data = (await response.json()) as {
|
|
choices: Array<{ message: { content: string }; finish_reason: string }>;
|
|
model: string;
|
|
usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
};
|
|
|
|
return {
|
|
content: data.choices[0]?.message?.content ?? '',
|
|
model: data.model,
|
|
finishReason:
|
|
(data.choices[0]?.finish_reason as ChatCompletionResponse['finishReason']) ?? null,
|
|
usage: {
|
|
promptTokens: data.usage.prompt_tokens,
|
|
completionTokens: data.usage.completion_tokens,
|
|
totalTokens: data.usage.total_tokens,
|
|
},
|
|
};
|
|
}
|
|
}
|