mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
The /v1/models catalog mirrors `claude/<provider>/<model>` ids purely from the alias gate -- ccAliasPredicate.ts consults no provider registry. The request path additionally required the prefix to be an open-sse REGISTRY entry or an operator-defined custom node. Enterprise-cloud providers such as azure-ai / azure-openai live only in the provider catalog (src/shared/constants/providers/apikey/enterprise-cloud.ts). They route fine directly -- `azure-ai/Phi-4` returns 200 -- but have no open-sse registry entry, so the two sides disagreed: the catalog advertised `claude/azure-ai/<model>` while stripCcDiscoveryAlias refused to strip it. The unstripped id then fell through to normal resolution, which splits on the first / and parsed `claude` as the provider. Every Claude Code request for an Azure model was routed to the Claude provider instead: ROUTING: Provider: claude, Model: azure-ai/DeepSeek-V4-Flash Extract the predicate as `isRoutableProviderPrefix()` and widen it to the provider catalog (id + alias) alongside the open-sse registry, so the request path recognises exactly what the catalog can advertise. Regression guard: tests/unit/cc-discovery-alias-routable-prefix.test.ts pins azure-ai/azure-openai/azure as routable, keeps openai/anthropic routable, and keeps an unknown prefix non-routable. Verified failing before the widening. Co-authored-by: Mihaly Bodo <michael@proton-quantum.com>
207 lines
8.6 KiB
TypeScript
207 lines
8.6 KiB
TypeScript
/**
|
|
* Request-path resolver for the `claude/…` cc-discovery aliases.
|
|
*
|
|
* The /v1/models catalog mirrors every enabled non-Claude model (and combo)
|
|
* under a `claude/<id>` id so Claude Code's gateway model discovery — which only
|
|
* lists ids starting with `claude`/`anthropic` — can surface them. When a client
|
|
* sends one of those mirror ids back on a request, it must be resolved to the
|
|
* real id BEFORE provider/combo resolution runs on the raw string. This module
|
|
* is the single shared entry point for that, called from every request transport
|
|
* that resolves a provider from the client model string:
|
|
* - src/sse/handlers/chat.ts (all /v1/chat, /v1/messages, /v1/responses, … )
|
|
* - src/app/api/internal/codex-responses-ws/route.ts (Codex Responses WS bridge)
|
|
*
|
|
* A legitimate `claude/<real-claude-model>` id (the actual Claude OAuth provider
|
|
* namespace) is always left untouched — see the pure `stripCcDiscoveryAlias`.
|
|
*/
|
|
import {
|
|
stripCcDiscoveryAlias,
|
|
type CcDiscoveryStripResult,
|
|
} from "@omniroute/open-sse/handlers/chatCore/ccDiscoveryAliasStrip.ts";
|
|
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
|
|
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
|
import { getProviderById, getProviderByAlias } from "@/shared/constants/providers";
|
|
import { getCachedProviderNodes } from "@/lib/db/readCache";
|
|
import { getComboByName } from "@/lib/db/combos";
|
|
import {
|
|
resolveCcAliasEnabled,
|
|
getCcAliasGlobalState,
|
|
getCcAliasProviderSetting,
|
|
getCcAliasModelSetting,
|
|
type CcAliasSetting,
|
|
} from "@/lib/db/ccDiscoveryAliases";
|
|
import { incrementCcAliasRequestCount } from "@/lib/db/ccDiscoveryMetrics";
|
|
|
|
// Virtual provider key combos are gated under (mirrors
|
|
// src/app/api/v1/models/ccAliasPredicate.ts — combos have no real provider id,
|
|
// so the gate addresses them via this synthetic key on BOTH the catalog and the
|
|
// request side; they must match or an alias could be listed yet rejected).
|
|
const CC_DISCOVERY_COMBO_PROVIDER_KEY = "combo";
|
|
const CC_DISCOVERY_PREFIX = "claude/";
|
|
|
|
/** Injectable lookups — real implementations in {@link resolveCcDiscoveryAliasStrip}. */
|
|
export interface CcDiscoveryResolveDeps {
|
|
/** Ids of the real "claude" OAuth provider's own models. */
|
|
claudeModelIds: Set<string>;
|
|
/** True when `prefix` is a built-in registry provider id or alias. */
|
|
isRegistryProvider(prefix: string): boolean;
|
|
/** Prefixes of operator-defined compatible provider nodes (DB) — Gap 1. */
|
|
customProviderPrefixes: Set<string>;
|
|
/** Combo row by name (or null). Only called for `claude/combo/<name>` ids. */
|
|
getCombo(name: string): Promise<{ models?: unknown[] } | null>;
|
|
gateGlobal(): boolean;
|
|
gateProvider(providerId: string): CcAliasSetting;
|
|
gateModel(providerId: string, modelId: string): CcAliasSetting;
|
|
}
|
|
|
|
type CcAliasTarget = {
|
|
/** `claude/combo/<name>` rather than `claude/<provider>/<model>`. */
|
|
isComboAlias: boolean;
|
|
comboName: string | null;
|
|
providerPrefix: string | null;
|
|
modelPart: string | null;
|
|
};
|
|
|
|
/** Split the part after `claude/` into either a combo name or a provider/model pair. */
|
|
function parseCcAliasTarget(rest: string): CcAliasTarget {
|
|
if (rest.startsWith("combo/")) {
|
|
return {
|
|
isComboAlias: true,
|
|
comboName: rest.slice("combo/".length),
|
|
providerPrefix: null,
|
|
modelPart: null,
|
|
};
|
|
}
|
|
const slashIndex = rest.indexOf("/");
|
|
if (slashIndex <= 0) {
|
|
return { isComboAlias: false, comboName: null, providerPrefix: null, modelPart: null };
|
|
}
|
|
return {
|
|
isComboAlias: false,
|
|
comboName: null,
|
|
providerPrefix: rest.slice(0, slashIndex),
|
|
modelPart: rest.slice(slashIndex + 1),
|
|
};
|
|
}
|
|
|
|
/** Resolve the three-level gate (model > provider > global) for a parsed alias target. */
|
|
function resolveGateFor(target: CcAliasTarget, deps: CcDiscoveryResolveDeps): boolean {
|
|
const gateProviderId = target.isComboAlias
|
|
? CC_DISCOVERY_COMBO_PROVIDER_KEY
|
|
: target.providerPrefix;
|
|
if (!gateProviderId) return deps.gateGlobal();
|
|
|
|
const modelKey = target.isComboAlias ? target.comboName : target.modelPart;
|
|
return resolveCcAliasEnabled({
|
|
model: deps.gateModel(gateProviderId, modelKey as string),
|
|
provider: deps.gateProvider(gateProviderId),
|
|
global: deps.gateGlobal(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Testable core: resolve a `claude/…` alias to its real id using injected
|
|
* lookups (no DB/registry access here — that lives in the production wrapper).
|
|
*/
|
|
export async function resolveCcDiscoveryAliasStripWith(
|
|
modelStr: string | null | undefined,
|
|
deps: CcDiscoveryResolveDeps
|
|
): Promise<CcDiscoveryStripResult> {
|
|
if (typeof modelStr !== "string" || !modelStr.startsWith(CC_DISCOVERY_PREFIX)) {
|
|
return { model: modelStr ?? "", stripped: false };
|
|
}
|
|
|
|
const rest = modelStr.slice(CC_DISCOVERY_PREFIX.length);
|
|
|
|
// Legitimate Claude OAuth provider model — never touch it.
|
|
if (deps.claudeModelIds.has(rest)) {
|
|
return { model: modelStr, stripped: false };
|
|
}
|
|
|
|
const target = parseCcAliasTarget(rest);
|
|
const combo = target.comboName ? await deps.getCombo(target.comboName) : null;
|
|
const comboExists = combo !== null && Array.isArray(combo?.models) && combo.models.length > 0;
|
|
const gateEnabled = resolveGateFor(target, deps);
|
|
|
|
// NOTE: no DB side effects here — this injectable core stays pure so its unit
|
|
// tests open no SQLite handle. The usage metric is recorded by the production
|
|
// wrapper `resolveCcDiscoveryAliasStrip` below, which already touches the DB.
|
|
return stripCcDiscoveryAlias(modelStr, {
|
|
isClaudeProviderModel: (r) => deps.claudeModelIds.has(r),
|
|
// Gap 1: recognize both built-in registry providers AND operator-defined
|
|
// compatible nodes, matching exactly the prefixes the catalog can mirror.
|
|
isKnownProviderPrefix: (prefix) =>
|
|
deps.isRegistryProvider(prefix) || deps.customProviderPrefixes.has(prefix),
|
|
hasCombo: () => comboExists,
|
|
aliasEnabledFor: () => gateEnabled,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* True when `prefix` names a provider the router can actually reach.
|
|
*
|
|
* Deliberately broader than the `open-sse` REGISTRY alone: enterprise-cloud
|
|
* providers such as `azure-ai` / `azure-openai` live only in the provider
|
|
* CATALOG (src/shared/constants/providers/…) yet route fine, so a registry-only
|
|
* check made the request path reject `claude/azure-ai/<model>` ids that the
|
|
* catalog had already advertised — see cc-discovery-alias-routable-prefix.test.ts.
|
|
*/
|
|
export function isRoutableProviderPrefix(prefix: string): boolean {
|
|
if (!prefix) return false;
|
|
if (getRegistryEntry(prefix) !== null) return true;
|
|
if (getProviderById(prefix) !== undefined) return true;
|
|
return getProviderByAlias(prefix) !== null;
|
|
}
|
|
|
|
/**
|
|
* Production entry point: build the real lookups and resolve. Cheap-exit for any
|
|
* id that does not start with `claude/` (the overwhelmingly common case) so a
|
|
* normal request pays only a single `startsWith` check.
|
|
*/
|
|
export async function resolveCcDiscoveryAliasStrip(
|
|
modelStr: string | null | undefined
|
|
): Promise<CcDiscoveryStripResult> {
|
|
if (typeof modelStr !== "string" || !modelStr.startsWith(CC_DISCOVERY_PREFIX)) {
|
|
return { model: modelStr ?? "", stripped: false };
|
|
}
|
|
|
|
const claudeModelIds = new Set(getModelsByProviderId("claude").map((m) => m.id));
|
|
|
|
// 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<string>();
|
|
try {
|
|
const nodes = (await getCachedProviderNodes()) as Array<{ prefix?: unknown }>;
|
|
for (const node of nodes) {
|
|
if (typeof node?.prefix === "string" && node.prefix) {
|
|
customProviderPrefixes.add(node.prefix);
|
|
}
|
|
}
|
|
} catch {
|
|
// A node-fetch failure just means custom prefixes are unavailable this call;
|
|
// built-in registry providers still resolve.
|
|
}
|
|
|
|
const { enabled: globalEnabled } = getCcAliasGlobalState();
|
|
|
|
const result = await resolveCcDiscoveryAliasStripWith(modelStr, {
|
|
claudeModelIds,
|
|
isRegistryProvider: (prefix) => isRoutableProviderPrefix(prefix),
|
|
customProviderPrefixes,
|
|
getCombo: (name) => getComboByName(name),
|
|
gateGlobal: () => globalEnabled,
|
|
gateProvider: (providerId) => getCcAliasProviderSetting(providerId),
|
|
gateModel: (providerId, modelId) => getCcAliasModelSetting(providerId, modelId),
|
|
});
|
|
|
|
// Best-effort usage metric — never blocks/slows the request, never throws
|
|
// (incrementCcAliasRequestCount already swallows its own errors). Lives in the
|
|
// production wrapper (not the injectable core) so the core's unit tests stay
|
|
// DB-free.
|
|
if (result.stripped) {
|
|
incrementCcAliasRequestCount(result.model);
|
|
}
|
|
|
|
return result;
|
|
}
|