History

[codex] preserve custom model slugs (#4168)

Maxwell Young committed 3 months ago

Commit fa69f051 parent c71016711 files · +88 −58Browse files

Unified · Split

11 files
  1. apps/server/src/provider/Layers/ClaudeProvider.ts +0 −5
  2. apps/server/src/provider/Layers/CursorProvider.ts +1 −4
  3. apps/server/src/provider/Layers/GrokProvider.ts +1 −8
  4. apps/server/src/provider/Layers/OpenCodeProvider.ts +3 −22
  5. apps/server/src/provider/providerSnapshot.test.ts +20 −2
  6. apps/server/src/provider/providerSnapshot.ts +2 −3
  7. apps/web/src/components/settings/ProviderModelsSection.tsx +2 −2
  8. apps/web/src/modelSelection.test.ts +33 −0
  9. apps/web/src/modelSelection.ts +4 −6
  10. packages/shared/src/model.test.ts +12 −1
  11. packages/shared/src/model.ts +10 −5
apps/server/src/provider/Layers/ClaudeProvider.ts+0−5
@@ -2,7 +2,6 @@ import { · 1 unmodified line
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 · 35 unmodified lines
44 optionDescriptors: [],43 optionDescriptors: [],
45});44});
46 45
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")( · 640 unmodified lines
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")( · 84 unmodified lines
782 779
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 = ( · 62 unmodified lines
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 { · 7 unmodified lines
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" · 36 unmodified lines
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 = ( · 518 unmodified lines
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}
581 579
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: { · 55 unmodified lines
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 @@
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 = { · 30 unmodified lines
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( · 53 unmodified lines
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}
108 101
109function buildGrokDiscoveredModelsFromSessionModelState(102function buildGrokDiscoveredModelsFromSessionModelState(
apps/server/src/provider/Layers/OpenCodeProvider.ts+3−22
@@ -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 { · 19 unmodified lines
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";
27 26
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 = ( · 227 unmodified lines
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 · 53 unmodified lines
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 · 9 unmodified lines
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 · 39 unmodified lines
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 · 41 unmodified lines
444 426
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 @@
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", () => { · 32 unmodified lines
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", () => { · 7 unmodified lines
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});
56 74
57describe("ProviderCommandNotFoundError", () => {75describe("ProviderCommandNotFoundError", () => {
apps/server/src/provider/providerSnapshot.ts+2−3
@@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError"; · 12 unmodified lines
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 { · 120 unmodified lines
140 140
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( · 2 unmodified lines
149 const customEntries: ServerProviderModel[] = [];148 const customEntries: ServerProviderModel[] = [];
150 149
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 { · 15 unmodified lines
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";
20 20
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({ · 88 unmodified lines
111 }, [favoriteModelSet, modelOrder, models]);111 }, [favoriteModelSet, modelOrder, models]);
112 112
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−0
@@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => { · 100 unmodified lines
101 ).toBe("openai/gpt-5.5");101 ).toBe("openai/gpt-5.5");
102 });102 });
103 103
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 { · 8 unmodified lines
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( · 101 unmodified lines
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>();
124 123
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( · 33 unmodified lines
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( · 36 unmodified lines
206 );205 );
207 206
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 @@
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";
3 3
4import {4import {
5 buildProviderOptionSelectionsFromDescriptors,5 buildProviderOptionSelectionsFromDescriptors,
@@ -10,6 +10,8 @@ import { · 4 unmodified lines
10 getProviderOptionDescriptors,10 getProviderOptionDescriptors,
11 getProviderOptionBooleanSelectionValue,11 getProviderOptionBooleanSelectionValue,
12 getProviderOptionStringSelectionValue,12 getProviderOptionStringSelectionValue,
13 normalizeCustomModelSlug,
14 normalizeModelSlug,
13} from "./model.ts";15} from "./model.ts";
14 16
15const codexCaps: ModelCapabilities = createModelCapabilities({17const codexCaps: ModelCapabilities = createModelCapabilities({
@@ -144,3 +146,12 @@ describe("descriptor helpers", () => { · 128 unmodified lines
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( · 235 unmodified lines
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( · 5 unmodified lines
252 return typeof aliased === "string" ? aliased : trimmed;248 return typeof aliased === "string" ? aliased : trimmed;
253}249}
254 250
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,