mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
sortTargetsByUsage (open-sse/services/combo/targetSorters.ts, since #7015/#7059) keys usage lookups by the per-target executionKey (combo-name + step-id), not by the bare model string, so accounts sharing a modelStr don't collapse into a single usage bucket. Three tests called recordComboRequest() directly without a `target`, so the recorded usage landed under a modelStr fallback key that never matches the real executionKey computed at combo-resolution time — every target read back as 0 usage and the original combo order won, failing the "prefers the least-used model" assertions. Production is unaffected: every real combo.ts call site already passes `target: toRecordedTarget(target)`. Fixed by priming usage through real handleComboChat calls (which route recordComboRequest through the actual resolved target) instead of calling recordComboRequest() directly with an unlinked target.
3243 lines
96 KiB
TypeScript
3243 lines
96 KiB
TypeScript
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-routing-"));
|
|
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const {
|
|
getComboFromData,
|
|
getComboModelsFromData,
|
|
validateComboDAG,
|
|
resolveNestedComboModels,
|
|
handleComboChat,
|
|
} = await import("../../open-sse/services/combo.ts");
|
|
const { resolveReasoningBufferedMaxTokens } =
|
|
await import("../../open-sse/services/reasoningTokenBuffer.ts");
|
|
const { normalizeComboStep } = await import("../../src/lib/combos/steps.ts");
|
|
const { registerStrategy } = await import("../../open-sse/services/autoCombo/routerStrategy.ts");
|
|
const { touchSession, clearSessions } = await import("../../open-sse/services/sessionManager.ts");
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const settingsDb = await import("../../src/lib/db/settings.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const evalsDb = await import("../../src/lib/db/evals.ts");
|
|
const { saveModelsDevCapabilities, clearModelsDevCapabilities } =
|
|
await import("../../src/lib/modelsDevSync.ts");
|
|
const { getComboMetrics, recordComboRequest, resetAllComboMetrics } =
|
|
await import("../../open-sse/services/comboMetrics.ts");
|
|
const { resetEvalRoutingCache } = await import("../../open-sse/services/evalRouting.ts");
|
|
const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts");
|
|
const { acquire: acquireSemaphore, resetAll: resetAllSemaphores } =
|
|
await import("../../open-sse/services/rateLimitSemaphore.ts");
|
|
const { _resetAllDecks } = await import("../../src/shared/utils/shuffleDeck.ts");
|
|
const { _setSecureRandomFloatSource } = await import("../../src/shared/utils/secureRandom.ts");
|
|
|
|
function createLog() {
|
|
const entries: any[] = [];
|
|
return {
|
|
info: (tag: any, msg: any) => entries.push({ level: "info", tag, msg }),
|
|
warn: (tag: any, msg: any) => entries.push({ level: "warn", tag, msg }),
|
|
error: (tag: any, msg: any) => entries.push({ level: "error", tag, msg }),
|
|
debug: (tag: any, msg: any) => entries.push({ level: "debug", tag, msg }),
|
|
entries,
|
|
};
|
|
}
|
|
|
|
function okResponse(body: any = { choices: [{ message: { content: "ok" } }] }) {
|
|
return new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function errorResponse(status: number, message: string = `Error ${status}`) {
|
|
return new Response(JSON.stringify({ error: { message } }), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function waitForBackgroundWork() {
|
|
return new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
|
|
function providerBreakerOpenResponse() {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: "Provider circuit breaker is open",
|
|
code: "provider_circuit_open",
|
|
},
|
|
}),
|
|
{
|
|
status: 503,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-omniroute-provider-breaker": "open",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
function streamResponse(chunks: any[]) {
|
|
return new Response(chunks.join(""), {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
|
|
function capabilityEntry(limitContext: unknown, overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
tool_call: true,
|
|
reasoning: false,
|
|
attachment: false,
|
|
structured_output: true,
|
|
temperature: true,
|
|
modalities_input: JSON.stringify(["text"]),
|
|
modalities_output: JSON.stringify(["text"]),
|
|
knowledge_cutoff: null,
|
|
release_date: null,
|
|
last_updated: null,
|
|
status: null,
|
|
family: null,
|
|
open_weights: false,
|
|
limit_context: limitContext,
|
|
limit_input: limitContext,
|
|
limit_output: 4096,
|
|
interleaved_field: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function getComboTargetExecutionKey(comboName: string, index: number, stepInput: any) {
|
|
const step = normalizeComboStep(stepInput, { comboName, index });
|
|
if (!step) throw new Error(`Failed to normalize combo step for ${comboName}#${index}`);
|
|
return `combo:${comboName}:${step.id}`;
|
|
}
|
|
|
|
async function cleanupTestDataDir() {
|
|
let lastError;
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
try {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
return;
|
|
} catch (error: any) {
|
|
lastError = error;
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
}
|
|
|
|
if (lastError) {
|
|
throw lastError;
|
|
}
|
|
}
|
|
|
|
async function resetStorage() {
|
|
await cleanupTestDataDir();
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
await settingsDb.resetAllPricing();
|
|
settingsDb.clearAllLKGP();
|
|
clearModelsDevCapabilities();
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
resetAllComboMetrics();
|
|
resetEvalRoutingCache();
|
|
resetAllCircuitBreakers();
|
|
resetAllSemaphores();
|
|
_resetAllDecks();
|
|
clearSessions();
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
resetAllComboMetrics();
|
|
resetEvalRoutingCache();
|
|
resetAllCircuitBreakers();
|
|
resetAllSemaphores();
|
|
_resetAllDecks();
|
|
clearModelsDevCapabilities();
|
|
settingsDb.clearAllLKGP();
|
|
if (ORIGINAL_DATA_DIR === undefined) {
|
|
delete process.env.DATA_DIR;
|
|
} else {
|
|
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
|
}
|
|
await cleanupTestDataDir();
|
|
});
|
|
|
|
test("getComboFromData and getComboModelsFromData resolve combos from array and object containers", () => {
|
|
const combos = [
|
|
{ name: "alpha", models: ["openai/gpt-4o-mini", { model: "claude/sonnet", weight: 2 }] },
|
|
];
|
|
|
|
const fromArray = getComboFromData("alpha", combos);
|
|
const fromObject = getComboFromData("alpha", { combos });
|
|
const models = getComboModelsFromData("alpha", { combos });
|
|
|
|
assert.equal(fromArray.name, "alpha");
|
|
assert.equal(fromObject.name, "alpha");
|
|
assert.deepEqual(models, ["openai/gpt-4o-mini", "claude/sonnet"]);
|
|
});
|
|
|
|
test("validateComboDAG rejects circular references and resolveNestedComboModels expands nested combos", () => {
|
|
const combos = [
|
|
{ name: "root", models: ["child-a", "openai/gpt-4o-mini"] },
|
|
{ name: "child-a", models: ["child-b", "claude/sonnet"] },
|
|
{ name: "child-b", models: ["groq/llama-3.3-70b"] },
|
|
];
|
|
|
|
validateComboDAG("root", combos);
|
|
assert.deepEqual(resolveNestedComboModels(combos[0], combos), [
|
|
"groq/llama-3.3-70b",
|
|
"claude/sonnet",
|
|
"openai/gpt-4o-mini",
|
|
]);
|
|
|
|
assert.throws(
|
|
() =>
|
|
validateComboDAG("loop-a", [
|
|
{ name: "loop-a", models: ["loop-b"] },
|
|
{ name: "loop-b", models: ["loop-a"] },
|
|
]),
|
|
/Circular combo reference detected/
|
|
);
|
|
});
|
|
|
|
test("resolveNestedComboModels expands explicit combo-ref steps", () => {
|
|
const combos = [
|
|
{
|
|
name: "root",
|
|
models: [
|
|
{ id: "root-ref-child", kind: "combo-ref", comboName: "child", weight: 0 },
|
|
{ id: "root-model", kind: "model", providerId: "openai", model: "openai/gpt-4o-mini" },
|
|
],
|
|
},
|
|
{
|
|
name: "child",
|
|
models: [
|
|
{ id: "child-model", kind: "model", providerId: "anthropic", model: "claude/sonnet" },
|
|
],
|
|
},
|
|
];
|
|
|
|
validateComboDAG("root", combos);
|
|
assert.deepEqual(resolveNestedComboModels(combos[0], combos), [
|
|
"claude/sonnet",
|
|
"openai/gpt-4o-mini",
|
|
]);
|
|
});
|
|
|
|
test("validateComboDAG enforces maximum nesting depth", () => {
|
|
const combos = [
|
|
{ name: "c1", models: ["c2"] },
|
|
{ name: "c2", models: ["c3"] },
|
|
{ name: "c3", models: ["c4"] },
|
|
{ name: "c4", models: ["c5"] },
|
|
{ name: "c5", models: ["openai/gpt-4o-mini"] },
|
|
];
|
|
|
|
assert.throws(() => validateComboDAG("c1", combos), /Max combo nesting depth/);
|
|
});
|
|
|
|
test("handleComboChat priority strategy defaults to first model and records success metrics", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "priority-default",
|
|
models: ["openai/gpt-4o-mini", "claude/sonnet"],
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
const metrics = getComboMetrics("priority-default");
|
|
const firstStep = normalizeComboStep(combo.models[0], {
|
|
comboName: combo.name,
|
|
index: 0,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/gpt-4o-mini"]);
|
|
assert.equal(metrics.totalRequests, 1);
|
|
assert.equal(metrics.totalSuccesses, 1);
|
|
assert.equal(metrics.byModel["openai/gpt-4o-mini"].requests, 1);
|
|
assert.equal(metrics.byTarget[firstStep.id].requests, 1);
|
|
assert.equal(metrics.byTarget[firstStep.id].model, "openai/gpt-4o-mini");
|
|
assert.equal(metrics.strategy, "priority");
|
|
});
|
|
|
|
test("handleComboChat runs shadow targets without changing the primary response or metrics", async () => {
|
|
const calls: any[] = [];
|
|
const shadowRequests: any[] = [];
|
|
const combo = {
|
|
name: "shadow-routing-priority",
|
|
models: ["openai/gpt-4o-mini"],
|
|
config: {
|
|
shadowRouting: {
|
|
enabled: true,
|
|
targets: [
|
|
{
|
|
kind: "model",
|
|
id: "shadow-anthropic",
|
|
providerId: "anthropic",
|
|
model: "anthropic/claude-3-haiku",
|
|
label: "Dark launch target",
|
|
},
|
|
],
|
|
sampleRate: 1,
|
|
maxTargets: 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: { stream: true },
|
|
combo,
|
|
handleSingleModel: async (body: any, modelStr: any, target?: any) => {
|
|
calls.push({
|
|
modelStr,
|
|
trafficType: target?.trafficType ?? "production",
|
|
stream: body.stream,
|
|
});
|
|
if (target?.trafficType === "shadow") {
|
|
shadowRequests.push({
|
|
executionKey: target.executionKey,
|
|
hasBodyMarker: body._omnirouteShadowRouting === true,
|
|
});
|
|
}
|
|
if (target?.trafficType === "shadow") return errorResponse(503, "shadow failed");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
await waitForBackgroundWork();
|
|
|
|
const metrics = getComboMetrics("shadow-routing-priority");
|
|
|
|
assert.equal(result.ok, true);
|
|
calls.sort((a, b) => a.trafficType.localeCompare(b.trafficType));
|
|
assert.deepEqual(calls, [
|
|
{ modelStr: "openai/gpt-4o-mini", trafficType: "production", stream: true },
|
|
{ modelStr: "anthropic/claude-3-haiku", trafficType: "shadow", stream: false },
|
|
]);
|
|
assert.deepEqual(shadowRequests, [
|
|
{ executionKey: "shadow>shadow-anthropic", hasBodyMarker: false },
|
|
]);
|
|
assert.equal(metrics.totalRequests, 1);
|
|
assert.equal(metrics.totalSuccesses, 1);
|
|
assert.equal(metrics.byModel["openai/gpt-4o-mini"].requests, 1);
|
|
assert.equal(metrics.byModel["anthropic/claude-3-haiku"], undefined);
|
|
assert.equal(metrics.shadow.totalRequests, 1);
|
|
assert.equal(metrics.shadow.totalFailures, 1);
|
|
assert.equal(metrics.shadow.byModel["anthropic/claude-3-haiku"].requests, 1);
|
|
});
|
|
|
|
test("handleComboChat isolates nested shadow request bodies", async () => {
|
|
type MutableChatBody = { messages: Array<{ role: string; content: string }>; stream: boolean };
|
|
|
|
const shadowMessages: string[] = [];
|
|
const body = { messages: [{ role: "user", content: "original" }], stream: true };
|
|
const combo = {
|
|
name: "shadow-routing-isolation",
|
|
models: ["openai/gpt-4o-mini"],
|
|
config: {
|
|
shadowRouting: {
|
|
enabled: true,
|
|
targets: ["anthropic/claude-3-haiku"],
|
|
sampleRate: 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body,
|
|
combo,
|
|
handleSingleModel: async (
|
|
requestBody: MutableChatBody,
|
|
_modelStr: unknown,
|
|
target?: unknown
|
|
) => {
|
|
const trafficType =
|
|
target && typeof target === "object" && "trafficType" in target
|
|
? (target as { trafficType?: unknown }).trafficType
|
|
: null;
|
|
if (trafficType === "shadow") {
|
|
shadowMessages.push(requestBody.messages[0].content);
|
|
return okResponse();
|
|
}
|
|
requestBody.messages[0].content = "mutated by production";
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
await waitForBackgroundWork();
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(shadowMessages, ["original"]);
|
|
});
|
|
|
|
test("handleComboChat honors shadow sampleRate zero", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "shadow-routing-sampled-out",
|
|
models: ["openai/gpt-4o-mini"],
|
|
config: {
|
|
shadowRouting: {
|
|
enabled: true,
|
|
targets: ["anthropic/claude-3-haiku"],
|
|
sampleRate: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any, target?: any) => {
|
|
calls.push({ modelStr, trafficType: target?.trafficType ?? "production" });
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
await waitForBackgroundWork();
|
|
|
|
const metrics = getComboMetrics("shadow-routing-sampled-out");
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, [{ modelStr: "openai/gpt-4o-mini", trafficType: "production" }]);
|
|
assert.equal(metrics.totalRequests, 1);
|
|
assert.equal(metrics.shadow.totalRequests, 0);
|
|
});
|
|
|
|
test("handleComboChat priority strategy honors composite tier order before fallback", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "priority-composite-tiers",
|
|
strategy: "priority",
|
|
models: [
|
|
{
|
|
kind: "model",
|
|
id: "step-primary",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
},
|
|
{
|
|
kind: "model",
|
|
id: "step-backup",
|
|
providerId: "anthropic",
|
|
model: "claude/sonnet",
|
|
},
|
|
{
|
|
kind: "model",
|
|
id: "step-last",
|
|
providerId: "google",
|
|
model: "gemini/gemini-2.5-flash",
|
|
},
|
|
],
|
|
config: {
|
|
maxRetries: 0,
|
|
compositeTiers: {
|
|
defaultTier: "backup",
|
|
tiers: {
|
|
backup: {
|
|
stepId: "step-backup",
|
|
fallbackTier: "primary",
|
|
},
|
|
primary: {
|
|
stepId: "step-primary",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "claude/sonnet") {
|
|
return errorResponse(503, "backup failed");
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["claude/sonnet", "openai/gpt-4o-mini"]);
|
|
});
|
|
|
|
test("handleComboChat weighted strategy selects by weight and falls back in descending weight order", async () => {
|
|
const calls: any[] = [];
|
|
|
|
_setSecureRandomFloatSource(() => 0.95);
|
|
|
|
try {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "weighted-selection",
|
|
strategy: "weighted",
|
|
models: [
|
|
{ model: "openai/gpt-4o-mini", weight: 1 },
|
|
{ model: "claude/sonnet", weight: 9 },
|
|
],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "claude/sonnet") return errorResponse(500, "temporary");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["claude/sonnet", "openai/gpt-4o-mini"]);
|
|
} finally {
|
|
_setSecureRandomFloatSource(null);
|
|
}
|
|
});
|
|
|
|
test("handleComboChat weighted strategy falls back to uniform random when all weights are zero", async () => {
|
|
const calls: any[] = [];
|
|
_setSecureRandomFloatSource(() => 0.75);
|
|
|
|
try {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "weighted-zero-fallback",
|
|
strategy: "weighted",
|
|
models: [
|
|
{ model: "model-a", weight: 0 },
|
|
{ model: "model-b", weight: 0 },
|
|
],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-b"]);
|
|
} finally {
|
|
_setSecureRandomFloatSource(null);
|
|
}
|
|
});
|
|
|
|
test("handleComboChat random strategy uses shuffled model order", async () => {
|
|
const calls: any[] = [];
|
|
const sequence = [0.99, 0.0];
|
|
let idx = 0;
|
|
_setSecureRandomFloatSource(() => sequence[idx++] ?? 0);
|
|
|
|
try {
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "random-order",
|
|
strategy: "random",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(calls.length, 1);
|
|
assert.notEqual(calls[0], "model-a");
|
|
} finally {
|
|
_setSecureRandomFloatSource(null);
|
|
}
|
|
});
|
|
|
|
test("handleComboChat fill-first explicitly preserves priority order", async () => {
|
|
const calls: any[] = [];
|
|
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "fill-first-order",
|
|
strategy: "fill-first",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.deepEqual(calls, ["model-a"]);
|
|
});
|
|
|
|
test("handleComboChat p2c selects the better of two random choices by metrics", async () => {
|
|
const calls: any[] = [];
|
|
const sequence = [0.0, 0.0];
|
|
let idx = 0;
|
|
|
|
recordComboRequest("p2c-combo", "model-a", {
|
|
success: true,
|
|
latencyMs: 2000,
|
|
strategy: "p2c",
|
|
});
|
|
recordComboRequest("p2c-combo", "model-b", {
|
|
success: true,
|
|
latencyMs: 20,
|
|
strategy: "p2c",
|
|
});
|
|
_setSecureRandomFloatSource(() => sequence[idx++] ?? 0);
|
|
|
|
try {
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "p2c-combo",
|
|
strategy: "p2c",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.deepEqual(calls, ["model-b"]);
|
|
} finally {
|
|
_setSecureRandomFloatSource(null);
|
|
}
|
|
});
|
|
|
|
test("handleComboChat least-used strategy prefers the model with fewer recorded requests", async () => {
|
|
// Least-used sorts by the per-target executionKey (combo-name + step-id),
|
|
// not by the bare model string (#7015/#7059 — sortTargetsByUsage keys
|
|
// byTarget[executionKey] so accounts sharing a modelStr don't collapse into
|
|
// one shared bucket). Calling recordComboRequest() directly without a
|
|
// `target` falls back to keying by modelStr, which never matches the real
|
|
// executionKey computed at combo-resolution time. Drive usage through real
|
|
// handleComboChat calls instead, so recordComboRequest is invoked with the
|
|
// actual resolved target (matching production behavior).
|
|
const comboDef = {
|
|
name: "least-used-combo",
|
|
strategy: "least-used",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
};
|
|
const primingCalls: any[] = [];
|
|
const runPrimingCall = () =>
|
|
handleComboChat({
|
|
body: {},
|
|
combo: comboDef,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
primingCalls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
// Tied at 0 usage, order is preserved: 1st priming call picks model-a,
|
|
// 2nd priming call (model-a now at 1) picks model-b (tied with model-c at
|
|
// 0, but model-b sorts first). That leaves model-a and model-b each used
|
|
// once, model-c untouched.
|
|
await runPrimingCall();
|
|
await runPrimingCall();
|
|
assert.deepEqual(primingCalls, ["model-a", "model-b"]);
|
|
|
|
const calls: any[] = [];
|
|
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: comboDef,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(calls[0], "model-c");
|
|
});
|
|
|
|
test("handleComboChat skips unavailable models and falls through to the next active target", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "availability-skip",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async (modelStr) => modelStr !== "model-a",
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-b"]);
|
|
});
|
|
|
|
test("handleComboChat falls through empty successful responses and records failure metrics before succeeding", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-fallback",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return okResponse({ choices: [{ message: { content: "" } }] });
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "fallback ok" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const metrics = getComboMetrics("quality-fallback");
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
assert.equal(metrics.totalRequests, 2);
|
|
assert.equal(metrics.totalFailures, 1);
|
|
assert.equal(metrics.totalSuccesses, 1);
|
|
assert.equal(metrics.byModel["model-a"].lastStatus, "error");
|
|
assert.equal(metrics.byModel["model-b"].lastStatus, "ok");
|
|
});
|
|
|
|
test("handleComboChat records per-target metrics separately when the same model repeats with different accounts", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "per-target-repeat",
|
|
strategy: "priority",
|
|
models: [
|
|
{
|
|
kind: "model",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
connectionId: "conn-openai-a",
|
|
label: "Account A",
|
|
},
|
|
{
|
|
kind: "model",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
connectionId: "conn-openai-b",
|
|
label: "Account B",
|
|
},
|
|
],
|
|
config: { maxRetries: 0 },
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any, target: any) => {
|
|
calls.push(`${modelStr}:${target?.connectionId || "none"}`);
|
|
if (target?.connectionId === "conn-openai-a") {
|
|
return errorResponse(503, "account-a down");
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const firstStep = normalizeComboStep(combo.models[0], {
|
|
comboName: combo.name,
|
|
index: 0,
|
|
});
|
|
const secondStep = normalizeComboStep(combo.models[1], {
|
|
comboName: combo.name,
|
|
index: 1,
|
|
});
|
|
const metrics = getComboMetrics(combo.name);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/gpt-4o-mini:conn-openai-a", "openai/gpt-4o-mini:conn-openai-b"]);
|
|
assert.equal(metrics.byModel["openai/gpt-4o-mini"].requests, 2);
|
|
assert.equal(metrics.byTarget[firstStep.id].failures, 1);
|
|
assert.equal(metrics.byTarget[firstStep.id].connectionId, "conn-openai-a");
|
|
assert.equal(metrics.byTarget[secondStep.id].successes, 1);
|
|
assert.equal(metrics.byTarget[secondStep.id].connectionId, "conn-openai-b");
|
|
});
|
|
|
|
test("handleComboChat preserves the first failure status but surfaces the last error message", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "all-fail",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
return errorResponse(modelStr === "model-a" ? 500 : 429, `fail:${modelStr}`);
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 500);
|
|
assert.equal(payload.error.message, "fail:model-b");
|
|
});
|
|
|
|
test("handleComboChat round-robin rotates sequentially across requests", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "rr-sequence",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0, concurrencyPerModel: 1, queueTimeoutMs: 1000 },
|
|
};
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
}
|
|
|
|
assert.deepEqual(calls, ["model-a", "model-b", "model-a"]);
|
|
});
|
|
|
|
test("handleComboChat round-robin starts from composite tier default ordering", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "rr-composite-order",
|
|
strategy: "round-robin",
|
|
models: [
|
|
{
|
|
kind: "model",
|
|
id: "step-primary",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
},
|
|
{
|
|
kind: "model",
|
|
id: "step-backup",
|
|
providerId: "anthropic",
|
|
model: "claude/sonnet",
|
|
},
|
|
],
|
|
config: {
|
|
maxRetries: 0,
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 1000,
|
|
compositeTiers: {
|
|
defaultTier: "backup",
|
|
tiers: {
|
|
backup: {
|
|
stepId: "step-backup",
|
|
fallbackTier: "primary",
|
|
},
|
|
primary: {
|
|
stepId: "step-primary",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
}
|
|
|
|
assert.deepEqual(calls, ["claude/sonnet", "openai/gpt-4o-mini"]);
|
|
});
|
|
|
|
test("combo helpers short-circuit safely for missing combos, cycles, and excessive depth", () => {
|
|
assert.equal(getComboFromData("missing", null), null);
|
|
assert.equal(getComboModelsFromData("missing", { combos: [] }), null);
|
|
|
|
assert.doesNotThrow(() =>
|
|
validateComboDAG("ghost", {
|
|
combos: [{ name: "alpha", models: ["openai/gpt-4o-mini"] }],
|
|
})
|
|
);
|
|
assert.doesNotThrow(() => validateComboDAG("empty", [{ name: "empty" }]));
|
|
|
|
assert.deepEqual(
|
|
resolveNestedComboModels(
|
|
{ name: "loop", models: ["model-a", "model-b"] },
|
|
[],
|
|
new Set(["loop"])
|
|
),
|
|
[]
|
|
);
|
|
|
|
assert.deepEqual(
|
|
resolveNestedComboModels(
|
|
{ name: "deep", models: ["model-a", { model: "model-b", weight: 2 }] },
|
|
[],
|
|
new Set(),
|
|
99
|
|
),
|
|
["model-a", "model-b"]
|
|
);
|
|
});
|
|
|
|
test("handleComboChat accepts binary and Responses-style 200 bodies but falls through malformed success payloads", async () => {
|
|
const binaryResult = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-binary",
|
|
strategy: "priority",
|
|
models: ["model-a"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async () =>
|
|
new Response("binary-payload", {
|
|
status: 200,
|
|
headers: { "content-type": "application/octet-stream" },
|
|
}),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(binaryResult.ok, true);
|
|
assert.equal(await binaryResult.text(), "binary-payload");
|
|
|
|
const responsesResult = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-responses",
|
|
strategy: "priority",
|
|
models: ["model-a"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async () =>
|
|
okResponse({
|
|
output: [{ type: "output_text", text: "done" }],
|
|
}),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(responsesResult.ok, true);
|
|
|
|
const calls: any[] = [];
|
|
const malformedResult = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-malformed",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return new Response("", {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
if (modelStr === "model-b") {
|
|
return okResponse({ choices: [{}] });
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "recovered" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(malformedResult.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b", "model-c"]);
|
|
});
|
|
|
|
test("handleComboChat accepts text-mode SSE payloads as valid non-streaming passthrough responses", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-sse-data",
|
|
strategy: "priority",
|
|
models: ["model-a"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async () =>
|
|
new Response('data: {"choices":[{"delta":{"content":"hello"}}]}\n\n', {
|
|
status: 200,
|
|
headers: { "content-type": "text/plain" },
|
|
}),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.match(await result.text(), /^data:/);
|
|
});
|
|
|
|
test("handleComboChat falls through invalid JSON and embedded 200 error bodies before succeeding", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "quality-invalid-json-and-error",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return new Response("{bad-json", {
|
|
status: 200,
|
|
headers: { "content-type": "text/plain" },
|
|
});
|
|
}
|
|
if (modelStr === "model-b") {
|
|
return new Response(JSON.stringify({ error: { message: "embedded upstream failure" } }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse({ choices: [{ delta: { content: "recovered" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b", "model-c"]);
|
|
});
|
|
|
|
test("handleComboChat returns the earliest retry-after when all priority targets are rate-limited", async () => {
|
|
const soon = new Date(Date.now() + 1_000).toISOString();
|
|
const later = new Date(Date.now() + 5_000).toISOString();
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "priority-retry-after",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) =>
|
|
new Response(
|
|
JSON.stringify({
|
|
error: { message: `limited:${modelStr}` },
|
|
retryAfter: modelStr === "model-a" ? later : soon,
|
|
}),
|
|
{
|
|
status: 429,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: { maxRetries: 0, retryDelayMs: 1 },
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 429);
|
|
assert.match(payload.error.message, /limited:model-b/);
|
|
assert.ok(Number(result.headers.get("Retry-After")) >= 1);
|
|
});
|
|
|
|
test("handleComboChat returns 404 model_not_found when a combo has no executable targets", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "empty-priority",
|
|
strategy: "priority",
|
|
models: [],
|
|
},
|
|
handleSingleModel: async () => {
|
|
throw new Error("handleSingleModel should not run for empty combos");
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 404);
|
|
assert.equal(payload.error.code, "model_not_found");
|
|
assert.match(payload.error.message, /Combo has no executable targets/);
|
|
});
|
|
|
|
test("handleComboChat round-robin returns 404 when no models are configured", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-empty",
|
|
strategy: "round-robin",
|
|
models: [],
|
|
},
|
|
handleSingleModel: async () => {
|
|
throw new Error("handleSingleModel should not run for empty round-robin combos");
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 404);
|
|
assert.equal(payload.error.code, "model_not_found");
|
|
assert.match(payload.error.message, /Round-robin combo has no executable targets/);
|
|
});
|
|
|
|
test("handleComboChat round-robin falls through semaphore timeouts and malformed success payloads", async () => {
|
|
const release = await acquireSemaphore(
|
|
getComboTargetExecutionKey("rr-timeout-fallback", 0, "model-a"),
|
|
{
|
|
maxConcurrency: 1,
|
|
timeoutMs: 100,
|
|
}
|
|
);
|
|
const calls: any[] = [];
|
|
|
|
try {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-timeout-fallback",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-b") {
|
|
return okResponse({ choices: [{}] });
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "rr ok" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-b", "model-c"]);
|
|
} finally {
|
|
release();
|
|
}
|
|
});
|
|
|
|
test("handleComboChat round-robin surfaces retry-after metadata after exhausting all models", async () => {
|
|
const sooner = new Date(Date.now() + 1_500).toISOString();
|
|
const later = new Date(Date.now() + 7_000).toISOString();
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-retry-after",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) =>
|
|
new Response(
|
|
JSON.stringify({
|
|
error: { message: `rr-limited:${modelStr}` },
|
|
retryAfter: modelStr === "model-a" ? later : sooner,
|
|
}),
|
|
{
|
|
status: 429,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 429);
|
|
assert.match(payload.error.message, /rr-limited:model-b/);
|
|
assert.ok(Number(result.headers.get("Retry-After")) >= 1);
|
|
});
|
|
|
|
test("handleComboChat falls through generic 400s when a later priority target succeeds", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "priority-generic-400-recover",
|
|
strategy: "priority",
|
|
models: ["provider-a/model-a", "provider-b/model-b"],
|
|
config: { maxRetries: 0, retryDelayMs: 1 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "provider-a/model-a") {
|
|
return new Response(JSON.stringify({ error: { message: "Instructions are required" } }), {
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "recovered" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 200);
|
|
assert.equal(payload.choices[0].message.content, "recovered");
|
|
assert.deepEqual(calls, ["provider-a/model-a", "provider-b/model-b"]);
|
|
});
|
|
|
|
test("handleComboChat preserves fallback request bodies when zero-latency optimizations are disabled", async () => {
|
|
const longToolOutput = "x".repeat(2500);
|
|
let fallbackBody: any = null;
|
|
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [
|
|
{ role: "user", content: "please inspect this output" },
|
|
{ role: "tool", content: longToolOutput },
|
|
],
|
|
},
|
|
combo: {
|
|
name: "zero-latency-disabled-preserves-body",
|
|
strategy: "priority",
|
|
models: ["provider-a/model-a", "provider-b/model-b"],
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
fallbackCompressionMode: "lite",
|
|
fallbackCompressionThreshold: 1,
|
|
},
|
|
},
|
|
handleSingleModel: async (requestBody: any, modelStr: any) => {
|
|
if (modelStr === "provider-a/model-a") {
|
|
return errorResponse(500, "first target failed");
|
|
}
|
|
fallbackBody = requestBody;
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(fallbackBody.messages[1].content, longToolOutput);
|
|
});
|
|
|
|
test("handleComboChat applies fallback compression only after explicit zero-latency opt-in", async () => {
|
|
const longToolOutput = "x".repeat(2500);
|
|
let fallbackBody: any = null;
|
|
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [
|
|
{ role: "user", content: "please inspect this output" },
|
|
{ role: "tool", content: longToolOutput },
|
|
],
|
|
},
|
|
combo: {
|
|
name: "zero-latency-enabled-compresses-fallback",
|
|
strategy: "priority",
|
|
models: ["provider-a/model-a", "provider-b/model-b"],
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
zeroLatencyOptimizationsEnabled: true,
|
|
fallbackCompressionMode: "lite",
|
|
fallbackCompressionThreshold: 1,
|
|
},
|
|
},
|
|
handleSingleModel: async (requestBody: any, modelStr: any) => {
|
|
if (modelStr === "provider-a/model-a") {
|
|
return errorResponse(500, "first target failed");
|
|
}
|
|
fallbackBody = requestBody;
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
const fallbackToolContent = fallbackBody.messages[1].content;
|
|
assert.equal(typeof fallbackToolContent, "string");
|
|
assert.ok(fallbackToolContent.length < longToolOutput.length);
|
|
assert.match(fallbackToolContent, /truncated/i);
|
|
});
|
|
|
|
test("handleComboChat suppresses hedging unless zero-latency optimizations are enabled", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "hedging-disabled-with-subfeature-set",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
hedging: true,
|
|
hedgeDelayMs: 1,
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
return okResponse({ choices: [{ message: { content: modelStr } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(payload.choices[0].message.content, "model-a");
|
|
assert.deepEqual(calls, ["model-a"]);
|
|
});
|
|
|
|
test("handleComboChat starts hedged fallback only after explicit zero-latency opt-in", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "hedging-enabled-with-zero-latency",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
zeroLatencyOptimizationsEnabled: true,
|
|
hedging: true,
|
|
hedgeDelayMs: 1,
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any, target: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
await new Promise((resolve) => {
|
|
const timer = setTimeout(resolve, 100);
|
|
target?.modelAbortSignal?.addEventListener(
|
|
"abort",
|
|
() => {
|
|
clearTimeout(timer);
|
|
resolve(undefined);
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
return okResponse({ choices: [{ message: { content: "slow" } }] });
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "fast" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(payload.choices[0].message.content, "fast");
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
test("handleComboChat round-robin falls through generic 400s when a later model succeeds", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-generic-400-recover",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return new Response(JSON.stringify({ error: { message: "generic bad request" } }), {
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
test("handleComboChat round-robin falls through 400s and returns the final error payload when no target recovers", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-provider-scoped-400-fallback",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return new Response(
|
|
JSON.stringify({ error: { message: "unsupported message role for this provider" } }),
|
|
{
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
return errorResponse(500, "rr-final-fail");
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 400);
|
|
assert.equal(payload.error.message, "rr-final-fail");
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
test("handleComboChat strict-random uses the shared deck without repeating within a cycle", async () => {
|
|
const calls: any[] = [];
|
|
const combo = {
|
|
name: "strict-random-deck",
|
|
strategy: "strict-random",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
};
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
}
|
|
|
|
assert.equal(new Set(calls).size, 3);
|
|
});
|
|
|
|
test("handleComboChat cost-optimized orders models by the cheapest configured input price", async () => {
|
|
await settingsDb.updatePricing({
|
|
openai: {
|
|
"gpt-4o-mini": { input: 5, output: 10 },
|
|
"gpt-4o": { input: 1, output: 2 },
|
|
"gpt-4o-nano": { input: 0.1, output: 0.2 },
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "cost-optimized-combo",
|
|
strategy: "cost-optimized",
|
|
models: ["openai/gpt-4o-mini", "openai/gpt-4o", "openai/gpt-4o-nano"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "openai/gpt-4o-nano");
|
|
});
|
|
|
|
test("handleComboChat weighted strategy resolves nested combos before falling back to the next weighted target", async () => {
|
|
const calls: any[] = [];
|
|
_setSecureRandomFloatSource(() => 0.01);
|
|
|
|
try {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "weighted-nested-selection",
|
|
strategy: "weighted",
|
|
models: [
|
|
{ model: "nested-priority", weight: 9 },
|
|
{ model: "model-c", weight: 1 },
|
|
],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") return errorResponse(500, "nested-first-fail");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: [
|
|
{
|
|
name: "weighted-nested-selection",
|
|
models: [
|
|
{ model: "nested-priority", weight: 9 },
|
|
{ model: "model-c", weight: 1 },
|
|
],
|
|
},
|
|
{ name: "nested-priority", models: ["model-a", "model-b"] },
|
|
],
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
} finally {
|
|
_setSecureRandomFloatSource(null);
|
|
}
|
|
});
|
|
|
|
test("handleComboChat context-optimized orders models by the largest synced context window", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"gpt-4o-mini": capabilityEntry(128000),
|
|
"gpt-4o": capabilityEntry(64000),
|
|
"gpt-4o-max": capabilityEntry(256000),
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "context-optimized-combo",
|
|
strategy: "context-optimized",
|
|
models: ["openai/gpt-4o-mini", "openai/gpt-4o", "openai/gpt-4o-max"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "openai/gpt-4o-max");
|
|
});
|
|
|
|
test("handleComboChat context-optimized preserves order when all context limits are unknown", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "context-optimized-unknown",
|
|
strategy: "context-optimized",
|
|
models: ["unknown/model-a", "unknown/model-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["unknown/model-a"]);
|
|
});
|
|
|
|
test("handleComboChat skips fallback targets with too small context windows", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"tiny-context": capabilityEntry(32),
|
|
"large-context": capabilityEntry(4096),
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [{ role: "user", content: "x".repeat(800) }],
|
|
},
|
|
combo: {
|
|
name: "context-aware-fallback-context",
|
|
strategy: "priority",
|
|
models: ["openai/tiny-context", "openai/large-context"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/large-context"]);
|
|
});
|
|
|
|
test("handleComboChat skips tool, vision, and structured-output incompatible fallbacks", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"no-tools": capabilityEntry(128000, {
|
|
tool_call: false,
|
|
attachment: true,
|
|
structured_output: true,
|
|
modalities_input: JSON.stringify(["text", "image"]),
|
|
}),
|
|
"no-vision": capabilityEntry(128000, {
|
|
tool_call: true,
|
|
attachment: false,
|
|
structured_output: true,
|
|
modalities_input: JSON.stringify(["text"]),
|
|
}),
|
|
"no-json": capabilityEntry(128000, {
|
|
tool_call: true,
|
|
attachment: true,
|
|
structured_output: false,
|
|
modalities_input: JSON.stringify(["text", "image"]),
|
|
}),
|
|
compatible: capabilityEntry(128000, {
|
|
tool_call: true,
|
|
attachment: true,
|
|
structured_output: true,
|
|
modalities_input: JSON.stringify(["text", "image"]),
|
|
}),
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "Describe this image and return JSON." },
|
|
{ type: "image_url", image_url: { url: "data:image/png;base64,AAAA" } },
|
|
],
|
|
},
|
|
],
|
|
tools: [{ type: "function", function: { name: "save", parameters: {} } }],
|
|
response_format: { type: "json_schema", json_schema: { name: "result", schema: {} } },
|
|
},
|
|
combo: {
|
|
name: "context-aware-fallback-capabilities",
|
|
strategy: "priority",
|
|
models: ["openai/no-tools", "openai/no-vision", "openai/no-json", "openai/compatible"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/compatible"]);
|
|
});
|
|
|
|
test("handleComboChat preserves strategy order when context-aware filtering rejects all targets", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"no-tools-a": capabilityEntry(128000, { tool_call: false }),
|
|
"no-tools-b": capabilityEntry(128000, { tool_call: false }),
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [{ role: "user", content: "Use a tool." }],
|
|
tools: [{ type: "function", function: { name: "lookup", parameters: {} } }],
|
|
},
|
|
combo: {
|
|
name: "context-aware-fallback-full-filter",
|
|
strategy: "priority",
|
|
models: ["openai/no-tools-a", "openai/no-tools-b"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/no-tools-a"]);
|
|
});
|
|
|
|
test("handleComboChat eval-driven routing prioritizes higher scoring evaluated targets", async () => {
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "routing-quality",
|
|
suiteName: "Routing Quality",
|
|
target: { type: "model", id: "openai/eval-low", label: "Model: openai/eval-low" },
|
|
summary: { total: 10, passed: 4, failed: 6, passRate: 40 },
|
|
avgLatencyMs: 100,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "routing-quality",
|
|
suiteName: "Routing Quality",
|
|
target: { type: "model", id: "openai/eval-high", label: "Model: openai/eval-high" },
|
|
summary: { total: 10, passed: 9, failed: 1, passRate: 90 },
|
|
avgLatencyMs: 120,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "eval-driven-priority",
|
|
strategy: "priority",
|
|
models: ["openai/eval-low", "openai/eval-high"],
|
|
config: {
|
|
evalRouting: {
|
|
enabled: true,
|
|
suiteIds: ["routing-quality"],
|
|
qualityWeight: 1,
|
|
latencyWeight: 0,
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/eval-high"]);
|
|
});
|
|
|
|
test("handleComboChat eval-driven routing ignores stale and undersized eval runs", async () => {
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "routing-quality",
|
|
suiteName: "Routing Quality",
|
|
target: { type: "model", id: "openai/stale-good", label: "Model: openai/stale-good" },
|
|
summary: { total: 10, passed: 10, failed: 0, passRate: 100 },
|
|
avgLatencyMs: 50,
|
|
results: [],
|
|
createdAt: "2020-01-01T00:00:00.000Z",
|
|
});
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "routing-quality",
|
|
suiteName: "Routing Quality",
|
|
target: { type: "model", id: "openai/small-good", label: "Model: openai/small-good" },
|
|
summary: { total: 1, passed: 1, failed: 0, passRate: 100 },
|
|
avgLatencyMs: 50,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "eval-driven-ignored-runs",
|
|
strategy: "priority",
|
|
models: ["openai/stale-good", "openai/small-good", "openai/no-evals"],
|
|
config: {
|
|
evalRouting: {
|
|
enabled: true,
|
|
suiteIds: ["routing-quality"],
|
|
maxAgeHours: 24,
|
|
minCases: 5,
|
|
qualityWeight: 1,
|
|
latencyWeight: 0,
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/stale-good"]);
|
|
});
|
|
|
|
test("handleComboChat eval-driven routing can match bare model eval target ids", async () => {
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "routing-quality",
|
|
suiteName: "Routing Quality",
|
|
target: { type: "model", id: "bare-high", label: "Model: bare-high" },
|
|
summary: { total: 8, passed: 8, failed: 0, passRate: 100 },
|
|
avgLatencyMs: 200,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "eval-driven-bare-model-id",
|
|
strategy: "priority",
|
|
models: ["openai/bare-low", "openai/bare-high"],
|
|
config: {
|
|
evalRouting: {
|
|
enabled: true,
|
|
suiteIds: ["routing-quality"],
|
|
qualityWeight: 1,
|
|
latencyWeight: 0,
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/bare-high"]);
|
|
});
|
|
|
|
test("handleComboChat normalizes legacy strategy names at runtime", async () => {
|
|
// See the least-used test above: recordComboRequest() must be driven
|
|
// through a real handleComboChat call so it records against the actual
|
|
// resolved executionKey, not a bare modelStr fallback that never matches
|
|
// sortTargetsByUsage's lookup (#7015/#7059).
|
|
const comboDef = {
|
|
name: "legacy-usage-combo",
|
|
strategy: "usage",
|
|
models: ["model-a", "model-b"],
|
|
};
|
|
const primingCalls: any[] = [];
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: comboDef,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
primingCalls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
// Tied at 0 usage, order preserved: model-a (first in the list) wins.
|
|
assert.deepEqual(primingCalls, ["model-a"]);
|
|
|
|
const usageCalls: any[] = [];
|
|
|
|
await handleComboChat({
|
|
body: {},
|
|
combo: comboDef,
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
usageCalls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.deepEqual(usageCalls, ["model-b"]);
|
|
});
|
|
|
|
test("handleComboChat returns a 503 when every model is unavailable before execution", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "inactive-accounts",
|
|
strategy: "priority",
|
|
models: ["openai/model-a", "openai/model-b"],
|
|
},
|
|
handleSingleModel: async () => {
|
|
throw new Error("handleSingleModel should not run when all models are inactive");
|
|
},
|
|
isModelAvailable: async () => false,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 503);
|
|
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
|
});
|
|
|
|
test("handleComboChat treats provider circuit breaker responses as ordinary target failures", async () => {
|
|
const calls = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "provider-breaker-open",
|
|
strategy: "priority",
|
|
models: ["openai/model-a", "openai/model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body, modelStr) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "openai/model-a") {
|
|
return providerBreakerOpenResponse();
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/model-a", "openai/model-b"]);
|
|
});
|
|
|
|
test("handleComboChat auto strategy honors LKGP after filtering to tool-capable models", async () => {
|
|
await settingsDb.setLKGP("auto-lkgp", "auto-lkgp", "claude");
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [{ role: "user", content: "Write code using a tool" }],
|
|
tools: [{ type: "function", function: { name: "lookup_weather" } }],
|
|
},
|
|
combo: {
|
|
id: "auto-lkgp",
|
|
name: "auto-lkgp",
|
|
strategy: "auto",
|
|
models: ["openai/gpt-oss-120b", "openai/gpt-4o-mini", "claude/claude-sonnet-4-6"],
|
|
autoConfig: { routerStrategy: "lkgp" },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "claude/claude-sonnet-4-6");
|
|
});
|
|
|
|
test("handleComboChat auto strategy preserves selected same-provider connection identity", async () => {
|
|
const connA = await providersDb.createProviderConnection({
|
|
provider: "openai",
|
|
authType: "apikey",
|
|
name: "OpenAI A",
|
|
apiKey: "sk-auto-conn-a",
|
|
defaultModel: "gpt-4o-mini",
|
|
});
|
|
const connB = await providersDb.createProviderConnection({
|
|
provider: "openai",
|
|
authType: "apikey",
|
|
name: "OpenAI B",
|
|
apiKey: "sk-auto-conn-b",
|
|
defaultModel: "gpt-4o-mini",
|
|
});
|
|
touchSession("sticky-auto-session", connB.id);
|
|
|
|
const calls: Array<{ modelStr: string; connectionId: string | null | undefined }> = [];
|
|
const result = await handleComboChat({
|
|
body: { messages: [{ role: "user", content: "Continue the existing conversation" }] },
|
|
combo: {
|
|
id: "auto-same-provider-connection",
|
|
name: "auto-same-provider-connection",
|
|
strategy: "auto",
|
|
models: [
|
|
{
|
|
kind: "model",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
connectionId: connA.id,
|
|
label: "OpenAI A",
|
|
},
|
|
{
|
|
kind: "model",
|
|
providerId: "openai",
|
|
model: "openai/gpt-4o-mini",
|
|
connectionId: connB.id,
|
|
label: "OpenAI B",
|
|
},
|
|
],
|
|
autoConfig: {
|
|
candidatePool: ["openai"],
|
|
explorationRate: 0,
|
|
weights: {
|
|
quota: 0,
|
|
health: 0,
|
|
costInv: 0,
|
|
latencyInv: 0,
|
|
taskFit: 0,
|
|
stability: 0,
|
|
tierPriority: 0,
|
|
tierAffinity: 0,
|
|
specificityMatch: 0,
|
|
contextAffinity: 1,
|
|
resetWindowAffinity: 0,
|
|
connectionDensity: 0,
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any, target: any) => {
|
|
calls.push({ modelStr, connectionId: target?.connectionId });
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: { sessionId: "sticky-auto-session" } as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls.length, 1, "successful auto dispatch should call exactly one selected target");
|
|
assert.deepEqual(calls, [
|
|
{
|
|
modelStr: "openai/gpt-4o-mini",
|
|
connectionId: connB.id,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("handleComboChat standalone lkgp strategy prioritizes the last known good provider", async () => {
|
|
await settingsDb.setLKGP("standalone-lkgp", "standalone-lkgp", "anthropic");
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
id: "standalone-lkgp",
|
|
name: "standalone-lkgp",
|
|
strategy: "lkgp",
|
|
models: ["openai/gpt-4o-mini", "anthropic/claude-sonnet-4-6"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "anthropic/claude-sonnet-4-6");
|
|
});
|
|
|
|
test("handleComboChat standalone lkgp strategy falls back to original order when no state exists", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
id: "standalone-lkgp-no-state",
|
|
name: "standalone-lkgp-no-state",
|
|
strategy: "lkgp",
|
|
models: ["openai/gpt-4o-mini", "anthropic/claude-sonnet-4-6"],
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "openai/gpt-4o-mini");
|
|
});
|
|
|
|
test("handleComboChat standalone lkgp strategy updates LKGP after a successful call", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
id: "standalone-lkgp-save",
|
|
name: "standalone-lkgp-save",
|
|
strategy: "lkgp",
|
|
models: ["openai/gpt-4o-mini"],
|
|
},
|
|
handleSingleModel: async () => okResponse(),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
// Give the async fire-and-forget LKGP update a chance to execute
|
|
let persistedProvider: any = null;
|
|
for (let i = 0; i < 20; i++) {
|
|
persistedProvider = await settingsDb.getLKGP("standalone-lkgp-save", "standalone-lkgp-save");
|
|
if (persistedProvider?.provider === "openai") {
|
|
break;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
|
|
assert.equal(result.ok, true);
|
|
// getLKGP now returns LKGPRecord | null — source: src/lib/db/settings.ts getLKGP()
|
|
assert.equal(persistedProvider?.provider, "openai");
|
|
});
|
|
|
|
test("handleComboChat auto strategy falls back to the full pool when tool filtering empties candidates", async () => {
|
|
await settingsDb.updatePricing({
|
|
openai: {
|
|
"gpt-oss-120b": { input: 5, output: 10 },
|
|
},
|
|
deepseek: {
|
|
reasoner: { input: 0.1, output: 0.2 },
|
|
},
|
|
});
|
|
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
input: [{ role: "user", text: "Summarize this request" }],
|
|
tools: [{ type: "function", function: { name: "unsupported_tool" } }],
|
|
},
|
|
combo: {
|
|
name: "auto-cost-fallback",
|
|
strategy: "auto",
|
|
models: ["openai/gpt-oss-120b", "deepseek/reasoner"],
|
|
autoConfig: { routerStrategy: "cost" },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
intentSimpleMaxWords: 5,
|
|
intentExtraSimpleKeywords: "summarize, brief",
|
|
},
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "deepseek/reasoner");
|
|
});
|
|
|
|
test("handleComboChat auto strategy falls back to rules when a custom router strategy throws", async () => {
|
|
registerStrategy("throwing-test", {
|
|
name: "throwing-test",
|
|
description: "test strategy that always throws",
|
|
select() {
|
|
throw new Error("synthetic router failure");
|
|
},
|
|
});
|
|
|
|
const log = createLog();
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: { prompt: "Hello there" },
|
|
combo: {
|
|
name: "auto-throwing-strategy",
|
|
strategy: "auto",
|
|
models: ["openai/gpt-4o-mini"],
|
|
autoConfig: { routerStrategy: "throwing-test" },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log,
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/gpt-4o-mini"]);
|
|
assert.ok(
|
|
log.entries.some(
|
|
(entry) => entry.level === "warn" && /falling back to rules/i.test(String(entry.msg))
|
|
)
|
|
);
|
|
});
|
|
|
|
test("handleComboChat auto strategy reads strategyName from combo.config.auto and can prefer latency", async () => {
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: { prompt: "Just answer briefly" },
|
|
combo: {
|
|
name: "auto-latency-strategy-name",
|
|
strategy: "auto",
|
|
models: ["openai/gpt-4o-mini", "gemini/gemini-2.5-flash"],
|
|
config: {
|
|
auto: {
|
|
strategyName: "latency",
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "gemini/gemini-2.5-flash");
|
|
});
|
|
|
|
test("handleComboChat auto strategy can route by SLA targets", async () => {
|
|
const calls: any[] = [];
|
|
const log = createLog();
|
|
const result = await handleComboChat({
|
|
body: { prompt: "Keep this response fast and reliable" },
|
|
combo: {
|
|
name: "auto-sla-aware",
|
|
strategy: "auto",
|
|
models: ["openai/gpt-4o-mini", "gemini/gemini-2.5-flash", "claude/claude-sonnet-4-6"],
|
|
autoConfig: {
|
|
routerStrategy: "sla-aware",
|
|
slaTargetP95Ms: 1500,
|
|
slaMaxErrorRate: 0.05,
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log,
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls[0], "gemini/gemini-2.5-flash");
|
|
assert.ok(
|
|
log.entries.some(
|
|
(entry) => entry.level === "info" && /strategy=sla-aware/i.test(String(entry.msg))
|
|
)
|
|
);
|
|
});
|
|
|
|
test("handleComboChat context cache protection pins the model and tags tool-call responses", async () => {
|
|
// PR #3399: <omniModel> tag extraction replaced by server-side session pinning.
|
|
// combo uses priority order; the tag in the input message is stripped but no
|
|
// longer drives routing. No <omniModel> tag is injected in responses.
|
|
const calls: any[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
model: "openai/gpt-4o-mini",
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: "cached\n<omniModel>claude/claude-sonnet-4-6</omniModel>",
|
|
},
|
|
],
|
|
},
|
|
combo: {
|
|
name: "context-cache-pinned",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4o-mini", "claude/claude-sonnet-4-6"],
|
|
context_cache_protection: true,
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
return okResponse({
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: "assistant",
|
|
tool_calls: [
|
|
{
|
|
id: "call_1",
|
|
type: "function",
|
|
function: { name: "lookup_weather", arguments: "{}" },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.ok, true);
|
|
// Server-side pinning: routes via priority order, not the <omniModel> tag.
|
|
assert.deepEqual(calls, ["openai/gpt-4o-mini"]);
|
|
// No <omniModel> tag injected into response content (replaced by session store).
|
|
assert.ok(!payload.choices[0].message.content);
|
|
});
|
|
|
|
test("handleComboChat context cache protection does not inject omniModel tag in streamed output", async () => {
|
|
// PR #3399: <omniModel> tag injection in stream output removed (server-side session pinning).
|
|
const result = await handleComboChat({
|
|
body: { stream: true, messages: [{ role: "user", content: "stream it" }] },
|
|
combo: {
|
|
name: "context-cache-stream",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4o-mini"],
|
|
context_cache_protection: true,
|
|
},
|
|
handleSingleModel: async () =>
|
|
streamResponse([
|
|
'data: {"choices":[{"index":0,"delta":{"content":"hello world"},"finish_reason":null}]}\n\n',
|
|
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
|
|
"data: [DONE]\n\n",
|
|
]),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const text = await result.text();
|
|
assert.equal(result.ok, true);
|
|
assert.match(text, /hello world/);
|
|
assert.doesNotMatch(text, /<omniModel>/);
|
|
});
|
|
|
|
test("handleComboChat context cache protection does not inject tag for tool-call-only streams", async () => {
|
|
// PR #3399: <omniModel> tag injection removed; tool-call streams pass through unmodified.
|
|
const result = await handleComboChat({
|
|
body: { stream: true, messages: [{ role: "user", content: "tool only" }] },
|
|
combo: {
|
|
name: "context-cache-tool-stream",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4o-mini"],
|
|
context_cache_protection: true,
|
|
},
|
|
handleSingleModel: async () =>
|
|
streamResponse([
|
|
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"lookup","arguments":"{}"}}]},"finish_reason":null}]}\n\n',
|
|
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}\n\n',
|
|
"data: [DONE]\n\n",
|
|
]),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const text = await result.text();
|
|
assert.equal(result.ok, true);
|
|
assert.match(text, /"finish_reason":"tool_calls"/);
|
|
assert.doesNotMatch(text, /<omniModel>/);
|
|
});
|
|
|
|
test("handleComboChat context cache protection flushes cleanly when a stream ends without content", async () => {
|
|
// PR #3399: no <omniModel> tag injected; empty stream passes through [DONE] unchanged.
|
|
const result = await handleComboChat({
|
|
body: { stream: true, messages: [{ role: "user", content: "empty stream" }] },
|
|
combo: {
|
|
name: "context-cache-empty-stream",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4o-mini"],
|
|
context_cache_protection: true,
|
|
},
|
|
handleSingleModel: async () => streamResponse(["data: [DONE]\n\n"]),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const text = await result.text();
|
|
assert.equal(result.ok, true);
|
|
assert.match(text, /data: \[DONE\]/);
|
|
assert.doesNotMatch(text, /<omniModel>/);
|
|
});
|
|
|
|
test("handleComboChat round-robin resolves nested combos and returns inactive when every target is skipped", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-nested-inactive",
|
|
strategy: "round-robin",
|
|
models: ["nested-combo"],
|
|
},
|
|
handleSingleModel: async () => {
|
|
throw new Error("round-robin should not execute when all nested targets are inactive");
|
|
},
|
|
isModelAvailable: async () => false,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: [
|
|
{ name: "rr-nested-inactive", models: ["nested-combo"] },
|
|
{ name: "nested-combo", models: ["openai/model-a"] },
|
|
],
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 503);
|
|
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
|
});
|
|
|
|
test("handleComboChat round-robin treats provider circuit breaker responses as ordinary target failures", async () => {
|
|
const calls = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-provider-breaker-open",
|
|
strategy: "round-robin",
|
|
models: ["openai/model-a", "openai/model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body, modelStr) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "openai/model-a") {
|
|
return providerBreakerOpenResponse();
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/model-a", "openai/model-b"]);
|
|
});
|
|
|
|
test("handleComboChat round-robin retries a transient failure on the same model before succeeding", async () => {
|
|
const calls: any[] = [];
|
|
let attempts = 0;
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-same-model-retry",
|
|
strategy: "round-robin",
|
|
models: ["model-a"],
|
|
config: { maxRetries: 1, retryDelayMs: 1, concurrencyPerModel: 1, queueTimeoutMs: 5 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
attempts += 1;
|
|
if (attempts === 1) return errorResponse(503, "try again");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-a"]);
|
|
});
|
|
|
|
test("handleComboChat round-robin recovers from 400s when a later model succeeds", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-provider-400-recover",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0, retryDelayMs: 1, concurrencyPerModel: 1, queueTimeoutMs: 5 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") {
|
|
return new Response(
|
|
JSON.stringify({ error: { message: "unsupported message role for this provider" } }),
|
|
{
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
return okResponse({ choices: [{ message: { content: "recovered" } }] });
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
test("handleComboChat single-target quality failure returns explicit quality error instead of ALL_ACCOUNTS_INACTIVE", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "single-target-quality-failure",
|
|
strategy: "priority",
|
|
models: ["openai/model-a"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async () =>
|
|
new Response('{"choices":[{"message":{"content":"unterminated"}}', {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 502);
|
|
assert.match(payload.error.message, /quality validation/i);
|
|
assert.notEqual(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
|
});
|
|
|
|
test("handleComboChat round-robin single-target quality failure returns explicit quality error instead of ALL_ACCOUNTS_INACTIVE", async () => {
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-single-target-quality-failure",
|
|
strategy: "round-robin",
|
|
models: ["openai/model-a"],
|
|
config: { maxRetries: 0, retryDelayMs: 1, concurrencyPerModel: 1, queueTimeoutMs: 5 },
|
|
},
|
|
handleSingleModel: async () =>
|
|
new Response('{"choices":[{"message":{"content":"unterminated"}}', {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.status, 502);
|
|
assert.match(payload.error.message, /quality validation/i);
|
|
assert.notEqual(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
|
});
|
|
|
|
test("handleComboChat falls back to next model when first model returns all-accounts-rate-limited 503", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "all-accounts-rate-limited-fallback",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-b") {
|
|
return okResponse({ choices: [{ message: { content: "ok" } }] });
|
|
}
|
|
// Simulate handleNoCredentials returning a 503 with "unavailable" message
|
|
// This is the signal emitted when getProviderCredentialsWithQuotaPreflight exhausts all accounts
|
|
return new Response(
|
|
JSON.stringify({ error: { message: `[provider/model] Service temporarily unavailable` } }),
|
|
{
|
|
status: 503,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
// First model returns 503 with "unavailable" → combo should try model-b next
|
|
// If the fix is not applied, combo would abort here and return 503 immediately
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
assert.equal(payload.choices[0].message.content, "ok");
|
|
});
|
|
|
|
test("handleComboChat round-robin falls back when all-accounts-rate-limited 503 is returned", async () => {
|
|
const calls: any[] = [];
|
|
|
|
// Use distinct provider prefixes so #1731 exhaustedProviders does not block model-b
|
|
// (getTargetProvider("openai/model-a") → "openai"; getTargetProvider("anthropic/model-b") → "anthropic")
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-all-accounts-rate-limited",
|
|
strategy: "round-robin",
|
|
models: ["openai/model-a", "anthropic/model-b"],
|
|
config: { maxRetries: 0, retryDelayMs: 1, concurrencyPerModel: 1, queueTimeoutMs: 5 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "anthropic/model-b") {
|
|
return okResponse({ choices: [{ message: { content: "ok" } }] });
|
|
}
|
|
// Simulate all accounts rate-limited — handleNoCredentials signal
|
|
return new Response(
|
|
JSON.stringify({ error: { message: `[provider/model] Service temporarily unavailable` } }),
|
|
{
|
|
status: 503,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/model-a", "anthropic/model-b"]);
|
|
assert.equal(payload.choices[0].message.content, "ok");
|
|
});
|
|
|
|
test("handleComboChat aborts combo when 503 response does NOT contain the unavailable signal", async () => {
|
|
const calls: any[] = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "503-no-signal-abort",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: any) => {
|
|
calls.push(modelStr);
|
|
// A generic 503 that is NOT an all-accounts-rate-limited signal
|
|
// (missing "unavailable" in message or wrong content-type)
|
|
return new Response(JSON.stringify({ error: { message: "Server error" } }), {
|
|
status: 503,
|
|
headers: { "content-type": "text/html" },
|
|
});
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as any;
|
|
// Without the fix, combo would abort (still 503). With the fix, it's still 503 because
|
|
// the signal check filters out non-JSON or non-"unavailable" responses.
|
|
assert.equal(result.status, 503);
|
|
// Model-a was tried, it failed with 503, so it fell back to model-b, which also returned 503.
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
assert.ok(
|
|
payload.error?.message?.includes("Server error") ||
|
|
payload.error?.message?.includes("unavailable") ||
|
|
result.status === 503
|
|
);
|
|
});
|
|
|
|
test("#3587 reasoning model gets max_tokens buffer applied", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"gpt-4o-reasoning": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
},
|
|
});
|
|
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 4096 },
|
|
combo: {
|
|
name: "reasoning-buffer",
|
|
models: ["openai/gpt-4o-reasoning"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>) => {
|
|
bodies.push(body);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(bodies.length, 1, "should have called handleSingleModel once");
|
|
// 4096 * 1.5 = 6144; max(4096+1000, 6144) = 6144
|
|
assert.equal(bodies[0].max_tokens, 6144, "max_tokens should be buffered for reasoning model");
|
|
});
|
|
|
|
test("#3587 reasoning buffer preserves max_tokens when the full buffer exceeds model cap", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"gemini-high-cap": capabilityEntry(65536, { reasoning: true, limit_output: 65536 }),
|
|
},
|
|
});
|
|
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/gemini-high-cap", 64000),
|
|
64000,
|
|
"near-cap requests should not be inflated beyond the model's accepted range"
|
|
);
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/gemini-high-cap", "4096"),
|
|
6144,
|
|
"numeric string max_tokens should be normalized before applying a safe buffer"
|
|
);
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/gemini-high-cap", "not-a-number"),
|
|
null,
|
|
"non-numeric string max_tokens should not be changed"
|
|
);
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/gemini-high-cap", 70000),
|
|
65536,
|
|
"already over-cap max_tokens should be clamped to a known explicit cap"
|
|
);
|
|
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 64000 },
|
|
combo: {
|
|
name: "reasoning-buffer-near-cap",
|
|
models: ["openai/gemini-high-cap"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>) => {
|
|
bodies.push(body);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(bodies.length, 1, "should have called handleSingleModel once");
|
|
assert.equal(bodies[0].max_tokens, 64000, "max_tokens should remain within the cap");
|
|
});
|
|
|
|
test("#3587 reasoning buffer is disabled without explicit model capability data", async () => {
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("missing-provider/unknown-reasoning-model", 100),
|
|
null,
|
|
"unknown models must not receive heuristic token inflation"
|
|
);
|
|
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"capless-reasoning": capabilityEntry(8192, {
|
|
reasoning: true,
|
|
limit_output: null,
|
|
}),
|
|
"default-cap-reasoning": capabilityEntry(8192, {
|
|
reasoning: true,
|
|
limit_output: 8192,
|
|
}),
|
|
},
|
|
});
|
|
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/capless-reasoning", 100),
|
|
null,
|
|
"reasoning metadata without an explicit output cap is not safe enough to inflate"
|
|
);
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/default-cap-reasoning", 300),
|
|
1300,
|
|
"explicit default-sized caps are treated as real capability data"
|
|
);
|
|
});
|
|
|
|
test("#3588 reasoning token buffer feature flag preserves client max_tokens", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"flagged-reasoning": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
},
|
|
});
|
|
|
|
assert.equal(
|
|
resolveReasoningBufferedMaxTokens("openai/flagged-reasoning", 4096, { enabled: false }),
|
|
null,
|
|
"disabled feature flag should skip reasoning token inflation"
|
|
);
|
|
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 4096 },
|
|
combo: {
|
|
name: "reasoning-buffer-disabled",
|
|
models: ["openai/flagged-reasoning"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>) => {
|
|
bodies.push(body);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
reasoningTokenBufferEnabled: false,
|
|
},
|
|
},
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(bodies.length, 1, "should have called handleSingleModel once");
|
|
assert.equal(bodies[0].max_tokens, 4096, "feature flag should preserve client max_tokens");
|
|
});
|
|
|
|
test("#3587 non-reasoning model does not get max_tokens buffer", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"gpt-4o-plain": capabilityEntry(4096, { reasoning: false }),
|
|
},
|
|
});
|
|
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 4096 },
|
|
combo: {
|
|
name: "no-reasoning-buffer",
|
|
models: ["openai/gpt-4o-plain"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>) => {
|
|
bodies.push(body);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(bodies.length, 1, "should have called handleSingleModel once");
|
|
// Non-reasoning model: max_tokens should NOT be buffered
|
|
assert.equal(
|
|
bodies[0].max_tokens,
|
|
4096,
|
|
"max_tokens should remain unchanged for non-reasoning model"
|
|
);
|
|
});
|
|
|
|
test("#3587 round-robin buffer does NOT compound across reasoning models", async () => {
|
|
// Two reasoning models in a round-robin combo. The first fails (400) so the
|
|
// loop falls through to the second. The buffer must be computed from the
|
|
// ORIGINAL max_tokens for each attempt — never from an already-buffered value —
|
|
// so both attempts see 6144 (4096 * 1.5), not [6144, 9216, ...]. Regression for
|
|
// the shared-`body` mutation that compounded the buffer on every RR iteration.
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"rr-reasoning-a": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
"rr-reasoning-b": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
},
|
|
});
|
|
|
|
const seen: Array<{ model: string; maxTokens: unknown }> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 4096 },
|
|
combo: {
|
|
name: "rr-reasoning-no-compound",
|
|
strategy: "round-robin",
|
|
models: ["openai/rr-reasoning-a", "openai/rr-reasoning-b"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>, modelStr: string) => {
|
|
seen.push({ model: modelStr, maxTokens: body.max_tokens });
|
|
if (modelStr === "openai/rr-reasoning-a") {
|
|
return new Response(JSON.stringify({ error: { message: "transient" } }), {
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(seen.length, 2, "both reasoning models should have been attempted");
|
|
// Each attempt buffers from the original 4096 → 6144. No compounding.
|
|
assert.equal(seen[0].maxTokens, 6144, "first reasoning model buffered from original");
|
|
assert.equal(
|
|
seen[1].maxTokens,
|
|
6144,
|
|
"second reasoning model must ALSO buffer from original 4096, not 6144"
|
|
);
|
|
});
|
|
|
|
test("#3588 round-robin honors disabled reasoning token buffer feature flag", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"rr-flagged-a": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
"rr-flagged-b": capabilityEntry(12000, { reasoning: true, limit_output: 12000 }),
|
|
},
|
|
});
|
|
|
|
const seen: Array<{ model: string; maxTokens: unknown }> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 4096 },
|
|
combo: {
|
|
name: "rr-reasoning-buffer-disabled",
|
|
strategy: "round-robin",
|
|
models: ["openai/rr-flagged-a", "openai/rr-flagged-b"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>, modelStr: string) => {
|
|
seen.push({ model: modelStr, maxTokens: body.max_tokens });
|
|
if (modelStr === "openai/rr-flagged-a") {
|
|
return new Response(JSON.stringify({ error: { message: "transient" } }), {
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
reasoningTokenBufferEnabled: false,
|
|
},
|
|
},
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(seen.length, 2, "both reasoning models should have been attempted");
|
|
assert.equal(seen[0].maxTokens, 4096, "first model should preserve client max_tokens");
|
|
assert.equal(seen[1].maxTokens, 4096, "second model should preserve client max_tokens");
|
|
});
|
|
|
|
test("#3587 round-robin keeps near-cap reasoning max_tokens unchanged", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"rr-near-cap-a": capabilityEntry(65536, { reasoning: true, limit_output: 65536 }),
|
|
"rr-near-cap-b": capabilityEntry(65536, { reasoning: true, limit_output: 65536 }),
|
|
},
|
|
});
|
|
|
|
const seen: Array<{ model: string; maxTokens: unknown }> = [];
|
|
const result = await handleComboChat({
|
|
body: { max_tokens: 64000 },
|
|
combo: {
|
|
name: "rr-reasoning-near-cap",
|
|
strategy: "round-robin",
|
|
models: ["openai/rr-near-cap-a", "openai/rr-near-cap-b"],
|
|
},
|
|
handleSingleModel: async (body: Record<string, unknown>, modelStr: string) => {
|
|
seen.push({ model: modelStr, maxTokens: body.max_tokens });
|
|
if (modelStr === "openai/rr-near-cap-a") {
|
|
return new Response(JSON.stringify({ error: { message: "transient" } }), {
|
|
status: 400,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: {
|
|
comboDefaults: {
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
maxRetries: 0,
|
|
retryDelayMs: 1,
|
|
},
|
|
},
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.equal(seen.length, 2, "both reasoning models should have been attempted");
|
|
assert.equal(seen[0].maxTokens, 64000, "first reasoning model should keep max_tokens");
|
|
assert.equal(seen[1].maxTokens, 64000, "second reasoning model should keep max_tokens");
|
|
});
|