Skip to content

feat(sse): route web_search requests to a configured model (#4481) - #4509

Merged
diegosouzapw merged 2 commits into
release/v3.8.33from
feat/websearch-tool-routing-4481
Jun 21, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.33from
feat/websearch-tool-routing-4481

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Layer 2 of #4481 (CCR-style Router.webSearch). Layer 1 (#4490) stopped MiniMax from 400ing on web_search_20250305 by falling back to OmniRoute's /v1/search. This adds the alternative an operator can opt into: route the whole request to a model that natively runs web search, the way claude-code-router's Router.webSearch does, while every non-search request stays on the default model.

Behavior

New optional setting webSearchRouteModel (a model string — provider,model / provider/model / alias / combo name). When a request carries a native web_search server tool (web_search, web_search_preview, or Anthropic's versioned web_search_20250305) and webSearchRouteModel is set, the request is routed to that model instead of the resolved default. Unset/empty = disabled (no behavior change).

  • The override lands at the request entrypoint before auto/combo resolution and the layer-1 web-search fallback, so the chosen target's own format + fallback handling applies (e.g. routing to a real Anthropic model keeps web_search_20250305 native via the existing Claude→Claude passthrough).
  • A custom function tool merely named web_search does not trigger routing (only the native server tool does).
  • Routing to the model you're already on is a no-op (no re-entry loop).

Implementation

  • New pure helper open-sse/services/webSearchRouting.ts (hasNativeWebSearchTool, resolveWebSearchRouteOverride) — no DB, unit-testable. Detection uses a web_search prefix match so the raw client tool web_search_20250305 is caught at the entrypoint (the exact-set check in webSearchFallback.ts only sees it post-normalization).
  • Thin wiring in src/sse/handlers/chat.ts, mirroring the adjacent T05 task-aware-routing override. Settings are read via getCachedSettings() only when a web-search tool is present (cheap on every other request).
  • webSearchRouteModel: z.string().max(200).optional() registered in settingsSchemas.ts.

Tests (TDD)

tests/unit/web-search-tool-routing-4481.test.ts — 13 cases (RED→GREEN): detection of plain/preview/versioned/future tool types, function-tool exclusion, malformed input; route/no-route across all gates (tool present, config set, blank, same-model no-op, trim, non-string); + source-guards asserting chat.ts wires the router and the Zod schema registers the key.

Validation

typecheck:core ✅ · typecheck:noimplicit:core ✅ · lint 0 err / 0 new warn ✅ · check:cycles ✅ · new tests 13/13 ✅ · layer-1 regression web-search-fallback-format 15/15 ✅ (this composes with, doesn't replace, the #4490 fallback) · response-model-echo 5/5.

check:file-size: chat.ts rebaselined 1491→1513 with justification. Note: 4 unrelated files (core.ts, usage/providerLimits.ts, constants/providers.ts, services/usage.ts) are flagged by check:file-size but are pre-existing drift on the base (origin/release/v3.8.32) from concurrent merges — untouched here (delta-0), to be reconciled separately.

Follow-ups (not in this PR)

  • A dashboard field for webSearchRouteModel in the Routing settings tab (the setting is already usable via the settings API).
  • Generalizing to a per-tool byTool map if other server tools need it (web_search is the only one with this problem today).

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements layer 2 web-search routing, which overrides the target model to a configured web-search-capable model when a native web-search tool is detected. The reviewer identified a high-severity security risk where this override occurs after API key policy enforcement, allowing clients to bypass restrictions and route requests to expensive models. They provided a code suggestion to re-evaluate the API key policy against the overridden model.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/sse/handlers/chat.ts
Comment on lines +413 to +424
if (hasNativeWebSearchTool(body)) {
const wsSettings = await getCachedSettings().catch(() => ({}) as Record<string, unknown>);
const wsRoute = resolveWebSearchRouteOverride(resolvedModelStr, body, wsSettings);
if (wsRoute.wasRouted) {
log.info(
"WEBSEARCH-ROUTE",
`web_search tool → model override: ${resolvedModelStr} → ${wsRoute.model}`
);
resolvedModelStr = wsRoute.model;
body = { ...body, model: wsRoute.model };
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Security/Policy Bypass Risk

The web-search routing override occurs after the API key policy enforcement (enforceApiKeyPolicy at line 300). If a client requests a cheap/allowed model but includes a native web-search tool, the request will bypass the policy check and be routed to the configured webSearchRouteModel (which could be an expensive or restricted model like Claude 3.5 Sonnet).

To prevent this policy bypass, we should re-evaluate the API key policy against the new target model if a route override occurs.

Suggested change
if (hasNativeWebSearchTool(body)) {
const wsSettings = await getCachedSettings().catch(() => ({}) as Record<string, unknown>);
const wsRoute = resolveWebSearchRouteOverride(resolvedModelStr, body, wsSettings);
if (wsRoute.wasRouted) {
log.info(
"WEBSEARCH-ROUTE",
`web_search tool → model override: ${resolvedModelStr} → ${wsRoute.model}`
);
resolvedModelStr = wsRoute.model;
body = { ...body, model: wsRoute.model };
}
}
if (hasNativeWebSearchTool(body)) {
const wsSettings = await getCachedSettings().catch(() => ({}) as Record<string, unknown>);
const wsRoute = resolveWebSearchRouteOverride(resolvedModelStr, body, wsSettings);
if (wsRoute.wasRouted) {
const policyOverride = await enforceApiKeyPolicy(request, wsRoute.model);
if (policyOverride.rejection) {
log.warn(
"POLICY",
"API key policy rejected routed web-search model: " + wsRoute.model + " (key=" + (apiKeyInfo?.id || "unknown") + ")"
);
return policyOverride.rejection;
}
log.info(
"WEBSEARCH-ROUTE",
"web_search tool → model override: " + resolvedModelStr + " → " + wsRoute.model
);
resolvedModelStr = wsRoute.model;
body = { ...body, model: wsRoute.model };
}
}

@diegosouzapw
diegosouzapw force-pushed the feat/websearch-tool-routing-4481 branch from 4125430 to 3d4eb39 Compare June 21, 2026 15:05
@diegosouzapw
diegosouzapw merged commit 185c474 into release/v3.8.33 Jun 21, 2026
3 checks passed
@diegosouzapw diegosouzapw mentioned this pull request Jun 22, 2026
@diegosouzapw
diegosouzapw deleted the feat/websearch-tool-routing-4481 branch June 23, 2026 13:13
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…apw#4509, diegosouzapw#4481)

Routes requests carrying a web_search tool to a configured webSearchRouteModel (diegosouzapw#4481 layer-2).

Integrated into release/v3.8.33.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant