UNPKG

research-cli

Version:

AI-powered research assistant with web search capabilities and beautiful terminal UI

150 lines 5.79 kB
/** * OpenAI Responses API provider implementation */ import OpenAI from "openai"; import { getOpenAIApiKey } from "../utils/credentials.js"; export class OpenAIProvider { name = "openai"; supportsWebSearch = true; client = null; async getClient() { if (!this.client) { const apiKey = await getOpenAIApiKey(); this.client = new OpenAI({ apiKey, }); } return this.client; } async *streamQuery(input) { try { const model = input.model || "gpt-4o"; // Get the initialized client (this may prompt for API key) const client = await this.getClient(); // Emit start event const startEvent = { type: "start", data: { provider: this.name, model, timestamp: Date.now(), }, }; yield startEvent; // Build tools array for web search const tools = []; if (input.webSearch) { const webSearchTool = { type: "web_search", user_location: { type: "approximate", country: "US", }, search_context_size: input.webSearchContextSize || "low", }; tools.push(webSearchTool); } // CRITICAL: Use responses.stream() NOT chat.completions.create() const stream = await client.responses.stream({ model, input: input.query, tools: tools.length > 0 ? tools : undefined, stream: true, }); let fullAnswer = ""; const citations = []; // Process streaming events - using actual Responses API event types for await (const event of stream) { switch (event.type) { case "response.web_search_call.in_progress": { const toolCallEvent = { type: "tool_call", data: { name: "web_search", args: {}, }, }; yield toolCallEvent; break; } case "response.web_search_call.completed": { const toolResultEvent = { type: "tool_result", data: { name: "web_search", results: [], }, }; yield toolResultEvent; break; } case "response.output_text.delta": // CRITICAL: event.delta is a string, not event.delta.text if (event.delta) { fullAnswer += event.delta; const chunkEvent = { type: "chunk", data: { text: event.delta, }, }; yield chunkEvent; } break; case "response.completed": { // Extract usage data from the completed response const usageData = event.response?.usage; const finalEvent = { type: "final", data: { answer: fullAnswer, citations, usage: usageData ? { input_tokens: usageData.input_tokens || 0, output_tokens: usageData.output_tokens || 0, total_tokens: usageData.total_tokens || 0, reasoning_tokens: usageData.output_tokens_details?.reasoning_tokens || 0, } : undefined, }, }; yield finalEvent; break; } default: // Ignore other event types like response.created, response.in_progress, etc. break; } } } catch (error) { const errorEvent = { type: "error", data: { message: error instanceof Error ? error.message : "Unknown error occurred", code: this.getErrorCode(error), }, }; yield errorEvent; } } getErrorCode(error) { if (error instanceof OpenAI.APIError) { if (error.status === 401) return "API_KEY_MISSING"; if (error.status === 429) return "RATE_LIMITED"; if (error.status >= 500) return "PROVIDER_TIMEOUT"; } if (error instanceof Error) { if (error.message.includes("timeout")) return "PROVIDER_TIMEOUT"; if (error.message.includes("network")) return "NETWORK_ERROR"; } return "UNKNOWN_ERROR"; } } //# sourceMappingURL=openai.js.map