Menu
popagent
publicLatest change dc4b371554df3a78c40c29085ead1db3de7e24f8 - Add model routing policy, companyStatus tool, Markdown channel output, compact Runtime settings by AkurAI Build
import { describe, expect, test } from "bun:test";
import { ModelRouter } from "./model-router";
import { applyReasoningEffort } from "./models";
import { modelSpec, parseModelSpec, ROUTING_MODEL_ID, type ModelRouting } from "./api-types";
const source = (routing: Partial<ModelRouting>, defaultModel = "cc/claude-sonnet-5") => ({
get: async () => ({
defaultModel,
modelRouting: { enabled: true, strategy: "fallback" as const, cooldownMs: 600_000, routes: [], ...routing },
}),
});
const routes = [
{ model: "cc/claude-sonnet-5", reasoningEffort: "low" as const },
{ model: "cx/gpt-5.6-sol", reasoningEffort: "low" as const },
];
describe("model specs", () => {
test("encode reasoning effort in the model id and strip it on the wire", () => {
expect(modelSpec(routes[0]!)).toBe("cc/claude-sonnet-5@low");
expect(parseModelSpec("cx/gpt-5.6-sol@high")).toEqual({ model: "cx/gpt-5.6-sol", reasoningEffort: "high" });
expect(parseModelSpec("cx/gpt-5.6-sol")).toEqual({ model: "cx/gpt-5.6-sol", reasoningEffort: "default" });
expect(applyReasoningEffort({ model: "cc/claude-sonnet-5@low", messages: [] }))
.toEqual({ model: "cc/claude-sonnet-5", reasoning_effort: "low", messages: [] });
expect(applyReasoningEffort({ model: "cc/claude-sonnet-5", messages: [] })).toEqual({ model: "cc/claude-sonnet-5", messages: [] });
});
});
describe("ModelRouter", () => {
test("passes concrete model ids through untouched", async () => {
const router = new ModelRouter(source({ routes }));
expect(await router.pick("ag/gemini-3-flash")).toBe("ag/gemini-3-flash");
});
test("falls back to the runtime default when routing is disabled or empty", async () => {
expect(await new ModelRouter(source({ enabled: false, routes })).pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5");
expect(await new ModelRouter(source({ routes: [] })).pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5");
});
test("fallback prefers the primary and moves on only after a quota or unknown-model failure", async () => {
let now = 1_000_000;
const router = new ModelRouter(source({ routes }), () => now);
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
expect(await router.reportFailure("cc/claude-sonnet-5@low", new Error("model timed out"))).toBe(false);
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
expect(await router.reportFailure("cc/claude-sonnet-5@low", Object.assign(new Error("[429]: The usage limit has been reached (reset after 30m)"), { statusCode: 429 }))).toBe(true);
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cx/gpt-5.6-sol@low");
// The 30 m reset hint extends the configured 10 m cooldown.
now += 15 * 60_000;
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cx/gpt-5.6-sol@low");
now += 16 * 60_000;
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
expect(await router.reportFailure("cx/gpt-5.6-sol@low", new Error("Unknown model: cx/gpt-5.6-sol"))).toBe(true);
const health = await router.health();
expect(health.routes.map((route) => [route.spec, route.cooldownUntil !== null])).toEqual([
["cc/claude-sonnet-5@low", false],
["cx/gpt-5.6-sol@low", true],
]);
});
test("round-robin rotates through healthy routes and never stalls when all are cooling", async () => {
let now = 5_000;
const router = new ModelRouter(source({ strategy: "round-robin", routes }), () => now);
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cx/gpt-5.6-sol@low");
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
await router.reportFailure("cc/claude-sonnet-5@low", new Error("rate limit"));
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cx/gpt-5.6-sol@low");
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cx/gpt-5.6-sol@low");
now += 1_000;
await router.reportFailure("cx/gpt-5.6-sol@low", new Error("rate limit"));
// Both cooling: the one that recovers first is used rather than nothing.
expect(await router.pick(ROUTING_MODEL_ID)).toBe("cc/claude-sonnet-5@low");
});
test("ignores failures for specs outside the policy", async () => {
const router = new ModelRouter(source({ routes }));
expect(await router.reportFailure("ag/gemini-3-flash", new Error("rate limit"))).toBe(false);
});
});