From e34362c55ac74b069aaeb79e333f4fdb85511b11 Mon Sep 17 00:00:00 2001 From: Praveen K Palaniswamy Date: Wed, 28 Oct 2026 20:25:18 -0400 Subject: [PATCH 1/3] fix(api): honor the Claude Code exposure toggle for compatible providers (#18517) The provider page saves the cc discovery alias setting under its route id, which for an OpenAI-compatible provider is the node id, while the /v1/models predicate looked the setting up by the entry's public prefix. The toggle returned 200 but never produced a claude// mirror. The predicate now also checks the node id for a compatible-provider prefix (prefix key first, so rows keyed by the prefix keep winning), for both the provider-level and model-level settings. The catalog already builds the prefix -> node id map for pricing, so it is passed through as-is. --- src/app/api/v1/models/catalogResponse.ts | 10 +- src/app/api/v1/models/ccAliasPredicate.ts | 25 ++++- .../cc-alias-provider-node-key-18517.test.ts | 92 +++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 tests/unit/cc-alias-provider-node-key-18517.test.ts diff --git a/src/app/api/v1/models/catalogResponse.ts b/src/app/api/v1/models/catalogResponse.ts index 5bbc72d11092..a424de6c8c11 100644 --- a/src/app/api/v1/models/catalogResponse.ts +++ b/src/app/api/v1/models/catalogResponse.ts @@ -131,7 +131,8 @@ async function buildGatewayLiveCatalogCheck( */ function appendCcMirrorsWhenEnabled( models: Array>, - mirrorRefinements: boolean + mirrorRefinements: boolean, + providerNodeIdsByPrefix?: Record ): Array> { const ccAliasGlobal = isCcAliasGlobalEnabled(); const ccAliasSettings = getCcAliasSettingsBulk(); @@ -144,6 +145,7 @@ function appendCcMirrorsWhenEnabled( global: ccAliasGlobal, providers: ccAliasSettings.providers, models: ccAliasSettings.models, + providerNodeIdsByPrefix, }), { refined: mirrorRefinements, isResolvableAutoId: isResolvableBuiltinAutoId } ); @@ -248,7 +250,11 @@ export async function applyCatalogPostFilters( // mirrors that cannot resolve, and checks gateways against what they will receive. const mirrorRefinements = isCatalogMirrorRefinementsEnabled(); if (ctx.allowCcDiscoveryAliases !== false) { - finalModels = appendCcMirrorsWhenEnabled(finalModels, mirrorRefinements); + finalModels = appendCcMirrorsWhenEnabled( + finalModels, + mirrorRefinements, + ctx.pricing?.providerNodeIdsByPrefix + ); } // Advertise `/` functional-gateway mirrors so discovery diff --git a/src/app/api/v1/models/ccAliasPredicate.ts b/src/app/api/v1/models/ccAliasPredicate.ts index d35ff1a6ceb4..131471fe2c2b 100644 --- a/src/app/api/v1/models/ccAliasPredicate.ts +++ b/src/app/api/v1/models/ccAliasPredicate.ts @@ -28,6 +28,12 @@ export interface CcAliasGateSnapshot { global: boolean; providers: Map; models: Map; // key `${providerId}/${modelId}` + /** + * #18517: compatible-provider prefix -> node id. The dashboard toggle saves the + * setting under the provider page's route id (the node id), while catalog entries + * carry the node's public prefix, so both keys are checked for such entries. + */ + providerNodeIdsByPrefix?: Record; } const COMBO_PROVIDER_KEY = "combo"; @@ -36,6 +42,15 @@ function settingFor(map: Map, key: string): CcAliasSetting return map.get(key) ?? null; } +/** First non-null setting across the candidate keys, in order. */ +function firstSetting(map: Map, keys: string[]): CcAliasSetting { + for (const key of keys) { + const setting = settingFor(map, key); + if (setting !== null) return setting; + } + return null; +} + /** * Builds a predicate suitable for `appendCcDiscoveryAliases`'s `isEnabled` * argument from a single settings snapshot (global flag + bulk provider/model @@ -64,8 +79,14 @@ export function buildCcAliasPredicate( const providerId = id.slice(0, slashIndex); const modelId = id.slice(slashIndex + 1); - const provider = settingFor(snapshot.providers, providerId); - const model = settingFor(snapshot.models, `${providerId}/${modelId}`); + // The public prefix is checked first, so a row keyed by it keeps winning. + const nodeId = snapshot.providerNodeIdsByPrefix?.[providerId]; + const providerKeys = nodeId && nodeId !== providerId ? [providerId, nodeId] : [providerId]; + const provider = firstSetting(snapshot.providers, providerKeys); + const model = firstSetting( + snapshot.models, + providerKeys.map((key) => `${key}/${modelId}`) + ); return resolveCcAliasEnabled({ model, provider, global: snapshot.global }); }; } diff --git a/tests/unit/cc-alias-provider-node-key-18517.test.ts b/tests/unit/cc-alias-provider-node-key-18517.test.ts new file mode 100644 index 000000000000..314a399e90c4 --- /dev/null +++ b/tests/unit/cc-alias-provider-node-key-18517.test.ts @@ -0,0 +1,92 @@ +/** + * #18517: the "Expose in Claude Code" toggle on a provider's dashboard page saves + * `provider:<[id]>` / `model:<[id]>/` under the page's route id, which for an + * OpenAI-compatible provider is the node UUID. The catalog predicate looked the + * setting up by the entry's public prefix (`local/...`), so a toggle saved from the + * dashboard never matched and no `claude//` mirror was listed. + */ + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cc-alias-18517-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +delete process.env.EXPOSE_CC_DISCOVERY_ALIASES; + +const core = await import("../../src/lib/db/core.ts"); +const ccAliases = await import("../../src/lib/db/ccDiscoveryAliases.ts"); +const { buildCcAliasPredicate } = await import("../../src/app/api/v1/models/ccAliasPredicate.ts"); +const { applyCatalogPostFilters } = await import("../../src/app/api/v1/models/catalogResponse.ts"); + +const NODE_ID = "openai-compatible-chat-3f2a9c1e-0000-4000-8000-000000000001"; + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("provider toggle saved under a compatible node id matches the node's prefix", () => { + const predicate = buildCcAliasPredicate({ + global: false, + providers: new Map([[NODE_ID, "on"]]), + models: new Map(), + providerNodeIdsByPrefix: { local: NODE_ID }, + }); + assert.equal(predicate({ id: "local/qwen/qwen3.8-27b", owned_by: "local" }), true); + assert.equal(predicate({ id: "openai/gpt-5.6", owned_by: "openai" }), false); +}); + +test("model toggle saved under a compatible node id matches the node's prefix", () => { + const predicate = buildCcAliasPredicate({ + global: true, + providers: new Map(), + models: new Map([[`${NODE_ID}/qwen/qwen3.8-27b`, "off"]]), + providerNodeIdsByPrefix: { local: NODE_ID }, + }); + assert.equal(predicate({ id: "local/qwen/qwen3.8-27b", owned_by: "local" }), false); + assert.equal(predicate({ id: "local/qwen/qwen3.8-9b", owned_by: "local" }), true); +}); + +test("a setting keyed by the public prefix still wins over the resolved id", () => { + const predicate = buildCcAliasPredicate({ + global: false, + providers: new Map([ + ["local", "off"], + [NODE_ID, "on"], + ]), + models: new Map(), + providerNodeIdsByPrefix: { local: NODE_ID }, + }); + assert.equal(predicate({ id: "local/qwen/qwen3.8-27b", owned_by: "local" }), false); +}); + +test("model-level setting under any key wins over a provider setting under any key", () => { + const predicate = buildCcAliasPredicate({ + global: false, + providers: new Map([["local", "on"]]), + models: new Map([[`${NODE_ID}/qwen/qwen3.8-27b`, "off"]]), + providerNodeIdsByPrefix: { local: NODE_ID }, + }); + assert.equal(predicate({ id: "local/qwen/qwen3.8-27b", owned_by: "local" }), false); +}); + +test("catalog post-filter lists claude// mirrors for a toggle saved by node id", async () => { + ccAliases.setCcAliasProviderSetting(NODE_ID, "on"); + const rows = [ + { id: "local/qwen/qwen3.8-27b", object: "model", owned_by: "local", root: "qwen/qwen3.8-27b" }, + ]; + const out = await applyCatalogPostFilters(new Request("http://localhost/v1/models"), rows, { + connections: [], + prefixMode: "dual", + aliasToProviderId: {}, + pricing: { providerNodeIdsByPrefix: { local: NODE_ID }, resolveComboTarget: () => null }, + }); + assert.ok( + out.some((m) => m.id === "claude/local/qwen/qwen3.8-27b"), + `expected a claude/ mirror, got ${JSON.stringify(out.map((m) => m.id))}` + ); +}); From 3df33d8be07bb2c642e2204a6dd701def0495409 Mon Sep 17 00:00:00 2001 From: Praveen K Palaniswamy Date: Wed, 28 Oct 2026 20:40:34 -0400 Subject: [PATCH 2/3] chore(changelog): add the #18522 fragment --- changelog.d/fixes/18522-cc-alias-compatible-provider-key.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/18522-cc-alias-compatible-provider-key.md diff --git a/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md b/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md new file mode 100644 index 000000000000..3307aca41119 --- /dev/null +++ b/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md @@ -0,0 +1 @@ +- **fix(api):** the "Expose in Claude Code" provider toggle now takes effect for OpenAI-compatible providers. The dashboard stores the setting under the provider's node id while `/v1/models` read it by the node's prefix, so no `claude//` mirror was listed ([#18522](https://github.com/cheaper-inference/OmniRoute/pull/18522), fixes [#18517](https://github.com/cheaper-inference/OmniRoute/issues/18517)) From 57b34fcd09491d3cc453a8ec2fc80f6ad4b6b176 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:12:04 -0300 Subject: [PATCH 3/3] fix(api): honor node-id alias toggles on the request path The catalog now lists claude// for a toggle saved under a compatible node's id, but the request-time resolver still read only the prefix, so an advertised id was not stripped. Check the prefix and then the node id for both the provider and model gates, using the catalog's winner rule. --- .../18522-cc-alias-compatible-provider-key.md | 2 +- src/lib/ccDiscoveryAliasResolve.ts | 48 ++++++- .../cc-alias-provider-node-key-18517.test.ts | 124 ++++++++++++++++++ 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md b/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md index 3307aca41119..1f9038f95cd2 100644 --- a/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md +++ b/changelog.d/fixes/18522-cc-alias-compatible-provider-key.md @@ -1 +1 @@ -- **fix(api):** the "Expose in Claude Code" provider toggle now takes effect for OpenAI-compatible providers. The dashboard stores the setting under the provider's node id while `/v1/models` read it by the node's prefix, so no `claude//` mirror was listed ([#18522](https://github.com/cheaper-inference/OmniRoute/pull/18522), fixes [#18517](https://github.com/cheaper-inference/OmniRoute/issues/18517)) +- **fix(api):** the "Expose in Claude Code" provider toggle now takes effect for OpenAI-compatible providers. The dashboard stores the setting under the provider's node id while `/v1/models` and the request-time `claude//` resolver read it by the node's prefix, so no mirror was listed or routed ([#18522](https://github.com/cheaper-inference/OmniRoute/pull/18522), fixes [#18517](https://github.com/cheaper-inference/OmniRoute/issues/18517)) diff --git a/src/lib/ccDiscoveryAliasResolve.ts b/src/lib/ccDiscoveryAliasResolve.ts index 75d8c1bc78a7..e93cb8c499f7 100644 --- a/src/lib/ccDiscoveryAliasResolve.ts +++ b/src/lib/ccDiscoveryAliasResolve.ts @@ -22,6 +22,11 @@ import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; import { getProviderById, getProviderByAlias } from "@/shared/constants/providers"; import { getCachedProviderNodes } from "@/lib/db/readCache"; +import { + isProviderNodePrefixReserved, + selectCompatibleNodeForPrefix, + type CompatibleNodeLike, +} from "@/lib/providerNodePrefixes"; import { getComboByName } from "@/lib/db/combos"; import { createBuiltinAutoCombo } from "@omniroute/open-sse/services/autoCombo/builtinCatalog.ts"; import { @@ -48,6 +53,12 @@ export interface CcDiscoveryResolveDeps { isRegistryProvider(prefix: string): boolean; /** Prefixes of operator-defined compatible provider nodes (DB) — Gap 1. */ customProviderPrefixes: Set; + /** + * #18517: compatible-node prefix -> node id. The dashboard toggle saves its setting under the + * node id while request ids carry the prefix, so the gate checks both. A Map because the prefix + * comes from the client's model string, and a plain object would match `constructor`. + */ + providerNodeIdsByPrefix?: ReadonlyMap; /** Combo row by name (or null). Only called for `claude/combo/` ids. */ getCombo(name: string): Promise<{ models?: unknown[] } | null>; gateGlobal(): boolean; @@ -85,17 +96,38 @@ function parseCcAliasTarget(rest: string): CcAliasTarget { }; } -/** Resolve the three-level gate (model > provider > global) for a parsed alias target. */ +/** First non-null setting across the candidate provider keys, in order. */ +function firstGateSetting( + providerKeys: string[], + lookup: (providerKey: string) => CcAliasSetting +): CcAliasSetting { + for (const providerKey of providerKeys) { + const setting = lookup(providerKey); + if (setting !== null) return setting; + } + return null; +} + +/** + * Resolve the three-level gate (model > provider > global) for a parsed alias target. Same key + * order as the catalog predicate (src/app/api/v1/models/ccAliasPredicate.ts): the public prefix + * first, then the node id. They must match or an alias could be listed yet not routed. + */ function resolveGateFor(target: CcAliasTarget, deps: CcDiscoveryResolveDeps): boolean { const gateProviderId = target.isComboAlias ? CC_DISCOVERY_COMBO_PROVIDER_KEY : target.providerPrefix; if (!gateProviderId) return deps.gateGlobal(); + const nodeId = target.isComboAlias + ? undefined + : deps.providerNodeIdsByPrefix?.get(gateProviderId); + const providerKeys = + nodeId && nodeId !== gateProviderId ? [gateProviderId, nodeId] : [gateProviderId]; const modelKey = target.isComboAlias ? target.comboName : target.modelPart; return resolveCcAliasEnabled({ - model: deps.gateModel(gateProviderId, modelKey as string), - provider: deps.gateProvider(gateProviderId), + model: firstGateSetting(providerKeys, (key) => deps.gateModel(key, modelKey as string)), + provider: firstGateSetting(providerKeys, deps.gateProvider), global: deps.gateGlobal(), }); } @@ -171,13 +203,20 @@ export async function resolveCcDiscoveryAliasStrip( // Operator-defined compatible provider nodes (OpenAI/Anthropic-compatible), // keyed by their `prefix` — the same source catalog.ts uses to mirror them. const customProviderPrefixes = new Set(); + const providerNodeIdsByPrefix = new Map(); try { - const nodes = (await getCachedProviderNodes()) as Array<{ prefix?: unknown }>; + const nodes = (await getCachedProviderNodes()) as CompatibleNodeLike[]; for (const node of nodes) { if (typeof node?.prefix === "string" && node.prefix) { customProviderPrefixes.add(node.prefix); } } + // The same winner rule catalog.ts uses, so both sides read the same node's toggle. + for (const prefix of customProviderPrefixes) { + if (isProviderNodePrefixReserved(prefix)) continue; + const winner = selectCompatibleNodeForPrefix(nodes, prefix); + if (winner?.id) providerNodeIdsByPrefix.set(prefix, winner.id); + } } catch { // A node-fetch failure just means custom prefixes are unavailable this call; // built-in registry providers still resolve. @@ -189,6 +228,7 @@ export async function resolveCcDiscoveryAliasStrip( claudeModelIds, isRegistryProvider: (prefix) => isRoutableProviderPrefix(prefix), customProviderPrefixes, + providerNodeIdsByPrefix, getCombo: async (name) => { if (name.startsWith("auto/")) { try { diff --git a/tests/unit/cc-alias-provider-node-key-18517.test.ts b/tests/unit/cc-alias-provider-node-key-18517.test.ts index 314a399e90c4..6a8648e6cb5e 100644 --- a/tests/unit/cc-alias-provider-node-key-18517.test.ts +++ b/tests/unit/cc-alias-provider-node-key-18517.test.ts @@ -21,6 +21,10 @@ const core = await import("../../src/lib/db/core.ts"); const ccAliases = await import("../../src/lib/db/ccDiscoveryAliases.ts"); const { buildCcAliasPredicate } = await import("../../src/app/api/v1/models/ccAliasPredicate.ts"); const { applyCatalogPostFilters } = await import("../../src/app/api/v1/models/catalogResponse.ts"); +const { resolveCcDiscoveryAliasStrip, resolveCcDiscoveryAliasStripWith } = + await import("../../src/lib/ccDiscoveryAliasResolve.ts"); +const { createProviderNode, deleteProviderNode } = await import("../../src/lib/db/providers.ts"); +const { invalidateDbCache } = await import("../../src/lib/db/readCache.ts"); const NODE_ID = "openai-compatible-chat-3f2a9c1e-0000-4000-8000-000000000001"; @@ -90,3 +94,123 @@ test("catalog post-filter lists claude// mirrors for a toggle saved by n `expected a claude/ mirror, got ${JSON.stringify(out.map((m) => m.id))}` ); }); + +const MIRROR_ID = "claude/local/qwen/qwen3.8-27b"; +const REAL_ID = "local/qwen/qwen3.8-27b"; + +/** Injected lookups over plain maps: no DB, so only the gate's key choice is under test. */ +function resolverDeps( + settings: { + global?: boolean; + providers?: Record; + models?: Record; + }, + nodeIds: Array<[string, string]> = [["local", NODE_ID]] +) { + return { + claudeModelIds: new Set(), + isRegistryProvider: () => false, + customProviderPrefixes: new Set(["local", "constructor"]), + providerNodeIdsByPrefix: new Map(nodeIds), + getCombo: async () => null, + gateGlobal: () => settings.global ?? false, + gateProvider: (providerId: string) => settings.providers?.[providerId] ?? null, + gateModel: (providerId: string, modelId: string) => + settings.models?.[`${providerId}/${modelId}`] ?? null, + }; +} + +test("resolver strips a mirror when the provider toggle is saved under the node id", async () => { + const result = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ providers: { [NODE_ID]: "on" } }) + ); + assert.deepEqual(result, { model: REAL_ID, stripped: true }); +}); + +test("resolver honors a provider 'off' saved under the node id over the global flag", async () => { + const result = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ global: true, providers: { [NODE_ID]: "off" } }) + ); + assert.equal(result.stripped, false); +}); + +test("resolver honors a model toggle saved under the node id", async () => { + const on = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ models: { [`${NODE_ID}/qwen/qwen3.8-27b`]: "on" } }) + ); + assert.deepEqual(on, { model: REAL_ID, stripped: true }); + + const off = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ global: true, models: { [`${NODE_ID}/qwen/qwen3.8-27b`]: "off" } }) + ); + assert.equal(off.stripped, false); +}); + +test("resolver keeps the catalog's precedence: prefix before node id, model before provider", async () => { + const prefixWins = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ providers: { local: "off", [NODE_ID]: "on" } }) + ); + assert.equal(prefixWins.stripped, false); + + const modelWins = await resolveCcDiscoveryAliasStripWith( + MIRROR_ID, + resolverDeps({ + providers: { local: "on" }, + models: { [`${NODE_ID}/qwen/qwen3.8-27b`]: "off" }, + }) + ); + assert.equal(modelWins.stripped, false); +}); + +test("resolver falls back to the prefix alone without a node map or for an unmapped prefix", async () => { + const { providerNodeIdsByPrefix: _omitted, ...withoutMap } = resolverDeps({ + providers: { [NODE_ID]: "on" }, + }); + const noMap = await resolveCcDiscoveryAliasStripWith(MIRROR_ID, withoutMap); + assert.equal(noMap.stripped, false); + + // A client-supplied prefix naming an Object.prototype member must not reach the gate as a key. + const asked: string[] = []; + const deps = resolverDeps({}); + await resolveCcDiscoveryAliasStripWith("claude/constructor/x", { + ...deps, + gateProvider: (providerId: string) => { + asked.push(providerId); + return null; + }, + }); + assert.deepEqual(asked, ["constructor"]); +}); + +test("catalog and request path agree for a toggle saved under a real node's id", async () => { + const node = await createProviderNode({ + type: "openai-compatible", + name: "Local", + prefix: "local", + baseUrl: "http://localhost:9999/v1", + apiType: "chat", + }); + invalidateDbCache(); + try { + ccAliases.setCcAliasProviderSetting(NODE_ID, null); + assert.equal((await resolveCcDiscoveryAliasStrip(MIRROR_ID)).stripped, false); + + ccAliases.setCcAliasProviderSetting(node.id, "on"); + assert.deepEqual(await resolveCcDiscoveryAliasStrip(MIRROR_ID), { + model: REAL_ID, + stripped: true, + }); + + ccAliases.setCcAliasProviderSetting(node.id, "off"); + assert.equal((await resolveCcDiscoveryAliasStrip(MIRROR_ID)).stripped, false); + } finally { + ccAliases.setCcAliasProviderSetting(node.id, null); + await deleteProviderNode(node.id); + invalidateDbCache(); + } +});