From 5dc9a8ad801577d795dd49d029df80ddeba8a2e3 Mon Sep 17 00:00:00 2001 From: Erick Kinnee Date: Sun, 19 Jul 2026 21:21:47 -0500 Subject: [PATCH] fix(api): enumerate tiered auto combo endpoints in /api/combos/auto (#7662) * fix(api): enumerate tiered auto combo endpoints in /api/combos/auto The backend already supports auto/[:] routing via suffixComposition.ts + virtualFactory.ts, but GET /api/combos/auto only exposed 6 flat variants. This adds a second loop enumerating the 10 curated AUTO_SUFFIX_VARIANTS (auto/coding:free, auto/coding:cheap, auto/coding:pro, auto/reasoning, auto/vision, etc.). Fixes #7619 * fix(combos): enumerate template and family auto variants in GET /api/combos/auto The endpoint was missing 27 auto variants that /v1/models already advertises, causing 404s when clients tried to use them: - 20 template variants (auto/best-coding, auto/pro-*, auto/claude-*, auto/best-free, etc.) - 7 family variants (auto/glm, auto/minimax, auto/mimo, auto/zai, auto/gemma, auto/llama, auto/gemini) Fixes #7619 Refs #6453 * fix(combos): swap Phase B/C ordering to match catalog.ts Template variants (Phase C) now enumerate before suffix variants (Phase B) so that overlapping ids like auto/reasoning and auto/vision use template resolution (variant-based) rather than suffix resolution (category-based), matching the behavior in catalog.ts. * fix(combos): fix comment labels and redundant as const * fix(api): fall back to a positive max_output_tokens for /api/combos/auto computeAdvertisedLimits() has no generic default for maxOutputTokens the way getTokenLimit() does for context length: when a combo's candidate pool is entirely unregistered models (e.g. a no-auth provider's model like duckduckgo-web/llama-4-scout), it legitimately returns null. The new template/suffix/family enumeration surfaces exactly that case (e.g. auto/llama), so /api/combos/auto advertised max_output_tokens: null and broke tests/unit/auto-combo-context-advertising.test.ts. Mirror the existing fallback already used by src/app/api/v1/models/catalog.ts (advertisedContextLength || 128000, advertisedMaxOutputTokens || 8192) at all 4 combo-push sites in this route so clients never see a disabling null/0 for a non-empty candidate pool. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): prefix #7662 fragment with markdown bullet Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Erick Kinnee Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- ...7662-combos-auto-enumerate-all-variants.md | 1 + src/app/api/combos/auto/route.ts | 137 +++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/7662-combos-auto-enumerate-all-variants.md diff --git a/changelog.d/fixes/7662-combos-auto-enumerate-all-variants.md b/changelog.d/fixes/7662-combos-auto-enumerate-all-variants.md new file mode 100644 index 0000000000..8bfb299c77 --- /dev/null +++ b/changelog.d/fixes/7662-combos-auto-enumerate-all-variants.md @@ -0,0 +1 @@ +- GET /api/combos/auto now enumerates template variants (auto/best-coding, auto/pro-_, auto/claude-_, auto/best-free) and family variants (auto/glm, auto/llama, auto/gemini, etc.) in addition to the flat and suffix variants it already exposed. Previously these 27 variants were advertised by /v1/models but missing from the combos endpoint, causing 404s when clients tried to use them. diff --git a/src/app/api/combos/auto/route.ts b/src/app/api/combos/auto/route.ts index 7c282dd046..8c35205af3 100644 --- a/src/app/api/combos/auto/route.ts +++ b/src/app/api/combos/auto/route.ts @@ -4,6 +4,12 @@ import { VALID_VARIANTS, type AutoVariant, } from "@omniroute/open-sse/services/autoCombo/autoPrefix"; +import { + AUTO_SUFFIX_VARIANTS, + AUTO_TEMPLATE_VARIANTS, + AUTO_FAMILY_IDS, +} from "@omniroute/open-sse/services/autoCombo/builtinCatalog"; +import { parseAutoSuffix } from "@omniroute/open-sse/services/autoCombo/suffixComposition"; const ALL_VARIANTS: Array<{ variant: AutoVariant | undefined; name: string }> = [ { variant: undefined, name: "Auto" }, @@ -23,11 +29,14 @@ export async function GET(request: Request) { await import("@omniroute/open-sse/services/autoCombo/virtualFactory"); const combos = []; + const seenIds = new Set(); for (const { variant, name } of ALL_VARIANTS) { try { const virtual = await createVirtualAutoCombo(variant); + const id = variant ? `auto/${variant}` : "auto"; + seenIds.add(id); combos.push({ - id: variant ? `auto/${variant}` : "auto", + id, name, variant: variant ?? null, type: "auto", @@ -36,8 +45,12 @@ export async function GET(request: Request) { candidateCount: virtual.candidatePool?.length ?? 0, // MAX of candidates' windows — consumers (opencode plugin) need a // real value here: advertising 0 disables client auto-compaction. - context_length: virtual.advertisedContextLength ?? null, - max_output_tokens: virtual.advertisedMaxOutputTokens ?? null, + // #7662: mirror catalog.ts's established fallback (advertisedMaxOutputTokens + // has no generic default in computeAdvertisedLimits() the way context length + // does — an all-unregistered candidate pool, e.g. a no-auth provider's model, + // otherwise advertises null and disables client auto-compaction). + context_length: virtual.advertisedContextLength || 128000, + max_output_tokens: virtual.advertisedMaxOutputTokens || 8192, config: virtual.config ?? {}, }); } catch { @@ -45,6 +58,124 @@ export async function GET(request: Request) { } } + // Phase B: enumerate template variants (auto/best-coding, auto/pro-*, + // auto/claude-*, auto/best-free, etc.) that the backend already supports + // via builtinCatalog.ts but were not exposed by this endpoint. + // Run BEFORE suffix variants so template resolution wins for overlapping + // ids (auto/reasoning, auto/vision), matching catalog.ts behavior. + for (const modelStr of Object.keys(AUTO_TEMPLATE_VARIANTS)) { + if (seenIds.has(modelStr)) continue; + try { + const variant = AUTO_TEMPLATE_VARIANTS[modelStr]; + const spec = modelStr === "auto/best-free" ? { tier: "free" as const } : undefined; + const virtual = await createVirtualAutoCombo(variant, spec); + + const displayName = variant + ? `Auto ${variant.charAt(0).toUpperCase() + variant.slice(1)}` + : "Auto Chat"; + + combos.push({ + id: modelStr, + name: displayName, + variant: null, + type: "auto", + isHidden: false, + candidatePool: virtual.candidatePool ?? [], + candidateCount: virtual.candidatePool?.length ?? 0, + // #7662: mirror catalog.ts's established fallback (advertisedMaxOutputTokens + // has no generic default in computeAdvertisedLimits() the way context length + // does — an all-unregistered candidate pool, e.g. a no-auth provider's model, + // otherwise advertises null and disables client auto-compaction). + context_length: virtual.advertisedContextLength || 128000, + max_output_tokens: virtual.advertisedMaxOutputTokens || 8192, + config: virtual.config ?? {}, + }); + seenIds.add(modelStr); + } catch { + // Individual variant failure — skip, don't break the whole list + } + } + + // Phase C: enumerate tiered `auto/[:]` variants + // (e.g. auto/coding:free, auto/reasoning:pro) that the backend already + // supports via suffixComposition.ts + virtualFactory.ts but were not + // exposed by this endpoint. + for (const modelStr of AUTO_SUFFIX_VARIANTS) { + if (seenIds.has(modelStr)) continue; + try { + const suffix = modelStr.slice("auto/".length); + const parsed = parseAutoSuffix(suffix); + if (!parsed.valid) continue; + + const virtual = await createVirtualAutoCombo(undefined, { + category: parsed.category, + tier: parsed.tier, + }); + + // Build a human-readable name from the category and tier + const catName = parsed.category + ? parsed.category.charAt(0).toUpperCase() + parsed.category.slice(1) + : ""; + const tierName = parsed.tier + ? `${parsed.tier.charAt(0).toUpperCase() + parsed.tier.slice(1)}` + : ""; + const displayName = tierName ? `${catName} ${tierName}` : catName; + + combos.push({ + id: modelStr, + name: `Auto ${displayName}`, + variant: null, + type: "auto", + isHidden: false, + candidatePool: virtual.candidatePool ?? [], + candidateCount: virtual.candidatePool?.length ?? 0, + // #7662: mirror catalog.ts's established fallback (advertisedMaxOutputTokens + // has no generic default in computeAdvertisedLimits() the way context length + // does — an all-unregistered candidate pool, e.g. a no-auth provider's model, + // otherwise advertises null and disables client auto-compaction). + context_length: virtual.advertisedContextLength || 128000, + max_output_tokens: virtual.advertisedMaxOutputTokens || 8192, + config: virtual.config ?? {}, + }); + seenIds.add(modelStr); + } catch { + // Individual variant failure — skip, don't break the whole list + } + } + + // Phase D: enumerate family variants (auto/glm, auto/llama, + // auto/gemini, etc.) that the backend already supports via modelFamily.ts + // but were not exposed by this endpoint. + for (const modelStr of AUTO_FAMILY_IDS) { + if (seenIds.has(modelStr)) continue; + try { + const suffix = modelStr.slice("auto/".length); + const virtual = await createVirtualAutoCombo(undefined, { family: suffix }); + + const displayName = `Auto ${suffix.charAt(0).toUpperCase() + suffix.slice(1)}`; + + combos.push({ + id: modelStr, + name: displayName, + variant: null, + type: "auto", + isHidden: false, + candidatePool: virtual.candidatePool ?? [], + candidateCount: virtual.candidatePool?.length ?? 0, + // #7662: mirror catalog.ts's established fallback (advertisedMaxOutputTokens + // has no generic default in computeAdvertisedLimits() the way context length + // does — an all-unregistered candidate pool, e.g. a no-auth provider's model, + // otherwise advertises null and disables client auto-compaction). + context_length: virtual.advertisedContextLength || 128000, + max_output_tokens: virtual.advertisedMaxOutputTokens || 8192, + config: virtual.config ?? {}, + }); + seenIds.add(modelStr); + } catch { + // Individual variant failure — skip, don't break the whole list + } + } + return NextResponse.json({ combos }); } catch (error) { console.error("Error fetching auto combos:", error);