| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; |
| 3 | |
| 4 | import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts"; |
| 5 | import { resolveProviderCatalog, type ModelManifestData } from "./ModelManifest.ts"; |
| 6 | import { |
| 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 | |
| 26 | const 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 | |
| 74 | const CLAUDE = ProviderDriverKind.make("claudeAgent"); |
| 75 | |
| 76 | /** Resolves Claude's catalog the way the driver does: through its manifest catalog entry. */ |
| 77 | const catalogFromManifest = (data: ModelManifestData) => |
| 78 | resolveClaudeModelCatalog(resolveProviderCatalog(data, CLAUDE) ?? undefined); |
| 79 | |
| 80 | describe("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 | }); |