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
5 changes: 0 additions & 5 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
type ClaudeSettings,
type ModelCapabilities,
type ModelSelection,
ProviderDriverKind,
type ServerProviderModel,
type ServerProviderSlashCommand,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -43,7 +42,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili
optionDescriptors: [],
});

const PROVIDER = ProviderDriverKind.make("claudeAgent");
const CLAUDE_PRESENTATION = {
displayName: "Claude",
showInteractionModeToggle: true,
Expand Down Expand Up @@ -668,7 +666,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
const checkedAt = DateTime.formatIso(yield* DateTime.now);
const allModels = providerModelsFromSettings(
BUILT_IN_MODELS,
PROVIDER,
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down Expand Up @@ -759,7 +756,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(

const models = providerModelsFromSettings(
getBuiltInClaudeModelsForVersion(parsedVersion),
PROVIDER,
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down Expand Up @@ -827,7 +823,6 @@ export const makePendingClaudeProvider = (
const checkedAt = yield* nowIso;
const models = providerModelsFromSettings(
BUILT_IN_MODELS,
PROVIDER,
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
ServerProviderModel,
ServerProviderState,
} from "@t3tools/contracts";
import { ProviderDriverKind } from "@t3tools/contracts";
import type * as EffectAcpSchema from "effect-acp/schema";
import { causeErrorTag } from "@t3tools/shared/observability";
import * as Crypto from "effect/Crypto";
Expand Down Expand Up @@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"
const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect(
CursorListAvailableModelsResponse,
);
const PROVIDER = ProviderDriverKind.make("cursor");
const CURSOR_PRESENTATION = {
displayName: "Cursor",
badgeLabel: "Early Access",
Expand Down Expand Up @@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = (
export function getCursorFallbackModels(
cursorSettings: Pick<CursorSettings, "customModels">,
): ReadonlyArray<ServerProviderModel> {
return providerModelsFromSettings([], PROVIDER, cursorSettings.customModels, EMPTY_CAPABILITIES);
return providerModelsFromSettings([], cursorSettings.customModels, EMPTY_CAPABILITIES);
}

/** Timeout for `agent about` — it's slower than a simple `--version` probe. */
Expand Down Expand Up @@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: {
checkedAt: input.checkedAt,
models: providerModelsFromSettings(
input.discoveredModels ?? [],
PROVIDER,
input.cursorSettings.customModels,
EMPTY_CAPABILITIES,
),
Expand Down
9 changes: 1 addition & 8 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
type GrokSettings,
type ModelCapabilities,
ProviderDriverKind,
type ServerProvider,
type ServerProviderModel,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -38,7 +37,6 @@ const GROK_PRESENTATION = {
showInteractionModeToggle: false,
requiresNewThreadForModelChange: true,
} as const;
const PROVIDER = ProviderDriverKind.make("grok");
const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
});
Expand Down Expand Up @@ -98,12 +96,7 @@ function grokModelsFromSettings(
customModels: ReadonlyArray<string> | undefined,
builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS,
): ReadonlyArray<ServerProviderModel> {
return providerModelsFromSettings(
builtInModels,
PROVIDER,
customModels ?? [],
EMPTY_CAPABILITIES,
);
return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
}

function buildGrokDiscoveredModelsFromSessionModelState(
Expand Down
25 changes: 3 additions & 22 deletions apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
ProviderDriverKind,
type ModelCapabilities,
type OpenCodeSettings,
type ServerProviderModel,
Expand All @@ -25,7 +24,6 @@ import {
} from "../opencodeRuntime.ts";
import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";

const PROVIDER = ProviderDriverKind.make("opencode");
const OPENCODE_PRESENTATION = {
displayName: "OpenCode",
showInteractionModeToggle: false,
Expand Down Expand Up @@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = (
const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
const models = providerModelsFromSettings(
[],
PROVIDER,
openCodeSettings.customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
);
Expand Down Expand Up @@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
presentation: OPENCODE_PRESENTATION,
enabled: openCodeSettings.enabled,
checkedAt,
models: providerModelsFromSettings(
[],
PROVIDER,
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
probe: {
installed: failure.installed,
version,
Expand All @@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
presentation: OPENCODE_PRESENTATION,
enabled: false,
checkedAt,
models: providerModelsFromSettings(
[],
PROVIDER,
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
probe: {
installed: false,
version: null,
Expand Down Expand Up @@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
presentation: OPENCODE_PRESENTATION,
enabled: openCodeSettings.enabled,
checkedAt,
models: providerModelsFromSettings(
[],
PROVIDER,
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
probe: {
installed: true,
version,
Expand Down Expand Up @@ -438,7 +420,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu

const models = providerModelsFromSettings(
flattenOpenCodeModels(inventoryExit.value),
PROVIDER,
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
);
Expand Down
22 changes: 20 additions & 2 deletions apps/server/src/provider/providerSnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts";
import type { ModelCapabilities } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { createModelCapabilities } from "@t3tools/shared/model";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => {
it("applies the provided capabilities to custom models", () => {
const models = providerModelsFromSettings(
[],
ProviderDriverKind.make("opencode"),
["openai/gpt-5"],
OPENCODE_CUSTOM_MODEL_CAPABILITIES,
);
Expand All @@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => {
},
]);
});

it("preserves a custom slug that collides with a provider alias", () => {
const capabilities = createModelCapabilities({ optionDescriptors: [] });
const models = providerModelsFromSettings(
[
{
slug: "claude-opus-4-8",
name: "Claude Opus 4.8",
isCustom: false,
capabilities,
},
],
[" opus "],
capabilities,
);

expect(models.map((model) => model.slug)).toEqual(["claude-opus-4-8", "opus"]);
expect(models[1]?.isCustom).toBe(true);
});
});

describe("ProviderCommandNotFoundError", () => {
Expand Down
5 changes: 2 additions & 3 deletions apps/server/src/provider/providerSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { normalizeModelSlug } from "@t3tools/shared/model";
import { normalizeCustomModelSlug } from "@t3tools/shared/model";
import { isWindowsCommandNotFound } from "../processRunner.ts";
import { createProviderVersionAdvisory } from "./providerMaintenance.ts";
import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
Expand Down Expand Up @@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null {

export function providerModelsFromSettings(
builtInModels: ReadonlyArray<ServerProviderModel>,
provider: ProviderDriverKind,
customModels: ReadonlyArray<string>,
customModelCapabilities: ModelCapabilities,
): ReadonlyArray<ServerProviderModel> {
Expand All @@ -149,7 +148,7 @@ export function providerModelsFromSettings(
const customEntries: ServerProviderModel[] = [];

for (const candidate of customModels) {
const normalized = normalizeModelSlug(candidate, provider);
const normalized = normalizeCustomModelSlug(candidate);
if (!normalized || seen.has(normalized)) {
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/settings/ProviderModelsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type ProviderInstanceId,
type ServerProviderModel,
} from "@t3tools/contracts";
import { normalizeModelSlug } from "@t3tools/shared/model";
import { normalizeCustomModelSlug } from "@t3tools/shared/model";

import { cn } from "../../lib/utils";
import { sortModelsForProviderInstance } from "../../modelOrdering";
Expand Down Expand Up @@ -111,7 +111,7 @@ export function ProviderModelsSection({
}, [favoriteModelSet, modelOrder, models]);

const handleAdd = () => {
const normalized = driverKind ? normalizeModelSlug(input, driverKind) : input.trim() || null;
const normalized = normalizeCustomModelSlug(input);
if (!normalized) {
setError("Enter a model slug.");
return;
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/modelSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => {
).toBe("openai/gpt-5.5");
});

it("preserves a custom slug that collides with a provider alias", () => {
const providers = [
provider({
provider: ProviderDriverKind.make("claudeAgent"),
instanceId: "claude_openrouter",
models: ["claude-opus-4-8"],
}),
];
const settings: UnifiedSettings = {
...settingsWithProviderInstances(),
providerInstances: {
...settingsWithProviderInstances().providerInstances,
[ProviderInstanceId.make("claude_openrouter")]: {
driver: ProviderDriverKind.make("claudeAgent"),
config: { customModels: ["opus"] },
},
},
};
const openrouter = deriveProviderInstanceEntries(providers)[0]!;

expect(
getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug),
).toEqual(["claude-opus-4-8", "opus"]);
expect(
resolveAppModelSelectionForInstance(
ProviderInstanceId.make("claude_openrouter"),
settings,
providers,
"opus",
),
).toBe("opus");
});

it("includes Grok custom models from the selected provider instance", () => {
const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })];
const settings: UnifiedSettings = {
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/modelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@t3tools/contracts";
import {
createModelSelection,
normalizeModelSlug,
normalizeCustomModelSlug,
resolveSelectableModel,
} from "@t3tools/shared/model";
import { getComposerProviderState } from "./components/chat/composerProviderState";
Expand Down Expand Up @@ -117,13 +117,12 @@ function applyInstanceModelPreferences(
export function normalizeCustomModelSlugs(
models: Iterable<string | null | undefined>,
builtInModelSlugs: ReadonlySet<string>,
provider: ProviderDriverKind = ProviderDriverKind.make("codex"),
): string[] {
const normalizedModels: string[] = [];
const seen = new Set<string>();

for (const candidate of models) {
const normalized = normalizeModelSlug(candidate, provider);
const normalized = normalizeCustomModelSlug(candidate);
if (
!normalized ||
normalized.length > MAX_CUSTOM_MODEL_LENGTH ||
Expand Down Expand Up @@ -163,7 +162,7 @@ export function getAppModelOptions(
// see the user's authored custom models.
const defaultInstanceId = defaultInstanceIdForDriver(provider);
const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider);
for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, provider)) {
for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
if (seen.has(slug)) {
continue;
}
Expand Down Expand Up @@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance(
);

const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);
const normalizer = entry.driverKind;
for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, normalizer)) {
for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) {
if (seen.has(slug)) {
continue;
}
Expand Down
13 changes: 12 additions & 1 deletion packages/shared/src/model.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vite-plus/test";
import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";

import {
buildProviderOptionSelectionsFromDescriptors,
Expand All @@ -10,6 +10,8 @@ import {
getProviderOptionDescriptors,
getProviderOptionBooleanSelectionValue,
getProviderOptionStringSelectionValue,
normalizeCustomModelSlug,
normalizeModelSlug,
} from "./model.ts";

const codexCaps: ModelCapabilities = createModelCapabilities({
Expand Down Expand Up @@ -144,3 +146,12 @@ describe("descriptor helpers", () => {
expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true);
});
});

describe("model slug normalization", () => {
it("preserves exact custom slugs instead of expanding provider aliases", () => {
const claude = ProviderDriverKind.make("claudeAgent");

expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8");
expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
});
});
Loading
Loading