UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

476 lines (431 loc) 14.1 kB
/** * OpenAI Provider Implementation * Supports GPT-4, GPT-3.5, and other OpenAI models */ import { BaseProvider } from './base-provider.js'; import { LLMProvider, LLMModel, LLMRequest, LLMResponse, LLMStreamEvent, ModelInfo, ProviderCapabilities, HealthCheckResult, LLMProviderError, RateLimitError, AuthenticationError, ModelNotFoundError, } from './types.js'; interface OpenAIRequest { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant' | 'function'; content: string; name?: string; function_call?: { name: string; arguments: string; }; }>; temperature?: number; max_tokens?: number; top_p?: number; frequency_penalty?: number; presence_penalty?: number; stop?: string[]; stream?: boolean; functions?: Array<{ name: string; description: string; parameters: any; }>; function_call?: 'auto' | 'none' | { name: string }; } interface OpenAIResponse { id: string; object: string; created: number; model: string; choices: Array<{ index: number; message: { role: string; content: string | null; function_call?: { name: string; arguments: string; }; }; finish_reason: 'stop' | 'length' | 'function_call' | 'content_filter'; }>; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; } interface OpenAIStreamChunk { id: string; object: string; created: number; model: string; choices: Array<{ index: number; delta: { role?: string; content?: string; function_call?: { name?: string; arguments?: string; }; }; finish_reason?: string; }>; } export class OpenAIProvider extends BaseProvider { readonly name: LLMProvider = 'openai'; readonly capabilities: ProviderCapabilities = { supportedModels: [ 'gpt-4-turbo-preview', 'gpt-4', 'gpt-4-32k', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', ], maxContextLength: { 'gpt-4-turbo-preview': 128000, 'gpt-4': 8192, 'gpt-4-32k': 32768, 'gpt-3.5-turbo': 4096, 'gpt-3.5-turbo-16k': 16384, } as Record<LLMModel, number>, maxOutputTokens: { 'gpt-4-turbo-preview': 4096, 'gpt-4': 4096, 'gpt-4-32k': 4096, 'gpt-3.5-turbo': 4096, 'gpt-3.5-turbo-16k': 4096, } as Record<LLMModel, number>, supportsStreaming: true, supportsFunctionCalling: true, supportsSystemMessages: true, supportsVision: true, // GPT-4 with vision supportsAudio: false, supportsTools: true, supportsFineTuning: true, supportsEmbeddings: true, supportsLogprobs: true, supportsBatching: true, rateLimit: { requestsPerMinute: 3500, tokensPerMinute: 90000, concurrentRequests: 100, }, pricing: { 'gpt-4-turbo-preview': { promptCostPer1k: 0.01, completionCostPer1k: 0.03, currency: 'USD', }, 'gpt-4': { promptCostPer1k: 0.03, completionCostPer1k: 0.06, currency: 'USD', }, 'gpt-4-32k': { promptCostPer1k: 0.06, completionCostPer1k: 0.12, currency: 'USD', }, 'gpt-3.5-turbo': { promptCostPer1k: 0.0005, completionCostPer1k: 0.0015, currency: 'USD', }, 'gpt-3.5-turbo-16k': { promptCostPer1k: 0.003, completionCostPer1k: 0.004, currency: 'USD', }, }, }; private baseUrl: string; private headers: Record<string, string>; protected async doInitialize(): Promise<void> { if (!this.config.apiKey) { throw new AuthenticationError('OpenAI API key is required', 'openai'); } this.baseUrl = this.config.apiUrl || 'https://api.openai.com/v1'; this.headers = { 'Authorization': `Bearer ${this.config.apiKey}`, 'Content-Type': 'application/json', }; // Add organization header if provided if (this.config.providerOptions?.organization) { this.headers['OpenAI-Organization'] = this.config.providerOptions.organization; } } protected async doComplete(request: LLMRequest): Promise<LLMResponse> { const openAIRequest: OpenAIRequest = { model: this.mapToOpenAIModel(request.model || this.config.model), messages: request.messages.map((msg) => ({ role: msg.role, content: msg.content, ...(msg.name && { name: msg.name }), ...(msg.functionCall && { function_call: msg.functionCall }), })), temperature: request.temperature ?? this.config.temperature, max_tokens: request.maxTokens ?? this.config.maxTokens, top_p: request.topP ?? this.config.topP, frequency_penalty: request.frequencyPenalty ?? this.config.frequencyPenalty, presence_penalty: request.presencePenalty ?? this.config.presencePenalty, stop: request.stopSequences ?? this.config.stopSequences, stream: false, }; // Add function calling if present if (request.functions) { openAIRequest.functions = request.functions; openAIRequest.function_call = request.functionCall; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.config.timeout || 60000); try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: this.headers, body: JSON.stringify(openAIRequest), signal: controller.signal, }); clearTimeout(timeout); if (!response.ok) { await this.handleErrorResponse(response); } const data: OpenAIResponse = await response.json(); const choice = data.choices[0]; // Calculate cost const model = request.model || this.config.model; const pricing = this.capabilities.pricing![model]; const promptCost = (data.usage.prompt_tokens / 1000) * pricing.promptCostPer1k; const completionCost = (data.usage.completion_tokens / 1000) * pricing.completionCostPer1k; return { id: data.id, model: this.mapFromOpenAIModel(data.model), provider: 'openai', content: choice.message.content || '', functionCall: choice.message.function_call, usage: { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens, }, cost: { promptCost, completionCost, totalCost: promptCost + completionCost, currency: 'USD', }, finishReason: choice.finish_reason, }; } catch (error) { clearTimeout(timeout); throw this.transformError(error); } } protected async *doStreamComplete(request: LLMRequest): AsyncIterable<LLMStreamEvent> { const openAIRequest: OpenAIRequest = { model: this.mapToOpenAIModel(request.model || this.config.model), messages: request.messages.map((msg) => ({ role: msg.role, content: msg.content, ...(msg.name && { name: msg.name }), ...(msg.functionCall && { function_call: msg.functionCall }), })), temperature: request.temperature ?? this.config.temperature, max_tokens: request.maxTokens ?? this.config.maxTokens, top_p: request.topP ?? this.config.topP, frequency_penalty: request.frequencyPenalty ?? this.config.frequencyPenalty, presence_penalty: request.presencePenalty ?? this.config.presencePenalty, stop: request.stopSequences ?? this.config.stopSequences, stream: true, }; if (request.functions) { openAIRequest.functions = request.functions; openAIRequest.function_call = request.functionCall; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), (this.config.timeout || 60000) * 2); try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: this.headers, body: JSON.stringify(openAIRequest), signal: controller.signal, }); if (!response.ok) { await this.handleErrorResponse(response); } const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; let totalPromptTokens = 0; let totalCompletionTokens = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const chunk: OpenAIStreamChunk = JSON.parse(data); const delta = chunk.choices[0].delta; if (delta.content) { yield { type: 'content', delta: { content: delta.content }, }; } if (delta.function_call) { yield { type: 'function_call', delta: { functionCall: delta.function_call }, }; } if (chunk.choices[0].finish_reason) { // Estimate tokens for streaming const promptTokens = this.estimateTokens(JSON.stringify(request.messages)); const completionTokens = Math.max(totalCompletionTokens, 100); // Minimum estimate const model = request.model || this.config.model; const pricing = this.capabilities.pricing![model]; const promptCost = (promptTokens / 1000) * pricing.promptCostPer1k; const completionCost = (completionTokens / 1000) * pricing.completionCostPer1k; yield { type: 'done', usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens, }, cost: { promptCost, completionCost, totalCost: promptCost + completionCost, currency: 'USD', }, }; } } catch (e) { this.logger.warn('Failed to parse OpenAI stream chunk', { data, error: e }); } } } } } catch (error) { clearTimeout(timeout); throw this.transformError(error); } finally { clearTimeout(timeout); } } async listModels(): Promise<LLMModel[]> { return this.capabilities.supportedModels; } async getModelInfo(model: LLMModel): Promise<ModelInfo> { return { model, name: model, description: this.getModelDescription(model), contextLength: this.capabilities.maxContextLength[model] || 4096, maxOutputTokens: this.capabilities.maxOutputTokens[model] || 4096, supportedFeatures: [ 'chat', 'completion', 'function_calling', ...(model.includes('gpt-4') ? ['vision'] : []), ], pricing: this.capabilities.pricing![model], }; } protected async doHealthCheck(): Promise<HealthCheckResult> { try { const response = await fetch(`${this.baseUrl}/models`, { headers: this.headers, }); if (!response.ok) { throw new Error(`Health check failed: ${response.status}`); } return { healthy: true, timestamp: new Date(), }; } catch (error) { return { healthy: false, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date(), }; } } private mapToOpenAIModel(model: LLMModel): string { // Map our model names to OpenAI model names if needed const modelMap: Record<string, string> = { 'gpt-4-turbo-preview': 'gpt-4-turbo-preview', 'gpt-4': 'gpt-4', 'gpt-4-32k': 'gpt-4-32k', 'gpt-3.5-turbo': 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k': 'gpt-3.5-turbo-16k', }; return modelMap[model] || model; } private mapFromOpenAIModel(model: string): LLMModel { // Ensure the model is in our supported list return this.capabilities.supportedModels.find((m) => m === model) || 'gpt-3.5-turbo'; } private getModelDescription(model: LLMModel): string { const descriptions: Record<string, string> = { 'gpt-4-turbo-preview': 'Latest GPT-4 Turbo model with improved performance', 'gpt-4': 'Most capable GPT-4 model for complex tasks', 'gpt-4-32k': 'GPT-4 with extended 32k context window', 'gpt-3.5-turbo': 'Fast and efficient model for most tasks', 'gpt-3.5-turbo-16k': 'GPT-3.5 Turbo with extended context', }; return descriptions[model] || 'OpenAI language model'; } private async handleErrorResponse(response: Response): Promise<void> { const errorText = await response.text(); let errorData: any; try { errorData = JSON.parse(errorText); } catch { errorData = { error: { message: errorText } }; } const message = errorData.error?.message || 'Unknown error'; switch (response.status) { case 401: throw new AuthenticationError(message, 'openai', errorData); case 429: const retryAfter = response.headers.get('retry-after'); throw new RateLimitError( message, 'openai', retryAfter ? parseInt(retryAfter) : undefined, errorData ); case 404: throw new ModelNotFoundError(this.config.model, 'openai', errorData); default: throw new LLMProviderError( message, `OPENAI_${response.status}`, 'openai', response.status, response.status >= 500, errorData ); } } }