authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-12 16:30:51-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 16:30:51-07:00
logffb4af8d5d4fcfc5e9322295ff846a6d1e7ffbf8
tree8ff28101c7c1d5372b46ed62cc8f62ab543760fb
parenta3c4e9e9360d65225fb726ebfe460427a6874741
signature Signed by PGP key B5690EEEBB952194

feat(server): rename built-in models from settings (#18840)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

5 files changed, 70 insertions(+), 31 deletions(-)

apps/server/src/provider/CodexProvider.ts+2-20
...@@ -32,7 +32,6 @@ import {...@@ -32,7 +32,6 @@ import {
32 codexModelFamily,32 codexModelFamily,
33 createModelCapabilities,33 createModelCapabilities,
34 formatCodexModelName,34 formatCodexModelName,
35 readCustomModelEntries,
36} from "@t3tools/shared/model";35} from "@t3tools/shared/model";
37import { resolveSpawnCommand } from "@t3tools/shared/shell";36import { resolveSpawnCommand } from "@t3tools/shared/shell";
38import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";37import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";
...@@ -40,6 +39,7 @@ import {...@@ -40,6 +39,7 @@ import {
40 AUTH_PROBE_TIMEOUT_MS,39 AUTH_PROBE_TIMEOUT_MS,
41 buildServerProvider,40 buildServerProvider,
42 COMPACT_SLASH_COMMAND,41 COMPACT_SLASH_COMMAND,
42 providerModelsFromSettings,
43 type ServerProviderDraft,43 type ServerProviderDraft,
44} from "@t3tools/provider-core/server/snapshotProbe";44} from "@t3tools/provider-core/server/snapshotProbe";
45import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";45import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
...@@ -277,26 +277,8 @@ function appendCustomCodexModels(...@@ -277,26 +277,8 @@ function appendCustomCodexModels(
277 models: ReadonlyArray<ServerProviderModel>,277 models: ReadonlyArray<ServerProviderModel>,
278 customModels: ReadonlyArray<CustomModelSetting>,278 customModels: ReadonlyArray<CustomModelSetting>,
279): ReadonlyArray<ServerProviderModel> {279): ReadonlyArray<ServerProviderModel> {
280 if (customModels.length === 0) {
281 return models;
282 }
283
284 const seen = new Set(models.map((model) => model.slug));
285 const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null;280 const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null;
286 const customEntries: ServerProviderModel[] = [];281 return providerModelsFromSettings(models, customModels, fallbackCapabilities);
287 for (const entry of readCustomModelEntries(customModels)) {
288 if (seen.has(entry.slug)) {
289 continue;
290 }
291 seen.add(entry.slug);
292 customEntries.push({
293 slug: entry.slug,
294 name: entry.name,
295 isCustom: true,
296 capabilities: entry.capabilities ?? fallbackCapabilities,
297 });
298 }
299 return customEntries.length === 0 ? models : [...models, ...customEntries];
300}282}
301283
302function parseCodexSkillsListResponse(284function parseCodexSkillsListResponse(
apps/web/src/components/settings/ProviderInstanceCard.tsx+16-7
...@@ -181,13 +181,22 @@ export function deriveProviderModelsForDisplay(input: {...@@ -181,13 +181,22 @@ export function deriveProviderModelsForDisplay(input: {
181 ),181 ),
182 );182 );
183 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];183 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];
184 const customModels = input.customModels.map((entry) => ({184 const serverSlugs = new Set(serverModels.map((model) => model.slug));
185 slug: entry.slug,185 // A custom entry with a built-in slug only renames that built-in on the
186 name: entry.name,186 // server, so the built-in row already shows it.
187 isCustom: true,187 const customModels = input.customModels.flatMap((entry) =>
188 capabilities:188 serverSlugs.has(entry.slug)
189 entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null,189 ? []
190 }));190 : [
191 {
192 slug: entry.slug,
193 name: entry.name,
194 isCustom: true,
195 capabilities:
196 entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null,
197 },
198 ],
199 );
191 return [...serverModels, ...customModels];200 return [...serverModels, ...customModels];
192}201}
193202
docs/user/composer.md+15
...@@ -85,6 +85,21 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted...@@ -85,6 +85,21 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted
85name and options. Only options supported by the provider integration affect turns. Antigravity85name and options. Only options supported by the provider integration affect turns. Antigravity
86uses its account catalog and does not support custom models.86uses its account catalog and does not support custom models.
8787
88To rename a built-in model on every client, edit the server's `settings.json` in its `userdata`
89directory. Add a custom model entry with the built-in model ID and a new name under that provider
90instance:
91
92```json
93{
94 "providerInstances": {
95 "codex": {
96 "driver": "codex",
97 "config": { "customModels": [{ "slug": "gpt-5.5", "name": "Team GPT" }] }
98 }
99 }
100}
101```
102
88## Model defaults103## Model defaults
89104
90T3 Code remembers your provider, model, and model options for new threads. A105T3 Code remembers your provider, model, and model options for new threads. A
packages/provider-core/src/server/snapshotProbe.test.ts+19
...@@ -75,6 +75,25 @@ describe("providerModelsFromSettings", () => {...@@ -75,6 +75,25 @@ describe("providerModelsFromSettings", () => {
75 ]);75 ]);
76 });76 });
7777
78 it("renames a built-in model when a named entry uses its slug", () => {
79 const capabilities = createModelCapabilities({ optionDescriptors: [] });
80 const models = providerModelsFromSettings(
81 [
82 {
83 slug: "gpt-5.5",
84 name: "GPT-5.5",
85 shortName: "5.5",
86 isCustom: false,
87 capabilities,
88 },
89 ],
90 [{ slug: "gpt-5.5", name: "Team GPT" }],
91 capabilities,
92 );
93
94 expect(models).toEqual([{ slug: "gpt-5.5", name: "Team GPT", isCustom: false, capabilities }]);
95 });
96
78 it("preserves a custom slug that collides with a provider alias", () => {97 it("preserves a custom slug that collides with a provider alias", () => {
79 const capabilities = createModelCapabilities({ optionDescriptors: [] });98 const capabilities = createModelCapabilities({ optionDescriptors: [] });
80 const models = providerModelsFromSettings(99 const models = providerModelsFromSettings(
packages/provider-core/src/server/snapshotProbe.ts+18-4
...@@ -146,18 +146,32 @@ export function parseGenericCliVersion(output: string): string | null {...@@ -146,18 +146,32 @@ export function parseGenericCliVersion(output: string): string | null {
146/**146/**
147 * Append the user's custom models after the built-ins. A custom entry that147 * Append the user's custom models after the built-ins. A custom entry that
148 * declares its own capabilities keeps them; a bare slug gets the driver's148 * declares its own capabilities keeps them; a bare slug gets the driver's
149 * default set. Slugs that collide with a built-in are dropped.149 * default set. A named entry whose slug matches a built-in renames that
150 * built-in instead (for example `{ "slug": "gpt-5.5", "name": "GPT" }` in
151 * `settings.json`); other fields on such an entry are ignored.
150 */152 */
151export function providerModelsFromSettings(153export function providerModelsFromSettings(
152 builtInModels: ReadonlyArray<ServerProviderModel>,154 builtInModels: ReadonlyArray<ServerProviderModel>,
153 customModels: ReadonlyArray<CustomModelSetting>,155 customModels: ReadonlyArray<CustomModelSetting>,
154 customModelCapabilities: ModelCapabilities,156 customModelCapabilities: ModelCapabilities | null,
155): ReadonlyArray<ServerProviderModel> {157): ReadonlyArray<ServerProviderModel> {
156 const resolvedBuiltInModels = [...builtInModels];158 const entries = readCustomModelEntries(customModels);
159 if (entries.length === 0) return builtInModels;
160
161 const nameOverrides = new Map(
162 entries.filter((entry) => entry.name !== entry.slug).map((entry) => [entry.slug, entry.name]),
163 );
164 const resolvedBuiltInModels = builtInModels.map((model) => {
165 const name = nameOverrides.get(model.slug);
166 if (!name) return model;
167 // The built-in short name would undo the override in compact UI.
168 const { shortName: _shortName, ...rest } = model;
169 return { ...rest, name };
170 });
157 const seen = new Set(resolvedBuiltInModels.map((model) => model.slug));171 const seen = new Set(resolvedBuiltInModels.map((model) => model.slug));
158 const customEntries: ServerProviderModel[] = [];172 const customEntries: ServerProviderModel[] = [];
159173
160 for (const entry of readCustomModelEntries(customModels)) {174 for (const entry of entries) {
161 if (seen.has(entry.slug)) {175 if (seen.has(entry.slug)) {
162 continue;176 continue;
163 }177 }