chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createChatPipelineHarness } from "./_chatPipelineHarness.ts";
|
||||
|
||||
const harness = await createChatPipelineHarness("combo-routing");
|
||||
const callLogs = await import("../../src/lib/usage/callLogs.ts");
|
||||
const {
|
||||
BaseExecutor,
|
||||
buildClaudeResponse,
|
||||
buildGeminiResponse,
|
||||
buildOpenAIResponse,
|
||||
buildRequest,
|
||||
combosDb,
|
||||
handleChat,
|
||||
modelComboMappingsDb,
|
||||
resetStorage,
|
||||
seedConnection,
|
||||
toPlainHeaders,
|
||||
waitFor,
|
||||
} = harness;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
BaseExecutor.RETRY_CONFIG.delayMs = 0;
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
BaseExecutor.RETRY_CONFIG.delayMs = harness.originalRetryDelayMs;
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await harness.cleanup();
|
||||
});
|
||||
|
||||
function buildOpenAIChatBody(model, content = `Route ${model}`) {
|
||||
return {
|
||||
model,
|
||||
stream: false,
|
||||
messages: [{ role: "user", content }],
|
||||
};
|
||||
}
|
||||
|
||||
test("combo routes requests by exact combo name", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-combo-exact" });
|
||||
await combosDb.createCombo({
|
||||
name: "router-priority",
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4o-mini"],
|
||||
});
|
||||
|
||||
const fetchCalls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
fetchCalls.push({
|
||||
url: String(url),
|
||||
headers: toPlainHeaders(init.headers),
|
||||
});
|
||||
return buildOpenAIResponse("Exact combo route");
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-priority"),
|
||||
})
|
||||
);
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /\/chat\/completions$/);
|
||||
assert.equal(fetchCalls[0].headers.authorization, "Bearer sk-openai-combo-exact");
|
||||
assert.equal(json.choices[0].message.content, "Exact combo route");
|
||||
});
|
||||
|
||||
test("round-robin combo cycles through three providers", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-rr" });
|
||||
await seedConnection("claude", { apiKey: "sk-claude-rr" });
|
||||
await seedConnection("gemini", { apiKey: "sk-gemini-rr" });
|
||||
await combosDb.createCombo({
|
||||
name: "router-rr",
|
||||
strategy: "round-robin",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
models: ["openai/gpt-4o-mini", "claude/claude-3-5-sonnet-20241022", "gemini/gemini-2.5-flash"],
|
||||
});
|
||||
|
||||
const seenProviders = [];
|
||||
globalThis.fetch = async (url) => {
|
||||
const target = String(url);
|
||||
if (target.includes("/chat/completions")) {
|
||||
seenProviders.push("openai");
|
||||
return buildOpenAIResponse("OpenAI round-robin");
|
||||
}
|
||||
if (target.includes("?beta=true")) {
|
||||
seenProviders.push("claude");
|
||||
return buildClaudeResponse("Claude round-robin");
|
||||
}
|
||||
seenProviders.push("gemini");
|
||||
return buildGeminiResponse("Gemini round-robin");
|
||||
};
|
||||
|
||||
// `stickyRoundRobinLimit` defaults to 3 (src/lib/db/settings.ts), so a combo using the
|
||||
// round-robin strategy serves 3 consecutive requests per target before advancing to the
|
||||
// next one — batched rotation, not one-request-per-target. Nine requests therefore cover
|
||||
// a full cycle: openai x3 -> claude x3 -> gemini x3.
|
||||
const responses = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
responses.push(await handleChat(buildRequest({ body: buildOpenAIChatBody("router-rr") })));
|
||||
}
|
||||
|
||||
for (const response of responses) {
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(seenProviders, [
|
||||
"openai",
|
||||
"openai",
|
||||
"openai",
|
||||
"claude",
|
||||
"claude",
|
||||
"claude",
|
||||
"gemini",
|
||||
"gemini",
|
||||
"gemini",
|
||||
]);
|
||||
});
|
||||
|
||||
test("priority combo sticks to the primary model while healthy", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-priority" });
|
||||
await seedConnection("claude", { apiKey: "sk-claude-priority" });
|
||||
await combosDb.createCombo({
|
||||
name: "router-priority-healthy",
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4o-mini", "claude/claude-3-5-sonnet-20241022"],
|
||||
});
|
||||
|
||||
const seenTargets = [];
|
||||
globalThis.fetch = async (url) => {
|
||||
seenTargets.push(String(url));
|
||||
return buildOpenAIResponse("Primary stayed active");
|
||||
};
|
||||
|
||||
const first = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-priority-healthy", "Route priority first"),
|
||||
})
|
||||
);
|
||||
const second = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-priority-healthy", "Route priority second"),
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(second.status, 200);
|
||||
assert.equal(seenTargets.length, 2);
|
||||
assert.ok(seenTargets.every((target) => target.includes("/chat/completions")));
|
||||
});
|
||||
|
||||
test("priority combo falls back to the secondary model when the first one fails", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-fallback" });
|
||||
await seedConnection("claude", { apiKey: "sk-claude-fallback" });
|
||||
await combosDb.createCombo({
|
||||
name: "router-fallback",
|
||||
strategy: "priority",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
models: ["openai/gpt-4o-mini", "claude/claude-3-5-sonnet-20241022"],
|
||||
});
|
||||
|
||||
const attempts = [];
|
||||
globalThis.fetch = async (url) => {
|
||||
const target = String(url);
|
||||
attempts.push(target);
|
||||
if (attempts.length === 1) {
|
||||
return new Response(JSON.stringify({ error: { message: "primary down" } }), {
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return buildClaudeResponse("Fallback answered");
|
||||
};
|
||||
|
||||
const response = await handleChat(buildRequest({ body: buildOpenAIChatBody("router-fallback") }));
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(attempts.length, 2);
|
||||
assert.match(attempts[0], /\/chat\/completions$/);
|
||||
assert.match(attempts[1], /\?beta=true$/);
|
||||
assert.equal(json.choices[0].message.content, "Fallback answered");
|
||||
});
|
||||
|
||||
test("priority combo can repeat the same provider/model with different fixed accounts", async () => {
|
||||
const firstConn = await seedConnection("openai", {
|
||||
name: "openai-fixed-1",
|
||||
apiKey: "sk-openai-fixed-1",
|
||||
});
|
||||
const secondConn = await seedConnection("openai", {
|
||||
name: "openai-fixed-2",
|
||||
apiKey: "sk-openai-fixed-2",
|
||||
});
|
||||
assert.notEqual(firstConn.id, secondConn.id);
|
||||
await combosDb.createCombo({
|
||||
name: "router-fixed-accounts",
|
||||
strategy: "priority",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
models: [
|
||||
{
|
||||
id: "step-openai-primary",
|
||||
kind: "model",
|
||||
providerId: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
connectionId: firstConn.id,
|
||||
},
|
||||
{
|
||||
id: "step-openai-secondary",
|
||||
kind: "model",
|
||||
providerId: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
connectionId: secondConn.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const authHeaders = [];
|
||||
let firstAttemptHeader = null;
|
||||
globalThis.fetch = async (_url, init = {}) => {
|
||||
const headers = toPlainHeaders(init.headers);
|
||||
authHeaders.push(headers.authorization);
|
||||
if (!firstAttemptHeader) {
|
||||
firstAttemptHeader = headers.authorization;
|
||||
return new Response(JSON.stringify({ error: { message: "first account down" } }), {
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return buildOpenAIResponse("Second fixed account answered");
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-fixed-accounts"),
|
||||
})
|
||||
);
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(authHeaders.length, 2);
|
||||
assert.notEqual(authHeaders[0], authHeaders[1]);
|
||||
assert.deepEqual(
|
||||
new Set(authHeaders),
|
||||
new Set(["Bearer sk-openai-fixed-1", "Bearer sk-openai-fixed-2"])
|
||||
);
|
||||
assert.equal(json.choices[0].message.content, "Second fixed account answered");
|
||||
|
||||
const comboLogs = await waitFor(async () => {
|
||||
const logs = await callLogs.getCallLogs({ combo: true, limit: 10 });
|
||||
return logs.length >= 2 ? logs : null;
|
||||
});
|
||||
|
||||
assert.ok(comboLogs, "expected combo call logs to be persisted");
|
||||
const targetLogs = comboLogs
|
||||
.filter((entry) => entry.comboName === "router-fixed-accounts")
|
||||
.sort((left, right) => (left.timestamp || "").localeCompare(right.timestamp || ""));
|
||||
|
||||
assert.equal(targetLogs.length, 2);
|
||||
assert.deepEqual(
|
||||
targetLogs.map((entry) => ({
|
||||
comboStepId: entry.comboStepId,
|
||||
comboExecutionKey: entry.comboExecutionKey,
|
||||
status: entry.status,
|
||||
})),
|
||||
[
|
||||
{
|
||||
comboStepId: "step-openai-primary",
|
||||
comboExecutionKey: "step-openai-primary",
|
||||
status: 503,
|
||||
},
|
||||
{
|
||||
comboStepId: "step-openai-secondary",
|
||||
comboExecutionKey: "step-openai-secondary",
|
||||
status: 200,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("model combo mappings route explicit model ids through the configured combo", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-mapped" });
|
||||
const combo = await combosDb.createCombo({
|
||||
name: "mapped-router",
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4o-mini"],
|
||||
});
|
||||
await modelComboMappingsDb.createModelComboMapping({
|
||||
pattern: "tenant/mapped-model",
|
||||
comboId: combo.id,
|
||||
priority: 100,
|
||||
});
|
||||
|
||||
const fetchCalls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
fetchCalls.push({
|
||||
url: String(url),
|
||||
headers: toPlainHeaders(init.headers),
|
||||
});
|
||||
return buildOpenAIResponse("Mapped combo route");
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("tenant/mapped-model"),
|
||||
})
|
||||
);
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].headers.authorization, "Bearer sk-openai-mapped");
|
||||
assert.equal(json.choices[0].message.content, "Mapped combo route");
|
||||
});
|
||||
|
||||
test("wildcard model combo mappings resolve arbitrary matching models", async () => {
|
||||
await seedConnection("gemini", { apiKey: "sk-gemini-wild" });
|
||||
const combo = await combosDb.createCombo({
|
||||
name: "wild-router",
|
||||
strategy: "priority",
|
||||
models: ["gemini/gemini-2.5-flash"],
|
||||
});
|
||||
await modelComboMappingsDb.createModelComboMapping({
|
||||
pattern: "tenant/*",
|
||||
comboId: combo.id,
|
||||
priority: 10,
|
||||
});
|
||||
|
||||
const fetchCalls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
fetchCalls.push({
|
||||
url: String(url),
|
||||
headers: toPlainHeaders(init.headers),
|
||||
});
|
||||
return buildGeminiResponse("Wildcard combo route");
|
||||
};
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("tenant/any-model-name"),
|
||||
})
|
||||
);
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /generateContent$/);
|
||||
assert.equal(fetchCalls[0].headers["x-goog-api-key"], "sk-gemini-wild");
|
||||
assert.equal(json.choices[0].message.content, "Wildcard combo route");
|
||||
});
|
||||
|
||||
test("unmapped custom model requests fail after combo resolution falls through", async () => {
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("tenant/unmapped-model"),
|
||||
})
|
||||
);
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
// Upstream port decolua/9router#336: 400 → 404 so combo routing can fall through
|
||||
// to the next target when a provider has zero usable credentials.
|
||||
assert.equal(response.status, 404);
|
||||
assert.match(json.error.message, /No active credentials for provider: tenant/);
|
||||
});
|
||||
|
||||
test("strategy updates take effect for later requests on the same combo name", async () => {
|
||||
await seedConnection("openai", { apiKey: "sk-openai-update" });
|
||||
await seedConnection("claude", { apiKey: "sk-claude-update" });
|
||||
const combo = await combosDb.createCombo({
|
||||
name: "router-dynamic",
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4o-mini", "claude/claude-3-5-sonnet-20241022"],
|
||||
});
|
||||
|
||||
const seenProviders = [];
|
||||
globalThis.fetch = async (url) => {
|
||||
const target = String(url);
|
||||
if (target.includes("?beta=true")) {
|
||||
seenProviders.push("claude");
|
||||
return buildClaudeResponse("Claude after update");
|
||||
}
|
||||
seenProviders.push("openai");
|
||||
return buildOpenAIResponse("OpenAI before update");
|
||||
};
|
||||
|
||||
const initial = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-dynamic", "Route dynamic initial"),
|
||||
})
|
||||
);
|
||||
await combosDb.updateCombo((combo as any).id, {
|
||||
strategy: "round-robin",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
});
|
||||
// After the strategy flips to round-robin the new behavior must take effect. With the
|
||||
// default stickyRoundRobinLimit of 3 the round-robin batch serves openai three times
|
||||
// before advancing, so the 4th post-update request rotates to claude — a provider the
|
||||
// previous "priority" strategy would never have reached. That rotation is the proof the
|
||||
// updated strategy is live.
|
||||
const postUpdate = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
postUpdate.push(
|
||||
await handleChat(
|
||||
buildRequest({ body: buildOpenAIChatBody("router-dynamic", `Route dynamic ${i}`) })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
assert.equal(initial.status, 200);
|
||||
for (const response of postUpdate) {
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(seenProviders, ["openai", "openai", "openai", "openai", "claude"]);
|
||||
});
|
||||
Reference in New Issue
Block a user