Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions apps/server/src/provider/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ import {
codexModelFamily,
createModelCapabilities,
formatCodexModelName,
readCustomModelEntries,
} from "@t3tools/shared/model";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";
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";
Expand Down Expand Up @@ -277,26 +277,8 @@ function appendCustomCodexModels(
models: ReadonlyArray<ServerProviderModel>,
customModels: ReadonlyArray<CustomModelSetting>,
): ReadonlyArray<ServerProviderModel> {
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(
Expand Down
23 changes: 16 additions & 7 deletions apps/web/src/components/settings/ProviderInstanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down
15 changes: 15 additions & 0 deletions docs/user/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/provider-core/src/server/snapshotProbe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
22 changes: 18 additions & 4 deletions packages/provider-core/src/server/snapshotProbe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerProviderModel>,
customModels: ReadonlyArray<CustomModelSetting>,
customModelCapabilities: ModelCapabilities,
customModelCapabilities: ModelCapabilities | null,
): ReadonlyArray<ServerProviderModel> {
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;
}
Expand Down
Loading