auto-model-router 0.4.14 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { addExtensions, codexBlock, joinTeam, setDotenv, type JoinOptions } from "../src/cli/join.ts";
7
+ import { parseTeamClient, readTeamClient, teamProviderRegistration } from "../omp-extension/team-logic.ts";
8
+
9
+ /**
10
+ * Team-client mode: team.json puts the omp extensions on the team endpoint,
11
+ * and `join` configures every harness it finds without touching anything it
12
+ * does not recognise.
13
+ */
14
+
15
+ describe("team-logic", () => {
16
+ test("team.json is parsed defensively and turned into omp's provider registration", () => {
17
+ expect(parseTeamClient("nope")).toBeNull();
18
+ expect(parseTeamClient(JSON.stringify({ url: "https://t/", key: "" }))).toBeNull();
19
+ const t = parseTeamClient(JSON.stringify({ url: "https://team.example/", key: "amrt_k", userId: "u_1", name: "Ada" }))!;
20
+ expect(t.url).toBe("https://team.example");
21
+ const reg = teamProviderRegistration(t, "sess-1", true, { inputPerMtok: 1, outputPerMtok: 4 });
22
+ expect(reg).toMatchObject({ baseUrl: "https://team.example/v1", api: "openai-completions", apiKey: "amrt_k", headers: { "X-Omp-Session": "sess-1", "X-Omp-Subagent": "1" } });
23
+ expect(reg.models.map((m) => m.id)).toEqual(["auto", "auto-cheap", "auto-max"]);
24
+ expect(reg.models[0]!.cost).toEqual({ input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1.25 });
25
+ const dir = mkdtempSync(join(tmpdir(), "amr-team-client-"));
26
+ expect(readTeamClient(dir)).toBeNull();
27
+ writeFileSync(join(dir, "team.json"), JSON.stringify({ url: "https://t", key: "k" }));
28
+ expect(readTeamClient(dir)?.key).toBe("k");
29
+ rmSync(dir, { recursive: true, force: true });
30
+ });
31
+
32
+ test("text edits keep files byte-identical apart from the lines they add", () => {
33
+ expect(addExtensions("", ["/a.ts"])).toBe("extensions:\n - /a.ts\n");
34
+ expect(addExtensions("foo: 1\nextensions:\n - /x.ts\nbar: 2\n", ["/x.ts", "/a.ts"])).toBe("foo: 1\nextensions:\n - /a.ts\n - /x.ts\nbar: 2\n");
35
+ expect(addExtensions("foo: 1\r\n", ["/a.ts"])).toBe("foo: 1\r\nextensions:\r\n - /a.ts\r\n");
36
+ expect(setDotenv("A=1\nB=2\n", { B: "3", C: "4" })).toBe("A=1\nB=3\nC=4\n");
37
+ expect(setDotenv("", { A: "1" })).toBe("A=1\n");
38
+ expect(codexBlock("https://t")).toContain('base_url = "https://t/v1"');
39
+ });
40
+ });
41
+
42
+ describe("join", () => {
43
+ function scenario(extra: Partial<JoinOptions> = {}): { home: string; o: JoinOptions } {
44
+ const home = mkdtempSync(join(tmpdir(), "amr-join-"));
45
+ const agent = join(home, ".omp", "agent");
46
+ mkdirSync(agent, { recursive: true });
47
+ writeFileSync(join(agent, "config.yml"), "extensions:\n - E:/other/ext.ts\nsetupVersion: 2\n");
48
+ mkdirSync(join(home, ".hermes"), { recursive: true });
49
+ writeFileSync(join(home, ".hermes", ".env"), "OPENAI_API_KEY=x\n");
50
+ mkdirSync(join(home, ".codex"), { recursive: true });
51
+ writeFileSync(join(home, ".codex", "config.toml"), 'model = "gpt-5"\n');
52
+ const o: JoinOptions = { url: "https://team.example", key: "amrt_key", userId: "u_ada", name: "Ada", profile: false, dryRun: false, only: [], env: { HOME: home, HERMES_HOME: join(home, ".hermes"), PI_CODING_AGENT_DIR: agent, AUTO_MODEL_ROUTER_HOME: join(home, ".auto-model-router") }, home, packageDir: process.cwd(), platform: "linux", pathHas: (b) => b === "claude" || b === "aider", ...extra };
53
+ return { home, o };
54
+ }
55
+
56
+ test("writes team.json and configures omp, Hermes, Codex, Aider and Claude Code idempotently", () => {
57
+ const { home, o } = scenario();
58
+ const r1 = joinTeam(o);
59
+ expect(existsSync(r1.teamFile)).toBe(true);
60
+ expect(JSON.parse(readFileSync(r1.teamFile, "utf8"))).toMatchObject({ url: "https://team.example", key: "amrt_key", userId: "u_ada", name: "Ada" });
61
+ const ompCfg = readFileSync(join(home, ".omp", "agent", "config.yml"), "utf8");
62
+ expect(ompCfg).toContain("extensions:\n - ");
63
+ expect(ompCfg).toContain("omp-extension/router-embed.ts");
64
+ expect(ompCfg).toContain("E:/other/ext.ts");
65
+ expect(ompCfg).toContain("setupVersion: 2");
66
+ expect(existsSync(join(home, ".hermes", "plugins", "model-providers", "auto-model-router", "__init__.py"))).toBe(true);
67
+ expect(existsSync(join(home, ".hermes", "plugins", "auto-model-router", "plugin.yaml"))).toBe(true);
68
+ expect(readFileSync(join(home, ".hermes", ".env"), "utf8")).toBe("OPENAI_API_KEY=x\nAUTO_MODEL_ROUTER_URL=https://team.example\nAUTO_MODEL_ROUTER_API_KEY=amrt_key\n");
69
+ expect(readFileSync(join(home, ".codex", "config.toml"), "utf8")).toContain("[model_providers.auto-model-router]");
70
+ expect(readFileSync(join(home, ".aider.conf.yml"), "utf8")).toContain("openai-api-base: https://team.example/v1");
71
+ expect(r1.configured.join("\n")).toMatch(/omp[\s\S]*Hermes[\s\S]*Codex[\s\S]*Aider[\s\S]*Claude Code/);
72
+ expect(r1.envLines).toEqual(["AUTO_MODEL_ROUTER_URL=https://team.example", "AUTO_MODEL_ROUTER_API_KEY=amrt_key", "ANTHROPIC_BASE_URL=https://team.example", "ANTHROPIC_API_KEY=amrt_key"]);
73
+ // Running again changes nothing.
74
+ const snapshot = [ompCfg, readFileSync(join(home, ".codex", "config.toml"), "utf8"), readFileSync(join(home, ".aider.conf.yml"), "utf8")];
75
+ joinTeam(o);
76
+ expect([readFileSync(join(home, ".omp", "agent", "config.yml"), "utf8"), readFileSync(join(home, ".codex", "config.toml"), "utf8"), readFileSync(join(home, ".aider.conf.yml"), "utf8")]).toEqual(snapshot);
77
+ rmSync(home, { recursive: true, force: true });
78
+ });
79
+
80
+ test("--harness restricts, --dry-run writes nothing, --profile appends once to the shell rc", () => {
81
+ const { home, o } = scenario({ only: ["omp"], dryRun: true });
82
+ const r = joinTeam(o);
83
+ expect(existsSync(r.teamFile)).toBe(false);
84
+ expect(r.configured.some((c) => c.startsWith("omp"))).toBe(true);
85
+ expect(r.skipped.some((s) => s.startsWith("Hermes"))).toBe(true);
86
+ expect(existsSync(join(home, ".hermes", "plugins"))).toBe(false);
87
+ const { home: h2, o: o2 } = scenario({ profile: true, env: { SHELL: "/bin/zsh" } });
88
+ o2.env = { ...o.env, HOME: h2, HERMES_HOME: join(h2, ".hermes"), PI_CODING_AGENT_DIR: join(h2, ".omp", "agent"), AUTO_MODEL_ROUTER_HOME: join(h2, ".auto-model-router"), SHELL: "/bin/zsh" };
89
+ joinTeam(o2);
90
+ joinTeam(o2);
91
+ const rc = readFileSync(join(h2, ".zshrc"), "utf8");
92
+ expect(rc.split("# auto-model-router team").length).toBe(2);
93
+ expect(rc).toContain("export ANTHROPIC_BASE_URL=https://team.example");
94
+ rmSync(home, { recursive: true, force: true });
95
+ rmSync(h2, { recursive: true, force: true });
96
+ });
97
+ });
package/test/turn.test.ts CHANGED
@@ -77,6 +77,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
77
77
  compaction: { enabled: false, budgetTokens: 40_000, floorRatio: 1, fitToWindow: true, protectRecentTurns: 4, maxToolResultBytes: 4_096, keepHeadBytes: 512, keepTailBytes: 512, elideSupersededReads: true, collapseDuplicateResults: true, replanGrowthRatio: 1, digestToolResults: false, digestMaxPerTurn: 2 },
78
78
  budget: { onExceeded: "downgrade" },
79
79
  report: { baselines: [], dailySummary: false },
80
+ anthropic: { models: { "*haiku*": "auto-cheap", "claude-*": "auto" } },
80
81
  harnessSwitch: { enabled: false, models: {}, minConfidence: 0.6 },
81
82
  digest: { enabled: false, minBytes: 12_000, maxBytes: 400_000, tools: ["read"], fromTier: "moderate", tier: "simple", model: "", maxOutputTokens: 700, maxCostUsd: 0.02, timeoutMs: 25_000, toolAliases: {} },
82
83
  profiles: [],