fix(compression): apply compression combo assignments to routing combos (#7779)

* 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(compression): apply compression combo assignments to routing combos

Routing combos (e.g. codex, free-only, or-free) use provider-prefixed model
strings like codex/gpt-5.5 and go through handleSingleModelChat, which
passes comboName: null, isCombo: false. The compression combo assignment
lookup in chatCore.ts was gated behind if (isCombo && comboName), so
routing combos never had their compression combos applied.

Fix:
- Add routingComboId parameter threaded through handleSingleModelChat →
  executeChatWithBreaker → handleChatCore
- In handleChat(), resolve the routing combo UUID from the model string's
  provider prefix via getComboByName
- In chatCore.ts, change the gate to (isCombo && comboName) || routingComboId
  and add routingComboId to the lookup key array

Fixes #7771

* fix(autoCombo): guarantee positive maxOutputTokens fallback in computeAdvertisedLimits

GET /api/combos/auto now enumerates auto/<family> variants (auto/llama,
auto/glm, etc). computeAdvertisedLimits() already guaranteed a positive
contextLength for any non-empty candidate pool via getTokenLimit()'s
fallback chain, but had no equivalent fallback for maxOutputTokens —
candidates whose registry entry and models.dev sync data both lack that
field (common for no-auth/free-tier providers matching a family filter,
e.g. llama-* on groq/bazaarlink/etc) left maxOutputTokens null, which
tests/unit/auto-combo-context-advertising.test.ts catches as a contract
violation of the endpoint (opencode disables smart auto-compaction when
a limit is falsy — the same bug class this module's docstring already
describes for contextLength).

Fall back to a conservative generic default (4096) when no candidate in
the pool resolves a known maxOutputTokens, mirroring the existing
contextLength guarantee.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(autoCombo): align advertised max_output_tokens fallback with the catalog convention (8192)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): file-size baseline for chatHelpers routingComboId thread (876->877)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Erick Kinnee <erick@ekinnee.dev>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Erick Kinnee
2026-07-19 21:38:13 -05:00
committed by GitHub
parent f8dd12b721
commit eba6ecaf2b
5 changed files with 41 additions and 4 deletions

View File

@@ -280,7 +280,8 @@
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
"_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
"src/sse/handlers/chat.ts": 1797,
"src/sse/handlers/chatHelpers.ts": 876,
"_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
"src/sse/handlers/chatHelpers.ts": 877,
"src/sse/services/auth.ts": 2462,
"open-sse/executors/default.ts": 890,
"open-sse/translator/request/openai-responses.ts": 902,

View File

@@ -389,6 +389,7 @@ export async function handleChatCore({
comboName,
comboStrategy = null,
isCombo = false,
routingComboId = null,
comboStepId = null,
comboExecutionKey = null,
cachedSettings = null,
@@ -1146,11 +1147,11 @@ export async function handleChatCore({
compressionComboApplied = true;
return true;
};
if (isCombo && comboName) {
if ((isCombo && comboName) || routingComboId) {
try {
const { getComboByName } = await import("../../src/lib/localDb");
let comboConfig = await getComboByName(comboName);
if (!comboConfig && comboName.startsWith("combo/")) {
if (!comboConfig && comboName?.startsWith("combo/")) {
comboConfig = await getComboByName(comboName.substring(6));
}
const comboRuntimeConfig =
@@ -1185,7 +1186,8 @@ export async function handleChatCore({
const routingComboIds = [
comboConfig?.id,
comboName,
comboName.startsWith("combo/") ? comboName.substring(6) : null,
routingComboId,
comboName?.startsWith("combo/") ? comboName.substring(6) : null,
].filter((id): id is string => typeof id === "string" && id.length > 0);
if (routingComboIds.length > 0) {
const { getCompressionComboForRoutingCombo } =

View File

@@ -233,7 +233,19 @@ function getNoAuthCandidates(
*
* Unknown candidates resolve through getTokenLimit()'s fallback chain, so a
* non-empty pool always yields a positive contextLength.
*
* maxOutputTokens has no such guaranteed fallback in getResolvedModelCapabilities()
* — registry entries and models.dev sync data are both optional per model, so a
* candidate pool whose members all lack that specific field (e.g. #6453's
* provider-family combos, `auto/llama` and friends, over no-auth/free-tier
* registry entries that were never annotated with maxOutputTokens) would
* otherwise advertise `null`, which mirrors the `context: 0` bug this module's
* docstring describes for contextLength (opencode disables smart auto-compaction
* entirely when a limit is falsy). Fall back to a conservative generic default so
* a non-empty pool always yields a positive maxOutputTokens too.
*/
const DEFAULT_ADVERTISED_MAX_OUTPUT_TOKENS = 8192;
export function computeAdvertisedLimits(candidates: Array<{ provider: string; model: string }>): {
contextLength: number | null;
maxOutputTokens: number | null;
@@ -257,6 +269,9 @@ export function computeAdvertisedLimits(candidates: Array<{ provider: string; mo
maxOutputTokens = maxOutputTokens === null ? output : Math.max(maxOutputTokens, output);
}
}
if (maxOutputTokens === null) {
maxOutputTokens = DEFAULT_ADVERTISED_MAX_OUTPUT_TOKENS;
}
return { contextLength, maxOutputTokens };
}

View File

@@ -888,6 +888,20 @@ export async function handleChat(
telemetry.endPhase();
// Single model request
// Try to resolve routing combo from model prefix for compression combo lookup
let routingComboId: string | null = null;
if (!combo) {
const providerPrefix = resolvedModelStr.split("/")[0];
if (providerPrefix) {
try {
const { getComboByName } = await import("@/lib/localDb");
const routingCombo = await getComboByName(providerPrefix);
if (routingCombo?.id) {
routingComboId = routingCombo.id;
}
} catch {}
}
}
const response = await handleSingleModelChat(
body,
resolvedModelStr,
@@ -902,6 +916,7 @@ export async function handleChat(
forceLiveComboTest: isComboLiveTest,
forcedConnectionId: requestedConnectionId,
correlationId: reqId,
routingComboId,
reasoningDecision,
reasoningIntent,
reasoningRequestTags: requestRoutingTags.tags,
@@ -953,6 +968,7 @@ async function handleSingleModelChat(
cachedSettings?: any;
providerId?: string | null;
correlationId?: string | null;
routingComboId?: string | null;
modelPinned?: boolean;
reasoningDecision?: ReasoningRuleDecision | null;
reasoningIntent?: ExtractedReasoningIntent | null;
@@ -1380,6 +1396,7 @@ async function handleSingleModelChat(
skipUpstreamRetry: runtimeOptions.skipUpstreamRetry ?? false,
correlationId: runtimeOptions?.correlationId ?? null,
modelPinned: runtimeOptions?.modelPinned ?? false,
routingComboId: runtimeOptions?.routingComboId ?? null,
});
if (telemetry) telemetry.endPhase();

View File

@@ -395,6 +395,7 @@ export async function executeChatWithBreaker({
trafficType = "production",
correlationId = null,
modelPinned = false,
routingComboId = null,
}: ExecuteChatWithBreakerOptions): Promise<{ result: any; tlsFingerprintUsed: boolean }> {
let tlsFingerprintUsed = false;
const normalizedTrafficType: TrafficType =
@@ -432,6 +433,7 @@ export async function executeChatWithBreaker({
trafficType: normalizedTrafficType,
correlationId,
modelPinned,
routingComboId,
onCredentialsRefreshed: async (newCreds: any) => {
await updateProviderCredentials(credentials.connectionId, {
accessToken: newCreds.accessToken,