mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
Routes requests carrying a web_search tool to a configured webSearchRouteModel (#4481 layer-2). Integrated into release/v3.8.33.
This commit is contained in:
committed by
GitHub
parent
f3253ee706
commit
185c474ee0
@@ -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.",
|
||||
@@ -163,7 +165,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,
|
||||
@@ -199,7 +201,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,
|
||||
|
||||
68
open-sse/services/webSearchRouting.ts
Normal file
68
open-sse/services/webSearchRouting.ts
Normal file
@@ -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 };
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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.
|
||||
|
||||
151
tests/unit/web-search-tool-routing-4481.test.ts
Normal file
151
tests/unit/web-search-tool-routing-4481.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// #4481 layer 2 — CCR-style `Router.webSearch`. When a request carries a NATIVE
|
||||
// web-search server tool (`web_search`, `web_search_preview`, or Anthropic's versioned
|
||||
// `web_search_20250305`) and an operator configured `webSearchRouteModel`, route the
|
||||
// whole request to that model instead of the default — so a provider that doesn't
|
||||
// implement the server tool (e.g. MiniMax) isn't asked to run a tool it 400s on.
|
||||
// Pure helpers, no DB.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import {
|
||||
hasNativeWebSearchTool,
|
||||
resolveWebSearchRouteOverride,
|
||||
} from "../../open-sse/services/webSearchRouting.ts";
|
||||
|
||||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
// ── Wiring source-guard (RED on base: chat.ts doesn't call the router yet) ─
|
||||
|
||||
test("chat.ts wires the web-search router at the request entrypoint", () => {
|
||||
const chat = readFileSync(join(REPO_ROOT, "src/sse/handlers/chat.ts"), "utf8");
|
||||
assert.match(chat, /from "@omniroute\/open-sse\/services\/webSearchRouting\.ts"/);
|
||||
assert.match(chat, /hasNativeWebSearchTool\(body\)/);
|
||||
assert.match(chat, /resolveWebSearchRouteOverride\(/);
|
||||
});
|
||||
|
||||
test("webSearchRouteModel is registered in the settings Zod schema", () => {
|
||||
const schema = readFileSync(join(REPO_ROOT, "src/shared/validation/settingsSchemas.ts"), "utf8");
|
||||
assert.match(schema, /webSearchRouteModel:\s*z\.string\(\)/);
|
||||
});
|
||||
|
||||
test("the Routing settings tab exposes a webSearchRouteModel field", () => {
|
||||
const tab = readFileSync(
|
||||
join(REPO_ROOT, "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
assert.match(tab, /settings\.webSearchRouteModel/);
|
||||
assert.match(tab, /updateSetting\(\{ webSearchRouteModel:/);
|
||||
assert.match(tab, /webSearchRouteTitle/);
|
||||
});
|
||||
|
||||
test("en.json defines the web-search-routing UI strings", () => {
|
||||
const en = JSON.parse(readFileSync(join(REPO_ROOT, "src/i18n/messages/en.json"), "utf8"));
|
||||
const s = en.settings || {};
|
||||
assert.equal(typeof s.webSearchRouteTitle, "string");
|
||||
assert.equal(typeof s.webSearchRouteDesc, "string");
|
||||
assert.equal(typeof s.webSearchRoutePlaceholder, "string");
|
||||
});
|
||||
|
||||
// ── hasNativeWebSearchTool ───────────────────────────────────────────────
|
||||
|
||||
test("detects the plain and preview native web-search tool types", () => {
|
||||
assert.equal(hasNativeWebSearchTool({ tools: [{ type: "web_search" }] }), true);
|
||||
assert.equal(hasNativeWebSearchTool({ tools: [{ type: "web_search_preview" }] }), true);
|
||||
});
|
||||
|
||||
test("detects Anthropic's versioned web_search_20250305 (and future dated names)", () => {
|
||||
assert.equal(
|
||||
hasNativeWebSearchTool({ tools: [{ type: "web_search_20250305", name: "web_search" }] }),
|
||||
true
|
||||
);
|
||||
assert.equal(hasNativeWebSearchTool({ tools: [{ type: "web_search_20251201" }] }), true);
|
||||
});
|
||||
|
||||
test("detects a native web-search tool anywhere in a mixed tools array", () => {
|
||||
assert.equal(
|
||||
hasNativeWebSearchTool({
|
||||
tools: [{ type: "function", function: { name: "x" } }, { type: "web_search_20250305" }],
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("ignores a custom FUNCTION tool merely named web_search (has a function field)", () => {
|
||||
assert.equal(
|
||||
hasNativeWebSearchTool({
|
||||
tools: [{ type: "function", function: { name: "web_search", parameters: {} } }],
|
||||
}),
|
||||
false
|
||||
);
|
||||
// A bare web_search type WITH a function field is also not the native server tool.
|
||||
assert.equal(
|
||||
hasNativeWebSearchTool({ tools: [{ type: "web_search", function: { name: "x" } }] }),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("returns false for no tools / non-web-search tools / malformed input", () => {
|
||||
assert.equal(hasNativeWebSearchTool({ tools: [] }), false);
|
||||
assert.equal(hasNativeWebSearchTool({ tools: [{ type: "code_interpreter" }] }), false);
|
||||
assert.equal(hasNativeWebSearchTool({}), false);
|
||||
assert.equal(hasNativeWebSearchTool({ tools: "nope" }), false);
|
||||
assert.equal(hasNativeWebSearchTool(null), false);
|
||||
assert.equal(hasNativeWebSearchTool(undefined), false);
|
||||
});
|
||||
|
||||
// ── resolveWebSearchRouteOverride ────────────────────────────────────────
|
||||
|
||||
const bodyWithSearch = { tools: [{ type: "web_search_20250305", name: "web_search" }] };
|
||||
|
||||
test("routes to the configured model when a native web-search tool is present", () => {
|
||||
const r = resolveWebSearchRouteOverride("minimax,MiniMax-M3", bodyWithSearch, {
|
||||
webSearchRouteModel: "openrouter,anthropic/claude-3.5-sonnet",
|
||||
});
|
||||
assert.deepEqual(r, { wasRouted: true, model: "openrouter,anthropic/claude-3.5-sonnet" });
|
||||
});
|
||||
|
||||
test("does NOT route when the request has no native web-search tool", () => {
|
||||
const r = resolveWebSearchRouteOverride("minimax,MiniMax-M3", { tools: [{ type: "function" }] }, {
|
||||
webSearchRouteModel: "openrouter,anthropic/claude-3.5-sonnet",
|
||||
});
|
||||
assert.deepEqual(r, { wasRouted: false, model: "minimax,MiniMax-M3" });
|
||||
});
|
||||
|
||||
test("does NOT route when no route model is configured (or it is blank)", () => {
|
||||
assert.deepEqual(resolveWebSearchRouteOverride("minimax,MiniMax-M3", bodyWithSearch, {}), {
|
||||
wasRouted: false,
|
||||
model: "minimax,MiniMax-M3",
|
||||
});
|
||||
assert.deepEqual(
|
||||
resolveWebSearchRouteOverride("minimax,MiniMax-M3", bodyWithSearch, {
|
||||
webSearchRouteModel: " ",
|
||||
}),
|
||||
{ wasRouted: false, model: "minimax,MiniMax-M3" }
|
||||
);
|
||||
});
|
||||
|
||||
test("does NOT route (no-op) when the configured model equals the current model", () => {
|
||||
const r = resolveWebSearchRouteOverride("openrouter,claude", bodyWithSearch, {
|
||||
webSearchRouteModel: " openrouter,claude ",
|
||||
});
|
||||
assert.deepEqual(r, { wasRouted: false, model: "openrouter,claude" });
|
||||
});
|
||||
|
||||
test("trims the configured route model", () => {
|
||||
const r = resolveWebSearchRouteOverride("minimax,M3", bodyWithSearch, {
|
||||
webSearchRouteModel: " anthropic/claude-opus-4-8 ",
|
||||
});
|
||||
assert.deepEqual(r, { wasRouted: true, model: "anthropic/claude-opus-4-8" });
|
||||
});
|
||||
|
||||
test("ignores a non-string route config", () => {
|
||||
const r = resolveWebSearchRouteOverride("minimax,M3", bodyWithSearch, {
|
||||
webSearchRouteModel: 123 as unknown as string,
|
||||
});
|
||||
assert.deepEqual(r, { wasRouted: false, model: "minimax,M3" });
|
||||
});
|
||||
Reference in New Issue
Block a user