openai-client.ts•6.65 kB
import OpenAI from 'openai';
import { config } from './config.js';
export interface OpenAIRequest {
prompt: string;
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
model?: string;
reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
verbosity?: 'low' | 'medium' | 'high';
}
export interface OpenAIResponse {
content: string;
reasoning?: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
reasoningTokens?: number;
};
model: string;
finishReason?: string;
}
export class OpenAIClient {
private client: OpenAI;
constructor() {
this.client = new OpenAI({
apiKey: config.openai.apiKey,
baseURL: config.openai.baseURL
});
}
async chat(request: OpenAIRequest): Promise<OpenAIResponse> {
try {
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
if (request.systemPrompt) {
messages.push({
role: 'system',
content: request.systemPrompt
});
}
messages.push({
role: 'user',
content: request.prompt
});
const model = request.model || config.openai.model;
const completionParams: any = {
model,
messages
};
// Use appropriate parameters based on model
if (model === 'gpt-5' || model.startsWith('gpt-5')) {
completionParams.max_completion_tokens = request.maxTokens || 2000;
// GPT-5 new parameters
if (request.reasoningEffort) {
completionParams.reasoning_effort = request.reasoningEffort;
} else {
completionParams.reasoning_effort = 'medium'; // Default for GPT-5
}
if (request.verbosity) {
completionParams.verbosity = request.verbosity;
}
// GPT-5 supports temperature but defaults to 1
if (request.temperature !== undefined) {
completionParams.temperature = request.temperature;
}
} else {
completionParams.max_tokens = request.maxTokens || 2000;
completionParams.temperature = request.temperature || 0.7;
}
const response = await this.client.chat.completions.create(completionParams);
const choice = response.choices[0];
if (!choice?.message) {
throw new Error('No message in response from OpenAI');
}
// Handle GPT-5 reasoning and content
let content = choice.message.content || '';
let reasoning = '';
// Extract reasoning tokens count
const reasoningTokens = response.usage?.completion_tokens_details?.reasoning_tokens || 0;
// For GPT-5, if content is empty but reasoning tokens exist, fallback to GPT-4o
if (!content && reasoningTokens > 0) {
if (config.server.debug) {
console.error('GPT-5 Reasoning Mode Detected - Falling back to GPT-4o:', {
reasoningTokens,
totalTokens: response.usage?.total_tokens,
hasContent: false
});
}
// Automatic fallback to GPT-4o for actual content
try {
const fallbackParams = {
...completionParams,
model: 'gpt-4o'
};
// Remove GPT-5 specific parameters for GPT-4o
delete fallbackParams.reasoning_effort;
delete fallbackParams.verbosity;
delete fallbackParams.max_completion_tokens;
fallbackParams.max_tokens = request.maxTokens || 2000;
fallbackParams.temperature = request.temperature || 0.7;
const fallbackResponse = await this.client.chat.completions.create(fallbackParams);
const fallbackContent = fallbackResponse.choices[0]?.message?.content || '';
if (fallbackContent) {
content = `[GPT-5 reasoning: ${reasoningTokens} tokens] ${fallbackContent}`;
// Update usage to include both models
if (response.usage && fallbackResponse.usage) {
response.usage.total_tokens += fallbackResponse.usage.total_tokens;
response.usage.completion_tokens += fallbackResponse.usage.completion_tokens;
}
} else {
content = `[GPT-5 used ${reasoningTokens} reasoning tokens but produced no content. GPT-4o fallback also failed.]`;
}
} catch (fallbackError) {
content = `[GPT-5 used ${reasoningTokens} reasoning tokens but content unavailable. GPT-4o fallback error: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}]`;
}
}
// Debug logging for development
if (config.server.debug) {
console.error('GPT-5 Response Analysis:', {
hasContent: !!choice.message.content,
contentLength: choice.message.content?.length || 0,
reasoningTokens,
finishReason: choice.finish_reason,
model: response.model
});
}
return {
content: content,
reasoning: reasoning || undefined,
usage: response.usage ? {
promptTokens: response.usage.prompt_tokens,
completionTokens: response.usage.completion_tokens,
totalTokens: response.usage.total_tokens,
reasoningTokens: reasoningTokens
} : undefined,
model: response.model,
finishReason: choice.finish_reason || undefined
};
} catch (error) {
if (config.server.debug) {
console.error('OpenAI API Error:', error);
}
if (error instanceof Error) {
throw new Error(`OpenAI API Error: ${error.message}`);
}
throw new Error('Unknown OpenAI API Error');
}
}
async listModels(): Promise<any[]> {
try {
const response = await this.client.models.list();
return response.data;
} catch (error) {
if (config.server.debug) {
console.error('List models error:', error);
}
throw new Error(`Failed to list models: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async testConnection(): Promise<boolean> {
try {
const response = await this.chat({
prompt: 'Test connection - respond with "OK"',
systemPrompt: 'You are a test assistant. Respond only with "OK".',
maxTokens: 10
});
if (config.server.debug) {
console.error('Connection test successful:', response.content);
}
return true;
} catch (error) {
console.error('OpenAI connection test failed:', error);
return false;
}
}
}