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 {
3232 codexModelFamily,
3333 createModelCapabilities,
3434 formatCodexModelName,
35 readCustomModelEntries,
3635} from "@t3tools/shared/model";
3736import { resolveSpawnCommand } from "@t3tools/shared/shell";
3837import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";
......@@ -40,6 +39,7 @@ import {
4039 AUTH_PROBE_TIMEOUT_MS,
4140 buildServerProvider,
4241 COMPACT_SLASH_COMMAND,
42 providerModelsFromSettings,
4343 type ServerProviderDraft,
4444} from "@t3tools/provider-core/server/snapshotProbe";
4545import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
......@@ -277,26 +277,8 @@ function appendCustomCodexModels(
277277 models: ReadonlyArray<ServerProviderModel>,
278278 customModels: ReadonlyArray<CustomModelSetting>,
279279): ReadonlyArray<ServerProviderModel> {
280 if (customModels.length === 0) {
281 return models;
282 }
283
284 const seen = new Set(models.map((model) => model.slug));
285280 const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null;
286 const customEntries: ServerProviderModel[] = [];
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];
281 return providerModelsFromSettings(models, customModels, fallbackCapabilities);
300282}
301283
302284function parseCodexSkillsListResponse(
apps/web/src/components/settings/ProviderInstanceCard.tsx+16-7
......@@ -181,13 +181,22 @@ export function deriveProviderModelsForDisplay(input: {
181181 ),
182182 );
183183 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];
184 const customModels = input.customModels.map((entry) => ({
185 slug: entry.slug,
186 name: entry.name,
187 isCustom: true,
188 capabilities:
189 entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null,
190 }));
184 const serverSlugs = new Set(serverModels.map((model) => model.slug));
185 // A custom entry with a built-in slug only renames that built-in on the
186 // server, so the built-in row already shows it.
187 const customModels = input.customModels.flatMap((entry) =>
188 serverSlugs.has(entry.slug)
189 ? []
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 );
191200 return [...serverModels, ...customModels];
192201}
193202
docs/user/composer.md+15
......@@ -85,6 +85,21 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted
8585name and options. Only options supported by the provider integration affect turns. Antigravity
8686uses 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
88103## Model defaults
89104
90105T3 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", () => {
7575 ]);
7676 });
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
7897 it("preserves a custom slug that collides with a provider alias", () => {
7998 const capabilities = createModelCapabilities({ optionDescriptors: [] });
8099 const models = providerModelsFromSettings(
packages/provider-core/src/server/snapshotProbe.ts+18-4
......@@ -146,18 +146,32 @@ export function parseGenericCliVersion(output: string): string | null {
146146/**
147147 * Append the user's custom models after the built-ins. A custom entry that
148148 * 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.
150152 */
151153export function providerModelsFromSettings(
152154 builtInModels: ReadonlyArray<ServerProviderModel>,
153155 customModels: ReadonlyArray<CustomModelSetting>,
154 customModelCapabilities: ModelCapabilities,
156 customModelCapabilities: ModelCapabilities | null,
155157): 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 });
157171 const seen = new Set(resolvedBuiltInModels.map((model) => model.slug));
158172 const customEntries: ServerProviderModel[] = [];
159173
160 for (const entry of readCustomModelEntries(customModels)) {
174 for (const entry of entries) {
161175 if (seen.has(entry.slug)) {
162176 continue;
163177 }