Skip to content

Commit fa69f05

Browse files
authored
[codex] preserve custom model slugs (pingdotgg#4168)
1 parent c710167 commit fa69f05

11 files changed

Lines changed: 88 additions & 58 deletions

File tree

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
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
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")(
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")(
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 = (
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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
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"
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 = (
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: {
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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = {
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(
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

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
ProviderDriverKind,
32
type ModelCapabilities,
43
type OpenCodeSettings,
54
type ServerProviderModel,
@@ -25,7 +24,6 @@ import {
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 = (
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
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
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
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
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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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", () => {
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", () => {
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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError";
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 {
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(
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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
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({
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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => {
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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
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(
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(
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(
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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 {
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", () => {
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+
});

0 commit comments

Comments
 (0)