main
  1import { describe, it } from "node:test";
  2import assert from "node:assert/strict";
  3import { resolveSubagentModel } from "./model-routing.ts";
  4
  5const catalog = [
  6	{ provider: "github-copilot", modelId: "claude-opus-5" },
  7	{ provider: "github-copilot", modelId: "claude-opus-4-8" },
  8	{ provider: "github-copilot", modelId: "claude-opus-4-7" },
  9	{ provider: "anthropic-vertex", modelId: "claude-opus-5" },
 10	{ provider: "anthropic-vertex", modelId: "claude-opus-4-8" },
 11	{ provider: "anthropic-vertex", modelId: "claude-opus-4-7" },
 12	{ provider: "google", modelId: "claude-opus-4-8" },
 13];
 14const ready = new Set(catalog.map(({ provider, modelId }) => `${provider}\u0000${modelId}`));
 15
 16const baseOptions = {
 17	parentProvider: "github-copilot",
 18	providerPreference: ["anthropic-vertex", "google"],
 19	requestedModel: "claude-opus-5",
 20	modelFallbacks: { "claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7"] },
 21	providerModelMasks: {},
 22	catalog,
 23	ready,
 24};
 25
 26function resolve(overrides: Partial<typeof baseOptions> = {}) {
 27	return resolveSubagentModel({ ...baseOptions, ...overrides });
 28}
 29
 30describe("resolveSubagentModel", () => {
 31	it("prefers the parent provider for the requested model", () => {
 32		assert.deepEqual(resolve(), {
 33			selected: { provider: "github-copilot", modelId: "claude-opus-5" },
 34			rejected: [],
 35		});
 36	});
 37
 38	it("skips a masked parent provider/model pair before selection", () => {
 39		assert.deepEqual(
 40			resolve({
 41				providerModelMasks: { "github-copilot": ["claude-opus-5"] },
 42			}),
 43			{
 44				selected: { provider: "github-copilot", modelId: "claude-opus-4-8" },
 45				rejected: [{ provider: "github-copilot", modelId: "claude-opus-5", reason: "masked" }],
 46			},
 47		);
 48	});
 49
 50	it("uses an allowed same-tier fallback on the parent provider", () => {
 51		const result = resolve({
 52			providerModelMasks: { "github-copilot": ["claude-opus-5"] },
 53		});
 54		assert.deepEqual(result.selected, { provider: "github-copilot", modelId: "claude-opus-4-8" });
 55	});
 56
 57	it("uses the configured Vertex mask without masking the same Copilot model", () => {
 58		const result = resolve({
 59			parentProvider: "anthropic-vertex",
 60			providerModelMasks: { "anthropic-vertex": ["claude-opus-5", "claude-opus-4-8"] },
 61		});
 62		assert.deepEqual(result.selected, { provider: "anthropic-vertex", modelId: "claude-opus-4-7" });
 63		assert.deepEqual(result.rejected, [
 64			{ provider: "anthropic-vertex", modelId: "claude-opus-5", reason: "masked" },
 65			{ provider: "anthropic-vertex", modelId: "claude-opus-4-8", reason: "masked" },
 66		]);
 67	});
 68
 69	it("uses only a pinned provider instead of the parent provider", () => {
 70		const result = resolve({ pinnedProvider: "google" });
 71		assert.deepEqual(result.selected, { provider: "google", modelId: "claude-opus-4-8" });
 72	});
 73
 74	it("does not fall back to another provider for a pinned provider", () => {
 75		const result = resolve({
 76			pinnedProvider: "google",
 77			catalog: [{ provider: "github-copilot", modelId: "claude-opus-5" }],
 78			ready: new Set(["github-copilot\u0000claude-opus-5"]),
 79		});
 80		assert.equal(result.selected, null);
 81		assert.ok(result.rejected.every((entry) => entry.provider === "google"));
 82	});
 83
 84	it("moves to configured providers after parent candidates are unavailable", () => {
 85		const result = resolve({
 86			catalog: [{ provider: "anthropic-vertex", modelId: "claude-opus-4-8" }],
 87			ready: new Set(["anthropic-vertex\u0000claude-opus-4-8"]),
 88		});
 89		assert.deepEqual(result.selected, { provider: "anthropic-vertex", modelId: "claude-opus-4-8" });
 90	});
 91
 92	it("does not invent a cross-tier fallback", () => {
 93		const result = resolve({
 94			catalog: [{ provider: "github-copilot", modelId: "claude-sonnet-5" }],
 95			ready: new Set(["github-copilot\u0000claude-sonnet-5"]),
 96		});
 97		assert.equal(result.selected, null);
 98		assert.ok(result.rejected.every((entry) => entry.modelId.startsWith("claude-opus-")));
 99	});
100
101	it("reports catalog and credential rejection reasons", () => {
102		const result = resolve({
103			catalog: [{ provider: "github-copilot", modelId: "claude-opus-5" }],
104			ready: new Set(),
105		});
106		assert.deepEqual(result.selected, null);
107		assert.ok(result.rejected.some((entry) => entry.reason === "missing-credentials"));
108		assert.ok(result.rejected.some((entry) => entry.reason === "not-in-catalog"));
109	});
110
111	it("retains static preference behavior when no parent provider or new settings exist", () => {
112		const result = resolveSubagentModel({
113			parentProvider: undefined,
114			providerPreference: ["anthropic-vertex", "github-copilot"],
115			requestedModel: "claude-opus-5",
116			modelFallbacks: {},
117			providerModelMasks: {},
118			catalog,
119			ready,
120		});
121		assert.deepEqual(result.selected, { provider: "anthropic-vertex", modelId: "claude-opus-5" });
122	});
123
124	it("selects a dated catalog model for an undated requested ID", () => {
125		const result = resolveSubagentModel({
126			parentProvider: "anthropic-vertex",
127			providerPreference: [],
128			requestedModel: "claude-haiku-4-5",
129			modelFallbacks: {},
130			providerModelMasks: {},
131			catalog: [{ provider: "anthropic-vertex", modelId: "claude-haiku-4-5@20251001" }],
132			ready: new Set(["anthropic-vertex\u0000claude-haiku-4-5@20251001"]),
133		});
134		assert.deepEqual(result.selected, {
135			provider: "anthropic-vertex",
136			modelId: "claude-haiku-4-5@20251001",
137		});
138	});
139});