69 lines
3.1 KiB
TypeScript
69 lines
3.1 KiB
TypeScript
/**
|
|
* #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 };
|
|
}
|