mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
fix(combos): auto-promote zeroLatencyOptimizationsEnabled so legacy configs (pre-3.8.33 fallbackCompressionMode="lite") round-trip on the first GUI edit (#4774)
Auto-promote zeroLatencyOptimizationsEnabled + strip v3.8.31-era removed keys so legacy combo configs round-trip through PUT /api/combos/{id} on first GUI edit (closes #4382 followup). Pre-merge: rewrote the now-stale reject test to assert auto-promotion + added passthrough/round-trip regression guards; reconciled combos/page.tsx file-size baseline. Integrated into release/v3.8.35.
This commit is contained in:
@@ -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 (<cap, unit-tested); cohesive DB wiring next to getProviderNodeById, not extractable. Covered by tests/unit/provider-node-select-4421.test.ts.",
|
||||
"_rebaseline_2026_06_21_v3833_release_basered_fixes": "Release v3.8.33: 5 deterministic base-reds inherited from parallel-session merges (fast-gates PR→release não rodam test:unit completo) consertados no PR de release. src/sse/services/auth.ts 2279->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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
54
src/lib/db/migrations/103_strip_legacy_combo_config_keys.sql
Normal file
54
src/lib/db/migrations/103_strip_legacy_combo_config_keys.sql
Normal file
@@ -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'
|
||||
)
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user