Files
OmniRoute/open-sse/services/comboConfig.ts
Diego Rodrigues de Sa e Souza 97aac6ac6c fix(ci): clear base-reds on release/v3.8.50 (round 4) (#10260)
* fix(ci): clear base-reds on release/v3.8.50 (round 4)

Drains the HARD failures reported by Release-Green run 31693210948 on issue #9985
(ESLint errors: 2) plus the merge-integrity red every open PR is inheriting.

- ESLint error 1: @omniroute/opencode-plugin/src/index.ts had a stray extra
  '});' (introduced by #9316) that broke parsing with 'unexpected file in NFT list'
  on the build path.
- ESLint error 2: cli-env-inline-comment-10100.test.ts used new Function to extract
  parseEnvValue from the bin entrypoint (no-new-func, Hard Rule #3). Extracted the
  helper to bin/cli/utils/parseEnvValue.mjs and import it from both the entrypoint
  and the test (same behavior, no eval).
- open-sse-typecheck (Fast Quality Gates): open-sse/utils/stream.ts imported
  sseCommentsEnabled twice (#9378) causing TS2300 Duplicate identifier; removed the
  duplicate import.
- Merge integrity (changelog + generated skills): skills/omni-settings/SKILL.md was
  edited manually by #10169 without updating the generator source, so
  check:agent-skills-sync failed on every PR (Generated: 1). Moved the curated
  thinking-budget content into a <!-- skill:custom-start --> block (the documented
  preservation mechanism), which the generator now keeps in sync.

Refs #9985

* fix(tests): align wave1-a poolside test with #10216 probed catalog

#10216 published Poolside's two authenticated-probe models
(poolside/laguna-xs-2.1, poolside/laguna-s-2.1) as static seeds, but the
wave1-a free-tier test still asserted 'no invented static model ids'
(entry.models === []), failing every open PR. Separate poolside from the
empty-models assertion and pin its probed catalog explicitly so a future
catalog change is a deliberate update, not a silent drift.

* fix(pack): register parseEnvValue.mjs in PACK_ARTIFACT_REQUIRED_PATHS

The extract of parseEnvValue to bin/cli/utils/parseEnvValue.mjs added a new
direct import to bin/omniroute.mjs, which pack-artifact-entrypoint-closures
enforces against PACK_ARTIFACT_REQUIRED_PATHS. Register the module so a future
tarball omission fails loudly.

* fix(combo): restore default same-model retry semantics after #10217

#10217 wired config.failoverBeforeRetry into the same-model retry guard in
both the priority/auto and round-robin loops, but DEFAULT_COMBO_CONFIG
defaulted the flag to true — flipping same-model retry off for every combo
that never touched the setting, not just the opt-in case. Round-4 bisect
(06f41cda63 vs d2fd88dfbc) reproduced this against
tests/unit/combo-499-abort.test.ts, tests/unit/combo-quota-exhaustion-only-fallback.test.ts
and tests/unit/combo-stream-readiness-fallback.test.ts. Flip the default to
false so the historical retry-before-failover behavior returns for combos
that never set the flag, while explicit opt-in (the two new tests #10217
added to combo-routing-engine.test.ts) still works.

* fix(quality): register visionBridge-responses-9597 in stryker tap.testFiles

check-mutation-test-coverage.mjs flagged tests/unit/guardrails/visionBridge-responses-9597.test.ts
as covering open-sse/services/combo/comboStructure.ts without being listed
in stryker.conf.json's tap.testFiles array. Add it so mutation coverage
attribution stays accurate.

* test(pack): expect parseEnvValue.mjs in the missing-artifact-paths fixture

The prior commit on this branch registered bin/cli/utils/parseEnvValue.mjs
in PACK_ARTIFACT_REQUIRED_PATHS but the "findMissingArtifactPaths flags
missing root runtime files in the tarball" test still hardcoded the old
expected list, so it never accounted for the new required path being
absent from the simulated tarball. Add it in its alphabetical slot.

* chore(lint): prune stale no-explicit-any suppression for call-log-file-rotation

--prune-suppressions found tests/unit/call-log-file-rotation.test.ts no
longer produces the 5 suppressed @typescript-eslint/no-explicit-any
warnings recorded in config/quality/eslint-suppressions.json. Remove the
dead entry so a regression would be caught again. Full-tree run with
--max-warnings 0 is clean: 0 errors, 0 warnings.

* fix(combo): decouple failoverBeforeRetry same-model guard from the skipUpstreamRetry default

Audit found that DEFAULT_COMBO_CONFIG.failoverBeforeRetry has defaulted to
true since before #10217 (predates #2417), and that value also feeds the
independent skipUpstreamRetry mechanism (src/sse/handlers/chat.ts:859,1126).
The previous commit on this branch flipped that default to false to fix the
#10217 same-model retry guard, which silently disabled skipUpstreamRetry's
own default-on behavior for every combo without an opt-in — a regression in
the opposite direction (executor-level retries before the loop's own
failover, changing latency/failure behavior).

Revert the default back to true and decouple the two mechanisms instead:
resolveComboConfig/resolveComboSetupConfig now also compute
failoverBeforeRetryExplicit, true only when a cascade layer (combo/provider/
global) literally sets failoverBeforeRetry to true — not merely inherited
from the default. The #10217 same-model retry guards in combo.ts (priority/
auto and round-robin loops) now read failoverBeforeRetryExplicit instead of
config.failoverBeforeRetry, restoring opt-in-only behavior for that guard
while the skipUpstreamRetry pass-through (config.failoverBeforeRetry at
combo.ts:1297,2865) is untouched and keeps its historical default-on.

* fix(combo,i18n): align getDefaultComboConfig with 10217 explicit flag; pt denoRelay entities

Two round-4 follow-ups exposed by the combinated base-red PR run:

1. comboConfig.ts: #10217 round-4 fix (104afeda4e) added
   failoverBeforeRetryExplicit to resolveComboConfig/resolveComboSetupConfig
   but getDefaultComboConfig() returned only DEFAULT_COMBO_CONFIG, so the
   combo-config.test.ts deepEqual (resolveComboConfig(null) ===
   getDefaultComboConfig()) failed on the extra field. Mirror the opt-in flag
   as false in the default.

2. pt.json: denoRelayOrgDomainHint still carried raw <app-name>/<org-slug>
   (the UNCLOSED_TAG RSC regression) — encode as &lt;...&gt; like the other
   42 locales, greening i18n-deno-relay-unclosed-tag.test.ts.

* chore(lint): disable @next/next/no-location-assign-relative-destination pending per-case review (#10292)

The eslint-config-next bump in #10043 shipped this new rule, flagging 6
pre-existing window.location.href navigations — several are deliberate
full-page reloads (login/logout state reset). Off with tracking issue
rather than a blanket router.push rewrite.

* fix(i18n): fill 439 missing UI keys (thinkingMode ×39 locales + pt catch-up) to restore 100% coverage

The #10169 Thinking Budget keys existed only in en/pt-BR/vi and the pt (PT-PT)
catalog from #10250 lagged 88 recent keys, dropping i18nUiCoverage to 99.3%
vs the frozen 100% ratchet baseline. Translated via the i18n:sync-ui marker
pipeline; glossary + ICU placeholder post-pass clean.

* fix(i18n): zh-TW glossary — replace retired 默認 with canonical 預設 in new thinkingMode keys

* chore(quality): rebase dead-code baseline 248 -> 409 for knip 6.32 bump (#10043)

dependabot #10043 upgraded knip 6.27 -> 6.32, which detects 162 MORE
genuinely-unused exports (331 vs 169) that 6.27 missed; DEAD_FILES
unchanged (78). Reproduced identically on the clean release/v3.8.50 tip
266e39d3 with a fresh 6.32 node_modules, so every PR is born red until
the tool change is absorbed. Owner authorized rebaseline (2026-08-13 via
PR #10260). Structural cleanup of the newly-surfaced dead exports remains
separate debt.

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-13 23:02:48 -03:00

375 lines
16 KiB
TypeScript

/**
* Combo Configuration Resolver
*
* Implements 3-layer cascade: Global Defaults → Provider Overrides → Per-Combo Config
* Most specific wins.
*/
import { MAX_TIMER_TIMEOUT_MS } from "../../src/shared/utils/runtimeTimeouts.ts";
import type { ComboCooldownWaitSettings } from "../../src/lib/resilience/settings.ts";
import type { ResponseValidationConfig } from "./combo/responseValidation.ts";
/**
* Maximum number of concurrent pre-screen checks (provider profile + availability)
* when running parallel pre-screening for priority strategy combos.
*/
export const PRE_SCREEN_CONCURRENCY = 5;
/**
* Default per-target timeout for combo fallback when a combo does not set its own
* `targetTimeoutMs`. Combos exist to fail over fast, so inheriting the full upstream
* request timeout (FETCH_TIMEOUT_MS, 600s by default) made a single hung target stall
* the whole combo for up to 10 minutes before falling through to the next model
* (escalated cmqlrhd7c). For STREAMING requests this only bounds the time-to-first-headers
* — token generation streams after the response resolves, so it is NOT cut short. Operators
* can still raise it per-combo via `targetTimeoutMs` (capped at the upstream ceiling), or set
* a longer value for slow non-streaming reasoning combos.
*/
export const DEFAULT_COMBO_TARGET_TIMEOUT_MS = 120_000;
/**
* Small buffer added on top of the combo-cooldown-wait budget (see below) when deriving
* the per-target timeout floor for wait-eligible combos. The wait itself is bounded by
* `resilienceSettings.comboCooldownWait.budgetMs`; this buffer only needs to cover the
* dispatch overhead between the wait resolving and the upstream response headers
* arriving (streaming responses aren't cut short past that point — see
* DEFAULT_COMBO_TARGET_TIMEOUT_MS above), not a full generation.
*/
export const COMBO_TARGET_TIMEOUT_WAIT_BUFFER_MS = 10_000;
/**
* Whether a combo's cooldown-aware wait+retry (#7360 / #7301) engages for this request.
* When the operator has the feature enabled, EVERY combo strategy waits out a short
* transient cooldown instead of crystallizing a 429 into a combo-level failure.
* Shared by combo.ts (to decide whether to wait) and comboSetup.ts (to size the
* per-target timeout floor so it doesn't cut the wait off early — see
* resolveComboTargetTimeoutMsForCombo below).
*
* `strategy` is retained in the signature for call-site clarity; eligibility is
* strategy-agnostic (the wait path in combo.ts already uses the real model-lockout
* reason for every strategy).
*/
export function isComboCooldownWaitEligible(
_strategy: string,
comboCooldownWait: Pick<ComboCooldownWaitSettings, "enabled">
): boolean {
return comboCooldownWait.enabled;
}
/**
* Per-target timeout floor to use for a combo, accounting for the cooldown-wait budget.
* When the combo is wait-eligible (see isComboCooldownWaitEligible), a single target's
* dispatch can legitimately wait out cooldowns for up to `comboCooldownWait.budgetMs`
* before it resolves — so the per-target timeout must never be shorter than that budget,
* or the wait gets cut off mid-retry and the target times out with a synthetic 504
* (`combo_target_timeout`, open-sse/services/combo/targetTimeoutRunner.ts) instead of completing the wait. This
* only raises the *default* floor; an operator's explicit `targetTimeoutMs` on the combo
* still wins (see resolveComboTargetTimeoutMs).
*/
export function resolveComboTargetTimeoutMsForCombo(
config: Record<string, unknown> | null | undefined,
upstreamTimeoutMs: number,
strategy: string,
comboCooldownWait: Pick<ComboCooldownWaitSettings, "enabled" | "budgetMs">
): number {
const defaultTimeoutMs = isComboCooldownWaitEligible(strategy, comboCooldownWait)
? Math.max(
DEFAULT_COMBO_TARGET_TIMEOUT_MS,
comboCooldownWait.budgetMs + COMBO_TARGET_TIMEOUT_WAIT_BUFFER_MS
)
: DEFAULT_COMBO_TARGET_TIMEOUT_MS;
return resolveComboTargetTimeoutMs(config, upstreamTimeoutMs, defaultTimeoutMs);
}
/**
* Default pre-cascade semaphore queue depth for round-robin combos (#3872). When a
* combo member's concurrency slot is saturated, this many requests wait in the
* member's queue before `SEMAPHORE_QUEUE_FULL` triggers a cascade to the next member.
* Kept at 20 for backward compatibility; operators wanting faster failover can lower
* it (0 = never queue, fail over to the next member immediately).
*/
export const DEFAULT_COMBO_QUEUE_DEPTH = 20;
/** Upper bound for the configurable combo queue depth (defensive clamp). */
export const MAX_COMBO_QUEUE_DEPTH = 100;
const DEFAULT_COMBO_CONFIG = {
strategy: "priority",
maxRetries: 1,
retryDelayMs: 2000,
fallbackDelayMs: 0,
// #9100: round-robin combo concurrency was hard-capped at 3 concurrent
// requests per model with no override — 5 concurrent requests through a
// round-robin combo serialized behind that cap. Now configurable via
// COMBO_CONCURRENCY_PER_MODEL (validated to >= 1, clamped to <= 32; default
// 3 preserves the historical behavior).
concurrencyPerModel: Math.min(
Math.max(Number(process.env.COMBO_CONCURRENCY_PER_MODEL) || 3, 1),
32
),
queueTimeoutMs: 120000, // max wait time in semaphore queue (round-robin); raised from 30s for browser-automation providers like gemini-web (#9407)
queueDepth: DEFAULT_COMBO_QUEUE_DEPTH, // pre-cascade semaphore queue depth (round-robin, #3872)
handoffThreshold: 0.85,
handoffModel: "",
handoffProviders: ["codex"],
maxMessagesForSummary: 30,
maxComboDepth: 3,
nestedComboMode: "flatten",
trackMetrics: true,
reasoningTokenBufferEnabled: true,
manifestRouting: false,
// Complexity-aware auto routing (2026): when on, the auto router scores
// candidates by how well their tier matches the request's classified
// difficulty (feeds tierAffinity/specificityMatch). Opt-in — off by default.
complexityAwareRouting: false,
resetAwareSessionWeight: 0.35,
resetAwareWeeklyWeight: 0.65,
resetAwareTieBandPercent: 5,
resetAwareExhaustionGuardPercent: 10,
// Historical default (predates #2417/#10217) — true. This value feeds TWO
// independent mechanisms and must stay true-by-default for one of them:
// 1. skipUpstreamRetry (src/sse/handlers/chat.ts:859,1126) — the
// lower-level executor retry skip. Always default-on; changing this
// default flips that mechanism's behavior for every combo, not just
// opted-in ones.
// 2. The #10217 same-model retry guard in this file's combo.ts callers
// (priority/auto + round-robin loops) — meant to be OPT-IN only. That
// guard must NOT read this field directly; it consults the sibling
// `failoverBeforeRetryExplicit` flag computed below in
// resolveComboConfig/resolveComboSetupConfig, which is true only when
// an actual cascade layer (combo/provider/global) set the flag to
// true, not merely inherited from this default. See round-4 base-red
// bisect (06f41cda63 vs d2fd88dfbc) — flipping THIS default to false
// "fixed" mechanism 2 but silently broke mechanism 1's default-on
// behavior for every combo without an explicit opt-in.
failoverBeforeRetry: true,
// Feature 4985: configurable response-body validation predicate (per-combo). When set,
// a 200 OK whose body fails the predicate fails over to the next target.
responseValidation: undefined as ResponseValidationConfig | undefined,
maxSetRetries: 0,
setRetryDelayMs: 2000,
// Zero-latency optimizations are opt-in because some modes can race targets or
// mutate fallback request bodies for lower tail latency.
zeroLatencyOptimizationsEnabled: false,
// Hedging (Speculative Execution) defaults
hedging: false,
hedgeDelayMs: 500,
// Mid-Stream Fallback Compression defaults
fallbackCompressionMode: "lite",
fallbackCompressionThreshold: 1000,
// Predictive TTFT Circuit Breaker defaults
predictiveTtftMs: 0,
// Pipeline defaults
pipeline_enabled: false,
task_detection: "pattern",
max_reflection_loops: 1,
skip_pipeline_for_tokens_under: 50,
pipeline_fallback: "single-provider",
resetAwareQuotaCacheTtlMs: 0,
resetAwareQuotaCacheMaxStaleMs: 0,
// Global combo timeout (0 = disabled). When set, limits the total wall-clock time
// the combo spends iterating through targets. After each target completes, if the
// elapsed time exceeds comboTimeoutMs, remaining targets are skipped and a 504 with
// aggregated error diagnostics is returned. Backward-compatible: 0 preserves the
// legacy unlimited-iteration behavior.
comboTimeoutMs: 0,
shadowRouting: {
enabled: false,
targets: [],
sampleRate: 1,
maxTargets: 2,
timeoutMs: 30000,
},
evalRouting: {
enabled: false,
suiteIds: [],
maxAgeHours: 720,
minCases: 1,
qualityWeight: 0.85,
latencyWeight: 0.15,
cacheTtlMs: 60000,
},
// Context window requirements for combo target filtering/sorting (undefined by
// default — declared here so resolveComboSetupConfig's inferred return type
// includes the key; combo.ts reads config.contextRequirements).
contextRequirements: undefined as
| {
minContextWindow?: number;
maxContextWindow?: number;
preferLargeContext?: boolean;
contextFilterMode?: "strict" | "lenient";
}
| undefined,
};
const LEGACY_COMBO_RESILIENCE_KEYS = new Set([
"timeoutMs",
"healthCheckEnabled",
"healthCheckTimeoutMs",
]);
type ComboConfigRecord = Record<string, unknown>;
type ComboConfigLike =
| {
config?: ComboConfigRecord | null;
}
| null
| undefined;
type ComboSettingsLike =
| {
comboDefaults?: ComboConfigRecord | null;
providerOverrides?: Record<string, ComboConfigRecord | null | undefined> | null;
}
| null
| undefined;
function isRecord(value: unknown): value is ComboConfigRecord {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function normalizePositiveTimeoutMs(value: unknown): number {
const numericValue = Number(value);
if (!Number.isFinite(numericValue) || numericValue <= 0) return 0;
return Math.min(Math.floor(numericValue), MAX_TIMER_TIMEOUT_MS);
}
export function resolveComboTargetTimeoutMs(
config: Record<string, unknown> | null | undefined,
upstreamTimeoutMs: number,
defaultTimeoutMs: number = 0
): number {
const ceilingTimeoutMs = normalizePositiveTimeoutMs(upstreamTimeoutMs);
const configuredTimeoutMs = isRecord(config)
? normalizePositiveTimeoutMs(config.targetTimeoutMs)
: 0;
// Explicit per-combo config: honour it, but never extend past the upstream ceiling.
if (configuredTimeoutMs > 0) {
if (ceilingTimeoutMs <= 0) return configuredTimeoutMs;
return Math.min(configuredTimeoutMs, ceilingTimeoutMs);
}
// Unset config: fall back to the saner combo default (when provided) so a hung target
// fails over fast instead of inheriting the full upstream timeout. Never exceed the
// ceiling. When no default is given OR the upstream timeout is disabled (0 = unbounded),
// preserve the legacy "inherit the upstream ceiling" behavior.
const fallbackDefaultMs = normalizePositiveTimeoutMs(defaultTimeoutMs);
if (ceilingTimeoutMs <= 0) return ceilingTimeoutMs;
if (fallbackDefaultMs <= 0) return ceilingTimeoutMs;
return Math.min(fallbackDefaultMs, ceilingTimeoutMs);
}
/**
* Resolve the effective pre-cascade semaphore queue depth for a round-robin combo
* (#3872). Falls back to `DEFAULT_COMBO_QUEUE_DEPTH` for missing/invalid/negative
* values and clamps to `MAX_COMBO_QUEUE_DEPTH`. `0` is valid and meaningful: it makes
* a saturated combo member fail over to the next member immediately instead of queueing.
*/
export function resolveComboQueueDepth(config: Record<string, unknown> | null | undefined): number {
const raw = isRecord(config) ? Number(config.queueDepth) : Number.NaN;
if (!Number.isFinite(raw) || raw < 0) return DEFAULT_COMBO_QUEUE_DEPTH;
return Math.min(Math.floor(raw), MAX_COMBO_QUEUE_DEPTH);
}
/**
* Resolve effective config for a combo, applying cascade:
* DEFAULT_COMBO_CONFIG → settings.comboDefaults → settings.providerOverrides[provider] → combo.config
*
* @param {Object} combo - The combo object { config, ... }
* @param {Object} settings - App settings from localDb
* @param {string} [provider] - Optional provider to apply provider-level overrides
* @returns {Object} Resolved config
*/
export function resolveComboConfig(
combo: ComboConfigLike,
settings: ComboSettingsLike,
provider?: string | null
) {
const global = settings?.comboDefaults || {};
const providerOverride = provider ? settings?.providerOverrides?.[provider] || {} : {};
const comboConfig = combo?.config || {};
// Clean undefined values before spreading
const clean = (obj: ComboConfigRecord) =>
Object.fromEntries(
Object.entries(obj).filter(
([key, value]) =>
value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key)
)
);
const cleanGlobal = clean(global);
const cleanProviderOverride = clean(providerOverride);
const cleanComboConfig = clean(comboConfig);
const merged = {
...DEFAULT_COMBO_CONFIG,
...cleanGlobal,
...cleanProviderOverride,
...cleanComboConfig,
};
// #10217 round-4 fix: `failoverBeforeRetry` defaults to true (see comment on
// DEFAULT_COMBO_CONFIG above) and feeds two independent mechanisms. Callers
// that gate the OPT-IN same-model retry guard (combo.ts) must NOT read
// `merged.failoverBeforeRetry` directly — that stays true unless a layer
// explicitly disables it, which can't distinguish "inherited default" from
// "operator opted in". This flag is true only when some cascade layer
// literally set the value to true, i.e. a genuine opt-in.
const failoverBeforeRetryExplicit =
cleanComboConfig.failoverBeforeRetry === true ||
cleanProviderOverride.failoverBeforeRetry === true ||
cleanGlobal.failoverBeforeRetry === true;
return {
...merged,
failoverBeforeRetryExplicit,
shadowRouting: {
...DEFAULT_COMBO_CONFIG.shadowRouting,
...(isRecord(global.shadowRouting) ? clean(global.shadowRouting) : {}),
...(isRecord(providerOverride.shadowRouting) ? clean(providerOverride.shadowRouting) : {}),
...(isRecord(comboConfig.shadowRouting) ? clean(comboConfig.shadowRouting) : {}),
},
evalRouting: {
...DEFAULT_COMBO_CONFIG.evalRouting,
...(isRecord(global.evalRouting) ? clean(global.evalRouting) : {}),
...(isRecord(providerOverride.evalRouting) ? clean(providerOverride.evalRouting) : {}),
...(isRecord(comboConfig.evalRouting) ? clean(comboConfig.evalRouting) : {}),
},
};
}
/**
* Get the default combo config (used when no overrides exist)
*/
export function getDefaultComboConfig() {
return {
...DEFAULT_COMBO_CONFIG,
// Mirror resolveComboConfig's opt-in flag so a deepEqual against the
// default stays consistent (#10217 round-4 fix). With no cascade layer
// setting the flag, it is a genuine non-opt-in → false.
failoverBeforeRetryExplicit: false,
};
}
/**
* Resolve the effective combo config the same way handleComboChat does: cascade via
* resolveComboConfig when settings exist, else the defaults merged with the combo's own
* config. Encapsulated here so the ternary lives in one place (DRY) and its inferred union
* return type is the single source of truth for ComboContext.config (combo/context.ts).
*/
export function resolveComboSetupConfig(combo: ComboConfigLike, settings: ComboSettingsLike) {
if (settings) return resolveComboConfig(combo, settings);
const comboConfig = (combo?.config as Record<string, unknown>) || {};
return {
...getDefaultComboConfig(),
...comboConfig,
// See resolveComboConfig's failoverBeforeRetryExplicit comment — same
// distinction applies here (no `settings`, so only the combo's own config
// can opt in).
failoverBeforeRetryExplicit: comboConfig.failoverBeforeRetry === true,
};
}