1import { describe, expect, it } from "vite-plus/test";
2import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
3
4import {
5 applyClaudePromptEffortPrefix,
6 buildExplicitProviderOptionSelectionsFromDescriptors,
7 buildProviderOptionSelectionsFromDescriptors,
8 createModelCapabilities,
9 createModelSelection,
10 formatCodexModelName,
11 formatModelSlugName,
12 getProviderOptionCurrentLabel,
13 getModelSelectionBooleanOptionValue,
14 getModelSelectionStringOptionValue,
15 getProviderOptionDescriptors,
16 readCustomModelEntries,
17 toCustomModelSetting,
18 getProviderOptionBooleanSelectionValue,
19 getProviderOptionStringSelectionValue,
20 normalizeCustomModelSlug,
21 normalizeModelSlug,
22 modelSelectionsEqual,
23} from "./model.ts";
24
25it("keeps the Codex catalog display formatting", () => {
26 expect(formatCodexModelName("gpt-5.3-codex-spark")).toBe("GPT-5.3-Codex-Spark");
27 expect(formatCodexModelName("GPT Test")).toBe("GPT Test");
28});
29
30it.each([
31 ["gpt-5.4", "GPT-5.4"],
32 ["claude-opus-4-6", "Claude Opus 4.6"],
33 ["claude-sonnet-4-20250514", "Claude Sonnet 4 20250514"],
34 ["claude-opus-4-6[1m]", "Claude Opus 4.6[1m]"],
35 ["openai/gpt-5.4-mini", "openai/GPT-5.4-Mini"],
36 ["gemini-2.5-pro-preview-06-05", "Gemini 2.5 Pro Preview 06 05"],
37 ["custom/model-v2", "custom/model-v2"],
38 ["gpt-proxy", "gpt-proxy"],
39 ["My Custom Model", "My Custom Model"],
40])("formats a known model ID without losing its qualifiers: %s", (slug, expected) => {
41 expect(formatModelSlugName(slug)).toBe(expected);
42});
43
44const codexCaps: ModelCapabilities = createModelCapabilities({
45 optionDescriptors: [
46 {
47 id: "reasoningEffort",
48 label: "Reasoning",
49 type: "select",
50 options: [
51 { id: "xhigh", label: "Extra High" },
52 { id: "high", label: "High", isDefault: true },
53 ],
54 currentValue: "high",
55 },
56 {
57 id: "fastMode",
58 label: "Fast Mode",
59 type: "boolean",
60 },
61 ],
62});
63
64describe("model slug normalization", () => {
65 it("preserves exact custom slugs instead of expanding provider aliases", () => {
66 // Claude aliases now resolve through the model catalog (#9084), so the
67 // provider alias table passes unknown slugs through unchanged.
68 const claude = ProviderDriverKind.make("claudeAgent");
69
70 expect(normalizeModelSlug("opus", claude)).toBe("opus");
71 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
72 });
73});
74
75const claudeCaps: ModelCapabilities = createModelCapabilities({
76 optionDescriptors: [
77 {
78 id: "effort",
79 label: "Reasoning",
80 type: "select",
81 options: [
82 { id: "medium", label: "Medium" },
83 { id: "high", label: "High", isDefault: true },
84 { id: "ultrathink", label: "Ultrathink" },
85 ],
86 currentValue: "high",
87 promptInjectedValues: ["ultrathink"],
88 },
89 {
90 id: "contextWindow",
91 label: "Context Window",
92 type: "select",
93 options: [
94 { id: "200k", label: "200k" },
95 { id: "1m", label: "1M", isDefault: true },
96 ],
97 currentValue: "1m",
98 },
99 ],
100});
101
102describe("descriptor helpers", () => {
103 it("applies selection values to capability descriptors", () => {
104 expect(
105 getProviderOptionDescriptors({
106 caps: claudeCaps,
107 selections: [
108 { id: "effort", value: "medium" },
109 { id: "contextWindow", value: "200k" },
110 ],
111 }),
112 ).toEqual([
113 {
114 id: "effort",
115 label: "Reasoning",
116 type: "select",
117 options: [
118 { id: "medium", label: "Medium" },
119 { id: "high", label: "High", isDefault: true },
120 { id: "ultrathink", label: "Ultrathink" },
121 ],
122 currentValue: "medium",
123 promptInjectedValues: ["ultrathink"],
124 },
125 {
126 id: "contextWindow",
127 label: "Context Window",
128 type: "select",
129 options: [
130 { id: "200k", label: "200k" },
131 { id: "1m", label: "1M", isDefault: true },
132 ],
133 currentValue: "200k",
134 },
135 ]);
136 });
137
138 it("builds wire-format option selections from descriptors", () => {
139 const descriptors = getProviderOptionDescriptors({
140 caps: codexCaps,
141 selections: [
142 { id: "reasoningEffort", value: "high" },
143 { id: "fastMode", value: true },
144 ],
145 });
146
147 expect(buildProviderOptionSelectionsFromDescriptors(descriptors)).toEqual([
148 { id: "reasoningEffort", value: "high" },
149 { id: "fastMode", value: true },
150 ]);
151 });
152
153 it("builds dispatch options only from explicit selections", () => {
154 const descriptors = getProviderOptionDescriptors({
155 caps: codexCaps,
156 selections: [{ id: "fastMode", value: true }],
157 });
158
159 expect(buildExplicitProviderOptionSelectionsFromDescriptors(descriptors, undefined)).toBe(
160 undefined,
161 );
162 expect(
163 buildExplicitProviderOptionSelectionsFromDescriptors(descriptors, [
164 { id: "fastMode", value: true },
165 ]),
166 ).toEqual([{ id: "fastMode", value: true }]);
167 });
168
169 it("stores option selection arrays in model selections", () => {
170 expect(
171 createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [
172 { id: "reasoningEffort", value: "high" },
173 { id: "fastMode", value: true },
174 ]),
175 ).toEqual({
176 instanceId: "codex",
177 model: "gpt-5.4",
178 options: [
179 { id: "reasoningEffort", value: "high" },
180 { id: "fastMode", value: true },
181 ],
182 });
183 });
184
185 it("reads typed option selection values", () => {
186 const selection = createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [
187 { id: "reasoningEffort", value: "high" },
188 { id: "fastMode", value: true },
189 ]);
190
191 expect(getProviderOptionStringSelectionValue(selection.options, "reasoningEffort")).toBe(
192 "high",
193 );
194 expect(getProviderOptionStringSelectionValue(selection.options, "fastMode")).toBeUndefined();
195 expect(getProviderOptionBooleanSelectionValue(selection.options, "fastMode")).toBe(true);
196 expect(
197 getProviderOptionBooleanSelectionValue(selection.options, "reasoningEffort"),
198 ).toBeUndefined();
199 expect(getModelSelectionStringOptionValue(selection, "reasoningEffort")).toBe("high");
200 expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true);
201 });
202
203 it("compares complete model selections independent of option ordering", () => {
204 const left = createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [
205 { id: "reasoningEffort", value: "high" },
206 { id: "fastMode", value: true },
207 ]);
208 const reordered = createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [
209 { id: "fastMode", value: true },
210 { id: "reasoningEffort", value: "high" },
211 ]);
212
213 expect(modelSelectionsEqual(left, reordered)).toBe(true);
214 expect(
215 modelSelectionsEqual(left, {
216 ...reordered,
217 options: [
218 { id: "fastMode", value: true },
219 { id: "reasoningEffort", value: "medium" },
220 ],
221 }),
222 ).toBe(false);
223 expect(modelSelectionsEqual(left, { ...reordered, model: "gpt-5.5" })).toBe(false);
224 });
225});
226
227describe("applyClaudePromptEffortPrefix", () => {
228 it("keeps slash commands intact when ultrathink is selected", () => {
229 expect(applyClaudePromptEffortPrefix("/compact", "ultrathink")).toBe("/compact");
230 expect(applyClaudePromptEffortPrefix(" /compact keep recent errors ", "ultrathink")).toBe(
231 "/compact keep recent errors",
232 );
233 expect(applyClaudePromptEffortPrefix(" /review src/model.ts ", "ultrathink")).toBe(
234 "/review src/model.ts",
235 );
236 expect(applyClaudePromptEffortPrefix("/security-review", "ultrathink")).toBe(
237 "/security-review",
238 );
239 expect(applyClaudePromptEffortPrefix("/plugin:skill run", "ultrathink")).toBe(
240 "/plugin:skill run",
241 );
242 expect(applyClaudePromptEffortPrefix("/deploy.prod to staging", "ultrathink")).toBe(
243 "/deploy.prod to staging",
244 );
245 });
246
247 it("still adds the ultrathink prefix to ordinary prompts", () => {
248 expect(applyClaudePromptEffortPrefix("Investigate this failure", "ultrathink")).toBe(
249 "Ultrathink:\nInvestigate this failure",
250 );
251 expect(applyClaudePromptEffortPrefix("/home/theo/app.ts crashed on load", "ultrathink")).toBe(
252 "Ultrathink:\n/home/theo/app.ts crashed on load",
253 );
254 });
255});
256
257describe("readCustomModelEntries", () => {
258 const capabilities: ModelCapabilities = {
259 optionDescriptors: [
260 {
261 id: "effort",
262 label: "Reasoning",
263 type: "select",
264 options: [{ id: "high", label: "High", isDefault: true }],
265 currentValue: "high",
266 },
267 ],
268 };
269
270 it("resolves bare slugs and entries, trimming and deduplicating on slug", () => {
271 expect(
272 readCustomModelEntries([
273 " bare ",
274 { slug: "named", name: " Named ", capabilities },
275 "bare",
276 { slug: "named", name: "Second" },
277 "",
278 { name: "no slug" },
279 42,
280 ]),
281 ).toEqual([
282 { slug: "bare", name: "bare", capabilities: null },
283 { slug: "named", name: "Named", capabilities },
284 ]);
285 });
286
287 it("drops unparseable capabilities but keeps the entry", () => {
288 expect(
289 readCustomModelEntries([{ slug: "x", capabilities: { optionDescriptors: "nope" } }]),
290 ).toEqual([{ slug: "x", name: "x", capabilities: null }]);
291 expect(readCustomModelEntries("not a list")).toEqual([]);
292 });
293
294 it("writes the compact stored shape back", () => {
295 expect(toCustomModelSetting({ slug: "x", name: "x", capabilities: null })).toBe("x");
296 expect(
297 toCustomModelSetting({ slug: "x", name: "x", capabilities: { optionDescriptors: [] } }),
298 ).toBe("x");
299 expect(toCustomModelSetting({ slug: "x", name: "X", capabilities })).toEqual({
300 slug: "x",
301 name: "X",
302 capabilities,
303 });
304 });
305});
306
307describe("provider-reported option display", () => {
308 const selection = createModelSelection(ProviderInstanceId.make("opencode"), "ling");
309 const reported = { ...selection, options: [{ id: "variant", value: "default" }] };
310 const descriptor = {
311 id: "variant",
312 label: "Reasoning",
313 type: "select" as const,
314 options: [
315 { id: "none", label: "None" },
316 { id: "thinking", label: "Thinking" },
317 ],
318 };
319
320 it("shows explicit reports without adding a choice or a dispatch option", () => {
321 expect(getProviderOptionCurrentLabel(descriptor, selection, reported)).toBe("Default");
322 expect(
323 getProviderOptionCurrentLabel(descriptor, selection, {
324 ...reported,
325 options: [{ id: "variant", value: "thinking" }],
326 }),
327 ).toBe("Thinking");
328 expect(
329 getProviderOptionCurrentLabel(
330 { ...descriptor, currentValue: "none" },
331 { ...selection, options: [{ id: "variant", value: "none" }] },
332 reported,
333 ),
334 ).toBe("None");
335 expect(descriptor.options.map((option) => option.id)).toEqual(["none", "thinking"]);
336 expect(buildProviderOptionSelectionsFromDescriptors([descriptor])).toBeUndefined();
337 expect(getProviderOptionCurrentLabel(descriptor, selection)).toBe("Unknown");
338 const effortDescriptor = { ...descriptor, id: "effort", currentValue: "default" };
339 expect(getProviderOptionCurrentLabel(effortDescriptor, selection)).toBeUndefined();
340 expect(
341 getProviderOptionCurrentLabel(effortDescriptor, selection, {
342 ...reported,
343 model: "other",
344 options: [{ id: "effort", value: "default" }],
345 }),
346 ).toBeUndefined();
347 expect(
348 getProviderOptionCurrentLabel(effortDescriptor, selection, {
349 ...reported,
350 options: [{ id: "effort", value: "default" }],
351 }),
352 ).toBe("Default");
353 expect(
354 getProviderOptionCurrentLabel({ ...descriptor, currentValue: "thinking" }, selection),
355 ).toBe("Unknown");
356 });
357
358 it.each([
359 { ...selection, model: "other" },
360 { ...selection, instanceId: ProviderInstanceId.make("other") },
361 { ...selection, options: [{ id: "variant", value: "none" }] },
362 ])("ignores reports after changing the model, instance, or option: %j", (selected) => {
363 expect(getProviderOptionCurrentLabel(descriptor, selected, reported)).toBe("Unknown");
364 });
365});