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/<category>[:<tier>] 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 <erick@ekinnee.dev>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Erick Kinnee
2026-07-19 21:21:47 -05:00
committed by GitHub
parent 2acc8e84db
commit 5dc9a8ad80
2 changed files with 135 additions and 3 deletions

View File

@@ -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.

View File

@@ -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<string>();
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/<category>[:<tier>]` 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);