From fe93de79ae0da45a6c3cb5e37fd4a9f43cbd24dd Mon Sep 17 00:00:00 2001 From: Nikhil-Doye Date: Mon, 20 Oct 2025 14:18:06 -0400 Subject: [PATCH] Enhance OpenAI service with proxy support and error handling - Introduced a method to retrieve an optional proxy base URL for production use, improving security by allowing API calls to be routed through a backend. - Updated error handling to provide clear messages when the API key is not configured or when the proxy is not set in production. - Refactored the web scraping processor to handle undefined configuration values more gracefully, ensuring default behaviors are maintained. --- src/services/openaiService.ts | 100 +++++++++++++----- .../processors/webScrapingProcessor.ts | 3 +- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/services/openaiService.ts b/src/services/openaiService.ts index 7130b7b..5e38885 100644 --- a/src/services/openaiService.ts +++ b/src/services/openaiService.ts @@ -9,11 +9,19 @@ const getApiKey = (): string => { ); }; -// Initialize OpenAI client for DeepSeek +// Optional proxy base URL (recommended for production) +const getProxyBaseUrl = (): string | null => { + const fromStorage = localStorage.getItem("api_base_url"); + const fromEnv = process.env.REACT_APP_API_BASE; + return fromStorage || fromEnv || null; +}; + +// Initialize OpenAI client for DeepSeek (fallback-only; avoid in production) const openai = new OpenAI({ apiKey: getApiKey(), - baseURL: "https://api.deepseek.com/v1", // DeepSeek API endpoint - dangerouslyAllowBrowser: true, // Only for development - in production, use a backend + baseURL: "https://api.deepseek.com/v1", + // Keep browser use only as a last-resort fallback during local dev + dangerouslyAllowBrowser: true, }); export interface OpenAIConfig { @@ -36,29 +44,47 @@ export const callOpenAI = async ( config: OpenAIConfig ): Promise => { try { - // Check if API key is configured - const apiKey = getApiKey(); - if ( - !apiKey || - apiKey === "your_openai_api_key_here" || - apiKey === "your_deepseek_api_key_here" - ) { + // Prefer secure proxy in production + const proxyBase = getProxyBaseUrl(); + if (proxyBase) { + const resp = await fetch(`${proxyBase.replace(/\/$/, "")}/llm/chat`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt, config }), + }); + + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`Proxy error: ${resp.status} ${text}`); + } + + const data = await resp.json(); + return { + content: data.content || "", + usage: data.usage, + }; + } + + // Fallback (dev only): direct browser call (keys are exposed!) + if (process.env.NODE_ENV === "production") { throw new Error( - "DeepSeek API key not configured. Please configure your API key in the settings." + "LLM proxy not configured. In production, configure REACT_APP_API_BASE or localStorage 'api_base_url' to a secure backend." ); } - // Update the OpenAI client with the current API key - openai.apiKey = apiKey; + const apiKey = getApiKey(); + if (!apiKey) { + throw new Error( + "DeepSeek API key not configured. Set it in settings for local development, or configure a proxy for production." + ); + } + openai.apiKey = apiKey; const response = await openai.chat.completions.create({ model: config.model, - messages: [ - { - role: "user", - content: prompt, - }, - ], + messages: [{ role: "user", content: prompt }], temperature: config.temperature, max_tokens: config.maxTokens || 1000, }); @@ -90,20 +116,38 @@ export const generateEmbedding = async ( text: string, model: string = "text-embedding-ada-002" ): Promise => { - const apiKey = getApiKey(); - if (!apiKey || apiKey === "your_openai_api_key_here") { + // Prefer secure proxy in production + const proxyBase = getProxyBaseUrl(); + if (proxyBase) { + const resp = await fetch(`${proxyBase.replace(/\/$/, "")}/llm/embeddings`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text, model }), + }); + if (!resp.ok) { + const textBody = await resp.text(); + throw new Error(`Proxy error: ${resp.status} ${textBody}`); + } + const data = await resp.json(); + return data.embedding as number[]; + } + + // Fallback (dev only) + if (process.env.NODE_ENV === "production") { throw new Error( - "OpenAI API key not configured. Please configure your API key in the settings." + "Embedding proxy not configured. In production, configure REACT_APP_API_BASE or localStorage 'api_base_url' to a secure backend." + ); + } + + const apiKey = getApiKey(); + if (!apiKey) { + throw new Error( + "OpenAI API key not configured. Set it in settings for local development, or configure a proxy for production." ); } openai.apiKey = apiKey; - - const response = await openai.embeddings.create({ - model, - input: text, - }); - + const response = await openai.embeddings.create({ model, input: text }); return response.data[0].embedding; }; diff --git a/src/services/processors/webScrapingProcessor.ts b/src/services/processors/webScrapingProcessor.ts index b89639c..c78dd6c 100644 --- a/src/services/processors/webScrapingProcessor.ts +++ b/src/services/processors/webScrapingProcessor.ts @@ -11,7 +11,8 @@ export default async function webScrapingProcessor( const result = await scrapeWithFirecrawl({ url: config.url, formats: config.formats || ["markdown"], - onlyMainContent: config.onlyMainContent || true, + onlyMainContent: + config.onlyMainContent !== undefined ? config.onlyMainContent : true, maxLength: config.maxLength || 10000, });