diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 9ed238bf5d2ceb9981dcdc970612708b4d6f96bd..7d4bbfdf5d0fad1cc1f6f20aa882e62760fb2a2d 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -32,7 +32,6 @@ import { codexModelFamily, createModelCapabilities, formatCodexModelName, - readCustomModelEntries, } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; @@ -40,6 +39,7 @@ import { AUTH_PROBE_TIMEOUT_MS, buildServerProvider, COMPACT_SLASH_COMMAND, + providerModelsFromSettings, type ServerProviderDraft, } from "@t3tools/provider-core/server/snapshotProbe"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; @@ -277,26 +277,8 @@ function appendCustomCodexModels( models: ReadonlyArray, customModels: ReadonlyArray, ): ReadonlyArray { - if (customModels.length === 0) { - return models; - } - - const seen = new Set(models.map((model) => model.slug)); const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null; - const customEntries: ServerProviderModel[] = []; - for (const entry of readCustomModelEntries(customModels)) { - if (seen.has(entry.slug)) { - continue; - } - seen.add(entry.slug); - customEntries.push({ - slug: entry.slug, - name: entry.name, - isCustom: true, - capabilities: entry.capabilities ?? fallbackCapabilities, - }); - } - return customEntries.length === 0 ? models : [...models, ...customEntries]; + return providerModelsFromSettings(models, customModels, fallbackCapabilities); } function parseCodexSkillsListResponse( diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index b9f2a4569b0c24b298983a875ea4115bf1c23a90..53b6cbc2783f7af544330b11d7da95b62ba92cb8 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -181,13 +181,22 @@ export function deriveProviderModelsForDisplay(input: { ), ); const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? []; - const customModels = input.customModels.map((entry) => ({ - slug: entry.slug, - name: entry.name, - isCustom: true, - capabilities: - entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null, - })); + const serverSlugs = new Set(serverModels.map((model) => model.slug)); + // A custom entry with a built-in slug only renames that built-in on the + // server, so the built-in row already shows it. + const customModels = input.customModels.flatMap((entry) => + serverSlugs.has(entry.slug) + ? [] + : [ + { + slug: entry.slug, + name: entry.name, + isCustom: true, + capabilities: + entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null, + }, + ], + ); return [...serverModels, ...customModels]; } diff --git a/docs/user/composer.md b/docs/user/composer.md index 831a8ac9792d14b22a8f53d3ece08ce02677af1d..9fe7cb28d8c276721fdcc9134ae03633b0ad0a28 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -85,6 +85,21 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted name and options. Only options supported by the provider integration affect turns. Antigravity uses its account catalog and does not support custom models. +To rename a built-in model on every client, edit the server's `settings.json` in its `userdata` +directory. Add a custom model entry with the built-in model ID and a new name under that provider +instance: + +```json +{ + "providerInstances": { + "codex": { + "driver": "codex", + "config": { "customModels": [{ "slug": "gpt-5.5", "name": "Team GPT" }] } + } + } +} +``` + ## Model defaults T3 Code remembers your provider, model, and model options for new threads. A diff --git a/packages/provider-core/src/server/snapshotProbe.test.ts b/packages/provider-core/src/server/snapshotProbe.test.ts index fc207f90f340930c572cb3b9ff73efa8c2dd49dd..29ba4c9cd9d603cf9688ad5849f5b3d05e4f7b27 100644 --- a/packages/provider-core/src/server/snapshotProbe.test.ts +++ b/packages/provider-core/src/server/snapshotProbe.test.ts @@ -75,6 +75,25 @@ describe("providerModelsFromSettings", () => { ]); }); + it("renames a built-in model when a named entry uses its slug", () => { + const capabilities = createModelCapabilities({ optionDescriptors: [] }); + const models = providerModelsFromSettings( + [ + { + slug: "gpt-5.5", + name: "GPT-5.5", + shortName: "5.5", + isCustom: false, + capabilities, + }, + ], + [{ slug: "gpt-5.5", name: "Team GPT" }], + capabilities, + ); + + expect(models).toEqual([{ slug: "gpt-5.5", name: "Team GPT", isCustom: false, capabilities }]); + }); + it("preserves a custom slug that collides with a provider alias", () => { const capabilities = createModelCapabilities({ optionDescriptors: [] }); const models = providerModelsFromSettings( diff --git a/packages/provider-core/src/server/snapshotProbe.ts b/packages/provider-core/src/server/snapshotProbe.ts index 071d9fd5ae07e3ee0cd79380eb4486dbdc3c9943..7ca44770f2673ef2d780656a7fc812cf247cbf56 100644 --- a/packages/provider-core/src/server/snapshotProbe.ts +++ b/packages/provider-core/src/server/snapshotProbe.ts @@ -146,18 +146,32 @@ export function parseGenericCliVersion(output: string): string | null { /** * Append the user's custom models after the built-ins. A custom entry that * declares its own capabilities keeps them; a bare slug gets the driver's - * default set. Slugs that collide with a built-in are dropped. + * default set. A named entry whose slug matches a built-in renames that + * built-in instead (for example `{ "slug": "gpt-5.5", "name": "GPT" }` in + * `settings.json`); other fields on such an entry are ignored. */ export function providerModelsFromSettings( builtInModels: ReadonlyArray, customModels: ReadonlyArray, - customModelCapabilities: ModelCapabilities, + customModelCapabilities: ModelCapabilities | null, ): ReadonlyArray { - const resolvedBuiltInModels = [...builtInModels]; + const entries = readCustomModelEntries(customModels); + if (entries.length === 0) return builtInModels; + + const nameOverrides = new Map( + entries.filter((entry) => entry.name !== entry.slug).map((entry) => [entry.slug, entry.name]), + ); + const resolvedBuiltInModels = builtInModels.map((model) => { + const name = nameOverrides.get(model.slug); + if (!name) return model; + // The built-in short name would undo the override in compact UI. + const { shortName: _shortName, ...rest } = model; + return { ...rest, name }; + }); const seen = new Set(resolvedBuiltInModels.map((model) => model.slug)); const customEntries: ServerProviderModel[] = []; - for (const entry of readCustomModelEntries(customModels)) { + for (const entry of entries) { if (seen.has(entry.slug)) { continue; }