authorgravatar for maxtheyoung@gmail.comMaxwell Young <maxtheyoung@gmail.com> 2026-07-20 23:32:30+12:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-20 13:32:30+02:00
logfa69f05b69d05003a6994194c12edd1d8aee080c
tree879aa3622f9b3790869c873c86955073fbd804eb
parentc710167bde19e665c8517b606718bfbd406f2dd1
signature Signed by PGP key B5690EEEBB952194

[codex] preserve custom model slugs (#4168)


11 files changed, 88 insertions(+), 58 deletions(-)

apps/server/src/provider/Layers/ClaudeProvider.ts-5
...@@ -2,7 +2,6 @@ import {...@@ -2,7 +2,6 @@ import {
2 type ClaudeSettings,2 type ClaudeSettings,
3 type ModelCapabilities,3 type ModelCapabilities,
4 type ModelSelection,4 type ModelSelection,
5 ProviderDriverKind,
6 type ServerProviderModel,5 type ServerProviderModel,
7 type ServerProviderSlashCommand,6 type ServerProviderSlashCommand,
8} from "@t3tools/contracts";7} from "@t3tools/contracts";
...@@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili...@@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili
44 optionDescriptors: [],43 optionDescriptors: [],
45});44});
4645
47const PROVIDER = ProviderDriverKind.make("claudeAgent");
48const CLAUDE_PRESENTATION = {46const CLAUDE_PRESENTATION = {
49 displayName: "Claude",47 displayName: "Claude",
50 showInteractionModeToggle: true,48 showInteractionModeToggle: true,
...@@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
691 const checkedAt = DateTime.formatIso(yield* DateTime.now);689 const checkedAt = DateTime.formatIso(yield* DateTime.now);
692 const allModels = providerModelsFromSettings(690 const allModels = providerModelsFromSettings(
693 BUILT_IN_MODELS,691 BUILT_IN_MODELS,
694 PROVIDER,
695 claudeSettings.customModels,692 claudeSettings.customModels,
696 DEFAULT_CLAUDE_MODEL_CAPABILITIES,693 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
697 );694 );
...@@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
782779
783 const models = providerModelsFromSettings(780 const models = providerModelsFromSettings(
784 getBuiltInClaudeModelsForVersion(parsedVersion),781 getBuiltInClaudeModelsForVersion(parsedVersion),
785 PROVIDER,
786 claudeSettings.customModels,782 claudeSettings.customModels,
787 DEFAULT_CLAUDE_MODEL_CAPABILITIES,783 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
788 );784 );
...@@ -851,7 +847,6 @@ export const makePendingClaudeProvider = (...@@ -851,7 +847,6 @@ export const makePendingClaudeProvider = (
851 const checkedAt = yield* nowIso;847 const checkedAt = yield* nowIso;
852 const models = providerModelsFromSettings(848 const models = providerModelsFromSettings(
853 BUILT_IN_MODELS,849 BUILT_IN_MODELS,
854 PROVIDER,
855 claudeSettings.customModels,850 claudeSettings.customModels,
856 DEFAULT_CLAUDE_MODEL_CAPABILITIES,851 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
857 );852 );
apps/server/src/provider/Layers/CursorProvider.ts+1-4
...@@ -8,7 +8,6 @@ import type {...@@ -8,7 +8,6 @@ import type {
8 ServerProviderModel,8 ServerProviderModel,
9 ServerProviderState,9 ServerProviderState,
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import { ProviderDriverKind } from "@t3tools/contracts";
12import type * as EffectAcpSchema from "effect-acp/schema";11import type * as EffectAcpSchema from "effect-acp/schema";
13import { causeErrorTag } from "@t3tools/shared/observability";12import { causeErrorTag } from "@t3tools/shared/observability";
14import * as Crypto from "effect/Crypto";13import * as Crypto from "effect/Crypto";
...@@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"...@@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"
51const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect(50const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect(
52 CursorListAvailableModelsResponse,51 CursorListAvailableModelsResponse,
53);52);
54const PROVIDER = ProviderDriverKind.make("cursor");
55const CURSOR_PRESENTATION = {53const CURSOR_PRESENTATION = {
56 displayName: "Cursor",54 displayName: "Cursor",
57 badgeLabel: "Early Access",55 badgeLabel: "Early Access",
...@@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = (...@@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = (
576export function getCursorFallbackModels(574export function getCursorFallbackModels(
577 cursorSettings: Pick<CursorSettings, "customModels">,575 cursorSettings: Pick<CursorSettings, "customModels">,
578): ReadonlyArray<ServerProviderModel> {576): ReadonlyArray<ServerProviderModel> {
579 return providerModelsFromSettings([], PROVIDER, cursorSettings.customModels, EMPTY_CAPABILITIES);577 return providerModelsFromSettings([], cursorSettings.customModels, EMPTY_CAPABILITIES);
580}578}
581579
582/** Timeout for `agent about` — it's slower than a simple `--version` probe. */580/** Timeout for `agent about` — it's slower than a simple `--version` probe. */
...@@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: {...@@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: {
638 checkedAt: input.checkedAt,636 checkedAt: input.checkedAt,
639 models: providerModelsFromSettings(637 models: providerModelsFromSettings(
640 input.discoveredModels ?? [],638 input.discoveredModels ?? [],
641 PROVIDER,
642 input.cursorSettings.customModels,639 input.cursorSettings.customModels,
643 EMPTY_CAPABILITIES,640 EMPTY_CAPABILITIES,
644 ),641 ),
apps/server/src/provider/Layers/GrokProvider.ts+1-8
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import {1import {
2 type GrokSettings,2 type GrokSettings,
3 type ModelCapabilities,3 type ModelCapabilities,
4 ProviderDriverKind,
5 type ServerProvider,4 type ServerProvider,
6 type ServerProviderModel,5 type ServerProviderModel,
7} from "@t3tools/contracts";6} from "@t3tools/contracts";
...@@ -38,7 +37,6 @@ const GROK_PRESENTATION = {...@@ -38,7 +37,6 @@ const GROK_PRESENTATION = {
38 showInteractionModeToggle: false,37 showInteractionModeToggle: false,
39 requiresNewThreadForModelChange: true,38 requiresNewThreadForModelChange: true,
40} as const;39} as const;
41const PROVIDER = ProviderDriverKind.make("grok");
42const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({40const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
43 optionDescriptors: [],41 optionDescriptors: [],
44});42});
...@@ -98,12 +96,7 @@ function grokModelsFromSettings(...@@ -98,12 +96,7 @@ function grokModelsFromSettings(
98 customModels: ReadonlyArray<string> | undefined,96 customModels: ReadonlyArray<string> | undefined,
99 builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS,97 builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS,
100): ReadonlyArray<ServerProviderModel> {98): ReadonlyArray<ServerProviderModel> {
101 return providerModelsFromSettings(99 return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
102 builtInModels,
103 PROVIDER,
104 customModels ?? [],
105 EMPTY_CAPABILITIES,
106 );
107}100}
108101
109function buildGrokDiscoveredModelsFromSessionModelState(102function buildGrokDiscoveredModelsFromSessionModelState(
apps/server/src/provider/Layers/OpenCodeProvider.ts+3-22
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1import {1import {
2 ProviderDriverKind,
3 type ModelCapabilities,2 type ModelCapabilities,
4 type OpenCodeSettings,3 type OpenCodeSettings,
5 type ServerProviderModel,4 type ServerProviderModel,
...@@ -25,7 +24,6 @@ import {...@@ -25,7 +24,6 @@ import {
25} from "../opencodeRuntime.ts";24} from "../opencodeRuntime.ts";
26import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";25import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";
2726
28const PROVIDER = ProviderDriverKind.make("opencode");
29const OPENCODE_PRESENTATION = {27const OPENCODE_PRESENTATION = {
30 displayName: "OpenCode",28 displayName: "OpenCode",
31 showInteractionModeToggle: false,29 showInteractionModeToggle: false,
...@@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = (...@@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = (
259 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);257 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
260 const models = providerModelsFromSettings(258 const models = providerModelsFromSettings(
261 [],259 [],
262 PROVIDER,
263 openCodeSettings.customModels,260 openCodeSettings.customModels,
264 DEFAULT_OPENCODE_MODEL_CAPABILITIES,261 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
265 );262 );
...@@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
319 presentation: OPENCODE_PRESENTATION,316 presentation: OPENCODE_PRESENTATION,
320 enabled: openCodeSettings.enabled,317 enabled: openCodeSettings.enabled,
321 checkedAt,318 checkedAt,
322 models: providerModelsFromSettings(319 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
323 [],
324 PROVIDER,
325 customModels,
326 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
327 ),
328 probe: {320 probe: {
329 installed: failure.installed,321 installed: failure.installed,
330 version,322 version,
...@@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
340 presentation: OPENCODE_PRESENTATION,332 presentation: OPENCODE_PRESENTATION,
341 enabled: false,333 enabled: false,
342 checkedAt,334 checkedAt,
343 models: providerModelsFromSettings(335 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
344 [],
345 PROVIDER,
346 customModels,
347 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
348 ),
349 probe: {336 probe: {
350 installed: false,337 installed: false,
351 version: null,338 version: null,
...@@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
391 presentation: OPENCODE_PRESENTATION,378 presentation: OPENCODE_PRESENTATION,
392 enabled: openCodeSettings.enabled,379 enabled: openCodeSettings.enabled,
393 checkedAt,380 checkedAt,
394 models: providerModelsFromSettings(381 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
395 [],
396 PROVIDER,
397 customModels,
398 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
399 ),
400 probe: {382 probe: {
401 installed: true,383 installed: true,
402 version,384 version,
...@@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
444426
445 const models = providerModelsFromSettings(427 const models = providerModelsFromSettings(
446 flattenOpenCodeModels(inventoryExit.value),428 flattenOpenCodeModels(inventoryExit.value),
447 PROVIDER,
448 customModels,429 customModels,
449 DEFAULT_OPENCODE_MODEL_CAPABILITIES,430 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
450 );431 );
apps/server/src/provider/providerSnapshot.test.ts+20-2
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts";2import type { ModelCapabilities } from "@t3tools/contracts";
3import { HostProcessPlatform } from "@t3tools/shared/hostProcess";3import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
4import { createModelCapabilities } from "@t3tools/shared/model";4import { createModelCapabilities } from "@t3tools/shared/model";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
...@@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => {...@@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => {
38 it("applies the provided capabilities to custom models", () => {38 it("applies the provided capabilities to custom models", () => {
39 const models = providerModelsFromSettings(39 const models = providerModelsFromSettings(
40 [],40 [],
41 ProviderDriverKind.make("opencode"),
42 ["openai/gpt-5"],41 ["openai/gpt-5"],
43 OPENCODE_CUSTOM_MODEL_CAPABILITIES,42 OPENCODE_CUSTOM_MODEL_CAPABILITIES,
44 );43 );
...@@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => {...@@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => {
52 },51 },
53 ]);52 ]);
54 });53 });
54
55 it("preserves a custom slug that collides with a provider alias", () => {
56 const capabilities = createModelCapabilities({ optionDescriptors: [] });
57 const models = providerModelsFromSettings(
58 [
59 {
60 slug: "claude-opus-4-8",
61 name: "Claude Opus 4.8",
62 isCustom: false,
63 capabilities,
64 },
65 ],
66 [" opus "],
67 capabilities,
68 );
69
70 expect(models.map((model) => model.slug)).toEqual(["claude-opus-4-8", "opus"]);
71 expect(models[1]?.isCustom).toBe(true);
72 });
55});73});
5674
57describe("ProviderCommandNotFoundError", () => {75describe("ProviderCommandNotFoundError", () => {
apps/server/src/provider/providerSnapshot.ts+2-3
...@@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError";...@@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError";
13import * as Schema from "effect/Schema";13import * as Schema from "effect/Schema";
14import * as Stream from "effect/Stream";14import * as Stream from "effect/Stream";
15import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";15import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
16import { normalizeModelSlug } from "@t3tools/shared/model";16import { normalizeCustomModelSlug } from "@t3tools/shared/model";
17import { isWindowsCommandNotFound } from "../processRunner.ts";17import { isWindowsCommandNotFound } from "../processRunner.ts";
18import { createProviderVersionAdvisory } from "./providerMaintenance.ts";18import { createProviderVersionAdvisory } from "./providerMaintenance.ts";
19import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";19import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
...@@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null {...@@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null {
140140
141export function providerModelsFromSettings(141export function providerModelsFromSettings(
142 builtInModels: ReadonlyArray<ServerProviderModel>,142 builtInModels: ReadonlyArray<ServerProviderModel>,
143 provider: ProviderDriverKind,
144 customModels: ReadonlyArray<string>,143 customModels: ReadonlyArray<string>,
145 customModelCapabilities: ModelCapabilities,144 customModelCapabilities: ModelCapabilities,
146): ReadonlyArray<ServerProviderModel> {145): ReadonlyArray<ServerProviderModel> {
...@@ -149,7 +148,7 @@ export function providerModelsFromSettings(...@@ -149,7 +148,7 @@ export function providerModelsFromSettings(
149 const customEntries: ServerProviderModel[] = [];148 const customEntries: ServerProviderModel[] = [];
150149
151 for (const candidate of customModels) {150 for (const candidate of customModels) {
152 const normalized = normalizeModelSlug(candidate, provider);151 const normalized = normalizeCustomModelSlug(candidate);
153 if (!normalized || seen.has(normalized)) {152 if (!normalized || seen.has(normalized)) {
154 continue;153 continue;
155 }154 }
apps/web/src/components/settings/ProviderModelsSection.tsx+2-2
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
16 type ProviderInstanceId,16 type ProviderInstanceId,
17 type ServerProviderModel,17 type ServerProviderModel,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { normalizeModelSlug } from "@t3tools/shared/model";19import { normalizeCustomModelSlug } from "@t3tools/shared/model";
2020
21import { cn } from "../../lib/utils";21import { cn } from "../../lib/utils";
22import { sortModelsForProviderInstance } from "../../modelOrdering";22import { sortModelsForProviderInstance } from "../../modelOrdering";
...@@ -111,7 +111,7 @@ export function ProviderModelsSection({...@@ -111,7 +111,7 @@ export function ProviderModelsSection({
111 }, [favoriteModelSet, modelOrder, models]);111 }, [favoriteModelSet, modelOrder, models]);
112112
113 const handleAdd = () => {113 const handleAdd = () => {
114 const normalized = driverKind ? normalizeModelSlug(input, driverKind) : input.trim() || null;114 const normalized = normalizeCustomModelSlug(input);
115 if (!normalized) {115 if (!normalized) {
116 setError("Enter a model slug.");116 setError("Enter a model slug.");
117 return;117 return;
apps/web/src/modelSelection.test.ts+33
...@@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => {...@@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => {
101 ).toBe("openai/gpt-5.5");101 ).toBe("openai/gpt-5.5");
102 });102 });
103103
104 it("preserves a custom slug that collides with a provider alias", () => {
105 const providers = [
106 provider({
107 provider: ProviderDriverKind.make("claudeAgent"),
108 instanceId: "claude_openrouter",
109 models: ["claude-opus-4-8"],
110 }),
111 ];
112 const settings: UnifiedSettings = {
113 ...settingsWithProviderInstances(),
114 providerInstances: {
115 ...settingsWithProviderInstances().providerInstances,
116 [ProviderInstanceId.make("claude_openrouter")]: {
117 driver: ProviderDriverKind.make("claudeAgent"),
118 config: { customModels: ["opus"] },
119 },
120 },
121 };
122 const openrouter = deriveProviderInstanceEntries(providers)[0]!;
123
124 expect(
125 getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug),
126 ).toEqual(["claude-opus-4-8", "opus"]);
127 expect(
128 resolveAppModelSelectionForInstance(
129 ProviderInstanceId.make("claude_openrouter"),
130 settings,
131 providers,
132 "opus",
133 ),
134 ).toBe("opus");
135 });
136
104 it("includes Grok custom models from the selected provider instance", () => {137 it("includes Grok custom models from the selected provider instance", () => {
105 const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })];138 const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })];
106 const settings: UnifiedSettings = {139 const settings: UnifiedSettings = {
apps/web/src/modelSelection.ts+4-6
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import {10import {
11 createModelSelection,11 createModelSelection,
12 normalizeModelSlug,12 normalizeCustomModelSlug,
13 resolveSelectableModel,13 resolveSelectableModel,
14} from "@t3tools/shared/model";14} from "@t3tools/shared/model";
15import { getComposerProviderState } from "./components/chat/composerProviderState";15import { getComposerProviderState } from "./components/chat/composerProviderState";
...@@ -117,13 +117,12 @@ function applyInstanceModelPreferences(...@@ -117,13 +117,12 @@ function applyInstanceModelPreferences(
117export function normalizeCustomModelSlugs(117export function normalizeCustomModelSlugs(
118 models: Iterable<string | null | undefined>,118 models: Iterable<string | null | undefined>,
119 builtInModelSlugs: ReadonlySet<string>,119 builtInModelSlugs: ReadonlySet<string>,
120 provider: ProviderDriverKind = ProviderDriverKind.make("codex"),
121): string[] {120): string[] {
122 const normalizedModels: string[] = [];121 const normalizedModels: string[] = [];
123 const seen = new Set<string>();122 const seen = new Set<string>();
124123
125 for (const candidate of models) {124 for (const candidate of models) {
126 const normalized = normalizeModelSlug(candidate, provider);125 const normalized = normalizeCustomModelSlug(candidate);
127 if (126 if (
128 !normalized ||127 !normalized ||
129 normalized.length > MAX_CUSTOM_MODEL_LENGTH ||128 normalized.length > MAX_CUSTOM_MODEL_LENGTH ||
...@@ -163,7 +162,7 @@ export function getAppModelOptions(...@@ -163,7 +162,7 @@ export function getAppModelOptions(
163 // see the user's authored custom models.162 // see the user's authored custom models.
164 const defaultInstanceId = defaultInstanceIdForDriver(provider);163 const defaultInstanceId = defaultInstanceIdForDriver(provider);
165 const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider);164 const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider);
166 for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, provider)) {165 for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
167 if (seen.has(slug)) {166 if (seen.has(slug)) {
168 continue;167 continue;
169 }168 }
...@@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance(...@@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance(
206 );205 );
207206
208 const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);207 const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);
209 const normalizer = entry.driverKind;208 for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
210 for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, normalizer)) {
211 if (seen.has(slug)) {209 if (seen.has(slug)) {
212 continue;210 continue;
213 }211 }
packages/shared/src/model.test.ts+12-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";2import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
33
4import {4import {
5 buildProviderOptionSelectionsFromDescriptors,5 buildProviderOptionSelectionsFromDescriptors,
...@@ -10,6 +10,8 @@ import {...@@ -10,6 +10,8 @@ import {
10 getProviderOptionDescriptors,10 getProviderOptionDescriptors,
11 getProviderOptionBooleanSelectionValue,11 getProviderOptionBooleanSelectionValue,
12 getProviderOptionStringSelectionValue,12 getProviderOptionStringSelectionValue,
13 normalizeCustomModelSlug,
14 normalizeModelSlug,
13} from "./model.ts";15} from "./model.ts";
1416
15const codexCaps: ModelCapabilities = createModelCapabilities({17const codexCaps: ModelCapabilities = createModelCapabilities({
...@@ -144,3 +146,12 @@ describe("descriptor helpers", () => {...@@ -144,3 +146,12 @@ describe("descriptor helpers", () => {
144 expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true);146 expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true);
145 });147 });
146});148});
149
150describe("model slug normalization", () => {
151 it("preserves exact custom slugs instead of expanding provider aliases", () => {
152 const claude = ProviderDriverKind.make("claudeAgent");
153
154 expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8");
155 expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
156 });
157});
packages/shared/src/model.ts+10-5
...@@ -236,11 +236,7 @@ export function normalizeModelSlug(...@@ -236,11 +236,7 @@ export function normalizeModelSlug(
236 model: string | null | undefined,236 model: string | null | undefined,
237 provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND,237 provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND,
238): string | null {238): string | null {
239 if (typeof model !== "string") {239 const trimmed = normalizeCustomModelSlug(model);
240 return null;
241 }
242
243 const trimmed = model.trim();
244 if (!trimmed) {240 if (!trimmed) {
245 return null;241 return null;
246 }242 }
...@@ -252,6 +248,15 @@ export function normalizeModelSlug(...@@ -252,6 +248,15 @@ export function normalizeModelSlug(
252 return typeof aliased === "string" ? aliased : trimmed;248 return typeof aliased === "string" ? aliased : trimmed;
253}249}
254250
251/** Custom model identifiers are provider-owned, so only trim them; never expand aliases. */
252export function normalizeCustomModelSlug(model: string | null | undefined): string | null {
253 if (typeof model !== "string") {
254 return null;
255 }
256
257 return model.trim() || null;
258}
259
255export function resolveSelectableModel(260export function resolveSelectableModel(
256 provider: ProviderDriverKind,261 provider: ProviderDriverKind,
257 value: string | null | undefined,262 value: string | null | undefined,