Skip to content

Commit 598fff3

Browse files
maria-rcksjuliusmarmingecodex
authored andcommitted
feat(web): persist modelOptions, refactor provider specific logic (pingdotgg#1121)
Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>
1 parent d10016d commit 598fff3

19 files changed

Lines changed: 1504 additions & 351 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ release/
1717
apps/web/.playwright
1818
apps/web/playwright-report
1919
apps/web/src/components/__screenshots__
20-
.vitest-*
20+
.vitest-*
21+
__screenshots__/

apps/web/src/appSettings.test.ts

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
1533
const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
1634

1735
const originalWindow = globalThis.window;
@@ -128,13 +146,35 @@ describe("getAppModelOptions", () => {
128146

129147
describe("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+
217354
describe("AppSettingsSchema", () => {
218355
it("fills decoding defaults for persisted settings that predate newer keys", () => {
219356
const decode = Schema.decodeUnknownSync(Schema.fromJsonString(AppSettingsSchema));

0 commit comments

Comments
 (0)