Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_21_v3833_cycle_open_latent_filesize": "Abertura do ciclo v3.8.33: 4 arquivos cresceram no ciclo v3.8.32 sem bump de baseline e o drift escapou do fast-path do release (check:file-size não roda nas fast-gates p/ release/*, só no PR→main full CI, e o crescimento veio de commits entre fca66c644 e o head do merge 912239f46 — ex. #4475 targetFormat). Medido em origin/main (idêntico, cherry-picks deste ciclo NÃO tocam estes 4): open-sse/services/usage.ts 3408->3414, src/lib/db/core.ts 1820->1825, src/lib/usage/providerLimits.ts 949->950, src/shared/constants/providers.ts 3242->3243. Reconcílio ao valor real de main p/ abrir o .33 verde; shrink estrutural rastreado em #3501.",
"_rebaseline_2026_06_21_4481_websearch_routing_ui": "Feature #4481 layer 2 UI own growth: src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx 1594->1629 (+35 = one Settings → Routing <Card> with a text <Input> bound to the `webSearchRouteModel` setting, placed next to the adjacent echo/LKGP cards; value/onChange via updateSetting, i18n labels with inline English fallbacks). Presentational wiring for the setting shipped in the same PR (#4509); the routing logic itself lives in the already-tested pure leaf open-sse/services/webSearchRouting.ts. Cohesive next to the existing routing cards; not extractable. Covered by tests/unit/web-search-tool-routing-4481.test.ts (UI source-guard + en.json key presence).",
"_rebaseline_2026_06_21_4481_websearch_routing": "Feature #4481 layer 2 own growth: src/sse/handlers/chat.ts 1491->1513 (+22 = the CCR-style web-search routing hook at the request entrypoint, right after the T05 task-aware-routing block — a 5-line comment + one `if (hasNativeWebSearchTool(body)) { ... }` guard that reads getCachedSettings only when a web_search tool is present and overrides resolvedModelStr/body.model via the pure helper, plus a 4-line import). When a request carries a native web_search server tool and the operator set `webSearchRouteModel`, the whole request routes to that model instead of the default (some providers, e.g. MiniMax, don't implement Anthropic's web_search_20250305 server tool). The detection + override logic lives in the new pure leaf open-sse/services/webSearchRouting.ts (no DB, unit-testable); chat.ts is thin wiring mirroring the adjacent T05 override. Lands BEFORE auto/combo resolution + the layer-1 webSearchFallback so the target's own format/fallback handling applies. Not extractable further (it IS the entrypoint wiring). Covered by tests/unit/web-search-tool-routing-4481.test.ts. Structural shrink of this handler tracked separately.",
"_rebaseline_2026_06_21_4483_auto_quota_cutoff": "PR #4483 (megamen32) own growth + review fix: open-sse/services/combo.ts 2611->2623 (+12). The PR adds an auto-routing hard quota cutoff in buildAutoCandidates (evaluateQuotaCutoff + buildAutoQuotaThresholds/clampPercent/asThresholdMap/quotaWindowLookupNames helpers) that drops low-quota candidates before scoring, plus a 429 guard when all candidates are below cutoff. On review (owner decision) the cutoff was made OPT-IN behind a new QuotaPreflightSettings.enabled flag (default OFF via QUOTA_PREFLIGHT_CUTOFF_ENABLED) so default behavior is unchanged, and the `...eligibleTargets` last-resort fallback the PR removed was restored (dedupe makes it identical to pre-cutoff when OFF; when ON a blocked target survives as final fallback rather than vanishing). Cohesive at the existing candidate-build/select chokepoints; not extractable. Covered by tests/unit/combo/auto-quota-cutoff.test.ts + tests/unit/resilience-settings-quota-preflight.test.ts (default-off + opt-in round-trip).",
"_rebaseline_2026_06_21_4475_target_format_badge": "PR #4475 (adivekar-utexas) review: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts 955->974 (+19). Extracted the pure targetFormatBadgeI18nKey (the 6 targetFormat value->i18n-key mapping) out of the CustomModelsSection.tsx badge so it is unit-testable outside the .tsx (Rule #18 gap — the PR had no UI test). The .tsx now calls the helper instead of an inline if-chain. This leaf is the strangler-fig home for pure provider-page helpers (#3501), so receiving the extraction is on-purpose. Covered by tests/unit/provider-target-format-badge-4475.test.ts.",
"_rebaseline_2026_06_21_4427_low_noise_catalog": "PR #4427 (Rahulsharma0810) own growth: src/app/api/v1/models/catalog.ts 1478->1486 (+8). The opt-in MODELS_CATALOG_PREFIX_MODE (dual default | alias | canonical, with ?prefix= per-request override) gates the dual alias+canonical model emission at the three /v1/models push sites (static, synced, custom) behind includeAlias/includeCanonical, suppressing the duplicate cross-prefix entries (net +3 from the feature). On review, 4 incidental explanatory comments removed by the PR were restored (synced-models resolve, skip-static, try-block intent, strip-modelIdPrefix; +5) since their code is unchanged — useful docs on a non-trivial catalog function. Default `dual` keeps byte-identical output; request-side alias resolution unchanged. Structural shrink of this route tracked in #3789. Covered by tests/unit/models-catalog-low-noise-flag.test.ts.",
Expand Down Expand Up @@ -161,7 +163,7 @@
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1089,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 983,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1594,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
Expand Down Expand Up @@ -197,7 +199,7 @@
"src/shared/constants/sidebarVisibility.ts": 1100,
"src/shared/services/cliRuntime.ts": 1090,
"src/shared/validation/schemas.ts": 2523,
"src/sse/handlers/chat.ts": 1491,
"src/sse/handlers/chat.ts": 1513,
"src/sse/services/auth.ts": 2279
},
"testCap": 800,
Expand Down
68 changes: 68 additions & 0 deletions open-sse/services/webSearchRouting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* #4481 layer 2 — per-tool web-search model routing (CCR-style `Router.webSearch`).
*
* Some providers expose an Anthropic-compatible (Claude-format) endpoint but do NOT
* implement Anthropic's typed server tools, so forwarding `web_search_20250305` to them
* makes the upstream 400 (e.g. MiniMax: `invalid params ... (2013)`). Layer 1
* (`webSearchFallback.ts`) already converts the tool to OmniRoute's own `/v1/search`
* fallback for such providers. Layer 2 is 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 leaving every non-search request on
* the default model.
*
* Pure (no DB / no I/O) so it can be unit-tested and wired into the request entrypoint
* BEFORE combo/auto routing and before the layer-1 fallback is computed for the target.
*/

interface WebSearchRouteResult {
wasRouted: boolean;
model: string;
}

function asString(value: unknown): string {
return typeof value === "string" ? value : "";
}

/**
* True when the request body declares a NATIVE web-search server tool — `web_search`,
* `web_search_preview`, or any Anthropic dated variant (`web_search_20250305`, future
* `web_search_YYYYMMDD`). A custom *function* tool that merely happens to be named
* "web_search" (it carries a `function` field) is NOT a native server tool and is ignored.
*
* Uses a `web_search` prefix match on purpose: at the request entrypoint the client's tool
* is still in its raw form (Claude Code sends the versioned `web_search_20250305`), which
* the exact-set check in `webSearchFallback.ts` only sees after normalization.
*/
export function hasNativeWebSearchTool(body: unknown): boolean {
if (!body || typeof body !== "object") return false;
const tools = (body as { tools?: unknown }).tools;
if (!Array.isArray(tools)) return false;
return tools.some((tool) => {
if (!tool || typeof tool !== "object") return false;
const record = tool as { type?: unknown; function?: unknown };
if (record.function) return false; // a custom function tool, not the server tool
return asString(record.type).startsWith("web_search");
});
}

/**
* Resolve whether this request should be routed to the operator-configured web-search
* model. Routes only when (a) the request carries a native web-search tool, (b)
* `webSearchRouteModel` is a non-empty string, and (c) it differs from the current model
* (so re-entry on the already-chosen target is a no-op). The returned `model` is a model
* string (`provider,model` / `provider/model` / alias / combo name) resolved downstream
* by the normal routing pipeline.
*/
export function resolveWebSearchRouteOverride(
currentModel: string,
body: unknown,
settings: Record<string, unknown> | null | undefined
): WebSearchRouteResult {
const fallthrough: WebSearchRouteResult = { wasRouted: false, model: currentModel };
if (!hasNativeWebSearchTool(body)) return fallthrough;

const configured = asString(settings?.webSearchRouteModel).trim();
if (!configured || configured === currentModel) return fallthrough;

return { wasRouted: true, model: configured };
}
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,41 @@ export default function RoutingTab() {
</div>
</Card>

{/* #4481 layer 2 — Web-Search Routing (CCR-style Router.webSearch) */}
<Card>
<div className="flex gap-3">
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500 h-fit">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
travel_explore
</span>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold">
{t("webSearchRouteTitle") || "Web search routing"}
</h3>
<p className="text-sm text-text-muted mt-1">
{t("webSearchRouteDesc") ||
"When a request includes a native web_search tool, route the whole request to this model instead of the default — useful for providers that don't implement Anthropic's web_search server tool. Leave blank to disable."}
</p>
<div className="mt-3">
<Input
value={
typeof settings.webSearchRouteModel === "string"
? settings.webSearchRouteModel
: ""
}
onChange={(e) => updateSetting({ webSearchRouteModel: e.target.value })}
placeholder={
t("webSearchRoutePlaceholder") || "e.g. openrouter,anthropic/claude-3.5-sonnet"
}
disabled={loading}
aria-label={t("webSearchRouteTitle") || "Web search routing model"}
/>
</div>
</div>
</div>
</Card>

<Card>
<div className="flex items-start justify-between gap-4">
<div className="flex gap-3">
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5495,6 +5495,9 @@
"lkgpToggleDesc": "When enabled, the router remembers which provider last served a successful response and tries it first on subsequent requests.",
"echoRequestedModelTitle": "Echo requested model name in responses",
"echoRequestedModelDesc": "When enabled, the response `model` field echoes the alias or combo name the client requested instead of the upstream model name. Fixes strict clients (e.g. Claude Desktop) that reject a response whose model does not match the request.",
"webSearchRouteTitle": "Web search routing",
"webSearchRouteDesc": "When a request includes a native web_search tool, route the whole request to this model instead of the default — useful for providers that don't implement Anthropic's web_search server tool. Leave blank to disable.",
"webSearchRoutePlaceholder": "e.g. openrouter,anthropic/claude-3.5-sonnet",
"clearLkgpCache": "Clear LKGP Cache",
"lkgpCacheCleared": "LKGP cache cleared successfully",
"lkgpCacheClearFailed": "Failed to clear LKGP cache",
Expand Down
5 changes: 5 additions & 0 deletions src/shared/validation/settingsSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ export const updateSettingsSchema = z.object({
lkgpEnabled: z.boolean().optional(),
// #1311: echo the requested alias/combo name in the response model field (opt-in)
echoRequestedModelName: z.boolean().optional(),
// #4481 layer 2: CCR-style Router.webSearch — when a request carries a native
// web_search server tool, route the whole request to this model/provider instead of
// the default (for providers that don't implement Anthropic's web_search server tool).
// Empty/unset = disabled. Value is a model string ("provider,model" / alias / combo).
webSearchRouteModel: z.string().max(200).optional(),
backgroundDegradation: z.unknown().optional(),
bruteForceProtection: z.boolean().optional(),
// Auto-routing settings
Expand Down
22 changes: 22 additions & 0 deletions src/sse/handlers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ import {
applyTaskAwareRouting,
getTaskRoutingConfig,
} from "@omniroute/open-sse/services/taskAwareRouter.ts";
import {
hasNativeWebSearchTool,
resolveWebSearchRouteOverride,
} from "@omniroute/open-sse/services/webSearchRouting.ts";
import {
generateSessionId as generateStableSessionId,
touchSession,
Expand Down Expand Up @@ -401,6 +405,24 @@ export async function handleChat(
telemetry.endPhase();
}

// #4481 layer 2 — Web-Search Routing (CCR-style Router.webSearch): a native web_search
// server tool + a configured `webSearchRouteModel` routes the whole request to that
// model (some providers don't implement Anthropic's web_search_20250305 server tool).
// Settings are read only when a web-search tool is present; the override lands before
// auto/combo resolution and the layer-1 fallback so the target's own handling applies.
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 };
}
}
Comment on lines +413 to +424

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 };
}
}


// ── Zero-Config Auto-Routing (auto and auto/ prefix) ────────────────────────
// If the model ID is "auto" or starts with "auto/", bypass DB combo lookup
// entirely and generate a virtual auto-combo on-the-fly from connected providers.
Expand Down
Loading
Loading