UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

489 lines 21.6 kB
import { BaseProvider } from './base.js'; export class OpenAIProvider extends BaseProvider { name = 'openai'; models = [ 'gpt-4o', 'gpt-4.1', 'o3', 'o4-mini', ]; supportsStreaming = true; supportsTools = true; reasoningModels = ['o3', 'o4-mini']; pricing = { 'gpt-4o': { prompt: 2.50, completion: 10.00 }, 'gpt-4.1': { prompt: 2.00, completion: 8.00 }, 'o3': { prompt: 2.00, completion: 8.00 }, 'o4-mini': { prompt: 1.10, completion: 4.40 }, }; constructor(apiKey, baseUrl) { super(apiKey, baseUrl); } async complete(messages, options = {}) { this.validateMessages(messages); const model = options.model || 'gpt-4o'; const isReasoningModel = this.reasoningModels.includes(model); try { const requestBody = { model, input: messages.length === 1 && messages[0].role === 'user' && typeof messages[0].content === 'string' ? messages[0].content : this.formatMessagesForResponsesAPI(messages), temperature: options.temperature, max_tokens: options.maxTokens }; if (options.tools && options.tools.length > 0) { requestBody.tools = this.formatTools(options.tools); requestBody.tool_choice = 'auto'; } if (isReasoningModel) { requestBody.reasoning = { effort: 'high', summary: 'auto' }; } const response = await fetch(this.baseUrl || 'https://api.openai.com/v1/responses', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`); } const data = await response.json(); return this.parseResponsesAPIOutput(data, model); } catch (error) { if (error.message?.includes('429')) { await this.handleRateLimit(60); return this.complete(messages, options); } throw new Error(`OpenAI API error: ${error.message}`); } } async *stream(messages, options = {}) { this.validateMessages(messages); const model = options.model || 'gpt-4o'; const isReasoningModel = this.reasoningModels.includes(model); if (isReasoningModel) { try { const response = await this.complete(messages, { ...options, stream: false }); if (response.reasoning) { if (options.signal?.aborted) return; yield { delta: '💭 Thinking: ' + response.reasoning + '\n\n', reasoning: true, usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 } }; } if (response.toolCalls && response.toolCalls.length > 0) { for (const toolCall of response.toolCalls) { yield { delta: '', toolCall: toolCall }; } } const chunkSize = 20; for (let i = 0; i < response.content.length; i += chunkSize) { if (options.signal?.aborted) return; const chunk = response.content.slice(i, i + chunkSize); yield { delta: chunk, usage: { promptTokens: 0, completionTokens: Math.ceil(i / 4), totalTokens: Math.ceil(i / 4) } }; await new Promise((resolve, reject) => { const timeout = setTimeout(resolve, 30); if (options.signal) { const abortHandler = () => { clearTimeout(timeout); reject(new DOMException('Aborted', 'AbortError')); }; if (options.signal.aborted) { abortHandler(); } else { options.signal.addEventListener('abort', abortHandler, { once: true }); } } }); } return; } catch (error) { if (error.name === 'AbortError') { return; } throw error; } } const requestBody = { model, input: messages.length === 1 && messages[0].role === 'user' && typeof messages[0].content === 'string' ? messages[0].content : this.formatMessagesForResponsesAPI(messages), temperature: options.temperature, max_tokens: options.maxTokens, stream: true }; if (options.tools && options.tools.length > 0) { requestBody.tools = this.formatTools(options.tools); requestBody.tool_choice = 'auto'; } if (isReasoningModel) { requestBody.reasoning = { effort: 'high', summary: 'auto' }; } try { if (process.env.DEBUG) { console.log('OpenAI Responses API Request:', JSON.stringify(requestBody, null, 2)); if (requestBody.tools) { console.log('Tools being sent:', JSON.stringify(requestBody.tools, null, 2)); } } const response = await fetch(this.baseUrl || 'https://api.openai.com/v1/responses', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), signal: options.signal }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`); } if (!response.body) { throw new Error('No response body'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let totalTokens = 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 parsed = JSON.parse(data); if (process.env.DEBUG && parsed.type && !['response.function_call_arguments.delta', 'response.output_item.added', 'response.output_item.done', 'response.text.delta', 'response.content_part.delta', 'response.done'].includes(parsed.type)) { console.log('Unhandled streaming event type:', parsed.type); } if (parsed.type === 'response.done') { const usage = parsed.response?.usage || parsed.usage; if (usage) { yield { delta: '', usage: { promptTokens: usage.input_tokens || 0, completionTokens: usage.output_tokens || 0, totalTokens: usage.total_tokens || 0 } }; } } if (parsed.type === 'response.function_call_arguments.delta') { } else if (parsed.type === 'response.output_item.added' && parsed.item?.type === 'function_call') { } else if (parsed.type === 'response.output_item.done' && parsed.item?.type === 'function_call') { const args = parsed.item.arguments ? JSON.parse(parsed.item.arguments) : {}; yield { delta: '', toolCall: { id: parsed.item.call_id || parsed.item.id, name: parsed.item.name, arguments: args } }; } else if (parsed.type === 'response.output_item.added' && parsed.item?.type === 'message') { if (parsed.item.content && parsed.item.content[0]?.text) { const text = parsed.item.content[0].text; totalTokens += this.countTokens(text); yield { delta: text, usage: { promptTokens: 0, completionTokens: totalTokens, totalTokens } }; } } else if (parsed.type === 'response.output_item.done' && parsed.item?.type === 'message') { if (parsed.item.content && parsed.item.content[0]?.text) { const text = parsed.item.content[0].text; totalTokens += this.countTokens(text); yield { delta: text, usage: { promptTokens: 0, completionTokens: totalTokens, totalTokens } }; } } else if (parsed.type === 'response.text.delta' && parsed.delta) { totalTokens += this.countTokens(parsed.delta); yield { delta: parsed.delta, usage: { promptTokens: 0, completionTokens: totalTokens, totalTokens } }; } else if (parsed.type === 'response.content_part.delta' && parsed.delta?.text) { totalTokens += this.countTokens(parsed.delta.text); yield { delta: parsed.delta.text, usage: { promptTokens: 0, completionTokens: totalTokens, totalTokens } }; } else if (parsed.choices && parsed.choices[0]) { const choice = parsed.choices[0]; if (choice.delta?.content) { totalTokens += this.countTokens(choice.delta.content); yield { delta: choice.delta.content, usage: { promptTokens: 0, completionTokens: totalTokens, totalTokens } }; } } } catch (e) { console.error('Failed to parse streaming chunk:', e); } } } } } catch (error) { if (error.name === 'AbortError') { throw error; } if (isReasoningModel && error.message?.includes('streaming')) { const response = await this.complete(messages, options); if (response.reasoning) { if (options.signal?.aborted) return; yield { delta: '💭 Thinking: ' + response.reasoning + '\n\n', reasoning: true, usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 } }; } const chunkSize = 20; for (let i = 0; i < response.content.length; i += chunkSize) { if (options.signal?.aborted) return; const chunk = response.content.slice(i, i + chunkSize); yield { delta: chunk, usage: { promptTokens: 0, completionTokens: Math.ceil(i / 4), totalTokens: Math.ceil(i / 4) } }; await new Promise((resolve, reject) => { const timeout = setTimeout(resolve, 30); if (options.signal) { options.signal.addEventListener('abort', () => { clearTimeout(timeout); reject(new DOMException('Aborted', 'AbortError')); }, { once: true }); } }); } return; } throw error; } } calculateCost(usage, model = 'gpt-4o') { const pricing = this.pricing[model] || this.pricing['gpt-4o']; const promptCost = (usage.promptTokens / 1000000) * pricing.prompt; const completionCost = (usage.completionTokens / 1000000) * pricing.completion; return { amount: promptCost + completionCost, currency: 'USD', breakdown: { prompt: promptCost, completion: completionCost } }; } formatTools(tools) { return tools; } formatMessageContent(content) { if (typeof content === 'string') { return content; } if (content.length === 1 && content[0].type === 'text') { return content[0].text; } const formatted = []; for (const item of content) { switch (item.type) { case 'text': formatted.push({ type: 'input_text', text: item.text }); break; case 'image': formatted.push({ type: 'input_image', image_url: `data:${item.source.media_type};base64,${item.source.data}` }); break; case 'file': formatted.push({ type: 'input_file', file_url: `data:${item.media_type || 'application/pdf'};base64,${item.data}` }); break; } } return formatted; } formatMessagesForResponsesAPI(messages) { const formatted = []; const toolCallIds = new Set(); const toolResultIds = new Set(); for (const msg of messages) { if (msg.tool_calls) { for (const toolCall of msg.tool_calls) { toolCallIds.add(toolCall.id); } } if (msg.role === 'tool_result' && msg.tool_call_id) { toolResultIds.add(msg.tool_call_id); } } const orphanedResults = Array.from(toolResultIds).filter(id => !toolCallIds.has(id)); if (orphanedResults.length > 0) { console.error('Warning: Found orphaned tool results without matching tool calls:', orphanedResults); console.error('Tool call IDs:', Array.from(toolCallIds)); console.error('Tool result IDs:', Array.from(toolResultIds)); } for (const msg of messages) { if (msg.role === 'tool_result' && msg.tool_call_id) { if (toolCallIds.has(msg.tool_call_id)) { formatted.push({ type: 'function_call_output', call_id: msg.tool_call_id, output: typeof msg.content === 'string' ? msg.content : this.getTextContent(msg) }); } else { console.error(`Skipping orphaned tool result with call_id: ${msg.tool_call_id}`); } } else if (msg.role === 'assistant') { if (msg.content && msg.content !== '') { formatted.push({ role: 'assistant', content: this.formatMessageContent(msg.content) }); } if (msg.tool_calls) { for (const toolCall of msg.tool_calls) { formatted.push({ type: 'function_call', call_id: toolCall.id, name: toolCall.name, arguments: typeof toolCall.arguments === 'string' ? toolCall.arguments : JSON.stringify(toolCall.arguments) }); } } } else { formatted.push({ role: msg.role, content: this.formatMessageContent(msg.content) }); } } return formatted; } parseResponsesAPIOutput(data, model) { let content = ''; let reasoningSummary = ''; let toolCalls = []; for (const item of (data.output || [])) { switch (item.type) { case 'message': if (item.content && item.content[0]) { content = item.content[0].text || ''; } break; case 'reasoning': if (item.summary && item.summary[0]) { reasoningSummary = item.summary[0].text || ''; } break; case 'function_call': toolCalls.push({ id: item.call_id || item.id, name: item.name, arguments: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments }); break; } } const usage = { promptTokens: data.usage?.input_tokens || 0, completionTokens: data.usage?.output_tokens || 0, totalTokens: data.usage?.total_tokens || 0 }; return { content, usage, model, provider: this.name, toolCalls: toolCalls.length > 0 ? toolCalls : undefined, reasoning: reasoningSummary || undefined }; } } //# sourceMappingURL=openai.js.map