Files
OmniRoute/src/lib/db/ccDiscoveryAliases.ts
Diego Rodrigues de Sa e Souza cd9a631464 feat: Claude Code discovery aliases (surface non-Claude models in the /model picker) (#8666)
* feat(db): cc discovery alias gate storage + EXPOSE_CC_DISCOVERY_ALIASES flag

Adds the gate for claude/<provider>/<model> discovery-alias mirror ids on
the /v1/models catalog: a new runtime feature flag (env forces on and wins
over the dashboard DB override), per-provider and per-model "on"/"off"/null
overrides stored in key_value under the ccDiscoveryAliases namespace, and a
pure precedence resolver (model > provider > global). Catalog wiring is a
separate follow-up task; this only lands the gate + storage.

* feat(sse): synthesize claude/ discovery aliases for the model catalog

* feat(api): advertise cc discovery aliases on /v1/models behind the 3-level gate

* feat(sse): resolve claude/ discovery aliases on the request path

* fix(sse): import getComboByName from db/combos, not the localDb barrel

* fix(sse): cover custom-node prefixes and the Codex WS bridge in cc discovery alias resolution

* feat(dashboard): cc discovery alias toggles + flag-screen env warning

Adds the operator-facing UI/API layer for the Claude Code discovery-alias
gate (claude/<provider>/<model> mirror ids on /v1/models): REST endpoint
for provider/model overrides, a provider-detail card with 3-state
(inherit/on/off) toggles, an info button on the Claude Code tool card
linking to Feature Flags, and an env-source warning on the
EXPOSE_CC_DISCOVERY_ALIASES flag card when it's forced on via env.

* feat(api): cc discovery usage metrics

* fix(api): record cc alias metric in the production wrapper + atomic counter upsert

* docs: document cc discovery aliases (Claude Code guide + feature flag catalog)

* fix(sse): don't mirror built-in auto/* combos as discovery aliases (advertised-but-unroutable)

* i18n(vi): translate the discovery-alias strings instead of shipping placeholders

vi is the one locale with a strict "no internal missing markers" test, so the 17
__MISSING__ entries this branch added (the provider ccAlias panel, the info
button, the feature-flag description and the env warning) would have turned that
test red the moment the base itself was repaired. Translated, keeping every ICU
placeholder ({modelId}, {error}) and the literal claude/<provider>/<model> id
shape intact.

* chore(quality): raise the frozen caps this feature legitimately grows

catalog.ts 1615 -> 1639: the alias synthesis is wired into the catalog builder,
which is where the per-key-filtered list is assembled — the only place the mirror
entries can be appended after model hiding has been applied.

localDb.ts 808 -> 810: two re-export lines for the new ccDiscoveryAliases db
module, which is exactly what the "Adding a New DB Module" recipe prescribes.

* refactor(dashboard,api): keep the complexity ratchets flat

The feature added four cyclomatic violations and one cognitive one, which the
ratchets reject — the baseline only moves when a metric improves. Split the new
code instead:

- appendCcDiscoveryAliases: the four skip-guards become isMirrorableId().
- resolveCcDiscoveryAliasStripWith: alias parsing and gate resolution become
  parseCcAliasTarget() and resolveGateFor(), replacing a chain of ternaries that
  each re-tested isComboAlias.
- FeatureFlagCard: the env-precedence warning becomes its own component instead
  of a conditional branch inside an already-large render.
- ProviderCcAliasSection: the loader moves to useCcAliasData(), and the override
  list and add-row become ModelOverrideList / AddOverrideRow, bringing both
  oversized functions back under the 80-line rule.

Behavior unchanged — the 74 discovery-alias tests pass untouched. Both ratchets
now sit exactly at baseline (2188 / 971).
2026-07-26 17:19:19 -03:00

163 lines
5.3 KiB
TypeScript

/**
* db/ccDiscoveryAliases.ts — Claude Code discovery-alias gate.
*
* Gates `claude/<provider>/<model>` mirror ids on the /v1/models catalog so
* the Claude Code gateway's model discovery can list non-Claude models. This
* module only owns the gate (global flag + per-provider/per-model overrides
* + precedence resolver) — the catalog wiring itself lives elsewhere.
*
* Storage: `key_value` table, namespace `ccDiscoveryAliases`, following the
* established key_value pattern (see `db/models.ts::setModelAlias` /
* `db/paramFilters.ts`). Keys:
* - `provider:<providerId>` -> "on" | "off"
* - `model:<providerId>/<modelId>` -> "on" | "off"
* A missing key means "inherit" (null).
*
* Global enablement precedence (see `getCcAliasGlobalState`):
* env EXPOSE_CC_DISCOVERY_ALIASES ("1"|"true") > DB flag override > default
* (env wins over the dashboard DB override, matching the brief — this is the
* opposite precedence from the generic `resolveFeatureFlag` helper, which is
* why this module resolves the flag directly instead of reusing it).
*/
import { getFeatureFlagOverride } from "./featureFlags";
import { getDbInstance } from "./core";
const NAMESPACE = "ccDiscoveryAliases";
const FLAG_KEY = "EXPOSE_CC_DISCOVERY_ALIASES";
export type CcAliasSetting = "on" | "off" | null;
function providerKey(providerId: string): string {
return `provider:${providerId}`;
}
function modelKey(providerId: string, modelId: string): string {
return `model:${providerId}/${modelId}`;
}
function parseSetting(value: string | undefined): CcAliasSetting {
if (value === "on" || value === "off") return value;
return null;
}
/**
* Pure precedence resolver: model setting wins over provider setting, which
* wins over the global flag. `null`/`undefined` means "inherit" from the
* next level down.
*/
export function resolveCcAliasEnabled(opts: {
model?: CcAliasSetting;
provider?: CcAliasSetting;
global: boolean;
}): boolean {
if (opts.model === "on") return true;
if (opts.model === "off") return false;
if (opts.provider === "on") return true;
if (opts.provider === "off") return false;
return opts.global;
}
export function getCcAliasProviderSetting(providerId: string): CcAliasSetting {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get(NAMESPACE, providerKey(providerId)) as { value: string } | undefined;
return parseSetting(row?.value);
}
export function setCcAliasProviderSetting(providerId: string, v: CcAliasSetting): void {
const db = getDbInstance();
const key = providerKey(providerId);
if (v === null) {
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(NAMESPACE, key);
return;
}
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
NAMESPACE,
key,
v
);
}
export function getCcAliasModelSetting(providerId: string, modelId: string): CcAliasSetting {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get(NAMESPACE, modelKey(providerId, modelId)) as { value: string } | undefined;
return parseSetting(row?.value);
}
export function setCcAliasModelSetting(
providerId: string,
modelId: string,
v: CcAliasSetting
): void {
const db = getDbInstance();
const key = modelKey(providerId, modelId);
if (v === null) {
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(NAMESPACE, key);
return;
}
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
NAMESPACE,
key,
v
);
}
/**
* Loads every provider/model override in a single query — intended for the
* catalog builder, which needs the full set rather than per-row lookups.
*/
export function getCcAliasSettingsBulk(): {
providers: Map<string, "on" | "off">;
models: Map<string, "on" | "off">;
} {
const db = getDbInstance();
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = ?")
.all(NAMESPACE) as Array<{ key: string; value: string }>;
const providers = new Map<string, "on" | "off">();
const models = new Map<string, "on" | "off">();
for (const row of rows) {
const setting = parseSetting(row.value);
if (setting === null) continue;
if (row.key.startsWith("provider:")) {
providers.set(row.key.slice("provider:".length), setting);
} else if (row.key.startsWith("model:")) {
models.set(row.key.slice("model:".length), setting);
}
}
return { providers, models };
}
function envForcesGlobalOn(): boolean {
const raw = process.env[FLAG_KEY];
return raw === "1" || raw === "true";
}
/**
* Resolves the effective global state, and where it came from.
* Precedence: env forces on > DB override > definition default.
*/
export function getCcAliasGlobalState(): { enabled: boolean; source: "env" | "db" | "default" } {
if (envForcesGlobalOn()) {
return { enabled: true, source: "env" };
}
const dbOverride = getFeatureFlagOverride(FLAG_KEY);
if (dbOverride !== undefined) {
return { enabled: dbOverride === "true" || dbOverride === "1", source: "db" };
}
return { enabled: false, source: "default" };
}
export function isCcAliasGlobalEnabled(): boolean {
return getCcAliasGlobalState().enabled;
}