research-cli
Version:
AI-powered research assistant with web search capabilities and beautiful terminal UI
150 lines • 5.79 kB
JavaScript
/**
* 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