mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
3587 lines
109 KiB
TypeScript
3587 lines
109 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 { resolveComboTargets } = await import("../../open-sse/services/combo/comboStructure.ts");
|
|
const { applyPromptCacheAffinity } =
|
|
await import("../../open-sse/services/combo/promptCacheAffinity.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, maxRetries: 5, retryDelay: 100 });
|
|
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("getComboModelsFromData strips context-window tags before matching a combo", () => {
|
|
const combos = [{ name: "alpha", models: ["openai/gpt-4o-mini"] }];
|
|
|
|
assert.deepEqual(getComboModelsFromData("alpha[500k]", combos), ["openai/gpt-4o-mini"]);
|
|
assert.deepEqual(getComboModelsFromData("alpha[1M]", combos), ["openai/gpt-4o-mini"]);
|
|
assert.equal(getComboModelsFromData("alpha[beta]", combos), null);
|
|
});
|
|
|
|
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 preserves the weighted primary before prompt-cache affinity reordering", async () => {
|
|
const combo = {
|
|
name: "weighted-cache-affinity-protection",
|
|
strategy: "weighted",
|
|
models: [
|
|
{ model: "openai/gpt-4o-mini", weight: 1 },
|
|
{ model: "claude/sonnet", weight: 9 },
|
|
],
|
|
config: { maxRetries: 0 },
|
|
};
|
|
const resolvedTargets = resolveComboTargets(combo, null);
|
|
assert.equal(resolvedTargets.length, 2);
|
|
|
|
const cacheKey = Array.from({ length: 100 }, (_, index) => `weighted-cache-${index}`).find(
|
|
(key) =>
|
|
applyPromptCacheAffinity(resolvedTargets, { prompt_cache_key: key }).targets[0] !==
|
|
resolvedTargets[0]
|
|
);
|
|
assert.ok(cacheKey, "test fixture must exercise a different affinity winner");
|
|
|
|
const calls: string[] = [];
|
|
_setSecureRandomFloatSource(() => 0);
|
|
try {
|
|
const result = await handleComboChat({
|
|
body: { prompt_cache_key: cacheKey },
|
|
combo,
|
|
handleSingleModel: async (_body: Record<string, unknown>, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["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");
|
|
});
|
|
|
|
// #10314/#10501: superseded the original "last writer wins" contract (a single
|
|
// `lastError` + raw `[model (status), ...]` suffix). Combo terminal aggregation
|
|
// now lists every distinct per-target reason separately (comboErrorAggregation.ts
|
|
// ::formatComboOutcomes) and derives the terminal status from an explicit policy
|
|
// instead of whichever target happened to fail LAST — a provider 500 mixed with a
|
|
// rate_limit 429 is a heterogeneous, non-client-fault outcome, so it normalizes to
|
|
// a 5xx (::resolveComboTerminalStatus), never a bare 429 that would misrepresent
|
|
// model-a's real 500 as "the client should retry the rate limit".
|
|
test("handleComboChat surfaces EVERY failing target's reason (never drops one) and normalizes a heterogeneous 500+429 mix to 5xx (#8486/#10314/#10501)", 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.ok(
|
|
result.status >= 500,
|
|
`heterogeneous provider(500)+rate_limit(429) must normalize to a 5xx status, got ${result.status}`
|
|
);
|
|
assert.match(payload.error.message, /model-a.*fail:model-a.*HTTP 500/);
|
|
assert.match(payload.error.message, /model-b.*fail:model-b.*HTTP 429/);
|
|
});
|
|
|
|
interface ComboErrorPayload {
|
|
error: { code?: string; message: string };
|
|
}
|
|
|
|
test("handleComboChat global comboTimeoutMs stops iterating remaining targets and returns 504 with aggregated diagnostics", async () => {
|
|
const calls: string[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "timeout-combo",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b", "model-c"],
|
|
// An effectively-zero ceiling: the FIRST target still dispatches (the
|
|
// check only runs after a target completes), but by the time it
|
|
// resolves any nonzero elapsed time trips the timeout, so the loop
|
|
// must stop instead of trying model-b/model-c.
|
|
config: { maxRetries: 0, comboTimeoutMs: 1 },
|
|
},
|
|
handleSingleModel: async (_body: unknown, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
// A tiny real delay guarantees Date.now() advances past the 1ms ceiling
|
|
// before the post-target timeout check runs.
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
return errorResponse(500, `fail:${modelStr}`);
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
const payload = (await result.json()) as ComboErrorPayload;
|
|
|
|
assert.equal(result.status, 504);
|
|
assert.equal(payload.error.code, "COMBO_TIMEOUT");
|
|
assert.match(payload.error.message, /Combo global timeout \(1ms\)/);
|
|
assert.match(payload.error.message, /model-a \(500\)/);
|
|
assert.deepEqual(calls, ["model-a"], "remaining targets must be skipped once the timeout trips");
|
|
});
|
|
|
|
test("handleComboChat comboTimeoutMs=0 (default) never trips the global timeout, all targets still tried", async () => {
|
|
const calls: string[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "no-timeout-combo",
|
|
strategy: "priority",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 0 }, // comboTimeoutMs defaults to 0 (disabled)
|
|
},
|
|
handleSingleModel: async (_body: unknown, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") return errorResponse(500, "fail:model-a");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.status, 200);
|
|
assert.deepEqual(calls, ["model-a", "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"]);
|
|
});
|
|
|
|
// #10314/#10501: same policy update as the priority-strategy test above, applied
|
|
// to the round-robin twin. model-a's 400 is a genuine request-shape/model-class
|
|
// error, but model-b's 500 is an infra/provider failure — since NOT every target
|
|
// failed with a "model" (request-is-invalid) reason, this is a heterogeneous mix
|
|
// and must normalize to a 5xx, never a bare "trust the last target's status" 500.
|
|
test("handleComboChat round-robin surfaces EVERY target's reason and normalizes a heterogeneous 400+500 mix to 5xx (#8486/#10314/#10501)", 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.ok(
|
|
result.status >= 500,
|
|
`heterogeneous model(400)+provider(500) mix must normalize to a 5xx status, got ${result.status}`
|
|
);
|
|
assert.match(payload.error.message, /model-a.*unsupported message role.*HTTP 400/);
|
|
assert.match(payload.error.message, /model-b.*rr-final-fail.*HTTP 500/);
|
|
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 fails closed when context-aware filtering rejects all targets (#8488)", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"no-tools-a": capabilityEntry(128000, { tool_call: false }),
|
|
"no-tools-b": capabilityEntry(128000, { tool_call: false }),
|
|
},
|
|
});
|
|
|
|
const calls: string[] = [];
|
|
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: Record<string, unknown>, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, 400);
|
|
assert.deepEqual(calls, []);
|
|
const body = await result.json();
|
|
assert.match(String(body?.error?.message || ""), /supports tool calling/i);
|
|
assert.equal(body?.error?.code, "capability_mismatch");
|
|
assert.equal(body?.diagnostics?.terminalReason, "capability_mismatch");
|
|
});
|
|
|
|
test("handleComboChat compatFilterFailOpen restores dispatch when all targets fail tools (#8488)", async () => {
|
|
saveModelsDevCapabilities({
|
|
openai: {
|
|
"no-tools-a": capabilityEntry(128000, { tool_call: false }),
|
|
"no-tools-b": capabilityEntry(128000, { tool_call: false }),
|
|
},
|
|
});
|
|
|
|
const calls: string[] = [];
|
|
const result = await handleComboChat({
|
|
body: {
|
|
messages: [{ role: "user", content: "Use a tool." }],
|
|
tools: [{ type: "function", function: { name: "lookup", parameters: {} } }],
|
|
},
|
|
combo: {
|
|
name: "context-aware-fail-open",
|
|
strategy: "priority",
|
|
models: ["openai/no-tools-a", "openai/no-tools-b"],
|
|
config: { compatFilterFailOpen: true },
|
|
},
|
|
handleSingleModel: async (_body: Record<string, unknown>, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
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("cache-optimized preserves eval routing when no reusable cache key exists", async () => {
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "cache-miss-routing",
|
|
suiteName: "Cache Miss Routing",
|
|
target: { type: "model", id: "openai/cache-low", label: "Model: openai/cache-low" },
|
|
summary: { total: 10, passed: 2, failed: 8, passRate: 20 },
|
|
avgLatencyMs: 100,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
evalsDb.saveEvalRun({
|
|
suiteId: "cache-miss-routing",
|
|
suiteName: "Cache Miss Routing",
|
|
target: { type: "model", id: "openai/cache-high", label: "Model: openai/cache-high" },
|
|
summary: { total: 10, passed: 10, failed: 0, passRate: 100 },
|
|
avgLatencyMs: 100,
|
|
results: [],
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
const calls: string[] = [];
|
|
const result = await handleComboChat({
|
|
body: { messages: [{ role: "user", content: "First turn without reusable prefix" }] },
|
|
combo: {
|
|
name: "cache-optimized-miss",
|
|
strategy: "cache-optimized",
|
|
models: ["openai/cache-low", "openai/cache-high"],
|
|
config: {
|
|
evalRouting: {
|
|
enabled: true,
|
|
suiteIds: ["cache-miss-routing"],
|
|
qualityWeight: 1,
|
|
latencyWeight: 0,
|
|
},
|
|
},
|
|
},
|
|
handleSingleModel: async (_body: any, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: { promptCacheAffinityEnabled: false },
|
|
relayOptions: null as any,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["openai/cache-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_TARGETS_SKIPPED");
|
|
});
|
|
|
|
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);
|
|
assert.equal(persistedProvider?.provider, "openai");
|
|
});
|
|
|
|
test("handleComboChat standalone lkgp strategy clears LKGP after the last-known-good target fails", async () => {
|
|
// A prior successful request pinned "openai" as the last known good provider —
|
|
// exactly the state left behind by the previous (success) test's own scenario.
|
|
await settingsDb.setLKGP("standalone-lkgp-clear", "standalone-lkgp-clear", "openai");
|
|
|
|
const calls: string[] = [];
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
id: "standalone-lkgp-clear",
|
|
name: "standalone-lkgp-clear",
|
|
strategy: "lkgp",
|
|
// maxRetries: 0 below means this single target is tried exactly once,
|
|
// then the combo loop gives up on it (and on the whole combo, since it's
|
|
// the only model) — the exact "Done retrying this model" failure path.
|
|
models: ["openai/gpt-4o-mini"],
|
|
config: { maxRetries: 0 },
|
|
},
|
|
handleSingleModel: async (_body: Record<string, unknown>, modelStr: string) => {
|
|
calls.push(modelStr);
|
|
return errorResponse(504, "Stream produced no non-ping SSE event within 95000ms");
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
// Give the async fire-and-forget LKGP clear a chance to execute
|
|
let persistedProvider: Awaited<ReturnType<typeof settingsDb.getLKGP>> = null;
|
|
for (let i = 0; i < 20; i++) {
|
|
persistedProvider = await settingsDb.getLKGP("standalone-lkgp-clear", "standalone-lkgp-clear");
|
|
if (persistedProvider === null) {
|
|
break;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
|
|
assert.equal(result.ok, false, "the only target failed, so the whole combo call fails");
|
|
assert.deepEqual(calls, ["openai/gpt-4o-mini"]);
|
|
// The bug this guards: without clearing, a *separate* subsequent request would
|
|
// keep re-selecting "openai" via LKGP reordering even though it just failed.
|
|
assert.equal(
|
|
persistedProvider,
|
|
null,
|
|
"LKGP must be cleared after its target fails, not left pointing at a just-failed provider"
|
|
);
|
|
});
|
|
|
|
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: { promptCacheAffinityEnabled: false },
|
|
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_TARGETS_SKIPPED");
|
|
});
|
|
|
|
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: failoverBeforeRetry skips the same-model retry and goes straight to the sibling", async () => {
|
|
// #2417's whole point: failoverBeforeRetry should prefer a sibling model
|
|
// over hammering a rate-limited one again. Same shape as the test above
|
|
// (maxRetries: 1, a transient 429 on the first call) but with a second
|
|
// model available and failoverBeforeRetry set — calls must show a single
|
|
// model-a attempt followed directly by model-b, never a same-model retry.
|
|
const calls = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "rr-failover-before-retry",
|
|
strategy: "round-robin",
|
|
models: ["model-a", "model-b"],
|
|
config: {
|
|
maxRetries: 1,
|
|
retryDelayMs: 1,
|
|
failoverBeforeRetry: true,
|
|
concurrencyPerModel: 1,
|
|
queueTimeoutMs: 5,
|
|
},
|
|
},
|
|
handleSingleModel: async (_body, modelStr) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") return errorResponse(429, "rate limited");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
relayOptions: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
test("handleComboChat priority strategy: failoverBeforeRetry skips the same-model retry and goes straight to the sibling", async () => {
|
|
const calls = [];
|
|
|
|
const result = await handleComboChat({
|
|
body: {},
|
|
combo: {
|
|
name: "priority-failover-before-retry",
|
|
models: ["model-a", "model-b"],
|
|
config: { maxRetries: 1, retryDelayMs: 1, failoverBeforeRetry: true },
|
|
},
|
|
handleSingleModel: async (_body, modelStr) => {
|
|
calls.push(modelStr);
|
|
if (modelStr === "model-a") return errorResponse(429, "rate limited");
|
|
return okResponse();
|
|
},
|
|
isModelAvailable: async () => true,
|
|
log: createLog(),
|
|
settings: null,
|
|
allCombos: null,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(calls, ["model-a", "model-b"]);
|
|
});
|
|
|
|
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");
|
|
// #9507: buffer never enlarges an explicit client max_tokens; pass-through 4096.
|
|
assert.equal(bodies[0].max_tokens, 4096, "max_tokens forwarded verbatim (#9507)");
|
|
});
|
|
|
|
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"),
|
|
4096,
|
|
"numeric string max_tokens is normalized and forwarded verbatim (#9507)"
|
|
);
|
|
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),
|
|
300,
|
|
"explicit default-sized caps are treated as real capability data, forwarded verbatim (#9507)"
|
|
);
|
|
});
|
|
|
|
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 the original 4096 (no enlargement per #9507), not a compounded value. 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");
|
|
// #9507: buffer never enlarges, so each attempt sees the original 4096; no compounding.
|
|
assert.equal(seen[0].maxTokens, 4096, "first reasoning model forwards original (#9507)");
|
|
assert.equal(
|
|
seen[1].maxTokens,
|
|
4096,
|
|
"second reasoning model must ALSO forward original 4096, not a buffered value (#9507)"
|
|
);
|
|
});
|
|
|
|
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");
|
|
});
|