| author | |
| committer | |
| log | fa69f05b69d05003a6994194c12edd1d8aee080c |
| tree | 879aa3622f9b3790869c873c86955073fbd804eb |
| parent | c710167bde19e665c8517b606718bfbd406f2dd1 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 88 insertions(+), 58 deletions(-)
apps/server/src/provider/Layers/ClaudeProvider.ts-5| ... | @@ -2,7 +2,6 @@ import { | ... | @@ -2,7 +2,6 @@ import { |
| 2 | type ClaudeSettings, | 2 | type ClaudeSettings, |
| 3 | type ModelCapabilities, | 3 | type ModelCapabilities, |
| 4 | type ModelSelection, | 4 | type ModelSelection, |
| 5 | ProviderDriverKind, | ||
| 6 | type ServerProviderModel, | 5 | type ServerProviderModel, |
| 7 | type ServerProviderSlashCommand, | 6 | type ServerProviderSlashCommand, |
| 8 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| ... | @@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili | ... | @@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili |
| 44 | optionDescriptors: [], | 43 | optionDescriptors: [], |
| 45 | }); | 44 | }); |
| 46 | 45 | ||
| 47 | const PROVIDER = ProviderDriverKind.make("claudeAgent"); | ||
| 48 | const CLAUDE_PRESENTATION = { | 46 | const CLAUDE_PRESENTATION = { |
| 49 | displayName: "Claude", | 47 | displayName: "Claude", |
| 50 | showInteractionModeToggle: true, | 48 | showInteractionModeToggle: true, |
| ... | @@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 691 | const checkedAt = DateTime.formatIso(yield* DateTime.now); | 689 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| 692 | const allModels = providerModelsFromSettings( | 690 | const allModels = providerModelsFromSettings( |
| 693 | BUILT_IN_MODELS, | 691 | BUILT_IN_MODELS, |
| 694 | PROVIDER, | ||
| 695 | claudeSettings.customModels, | 692 | claudeSettings.customModels, |
| 696 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, | 693 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, |
| 697 | ); | 694 | ); |
| ... | @@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( | ... | @@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 782 | 779 | ||
| 783 | const models = providerModelsFromSettings( | 780 | const models = providerModelsFromSettings( |
| 784 | getBuiltInClaudeModelsForVersion(parsedVersion), | 781 | getBuiltInClaudeModelsForVersion(parsedVersion), |
| 785 | PROVIDER, | ||
| 786 | claudeSettings.customModels, | 782 | claudeSettings.customModels, |
| 787 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, | 783 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, |
| 788 | ); | 784 | ); |
| ... | @@ -851,7 +847,6 @@ export const makePendingClaudeProvider = ( | ... | @@ -851,7 +847,6 @@ export const makePendingClaudeProvider = ( |
| 851 | const checkedAt = yield* nowIso; | 847 | const checkedAt = yield* nowIso; |
| 852 | const models = providerModelsFromSettings( | 848 | const models = providerModelsFromSettings( |
| 853 | BUILT_IN_MODELS, | 849 | BUILT_IN_MODELS, |
| 854 | PROVIDER, | ||
| 855 | claudeSettings.customModels, | 850 | claudeSettings.customModels, |
| 856 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, | 851 | DEFAULT_CLAUDE_MODEL_CAPABILITIES, |
| 857 | ); | 852 | ); |
apps/server/src/provider/Layers/CursorProvider.ts+1-4| ... | @@ -8,7 +8,6 @@ import type { | ... | @@ -8,7 +8,6 @@ import type { |
| 8 | ServerProviderModel, | 8 | ServerProviderModel, |
| 9 | ServerProviderState, | 9 | ServerProviderState, |
| 10 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import { ProviderDriverKind } from "@t3tools/contracts"; | ||
| 12 | import type * as EffectAcpSchema from "effect-acp/schema"; | 11 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 13 | import { causeErrorTag } from "@t3tools/shared/observability"; | 12 | import { causeErrorTag } from "@t3tools/shared/observability"; |
| 14 | import * as Crypto from "effect/Crypto"; | 13 | import * as Crypto from "effect/Crypto"; |
| ... | @@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts" | ... | @@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts" |
| 51 | const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect( | 50 | const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect( |
| 52 | CursorListAvailableModelsResponse, | 51 | CursorListAvailableModelsResponse, |
| 53 | ); | 52 | ); |
| 54 | const PROVIDER = ProviderDriverKind.make("cursor"); | ||
| 55 | const CURSOR_PRESENTATION = { | 53 | const CURSOR_PRESENTATION = { |
| 56 | displayName: "Cursor", | 54 | displayName: "Cursor", |
| 57 | badgeLabel: "Early Access", | 55 | badgeLabel: "Early Access", |
| ... | @@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = ( | ... | @@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = ( |
| 576 | export function getCursorFallbackModels( | 574 | export function getCursorFallbackModels( |
| 577 | cursorSettings: Pick<CursorSettings, "customModels">, | 575 | cursorSettings: Pick<CursorSettings, "customModels">, |
| 578 | ): ReadonlyArray<ServerProviderModel> { | 576 | ): ReadonlyArray<ServerProviderModel> { |
| 579 | return providerModelsFromSettings([], PROVIDER, cursorSettings.customModels, EMPTY_CAPABILITIES); | 577 | return providerModelsFromSettings([], cursorSettings.customModels, EMPTY_CAPABILITIES); |
| 580 | } | 578 | } |
| 581 | 579 | ||
| 582 | /** Timeout for `agent about` — it's slower than a simple `--version` probe. */ | 580 | /** Timeout for `agent about` — it's slower than a simple `--version` probe. */ |
| ... | @@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: { | ... | @@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: { |
| 638 | checkedAt: input.checkedAt, | 636 | checkedAt: input.checkedAt, |
| 639 | models: providerModelsFromSettings( | 637 | models: providerModelsFromSettings( |
| 640 | input.discoveredModels ?? [], | 638 | input.discoveredModels ?? [], |
| 641 | PROVIDER, | ||
| 642 | input.cursorSettings.customModels, | 639 | input.cursorSettings.customModels, |
| 643 | EMPTY_CAPABILITIES, | 640 | EMPTY_CAPABILITIES, |
| 644 | ), | 641 | ), |
apps/server/src/provider/Layers/GrokProvider.ts+1-8| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | import { | 1 | import { |
| 2 | type GrokSettings, | 2 | type GrokSettings, |
| 3 | type ModelCapabilities, | 3 | type ModelCapabilities, |
| 4 | ProviderDriverKind, | ||
| 5 | type ServerProvider, | 4 | type ServerProvider, |
| 6 | type ServerProviderModel, | 5 | type ServerProviderModel, |
| 7 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| ... | @@ -38,7 +37,6 @@ const GROK_PRESENTATION = { | ... | @@ -38,7 +37,6 @@ const GROK_PRESENTATION = { |
| 38 | showInteractionModeToggle: false, | 37 | showInteractionModeToggle: false, |
| 39 | requiresNewThreadForModelChange: true, | 38 | requiresNewThreadForModelChange: true, |
| 40 | } as const; | 39 | } as const; |
| 41 | const PROVIDER = ProviderDriverKind.make("grok"); | ||
| 42 | const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ | 40 | const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ |
| 43 | optionDescriptors: [], | 41 | optionDescriptors: [], |
| 44 | }); | 42 | }); |
| ... | @@ -98,12 +96,7 @@ function grokModelsFromSettings( | ... | @@ -98,12 +96,7 @@ function grokModelsFromSettings( |
| 98 | customModels: ReadonlyArray<string> | undefined, | 96 | customModels: ReadonlyArray<string> | undefined, |
| 99 | builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS, | 97 | builtInModels: ReadonlyArray<ServerProviderModel> = GROK_BUILT_IN_MODELS, |
| 100 | ): ReadonlyArray<ServerProviderModel> { | 98 | ): ReadonlyArray<ServerProviderModel> { |
| 101 | return providerModelsFromSettings( | 99 | return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES); |
| 102 | builtInModels, | ||
| 103 | PROVIDER, | ||
| 104 | customModels ?? [], | ||
| 105 | EMPTY_CAPABILITIES, | ||
| 106 | ); | ||
| 107 | } | 100 | } |
| 108 | 101 | ||
| 109 | function buildGrokDiscoveredModelsFromSessionModelState( | 102 | function buildGrokDiscoveredModelsFromSessionModelState( |
apps/server/src/provider/Layers/OpenCodeProvider.ts+3-22| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | import { | 1 | import { |
| 2 | ProviderDriverKind, | ||
| 3 | type ModelCapabilities, | 2 | type ModelCapabilities, |
| 4 | type OpenCodeSettings, | 3 | type OpenCodeSettings, |
| 5 | type ServerProviderModel, | 4 | type ServerProviderModel, |
| ... | @@ -25,7 +24,6 @@ import { | ... | @@ -25,7 +24,6 @@ import { |
| 25 | } from "../opencodeRuntime.ts"; | 24 | } from "../opencodeRuntime.ts"; |
| 26 | import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2"; | 25 | import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2"; |
| 27 | 26 | ||
| 28 | const PROVIDER = ProviderDriverKind.make("opencode"); | ||
| 29 | const OPENCODE_PRESENTATION = { | 27 | const OPENCODE_PRESENTATION = { |
| 30 | displayName: "OpenCode", | 28 | displayName: "OpenCode", |
| 31 | showInteractionModeToggle: false, | 29 | showInteractionModeToggle: false, |
| ... | @@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = ( | ... | @@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = ( |
| 259 | const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso); | 257 | const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso); |
| 260 | const models = providerModelsFromSettings( | 258 | const models = providerModelsFromSettings( |
| 261 | [], | 259 | [], |
| 262 | PROVIDER, | ||
| 263 | openCodeSettings.customModels, | 260 | openCodeSettings.customModels, |
| 264 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, | 261 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, |
| 265 | ); | 262 | ); |
| ... | @@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu | ... | @@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu |
| 319 | presentation: OPENCODE_PRESENTATION, | 316 | presentation: OPENCODE_PRESENTATION, |
| 320 | enabled: openCodeSettings.enabled, | 317 | enabled: openCodeSettings.enabled, |
| 321 | checkedAt, | 318 | checkedAt, |
| 322 | models: providerModelsFromSettings( | 319 | models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), |
| 323 | [], | ||
| 324 | PROVIDER, | ||
| 325 | customModels, | ||
| 326 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, | ||
| 327 | ), | ||
| 328 | probe: { | 320 | probe: { |
| 329 | installed: failure.installed, | 321 | installed: failure.installed, |
| 330 | version, | 322 | version, |
| ... | @@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu | ... | @@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu |
| 340 | presentation: OPENCODE_PRESENTATION, | 332 | presentation: OPENCODE_PRESENTATION, |
| 341 | enabled: false, | 333 | enabled: false, |
| 342 | checkedAt, | 334 | checkedAt, |
| 343 | models: providerModelsFromSettings( | 335 | models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), |
| 344 | [], | ||
| 345 | PROVIDER, | ||
| 346 | customModels, | ||
| 347 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, | ||
| 348 | ), | ||
| 349 | probe: { | 336 | probe: { |
| 350 | installed: false, | 337 | installed: false, |
| 351 | version: null, | 338 | version: null, |
| ... | @@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu | ... | @@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu |
| 391 | presentation: OPENCODE_PRESENTATION, | 378 | presentation: OPENCODE_PRESENTATION, |
| 392 | enabled: openCodeSettings.enabled, | 379 | enabled: openCodeSettings.enabled, |
| 393 | checkedAt, | 380 | checkedAt, |
| 394 | models: providerModelsFromSettings( | 381 | models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), |
| 395 | [], | ||
| 396 | PROVIDER, | ||
| 397 | customModels, | ||
| 398 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, | ||
| 399 | ), | ||
| 400 | probe: { | 382 | probe: { |
| 401 | installed: true, | 383 | installed: true, |
| 402 | version, | 384 | version, |
| ... | @@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu | ... | @@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu |
| 444 | 426 | ||
| 445 | const models = providerModelsFromSettings( | 427 | const models = providerModelsFromSettings( |
| 446 | flattenOpenCodeModels(inventoryExit.value), | 428 | flattenOpenCodeModels(inventoryExit.value), |
| 447 | PROVIDER, | ||
| 448 | customModels, | 429 | customModels, |
| 449 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, | 430 | DEFAULT_OPENCODE_MODEL_CAPABILITIES, |
| 450 | ); | 431 | ); |
apps/server/src/provider/providerSnapshot.test.ts+20-2| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts"; | 2 | import type { ModelCapabilities } from "@t3tools/contracts"; |
| 3 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 3 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 4 | import { createModelCapabilities } from "@t3tools/shared/model"; | 4 | import { createModelCapabilities } from "@t3tools/shared/model"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| ... | @@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => { | ... | @@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => { |
| 38 | it("applies the provided capabilities to custom models", () => { | 38 | it("applies the provided capabilities to custom models", () => { |
| 39 | const models = providerModelsFromSettings( | 39 | const models = providerModelsFromSettings( |
| 40 | [], | 40 | [], |
| 41 | ProviderDriverKind.make("opencode"), | ||
| 42 | ["openai/gpt-5"], | 41 | ["openai/gpt-5"], |
| 43 | OPENCODE_CUSTOM_MODEL_CAPABILITIES, | 42 | OPENCODE_CUSTOM_MODEL_CAPABILITIES, |
| 44 | ); | 43 | ); |
| ... | @@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => { | ... | @@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => { |
| 52 | }, | 51 | }, |
| 53 | ]); | 52 | ]); |
| 54 | }); | 53 | }); |
| 54 | |||
| 55 | it("preserves a custom slug that collides with a provider alias", () => { | ||
| 56 | const capabilities = createModelCapabilities({ optionDescriptors: [] }); | ||
| 57 | const models = providerModelsFromSettings( | ||
| 58 | [ | ||
| 59 | { | ||
| 60 | slug: "claude-opus-4-8", | ||
| 61 | name: "Claude Opus 4.8", | ||
| 62 | isCustom: false, | ||
| 63 | capabilities, | ||
| 64 | }, | ||
| 65 | ], | ||
| 66 | [" opus "], | ||
| 67 | capabilities, | ||
| 68 | ); | ||
| 69 | |||
| 70 | expect(models.map((model) => model.slug)).toEqual(["claude-opus-4-8", "opus"]); | ||
| 71 | expect(models[1]?.isCustom).toBe(true); | ||
| 72 | }); | ||
| 55 | }); | 73 | }); |
| 56 | 74 | ||
| 57 | describe("ProviderCommandNotFoundError", () => { | 75 | describe("ProviderCommandNotFoundError", () => { |
apps/server/src/provider/providerSnapshot.ts+2-3| ... | @@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError"; | ... | @@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError"; |
| 13 | import * as Schema from "effect/Schema"; | 13 | import * as Schema from "effect/Schema"; |
| 14 | import * as Stream from "effect/Stream"; | 14 | import * as Stream from "effect/Stream"; |
| 15 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 15 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 16 | import { normalizeModelSlug } from "@t3tools/shared/model"; | 16 | import { normalizeCustomModelSlug } from "@t3tools/shared/model"; |
| 17 | import { isWindowsCommandNotFound } from "../processRunner.ts"; | 17 | import { isWindowsCommandNotFound } from "../processRunner.ts"; |
| 18 | import { createProviderVersionAdvisory } from "./providerMaintenance.ts"; | 18 | import { createProviderVersionAdvisory } from "./providerMaintenance.ts"; |
| 19 | import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; | 19 | import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; |
| ... | @@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null { | ... | @@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null { |
| 140 | 140 | ||
| 141 | export function providerModelsFromSettings( | 141 | export function providerModelsFromSettings( |
| 142 | builtInModels: ReadonlyArray<ServerProviderModel>, | 142 | builtInModels: ReadonlyArray<ServerProviderModel>, |
| 143 | provider: ProviderDriverKind, | ||
| 144 | customModels: ReadonlyArray<string>, | 143 | customModels: ReadonlyArray<string>, |
| 145 | customModelCapabilities: ModelCapabilities, | 144 | customModelCapabilities: ModelCapabilities, |
| 146 | ): ReadonlyArray<ServerProviderModel> { | 145 | ): ReadonlyArray<ServerProviderModel> { |
| ... | @@ -149,7 +148,7 @@ export function providerModelsFromSettings( | ... | @@ -149,7 +148,7 @@ export function providerModelsFromSettings( |
| 149 | const customEntries: ServerProviderModel[] = []; | 148 | const customEntries: ServerProviderModel[] = []; |
| 150 | 149 | ||
| 151 | for (const candidate of customModels) { | 150 | for (const candidate of customModels) { |
| 152 | const normalized = normalizeModelSlug(candidate, provider); | 151 | const normalized = normalizeCustomModelSlug(candidate); |
| 153 | if (!normalized || seen.has(normalized)) { | 152 | if (!normalized || seen.has(normalized)) { |
| 154 | continue; | 153 | continue; |
| 155 | } | 154 | } |
apps/web/src/components/settings/ProviderModelsSection.tsx+2-2| ... | @@ -16,7 +16,7 @@ import { | ... | @@ -16,7 +16,7 @@ import { |
| 16 | type ProviderInstanceId, | 16 | type ProviderInstanceId, |
| 17 | type ServerProviderModel, | 17 | type ServerProviderModel, |
| 18 | } from "@t3tools/contracts"; | 18 | } from "@t3tools/contracts"; |
| 19 | import { normalizeModelSlug } from "@t3tools/shared/model"; | 19 | import { normalizeCustomModelSlug } from "@t3tools/shared/model"; |
| 20 | 20 | ||
| 21 | import { cn } from "../../lib/utils"; | 21 | import { cn } from "../../lib/utils"; |
| 22 | import { sortModelsForProviderInstance } from "../../modelOrdering"; | 22 | import { sortModelsForProviderInstance } from "../../modelOrdering"; |
| ... | @@ -111,7 +111,7 @@ export function ProviderModelsSection({ | ... | @@ -111,7 +111,7 @@ export function ProviderModelsSection({ |
| 111 | }, [favoriteModelSet, modelOrder, models]); | 111 | }, [favoriteModelSet, modelOrder, models]); |
| 112 | 112 | ||
| 113 | const handleAdd = () => { | 113 | const handleAdd = () => { |
| 114 | const normalized = driverKind ? normalizeModelSlug(input, driverKind) : input.trim() || null; | 114 | const normalized = normalizeCustomModelSlug(input); |
| 115 | if (!normalized) { | 115 | if (!normalized) { |
| 116 | setError("Enter a model slug."); | 116 | setError("Enter a model slug."); |
| 117 | return; | 117 | return; |
apps/web/src/modelSelection.test.ts+33| ... | @@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => { | ... | @@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => { |
| 101 | ).toBe("openai/gpt-5.5"); | 101 | ).toBe("openai/gpt-5.5"); |
| 102 | }); | 102 | }); |
| 103 | 103 | ||
| 104 | it("preserves a custom slug that collides with a provider alias", () => { | ||
| 105 | const providers = [ | ||
| 106 | provider({ | ||
| 107 | provider: ProviderDriverKind.make("claudeAgent"), | ||
| 108 | instanceId: "claude_openrouter", | ||
| 109 | models: ["claude-opus-4-8"], | ||
| 110 | }), | ||
| 111 | ]; | ||
| 112 | const settings: UnifiedSettings = { | ||
| 113 | ...settingsWithProviderInstances(), | ||
| 114 | providerInstances: { | ||
| 115 | ...settingsWithProviderInstances().providerInstances, | ||
| 116 | [ProviderInstanceId.make("claude_openrouter")]: { | ||
| 117 | driver: ProviderDriverKind.make("claudeAgent"), | ||
| 118 | config: { customModels: ["opus"] }, | ||
| 119 | }, | ||
| 120 | }, | ||
| 121 | }; | ||
| 122 | const openrouter = deriveProviderInstanceEntries(providers)[0]!; | ||
| 123 | |||
| 124 | expect( | ||
| 125 | getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug), | ||
| 126 | ).toEqual(["claude-opus-4-8", "opus"]); | ||
| 127 | expect( | ||
| 128 | resolveAppModelSelectionForInstance( | ||
| 129 | ProviderInstanceId.make("claude_openrouter"), | ||
| 130 | settings, | ||
| 131 | providers, | ||
| 132 | "opus", | ||
| 133 | ), | ||
| 134 | ).toBe("opus"); | ||
| 135 | }); | ||
| 136 | |||
| 104 | it("includes Grok custom models from the selected provider instance", () => { | 137 | it("includes Grok custom models from the selected provider instance", () => { |
| 105 | const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })]; | 138 | const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })]; |
| 106 | const settings: UnifiedSettings = { | 139 | const settings: UnifiedSettings = { |
apps/web/src/modelSelection.ts+4-6| ... | @@ -9,7 +9,7 @@ import { | ... | @@ -9,7 +9,7 @@ import { |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { | 10 | import { |
| 11 | createModelSelection, | 11 | createModelSelection, |
| 12 | normalizeModelSlug, | 12 | normalizeCustomModelSlug, |
| 13 | resolveSelectableModel, | 13 | resolveSelectableModel, |
| 14 | } from "@t3tools/shared/model"; | 14 | } from "@t3tools/shared/model"; |
| 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; | 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; |
| ... | @@ -117,13 +117,12 @@ function applyInstanceModelPreferences( | ... | @@ -117,13 +117,12 @@ function applyInstanceModelPreferences( |
| 117 | export function normalizeCustomModelSlugs( | 117 | export function normalizeCustomModelSlugs( |
| 118 | models: Iterable<string | null | undefined>, | 118 | models: Iterable<string | null | undefined>, |
| 119 | builtInModelSlugs: ReadonlySet<string>, | 119 | builtInModelSlugs: ReadonlySet<string>, |
| 120 | provider: ProviderDriverKind = ProviderDriverKind.make("codex"), | ||
| 121 | ): string[] { | 120 | ): string[] { |
| 122 | const normalizedModels: string[] = []; | 121 | const normalizedModels: string[] = []; |
| 123 | const seen = new Set<string>(); | 122 | const seen = new Set<string>(); |
| 124 | 123 | ||
| 125 | for (const candidate of models) { | 124 | for (const candidate of models) { |
| 126 | const normalized = normalizeModelSlug(candidate, provider); | 125 | const normalized = normalizeCustomModelSlug(candidate); |
| 127 | if ( | 126 | if ( |
| 128 | !normalized || | 127 | !normalized || |
| 129 | normalized.length > MAX_CUSTOM_MODEL_LENGTH || | 128 | normalized.length > MAX_CUSTOM_MODEL_LENGTH || |
| ... | @@ -163,7 +162,7 @@ export function getAppModelOptions( | ... | @@ -163,7 +162,7 @@ export function getAppModelOptions( |
| 163 | // see the user's authored custom models. | 162 | // see the user's authored custom models. |
| 164 | const defaultInstanceId = defaultInstanceIdForDriver(provider); | 163 | const defaultInstanceId = defaultInstanceIdForDriver(provider); |
| 165 | const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider); | 164 | const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider); |
| 166 | for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, provider)) { | 165 | for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) { |
| 167 | if (seen.has(slug)) { | 166 | if (seen.has(slug)) { |
| 168 | continue; | 167 | continue; |
| 169 | } | 168 | } |
| ... | @@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance( | ... | @@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance( |
| 206 | ); | 205 | ); |
| 207 | 206 | ||
| 208 | const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind); | 207 | const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind); |
| 209 | const normalizer = entry.driverKind; | 208 | for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) { |
| 210 | for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, normalizer)) { | ||
| 211 | if (seen.has(slug)) { | 209 | if (seen.has(slug)) { |
| 212 | continue; | 210 | continue; |
| 213 | } | 211 | } |
packages/shared/src/model.test.ts+12-1| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts"; | 2 | import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | buildProviderOptionSelectionsFromDescriptors, | 5 | buildProviderOptionSelectionsFromDescriptors, |
| ... | @@ -10,6 +10,8 @@ import { | ... | @@ -10,6 +10,8 @@ import { |
| 10 | getProviderOptionDescriptors, | 10 | getProviderOptionDescriptors, |
| 11 | getProviderOptionBooleanSelectionValue, | 11 | getProviderOptionBooleanSelectionValue, |
| 12 | getProviderOptionStringSelectionValue, | 12 | getProviderOptionStringSelectionValue, |
| 13 | normalizeCustomModelSlug, | ||
| 14 | normalizeModelSlug, | ||
| 13 | } from "./model.ts"; | 15 | } from "./model.ts"; |
| 14 | 16 | ||
| 15 | const codexCaps: ModelCapabilities = createModelCapabilities({ | 17 | const codexCaps: ModelCapabilities = createModelCapabilities({ |
| ... | @@ -144,3 +146,12 @@ describe("descriptor helpers", () => { | ... | @@ -144,3 +146,12 @@ describe("descriptor helpers", () => { |
| 144 | expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true); | 146 | expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true); |
| 145 | }); | 147 | }); |
| 146 | }); | 148 | }); |
| 149 | |||
| 150 | describe("model slug normalization", () => { | ||
| 151 | it("preserves exact custom slugs instead of expanding provider aliases", () => { | ||
| 152 | const claude = ProviderDriverKind.make("claudeAgent"); | ||
| 153 | |||
| 154 | expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8"); | ||
| 155 | expect(normalizeCustomModelSlug(" opus ")).toBe("opus"); | ||
| 156 | }); | ||
| 157 | }); |
packages/shared/src/model.ts+10-5| ... | @@ -236,11 +236,7 @@ export function normalizeModelSlug( | ... | @@ -236,11 +236,7 @@ export function normalizeModelSlug( |
| 236 | model: string | null | undefined, | 236 | model: string | null | undefined, |
| 237 | provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, | 237 | provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, |
| 238 | ): string | null { | 238 | ): string | null { |
| 239 | if (typeof model !== "string") { | 239 | const trimmed = normalizeCustomModelSlug(model); |
| 240 | return null; | ||
| 241 | } | ||
| 242 | |||
| 243 | const trimmed = model.trim(); | ||
| 244 | if (!trimmed) { | 240 | if (!trimmed) { |
| 245 | return null; | 241 | return null; |
| 246 | } | 242 | } |
| ... | @@ -252,6 +248,15 @@ export function normalizeModelSlug( | ... | @@ -252,6 +248,15 @@ export function normalizeModelSlug( |
| 252 | return typeof aliased === "string" ? aliased : trimmed; | 248 | return typeof aliased === "string" ? aliased : trimmed; |
| 253 | } | 249 | } |
| 254 | 250 | ||
| 251 | /** Custom model identifiers are provider-owned, so only trim them; never expand aliases. */ | ||
| 252 | export function normalizeCustomModelSlug(model: string | null | undefined): string | null { | ||
| 253 | if (typeof model !== "string") { | ||
| 254 | return null; | ||
| 255 | } | ||
| 256 | |||
| 257 | return model.trim() || null; | ||
| 258 | } | ||
| 259 | |||
| 255 | export function resolveSelectableModel( | 260 | export function resolveSelectableModel( |
| 256 | provider: ProviderDriverKind, | 261 | provider: ProviderDriverKind, |
| 257 | value: string | null | undefined, | 262 | value: string | null | undefined, |