AkurAI Build
Menu

popagent

public

Latest 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);
  });
});