-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(web-search): opt-in hosted web-search bridge for the key-auth Responses passthrough #4142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
024e43d
38fc4f6
e748566
3557ada
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,7 @@ import { | |
| MODEL_ADAPTER_OVERRIDE_ALLOWED, | ||
| OPENAI_PROVIDER_TIER_VERSION, | ||
| pinnedWireAdapter, | ||
| PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS, | ||
| UPSTREAM_HTTP_VERSION_VALUES, | ||
| type OcxClaudeCodeConfig, | ||
| type OcxConfig, | ||
|
|
@@ -497,6 +498,47 @@ export function requestPacingConfigError(value: unknown): string | null { | |
| return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides"; | ||
| } | ||
|
|
||
| /** | ||
| * Bounds for the opt-in passthrough web-search bridge (`providers.<name>.webSearchBridge`, | ||
| * #3761). Strict for the same reason `retryOn429` is: a misspelled key here would silently | ||
| * leave the bridge disarmed while the operator believes they enabled it. `endpoint` is only | ||
| * shape-checked here; `planPassthroughWebSearchBridge` re-validates the origin before any key | ||
| * is sent to it, because config validation is not an authorization boundary. | ||
| */ | ||
| const providerWebSearchBridgeSchema = z.object({ | ||
| enabled: z.boolean().optional(), | ||
| backend: z.enum(PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS).optional(), | ||
| maxSearches: z.number().int().min(1).max(10).optional(), | ||
| timeoutMs: z.number().int().min(1_000).max(600_000).optional(), | ||
| endpoint: z.string().min(1).optional(), | ||
| }).strict(); | ||
|
|
||
| export function providerWebSearchBridgeConfigError(value: unknown): string | null { | ||
| if (value === undefined) return null; | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) { | ||
| return "webSearchBridge must be a plain object"; | ||
| } | ||
| const parsed = providerWebSearchBridgeSchema.safeParse(value); | ||
| if (!parsed.success) { | ||
| return "webSearchBridge accepts only enabled (boolean), backend " | ||
| + `(${PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS.join("|")}), maxSearches (1..10), ` | ||
| + "timeoutMs (1000..600000), and endpoint (absolute http(s) URL)"; | ||
| } | ||
| const endpoint = parsed.data.endpoint; | ||
| if (endpoint !== undefined) { | ||
| let url: URL; | ||
| try { | ||
| url = new URL(endpoint); | ||
| } catch { | ||
| return "webSearchBridge.endpoint must be an absolute http(s) URL"; | ||
| } | ||
| if (url.protocol !== "https:" && url.protocol !== "http:") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/web-search/ollama-executor.ts --items all
rg -n -C 5 'endpoint|fetch\(|Authorization|apiKey|redirect' \
src/web-search/ollama-executor.ts src/web-search/passthrough-bridge.tsRepository: lidge-jun/opencodex Length of output: 10889 Sensitive Data Exposure Reachability: External Reachability pathRequire HTTPS for the credential-bearing search endpoint.
Reject Proposed fix- + "timeoutMs (1000..600000), and endpoint (absolute http(s) URL)";
+ + "timeoutMs (1000..600000), and endpoint (absolute https URL)";
...
- return "webSearchBridge.endpoint must be an absolute http(s) URL";
+ return "webSearchBridge.endpoint must be an absolute https URL";
...
- if (url.protocol !== "https:" && url.protocol !== "http:") {
- return "webSearchBridge.endpoint must be an absolute http(s) URL";
+ if (url.protocol !== "https:") {
+ return "webSearchBridge.endpoint must be an absolute https URL";🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| return "webSearchBridge.endpoint must be an absolute http(s) URL"; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| const fastWireSchema = z.object({ | ||
| kind: z.string(), | ||
| canonicalToWire: z.record(z.string().trim(), z.string().trim()), | ||
|
|
@@ -600,6 +642,10 @@ const providerConfigSchema = z.object({ | |
| repairInvalidIds: z.boolean().optional(), | ||
| }).strict().optional(), | ||
| responsesSnapshotRepair: z.boolean().optional(), | ||
| // Invalid blocks degrade to "absent" rather than failing the whole config load: an unusable | ||
| // bridge block must never send an operator through invalid-config recovery for an opt-in | ||
| // feature that is off by default. The management write boundary still rejects it loudly. | ||
| webSearchBridge: providerWebSearchBridgeSchema.optional().catch(undefined), | ||
| xaiResponsesXSearch: z.boolean().optional(), | ||
| xaiResponsesDefaultVersion: z.number().int().positive().optional().catch(undefined), | ||
| }).passthrough(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,6 +150,11 @@ import { | |
| } from "../../oauth/generic-account-failover"; | ||
| import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot"; | ||
| import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search"; | ||
| import { | ||
| createOllamaBridgeExecutor, | ||
| createPassthroughWebSearchBridgeStream, | ||
| planPassthroughWebSearchBridge, | ||
| } from "../../web-search/passthrough-bridge"; | ||
| import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images"; | ||
| import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision"; | ||
| import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue"; | ||
|
|
@@ -5762,15 +5767,60 @@ async function handleResponsesInner( | |
| route.provider, | ||
| route.modelId, | ||
| ); | ||
| // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool, | ||
| // and this branch relays that declaration on the assumption the destination executes it. | ||
| // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named | ||
| // web_search that nothing runs, and the undeclared-tool guard below ends the turn. When the | ||
| // provider opts in, the bridge intercepts that one call, runs the search, continues the | ||
| // conversation upstream, and hands back ordinary Responses SSE — so every rewrite below, | ||
| // including the guard itself, still inspects the client-facing stream. Default OFF: without | ||
| // the opt-in this is one planner call and the relay is byte-identical to before. | ||
| const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, { | ||
| isPassthrough: true, | ||
| stream: parsed.stream === true, | ||
| }); | ||
| // The bridge wraps the RAW upstream body, so terminal repair below still owns the single | ||
| // client-facing terminal — the bridge drops the terminal of every intercepted leg. | ||
| const upstreamSseBody = webSearchBridgePlan | ||
| ? createPassthroughWebSearchBridgeStream({ | ||
| plan: webSearchBridgePlan, | ||
| firstLeg: upstreamResponse.body, | ||
| requestBody: request.body, | ||
| // Continuation legs replay the same built request with the executed search appended. | ||
| // The first leg already passed the recovery ladder, the outbound size ceiling, and the | ||
| // host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg. | ||
| send: (continuationBody: string) => fetchWithHeaderTimeout( | ||
| request.url, | ||
| { method: request.method, headers: request.headers, body: continuationBody }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Description: Determine whether the openai-responses adapter sets content-length/content-encoding in AdapterRequest.headers.
set -euo pipefail
# Locate the openai-responses adapter implementation.
fd -t f . src/adapters --exec rg -ln 'openai-responses'
# Inspect header construction in that adapter and any shared header builder.
rg -n -C 6 -i 'content-length|content-encoding' src/adapters src/providers
# Confirm the AdapterRequest headers type and who populates it.
rg -n -C 8 'interface AdapterRequest' srcRepository: lidge-jun/opencodex Length of output: 295 🤖 get_repo_knowledge executed:
Length of output: 10864 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- adapter files ---'
fd -t f . src/adapters | rg 'openai-responses|adapter|types|request'
printf '%s\n' '--- openai-responses adapter definitions and header construction ---'
for f in src/adapters/openai-responses.ts src/adapters/openai-responses-url.ts; do
if [ -f "$f" ]; then
printf '\n### %s\n' "$f"
ast-grep outline "$f"
rg -n -C 12 -i 'headers|content-length|content-encoding|AdapterRequest|buildRequest' "$f"
fi
done
printf '%s\n' '--- request type and relevant call sites ---'
rg -n -C 10 'interface AdapterRequest|type AdapterRequest|AdapterRequest' src
rg -n -C 12 'continuationBody|fetchWithHeaderTimeout' src/server/responses/core.tsRepository: lidge-jun/opencodex Length of output: 19377 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '### openai-responses.ts'
sed -n '1,260p' src/adapters/openai-responses.ts
printf '%s\n' '### openai-responses-url.ts'
sed -n '1,260p' src/adapters/openai-responses-url.ts
printf '%s\n' '### matching declarations and call sites'
rg -n -C 10 'AdapterRequest|content-length|content-encoding|continuationBody|fetchWithHeaderTimeout' src/adapters src/server/responses/core.tsRepository: lidge-jun/opencodex Length of output: 50376 🤖 get_repo_knowledge executed:
Length of output: 3973 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '### AdapterRequest contract'
sed -n '1,180p' src/adapters/base.ts
printf '%s\n' '### openai-responses request construction'
sed -n '2300,2570p' src/adapters/openai-responses.ts
printf '%s\n' '### provider header definition and normalization'
rg -n -C 12 'headers\??:|headers: Record|provider\.headers|Object\.assign\(headers|content-length|content-encoding' src/types.ts src/config.ts src/providers src/adapters src/server --glob '*.ts' | head -n 260Repository: lidge-jun/opencodex Length of output: 36789 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '### AdapterRequest contract'
sed -n '1,180p' src/adapters/base.ts
printf '%s\n' '### provider header declarations and use'
rg -n -C 10 'headers\??:|headers: Record|provider\.headers|Object\.assign\(headers|content-length|content-encoding' src/types.ts src/config.ts src/providers src/adapters --glob '*.ts'Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '### provider header validation'
rg -n -C 18 'function providerHeadersConfigError|providerHeadersConfigError|headers\?: Record<string, string>' src/config.ts src/types.tsRepository: lidge-jun/opencodex Length of output: 6439 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 25 'providerHeadersConfigError' src
rg -n -C 5 'headers\?:' src/types.tsRepository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,130p' src/config/provider-validation.tsRepository: lidge-jun/opencodex Length of output: 6135 Strip transport metadata before sending continuation bodies
🛡️ Defensive fix- send: (continuationBody: string) => fetchWithHeaderTimeout(
- request.url,
- { method: request.method, headers: request.headers, body: continuationBody },
+ send: (continuationBody: string) => fetchWithHeaderTimeout(
+ request.url,
+ {
+ method: request.method,
+ headers: (() => {
+ const headers = new Headers(request.headers);
+ headers.delete("content-length");
+ headers.delete("content-encoding");
+ return headers;
+ })(),
+ body: continuationBody,
+ },🤖 Prompt for AI Agents |
||
| upstream.signal, | ||
| connectMs, | ||
| true, | ||
| providerFetch(route.provider, options.codexWsRuntimeIdentity, { | ||
| dispatchOverride: oauthDispatch(request), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Description: Confirm the API-key rotation branch of oauthDispatch rebuilds the body and can fire for key-auth providers.
set -euo pipefail
# The selection predicate used for api-key bindings.
rg -n -C 12 'providerApiKeySelectionIsCurrent' src
# The rebuild-and-replace branch inside oauthDispatch.
rg -n -C 6 'Object.assign\(wireRequest, rebuilt\)|dispatchInit = \{ \.\.\.dispatchInit' src/server/responses/core.ts
# Existing bridge continuation coverage: does any test assert the continuation body contents?
fd -t f 'web-search-passthrough-bridge.test.ts' tests --exec rg -n -C 4 'function_call_output|continuation'Repository: lidge-jun/opencodex Length of output: 15204 🤖 get_repo_knowledge executed:
Length of output: 8686 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- oauthDispatch and bridge executor ---'
sed -n '4125,4210p' src/server/responses/core.ts
sed -n '5765,5825p' src/server/responses/core.ts
printf '%s\n' '--- relevant bridge test ---'
sed -n '270,315p' tests/web-search/web-search-passthrough-bridge.test.ts
printf '%s\n' '--- providerFetch dispatch contract ---'
rg -n -C 18 'function providerFetch|const providerFetch|dispatchOverride' src/server/responses/fetch-helpers.ts src/server/responses/core.tsRepository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- bridge executor definition and key binding ---'
rg -n -C 20 'createOllamaBridgeExecutor|planPassthroughWebSearchBridge|requestBindings\.set' src/server/responses/core.ts src/server/responses
printf '%s\n' '--- bridge tests around credential failure and continuation ---'
sed -n '470,535p' tests/web-search/web-search-passthrough-bridge.test.ts
sed -n '590,635p' tests/web-search/web-search-passthrough-bridge.test.tsRepository: lidge-jun/opencodex Length of output: 39840 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 16 'export function createOllamaBridgeExecutor|function createOllamaBridgeExecutor|createOllamaBridgeExecutor' src/web-search/passthrough-bridge.tsRepository: lidge-jun/opencodex Length of output: 1473 Keep continuation bodies separate from credential refresh. At At Do not pass 🤖 Prompt for AI Agents |
||
| providerName: route.providerName, | ||
|
Comment on lines
+5791
to
+5800
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Route bridge continuation sends through bounded upstream recovery At Wrap each continuation send in the same bounded retry policy. Pass 🤖 Prompt for AI Agents |
||
| modelId: route.modelId, | ||
| }), | ||
| false, | ||
| ), | ||
| execute: createOllamaBridgeExecutor(webSearchBridgePlan, route.provider.apiKey ?? ""), | ||
| // Appending a search result can push the continuation past the ceiling the first leg | ||
| // was admitted under, so the same limit is re-applied before every later send. | ||
| checkOutboundBody: (continuationBody: string) => { | ||
| const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes); | ||
| return result.admitted ? undefined : describeOutboundBodyRefusal(result); | ||
| }, | ||
| signal: upstream.signal, | ||
| }) | ||
| : upstreamResponse.body; | ||
| const passthroughSseBody = terminalRepairPolicy | ||
| ? relayResponsesSseWithTerminalRepair( | ||
| upstreamResponse.body, | ||
| upstreamSseBody, | ||
| upstream, | ||
| terminalRepairPolicy, | ||
| translatorBudget, | ||
| options.responsesTerminalRepairScheduler, | ||
| ) | ||
| : upstreamResponse.body; | ||
| : upstreamSseBody; | ||
| const repairConfig = route.provider.responsesItemIdRepair; | ||
| // Grok Build renders deltas live but reconstructs its durable assistant | ||
| // turn from the completed response snapshot. Native Responses streams | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 8270
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50376
🤖 get_repo_knowledge executed:
get_repo_knowledge lidge-jun/opencodex /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/learnings /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventionsLength of output: 16857
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 23333
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 33020
Align the
webSearchBridgerow with the shipped schema and planner.The
backendfield acceptsollama,anthropic,xai,gemini, andexa, but onlyollamahas a shipped executor. List the full union in the type column.The planner does not check
xaiResponsesXSearchor the provider destination. The built-inxaiprovider permits key-auth override, and an explicitendpointcan arm the bridge for that provider. Remove “Never armed ... for a provider that executes hosted search,” or add the exclusion toplanPassthroughWebSearchBridgeinsrc/web-search/passthrough-bridge.ts.🤖 Prompt for AI Agents
Source: Path instructions