g1t/services/runner/src/model-env.test.ts

181 lines8,739 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Members can read a private repository's pull request forks1import assert from "node:assert/strict";
2import { createServer } from "node:http";
3import { test } from "node:test";
4
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier5import {
6 type AgentRouting,
7 type ChangeSize,
8 DEFAULT_ROUTING,
9 canReachModel,
10 changeSize,
11 chooseTier,
12 lastAttemptFailed,
13 modelEnv,
14 parseRouting,
15} from "./model-env.ts";
Members can read a private repository's pull request forks16
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier17const routes: AgentRouting = {
18 ...DEFAULT_ROUTING,
19 tiers: {
20 small: { modelName: "Claude Haiku 4.5", model: "claude-haiku-4-5-20251001" },
21 large: { modelName: "Claude Sonnet 5.5", model: "claude-sonnet-5-5" },
22 },
Members can read a private repository's pull request forks23};
Agents as a team: lifecycle, merge queue, billing and a new shell24const tags = { repo: "acme/site", pull: 12 };
Members can read a private repository's pull request forks25const direct = { ANTHROPIC_API_KEY: "sk-test", AI_GATEWAY_ID: "", CLOUDFLARE_ACCOUNT_ID: "acct" };
26
Agents as a team: lifecycle, merge queue, billing and a new shell27/** `ANTHROPIC_CUSTOM_HEADERS` as the harness reads it: one header per line. */
28function customHeaders(vars: Record<string, string>): Record<string, string> {
29 return Object.fromEntries(
30 vars.ANTHROPIC_CUSTOM_HEADERS.split("\n").map((line) => {
31 const at = line.indexOf(": ");
32 return [line.slice(0, at), line.slice(at + 2)];
33 }),
34 );
35}
36
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier37const small: ChangeSize = { files: 3, lines: 80, sensitive: [] };
38
39test("planning and catching up run on the small tier, making a change on the large", () => {
40 assert.equal(chooseTier("plan", {}, routes), "small");
41 assert.equal(chooseTier("update", {}, routes), "small");
42 assert.equal(chooseTier("implement", {}, routes), "large");
43 assert.equal(chooseTier("implement", { change: small }, routes), "large");
44});
45
46test("a review is small only for a small change that touches nothing sensitive", () => {
47 assert.equal(chooseTier("review", { change: small }, routes), "small");
48 assert.equal(chooseTier("review", { change: { ...small, lines: 200, files: 10 } }, routes), "small");
49 assert.equal(chooseTier("review", { change: { ...small, lines: 201 } }, routes), "large");
50 assert.equal(chooseTier("review", { change: { ...small, files: 11 } }, routes), "large");
51 assert.equal(chooseTier("review", { change: { ...small, sensitive: ["CI workflows"] } }, routes), "large");
52 assert.equal(chooseTier("review", { change: small, labels: ["Security"] }, routes), "large");
53 assert.equal(chooseTier("review", { change: small, labels: ["docs"] }, routes), "small");
54});
55
56test("a review of a change g1t cannot size runs on the large tier", () => {
57 assert.equal(chooseTier("review", {}, routes), "large");
58 assert.equal(chooseTier("review", { change: null }, routes), "large");
59 assert.equal(chooseTier("review", { change: { files: 0, lines: 0, sensitive: [] } }, routes), "large");
60});
61
62test("a retry after a failed attempt goes up to the large tier", () => {
63 assert.equal(chooseTier("plan", { retry: true }, routes), "large");
64 assert.equal(chooseTier("update", { retry: true }, routes), "large");
65 assert.equal(chooseTier("review", { change: small, retry: true }, routes), "large");
66});
67
68test("a retry is the same work again after its latest attempt failed", () => {
69 assert.equal(lastAttemptFailed([]), false);
70 assert.equal(lastAttemptFailed([{ status: "failed" }, { status: "succeeded" }]), true);
71 assert.equal(lastAttemptFailed([{ status: "succeeded" }, { status: "failed" }]), false);
72 assert.equal(lastAttemptFailed([{ status: "stopped", halted: "budget" }]), true);
73 assert.equal(lastAttemptFailed([{ status: "stopped", halted: null }]), false);
74 assert.equal(lastAttemptFailed([{ status: "running" }]), false);
75 assert.equal(lastAttemptFailed([{ status: "failed", title: "Add search" }], "Add search "), true);
76 assert.equal(lastAttemptFailed([{ status: "failed", title: "Add search" }], "Add billing"), false);
77});
78
79test("the configuration decides the tiers, the rules and the limits", () => {
80 const parsed = parseRouting(
81 JSON.stringify({
82 tiers: { small: { modelName: "Small", model: "small-1" } },
83 tasks: { update: "large" },
84 smallChange: { lines: 50 },
85 }),
86 );
87 assert.deepEqual(parsed.tiers.small, { modelName: "Small", model: "small-1" });
88 assert.deepEqual(parsed.tiers.large, DEFAULT_ROUTING.tiers.large);
89 assert.equal(chooseTier("update", {}, parsed), "large");
90 assert.equal(chooseTier("plan", {}, parsed), "small");
91 assert.equal(chooseTier("review", { change: { ...small, lines: 51 } }, parsed), "large");
92 assert.equal(chooseTier("review", { change: { ...small, files: 10, lines: 50 } }, parsed), "small");
93 assert.deepEqual(parseRouting(undefined), DEFAULT_ROUTING);
94 assert.deepEqual(parseRouting("not json"), DEFAULT_ROUTING);
Agents as a team: lifecycle, merge queue, billing and a new shell95});
96
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier97test("a change's size is its files and the lines added and removed", () => {
98 assert.deepEqual(
99 changeSize([{ additions: 10, deletions: 2 }, { additions: 0, deletions: 5 }], ["secrets"]),
100 { files: 2, lines: 17, sensitive: ["secrets"] },
101 );
102});
103
104test("the tier decides the model, and the harness's small tasks use the small tier", () => {
105 const large = modelEnv(direct, routes, "implement", "large", tags);
106 assert.equal(large.ANTHROPIC_MODEL, "claude-sonnet-5-5");
107 assert.equal(large.AGENT_MODEL_NAME, "Claude Sonnet 5.5");
108 assert.equal(large.ANTHROPIC_SMALL_FAST_MODEL, "claude-haiku-4-5-20251001");
109 assert.equal(large.ANTHROPIC_DEFAULT_HAIKU_MODEL, "claude-haiku-4-5-20251001");
110 const review = modelEnv(direct, routes, "review", "small", tags);
111 assert.equal(review.ANTHROPIC_MODEL, "claude-haiku-4-5-20251001");
112 assert.equal(review.AGENT_MODEL_NAME, "Claude Haiku 4.5");
113});
114
Members can read a private repository's pull request forks115test("without a gateway, requests go to the provider directly", () => {
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier116 const vars = modelEnv(direct, routes, "implement", "large", tags);
Members can read a private repository's pull request forks117 assert.equal(vars.ANTHROPIC_BASE_URL, undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell118 assert.equal(vars.ANTHROPIC_CUSTOM_HEADERS, undefined);
119 assert.equal(vars.ANTHROPIC_API_KEY, "sk-test");
Members can read a private repository's pull request forks120});
121
Agents as a team: lifecycle, merge queue, billing and a new shell122test("with a gateway, requests go through it and say what they are for", () => {
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier123 const vars = modelEnv({ ...direct, AI_GATEWAY_ID: "g1t" }, routes, "review", "large", tags);
Members can read a private repository's pull request forks124 assert.equal(vars.ANTHROPIC_BASE_URL, "https://gateway.ai.cloudflare.com/v1/acct/g1t/anthropic");
Agents as a team: lifecycle, merge queue, billing and a new shell125 assert.deepEqual(customHeaders(vars), {
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier126 "cf-aig-metadata": '{"task":"review","tier":"large","repo":"acme/site","pull":12}',
Agents as a team: lifecycle, merge queue, billing and a new shell127 });
Members can read a private repository's pull request forks128});
129
130test("an authenticated gateway is sent its token", () => {
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier131 const vars = modelEnv({ ...direct, AI_GATEWAY_ID: "g1t", AI_GATEWAY_TOKEN: "tok" }, routes, "implement", "large", tags);
Agents as a team: lifecycle, merge queue, billing and a new shell132 assert.equal(customHeaders(vars)["cf-aig-authorization"], "Bearer tok");
133 assert.equal(vars.ANTHROPIC_API_KEY, "sk-test");
Members can read a private repository's pull request forks134});
135
Agents as a team: lifecycle, merge queue, billing and a new shell136test("when the gateway holds the provider's key, the sandbox never gets it", () => {
137 const gatewayOnly = { AI_GATEWAY_ID: "g1t", AI_GATEWAY_TOKEN: "tok", CLOUDFLARE_ACCOUNT_ID: "acct" };
138 assert.equal(canReachModel(gatewayOnly), true);
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier139 assert.equal(modelEnv(gatewayOnly, routes, "implement", "large", tags).ANTHROPIC_API_KEY, "tok");
Agents as a team: lifecycle, merge queue, billing and a new shell140 assert.equal(canReachModel({ AI_GATEWAY_ID: "g1t", CLOUDFLARE_ACCOUNT_ID: "acct" }), false);
141 assert.equal(canReachModel(direct), true);
142});
143
Members can read a private repository's pull request forks144// A stand-in for the gateway: what a sandbox's agent sends, given these
145// variables, arrives at the gateway's path with both credentials.
146test("a request built from these variables reaches a gateway as expected", async () => {
Agents as a team: lifecycle, merge queue, billing and a new shell147 const seen: { url?: string; key?: string; gateway?: string; metadata?: string; model?: string } = {};
Members can read a private repository's pull request forks148 const server = createServer((request, response) => {
149 let body = "";
150 request.on("data", (chunk) => (body += chunk));
151 request.on("end", () => {
152 seen.url = request.url;
153 seen.key = request.headers["x-api-key"] as string;
154 seen.gateway = request.headers["cf-aig-authorization"] as string;
Agents as a team: lifecycle, merge queue, billing and a new shell155 seen.metadata = request.headers["cf-aig-metadata"] as string;
Members can read a private repository's pull request forks156 seen.model = JSON.parse(body).model;
157 response.setHeader("content-type", "application/json");
158 response.end(JSON.stringify({ type: "message", content: [] }));
159 });
160 });
161 await new Promise<void>((resolve) => server.listen(0, resolve));
162 const { port } = server.address() as { port: number };
163
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier164 const vars = modelEnv({ ...direct, AI_GATEWAY_ID: "g1t", AI_GATEWAY_TOKEN: "tok" }, routes, "implement", "large", tags);
Members can read a private repository's pull request forks165 // Same path as the real gateway, on the stand-in's address.
166 const base = vars.ANTHROPIC_BASE_URL.replace("https://gateway.ai.cloudflare.com", `http://localhost:${port}`);
167 await fetch(`${base}/v1/messages`, {
168 method: "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell169 headers: { "x-api-key": vars.ANTHROPIC_API_KEY, ...customHeaders(vars), "content-type": "application/json" },
Members can read a private repository's pull request forks170 body: JSON.stringify({ model: vars.ANTHROPIC_MODEL, max_tokens: 1, messages: [] }),
171 });
172 server.close();
173
174 assert.deepEqual(seen, {
175 url: "/v1/acct/g1t/anthropic/v1/messages",
176 key: "sk-test",
177 gateway: "Bearer tok",
Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier178 metadata: '{"task":"implement","tier":"large","repo":"acme/site","pull":12}',
Members can read a private repository's pull request forks179 model: "claude-sonnet-5-5",
180 });
181});