@@ -6,12 +6,30 @@ import {
66 DEFAULT_TIMESTAMP_FORMAT ,
77 getAppModelOptions ,
88 getAppSettingsSnapshot ,
9+ getCustomModelOptionsByProvider ,
10+ getCustomModelsByProvider ,
11+ getCustomModelsForProvider ,
12+ getDefaultCustomModelsForProvider ,
13+ MODEL_PROVIDER_SETTINGS ,
914 normalizeCustomModelSlugs ,
15+ patchCustomModels ,
1016 patchGitTextGenerationModelOverrides ,
1117 resolveAppModelSelection ,
1218 resolveGitTextGenerationModelSelection ,
1319} from "./appSettings" ;
1420
21+ /** Empty custom models for all providers — test helper */
22+ const EMPTY_CUSTOM_MODELS = {
23+ codex : [ ] as readonly string [ ] ,
24+ copilot : [ ] as readonly string [ ] ,
25+ claudeAgent : [ ] as readonly string [ ] ,
26+ cursor : [ ] as readonly string [ ] ,
27+ opencode : [ ] as readonly string [ ] ,
28+ geminiCli : [ ] as readonly string [ ] ,
29+ amp : [ ] as readonly string [ ] ,
30+ kilo : [ ] as readonly string [ ] ,
31+ } as const ;
32+
1533const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1" ;
1634
1735const originalWindow = globalThis . window ;
@@ -128,13 +146,35 @@ describe("getAppModelOptions", () => {
128146
129147describe ( "resolveAppModelSelection" , ( ) => {
130148 it ( "preserves saved custom model slugs instead of falling back to the default" , ( ) => {
131- expect ( resolveAppModelSelection ( "codex" , [ "galapagos-alpha" ] , "galapagos-alpha" ) ) . toBe (
132- "galapagos-alpha" ,
133- ) ;
149+ expect (
150+ resolveAppModelSelection (
151+ "codex" ,
152+ { ...EMPTY_CUSTOM_MODELS , codex : [ "galapagos-alpha" ] } ,
153+ "galapagos-alpha" ,
154+ ) ,
155+ ) . toBe ( "galapagos-alpha" ) ;
134156 } ) ;
135157
136158 it ( "falls back to the provider default when no model is selected" , ( ) => {
137- expect ( resolveAppModelSelection ( "codex" , [ ] , "" ) ) . toBe ( "gpt-5.4" ) ;
159+ expect ( resolveAppModelSelection ( "codex" , EMPTY_CUSTOM_MODELS , "" ) ) . toBe ( "gpt-5.4" ) ;
160+ } ) ;
161+
162+ it ( "resolves display names through the shared resolver" , ( ) => {
163+ expect ( resolveAppModelSelection ( "codex" , EMPTY_CUSTOM_MODELS , "GPT-5.3 Codex" ) ) . toBe (
164+ "gpt-5.3-codex" ,
165+ ) ;
166+ } ) ;
167+
168+ it ( "resolves aliases through the shared resolver" , ( ) => {
169+ expect ( resolveAppModelSelection ( "claudeAgent" , EMPTY_CUSTOM_MODELS , "sonnet" ) ) . toBe (
170+ "claude-sonnet-4-6" ,
171+ ) ;
172+ } ) ;
173+
174+ it ( "resolves transient selected custom models included in app model options" , ( ) => {
175+ expect ( resolveAppModelSelection ( "codex" , EMPTY_CUSTOM_MODELS , "custom/selected-model" ) ) . toBe (
176+ "custom/selected-model" ,
177+ ) ;
138178 } ) ;
139179} ) ;
140180
@@ -214,6 +254,103 @@ describe("provider-specific custom models", () => {
214254 } ) ;
215255} ) ;
216256
257+ describe ( "provider-indexed custom model settings" , ( ) => {
258+ const settings = {
259+ customCodexModels : [ "custom/codex-model" ] ,
260+ customClaudeModels : [ "claude/custom-opus" ] ,
261+ customCopilotModels : [ ] ,
262+ customCursorModels : [ ] ,
263+ customOpencodeModels : [ ] ,
264+ customGeminiCliModels : [ ] ,
265+ customAmpModels : [ ] ,
266+ customKiloModels : [ ] ,
267+ } as const ;
268+
269+ it ( "exports one provider config per provider" , ( ) => {
270+ expect ( MODEL_PROVIDER_SETTINGS . map ( ( config ) => config . provider ) ) . toEqual ( [
271+ "codex" ,
272+ "claudeAgent" ,
273+ ] ) ;
274+ } ) ;
275+
276+ it ( "reads custom models for each provider" , ( ) => {
277+ expect ( getCustomModelsForProvider ( settings , "codex" ) ) . toEqual ( [ "custom/codex-model" ] ) ;
278+ expect ( getCustomModelsForProvider ( settings , "claudeAgent" ) ) . toEqual ( [ "claude/custom-opus" ] ) ;
279+ } ) ;
280+
281+ it ( "reads default custom models for each provider" , ( ) => {
282+ const defaults = {
283+ customCodexModels : [ "default/codex-model" ] ,
284+ customClaudeModels : [ "claude/default-opus" ] ,
285+ customCopilotModels : [ ] ,
286+ customCursorModels : [ ] ,
287+ customOpencodeModels : [ ] ,
288+ customGeminiCliModels : [ ] ,
289+ customAmpModels : [ ] ,
290+ customKiloModels : [ ] ,
291+ } as const ;
292+
293+ expect ( getDefaultCustomModelsForProvider ( defaults , "codex" ) ) . toEqual ( [ "default/codex-model" ] ) ;
294+ expect ( getDefaultCustomModelsForProvider ( defaults , "claudeAgent" ) ) . toEqual ( [
295+ "claude/default-opus" ,
296+ ] ) ;
297+ } ) ;
298+
299+ it ( "patches custom models for codex" , ( ) => {
300+ expect ( patchCustomModels ( "codex" , [ "custom/codex-model" ] ) ) . toEqual ( {
301+ customCodexModels : [ "custom/codex-model" ] ,
302+ } ) ;
303+ } ) ;
304+
305+ it ( "patches custom models for claude" , ( ) => {
306+ expect ( patchCustomModels ( "claudeAgent" , [ "claude/custom-opus" ] ) ) . toEqual ( {
307+ customClaudeModels : [ "claude/custom-opus" ] ,
308+ } ) ;
309+ } ) ;
310+
311+ it ( "builds a complete provider-indexed custom model record" , ( ) => {
312+ expect ( getCustomModelsByProvider ( settings ) ) . toEqual ( {
313+ codex : [ "custom/codex-model" ] ,
314+ claudeAgent : [ "claude/custom-opus" ] ,
315+ } ) ;
316+ } ) ;
317+
318+ it ( "builds provider-indexed model options including custom models" , ( ) => {
319+ const modelOptionsByProvider = getCustomModelOptionsByProvider ( settings ) ;
320+
321+ expect (
322+ modelOptionsByProvider . codex . some ( ( option ) => option . slug === "custom/codex-model" ) ,
323+ ) . toBe ( true ) ;
324+ expect (
325+ modelOptionsByProvider . claudeAgent . some ( ( option ) => option . slug === "claude/custom-opus" ) ,
326+ ) . toBe ( true ) ;
327+ } ) ;
328+
329+ it ( "normalizes and deduplicates custom model options per provider" , ( ) => {
330+ const modelOptionsByProvider = getCustomModelOptionsByProvider ( {
331+ customCodexModels : [ " custom/codex-model " , "gpt-5.4" , "custom/codex-model" ] ,
332+ customClaudeModels : [ " sonnet " , "claude/custom-opus" , "claude/custom-opus" ] ,
333+ customCopilotModels : [ ] ,
334+ customCursorModels : [ ] ,
335+ customOpencodeModels : [ ] ,
336+ customGeminiCliModels : [ ] ,
337+ customAmpModels : [ ] ,
338+ customKiloModels : [ ] ,
339+ } ) ;
340+
341+ expect (
342+ modelOptionsByProvider . codex . filter ( ( option ) => option . slug === "custom/codex-model" ) ,
343+ ) . toHaveLength ( 1 ) ;
344+ expect ( modelOptionsByProvider . codex . some ( ( option ) => option . slug === "gpt-5.4" ) ) . toBe ( true ) ;
345+ expect (
346+ modelOptionsByProvider . claudeAgent . filter ( ( option ) => option . slug === "claude/custom-opus" ) ,
347+ ) . toHaveLength ( 1 ) ;
348+ expect (
349+ modelOptionsByProvider . claudeAgent . some ( ( option ) => option . slug === "claude-sonnet-4-6" ) ,
350+ ) . toBe ( true ) ;
351+ } ) ;
352+ } ) ;
353+
217354describe ( "AppSettingsSchema" , ( ) => {
218355 it ( "fills decoding defaults for persisted settings that predate newer keys" , ( ) => {
219356 const decode = Schema . decodeUnknownSync ( Schema . fromJsonString ( AppSettingsSchema ) ) ;
0 commit comments