From eba6ecaf2bd2b5b99b9a0c9797255838571ca6b5 Mon Sep 17 00:00:00 2001 From: Erick Kinnee Date: Sun, 19 Jul 2026 21:38:13 -0500 Subject: [PATCH] fix(compression): apply compression combo assignments to routing combos (#7779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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(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/ 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 Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- config/quality/file-size-baseline.json | 3 ++- open-sse/handlers/chatCore.ts | 8 +++++--- open-sse/services/autoCombo/virtualFactory.ts | 15 +++++++++++++++ src/sse/handlers/chat.ts | 17 +++++++++++++++++ src/sse/handlers/chatHelpers.ts | 2 ++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 763c8ae69e..d011602064 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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 791786 (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, diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 87d1e4650f..023c59c6b5 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -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 } = diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index 126b1650c3..a2638fd046 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -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 }; } diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index cfb4196ebc..194da49dd8 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -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(); diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 5cd5b90b95..bc3375cf91 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -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,