1import {
2 ANTIGRAVITY_DEFAULT_MODEL,
3 DEFAULT_TEXT_GENERATION_MODEL,
4 DEFAULT_SERVER_SETTINGS,
5 ProjectId,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 type ServerProvider,
9} from "@t3tools/contracts";
10import {
11 DEFAULT_CLIENT_SETTINGS,
12 DEFAULT_UNIFIED_SETTINGS,
13 type UnifiedSettings,
14} from "@t3tools/contracts/settings";
15import { describe, expect, it } from "vite-plus/test";
16import { createModelSelection } from "@t3tools/shared/model";
17import { deriveEffectiveComposerModelState } from "./composerDraftStore";
18import { getComposerProviderState } from "./components/chat/composerProviderState";
19import { resolveComposerProviderSelection } from "./components/ChatView.logic";
20import {
21 deriveProviderInstanceEntries,
22 NO_PROVIDER_MODEL_SELECTION,
23 providerInstanceEntriesFor,
24} from "./providerInstances";
25import {
26 defaultModelSelectionsAreMixed,
27 getAppModelOptionsForInstance,
28 getCustomModelOptionsByInstance,
29 getTextGenerationModelOptionsByInstance,
30 resolveAppModelSelectionForInstance,
31 resolveAppModelSelectionState,
32 resolveEffectiveDefaultModelSelection,
33} from "./modelSelection";
34
35function provider(input: {
36 provider?: ProviderDriverKind;
37 instanceId: string;
38 models?: ReadonlyArray<string>;
39 supportsTextGeneration?: boolean;
40}): ServerProvider {
41 const driver =
42 input.provider ??
43 (input.instanceId.startsWith("claude_")
44 ? ProviderDriverKind.make("claudeAgent")
45 : ProviderDriverKind.make("codex"));
46 return {
47 instanceId: ProviderInstanceId.make(input.instanceId),
48 driver,
49 ...(input.supportsTextGeneration === undefined
50 ? {}
51 : { supportsTextGeneration: input.supportsTextGeneration }),
52 enabled: true,
53 installed: true,
54 version: null,
55 status: "ready",
56 auth: { status: "authenticated" },
57 checkedAt: "2026-01-01T00:00:00.000Z",
58 models: (input.models ?? []).map((slug) => ({
59 slug,
60 name: slug,
61 isCustom: false,
62 capabilities: {},
63 })),
64 slashCommands: [],
65 skills: [],
66 };
67}
68
69function settingsWithProviderInstances(): UnifiedSettings {
70 return {
71 ...DEFAULT_UNIFIED_SETTINGS,
72 providerInstances: {
73 [ProviderInstanceId.make("claudeAgent")]: {
74 driver: ProviderDriverKind.make("claudeAgent"),
75 config: { customModels: [] },
76 },
77 [ProviderInstanceId.make("claude_openrouter")]: {
78 driver: ProviderDriverKind.make("claudeAgent"),
79 config: { customModels: ["openai/gpt-5.5"] },
80 },
81 },
82 };
83}
84
85describe("scoped default model selections", () => {
86 // regression: #27997 equal automatic defaults can resolve to different providers
87 it("compares automatic defaults using each environment's models", () => {
88 const codex = {
89 projectId: null,
90 settings: DEFAULT_UNIFIED_SETTINGS,
91 providers: [provider({ instanceId: "codex", models: ["gpt-6-astra"] })],
92 };
93 const claude = {
94 projectId: null,
95 settings: DEFAULT_UNIFIED_SETTINGS,
96 providers: [
97 provider({
98 instanceId: "claudeAgent",
99 provider: ProviderDriverKind.make("claudeAgent"),
100 models: ["claude-sonnet-4-6"],
101 }),
102 ],
103 };
104
105 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [codex, claude])).toBe(true);
106 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [claude, codex])).toBe(true);
107 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [codex, codex])).toBe(false);
108 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [claude])).toBe(false);
109 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [])).toBe(false);
110 expect(
111 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [codex, { ...codex, providers: [] }]),
112 ).toBe(true);
113 expect(
114 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [
115 codex,
116 { ...codex, providers: [provider({ instanceId: "codex", models: ["gpt-6-sol"] })] },
117 ]),
118 ).toBe(true);
119
120 const projectId = ProjectId.make("claude-only");
121 const project = {
122 projectId,
123 settings: {
124 ...DEFAULT_SERVER_SETTINGS,
125 projectSettingsOverrides: {
126 [projectId]: {
127 providerInstances: { [ProviderInstanceId.make("codex")]: { enabled: false } },
128 },
129 },
130 },
131 providers: [...codex.providers, ...claude.providers],
132 };
133 const remoteProject = { ...claude, projectId: ProjectId.make("remote-project") };
134 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [project, remoteProject])).toBe(
135 false,
136 );
137 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [remoteProject, project])).toBe(
138 false,
139 );
140 expect(
141 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [
142 project,
143 { ...project, projectId: ProjectId.make("other-project") },
144 ]),
145 ).toBe(true);
146
147 const configured = {
148 projectId: null,
149 settings: {
150 ...DEFAULT_SERVER_SETTINGS,
151 providerModelPreferences: {
152 [ProviderInstanceId.make("codex")]: {
153 hiddenModels: ["gpt-6-astra"],
154 modelOrder: [],
155 },
156 },
157 },
158 providers: [provider({ instanceId: "codex", models: ["gpt-6-astra", "gpt-6-sol"] })],
159 };
160 const inherited = { ...configured, settings: DEFAULT_SERVER_SETTINGS };
161 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [configured, inherited])).toBe(
162 true,
163 );
164 expect(defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [inherited, configured])).toBe(
165 true,
166 );
167 const clientSettings = {
168 ...DEFAULT_CLIENT_SETTINGS,
169 providerModelPreferences: configured.settings.providerModelPreferences,
170 };
171 expect(defaultModelSelectionsAreMixed(clientSettings, [configured, inherited])).toBe(false);
172 expect(defaultModelSelectionsAreMixed(clientSettings, [inherited, configured])).toBe(false);
173 expect(
174 defaultModelSelectionsAreMixed(clientSettings, [
175 { ...inherited, settings: { ...DEFAULT_SERVER_SETTINGS, providerModelPreferences: {} } },
176 inherited,
177 ]),
178 ).toBe(true);
179 });
180
181 it("keeps automatic, explicit, and different saved options distinct", () => {
182 const providers = [provider({ instanceId: "codex", models: ["gpt-6-astra"] })];
183 const selected = {
184 projectId: null,
185 providers,
186 settings: {
187 ...DEFAULT_UNIFIED_SETTINGS,
188 defaultModelSelection: createModelSelection(
189 ProviderInstanceId.make("codex"),
190 "gpt-6-astra",
191 ),
192 },
193 };
194 expect(
195 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [selected, { ...selected }]),
196 ).toBe(false);
197 expect(
198 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [
199 selected,
200 { projectId: null, providers, settings: DEFAULT_UNIFIED_SETTINGS },
201 ]),
202 ).toBe(true);
203 expect(
204 defaultModelSelectionsAreMixed(DEFAULT_CLIENT_SETTINGS, [
205 selected,
206 {
207 ...selected,
208 settings: {
209 ...selected.settings,
210 defaultModelSelection: {
211 ...selected.settings.defaultModelSelection,
212 options: [{ id: "reasoningEffort", value: "xhigh" }],
213 },
214 },
215 },
216 ]),
217 ).toBe(true);
218 });
219});
220
221describe("instance-scoped model selection", () => {
222 it("keeps custom models on the provider instance that declared them", () => {
223 const providers = [
224 provider({
225 instanceId: "claudeAgent",
226 models: ["claude-sonnet-4-6"],
227 }),
228 provider({
229 instanceId: "claude_openrouter",
230 models: ["claude-sonnet-4-6"],
231 }),
232 ];
233 const entries = deriveProviderInstanceEntries(providers);
234 const stock = entries.find((entry) => entry.instanceId === "claudeAgent")!;
235 const openrouter = entries.find((entry) => entry.instanceId === "claude_openrouter")!;
236
237 expect(
238 getAppModelOptionsForInstance(settingsWithProviderInstances(), stock).map(
239 (option) => option.slug,
240 ),
241 ).not.toContain("openai/gpt-5.5");
242 expect(
243 getAppModelOptionsForInstance(settingsWithProviderInstances(), openrouter).map(
244 (option) => option.slug,
245 ),
246 ).toContain("openai/gpt-5.5");
247 });
248
249 it("preserves a custom slug that collides with a provider alias", () => {
250 const providers = [
251 provider({
252 provider: ProviderDriverKind.make("claudeAgent"),
253 instanceId: "claude_openrouter",
254 models: ["claude-opus-4-8"],
255 }),
256 ];
257 const settings: UnifiedSettings = {
258 ...settingsWithProviderInstances(),
259 providerInstances: {
260 ...settingsWithProviderInstances().providerInstances,
261 [ProviderInstanceId.make("claude_openrouter")]: {
262 driver: ProviderDriverKind.make("claudeAgent"),
263 config: { customModels: ["opus"] },
264 },
265 },
266 };
267 const openrouter = deriveProviderInstanceEntries(providers)[0]!;
268
269 expect(
270 getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug),
271 ).toEqual(["claude-opus-4-8", "opus"]);
272 expect(
273 resolveAppModelSelectionForInstance(
274 ProviderInstanceId.make("claude_openrouter"),
275 settings,
276 providers,
277 "opus",
278 ),
279 ).toBe("opus");
280 });
281
282 it("offers and resolves every saved custom model, past the first 32", () => {
283 const providers = [
284 provider({
285 provider: ProviderDriverKind.make("claudeAgent"),
286 instanceId: "claude_openrouter",
287 }),
288 ];
289 const customModels = Array.from({ length: 40 }, (_, index) => `proxy/model-${index + 1}`);
290 const settings: UnifiedSettings = {
291 ...settingsWithProviderInstances(),
292 providerInstances: {
293 ...settingsWithProviderInstances().providerInstances,
294 [ProviderInstanceId.make("claude_openrouter")]: {
295 driver: ProviderDriverKind.make("claudeAgent"),
296 config: { customModels },
297 },
298 },
299 };
300 const openrouter = deriveProviderInstanceEntries(providers)[0]!;
301
302 expect(
303 getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug),
304 ).toEqual(customModels);
305 expect(
306 resolveAppModelSelectionForInstance(
307 ProviderInstanceId.make("claude_openrouter"),
308 settings,
309 providers,
310 "proxy/model-40",
311 ),
312 ).toBe("proxy/model-40");
313 });
314
315 it("drops server-reported custom rows that are no longer in settings", () => {
316 const baseProvider = provider({
317 instanceId: "claude_openrouter",
318 models: ["claude-sonnet-4-6"],
319 });
320 const providers = [
321 {
322 ...baseProvider,
323 models: [
324 ...baseProvider.models,
325 { slug: "removed/custom", name: "removed/custom", isCustom: true, capabilities: {} },
326 ],
327 },
328 ];
329 const openrouter = deriveProviderInstanceEntries(providers)[0]!;
330
331 expect(
332 getAppModelOptionsForInstance(settingsWithProviderInstances(), openrouter).map(
333 (option) => option.slug,
334 ),
335 ).toEqual(["claude-sonnet-4-6", "openai/gpt-5.5"]);
336 });
337
338 it("falls back when the selected model is hidden", () => {
339 const providers = [
340 provider({
341 instanceId: "claudeAgent",
342 models: ["claude-opus-4-6", "claude-sonnet-4-6"],
343 }),
344 ];
345 const settings: UnifiedSettings = {
346 ...settingsWithProviderInstances(),
347 providerModelPreferences: {
348 [ProviderInstanceId.make("claudeAgent")]: {
349 hiddenModels: ["claude-opus-4-6"],
350 modelOrder: [],
351 },
352 },
353 };
354
355 expect(
356 resolveAppModelSelectionForInstance(
357 ProviderInstanceId.make("claudeAgent"),
358 settings,
359 providers,
360 "claude-opus-4-6",
361 ),
362 ).toBe("claude-sonnet-4-6");
363 expect(
364 resolveAppModelSelectionForInstance(
365 ProviderInstanceId.make("claudeAgent"),
366 settings,
367 providers,
368 "claude-opus-4-6",
369 { preserveUnavailableSelection: true },
370 ),
371 ).toBe("claude-sonnet-4-6");
372 });
373
374 it("falls back instead of resolving a custom slug against the wrong instance", () => {
375 const providers = [
376 provider({
377 instanceId: "claudeAgent",
378 models: ["claude-sonnet-4-6"],
379 }),
380 provider({
381 instanceId: "claude_openrouter",
382 models: ["claude-sonnet-4-6"],
383 }),
384 ];
385
386 expect(
387 resolveAppModelSelectionForInstance(
388 ProviderInstanceId.make("claudeAgent"),
389 settingsWithProviderInstances(),
390 providers,
391 "openai/gpt-5.5",
392 ),
393 ).toBe("claude-sonnet-4-6");
394 });
395
396 describe.each([
397 {
398 driverName: "opencode",
399 availableModel: "opencode/big-pickle",
400 missingModel: "opencode/kimi-k3",
401 },
402 {
403 driverName: "antigravity",
404 availableModel: "gemini-3.1-pro",
405 missingModel: "gemini-3.1-pro-high",
406 },
407 {
408 driverName: "cursor",
409 availableModel: "auto-smart",
410 missingModel: "claude-opus-5-5",
411 },
412 ])("$driverName catalog gaps", ({ driverName, availableModel, missingModel }) => {
413 it("preserves a selected model when a catalog refresh no longer contains it", () => {
414 const providers = [
415 provider({
416 provider: ProviderDriverKind.make(driverName),
417 instanceId: driverName,
418 models: [availableModel],
419 }),
420 ];
421
422 expect(
423 resolveAppModelSelectionForInstance(
424 ProviderInstanceId.make(driverName),
425 settingsWithProviderInstances(),
426 providers,
427 missingModel,
428 { preserveUnavailableSelection: true },
429 ),
430 ).toBe(missingModel);
431 expect(
432 resolveAppModelSelectionForInstance(
433 ProviderInstanceId.make(driverName),
434 settingsWithProviderInstances(),
435 providers,
436 missingModel,
437 ),
438 ).toBe(availableModel);
439 });
440
441 it("adds the selected missing model as an unavailable option", () => {
442 const providers = [
443 provider({
444 provider: ProviderDriverKind.make(driverName),
445 instanceId: driverName,
446 models: [availableModel],
447 }),
448 ];
449 const entry = deriveProviderInstanceEntries(providers)[0]!;
450
451 expect(
452 getAppModelOptionsForInstance(settingsWithProviderInstances(), entry, missingModel),
453 ).toEqual([
454 expect.objectContaining({ slug: availableModel }),
455 expect.objectContaining({
456 slug: missingModel,
457 name: missingModel,
458 isUnavailable: true,
459 }),
460 ]);
461 });
462
463 it("does not resurrect a hidden model when the raw catalog omits it", () => {
464 const instanceId = ProviderInstanceId.make(driverName);
465 const driver = ProviderDriverKind.make(driverName);
466 const settings: UnifiedSettings = {
467 ...settingsWithProviderInstances(),
468 providerModelPreferences: {
469 [instanceId]: {
470 hiddenModels: [missingModel],
471 modelOrder: [],
472 },
473 },
474 };
475 const providers = [provider({ provider: driver, instanceId, models: [] })];
476 const entry = deriveProviderInstanceEntries(providers)[0]!;
477
478 expect(getAppModelOptionsForInstance(settings, entry, missingModel)).toEqual([]);
479 expect(
480 resolveAppModelSelectionForInstance(instanceId, settings, providers, missingModel, {
481 preserveUnavailableSelection: true,
482 }),
483 ).toBeNull();
484 });
485 });
486
487 it("falls back from an explicit non-OpenCode draft with a missing model", () => {
488 const instanceId = ProviderInstanceId.make("codex");
489 const driver = ProviderDriverKind.make("codex");
490 const providers = [provider({ provider: driver, instanceId, models: ["gpt-5.6-sol"] })];
491 const state = deriveEffectiveComposerModelState({
492 draft: {
493 activeProvider: instanceId,
494 modelSelectionByProvider: {
495 [instanceId]: createModelSelection(instanceId, "gpt-missing", [
496 { id: "effort", value: "max" },
497 ]),
498 },
499 },
500 providers,
501 selectedProvider: driver,
502 selectedInstanceId: instanceId,
503 threadModelSelection: null,
504 projectModelSelection: null,
505 settings: settingsWithProviderInstances(),
506 });
507 const dispatch = getComposerProviderState({
508 provider: driver,
509 model: state.selectedModel,
510 models: providers[0]!.models,
511 modelOptions: state.modelOptions?.[instanceId],
512 });
513
514 expect(state.selectedModel).toBe("gpt-5.6-sol");
515 expect(dispatch.modelOptionsForDispatch).toBeUndefined();
516 });
517
518 it("sends a Cursor thread's stored model and options when the catalog lacks them", () => {
519 const instanceId = ProviderInstanceId.make("cursor");
520 const driver = ProviderDriverKind.make("cursor");
521 const stored = createModelSelection(instanceId, "claude-opus-5-5", [
522 { id: "effort", value: "medium" },
523 ]);
524 const providers = [provider({ provider: driver, instanceId, models: ["auto-smart"] })];
525 const state = deriveEffectiveComposerModelState({
526 draft: null,
527 providers,
528 selectedProvider: driver,
529 selectedInstanceId: instanceId,
530 threadModelSelection: stored,
531 projectModelSelection: null,
532 settings: settingsWithProviderInstances(),
533 });
534 const dispatch = getComposerProviderState({
535 provider: driver,
536 model: state.selectedModel,
537 models: providers[0]!.models,
538 modelOptions: state.modelOptions?.[instanceId],
539 });
540
541 expect(
542 createModelSelection(instanceId, state.selectedModel, dispatch.modelOptionsForDispatch),
543 ).toEqual(stored);
544 });
545
546 it("preserves an explicit draft OpenCode selection while the catalog is empty", () => {
547 const instanceId = ProviderInstanceId.make("opencode_work");
548 const driver = ProviderDriverKind.make("opencode");
549 const draftSelection = createModelSelection(instanceId, "openrouter/kimi-k3", [
550 { id: "variant", value: "max" },
551 { id: "agent", value: "build" },
552 ]);
553 const providers = [provider({ provider: driver, instanceId, models: [] })];
554 const state = deriveEffectiveComposerModelState({
555 draft: {
556 activeProvider: instanceId,
557 modelSelectionByProvider: { [instanceId]: draftSelection },
558 },
559 providers,
560 selectedProvider: driver,
561 selectedInstanceId: instanceId,
562 threadModelSelection: null,
563 projectModelSelection: null,
564 settings: settingsWithProviderInstances(),
565 });
566
567 expect(state.selectedModel).toBe("openrouter/kimi-k3");
568 expect(state.modelOptions?.[instanceId]).toEqual(draftSelection.options);
569 });
570
571 it("preserves the Antigravity model in drafts and existing threads after sign-out", () => {
572 const instanceId = ProviderInstanceId.make("antigravity_work");
573 const driver = ProviderDriverKind.make("antigravity");
574 const saved = createModelSelection(instanceId, "gemini-3.1-pro-high");
575 const providers = [
576 {
577 ...provider({ provider: driver, instanceId, models: [] }),
578 status: "error" as const,
579 auth: { status: "unauthenticated" as const },
580 },
581 ];
582 for (const draft of [
583 null,
584 { activeProvider: instanceId, modelSelectionByProvider: { [instanceId]: saved } },
585 ]) {
586 const state = deriveEffectiveComposerModelState({
587 draft,
588 providers,
589 selectedProvider: driver,
590 selectedInstanceId: instanceId,
591 threadModelSelection: saved,
592 projectModelSelection: null,
593 settings: settingsWithProviderInstances(),
594 });
595 expect(state.selectedModel).toBe(saved.model);
596 }
597 });
598
599 it("does not borrow a default model while a new Antigravity account has no catalog", () => {
600 const driver = ProviderDriverKind.make("antigravity");
601 const instanceId = ProviderInstanceId.make("antigravity_work");
602 const providers = [
603 provider({ instanceId: "codex", models: ["gpt-5.6-sol"] }),
604 provider({ provider: driver, instanceId: "antigravity", models: ["gemini-other-account"] }),
605 provider({ provider: driver, instanceId, models: [] }),
606 ];
607
608 const otherAccountId = ProviderInstanceId.make("antigravity");
609 for (const draft of [
610 null,
611 {
612 activeProvider: instanceId,
613 modelSelectionByProvider: {
614 [otherAccountId]: createModelSelection(otherAccountId, "gemini-other-account"),
615 },
616 },
617 ]) {
618 const state = deriveEffectiveComposerModelState({
619 draft,
620 providers,
621 selectedProvider: driver,
622 selectedInstanceId: instanceId,
623 threadModelSelection: null,
624 projectModelSelection: createModelSelection(
625 ProviderInstanceId.make("codex"),
626 "gpt-5.6-sol",
627 ),
628 settings: settingsWithProviderInstances(),
629 });
630 expect(state.selectedModel).toBe("");
631 }
632 });
633
634 it("offers only account catalog models for Antigravity despite custom model settings", () => {
635 const driver = ProviderDriverKind.make("antigravity");
636 const customId = ProviderInstanceId.make("antigravity_work");
637 const nativeModel = "gemini-3.1-pro";
638 const settings: UnifiedSettings = {
639 ...DEFAULT_UNIFIED_SETTINGS,
640 providerInstances: {
641 [ProviderInstanceId.make("antigravity")]: {
642 driver,
643 config: { customModels: ["api-only-model"] },
644 },
645 [customId]: { driver, config: { customModels: ["unknown-model"] } },
646 },
647 };
648 const entries = deriveProviderInstanceEntries([
649 provider({ provider: driver, instanceId: "antigravity", models: [nativeModel] }),
650 provider({ provider: driver, instanceId: customId, models: [nativeModel] }),
651 ]);
652
653 for (const entry of entries) {
654 expect(getAppModelOptionsForInstance(settings, entry).map((model) => model.slug)).toEqual([
655 nativeModel,
656 ]);
657 }
658 });
659
660 it("resolves the Antigravity default marker without creating an unavailable model", () => {
661 const instanceId = ProviderInstanceId.make("antigravity_work");
662 const nativeModel = "gemini-3.1-pro";
663 const base = provider({
664 provider: ProviderDriverKind.make("antigravity"),
665 instanceId,
666 models: [nativeModel],
667 });
668 const liveProvider = {
669 ...base,
670 models: base.models.map((model) => ({
671 ...model,
672 isDefault: true,
673 aliases: [ANTIGRAVITY_DEFAULT_MODEL],
674 })),
675 };
676 const settings = settingsWithProviderInstances();
677
678 expect(
679 getAppModelOptionsForInstance(
680 settings,
681 deriveProviderInstanceEntries([liveProvider])[0]!,
682 ANTIGRAVITY_DEFAULT_MODEL,
683 ).map((model) => model.slug),
684 ).toEqual([nativeModel]);
685 expect(
686 resolveAppModelSelectionForInstance(
687 instanceId,
688 settings,
689 [liveProvider],
690 ANTIGRAVITY_DEFAULT_MODEL,
691 {
692 preserveUnavailableSelection: true,
693 },
694 ),
695 ).toBe(nativeModel);
696
697 const hiddenSettings: UnifiedSettings = {
698 ...settings,
699 providerModelPreferences: {
700 [instanceId]: { hiddenModels: [nativeModel], modelOrder: [] },
701 },
702 };
703 expect(
704 resolveAppModelSelectionForInstance(
705 instanceId,
706 hiddenSettings,
707 [liveProvider],
708 ANTIGRAVITY_DEFAULT_MODEL,
709 { preserveUnavailableSelection: true },
710 ),
711 ).toBeNull();
712 expect(
713 getAppModelOptionsForInstance(
714 settings,
715 deriveProviderInstanceEntries([{ ...base, models: [] }])[0]!,
716 ANTIGRAVITY_DEFAULT_MODEL,
717 ),
718 ).toEqual([]);
719 });
720
721 it("preserves saved options through dispatch when the model is absent from the catalog", () => {
722 const instanceId = ProviderInstanceId.make("opencode");
723 const driver = ProviderDriverKind.make("opencode");
724 const providers = [provider({ provider: driver, instanceId, models: ["opencode/big-pickle"] })];
725 const saved = createModelSelection(instanceId, "opencode/kimi-k3", [
726 { id: "variant", value: "max" },
727 { id: "agent", value: "build" },
728 ]);
729 const state = deriveEffectiveComposerModelState({
730 draft: null,
731 providers,
732 selectedProvider: driver,
733 selectedInstanceId: instanceId,
734 threadModelSelection: saved,
735 projectModelSelection: null,
736 settings: settingsWithProviderInstances(),
737 });
738 const dispatch = getComposerProviderState({
739 provider: driver,
740 model: state.selectedModel,
741 models: providers[0]!.models,
742 modelOptions: state.modelOptions?.[instanceId],
743 });
744
745 expect(
746 createModelSelection(instanceId, state.selectedModel, dispatch.modelOptionsForDispatch),
747 ).toEqual(saved);
748 });
749
750 describe("composer model option precedence", () => {
751 const instanceId = ProviderInstanceId.make("claudeAgent");
752 const driver = ProviderDriverKind.make("claudeAgent");
753 const base = provider({ provider: driver, instanceId, models: ["claude-opus-5-5"] });
754 const providers = [
755 {
756 ...base,
757 models: base.models.map((model) => ({
758 ...model,
759 capabilities: {
760 optionDescriptors: [
761 {
762 id: "effort",
763 label: "Effort",
764 type: "select" as const,
765 options: [
766 { id: "medium", label: "Medium", isDefault: true },
767 { id: "high", label: "High" },
768 ],
769 currentValue: "medium",
770 },
771 ],
772 },
773 })),
774 },
775 ];
776 const settingsDefault = createModelSelection(instanceId, "claude-opus-5-5", [
777 { id: "effort", value: "high" },
778 ]);
779 const dispatchedSelection = (
780 threadModelSelection: ReturnType<typeof createModelSelection> | null,
781 ) => {
782 const state = deriveEffectiveComposerModelState({
783 draft: null,
784 providers,
785 selectedProvider: driver,
786 selectedInstanceId: instanceId,
787 threadModelSelection,
788 projectModelSelection: settingsDefault,
789 settings: settingsWithProviderInstances(),
790 });
791 const dispatch = getComposerProviderState({
792 provider: driver,
793 model: state.selectedModel,
794 models: providers[0]!.models,
795 modelOptions: state.modelOptions?.[instanceId],
796 });
797 return createModelSelection(
798 instanceId,
799 state.selectedModel,
800 dispatch.modelOptionsForDispatch,
801 );
802 };
803
804 it("does not inherit settings options for an optionless existing thread", () => {
805 const threadSelection = createModelSelection(instanceId, "claude-opus-5-5");
806 expect(dispatchedSelection(threadSelection)).toEqual(threadSelection);
807 });
808
809 it("keeps the options it was saved with", () => {
810 const threadSelection = createModelSelection(instanceId, "claude-opus-5-5", [
811 { id: "effort", value: "medium" },
812 ]);
813 expect(dispatchedSelection(threadSelection)).toEqual(threadSelection);
814 });
815
816 it("leaves the settings default's options in place before a thread exists", () => {
817 expect(dispatchedSelection(null)).toEqual(settingsDefault);
818 });
819 });
820
821 it("keeps a custom-instance draft model while dropping unsupported options", () => {
822 const instanceId = ProviderInstanceId.make("claude_openrouter");
823 const driver = ProviderDriverKind.make("claudeAgent");
824 const providers = [
825 provider({ provider: driver, instanceId: "claudeAgent", models: ["claude-opus-5"] }),
826 provider({ provider: driver, instanceId, models: ["claude-opus-5"] }),
827 ];
828 const threadSelection = createModelSelection(instanceId, "claude-opus-5", [
829 { id: "effort", value: "high" },
830 ]);
831 const draftSelection = createModelSelection(instanceId, "openai/gpt-5.5", [
832 { id: "effort", value: "max" },
833 ]);
834 const state = deriveEffectiveComposerModelState({
835 draft: {
836 activeProvider: instanceId,
837 modelSelectionByProvider: { [instanceId]: draftSelection },
838 },
839 providers,
840 selectedProvider: driver,
841 selectedInstanceId: instanceId,
842 threadModelSelection: threadSelection,
843 projectModelSelection: null,
844 settings: settingsWithProviderInstances(),
845 });
846 const dispatch = getComposerProviderState({
847 provider: driver,
848 model: state.selectedModel,
849 models: providers[1]!.models,
850 modelOptions: state.modelOptions?.[instanceId],
851 });
852
853 expect(
854 createModelSelection(instanceId, state.selectedModel, dispatch.modelOptionsForDispatch),
855 ).toEqual(createModelSelection(instanceId, "openai/gpt-5.5"));
856 });
857
858 it("keeps the default instance's draft model off a custom instance of the same driver", () => {
859 const driver = ProviderDriverKind.make("opencode");
860 const defaultInstanceId = ProviderInstanceId.make("opencode");
861 const kimiInstanceId = ProviderInstanceId.make("kimi_api");
862 const kimiModel = "kimi-code-plan-global/k3.1-pro";
863 const providers = [
864 provider({
865 provider: driver,
866 instanceId: defaultInstanceId,
867 models: ["opencode-go/kimi-k3.1-pro"],
868 }),
869 provider({ provider: driver, instanceId: kimiInstanceId, models: [kimiModel] }),
870 ];
871 const state = deriveEffectiveComposerModelState({
872 draft: {
873 activeProvider: kimiInstanceId,
874 modelSelectionByProvider: {
875 [defaultInstanceId]: createModelSelection(defaultInstanceId, "opencode-go/kimi-k3.1-pro"),
876 },
877 },
878 providers,
879 selectedProvider: driver,
880 selectedInstanceId: kimiInstanceId,
881 threadModelSelection: null,
882 projectModelSelection: null,
883 settings: DEFAULT_UNIFIED_SETTINGS,
884 });
885
886 expect(state.selectedModel).toBe(kimiModel);
887 });
888
889 it("self-heals a persisted selection pointing at a text-generation-incapable instance", () => {
890 const providers = [
891 provider({
892 provider: ProviderDriverKind.make("acpRegistry"),
893 instanceId: "acp_gemini",
894 models: ["default"],
895 supportsTextGeneration: false,
896 }),
897 provider({
898 instanceId: "codex",
899 models: ["gpt-5.6-luna"],
900 }),
901 ];
902 const settings: UnifiedSettings = {
903 ...settingsWithProviderInstances(),
904 textGenerationModelSelection: {
905 instanceId: ProviderInstanceId.make("acp_gemini"),
906 model: "default",
907 },
908 };
909
910 expect(resolveAppModelSelectionState(settings, providers).instanceId).toBe(
911 ProviderInstanceId.make("codex"),
912 );
913 });
914 it("falls back to the text generation default, not the chat default, for a removed model", () => {
915 const codex = provider({ instanceId: "codex", models: ["gpt-6-astra", "gpt-6-luna"] });
916 const providers = [
917 { ...codex, models: codex.models.map((m, i) => (i === 0 ? { ...m, isDefault: true } : m)) },
918 ];
919 const settings: UnifiedSettings = {
920 ...settingsWithProviderInstances(),
921 textGenerationModelSelection: createModelSelection(
922 ProviderInstanceId.make("codex"),
923 "gpt-5.6-luna",
924 ),
925 };
926
927 expect(resolveAppModelSelectionState(settings, providers).model).toBe(
928 DEFAULT_TEXT_GENERATION_MODEL,
929 );
930 });
931
932 it("does not select a provider that cannot generate system text", () => {
933 const instanceId = ProviderInstanceId.make("antigravity");
934 const unsupported = {
935 ...provider({
936 provider: ProviderDriverKind.make("antigravity"),
937 instanceId,
938 models: ["gemini-3.1-pro"],
939 }),
940 supportsTextGeneration: false,
941 };
942 const supported = provider({ instanceId: "codex", models: ["gpt-5.6-sol"] });
943 const settings = {
944 ...settingsWithProviderInstances(),
945 textGenerationModelSelection: createModelSelection(instanceId, "gemini-3.1-pro"),
946 };
947
948 expect(resolveAppModelSelectionState(settings, [unsupported, supported])).toEqual(
949 createModelSelection(supported.instanceId, "gpt-5.6-sol"),
950 );
951 expect(resolveAppModelSelectionState(settings, [unsupported])).toEqual(
952 NO_PROVIDER_MODEL_SELECTION,
953 );
954 });
955
956 it("excludes customized extension models while preserving unknown custom and thread models", () => {
957 const pi = provider({
958 provider: ProviderDriverKind.make("pi"),
959 instanceId: "pi",
960 models: ["default", "demo/demo-model"],
961 });
962 const providers = [
963 {
964 ...pi,
965 models: pi.models.map((model) =>
966 model.slug === "demo/demo-model" ? { ...model, supportsTextGeneration: false } : model,
967 ),
968 },
969 ];
970 const settings: UnifiedSettings = {
971 ...DEFAULT_UNIFIED_SETTINGS,
972 providerInstances: {
973 [pi.instanceId]: {
974 driver: ProviderDriverKind.make("pi"),
975 config: {
976 customModels: [
977 { slug: "demo/demo-model", name: "My extension model" },
978 { slug: "unknown/model", name: "My custom model" },
979 ],
980 },
981 },
982 },
983 textGenerationModelSelection: createModelSelection(pi.instanceId, "demo/demo-model"),
984 };
985 const supported = providers.filter((provider) => provider.supportsTextGeneration !== false);
986 expect(
987 getTextGenerationModelOptionsByInstance(settings, supported, pi.instanceId, "demo/demo-model")
988 .get(pi.instanceId)
989 ?.map((option) => option.slug),
990 ).toEqual(["default", "unknown/model"]);
991 expect(resolveAppModelSelectionState(settings, supported)).toEqual(
992 createModelSelection(pi.instanceId, "default"),
993 );
994 expect(
995 resolveAppModelSelectionState(
996 {
997 ...settings,
998 textGenerationModelSelection: createModelSelection(pi.instanceId, "unknown/model"),
999 },
1000 supported,
1001 ).model,
1002 ).toBe("unknown/model");
1003 expect(
1004 getCustomModelOptionsByInstance(settings, providers)
1005 .get(pi.instanceId)
1006 ?.map((option) => option.slug),
1007 ).toEqual(["default", "demo/demo-model", "unknown/model"]);
1008 });
1009});
1010
1011describe("resolveAppModelSelectionState with the opencode plan agent", () => {
1012 const instanceId = ProviderInstanceId.make("opencode");
1013 const model = "opencode/gpt-5.4";
1014 const opencode: ServerProvider = {
1015 ...provider({ provider: ProviderDriverKind.make("opencode"), instanceId: "opencode" }),
1016 models: [
1017 {
1018 slug: model,
1019 name: model,
1020 isCustom: false,
1021 capabilities: {
1022 optionDescriptors: [
1023 {
1024 id: "agent",
1025 label: "Agent",
1026 type: "select",
1027 options: [
1028 { id: "build", label: "Build", isDefault: true },
1029 { id: "plan", label: "Plan" },
1030 ],
1031 currentValue: "build",
1032 },
1033 ],
1034 },
1035 },
1036 ],
1037 };
1038 const settings: UnifiedSettings = {
1039 ...DEFAULT_UNIFIED_SETTINGS,
1040 textGenerationModelSelection: createModelSelection(instanceId, model, [
1041 { id: "agent", value: "plan" },
1042 ]),
1043 };
1044
1045 it("keeps a stored plan agent this device did not pick", () => {
1046 expect(resolveAppModelSelectionState(settings, [opencode])).toEqual(
1047 createModelSelection(instanceId, model, [{ id: "agent", value: "plan" }]),
1048 );
1049 });
1050});
1051
1052describe("default model settings and new-thread composer parity", () => {
1053 it.each([
1054 ["hidden", "claudeAgent", "claude-opus-5", "claude-opus-5-5", "medium"],
1055 ["visible", "claudeAgent", "claude-opus-5", "claude-opus-5", "max"],
1056 ["unavailable", "opencode", "openrouter/kimi-k3", "openrouter/kimi-k3", null],
1057 ] as const)(
1058 "resolves the %s default and its traits",
1059 (visibility, kind, model, expectedModel, effort) => {
1060 const driver = ProviderDriverKind.make(kind);
1061 const instanceId = ProviderInstanceId.make(`${kind}_work`);
1062 const snapshot = provider({ provider: driver, instanceId });
1063 const models: ServerProvider["models"] = ["claude-opus-5-5", "claude-opus-5"].map(
1064 (slug, index) => ({
1065 slug,
1066 name: slug,
1067 isCustom: false,
1068 capabilities: {
1069 optionDescriptors: [
1070 {
1071 id: "effort",
1072 label: "Effort",
1073 type: "select",
1074 options: (index === 0 ? ["medium", "high"] : ["high", "max"]).map(
1075 (id, optionIndex) => ({ id, label: id, isDefault: optionIndex === 0 }),
1076 ),
1077 },
1078 ],
1079 },
1080 }),
1081 );
1082 const providers = [{ ...snapshot, models }];
1083 const stored = createModelSelection(instanceId, model, [{ id: "effort", value: "max" }]);
1084 const settings: UnifiedSettings = {
1085 ...settingsWithProviderInstances(),
1086 providerModelPreferences: {
1087 [instanceId]: { hiddenModels: visibility === "hidden" ? [model] : [], modelOrder: [] },
1088 },
1089 };
1090 // ChatView builds the local draft with the project default as its thread selection.
1091 const composer = deriveEffectiveComposerModelState({
1092 draft: null,
1093 providers,
1094 selectedProvider: driver,
1095 selectedInstanceId: instanceId,
1096 threadModelSelection: stored,
1097 projectModelSelection: stored,
1098 settings,
1099 });
1100 const effective = resolveEffectiveDefaultModelSelection(settings, providers, stored);
1101 const traits = (model: string, modelOptions: typeof stored.options) =>
1102 getComposerProviderState({
1103 provider: driver,
1104 model,
1105 models,
1106 modelOptions,
1107 });
1108 const composerTraits = traits(composer.selectedModel, composer.modelOptions?.[instanceId]);
1109
1110 expect(composer.selectedModel).toBe(expectedModel);
1111 expect(traits(effective.model, effective.options)).toEqual(composerTraits);
1112 expect(effective).toEqual({ ...stored, model: composer.selectedModel });
1113 expect(composerTraits.promptEffort).toBe(effort);
1114 },
1115 );
1116});
1117
1118describe("composer fallback to another instance of the same driver", () => {
1119 const claude = ProviderDriverKind.make("claudeAgent");
1120 const codex = ProviderDriverKind.make("codex");
1121 const disabled = ProviderInstanceId.make("claudeAgent");
1122 const fallback = ProviderInstanceId.make("claude_team");
1123 const effortModels: ServerProvider["models"] = [
1124 {
1125 slug: "claude-opus-5-5",
1126 name: "Claude Opus 5.5",
1127 isCustom: false,
1128 capabilities: {
1129 optionDescriptors: [
1130 {
1131 id: "effort",
1132 label: "Effort",
1133 type: "select",
1134 options: ["medium", "high", "xhigh"].map((id) => ({
1135 id,
1136 label: id,
1137 isDefault: id === "medium",
1138 })),
1139 },
1140 ],
1141 },
1142 },
1143 ];
1144 const providers: ReadonlyArray<ServerProvider> = [
1145 {
1146 ...provider({ provider: claude, instanceId: disabled }),
1147 enabled: false,
1148 models: effortModels,
1149 },
1150 { ...provider({ provider: claude, instanceId: fallback }), models: effortModels },
1151 ];
1152 const picked = createModelSelection(disabled, "claude-opus-5-5", [
1153 { id: "effort", value: "xhigh" },
1154 ]);
1155
1156 // The same steps useComposerProviderModel takes, without the draft store.
1157 function composerFor(input: {
1158 readonly providers: ReadonlyArray<ServerProvider>;
1159 readonly draft?: Parameters<typeof deriveEffectiveComposerModelState>[0]["draft"];
1160 readonly threadModelSelection: ReturnType<typeof createModelSelection>;
1161 readonly customModels?: Partial<Record<string, ReadonlyArray<string>>>;
1162 }) {
1163 const settings = {
1164 ...DEFAULT_UNIFIED_SETTINGS,
1165 providerInstances: Object.fromEntries(
1166 input.providers.map((provider) => [
1167 provider.instanceId,
1168 {
1169 driver: provider.driver,
1170 enabled: provider.enabled,
1171 config: {
1172 customModels: (input.customModels?.[provider.instanceId] ?? []).map((slug) => ({
1173 slug,
1174 name: slug,
1175 capabilities: null,
1176 })),
1177 },
1178 },
1179 ]),
1180 ),
1181 };
1182 const selection = resolveComposerProviderSelection({
1183 entries: providerInstanceEntriesFor(input.providers, settings, null),
1184 candidateInstanceIds: [input.threadModelSelection.instanceId],
1185 lockedProvider: null,
1186 lockedInstanceId: undefined,
1187 });
1188 const selectedInstanceId = selection.selectedProviderEntry!.instanceId;
1189 const selectedProvider = selection.selectedProviderEntry!.driverKind;
1190 const state = deriveEffectiveComposerModelState({
1191 draft: input.draft ?? null,
1192 providers: input.providers,
1193 selectedProvider,
1194 selectedInstanceId,
1195 requestedInstanceId: selection.requestedInstanceId,
1196 threadModelSelection: input.threadModelSelection,
1197 projectModelSelection: null,
1198 settings,
1199 });
1200 const traits = getComposerProviderState({
1201 provider: selectedProvider,
1202 model: state.selectedModel,
1203 models: selection.selectedProviderEntry!.models,
1204 modelOptions: state.modelOptions?.[selectedInstanceId],
1205 });
1206 return { selectedInstanceId, state, traits };
1207 }
1208
1209 it("keeps the requested instance's effort on the fallback instance", () => {
1210 const { selectedInstanceId, state, traits } = composerFor({
1211 providers,
1212 threadModelSelection: picked,
1213 });
1214
1215 expect(selectedInstanceId).toBe(fallback);
1216 expect(state.selectedModel).toBe("claude-opus-5-5");
1217 expect(traits.promptEffort).toBe("xhigh");
1218 expect(
1219 createModelSelection(fallback, state.selectedModel, traits.modelOptionsForDispatch),
1220 ).toEqual(
1221 createModelSelection(fallback, "claude-opus-5-5", [{ id: "effort", value: "xhigh" }]),
1222 );
1223 });
1224
1225 it("keeps options the fallback instance already has", () => {
1226 const { traits } = composerFor({
1227 providers,
1228 threadModelSelection: picked,
1229 draft: {
1230 activeProvider: disabled,
1231 modelSelectionByProvider: {
1232 [disabled]: picked,
1233 [fallback]: createModelSelection(fallback, "claude-opus-5-5", [
1234 { id: "effort", value: "high" },
1235 ]),
1236 },
1237 },
1238 });
1239
1240 expect(traits.promptEffort).toBe("high");
1241 });
1242
1243 it("does not carry options picked with another model", () => {
1244 // An optionless draft pick takes its options from the thread, which holds another model.
1245 const { state, traits } = composerFor({
1246 providers,
1247 threadModelSelection: createModelSelection(disabled, "claude-sonnet-4-6", [
1248 { id: "effort", value: "high" },
1249 ]),
1250 draft: {
1251 activeProvider: disabled,
1252 modelSelectionByProvider: { [disabled]: createModelSelection(disabled, "claude-opus-5-5") },
1253 },
1254 });
1255
1256 expect(state.selectedModel).toBe("claude-opus-5-5");
1257 expect(traits.promptEffort).toBe("medium");
1258 expect(traits.modelOptionsForDispatch).toBeUndefined();
1259 });
1260
1261 describe("with model aliases", () => {
1262 const reasoningModel = (
1263 slug: string,
1264 id: string,
1265 extra: Partial<ServerProvider["models"][number]> = {},
1266 ): ServerProvider["models"][number] => ({
1267 slug,
1268 name: slug,
1269 isCustom: false,
1270 ...extra,
1271 capabilities: {
1272 optionDescriptors: [
1273 {
1274 id,
1275 label: id,
1276 type: "select",
1277 options: ["medium", "high", "xhigh"].map((value) => ({
1278 id: value,
1279 label: value,
1280 isDefault: value === "medium",
1281 })),
1282 },
1283 ],
1284 },
1285 });
1286
1287 it.each([
1288 ["5.4", "gpt-5.4"],
1289 ["gpt-5-codex", "gpt-5.4"],
1290 ["gpt-5.4", "5.4"],
1291 ])("keeps the effort from %s on fallback %s", (savedModel, fallbackModel) => {
1292 const disabledCodex = ProviderInstanceId.make("codex");
1293 const fallbackCodex = ProviderInstanceId.make("codex_team");
1294 const models = [reasoningModel(fallbackModel, "reasoningEffort")];
1295 const { selectedInstanceId, state, traits } = composerFor({
1296 providers: [
1297 { ...provider({ provider: codex, instanceId: disabledCodex }), enabled: false, models },
1298 { ...provider({ provider: codex, instanceId: fallbackCodex }), models },
1299 ],
1300 threadModelSelection: createModelSelection(disabledCodex, savedModel, [
1301 { id: "reasoningEffort", value: "xhigh" },
1302 ]),
1303 });
1304
1305 expect(selectedInstanceId).toBe(fallbackCodex);
1306 expect(state.selectedModel).toBe(fallbackModel);
1307 expect(traits.promptEffort).toBe("xhigh");
1308 });
1309
1310 it.each([
1311 {
1312 // A disabled Codex instance reports only its custom models.
1313 state: "disabled",
1314 requested: {
1315 enabled: false,
1316 models: [reasoningModel("5.4", "reasoningEffort", { isCustom: true })],
1317 },
1318 },
1319 {
1320 // An unavailable instance reports no models; its custom models are only in settings.
1321 state: "unavailable",
1322 requested: { enabled: false, availability: "unavailable" as const, models: [] },
1323 },
1324 ])(
1325 "does not alias-expand a custom model named 5.4 when the requested instance is $state",
1326 ({ requested }) => {
1327 const disabledCodex = ProviderInstanceId.make("codex");
1328 const { state, traits } = composerFor({
1329 providers: [
1330 { ...provider({ provider: codex, instanceId: disabledCodex }), ...requested },
1331 {
1332 ...provider({ provider: codex, instanceId: "codex_team" }),
1333 models: [reasoningModel("gpt-5.4", "reasoningEffort")],
1334 },
1335 ],
1336 customModels: { [disabledCodex]: ["5.4"] },
1337 threadModelSelection: createModelSelection(disabledCodex, "5.4", [
1338 { id: "reasoningEffort", value: "xhigh" },
1339 ]),
1340 });
1341
1342 expect(state.selectedModel).toBe("gpt-5.4");
1343 expect(traits.modelOptionsForDispatch).toBeUndefined();
1344 },
1345 );
1346
1347 it("does not alias-expand the fallback instance's custom model", () => {
1348 const { state, traits } = composerFor({
1349 providers: [
1350 { ...provider({ provider: codex, instanceId: "codex" }), enabled: false },
1351 {
1352 ...provider({ provider: codex, instanceId: "codex_team" }),
1353 models: [reasoningModel("5.4", "reasoningEffort", { isCustom: true })],
1354 },
1355 ],
1356 customModels: { codex_team: ["5.4"] },
1357 threadModelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [
1358 { id: "reasoningEffort", value: "xhigh" },
1359 ]),
1360 });
1361
1362 expect(state.selectedModel).toBe("5.4");
1363 expect(traits.modelOptionsForDispatch).toBeUndefined();
1364 });
1365
1366 it("does not carry options through an alias that names another model per instance", () => {
1367 // "antigravity-default" is each Antigravity instance's current model.
1368 const antigravity = ProviderDriverKind.make("antigravity");
1369 const disabledAntigravity = ProviderInstanceId.make("antigravity");
1370 const current = { isDefault: true, aliases: [ANTIGRAVITY_DEFAULT_MODEL] };
1371 const { state, traits } = composerFor({
1372 providers: [
1373 {
1374 ...provider({ provider: antigravity, instanceId: disabledAntigravity }),
1375 enabled: false,
1376 models: [reasoningModel("gemini-3-pro", "thinking", current)],
1377 },
1378 {
1379 ...provider({ provider: antigravity, instanceId: "antigravity_work" }),
1380 models: [reasoningModel("claude-sonnet-4-6", "thinking", current)],
1381 },
1382 ],
1383 threadModelSelection: createModelSelection(disabledAntigravity, ANTIGRAVITY_DEFAULT_MODEL, [
1384 { id: "thinking", value: "high" },
1385 ]),
1386 });
1387
1388 expect(state.selectedModel).toBe("claude-sonnet-4-6");
1389 expect(traits.modelOptionsForDispatch).toBeUndefined();
1390 });
1391 });
1392
1393 it("does not carry options to another driver", () => {
1394 const codexOnly: ReadonlyArray<ServerProvider> = [
1395 {
1396 ...provider({ provider: claude, instanceId: disabled }),
1397 enabled: false,
1398 models: effortModels,
1399 },
1400 provider({ provider: codex, instanceId: "codex", models: ["gpt-6-astra"] }),
1401 ];
1402 const { selectedInstanceId, state } = composerFor({
1403 providers: codexOnly,
1404 threadModelSelection: picked,
1405 });
1406
1407 expect(selectedInstanceId).toBe(ProviderInstanceId.make("codex"));
1408 expect(state.modelOptions?.[selectedInstanceId]).toBeUndefined();
1409 });
1410});