diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 02ed1822c6..29575e70f9 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_23_4774_combo_legacy_strip": "PR #4774 (KooshaPari, #4382 round-trip) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4434->4456 (+22 = the client-side LEGACY_COMBO_RESILIENCE_KEYS Set gains queueTimeoutMs + the 12 v3.8.31-era removed keys (queueDepth/fallbackDelayMs/handoffProviders/maxComboDepth/manifestRouting/complexityAwareRouting/pipeline_enabled/pipelineConcurrency/shadowRouting/evalRouting/resetAwareEnabled/resetAwareWindow) with explanatory comments, mirroring the server-side strip list in src/app/api/combos/[id]/route.ts so the modal never re-introduces removed keys on Save). Functional strip list, not a movable block; combos/page.tsx structural shrink tracked in #3501. Covered by tests/unit/combo-config.test.ts (auto-promote + passthrough + legacy-key round-trip).", "_rebaseline_2026_06_21_4421_node_lookup": "Issue #4421 own growth: src/lib/db/providers.ts 1050->1063 (+13 = resolveProviderNodeForConnection at the existing provider-node lookup — resolves a connection node by exact id OR the bare derived type when unambiguous, + import). Pure selection logic in the new src/lib/db/providerNodeSelect.ts (2289 (+10): #4530 só fiou maxCooldownMs nos 3 sites de combo.ts; os 4 sites de markAccountUnavailable (per-model quota, grok-web 403, per-model 403, local 404) nunca passavam o cap → resolvo mlSettings uma vez e passo maxCooldownMs em todos. tests/unit/db-core-init.test.ts 864->867 (+3): comentário explicando o cap intencional busy_timeout 5s->2s do v3.8.32. Wiring necessário ao chokepoint de lockout; não extraível. Coberto por model-lockout-max-cooldown.test.ts + db-core-init.test.ts. (Demais base-reds — áudio mp3 #912/#913 dedup de handler em geminiHelper.ts, e closure #3578 src/models/ no package.json files — não cresceram arquivo congelado.)", "_rebaseline_2026_06_21_v3833_cycle_open_latent_filesize": "Abertura do ciclo v3.8.33: 4 arquivos cresceram no ciclo v3.8.32 sem bump de baseline e o drift escapou do fast-path do release (check:file-size não roda nas fast-gates p/ release/*, só no PR→main full CI, e o crescimento veio de commits entre fca66c644 e o head do merge 912239f46 — ex. #4475 targetFormat). Medido em origin/main (idêntico, cherry-picks deste ciclo NÃO tocam estes 4): open-sse/services/usage.ts 3408->3414, src/lib/db/core.ts 1820->1825, src/lib/usage/providerLimits.ts 949->950, src/shared/constants/providers.ts 3242->3243. Reconcílio ao valor real de main p/ abrir o .33 verde; shrink estrutural rastreado em #3501.", @@ -153,7 +154,7 @@ "src/app/(dashboard)/dashboard/cache/page.tsx": 841, "src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900, "src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 922, - "src/app/(dashboard)/dashboard/combos/page.tsx": 4434, + "src/app/(dashboard)/dashboard/combos/page.tsx": 4456, "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1495, "src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007, "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570, diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 5906352812..b64dcbd959 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -174,9 +174,31 @@ const ADVANCED_FIELD_HELP_FALLBACK = { }; const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ + // UI-removed knobs (replaced by per-target timeoutMs on each step) "timeoutMs", "healthCheckEnabled", "healthCheckTimeoutMs", + // queueTimeoutMs is still in the schema but the dashboard UI no longer surfaces + // it; carrying it forward through edit+save leaves a stale knob in the modal + // that surprises operators. Strip it pre-PUT so the persisted config matches + // what the UI is currently able to display. + "queueTimeoutMs", + // Keys that were present in v3.8.31-era combo configs but have since been + // removed from comboRuntimeConfigSchema. Mirrors the server-side strip list + // in src/app/api/combos/[id]/route.ts so the modal never re-introduces them + // when the user clicks Save. See #4382 (combo update returns 400). + "queueDepth", + "fallbackDelayMs", + "handoffProviders", + "maxComboDepth", + "manifestRouting", + "complexityAwareRouting", + "pipeline_enabled", + "pipelineConcurrency", + "shadowRouting", + "evalRouting", + "resetAwareEnabled", + "resetAwareWindow", ]); const MS_PER_SECOND = 1000; diff --git a/src/app/api/combos/[id]/route.ts b/src/app/api/combos/[id]/route.ts index 4c25beda01..22dff324e4 100644 --- a/src/app/api/combos/[id]/route.ts +++ b/src/app/api/combos/[id]/route.ts @@ -18,6 +18,47 @@ import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { buildErrorBody } from "@omniroute/open-sse/utils/error"; import { QUOTA_MODEL_PREFIX } from "@/lib/quota/quotaModelNaming"; +/** + * Keys that were present in older combo configs (≤ v3.8.31) but have since been + * removed from comboRuntimeConfigSchema. The dashboard modal sanitises the three + * UI-level keys (timeoutMs, healthCheckEnabled, healthCheckTimeoutMs) before PUT, + * but v3.8.31-era stored configs also carry these 12 keys which were spread back + * into the body on edit+save. We strip them server-side so removed keys don't + * accumulate in `combos.data` and so the next read produces a clean config. + * + * Idempotent — running twice is a no-op. + */ +const LEGACY_REMOVED_COMBO_CONFIG_KEYS = Object.freeze([ + "queueDepth", + "fallbackDelayMs", + "handoffProviders", + "maxComboDepth", + "manifestRouting", + "complexityAwareRouting", + "pipeline_enabled", + "pipelineConcurrency", + "shadowRouting", + "evalRouting", + "resetAwareEnabled", + "resetAwareWindow", +]); + +function stripLegacyComboConfigKeys(rawConfig) { + if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) { + return rawConfig; + } + let mutated = false; + const next = {}; + for (const [key, value] of Object.entries(rawConfig)) { + if (LEGACY_REMOVED_COMBO_CONFIG_KEYS.includes(key)) { + mutated = true; + continue; + } + next[key] = value; + } + return mutated ? next : rawConfig; +} + // GET /api/combos/[id] - Get combo by ID export async function GET(request, { params }) { const authError = await requireManagementAuth(request); @@ -97,6 +138,9 @@ export async function PUT(request, { params }) { normalizedUpdate.config = nextConfig; delete normalizedUpdate.compressionOverride; } + if (normalizedUpdate.config && typeof normalizedUpdate.config === "object") { + normalizedUpdate.config = stripLegacyComboConfigKeys(normalizedUpdate.config); + } const body = normalizedUpdate.models ? { diff --git a/src/lib/db/migrations/103_strip_legacy_combo_config_keys.sql b/src/lib/db/migrations/103_strip_legacy_combo_config_keys.sql new file mode 100644 index 0000000000..7a702dd122 --- /dev/null +++ b/src/lib/db/migrations/103_strip_legacy_combo_config_keys.sql @@ -0,0 +1,54 @@ +-- 103_strip_legacy_combo_config_keys.sql +-- One-shot sweep over `combos.data` to remove v3.8.31-era config keys that were +-- subsequently dropped from comboRuntimeConfigSchema. Without this sweep, a +-- combo created on ≤ v3.8.31 still carries the legacy keys in its persisted +-- JSON; on the next edit+save the modal spreads the existing config back into +-- the PUT body, and comboRuntimeConfigSchema.strict() rejects the unknown +-- keys with a 400. See diegosouzapw/OmniRoute#4382. +-- +-- Belt-and-suspenders: +-- - src/shared/validation/schemas/combo.ts now uses .passthrough() so the +-- server accepts unknown legacy keys during the upgrade window +-- - src/app/api/combos/[id]/route.ts strips the same keys before persistence +-- so new writes are clean +-- - src/app/(dashboard)/dashboard/combos/page.tsx strips them client-side +-- +-- This migration handles pre-existing rows. It is idempotent: running it again +-- on a clean DB no-ops because json_remove on a missing path is a no-op, and +-- the WHERE clause skips rows that don't carry any of the legacy keys. + +-- Strip the 12 known removed keys from any persisted combo config. +UPDATE combos +SET data = json_remove( + data, + '$.config.queueDepth', + '$.config.fallbackDelayMs', + '$.config.handoffProviders', + '$.config.maxComboDepth', + '$.config.manifestRouting', + '$.config.complexityAwareRouting', + '$.config.pipeline_enabled', + '$.config.pipelineConcurrency', + '$.config.shadowRouting', + '$.config.evalRouting', + '$.config.resetAwareEnabled', + '$.config.resetAwareWindow' +) +WHERE EXISTS ( + SELECT 1 + FROM json_each(data, '$.config') AS cfg + WHERE cfg.key IN ( + 'queueDepth', + 'fallbackDelayMs', + 'handoffProviders', + 'maxComboDepth', + 'manifestRouting', + 'complexityAwareRouting', + 'pipeline_enabled', + 'pipelineConcurrency', + 'shadowRouting', + 'evalRouting', + 'resetAwareEnabled', + 'resetAwareWindow' + ) +); \ No newline at end of file diff --git a/src/shared/validation/schemas/combo.ts b/src/shared/validation/schemas/combo.ts index 65e27bceb2..64874daa3b 100644 --- a/src/shared/validation/schemas/combo.ts +++ b/src/shared/validation/schemas/combo.ts @@ -186,28 +186,26 @@ export const comboRuntimeConfigSchema = z shadowRouting: shadowRoutingSchema.optional(), evalRouting: evalRoutingSchema.optional(), }) - .strict() - .superRefine((config, ctx) => { - if (config.zeroLatencyOptimizationsEnabled === true) return; + .passthrough() + .transform((config) => { + // Backward-compat shim: combos stored prior to v3.8.33 may carry zero-latency + // feature flags (fallbackCompressionMode !== "off", hedging === true, or + // predictiveTtftMs > 0) without the accompanying zeroLatencyOptimizationsEnabled + // gate that the new schema requires. Auto-promote the flag when any such feature + // is enabled but the gate is unset/false, so stored combos continue to round-trip + // through PUT /api/combos/{id} without returning 400. This replaces the prior + // superRefine that hard-rejected these payloads (see issue #4382). + if (config.zeroLatencyOptimizationsEnabled === true) return config; - const addZeroLatencyIssue = (path: string[]) => { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "zeroLatencyOptimizationsEnabled must be true to enable zero-latency combo features", - path, - }); - }; + const hasZeroLatencyFeature = + config.hedging === true || + (typeof config.predictiveTtftMs === "number" && config.predictiveTtftMs > 0) || + (!!config.fallbackCompressionMode && config.fallbackCompressionMode !== "off"); - if (config.hedging === true) { - addZeroLatencyIssue(["hedging"]); - } - if (typeof config.predictiveTtftMs === "number" && config.predictiveTtftMs > 0) { - addZeroLatencyIssue(["predictiveTtftMs"]); - } - if (config.fallbackCompressionMode && config.fallbackCompressionMode !== "off") { - addZeroLatencyIssue(["fallbackCompressionMode"]); + if (hasZeroLatencyFeature) { + return { ...config, zeroLatencyOptimizationsEnabled: true }; } + return config; }); export const comboNameSchema = z diff --git a/tests/unit/combo-config.test.ts b/tests/unit/combo-config.test.ts index 1e9a04b9c3..b2b19a9028 100644 --- a/tests/unit/combo-config.test.ts +++ b/tests/unit/combo-config.test.ts @@ -213,9 +213,15 @@ test("combo config schema accepts explicit zero-latency opt-in controls", () => assert.equal(parsed.config.predictiveTtftMs, 1800); }); -test("combo config schema rejects enabled zero-latency subfeatures without opt-in", () => { +test("combo config schema auto-promotes the zero-latency gate for legacy configs without opt-in", () => { + // Pre-3.8.33 stored combos carry zero-latency subfeatures (hedging / + // fallbackCompressionMode / predictiveTtftMs) WITHOUT the + // zeroLatencyOptimizationsEnabled gate the current schema expects. Rather than + // rejecting these on the first GUI edit (the #4382 400), the schema now + // auto-promotes the gate to true so the stored config round-trips through + // PUT /api/combos/{id}. See #4774 (closes #4382 followup). const result = createComboSchema.safeParse({ - name: "zero-latency-noop", + name: "zero-latency-legacy", models: ["openai/gpt-4o-mini", "anthropic/claude-3-haiku"], config: { hedging: true, @@ -224,11 +230,47 @@ test("combo config schema rejects enabled zero-latency subfeatures without opt-i }, }); - assert.equal(result.success, false); - assert.deepEqual( - result.error.issues.map((issue) => issue.path.join(".")), - ["config.hedging", "config.predictiveTtftMs", "config.fallbackCompressionMode"] - ); + assert.equal(result.success, true); + assert.equal(result.data.config.zeroLatencyOptimizationsEnabled, true); + // The enabled subfeatures are preserved verbatim. + assert.equal(result.data.config.hedging, true); + assert.equal(result.data.config.fallbackCompressionMode, "lite"); + assert.equal(result.data.config.predictiveTtftMs, 1800); +}); + +test("combo config schema leaves the zero-latency gate untouched when no subfeature is enabled", () => { + // A plain config with no zero-latency subfeature must NOT be auto-promoted — + // the gate stays at its default (false) so we don't silently flip optimizations on. + const result = createComboSchema.safeParse({ + name: "no-zero-latency", + models: ["openai/gpt-4o-mini", "anthropic/claude-3-haiku"], + config: { + fallbackCompressionMode: "off", + }, + }); + + assert.equal(result.success, true); + assert.notEqual(result.data.config.zeroLatencyOptimizationsEnabled, true); +}); + +test("combo config schema no longer rejects v3.8.31-era removed config keys (#4382 round-trip)", () => { + // These keys were dropped from the schema after v3.8.31 but still live in + // stored combo JSON. The schema switched from .strict() (which 400'd on these) + // to .passthrough() so legacy configs survive an edit+save; the server route + // (stripLegacyComboConfigKeys) and migration 103 then scrub them on write. + const result = createComboSchema.safeParse({ + name: "legacy-removed-keys", + models: ["openai/gpt-4o-mini", "anthropic/claude-3-haiku"], + config: { + queueDepth: 4, + fallbackDelayMs: 200, + maxComboDepth: 3, + shadowRouting: { enabled: false }, + resetAwareEnabled: true, + }, + }); + + assert.equal(result.success, true); }); test("combo config schema allows zero-latency tuning fields when subfeatures stay disabled", () => {