Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/shared/validation/schemas/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ export const comboRuntimeConfigSchema = z
return config;
});

// Per-combo universal handoff. Runtime reads combo.universal_handoff (and the
// camelCase alias) in phaseComboSetup; an omitted schema field is stripped by
// Zod before the route writes the combo, so enabled:false never persisted
// (#15251). The process-wide default stays enabled:true.
export const universalHandoffSchema = z
.object({
enabled: z.boolean().optional(),
trigger: z.enum(["always", "on-switch", "on-error"]).optional(),
providerAllowlist: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
maxMessagesForSummary: z.number().int().min(5).max(100).optional(),
handoffModel: z.string().trim().max(300).optional(),
ttlMinutes: z.number().int().min(1).max(10080).optional(),
preserveSystemPrompt: z.boolean().optional(),
relayMode: z.enum(["schema-locked", "standard"]).optional(),
})
.strict();

export const comboNameSchema = z
.string()
.trim()
Expand Down Expand Up @@ -374,6 +391,8 @@ export const createComboSchema = z
system_message: z.string().max(50000).optional(),
tool_filter_regex: z.string().max(1000).optional(),
context_cache_protection: z.boolean().optional(),
universal_handoff: universalHandoffSchema.optional(),
universalHandoff: universalHandoffSchema.optional(),
context_length: z.number().int().min(1000).max(2000000).optional(),
// Optional embedding dimensions override for embedding combos.
// When set, the value is injected into every upstream embedding request as
Expand Down Expand Up @@ -450,6 +469,8 @@ export const updateComboSchema = z
system_message: z.string().max(50000).optional().nullable(),
tool_filter_regex: z.string().max(1000).optional().nullable(),
context_cache_protection: z.boolean().optional().nullable(),
universal_handoff: universalHandoffSchema.optional().nullable(),
universalHandoff: universalHandoffSchema.optional().nullable(),
context_length: z.number().int().min(1000).max(2000000).optional().nullable(),
compressionOverride: comboCompressionOverrideSchema.optional(),
dimensions: z
Expand All @@ -473,6 +494,8 @@ export const updateComboSchema = z
value.system_message === undefined &&
value.tool_filter_regex === undefined &&
value.context_cache_protection === undefined &&
value.universal_handoff === undefined &&
value.universalHandoff === undefined &&
value.context_length === undefined &&
value.compressionOverride === undefined &&
value.dimensions === undefined
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/combo-universal-handoff-15251.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* #15251 — a combo payload that disables universal handoff must survive the
* combo API schemas. createComboSchema / updateComboSchema omitted the field,
* and Zod's default strip policy dropped it before the route wrote the combo,
* so an operator could not persist enabled: false.
*
* Does not touch DEFAULT_UNIVERSAL_HANDOFF_CONFIG.enabled (still true).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-handoff-15251-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const { createComboSchema, updateComboSchema } =
await import("../../src/shared/validation/schemas.ts");
const core = await import("../../src/lib/db/core.ts");
const combosDb = await import("../../src/lib/db/combos.ts");

const disabledHandoff = {
enabled: false,
trigger: "on-switch" as const,
providerAllowlist: [],
maxMessagesForSummary: 30,
handoffModel: "",
ttlMinutes: 300,
preserveSystemPrompt: true,
};

async function resetStorage() {
core.resetDbInstance();
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}

test.beforeEach(async () => {
await resetStorage();
});

test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});

test("createComboSchema keeps a disabled universal_handoff", () => {
const parsed = createComboSchema.parse({
name: "Handoff Off",
models: [{ provider: "openai", model: "gpt-4.1" }],
universal_handoff: disabledHandoff,
});
assert.deepEqual(parsed.universal_handoff, disabledHandoff);
});

test("updateComboSchema keeps a disabled universal_handoff and counts it as an update", () => {
const parsed = updateComboSchema.parse({
universal_handoff: disabledHandoff,
});
assert.deepEqual(parsed.universal_handoff, disabledHandoff);
assert.doesNotThrow(() => updateComboSchema.parse({ universal_handoff: disabledHandoff }));
});

test("updateComboSchema accepts camelCase universalHandoff and null to clear", () => {
const parsed = updateComboSchema.parse({ universalHandoff: disabledHandoff });
assert.deepEqual(parsed.universalHandoff, disabledHandoff);
const cleared = updateComboSchema.parse({ universal_handoff: null });
assert.equal(cleared.universal_handoff, null);
});

test("a disabled universal_handoff round-trips through createCombo and updateCombo", async () => {
const created = await combosDb.createCombo(
createComboSchema.parse({
name: "Persist Handoff Off",
models: [{ provider: "openai", model: "gpt-4.1" }],
universal_handoff: disabledHandoff,
})
);
assert.deepEqual(created.universal_handoff, disabledHandoff);

const reread = await combosDb.getComboById(created.id as string);
assert.ok(reread);
assert.deepEqual(reread!.universal_handoff, disabledHandoff);

const updated = await combosDb.updateCombo(
created.id as string,
updateComboSchema.parse({ universal_handoff: { enabled: false } })
);
assert.ok(updated);
assert.deepEqual(updated!.universal_handoff, { enabled: false });
});
Loading