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
22 type ClaudeSettings,
33 type ModelCapabilities,
44 type ModelSelection,
5− ProviderDriverKind,
65 type ServerProviderModel,
76 type ServerProviderSlashCommand,
87 } from "@t3tools/contracts";
@@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili · 35 unmodified lines
4443 optionDescriptors: [],
4544 });
4645
47−const PROVIDER = ProviderDriverKind.make("claudeAgent");
4846 const CLAUDE_PRESENTATION = {
4947 displayName: "Claude",
5048 showInteractionModeToggle: true,
@@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( · 640 unmodified lines
691689 const checkedAt = DateTime.formatIso(yield* DateTime.now);
692690 const allModels = providerModelsFromSettings(
693691 BUILT_IN_MODELS,
694− PROVIDER,
695692 claudeSettings.customModels,
696693 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
697694 );
@@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( · 84 unmodified lines
782779
783780 const models = providerModelsFromSettings(
784781 getBuiltInClaudeModelsForVersion(parsedVersion),
785− PROVIDER,
786782 claudeSettings.customModels,
787783 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
788784 );
@@ -851,7 +847,6 @@ export const makePendingClaudeProvider = ( · 62 unmodified lines
851847 const checkedAt = yield* nowIso;
852848 const models = providerModelsFromSettings(
853849 BUILT_IN_MODELS,
854− PROVIDER,
855850 claudeSettings.customModels,
856851 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
857852 );
apps/server/src/provider/Layers/CursorProvider.ts+1−4
@@ -8,7 +8,6 @@ import type { · 7 unmodified lines
88 ServerProviderModel,
99 ServerProviderState,
1010 } from "@t3tools/contracts";
11−import { ProviderDriverKind } from "@t3tools/contracts";
1211 import type * as EffectAcpSchema from "effect-acp/schema";
1312 import { causeErrorTag } from "@t3tools/shared/observability";
1413 import * as Crypto from "effect/Crypto";
@@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts" · 36 unmodified lines
5150 const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect(
5251 CursorListAvailableModelsResponse,
5352 );
54−const PROVIDER = ProviderDriverKind.make("cursor");
5553 const CURSOR_PRESENTATION = {
5654 displayName: "Cursor",
5755 badgeLabel: "Early Access",
@@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = ( · 518 unmodified lines
576574 export function getCursorFallbackModels(
577575 cursorSettings: Pick<CursorSettings, "customModels">,
578576 ): ReadonlyArray<ServerProviderModel> {
579− return providerModelsFromSettings([], PROVIDER, cursorSettings.customModels, EMPTY_CAPABILITIES);
577+ return providerModelsFromSettings([], cursorSettings.customModels, EMPTY_CAPABILITIES);
580578 }
581579
582580 /** Timeout for `agent about` — it's slower than a simple `--version` probe. */
@@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: { · 55 unmodified lines
638636 checkedAt: input.checkedAt,
639637 models: providerModelsFromSettings(
640638 input.discoveredModels ?? [],
641− PROVIDER,
642639 input.cursorSettings.customModels,
643640 EMPTY_CAPABILITIES,
644641 ),
apps/server/src/provider/Layers/GrokProvider.ts+1−8
@@ -1,7 +1,6 @@
11 import {
22 type GrokSettings,
33 type ModelCapabilities,
4− ProviderDriverKind,
54 type ServerProvider,
65 type ServerProviderModel,
76 } from "@t3tools/contracts";
@@ -38,7 +37,6 @@ const GROK_PRESENTATION = { · 30 unmodified lines
3837 showInteractionModeToggle: false,
3938 requiresNewThreadForModelChange: true,
4039 } as const;
41−const PROVIDER = ProviderDriverKind.make("grok");
4240 const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
4341 optionDescriptors: [],
4442 });
@@ -98,12 +96,7 @@ function grokModelsFromSettings( · 53 unmodified lines
9896 customModels: ReadonlyArray<string> | undefined,
9997 builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS,
10098 ): ReadonlyArray<ServerProviderModel> {
101− return providerModelsFromSettings(
102− builtInModels,
103− PROVIDER,
104− customModels ?? [],
105− EMPTY_CAPABILITIES,
106− );
99+ return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
107100 }
108101
109102 function buildGrokDiscoveredModelsFromSessionModelState(
apps/server/src/provider/Layers/OpenCodeProvider.ts+3−22
@@ -1,5 +1,4 @@
11 import {
2− ProviderDriverKind,
32 type ModelCapabilities,
43 type OpenCodeSettings,
54 type ServerProviderModel,
@@ -25,7 +24,6 @@ import { · 19 unmodified lines
2524 } from "../opencodeRuntime.ts";
2625 import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";
2726
28−const PROVIDER = ProviderDriverKind.make("opencode");
2927 const OPENCODE_PRESENTATION = {
3028 displayName: "OpenCode",
3129 showInteractionModeToggle: false,
@@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = ( · 227 unmodified lines
259257 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
260258 const models = providerModelsFromSettings(
261259 [],
262− PROVIDER,
263260 openCodeSettings.customModels,
264261 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
265262 );
@@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu · 53 unmodified lines
319316 presentation: OPENCODE_PRESENTATION,
320317 enabled: openCodeSettings.enabled,
321318 checkedAt,
322− models: providerModelsFromSettings(
323− [],
324− PROVIDER,
325− customModels,
326− DEFAULT_OPENCODE_MODEL_CAPABILITIES,
327− ),
319+ models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
328320 probe: {
329321 installed: failure.installed,
330322 version,
@@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu · 9 unmodified lines
340332 presentation: OPENCODE_PRESENTATION,
341333 enabled: false,
342334 checkedAt,
343− models: providerModelsFromSettings(
344− [],
345− PROVIDER,
346− customModels,
347− DEFAULT_OPENCODE_MODEL_CAPABILITIES,
348− ),
335+ models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
349336 probe: {
350337 installed: false,
351338 version: null,
@@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu · 39 unmodified lines
391378 presentation: OPENCODE_PRESENTATION,
392379 enabled: openCodeSettings.enabled,
393380 checkedAt,
394− models: providerModelsFromSettings(
395− [],
396− PROVIDER,
397− customModels,
398− DEFAULT_OPENCODE_MODEL_CAPABILITIES,
399− ),
381+ models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
400382 probe: {
401383 installed: true,
402384 version,
@@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu · 41 unmodified lines
444426
445427 const models = providerModelsFromSettings(
446428 flattenOpenCodeModels(inventoryExit.value),
447− PROVIDER,
448429 customModels,
449430 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
450431 );
apps/server/src/provider/providerSnapshot.test.ts+20−2
@@ -1,5 +1,5 @@
11 import { describe, expect, it } from "@effect/vitest";
2−import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts";
2+import type { ModelCapabilities } from "@t3tools/contracts";
33 import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
44 import { createModelCapabilities } from "@t3tools/shared/model";
55 import * as Effect from "effect/Effect";
@@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => { · 32 unmodified lines
3838 it("applies the provided capabilities to custom models", () => {
3939 const models = providerModelsFromSettings(
4040 [],
41− ProviderDriverKind.make("opencode"),
4241 ["openai/gpt-5"],
4342 OPENCODE_CUSTOM_MODEL_CAPABILITIES,
4443 );
@@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => { · 7 unmodified lines
5251 },
5352 ]);
5453 });
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+ });
5573 });
5674
5775 describe("ProviderCommandNotFoundError", () => {
apps/server/src/provider/providerSnapshot.ts+2−3
@@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError"; · 12 unmodified lines
1313 import * as Schema from "effect/Schema";
1414 import * as Stream from "effect/Stream";
1515 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
16−import { normalizeModelSlug } from "@t3tools/shared/model";
16+import { normalizeCustomModelSlug } from "@t3tools/shared/model";
1717 import { isWindowsCommandNotFound } from "../processRunner.ts";
1818 import { createProviderVersionAdvisory } from "./providerMaintenance.ts";
1919 import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
@@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null { · 120 unmodified lines
140140
141141 export function providerModelsFromSettings(
142142 builtInModels: ReadonlyArray<ServerProviderModel>,
143− provider: ProviderDriverKind,
144143 customModels: ReadonlyArray<string>,
145144 customModelCapabilities: ModelCapabilities,
146145 ): ReadonlyArray<ServerProviderModel> {
@@ -149,7 +148,7 @@ export function providerModelsFromSettings( · 2 unmodified lines
149148 const customEntries: ServerProviderModel[] = [];
150149
151150 for (const candidate of customModels) {
152− const normalized = normalizeModelSlug(candidate, provider);
151+ const normalized = normalizeCustomModelSlug(candidate);
153152 if (!normalized || seen.has(normalized)) {
154153 continue;
155154 }
apps/web/src/components/settings/ProviderModelsSection.tsx+2−2
@@ -16,7 +16,7 @@ import { · 15 unmodified lines
1616 type ProviderInstanceId,
1717 type ServerProviderModel,
1818 } from "@t3tools/contracts";
19−import { normalizeModelSlug } from "@t3tools/shared/model";
19+import { normalizeCustomModelSlug } from "@t3tools/shared/model";
2020
2121 import { cn } from "../../lib/utils";
2222 import { sortModelsForProviderInstance } from "../../modelOrdering";
@@ -111,7 +111,7 @@ export function ProviderModelsSection({ · 88 unmodified lines
111111 }, [favoriteModelSet, modelOrder, models]);
112112
113113 const handleAdd = () => {
114− const normalized = driverKind ? normalizeModelSlug(input, driverKind) : input.trim() || null;
114+ const normalized = normalizeCustomModelSlug(input);
115115 if (!normalized) {
116116 setError("Enter a model slug.");
117117 return;
apps/web/src/modelSelection.test.ts+33−0
@@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => { · 100 unmodified lines
101101 ).toBe("openai/gpt-5.5");
102102 });
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+
104137 it("includes Grok custom models from the selected provider instance", () => {
105138 const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })];
106139 const settings: UnifiedSettings = {
apps/web/src/modelSelection.ts+4−6
@@ -9,7 +9,7 @@ import { · 8 unmodified lines
99 } from "@t3tools/contracts";
1010 import {
1111 createModelSelection,
12− normalizeModelSlug,
12+ normalizeCustomModelSlug,
1313 resolveSelectableModel,
1414 } from "@t3tools/shared/model";
1515 import { getComposerProviderState } from "./components/chat/composerProviderState";
@@ -117,13 +117,12 @@ function applyInstanceModelPreferences( · 101 unmodified lines
117117 export function normalizeCustomModelSlugs(
118118 models: Iterable<string | null | undefined>,
119119 builtInModelSlugs: ReadonlySet<string>,
120− provider: ProviderDriverKind = ProviderDriverKind.make("codex"),
121120 ): string[] {
122121 const normalizedModels: string[] = [];
123122 const seen = new Set<string>();
124123
125124 for (const candidate of models) {
126− const normalized = normalizeModelSlug(candidate, provider);
125+ const normalized = normalizeCustomModelSlug(candidate);
127126 if (
128127 !normalized ||
129128 normalized.length > MAX_CUSTOM_MODEL_LENGTH ||
@@ -163,7 +162,7 @@ export function getAppModelOptions( · 33 unmodified lines
163162 // see the user's authored custom models.
164163 const defaultInstanceId = defaultInstanceIdForDriver(provider);
165164 const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider);
166− for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, provider)) {
165+ for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
167166 if (seen.has(slug)) {
168167 continue;
169168 }
@@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance( · 36 unmodified lines
206205 );
207206
208207 const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);
209− const normalizer = entry.driverKind;
210− for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, normalizer)) {
208+ for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
211209 if (seen.has(slug)) {
212210 continue;
213211 }
packages/shared/src/model.test.ts+12−1
@@ -1,5 +1,5 @@
11 import { describe, expect, it } from "vite-plus/test";
2−import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
2+import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
33
44 import {
55 buildProviderOptionSelectionsFromDescriptors,
@@ -10,6 +10,8 @@ import { · 4 unmodified lines
1010 getProviderOptionDescriptors,
1111 getProviderOptionBooleanSelectionValue,
1212 getProviderOptionStringSelectionValue,
13+ normalizeCustomModelSlug,
14+ normalizeModelSlug,
1315 } from "./model.ts";
1416
1517 const codexCaps: ModelCapabilities = createModelCapabilities({
@@ -144,3 +146,12 @@ describe("descriptor helpers", () => { · 128 unmodified lines
144146 expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true);
145147 });
146148 });
149+
150+describe("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
236236 model: string | null | undefined,
237237 provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND,
238238 ): string | null {
239− if (typeof model !== "string") {
240− return null;
241− }
242−
243− const trimmed = model.trim();
239+ const trimmed = normalizeCustomModelSlug(model);
244240 if (!trimmed) {
245241 return null;
246242 }
@@ -252,6 +248,15 @@ export function normalizeModelSlug( · 5 unmodified lines
252248 return typeof aliased === "string" ? aliased : trimmed;
253249 }
254250
251+/** Custom model identifiers are provider-owned, so only trim them; never expand aliases. */
252+export 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+
255260 export function resolveSelectableModel(
256261 provider: ProviderDriverKind,
257262 value: string | null | undefined,