feat(api/v1): include alias-backed models in /v1/models listing (#4630)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-23 21:56:57 -03:00
committed by GitHub
parent be2ab9e419
commit 69703ee7db
4 changed files with 215 additions and 1 deletions

View File

@@ -8,6 +8,7 @@
"_rebaseline_2026_06_21_4481_websearch_routing": "Feature #4481 layer 2 own growth: src/sse/handlers/chat.ts 1491->1513 (+22 = the CCR-style web-search routing hook at the request entrypoint, right after the T05 task-aware-routing block — a 5-line comment + one `if (hasNativeWebSearchTool(body)) { ... }` guard that reads getCachedSettings only when a web_search tool is present and overrides resolvedModelStr/body.model via the pure helper, plus a 4-line import). When a request carries a native web_search server tool and the operator set `webSearchRouteModel`, the whole request routes to that model instead of the default (some providers, e.g. MiniMax, don't implement Anthropic's web_search_20250305 server tool). The detection + override logic lives in the new pure leaf open-sse/services/webSearchRouting.ts (no DB, unit-testable); chat.ts is thin wiring mirroring the adjacent T05 override. Lands BEFORE auto/combo resolution + the layer-1 webSearchFallback so the target's own format/fallback handling applies. Not extractable further (it IS the entrypoint wiring). Covered by tests/unit/web-search-tool-routing-4481.test.ts. Structural shrink of this handler tracked separately.",
"_rebaseline_2026_06_21_4483_auto_quota_cutoff": "PR #4483 (megamen32) own growth + review fix: open-sse/services/combo.ts 2611->2623 (+12). The PR adds an auto-routing hard quota cutoff in buildAutoCandidates (evaluateQuotaCutoff + buildAutoQuotaThresholds/clampPercent/asThresholdMap/quotaWindowLookupNames helpers) that drops low-quota candidates before scoring, plus a 429 guard when all candidates are below cutoff. On review (owner decision) the cutoff was made OPT-IN behind a new QuotaPreflightSettings.enabled flag (default OFF via QUOTA_PREFLIGHT_CUTOFF_ENABLED) so default behavior is unchanged, and the `...eligibleTargets` last-resort fallback the PR removed was restored (dedupe makes it identical to pre-cutoff when OFF; when ON a blocked target survives as final fallback rather than vanishing). Cohesive at the existing candidate-build/select chokepoints; not extractable. Covered by tests/unit/combo/auto-quota-cutoff.test.ts + tests/unit/resilience-settings-quota-preflight.test.ts (default-off + opt-in round-trip).",
"_rebaseline_2026_06_21_4475_target_format_badge": "PR #4475 (adivekar-utexas) review: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts 955->974 (+19). Extracted the pure targetFormatBadgeI18nKey (the 6 targetFormat value->i18n-key mapping) out of the CustomModelsSection.tsx badge so it is unit-testable outside the .tsx (Rule #18 gap — the PR had no UI test). The .tsx now calls the helper instead of an inline if-chain. This leaf is the strangler-fig home for pure provider-page helpers (#3501), so receiving the extraction is on-purpose. Covered by tests/unit/provider-target-format-badge-4475.test.ts.",
"_rebaseline_2026_06_22_port_730_alias_backed_models": "Port of decolua/9router#730 own growth: src/app/api/v1/models/catalog.ts 1493->1574 (+81 = one import + one alias-backed walk after the customModels block, mirroring the existing static/custom emission path: resolve canonical providerId, gate on blocked/active/hidden, push aliasId + optional canonical providerPrefixedId with the same vision-fields enrichment). Surfaces models registered ONLY through the modelAliases key_value namespace (value = \"<providerKey>/<modelId>\", e.g. setModelAlias(\"kimi-k2.6\", \"custom/kimi-k2.6\")) so compatible-provider entries that resolve at request time but were silently dropped from /v1/models are now discoverable by OpenAI clients. Pair-extract is in the new pure leaf src/app/api/v1/models/aliasBackedModels.ts (no DB import, unit-testable via tests/unit/v1-models-aliases-included.test.ts). Cohesive wiring next to the other per-source push blocks; not extractable further without hiding the per-source dedupe boundary. Structural shrink of this god-file tracked in #3789.",
"_rebaseline_2026_06_21_4424_exact_dup_dedupe": "Feature #4424 follow-up own growth: src/app/api/v1/models/catalog.ts 1486->1493 (+7 = the final exact-duplicate-id guard at the existing finalModels chokepoint — one import + a 4-line comment + one `finalModels = dedupeExactCatalogIds(finalModels)` call). #4427 added the opt-in prefix mode but the catalog still emitted 3 exact-duplicate ids (codex/gpt-5.5, veo-free/seedance, veo-free/veo each listed twice) because they originate from different push sources whose local guards don't see each other. The collapse logic lives in the new pure leaf src/app/api/v1/models/catalogDedupe.ts (no DB import, unit-testable). Keyed by listing identity (id, type, subtype) so the intentional same-id audio transcription/speech pair is preserved; keep-first, order-preserving, independent of MODELS_CATALOG_PREFIX_MODE. Thin cohesive wiring at the single serialization boundary; not extractable further. Covered by tests/unit/models-catalog-exact-dup-4424.test.ts. Structural shrink of this god-file tracked in #3789.",
"_rebaseline_2026_06_21_4427_low_noise_catalog": "PR #4427 (Rahulsharma0810) own growth: src/app/api/v1/models/catalog.ts 1478->1486 (+8). The opt-in MODELS_CATALOG_PREFIX_MODE (dual default | alias | canonical, with ?prefix= per-request override) gates the dual alias+canonical model emission at the three /v1/models push sites (static, synced, custom) behind includeAlias/includeCanonical, suppressing the duplicate cross-prefix entries (net +3 from the feature). On review, 4 incidental explanatory comments removed by the PR were restored (synced-models resolve, skip-static, try-block intent, strip-modelIdPrefix; +5) since their code is unchanged — useful docs on a non-trivial catalog function. Default `dual` keeps byte-identical output; request-side alias resolution unchanged. Structural shrink of this route tracked in #3789. Covered by tests/unit/models-catalog-low-noise-flag.test.ts.",
"_rebaseline_2026_06_21_phase2_active_selector": "Compression Phase 2 (named profiles + active selector) own growth: chatCore.ts 5110->5125 (+15 at the existing compression-dispatch chokepoint, just before the selectCompressionStrategy call). chatCore now loads the operator's named compression combos once into a `namedCombos` map (a best-effort try/catch dynamic-import of listCompressionCombos, debug-logged on failure) and passes it as the new `combos` arg to selectCompressionStrategy + selectCompressionPlan, plus an `&& !activeComboResolves(config, namedCombos)` guard term on the legacy default-combo block so the seeded default cannot shadow the operator's active profile. The resolver itself stays pure (the `combos` threading + activeComboResolves live in open-sse/services/compression/strategySelector.ts, <cap). The two UI files net SHRINK or stay flat: CompressionHub.tsx 521->277 (master toggle/mode/reorder removed, replaced by the thin active-profile selector + preview) and CompressionCombosPageClient.tsx grows a few lines under its frozen for the active badge. Cohesive wiring at the existing compression chokepoint, mirroring the prior compression rebaselines (#4217/#4210/#3890); not extractable without hiding the dispatch boundary. Structural shrink of chatCore.ts tracked in #3501. Covered by tests/unit/compression/active-combo-dispatch.test.ts + active-combo-integration.test.ts + tests/unit/ui/compressionHub-active-selector.test.tsx + namedCombos-active-badge.test.tsx.",
@@ -188,7 +189,7 @@
"src/app/api/providers/[id]/models/route.ts": 2590,
"src/app/api/providers/[id]/test/route.ts": 917,
"src/app/api/usage/analytics/route.ts": 941,
"src/app/api/v1/models/catalog.ts": 1493,
"src/app/api/v1/models/catalog.ts": 1574,
"src/lib/cloudflaredTunnel.ts": 934,
"src/lib/db/apiKeys.ts": 1662,
"src/lib/db/core.ts": 1825,

View File

@@ -0,0 +1,50 @@
/**
* Extract `{providerKey, modelId}` pairs from the `modelAliases` key_value namespace
* so `/v1/models` can surface models that were registered ONLY through a model alias
* (e.g. `setModelAlias("kimi-k2.6", "custom/kimi-k2.6")`).
*
* Port of upstream decolua/9router PR #730 — kept as a PURE helper so the route can
* walk the result and merge it into the per-provider catalog without dragging DB or
* Next-runtime imports into a unit test.
*
* Each stored alias value is `"<providerKey>/<modelId>"`. `providerKey` may be either
* a provider id (e.g. `openai`) or a provider alias / prefix (e.g. `cu`, `custom`);
* the caller is responsible for resolving that against the active connection map.
*
* The split is on the FIRST `/` only — OpenRouter-style ids contain an internal slash
* (`openrouter/anthropic/claude-3.5-sonnet`) and must keep their full sub-path as the
* model id.
*/
export interface AliasBackedModel {
providerKey: string;
modelId: string;
}
export function extractAliasBackedModels(
aliases: Record<string, unknown> | null | undefined
): AliasBackedModel[] {
if (!aliases || typeof aliases !== "object") return [];
const seen = new Set<string>();
const out: AliasBackedModel[] = [];
for (const value of Object.values(aliases)) {
if (typeof value !== "string") continue;
const trimmed = value.trim();
if (trimmed.length === 0) continue;
const slash = trimmed.indexOf("/");
if (slash <= 0 || slash === trimmed.length - 1) continue;
const providerKey = trimmed.slice(0, slash);
const modelId = trimmed.slice(slash + 1);
if (!providerKey || !modelId) continue;
const dedupeKey = `${providerKey}/${modelId}`;
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
out.push({ providerKey, modelId });
}
return out;
}

View File

@@ -8,7 +8,9 @@ import {
getSettings,
getProviderNodes,
getModelIsHidden,
getModelAliases,
} from "@/lib/localDb";
import { extractAliasBackedModels } from "./aliasBackedModels";
import { appendNoThinkingVariants } from "@omniroute/open-sse/utils/noThinkingAlias";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry";
import { getAllImageModels } from "@omniroute/open-sse/config/imageRegistry";
@@ -1323,6 +1325,88 @@ export async function getUnifiedModelsResponse(
console.log("Could not fetch custom models");
}
// Port of decolua/9router#730 — surface models registered ONLY through a model
// alias (`key_value` namespace `modelAliases`, value `"<providerKey>/<modelId>"`).
// Without this walk, a compatible-provider entry like `setModelAlias("kimi-k2.6",
// "custom/kimi-k2.6")` resolves at request time but never shows up in `/v1/models`.
// We respect the same gating as the static/custom listing path: provider must be
// active (or noAuth+unblocked), model must not be hidden, and the canonical alias
// entry must not already exist (so we don't shadow combo / synced / custom rows).
try {
const modelAliases = await getModelAliases();
const aliasBacked = extractAliasBackedModels(modelAliases);
for (const { providerKey, modelId } of aliasBacked) {
const canonicalProviderId = resolveCanonicalProviderId(providerKey);
if (!canonicalProviderId) continue;
if (
blockedProviders.has(providerKey) ||
blockedProviders.has(canonicalProviderId) ||
isNoAuthProviderBlocked(blockedProviders, canonicalProviderId, providerKey)
) {
continue;
}
const alias = providerIdToAlias[canonicalProviderId] || providerKey;
if (
!activeAliases.has(alias) &&
!activeAliases.has(canonicalProviderId) &&
!activeAliases.has(providerKey)
) {
continue;
}
if (getModelIsHidden(canonicalProviderId, modelId)) continue;
const aliasId = `${alias}/${modelId}`;
const rawPrefixedId = `${providerKey}/${modelId}`;
if (
models.some((m: any) => m?.id === aliasId) ||
models.some((m: any) => m?.id === rawPrefixedId)
) {
continue;
}
const visionFields =
getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(modelId);
if (includeAlias) {
models.push({
id: aliasId,
object: "model",
created: timestamp,
owned_by: canonicalProviderId,
permission: [],
root: modelId,
parent: null,
...(visionFields || {}),
});
}
if (
includeCanonical &&
canonicalProviderId !== alias &&
!isNoAuthProviderKey(canonicalProviderId) &&
prefixRoutesToProvider(canonicalProviderId, canonicalProviderId)
) {
const providerPrefixedId = `${canonicalProviderId}/${modelId}`;
if (models.some((m: any) => m?.id === providerPrefixedId)) continue;
const providerVisionFields =
getVisionCapabilityFields(providerPrefixedId) || getVisionCapabilityFields(modelId);
models.push({
id: providerPrefixedId,
object: "model",
created: timestamp,
owned_by: canonicalProviderId,
permission: [],
root: modelId,
parent: includeAlias ? aliasId : null,
...(providerVisionFields || {}),
});
}
}
} catch (e) {
console.log("Could not fetch model aliases");
}
// Add managed fallback models for compatible providers that don't import a model list.
for (const conn of connections) {
const providerId = typeof conn.provider === "string" ? conn.provider : null;

View File

@@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
// Port of upstream decolua/9router PR #730 — `/v1/models` must surface models added
// through alias mappings (key_value namespace `modelAliases`) so compatible-provider
// entries like `custom/kimi-k2.6` registered only via `setModelAlias("kimi-k2.6",
// "custom/kimi-k2.6")` are still discoverable by OpenAI clients.
//
// The catalog already iterates `PROVIDER_MODELS`, synced available models, and
// `customModels`. It never iterated `modelAliases`, so values pointing to bare
// `<providerAlias>/<modelId>` entries were silently dropped from the listing even
// though the model resolves at request time. This helper exposes the alias-backed
// pairs as a pure function so the route can merge them into the per-provider model
// list without changing the rest of the pipeline.
import { extractAliasBackedModels } from "../../src/app/api/v1/models/aliasBackedModels.ts";
test("returns the {providerKey, modelId} pair for each alias-backed entry", () => {
const aliases = {
"kimi-k2.6": "custom/kimi-k2.6",
"gpt-4o-mini-fast": "openai/gpt-4o-mini",
};
const out = extractAliasBackedModels(aliases);
assert.deepEqual(out.sort((a, b) => a.providerKey.localeCompare(b.providerKey)), [
{ providerKey: "custom", modelId: "kimi-k2.6" },
{ providerKey: "openai", modelId: "gpt-4o-mini" },
]);
});
test("ignores alias values that are not strings", () => {
const aliases = {
bad1: 42,
bad2: null,
bad3: { foo: "openai/gpt-4o" },
good: "anthropic/claude-3-5-sonnet",
};
const out = extractAliasBackedModels(aliases);
assert.deepEqual(out, [{ providerKey: "anthropic", modelId: "claude-3-5-sonnet" }]);
});
test("ignores alias values without a provider/model split", () => {
const aliases = {
no_slash: "just-a-model-id",
empty: "",
only_prefix: "openai/",
only_suffix: "/model",
};
const out = extractAliasBackedModels(aliases);
assert.deepEqual(out, []);
});
test("preserves the full sub-path when the model id itself contains a slash", () => {
// e.g. openrouter-style "owner/model" identifiers
const aliases = {
or_alias: "openrouter/anthropic/claude-3.5-sonnet",
};
const out = extractAliasBackedModels(aliases);
assert.deepEqual(out, [
{ providerKey: "openrouter", modelId: "anthropic/claude-3.5-sonnet" },
]);
});
test("returns an empty list for an empty / null / undefined input", () => {
assert.deepEqual(extractAliasBackedModels({}), []);
assert.deepEqual(extractAliasBackedModels(null as unknown as Record<string, unknown>), []);
assert.deepEqual(
extractAliasBackedModels(undefined as unknown as Record<string, unknown>),
[]
);
});
test("de-duplicates entries that resolve to the same {providerKey, modelId}", () => {
const aliases = {
primary: "custom/kimi-k2.6",
secondary: "custom/kimi-k2.6",
};
const out = extractAliasBackedModels(aliases);
assert.deepEqual(out, [{ providerKey: "custom", modelId: "kimi-k2.6" }]);
});