From a93b162c0a02423edf5cd96ec63536207edb5ba7 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 23 Sep 2025 18:28:14 +0000 Subject: [PATCH] feat: add automatic web search support for OpenAI Native provider - Add openAiNativeWebSearchEnabled configuration option to provider settings - Implement web search tool integration in Responses API requests - Handle web search events and store results for citation formatting - Add citation formatting with clickable links in markdown format - Display search status updates during web search operations - Append sources list with numbered citations at end of response This enables automatic web search for OpenAI Native provider when the openAiNativeWebSearchEnabled option is set to true. The model will automatically search the web when helpful and include inline citations with a Sources list containing clickable links. --- packages/types/src/provider-settings.ts | 2 + src/api/providers/openai-native.ts | 66 ++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 8434158541..bd28014fc7 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -297,6 +297,8 @@ const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ // OpenAI Responses API service tier for openai-native provider only. // UI should only expose this when the selected model supports flex/priority. openAiNativeServiceTier: serviceTierSchema.optional(), + // Enable automatic web search for OpenAI Native provider + openAiNativeWebSearchEnabled: z.boolean().optional(), }) const mistralSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 8a205a06b4..3c366f4687 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -39,6 +39,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio private responseIdResolver: ((value: string | undefined) => void) | undefined // Resolved service tier from Responses API (actual tier used by OpenAI) private lastServiceTier: ServiceTier | undefined + // Store web search results for citation formatting + private lastWebSearchResults: any[] | undefined // Event types handled by the shared event processor to avoid duplication private readonly coreHandledEventTypes = new Set([ @@ -245,6 +247,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio store?: boolean instructions?: string service_tier?: ServiceTier + tools?: Array<{ type: string }> } // Validate requested tier against model support; if not supported, omit. @@ -283,6 +286,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio (requestedTier === "default" || allowedTierNames.has(requestedTier)) && { service_tier: requestedTier, }), + // Enable web search if configured + ...(this.options.openAiNativeWebSearchEnabled && { + tools: [{ type: "web_search" }], + }), } // Include text.verbosity only when the model explicitly supports it @@ -887,11 +894,26 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } // Handle web search events else if (parsed.type === "response.web_search_call.searching") { - // Web search in progress + // Web search in progress - could yield status update + if (parsed.query) { + yield { + type: "text", + text: `\n[Searching web for: "${parsed.query}"]\n`, + } + } } else if (parsed.type === "response.web_search_call.in_progress") { // Processing web search results } else if (parsed.type === "response.web_search_call.completed") { - // Web search completed + // Web search completed - results will be included in the response + if (parsed.results && Array.isArray(parsed.results)) { + // Store search results for citation formatting + this.lastWebSearchResults = parsed.results + } + } else if (parsed.type === "response.web_search_call.results") { + // Alternative event for web search results + if (parsed.results && Array.isArray(parsed.results)) { + this.lastWebSearchResults = parsed.results + } } // Handle code interpreter events else if (parsed.type === "response.code_interpreter_call_code.delta") { @@ -1016,6 +1038,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } } + // After content is complete, append citations if we have web search results + if (this.lastWebSearchResults && this.lastWebSearchResults.length > 0) { + yield* this.formatWebSearchCitations(this.lastWebSearchResults) + // Clear results after using them + this.lastWebSearchResults = undefined + } + // Usage for done/completed is already handled by processGpt5Event in SDK path. // For SSE path, usage often arrives separately; avoid double-emitting here. } @@ -1304,6 +1333,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } } + // Enable web search if configured + if (this.options.openAiNativeWebSearchEnabled) { + requestBody.tools = [{ type: "web_search" }] + } + // Only include temperature if the model supports it if (model.info.supportsTemperature !== false) { requestBody.temperature = @@ -1352,4 +1386,32 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio throw error } } + + /** + * Formats web search results as citations with clickable links + */ + private async *formatWebSearchCitations(results: any[]): ApiStream { + // Format citations with inline links + const citations: string[] = [] + const sources: string[] = [] + + for (let i = 0; i < results.length; i++) { + const result = results[i] + if (result.url && result.title) { + const citationNum = i + 1 + // Create inline citation reference + citations.push(`[${citationNum}]`) + // Create source entry with clickable link + sources.push(`[${citationNum}] [${result.title}](${result.url})`) + } + } + + // Only output if we have citations + if (sources.length > 0) { + yield { + type: "text", + text: "\n\n**Sources:**\n" + sources.join("\n"), + } + } + } }