| 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,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"; |
| 37 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; | 36 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 38 | import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; | 37 | import { 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"; |
| 45 | import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; | 45 | import { 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 | } |
| 301 | 283 | ||
| 302 | function parseCodexSkillsListResponse( | 284 | function 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 | } |
| 193 | 202 |
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 |
| 85 | name and options. Only options supported by the provider integration affect turns. Antigravity | 85 | name and options. Only options supported by the provider integration affect turns. Antigravity |
| 86 | uses its account catalog and does not support custom models. | 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 | ## Model defaults | 103 | ## Model defaults |
| 89 | 104 | ||
| 90 | T3 Code remembers your provider, model, and model options for new threads. A | 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,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 | 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 that | 147 | * 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's | 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 | export function providerModelsFromSettings( | 153 | export 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[] = []; |
| 159 | 173 | ||
| 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 | } |