diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index f0e4848b59..55b646b871 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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 = \"/\", 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, 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, diff --git a/src/app/api/v1/models/aliasBackedModels.ts b/src/app/api/v1/models/aliasBackedModels.ts new file mode 100644 index 0000000000..4d599107da --- /dev/null +++ b/src/app/api/v1/models/aliasBackedModels.ts @@ -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` 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 | null | undefined +): AliasBackedModel[] { + if (!aliases || typeof aliases !== "object") return []; + + const seen = new Set(); + 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; +} diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index aabacf77fa..2cc2c90c9c 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -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 `"/"`). + // 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; diff --git a/tests/unit/v1-models-aliases-included.test.ts b/tests/unit/v1-models-aliases-included.test.ts new file mode 100644 index 0000000000..eb59e45597 --- /dev/null +++ b/tests/unit/v1-models-aliases-included.test.ts @@ -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 +// `/` 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), []); + assert.deepEqual( + extractAliasBackedModels(undefined as unknown as Record), + [] + ); +}); + +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" }]); +});