mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 12:42:21 +03:00
feat(routing): per-model web-search interception rule (#3384)
Adds a per-provider/per-model interceptSearch rule (src/lib/db/interceptionRules.ts, key_value namespace interception_rules) that overrides the existing native web-search bypass defaults (Codex/Gemini/Claude->Claude passthrough) in webSearchFallback.ts. Wired at the existing prepareWebSearchFallbackBody() call site in chatCore.ts. Resolution precedence: per-model rule > provider-level rule > existing native-bypass defaults. This lands Phase 1-2 of the plan (rule store + search interception). Web-fetch interception and the dashboard UI toggle are tracked as follow-up phases.
This commit is contained in:
@@ -13,6 +13,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
### ✨ New Features
|
||||
|
||||
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
|
||||
- **Per-model web-search interception rule (Phase 1/2)**: a new `interceptSearch` rule, resolvable per provider or per model (`src/lib/db/interceptionRules.ts`, `key_value` namespace `interception_rules`), now overrides the existing native web-search bypass defaults (Codex/Gemini/Claude→Claude passthrough) — set `interceptSearch:true` to force a model's provider-native web-search tool call through OmniRoute's own `/v1/search` fallback even when it would otherwise be forwarded natively, or `interceptSearch:false` to force native passthrough for a model that would otherwise be converted. Per-model rule takes precedence over the provider-level rule, which takes precedence over the existing defaults when unset. Wired at the existing `prepareWebSearchFallbackBody()` call site in `chatCore.ts`. Web-fetch interception + the dashboard UI toggle are tracked as follow-up phases. Regression guard: `tests/unit/interception-rules.test.ts`, extended `tests/unit/web-search-fallback-format.test.ts` (#3384 — thanks @thomasmaerz)
|
||||
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
|
||||
- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen)
|
||||
- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7)
|
||||
|
||||
@@ -296,6 +296,7 @@ import {
|
||||
import { resolveBackgroundTaskRedirect } from "./chatCore/backgroundRedirect.ts";
|
||||
import type { CompressionConfig, CompressionPipelineStep } from "../services/compression/types.ts";
|
||||
import { prepareWebSearchFallbackBody } from "../services/webSearchFallback.ts";
|
||||
import { resolveInterceptSearch } from "@/lib/db/interceptionRules";
|
||||
import {
|
||||
resolveExplicitStreamAlias,
|
||||
resolveStreamFlag,
|
||||
@@ -731,12 +732,17 @@ export async function handleChatCore({
|
||||
// Initialize rate limit settings from persisted DB (once, lazy)
|
||||
await initializeRateLimits();
|
||||
|
||||
// #3384: per-model interception rule (src/lib/db/interceptionRules.ts) overrides the
|
||||
// native-bypass defaults below when the operator explicitly configured it for this
|
||||
// provider/model pair; undefined falls through to the existing bypass logic.
|
||||
const interceptSearchOverride = resolveInterceptSearch(provider, effectiveModel);
|
||||
const { body: bodyWithWebSearchFallback, fallback: webSearchFallbackPlan } =
|
||||
prepareWebSearchFallbackBody(body as Record<string, unknown>, {
|
||||
provider,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
nativeCodexPassthrough,
|
||||
interceptSearchOverride,
|
||||
});
|
||||
if (webSearchFallbackPlan.enabled) {
|
||||
body = bodyWithWebSearchFallback as typeof body;
|
||||
|
||||
@@ -142,12 +142,20 @@ export function supportsNativeWebSearchFallbackBypass({
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
nativeCodexPassthrough,
|
||||
interceptSearchOverride,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
sourceFormat?: string | null;
|
||||
targetFormat: string | null | undefined;
|
||||
nativeCodexPassthrough: boolean;
|
||||
// Per-model rule (#3384) — resolveInterceptSearch() in src/lib/db/interceptionRules.ts.
|
||||
// true = force interception (never bypass); false = force native bypass; undefined =
|
||||
// fall through to the native-bypass defaults below.
|
||||
interceptSearchOverride?: boolean;
|
||||
}): boolean {
|
||||
if (typeof interceptSearchOverride === "boolean") {
|
||||
return !interceptSearchOverride;
|
||||
}
|
||||
// Native Codex (OpenAI Responses) passthrough: the upstream runs web search itself.
|
||||
if (nativeCodexPassthrough) return true;
|
||||
// Gemini target: the Gemini translator maps built-in web search to googleSearch natively.
|
||||
@@ -171,6 +179,7 @@ export function prepareWebSearchFallbackBody<T extends JsonRecord>(
|
||||
sourceFormat?: string | null;
|
||||
targetFormat?: string | null;
|
||||
nativeCodexPassthrough: boolean;
|
||||
interceptSearchOverride?: boolean;
|
||||
}
|
||||
): { body: T; fallback: WebSearchFallbackPlan } {
|
||||
const tools = Array.isArray(body.tools) ? body.tools : null;
|
||||
|
||||
203
src/lib/db/interceptionRules.ts
Normal file
203
src/lib/db/interceptionRules.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* db/interceptionRules.ts — Per-model web-search / web-fetch interception rules (#3384).
|
||||
*
|
||||
* CRUD against the key_value table under namespace "interception_rules". Follows the
|
||||
* established key_value pattern from paramFilters.ts / databaseSettings.ts.
|
||||
*
|
||||
* Resolution precedence (see resolveInterceptSearch): per-model rule > provider-level
|
||||
* rule > undefined (caller falls back to the existing native-bypass defaults).
|
||||
*/
|
||||
|
||||
import { getDbInstance } from "./core";
|
||||
|
||||
const NAMESPACE = "interception_rules";
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export type FetchInterceptionBackend = "firecrawl" | "jina" | "tavily";
|
||||
|
||||
export interface ModelInterceptionRule {
|
||||
/** true = route through OmniRoute's /v1/search; false = force native passthrough. */
|
||||
interceptSearch?: boolean;
|
||||
/** true = route through OmniRoute's /v1/web/fetch; false = force native passthrough. */
|
||||
interceptFetch?: boolean;
|
||||
fetchBackend?: FetchInterceptionBackend;
|
||||
fetchProxyUrl?: string;
|
||||
}
|
||||
|
||||
export interface ProviderInterceptionRules {
|
||||
/** Provider-level default, used when a model has no override. */
|
||||
interceptSearch?: boolean;
|
||||
interceptFetch?: boolean;
|
||||
fetchBackend?: FetchInterceptionBackend;
|
||||
fetchProxyUrl?: string;
|
||||
/** Per-model overrides (stricter/looser than provider-level). */
|
||||
models?: Record<string, ModelInterceptionRule>;
|
||||
}
|
||||
|
||||
// ── Cache ───────────────────────────────────────────────────────────────────
|
||||
|
||||
let rulesCache: Map<string, ProviderInterceptionRules> | null = null;
|
||||
|
||||
function invalidateCache(): void {
|
||||
rulesCache = null;
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function toNormalizedString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function toOptionalBool(value: unknown): boolean | undefined {
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function toFetchBackend(value: unknown): FetchInterceptionBackend | undefined {
|
||||
return value === "firecrawl" || value === "jina" || value === "tavily" ? value : undefined;
|
||||
}
|
||||
|
||||
function parseStoredValue(raw: unknown): unknown {
|
||||
if (typeof raw !== "string") return raw;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function toModelInterceptionRule(raw: unknown): ModelInterceptionRule | null {
|
||||
if (!isRecord(raw)) return null;
|
||||
const rule: ModelInterceptionRule = {
|
||||
interceptSearch: toOptionalBool(raw.interceptSearch),
|
||||
interceptFetch: toOptionalBool(raw.interceptFetch),
|
||||
fetchBackend: toFetchBackend(raw.fetchBackend),
|
||||
fetchProxyUrl: toNormalizedString(raw.fetchProxyUrl) ?? undefined,
|
||||
};
|
||||
const hasAnyField = Object.values(rule).some((v) => v !== undefined);
|
||||
return hasAnyField ? rule : null;
|
||||
}
|
||||
|
||||
function toModelInterceptionRules(raw: unknown): Record<string, ModelInterceptionRule> {
|
||||
const models: Record<string, ModelInterceptionRule> = {};
|
||||
if (!isRecord(raw)) return models;
|
||||
for (const [modelId, val] of Object.entries(raw)) {
|
||||
const rule = toModelInterceptionRule(val);
|
||||
if (rule) models[modelId] = rule;
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
function toProviderInterceptionRules(raw: unknown): ProviderInterceptionRules | null {
|
||||
if (!isRecord(raw)) return null;
|
||||
const models = toModelInterceptionRules(raw.models);
|
||||
return {
|
||||
interceptSearch: toOptionalBool(raw.interceptSearch),
|
||||
interceptFetch: toOptionalBool(raw.interceptFetch),
|
||||
fetchBackend: toFetchBackend(raw.fetchBackend),
|
||||
fetchProxyUrl: toNormalizedString(raw.fetchProxyUrl) ?? undefined,
|
||||
models: Object.keys(models).length > 0 ? models : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Read ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function readNamespace(namespace: string): Record<string, unknown> {
|
||||
const db = getDbInstance();
|
||||
const rows = db
|
||||
.prepare("SELECT key, value FROM key_value WHERE namespace = ?")
|
||||
.all(namespace) as Array<{ key: string; value: string }>;
|
||||
|
||||
const values: Record<string, unknown> = {};
|
||||
for (const row of rows) {
|
||||
values[row.key] = parseStoredValue(row.value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function loadAllRules(): Map<string, ProviderInterceptionRules> {
|
||||
const raw = readNamespace(NAMESPACE);
|
||||
const map = new Map<string, ProviderInterceptionRules>();
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const parsed = toProviderInterceptionRules(value);
|
||||
if (parsed) map.set(key, parsed);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function loadRulesCached(): Map<string, ProviderInterceptionRules> {
|
||||
if (rulesCache === null) {
|
||||
rulesCache = loadAllRules();
|
||||
}
|
||||
return rulesCache;
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Get the interception rules for a single provider, or null if not configured. */
|
||||
export function getInterceptionRules(provider: string): ProviderInterceptionRules | null {
|
||||
return toNormalizedString(provider) ? (loadRulesCached().get(provider) ?? null) : null;
|
||||
}
|
||||
|
||||
/** Upsert the entire interception rule set for a provider. Invalidates the cache. */
|
||||
export function setInterceptionRules(provider: string, rules: ProviderInterceptionRules): void {
|
||||
const normalizedProvider = toNormalizedString(provider);
|
||||
if (!normalizedProvider) return;
|
||||
|
||||
const db = getDbInstance();
|
||||
const stmt = db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)"
|
||||
);
|
||||
|
||||
const normalized: ProviderInterceptionRules = {
|
||||
interceptSearch: rules.interceptSearch,
|
||||
interceptFetch: rules.interceptFetch,
|
||||
fetchBackend: rules.fetchBackend,
|
||||
fetchProxyUrl: rules.fetchProxyUrl,
|
||||
models: rules.models && Object.keys(rules.models).length > 0 ? rules.models : undefined,
|
||||
};
|
||||
|
||||
stmt.run(NAMESPACE, normalizedProvider, JSON.stringify(normalized));
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/** Delete the interception rules for a provider. Resets that provider to default behavior. */
|
||||
export function deleteInterceptionRules(provider: string): void {
|
||||
const normalizedProvider = toNormalizedString(provider);
|
||||
if (!normalizedProvider) return;
|
||||
|
||||
const db = getDbInstance();
|
||||
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(
|
||||
NAMESPACE,
|
||||
normalizedProvider
|
||||
);
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective `interceptSearch` override for a provider/model pair.
|
||||
*
|
||||
* Precedence: per-model rule > provider-level rule > undefined (no override — the
|
||||
* caller should fall back to the existing native-bypass defaults).
|
||||
*/
|
||||
export function resolveInterceptSearch(
|
||||
provider: string | null | undefined,
|
||||
model: string | null | undefined
|
||||
): boolean | undefined {
|
||||
const normalizedProvider = toNormalizedString(provider);
|
||||
if (!normalizedProvider) return undefined;
|
||||
|
||||
const rules = getInterceptionRules(normalizedProvider);
|
||||
if (!rules) return undefined;
|
||||
|
||||
const normalizedModel = toNormalizedString(model);
|
||||
if (normalizedModel && rules.models?.[normalizedModel]?.interceptSearch !== undefined) {
|
||||
return rules.models[normalizedModel].interceptSearch;
|
||||
}
|
||||
|
||||
return rules.interceptSearch;
|
||||
}
|
||||
16
src/lib/db/migrations/119_interception_rules.sql
Normal file
16
src/lib/db/migrations/119_interception_rules.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 119_interception_rules.sql
|
||||
-- Documents the interception_rules namespace in the key_value table (#3384).
|
||||
-- No schema change — the key_value table already exists.
|
||||
-- Key = provider ID, value = JSON with shape:
|
||||
-- {
|
||||
-- interceptSearch?: boolean,
|
||||
-- interceptFetch?: boolean,
|
||||
-- fetchBackend?: "firecrawl"|"jina"|"tavily",
|
||||
-- fetchProxyUrl?: string,
|
||||
-- models?: { [modelId]: { interceptSearch?, interceptFetch?, fetchBackend?, fetchProxyUrl? } }
|
||||
-- }
|
||||
--
|
||||
-- Resolution precedence: per-model rule > provider-level rule > undefined (caller
|
||||
-- falls back to the existing native web-search-bypass defaults in webSearchFallback.ts).
|
||||
--
|
||||
-- See: src/lib/db/interceptionRules.ts
|
||||
96
tests/unit/interception-rules.test.ts
Normal file
96
tests/unit/interception-rules.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, it, beforeEach, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
// Set DATA_DIR to a temp dir before any imports that touch the DB.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-interception-rules-"));
|
||||
process.env.DATA_DIR = tmpDir;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const {
|
||||
getInterceptionRules,
|
||||
setInterceptionRules,
|
||||
deleteInterceptionRules,
|
||||
resolveInterceptSearch,
|
||||
} = await import("../../src/lib/db/interceptionRules.ts");
|
||||
|
||||
// #3384 — per-model web-search/web-fetch interception rule store.
|
||||
describe("db/interceptionRules — per-model interception rules (#3384)", () => {
|
||||
function resetDb() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetDb();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns null for an unconfigured provider", () => {
|
||||
assert.equal(getInterceptionRules("anthropic"), null);
|
||||
});
|
||||
|
||||
it("round-trips a provider-level rule via set/get", () => {
|
||||
setInterceptionRules("anthropic", { interceptSearch: true, interceptFetch: false });
|
||||
const rules = getInterceptionRules("anthropic");
|
||||
assert.equal(rules?.interceptSearch, true);
|
||||
assert.equal(rules?.interceptFetch, false);
|
||||
});
|
||||
|
||||
it("round-trips a per-model override", () => {
|
||||
setInterceptionRules("anthropic", {
|
||||
interceptSearch: false,
|
||||
models: { "claude-opus-4": { interceptSearch: true } },
|
||||
});
|
||||
const rules = getInterceptionRules("anthropic");
|
||||
assert.equal(rules?.interceptSearch, false);
|
||||
assert.equal(rules?.models?.["claude-opus-4"]?.interceptSearch, true);
|
||||
});
|
||||
|
||||
it("delete resets a provider back to unconfigured", () => {
|
||||
setInterceptionRules("anthropic", { interceptSearch: true });
|
||||
deleteInterceptionRules("anthropic");
|
||||
assert.equal(getInterceptionRules("anthropic"), null);
|
||||
});
|
||||
|
||||
it("invalidates the in-memory cache after a write", () => {
|
||||
setInterceptionRules("openai", { interceptSearch: true });
|
||||
assert.equal(getInterceptionRules("openai")?.interceptSearch, true);
|
||||
setInterceptionRules("openai", { interceptSearch: false });
|
||||
assert.equal(getInterceptionRules("openai")?.interceptSearch, false);
|
||||
});
|
||||
|
||||
describe("resolveInterceptSearch — precedence", () => {
|
||||
it("returns undefined when no rule is configured (caller falls back to bypass defaults)", () => {
|
||||
assert.equal(resolveInterceptSearch("anthropic", "claude-opus-4"), undefined);
|
||||
});
|
||||
|
||||
it("returns the provider-level rule when no model override exists", () => {
|
||||
setInterceptionRules("anthropic", { interceptSearch: true });
|
||||
assert.equal(resolveInterceptSearch("anthropic", "claude-opus-4"), true);
|
||||
assert.equal(resolveInterceptSearch("anthropic", "claude-haiku-4"), true);
|
||||
});
|
||||
|
||||
it("per-model rule overrides the provider-level rule", () => {
|
||||
setInterceptionRules("anthropic", {
|
||||
interceptSearch: false,
|
||||
models: { "claude-opus-4": { interceptSearch: true } },
|
||||
});
|
||||
assert.equal(resolveInterceptSearch("anthropic", "claude-opus-4"), true);
|
||||
assert.equal(resolveInterceptSearch("anthropic", "claude-haiku-4"), false);
|
||||
});
|
||||
|
||||
it("returns undefined for an empty/missing provider", () => {
|
||||
assert.equal(resolveInterceptSearch("", "claude-opus-4"), undefined);
|
||||
assert.equal(resolveInterceptSearch(null, "claude-opus-4"), undefined);
|
||||
assert.equal(resolveInterceptSearch(undefined, "claude-opus-4"), undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -255,3 +255,61 @@ test("OpenAI -> Claude (non-passthrough): built-in web_search IS still converted
|
||||
const toolNames = tools.map((t) => (t.function ? t.function.name : t.name));
|
||||
assert.ok(toolNames.includes(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME));
|
||||
});
|
||||
|
||||
// ── #3384: per-model interceptSearch override wins over every native-bypass default ──
|
||||
|
||||
test("#3384 interceptSearchOverride=true forces interception even on the Claude->Claude bypass path", () => {
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
provider: "claude",
|
||||
sourceFormat: "claude",
|
||||
targetFormat: "claude",
|
||||
nativeCodexPassthrough: false,
|
||||
interceptSearchOverride: true,
|
||||
}),
|
||||
false,
|
||||
"explicit interceptSearch:true must NOT bypass, overriding the native Claude passthrough"
|
||||
);
|
||||
});
|
||||
|
||||
test("#3384 interceptSearchOverride=false forces native passthrough even for a standard provider", () => {
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
provider: "openai",
|
||||
sourceFormat: "openai",
|
||||
targetFormat: "openai",
|
||||
nativeCodexPassthrough: false,
|
||||
interceptSearchOverride: false,
|
||||
}),
|
||||
true,
|
||||
"explicit interceptSearch:false must bypass even though OpenAI->OpenAI has no native default bypass"
|
||||
);
|
||||
});
|
||||
|
||||
test("#3384 interceptSearchOverride=undefined falls through to the existing native-bypass defaults", () => {
|
||||
assert.equal(
|
||||
supportsNativeWebSearchFallbackBypass({
|
||||
provider: "claude",
|
||||
sourceFormat: "claude",
|
||||
targetFormat: "claude",
|
||||
nativeCodexPassthrough: false,
|
||||
interceptSearchOverride: undefined,
|
||||
}),
|
||||
true,
|
||||
"no override configured — default Claude->Claude bypass still applies"
|
||||
);
|
||||
});
|
||||
|
||||
test("#3384 end-to-end: interceptSearchOverride=true converts the tool on the Claude->Claude bypass path", () => {
|
||||
const inputBody = { tools: [{ type: "web_search" }] };
|
||||
const { fallback } = prepareWebSearchFallbackBody(inputBody, {
|
||||
provider: "claude",
|
||||
sourceFormat: "claude",
|
||||
targetFormat: "claude",
|
||||
nativeCodexPassthrough: false,
|
||||
interceptSearchOverride: true,
|
||||
});
|
||||
|
||||
assert.equal(fallback.enabled, true);
|
||||
assert.equal(fallback.toolName, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user