diff --git a/apps/mobile/package.json b/apps/mobile/package.json index bdb4c8ebfbca..e91ec70d0ba1 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -73,14 +73,6 @@ "@t3tools/mobile-review-diff-native": "file:./modules/t3-review-diff", "@t3tools/mobile-source-text": "file:./modules/t3-source-text", "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal", - "@t3tools/provider-acp-registry": "workspace:*", - "@t3tools/provider-core": "workspace:*", - "@t3tools/provider-cursor": "workspace:*", - "@t3tools/provider-fx": "workspace:*", - "@t3tools/provider-grok": "workspace:*", - "@t3tools/provider-muse": "workspace:*", - "@t3tools/provider-opencode": "workspace:*", - "@t3tools/provider-pi": "workspace:*", "@t3tools/shared": "workspace:*", "@tabler/icons-react-native": "^3.44.0", "clsx": "^2.1.1", diff --git a/apps/mobile/src/components/ComposerModelControl.tsx b/apps/mobile/src/components/ComposerModelControl.tsx index 431cf61b1ec6..e422cfabd459 100644 --- a/apps/mobile/src/components/ComposerModelControl.tsx +++ b/apps/mobile/src/components/ComposerModelControl.tsx @@ -70,7 +70,7 @@ export function ComposerModelControl(props: { /> ) } - label={label} + label={props.modelOption?.shortName ?? label} iconOnly={props.iconOnly} suffix={ summary ? ( diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 248594ab8655..1fe46085d9e8 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -10,7 +10,7 @@ import { useId, useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; import { AppText as Text } from "./ui/AppText"; -import { getProviderClient } from "../lib/providerClients"; +import { getProviderClient } from "@t3tools/client-runtime/provider-clients"; type ProviderIconProps = { readonly provider: string | null | undefined; diff --git a/apps/mobile/src/features/threads/thread-handoff-row.tsx b/apps/mobile/src/features/threads/thread-handoff-row.tsx index 4cb04fa6b3ec..7e9698c057aa 100644 --- a/apps/mobile/src/features/threads/thread-handoff-row.tsx +++ b/apps/mobile/src/features/threads/thread-handoff-row.tsx @@ -2,6 +2,7 @@ import * as Dialog from "../../components/ui/Dialog"; import { useAtomValue } from "@effect/atom-react"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { resolveHandoffEndpoints, type HandoffEndpoints } from "@t3tools/client-runtime/handoff"; +import { resolveModelShortName } from "@t3tools/client-runtime/provider-clients"; import { normalizeProviderAccentColor, resolveProviderInstanceDisplayName, @@ -136,7 +137,7 @@ function HandoffEndpoint(props: { const provider = props.providers.find((candidate) => candidate.instanceId === props.instanceId); const model = provider?.models.find((candidate) => candidate.slug === props.model); const label = - model?.shortName ?? + (model ? resolveModelShortName(model, provider?.driver) : undefined) ?? model?.name ?? props.model ?? (provider ? resolveProviderInstanceDisplayName(provider) : props.instanceId); diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index c36c63a4f145..930d25beb73e 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -11,15 +11,17 @@ import type { RuntimeMode, ServerConfig as T3ServerConfig, } from "@t3tools/contracts"; -import { resolveProjectProviderInstanceEnabled } from "@t3tools/contracts"; +import { isProviderDriverKind, resolveProjectProviderInstanceEnabled } from "@t3tools/contracts"; import { buildExplicitProviderOptionSelectionsFromDescriptors, getProviderOptionDescriptors, } from "@t3tools/shared/model"; +import { resolveModelShortName } from "@t3tools/client-runtime/provider-clients"; export type ModelOption = { readonly key: string; readonly label: string; + readonly shortName?: string; readonly subtitle: string; readonly providerKey: string; readonly providerLabel: string; @@ -319,9 +321,11 @@ export function buildModelOptions( models.sort((a, b) => (order.get(a.slug) ?? Infinity) - (order.get(b.slug) ?? Infinity)); for (const model of models) { const key = `${provider.instanceId}:${model.slug}`; + const shortName = resolveModelShortName(model, provider.driver); options.set(key, { key, label: model.name, + ...(shortName ? { shortName } : {}), subtitle: model.subProvider ?? "", providerKey: provider.instanceId, providerLabel, @@ -384,9 +388,13 @@ export function buildModelOptions( displayName: provider?.displayName ?? instanceConfig?.displayName, instanceId: fallbackModelSelection.instanceId, }); + const shortName = model + ? resolveModelShortName(model, isProviderDriverKind(providerDriver) ? providerDriver : null) + : undefined; options.set(key, { key, label: model?.name ?? fallbackModelSelection.model, + ...(shortName ? { shortName } : {}), subtitle: model?.subProvider ?? "", providerKey: fallbackModelSelection.instanceId, providerLabel, diff --git a/apps/mobile/src/lib/providerClients.ts b/apps/mobile/src/lib/providerClients.ts deleted file mode 100644 index 2a62b5fdc46f..000000000000 --- a/apps/mobile/src/lib/providerClients.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isProviderDriverKind } from "@t3tools/contracts"; -import { acpRegistryClient } from "@t3tools/provider-acp-registry/client"; -import { makeProviderClientRegistry } from "@t3tools/provider-core/client"; -import { cursorClient } from "@t3tools/provider-cursor/client"; -import { fxClient } from "@t3tools/provider-fx/client"; -import { grokClient } from "@t3tools/provider-grok/client"; -import { museClient } from "@t3tools/provider-muse/client"; -import { openCodeClient } from "@t3tools/provider-opencode/client"; -import { piClient } from "@t3tools/provider-pi/client"; - -const providerClients = makeProviderClientRegistry([ - cursorClient, - grokClient, - fxClient, - museClient, - openCodeClient, - piClient, - acpRegistryClient, -]); - -export function getProviderClient(driver: string | null | undefined) { - return isProviderDriverKind(driver) ? providerClients.get(driver) : undefined; -} diff --git a/apps/web/package.json b/apps/web/package.json index 9ab88beb3d7f..de75da24e380 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,14 +34,7 @@ "@pierre/trees": "1.0.0-beta.6", "@t3tools/client-runtime": "workspace:*", "@t3tools/contracts": "workspace:*", - "@t3tools/provider-acp-registry": "workspace:*", "@t3tools/provider-core": "workspace:*", - "@t3tools/provider-cursor": "workspace:*", - "@t3tools/provider-fx": "workspace:*", - "@t3tools/provider-grok": "workspace:*", - "@t3tools/provider-muse": "workspace:*", - "@t3tools/provider-opencode": "workspace:*", - "@t3tools/provider-pi": "workspace:*", "@t3tools/shared": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-router": "^1.160.2", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f0b6877c42e7..5369f2158e6e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1628,7 +1628,7 @@ export default function ChatView(props: ChatViewProps) { (model) => model.slug === providerSubagentModelSlug, ); const providerSubagentModelLabel = providerSubagentCatalogModel - ? getTriggerDisplayModelName(providerSubagentCatalogModel) + ? getTriggerDisplayModelName(providerSubagentCatalogModel, selectedProviderEntry?.driverKind) : formatModelSlugName(activeThread?.modelSelection.model ?? ""); const providerSubagentEffortLabel = activeThread === undefined diff --git a/apps/web/src/components/chat/ContextWindowMeter.logic.test.ts b/apps/web/src/components/chat/ContextWindowMeter.logic.test.ts index 670ccd541bdb..e8d08570707b 100644 --- a/apps/web/src/components/chat/ContextWindowMeter.logic.test.ts +++ b/apps/web/src/components/chat/ContextWindowMeter.logic.test.ts @@ -99,7 +99,7 @@ describe("resolveContextWindowModelDisplayName", () => { }, modelOptionsByInstance, ), - ).toBe("5.6 Sol"); + ).toBe("GPT-5.6 Sol"); }); }); diff --git a/apps/web/src/components/chat/ContextWindowMeter.logic.ts b/apps/web/src/components/chat/ContextWindowMeter.logic.ts index c6bcfba0b00a..5a0911a10a05 100644 --- a/apps/web/src/components/chat/ContextWindowMeter.logic.ts +++ b/apps/web/src/components/chat/ContextWindowMeter.logic.ts @@ -8,7 +8,7 @@ import { resolveSelectableProviderInstanceEntry, type ProviderInstanceEntry, } from "../../providerInstances"; -import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils"; +import { getDisplayModelName, type ModelEsque } from "./providerIconUtils"; // Fallback TTL: Claude's 1h cache plus slack. const CLAUDE_RESUME_COMPACTION_FALLBACK_MS = 70 * 60_000; @@ -96,7 +96,7 @@ export function resolveContextWindowModelDisplayName( .get(selection.instanceId) ?.find((model) => model.slug === selection.model); - return selectedModel ? getTriggerDisplayModelName(selectedModel) : selection.model; + return selectedModel ? getDisplayModelName(selectedModel) : selection.model; } export function formatContextWindowCompactionMessage( diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx index 5c35d78e3886..1d318443e089 100644 --- a/apps/web/src/components/chat/ModelListRow.tsx +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -1,11 +1,7 @@ import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts"; import { memo } from "react"; import { CheckIcon, StarIcon } from "lucide-react"; -import { - getDisplayModelName, - getProviderRowLabel, - getTriggerDisplayModelLabel, -} from "./providerIconUtils"; +import { getDisplayModelName, getProviderRowLabel } from "./providerIconUtils"; import { ComboboxItem } from "../ui/combobox"; import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; @@ -19,7 +15,6 @@ export const ModelListRow = memo(function ModelListRow(props: { index: number; slug: string; name: string; - shortName?: string | undefined; subProvider?: string | undefined; instanceId: ProviderInstanceId; driverKind: ProviderDriverKind; @@ -30,8 +25,6 @@ export const ModelListRow = memo(function ModelListRow(props: { isFavorite: boolean; isSelected: boolean; showProvider: boolean; - preferShortName?: boolean; - useTriggerLabel?: boolean; showNewBadge?: boolean; unavailable?: boolean; jumpLabel?: string | null; @@ -41,7 +34,6 @@ export const ModelListRow = memo(function ModelListRow(props: { const model = { slug: props.slug, name: props.name, - shortName: props.shortName, subProvider: props.subProvider, }; const providerLabel = getProviderRowLabel(props.providerDisplayName, props.subProvider); @@ -61,12 +53,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
- {props.useTriggerLabel - ? getTriggerDisplayModelLabel(model) - : getDisplayModelName( - model, - props.preferShortName ? { preferShortName: true } : undefined, - )} + {getDisplayModelName(model)}
{props.showNewBadge ? ( 2 ? `, ${selectedEntries.length - 2} more` : ""}` : undefined; const allModelNames = selectedEntries - ? selectedEntries.map((selection) => selection.label).join(", ") || "Choose models" + ? selectedEntries.map((selection) => selection.fullLabel).join(", ") || "Choose models" : undefined; const triggerTooltipContent = shortcutLabel ? `${props.triggerLabel ?? allModelNames ?? triggerLabel} · ${shortcutLabel}` @@ -193,7 +191,9 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { candidate.slug === model); const label = providerModel !== undefined - ? getTriggerDisplayModelName(providerModel) + ? getTriggerDisplayModelName(providerModel, entry?.driverKind) : model !== undefined && model.length > 0 ? model : (entry?.displayName ?? props.instanceId); diff --git a/apps/web/src/components/chat/providerIconUtils.ts b/apps/web/src/components/chat/providerIconUtils.ts index 9ab57d4f3d70..3640faefa5c3 100644 --- a/apps/web/src/components/chat/providerIconUtils.ts +++ b/apps/web/src/components/chat/providerIconUtils.ts @@ -1,3 +1,6 @@ +import type { ProviderDriverKind } from "@t3tools/contracts"; +import { resolveModelShortName } from "@t3tools/client-runtime/provider-clients"; + export type ModelEsque = { slug: string; name: string; @@ -47,10 +50,10 @@ export function getProviderRowLabel( return `${providerDisplayName} · ${sub}`; } -export function getTriggerDisplayModelName(model: ModelEsque): string { - return getDisplayModelName(model, { preferShortName: true }); -} - -export function getTriggerDisplayModelLabel(model: ModelEsque): string { - return getTriggerDisplayModelName(model); +export function getTriggerDisplayModelName( + model: ModelEsque, + driverKind: ProviderDriverKind | null | undefined, +): string { + const shortName = resolveModelShortName(model, driverKind); + return getDisplayModelName({ ...model, shortName }, { preferShortName: true }); } diff --git a/apps/web/src/components/guest/GuestPage.browser.test.tsx b/apps/web/src/components/guest/GuestPage.browser.test.tsx index f3668478edb8..bd29b183299b 100644 --- a/apps/web/src/components/guest/GuestPage.browser.test.tsx +++ b/apps/web/src/components/guest/GuestPage.browser.test.tsx @@ -246,7 +246,7 @@ describe("GuestPage", () => { expect(screen.container.querySelector('[aria-label="Fork from this response"]')).toBeNull(); expect(socket).not.toHaveBeenCalled(); // The composer names the thread's model, as the owner's does, with nothing to pick. - await expect.element(screen.getByLabelText("Model: 5.4")).toBeVisible(); + await expect.element(screen.getByLabelText("Model: GPT-5.4")).toBeVisible(); }); it("sends attached files with the message, as form data", async () => { diff --git a/apps/web/src/components/guest/GuestPage.tsx b/apps/web/src/components/guest/GuestPage.tsx index 2314fab42d18..e860920692ff 100644 --- a/apps/web/src/components/guest/GuestPage.tsx +++ b/apps/web/src/components/guest/GuestPage.tsx @@ -49,7 +49,7 @@ import { PierreEntryIcon } from "../chat/PierreEntryIcon"; import { GuestMessageName, guestBubbleClassName } from "../chat/guestMessageStyle"; import { MessagesTimeline } from "../chat/MessagesTimeline"; import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; -import { getTriggerDisplayModelName } from "../chat/providerIconUtils"; +import { getDisplayModelName, getTriggerDisplayModelName } from "../chat/providerIconUtils"; import { ScrollToEndButton } from "../chat/TimelineOverlays"; import type { TimelineRow } from "../chat/timelineRowContext"; import { useTimelineFollow } from "../chat/useTimelineFollow"; @@ -557,11 +557,12 @@ function UnplacedGuestMessage({ /** The thread's model, labeled as the owner's composer labels it, with no way to change it. */ function GuestModelLabel({ model }: { readonly model: GuestThreadModel }) { - const label = getTriggerDisplayModelName({ slug: model.name, ...model }); + const label = getTriggerDisplayModelName({ slug: model.name, ...model }, model.driver); + const fullLabel = getDisplayModelName({ slug: model.name, ...model }); return ( } - aria-label={`Model: ${label}`} + aria-label={`Model: ${fullLabel}`} className="pointer-events-none min-w-0 shrink" > {model.driver === null ? null : ( diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts index 552fc0d2867e..3b7f0ff6bb25 100644 --- a/apps/web/src/components/settings/providerDriverMeta.ts +++ b/apps/web/src/components/settings/providerDriverMeta.ts @@ -1,42 +1 @@ -import { - AntigravitySettings, - ClaudeSettings, - CodexSettings, - ProviderDriverKind, -} from "@t3tools/contracts"; -import { acpRegistryClient } from "@t3tools/provider-acp-registry/client"; -import { makeProviderClientRegistry } from "@t3tools/provider-core/client"; -import { cursorClient } from "@t3tools/provider-cursor/client"; -import { fxClient } from "@t3tools/provider-fx/client"; -import { grokClient } from "@t3tools/provider-grok/client"; -import { museClient } from "@t3tools/provider-muse/client"; -import { openCodeClient } from "@t3tools/provider-opencode/client"; -import { piClient } from "@t3tools/provider-pi/client"; - -export const providerClients = makeProviderClientRegistry([ - { - driverKind: ProviderDriverKind.make("codex"), - label: "Codex", - acpRegistryAgentIds: ["codex-acp"], - settingsSchema: CodexSettings, - }, - { - driverKind: ProviderDriverKind.make("claudeAgent"), - label: "Claude", - acpRegistryAgentIds: ["claude-acp"], - settingsSchema: ClaudeSettings, - }, - cursorClient, - grokClient, - fxClient, - openCodeClient, - { - driverKind: ProviderDriverKind.make("antigravity"), - label: "Antigravity", - acpRegistryAgentIds: ["antigravity-acp"], - settingsSchema: AntigravitySettings, - }, - museClient, - piClient, - acpRegistryClient, -]); +export { providerClients } from "@t3tools/client-runtime/provider-clients"; diff --git a/apps/web/src/components/sidebar/SidebarThreadRow.tsx b/apps/web/src/components/sidebar/SidebarThreadRow.tsx index 449be6dff7aa..b087e3d1e548 100644 --- a/apps/web/src/components/sidebar/SidebarThreadRow.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadRow.tsx @@ -104,7 +104,7 @@ import { ProjectFavicon, type ProjectFaviconProject } from "../ProjectFavicon"; import { ThreadSearchMatchExcerpt } from "../ThreadSearchMatch"; import { makeWorkspaceFileDropHandlers } from "../chat/workspaceFileDrop"; import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; -import { getTriggerDisplayModelLabel } from "../chat/providerIconUtils"; +import { getTriggerDisplayModelName } from "../chat/providerIconUtils"; import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances"; import { useThreadRunningTerminalIds } from "../../state/terminalSessions"; import { InlineButton } from "../ui/button"; @@ -771,7 +771,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP (model) => model.slug === thread.modelSelection.model, ); const modelLabel = selectedModel - ? getTriggerDisplayModelLabel(selectedModel) + ? getTriggerDisplayModelName(selectedModel, providerEntry?.driverKind) : thread.modelSelection.model; const isRemote = thread.environmentId !== props.currentEnvironmentId; @@ -1602,7 +1602,7 @@ export const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props (model) => model.slug === thread.modelSelection.model, ); const modelLabel = selectedModel - ? getTriggerDisplayModelLabel(selectedModel) + ? getTriggerDisplayModelName(selectedModel, providerEntry?.driverKind) : thread.modelSelection.model; const runningTerminalIds = useThreadRunningTerminalIds({ environmentId: thread.environmentId, diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 9028dd4e39da..db52028c397e 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -31,6 +31,7 @@ import { NO_PROVIDER_MODEL_SELECTION, } from "./providerInstances"; import { sortModelsForProviderInstance } from "./modelOrdering"; +import { resolveModelShortName } from "@t3tools/client-runtime/provider-clients"; export const MAX_CUSTOM_MODEL_LENGTH = 256; const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("codex"); @@ -85,13 +86,17 @@ function appendUnavailableDynamicModelSelection( return [...options, { slug, name: slug, isCustom: false, isUnavailable: true }]; } -function toAppModelOption(model: ServerProvider["models"][number]): AppModelOption { +function toAppModelOption( + model: ServerProvider["models"][number], + driverKind: ProviderDriverKind, +): AppModelOption { const option: AppModelOption = { slug: model.slug, name: model.name, isCustom: model.isCustom, }; - if (model.shortName) option.shortName = model.shortName; + const shortName = resolveModelShortName(model, driverKind); + if (shortName) option.shortName = shortName; if (model.subProvider) option.subProvider = model.subProvider; if (model.aliases) option.aliases = model.aliases; if (model.badge) option.badge = model.badge; @@ -159,7 +164,7 @@ function getAppModelOptions( // Server custom-model snapshots can lag removals; rebuild custom rows from settings. const options: AppModelOption[] = rawModels .filter((model) => !model.isCustom) - .map(toAppModelOption); + .map((model) => toAppModelOption(model, provider)); const seen = new Set(options.map((option) => option.slug)); const builtInModelSlugs = new Set( Arr.filterMap(getProviderModels(providers, provider), (model) => @@ -175,7 +180,13 @@ function getAppModelOptions( } seen.add(entry.slug); - options.push({ slug: entry.slug, name: entry.name, isCustom: true }); + const shortName = resolveModelShortName(entry, provider); + options.push({ + slug: entry.slug, + name: entry.name, + isCustom: true, + ...(shortName ? { shortName } : {}), + }); } const preferences = readInstanceModelPreferences(settings, defaultInstanceId); @@ -195,7 +206,7 @@ export function getAppModelOptionsForInstance( ): AppModelOption[] { const options: AppModelOption[] = entry.models .filter((model) => !model.isCustom) - .map(toAppModelOption); + .map((model) => toAppModelOption(model, entry.driverKind)); const seen = new Set(options.map((option) => option.slug)); const builtInModelSlugs = new Set( Arr.filterMap(entry.models, (model) => @@ -210,7 +221,13 @@ export function getAppModelOptionsForInstance( } seen.add(custom.slug); - options.push({ slug: custom.slug, name: custom.name, isCustom: true }); + const shortName = resolveModelShortName(custom, entry.driverKind); + options.push({ + slug: custom.slug, + name: custom.name, + isCustom: true, + ...(shortName ? { shortName } : {}), + }); } const preferences = readInstanceModelPreferences(settings, entry.instanceId); diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index 03779b627eea..9719f5936078 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -478,6 +478,10 @@ "./model-traits": { "types": "./src/modelTraits.ts", "default": "./src/modelTraits.ts" + }, + "./provider-clients": { + "types": "./src/providerClients.ts", + "default": "./src/providerClients.ts" } }, "scripts": { @@ -487,6 +491,14 @@ "dependencies": { "@pierre/diffs": "catalog:", "@t3tools/contracts": "workspace:*", + "@t3tools/provider-acp-registry": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/provider-cursor": "workspace:*", + "@t3tools/provider-fx": "workspace:*", + "@t3tools/provider-grok": "workspace:*", + "@t3tools/provider-muse": "workspace:*", + "@t3tools/provider-opencode": "workspace:*", + "@t3tools/provider-pi": "workspace:*", "@t3tools/shared": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-definitions": "workspace:*", diff --git a/packages/client-runtime/src/providerClients.ts b/packages/client-runtime/src/providerClients.ts new file mode 100644 index 000000000000..37cd15209614 --- /dev/null +++ b/packages/client-runtime/src/providerClients.ts @@ -0,0 +1,60 @@ +import { + AntigravitySettings, + ClaudeSettings, + CodexSettings, + isProviderDriverKind, + ProviderDriverKind, +} from "@t3tools/contracts"; +import { acpRegistryClient } from "@t3tools/provider-acp-registry/client"; +import { makeProviderClientRegistry } from "@t3tools/provider-core/client"; +import { modelShortName } from "@t3tools/provider-core/modelShortName"; +import { cursorClient } from "@t3tools/provider-cursor/client"; +import { fxClient } from "@t3tools/provider-fx/client"; +import { grokClient } from "@t3tools/provider-grok/client"; +import { museClient } from "@t3tools/provider-muse/client"; +import { openCodeClient } from "@t3tools/provider-opencode/client"; +import { piClient } from "@t3tools/provider-pi/client"; + +export const providerClients = makeProviderClientRegistry([ + { + driverKind: ProviderDriverKind.make("codex"), + label: "Codex", + modelFamilies: ["GPT-", "GPT "], + acpRegistryAgentIds: ["codex-acp"], + settingsSchema: CodexSettings, + }, + { + driverKind: ProviderDriverKind.make("claudeAgent"), + label: "Claude", + modelFamilies: ["Claude "], + acpRegistryAgentIds: ["claude-acp"], + settingsSchema: ClaudeSettings, + }, + cursorClient, + grokClient, + fxClient, + openCodeClient, + { + driverKind: ProviderDriverKind.make("antigravity"), + label: "Antigravity", + modelFamilies: ["Gemini "], + acpRegistryAgentIds: ["antigravity-acp"], + settingsSchema: AntigravitySettings, + }, + museClient, + piClient, + acpRegistryClient, +]); + +export function getProviderClient(driver: string | null | undefined) { + return isProviderDriverKind(driver) ? providerClients.get(driver) : undefined; +} + +export function resolveModelShortName( + model: { readonly name: string; readonly shortName?: string | undefined }, + driverKind: ProviderDriverKind | null | undefined, +): string | undefined { + if (model.shortName) return model.shortName; + const families = driverKind ? providerClients.get(driverKind)?.modelFamilies : undefined; + return modelShortName(model.name, families ?? []); +} diff --git a/packages/client-runtime/src/state/subagentDisplay.ts b/packages/client-runtime/src/state/subagentDisplay.ts index d4d0db34e2ac..295e557fc8ff 100644 --- a/packages/client-runtime/src/state/subagentDisplay.ts +++ b/packages/client-runtime/src/state/subagentDisplay.ts @@ -6,6 +6,7 @@ import type { } from "@t3tools/contracts"; import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model"; import { fileBasename } from "@t3tools/shared/path"; +import { resolveModelShortName } from "../providerClients.ts"; import { isTerminalSubagentStatus } from "./subagentRuntime.ts"; export function subagentGroupSummary( @@ -70,7 +71,7 @@ export function resolveSubagentMetadata(input: { : model; const catalogModel = input.provider?.models.find((candidate) => candidate.slug === slug); const reportedLabel = catalogModel - ? catalogModel.shortName || catalogModel.name + ? (resolveModelShortName(catalogModel, input.provider?.driver) ?? catalogModel.name) : model ? formatModelSlugName(model) : "Not reported"; diff --git a/packages/provider-core/package.json b/packages/provider-core/package.json index c45de9a9b149..ab93310316ac 100644 --- a/packages/provider-core/package.json +++ b/packages/provider-core/package.json @@ -7,6 +7,10 @@ "types": "./src/client.ts", "import": "./src/client.ts" }, + "./modelShortName": { + "types": "./src/modelShortName.ts", + "import": "./src/modelShortName.ts" + }, "./server/adapterDriver": { "types": "./src/server/adapterDriver.ts", "import": "./src/server/adapterDriver.ts" diff --git a/packages/provider-core/src/client.ts b/packages/provider-core/src/client.ts index cba77d33a6ab..646c5e1871ef 100644 --- a/packages/provider-core/src/client.ts +++ b/packages/provider-core/src/client.ts @@ -36,6 +36,8 @@ export interface ProviderClientDefinition { readonly driverKind: ProviderDriverKind; readonly label: string; readonly icon?: ProviderIcon; + /** Model-name family prefixes the provider's own icon already shows, e.g. "Claude ". */ + readonly modelFamilies?: ReadonlyArray; readonly settingsSchema: ProviderSettingsSchema; readonly environmentFields?: ReadonlyArray; readonly hasDefaultInstance?: boolean; diff --git a/packages/provider-core/src/modelShortName.test.ts b/packages/provider-core/src/modelShortName.test.ts new file mode 100644 index 000000000000..b58e2b32e935 --- /dev/null +++ b/packages/provider-core/src/modelShortName.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { modelShortName } from "./modelShortName.ts"; + +describe("modelShortName", () => { + it.each([ + ["Claude Opus 5.5", ["Claude "], "Opus 5.5"], + ["GPT-6.1-Sol", ["GPT-"], "6.1 Sol"], + ["GPT 6.0 Astra", ["GPT-", "GPT "], "6.0 Astra"], + ["Grok 4 Pro", [], undefined], + ["Claude Opus 5.5", [], "Opus 5.5"], + ["NVIDIA: Nemotron 3 Super", [], "Nemotron 3 Super"], + ["GPT-5.5", ["GPT-"], undefined], + ["Gemini 3.1 Pro", ["Gemini "], "3.1 Pro"], + ] as const)("gives %s on families %j the short name %s", (name, families, expected) => { + expect(modelShortName(name, families)).toBe(expected); + }); +}); diff --git a/packages/provider-core/src/modelShortName.ts b/packages/provider-core/src/modelShortName.ts new file mode 100644 index 000000000000..1be593e6eb85 --- /dev/null +++ b/packages/provider-core/src/modelShortName.ts @@ -0,0 +1,71 @@ +const FAMILY_PREFIXES = ["Claude ", "GPT-", "GPT ", "Gemini ", "Muse ", "Grok "] as const; + +// Words that describe a variant within a family rather than name a model. Off the +// family's native provider there is no icon saying which family "4 Pro" belongs to. +const VARIANT_WORDS = new Set([ + "pro", + "flash", + "mini", + "max", + "lite", + "nano", + "ultra", + "plus", + "fast", + "turbo", + "preview", + "latest", + "thinking", + "reasoning", + "build", + "code", + "image", + "vision", + "audio", + "live", + "custom", + "tools", + "exp", + "experimental", + "chat", + "tts", + "stt", + "non", + "transcribe", +]); + +const VERSION = /^v?\d+(?:\.\d+)*[a-z]?$/i; + +const VENDOR_PREFIX = /^[A-Z][\w.&\- ]{0,24}: (?=\S)/; + +export function modelShortName( + name: string, + nativeFamilies: ReadonlyArray, +): string | undefined { + const unprefixed = name.replace(VENDOR_PREFIX, ""); + const short = + withoutFamily(unprefixed, nativeFamilies) ?? (namesModel(unprefixed) ? unprefixed : name); + return short === name ? undefined : short; +} + +function withoutFamily(name: string, nativeFamilies: ReadonlyArray): string | undefined { + const prefix = FAMILY_PREFIXES.find((candidate) => name.startsWith(candidate)); + if (!prefix) return undefined; + const rest = name.slice(prefix.length).trim(); + const keep = nativeFamilies.includes(prefix) + ? nonVersionWords(rest).length > 0 + : namesModel(rest); + if (!keep) return undefined; + return /\s/.test(rest) ? rest : rest.replaceAll("-", " "); +} + +function nonVersionWords(text: string): ReadonlyArray { + return text + .replace(/\(.*?\)/g, " ") + .split(/[\s-]+/) + .filter((word) => word.length > 0 && !VERSION.test(word)); +} + +function namesModel(text: string): boolean { + return nonVersionWords(text).some((word) => !VARIANT_WORDS.has(word.toLowerCase())); +} diff --git a/packages/provider-cursor/src/client.ts b/packages/provider-cursor/src/client.ts index 158133d54c82..720b19de9542 100644 --- a/packages/provider-cursor/src/client.ts +++ b/packages/provider-cursor/src/client.ts @@ -6,6 +6,7 @@ import { CursorSettings } from "./settings.ts"; export const cursorClient = defineProviderClient({ driverKind: ProviderDriverKind.make("cursor"), label: "Cursor", + modelFamilies: ["Grok "], acpRegistryAgentIds: ["cursor"], settingsSchema: CursorSettings, environmentFields: [ diff --git a/packages/provider-grok/src/client.ts b/packages/provider-grok/src/client.ts index 42a4e7fbc177..d9df069b68d8 100644 --- a/packages/provider-grok/src/client.ts +++ b/packages/provider-grok/src/client.ts @@ -6,6 +6,7 @@ import { GrokSettings } from "./settings.ts"; export const grokClient = defineProviderClient({ driverKind: ProviderDriverKind.make("grok"), label: "Grok", + modelFamilies: ["Grok "], acpRegistryAgentIds: ["grok-build"], settingsSchema: GrokSettings, icon: { diff --git a/packages/provider-muse/src/client.ts b/packages/provider-muse/src/client.ts index 2d35baf20a9c..ff2d1a1ac100 100644 --- a/packages/provider-muse/src/client.ts +++ b/packages/provider-muse/src/client.ts @@ -6,6 +6,7 @@ import { MuseSettings } from "./settings.ts"; export const museClient = defineProviderClient({ driverKind: ProviderDriverKind.make("muse"), label: "Muse Code", + modelFamilies: ["Muse "], settingsSchema: MuseSettings, icon: { viewBox: "0 0 287.56 191", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae138a46cd08..0f997881d1ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -579,30 +579,6 @@ importers: '@t3tools/mobile-terminal-native': specifier: file:./modules/t3-terminal version: file:apps/mobile/modules/t3-terminal - '@t3tools/provider-acp-registry': - specifier: workspace:* - version: link:../../packages/provider-acp-registry - '@t3tools/provider-core': - specifier: workspace:* - version: link:../../packages/provider-core - '@t3tools/provider-cursor': - specifier: workspace:* - version: link:../../packages/provider-cursor - '@t3tools/provider-fx': - specifier: workspace:* - version: link:../../packages/provider-fx - '@t3tools/provider-grok': - specifier: workspace:* - version: link:../../packages/provider-grok - '@t3tools/provider-muse': - specifier: workspace:* - version: link:../../packages/provider-muse - '@t3tools/provider-opencode': - specifier: workspace:* - version: link:../../packages/provider-opencode - '@t3tools/provider-pi': - specifier: workspace:* - version: link:../../packages/provider-pi '@t3tools/shared': specifier: workspace:* version: link:../../packages/shared @@ -1061,30 +1037,9 @@ importers: '@t3tools/contracts': specifier: workspace:* version: link:../../packages/contracts - '@t3tools/provider-acp-registry': - specifier: workspace:* - version: link:../../packages/provider-acp-registry '@t3tools/provider-core': specifier: workspace:* version: link:../../packages/provider-core - '@t3tools/provider-cursor': - specifier: workspace:* - version: link:../../packages/provider-cursor - '@t3tools/provider-fx': - specifier: workspace:* - version: link:../../packages/provider-fx - '@t3tools/provider-grok': - specifier: workspace:* - version: link:../../packages/provider-grok - '@t3tools/provider-muse': - specifier: workspace:* - version: link:../../packages/provider-muse - '@t3tools/provider-opencode': - specifier: workspace:* - version: link:../../packages/provider-opencode - '@t3tools/provider-pi': - specifier: workspace:* - version: link:../../packages/provider-pi '@t3tools/shared': specifier: workspace:* version: link:../../packages/shared @@ -1444,6 +1399,30 @@ importers: '@t3tools/contracts': specifier: workspace:* version: link:../contracts + '@t3tools/provider-acp-registry': + specifier: workspace:* + version: link:../provider-acp-registry + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/provider-cursor': + specifier: workspace:* + version: link:../provider-cursor + '@t3tools/provider-fx': + specifier: workspace:* + version: link:../provider-fx + '@t3tools/provider-grok': + specifier: workspace:* + version: link:../provider-grok + '@t3tools/provider-muse': + specifier: workspace:* + version: link:../provider-muse + '@t3tools/provider-opencode': + specifier: workspace:* + version: link:../provider-opencode + '@t3tools/provider-pi': + specifier: workspace:* + version: link:../provider-pi '@t3tools/shared': specifier: workspace:* version: link:../shared