1import { assert, describe, it } from "@effect/vitest";
2import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
3
4import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts";
5import { resolveProviderCatalog, type ModelManifestData } from "./ModelManifest.ts";
6import {
7 formatClaudeVersionUpgradeMessage,
8 normalizeClaudeCatalogEffort,
9 resolveClaudeCatalogApiModelId,
10 resolveClaudeCatalogContextWindowTokens,
11 resolveClaudeCatalogEffort,
12 resolveClaudeModelCatalog,
13 resolveClaudeModelsForVersion,
14 resolveClaudeModelSlug,
15 resolveClaudeUpdateRequiredModels,
16 scopeClaudeModelCatalog,
17} from "./ClaudeModelCatalog.ts";
18
19/**
20 * Test policy: adding or changing a real Claude model in model-manifest.json
21 * must not add or update tests here. These synthetic fixtures cover resolver
22 * behavior once. Add a test only when Claude adapter semantics change, such
23 * as introducing a new compatibility rule or dispatch mapping type.
24 */
25
26const manifest = (): ModelManifestData => ({
27 version: 1,
28 currentModels: {},
29 providers: {
30 claudeAgent: {
31 profiles: {
32 synthetic: {
33 capabilities: {
34 optionDescriptors: [
35 {
36 id: "effort",
37 label: "Reasoning",
38 type: "select",
39 options: [{ id: "extreme", label: "Extreme", isDefault: true }],
40 },
41 {
42 id: "contextWindow",
43 label: "Context Window",
44 type: "select",
45 options: [
46 { id: "large", label: "Large", isDefault: true },
47 { id: "small", label: "Small" },
48 ],
49 },
50 ],
51 },
52 adapter: {
53 claudeCode: {
54 effortMap: { extreme: "high" },
55 modelSuffixes: { contextWindow: { large: "[large]" } },
56 },
57 },
58 },
59 },
60 models: [
61 {
62 slug: "claude-synthetic-next",
63 name: "Claude Synthetic Next",
64 aliases: ["synthetic"],
65 status: "current",
66 profile: "synthetic",
67 adapter: { claudeCode: { minVersion: "3.2.0" } },
68 },
69 ],
70 },
71 },
72});
73
74const CLAUDE = ProviderDriverKind.make("claudeAgent");
75
76/** Resolves Claude's catalog the way the driver does: through its manifest catalog entry. */
77const catalogFromManifest = (data: ModelManifestData) =>
78 resolveClaudeModelCatalog(resolveProviderCatalog(data, CLAUDE) ?? undefined);
79
80describe("Claude model catalog", () => {
81 it("resolves capacity from selected options and fixed catalog windows without guessing custom models", () => {
82 const source = manifest();
83 const profile = source.providers!.claudeAgent!.profiles.synthetic!;
84 const catalog = catalogFromManifest({
85 ...source,
86 providers: {
87 claudeAgent: {
88 ...source.providers!.claudeAgent!,
89 profiles: {
90 fixed: {
91 capabilities: { optionDescriptors: [] },
92 adapter: { claudeCode: { fixedContextWindowTokens: 64_000 } },
93 },
94 synthetic: {
95 ...profile,
96 adapter: { claudeCode: { contextWindowTokens: { large: 1_000_000, small: 32_000 } } },
97 },
98 },
99 models: [
100 ...source.providers!.claudeAgent!.models,
101 {
102 slug: "fixed",
103 name: "Fixed",
104 status: "current",
105 profile: "fixed",
106 },
107 ],
108 },
109 },
110 });
111 const selection = { instanceId: ProviderInstanceId.make("claudeAgent"), model: "synthetic" };
112 assert.equal(resolveClaudeCatalogContextWindowTokens(catalog, selection), 1_000_000);
113 assert.equal(
114 resolveClaudeCatalogContextWindowTokens(catalog, {
115 ...selection,
116 options: [{ id: "contextWindow", value: "small" }],
117 }),
118 32_000,
119 );
120 assert.equal(
121 resolveClaudeCatalogContextWindowTokens(catalog, { ...selection, model: "fixed" }),
122 64_000,
123 );
124 assert.isUndefined(
125 resolveClaudeCatalogContextWindowTokens(catalog, { ...selection, model: "custom" }),
126 );
127 });
128
129 it("filters models at runtime-version boundaries and derives the upgrade message", () => {
130 const catalog = catalogFromManifest(manifest());
131 assert.deepStrictEqual(resolveClaudeModelsForVersion(catalog, "3.1.9"), []);
132 assert.deepStrictEqual(
133 resolveClaudeModelsForVersion(catalog, "3.2.0").map((model) => model.slug),
134 ["claude-synthetic-next"],
135 );
136 assert.deepStrictEqual(resolveClaudeUpdateRequiredModels(catalog, "3.1.9"), [
137 { slug: "claude-synthetic-next", name: "Claude Synthetic Next", minVersion: "3.2.0" },
138 ]);
139 assert.deepStrictEqual(resolveClaudeUpdateRequiredModels(catalog, "3.2.0"), []);
140 assert.strictEqual(
141 formatClaudeVersionUpgradeMessage(catalog, "3.1.9"),
142 "Claude Code v3.1.9 is too old for Claude Synthetic Next. Upgrade to v3.2.0 or newer to access it.",
143 );
144 });
145
146 it("resolves aliases and declarative adapter mappings", () => {
147 const base = manifest();
148 const input: ModelManifestData = {
149 ...base,
150 providers: {
151 ...base.providers,
152 claudeAgent: {
153 ...base.providers!.claudeAgent!,
154 models: [
155 {
156 slug: "claude-synthetic-collision",
157 name: "Claude Synthetic Collision",
158 aliases: ["claude-synthetic-next"],
159 status: "current",
160 },
161 ...base.providers!.claudeAgent!.models,
162 ],
163 },
164 },
165 };
166 const catalog = catalogFromManifest(input);
167 assert.strictEqual(resolveClaudeModelSlug(catalog, "synthetic"), "claude-synthetic-next");
168 assert.strictEqual(
169 resolveClaudeModelSlug(catalog, "claude-synthetic-next"),
170 "claude-synthetic-next",
171 );
172 assert.strictEqual(normalizeClaudeCatalogEffort(catalog, "extreme", "synthetic"), "high");
173 assert.strictEqual(
174 resolveClaudeCatalogApiModelId(catalog, {
175 instanceId: ProviderInstanceId.make("claudeAgent"),
176 model: "synthetic",
177 }),
178 "claude-synthetic-next[large]",
179 );
180 });
181
182 it("rejects malformed adapter mappings", () => {
183 const base = manifest();
184 const malformed: ModelManifestData = {
185 ...base,
186 providers: {
187 ...base.providers,
188 claudeAgent: {
189 ...base.providers!.claudeAgent!,
190 profiles: {
191 ...base.providers!.claudeAgent!.profiles,
192 synthetic: {
193 ...base.providers!.claudeAgent!.profiles.synthetic!,
194 adapter: { claudeCode: { effortMap: { extreme: 123 } } },
195 },
196 },
197 },
198 },
199 };
200 assert.isFalse(hasValidClaudeManifestAdapters(malformed));
201 });
202
203 it("appends custom models with their own descriptors and keeps bare slugs opaque", () => {
204 const catalog = scopeClaudeModelCatalog(catalogFromManifest(manifest()), [
205 "synthetic",
206 {
207 slug: "claude-custom-tuned",
208 name: "Tuned",
209 capabilities: {
210 optionDescriptors: [
211 {
212 id: "effort",
213 label: "Reasoning",
214 type: "select",
215 options: [
216 { id: "gentle", label: "Gentle", isDefault: true },
217 { id: "brutal", label: "Brutal" },
218 ],
219 },
220 ],
221 },
222 },
223 ]);
224
225 // The bare custom slug shadows the built-in alias, so it no longer resolves to it.
226 assert.strictEqual(resolveClaudeModelSlug(catalog, "synthetic"), "synthetic");
227 assert.strictEqual(resolveClaudeCatalogEffort(catalog, "synthetic", "extreme"), undefined);
228
229 // The entry with descriptors resolves user-defined effort ids and passes
230 // them through untouched (no effortMap, no model suffix).
231 assert.strictEqual(
232 resolveClaudeCatalogEffort(catalog, "claude-custom-tuned", "brutal"),
233 "brutal",
234 );
235 assert.strictEqual(
236 resolveClaudeCatalogEffort(catalog, "claude-custom-tuned", "bogus"),
237 "gentle",
238 );
239 assert.strictEqual(
240 normalizeClaudeCatalogEffort(catalog, "brutal", "claude-custom-tuned"),
241 "brutal",
242 );
243 assert.strictEqual(
244 resolveClaudeCatalogApiModelId(catalog, {
245 instanceId: ProviderInstanceId.make("claudeAgent"),
246 model: "claude-custom-tuned",
247 options: [{ id: "effort", value: "brutal" }],
248 }),
249 "claude-custom-tuned",
250 );
251 assert.deepStrictEqual(
252 resolveClaudeModelsForVersion(catalog, "3.2.0").map((model) => model.slug),
253 ["claude-synthetic-next", "claude-custom-tuned"],
254 );
255 });
256});