| author | |
| committer | |
| log | ffb4af8d5d4fcfc5e9322295ff846a6d1e7ffbf8 |
| tree | 8ff28101c7c1d5372b46ed62cc8f62ab543760fb |
| parent | a3c4e9e9360d65225fb726ebfe460427a6874741 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 32 | codexModelFamily, |
| 33 | 33 | createModelCapabilities, |
| 34 | 34 | formatCodexModelName, |
| 35 | readCustomModelEntries, | |
| 36 | 35 | } from "@t3tools/shared/model"; |
| 37 | 36 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 38 | 37 | import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; |
| ... | ... | @@ -40,6 +39,7 @@ import { |
| 40 | 39 | AUTH_PROBE_TIMEOUT_MS, |
| 41 | 40 | buildServerProvider, |
| 42 | 41 | COMPACT_SLASH_COMMAND, |
| 42 | providerModelsFromSettings, | |
| 43 | 43 | type ServerProviderDraft, |
| 44 | 44 | } from "@t3tools/provider-core/server/snapshotProbe"; |
| 45 | 45 | import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; |
| ... | ... | @@ -277,26 +277,8 @@ function appendCustomCodexModels( |
| 277 | 277 | models: ReadonlyArray<ServerProviderModel>, |
| 278 | 278 | customModels: ReadonlyArray<CustomModelSetting>, |
| 279 | 279 | ): ReadonlyArray<ServerProviderModel> { |
| 280 | if (customModels.length === 0) { | |
| 281 | return models; | |
| 282 | } | |
| 283 | ||
| 284 | const seen = new Set(models.map((model) => model.slug)); | |
| 285 | 280 | 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); | |
| 300 | 282 | } |
| 301 | 283 | |
| 302 | 284 | function parseCodexSkillsListResponse( |
apps/web/src/components/settings/ProviderInstanceCard.tsx+16-7| ... | ... | @@ -181,13 +181,22 @@ export function deriveProviderModelsForDisplay(input: { |
| 181 | 181 | ), |
| 182 | 182 | ); |
| 183 | 183 | 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 | ); | |
| 191 | 200 | return [...serverModels, ...customModels]; |
| 192 | 201 | } |
| 193 | 202 |
docs/user/composer.md+15| ... | ... | @@ -85,6 +85,21 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted |
| 85 | 85 | name and options. Only options supported by the provider integration affect turns. Antigravity |
| 86 | 86 | uses its account catalog and does not support custom models. |
| 87 | 87 | |
| 88 | To rename a built-in model on every client, edit the server's `settings.json` in its `userdata` | |
| 89 | directory. Add a custom model entry with the built-in model ID and a new name under that provider | |
| 90 | instance: | |
| 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 | 103 | ## Model defaults |
| 89 | 104 | |
| 90 | 105 | T3 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 | 75 | ]); |
| 76 | 76 | }); |
| 77 | 77 | |
| 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 | 97 | it("preserves a custom slug that collides with a provider alias", () => { |
| 79 | 98 | const capabilities = createModelCapabilities({ optionDescriptors: [] }); |
| 80 | 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 | 146 | /** |
| 147 | 147 | * Append the user's custom models after the built-ins. A custom entry that |
| 148 | 148 | * 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 | */ |
| 151 | 153 | export function providerModelsFromSettings( |
| 152 | 154 | builtInModels: ReadonlyArray<ServerProviderModel>, |
| 153 | 155 | customModels: ReadonlyArray<CustomModelSetting>, |
| 154 | customModelCapabilities: ModelCapabilities, | |
| 156 | customModelCapabilities: ModelCapabilities | null, | |
| 155 | 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 | 171 | const seen = new Set(resolvedBuiltInModels.map((model) => model.slug)); |
| 158 | 172 | const customEntries: ServerProviderModel[] = []; |
| 159 | 173 | |
| 160 | for (const entry of readCustomModelEntries(customModels)) { | |
| 174 | for (const entry of entries) { | |
| 161 | 175 | if (seen.has(entry.slug)) { |
| 162 | 176 | continue; |
| 163 | 177 | } |