diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index 25171dc5bd1f4..1bb6c9dde0d4a 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -5,6 +5,7 @@ import { type ProviderInstanceId, type ServerProviderModel, } from "@t3tools/contracts"; +import { REASONING_SELECT_DESCRIPTOR_IDS } from "@t3tools/client-runtime/model-traits"; import { type CustomModelDefinition, normalizeCustomModelSlug } from "@t3tools/shared/model"; import { useStableCallback } from "../../hooks/useStableCallback"; @@ -27,7 +28,21 @@ const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial + REASONING_SELECT_DESCRIPTOR_IDS.has(descriptor.id), + ); +} + +// Reasoning is the norm, so only its absence is labeled, and only when other models have it. +// Custom and null capabilities are fallbacks or user-written, not discovered, so they prove nothing. +// Alias slugs ("default", Cursor's "auto") defer to the provider's own model choice, so reasoning is unknown. +function describeModelCapabilities( + model: ServerProviderModel, + flagMissingReasoning: boolean, +): string[] { const descriptors = model.capabilities?.optionDescriptors ?? []; const labels: string[] = []; const hasFastMode = descriptors.some( @@ -38,18 +53,14 @@ function describeModelCapabilities(model: ServerProviderModel): string[] { descriptor.options.some((option) => option.id === "fast" || option.label === "Fast")), ); if (hasFastMode) labels.push("Fast mode"); - if (descriptors.some((descriptor) => descriptor.id === "thinking")) labels.push("Thinking"); if ( - descriptors.some( - (descriptor) => - descriptor.type === "select" && - (descriptor.id === "reasoningEffort" || - descriptor.id === "effort" || - descriptor.id === "reasoning" || - descriptor.id === "variant"), - ) + flagMissingReasoning && + !model.isCustom && + model.capabilities && + !PROVIDER_ALIAS_SLUGS.has(model.slug) && + !hasReasoningControl(model) ) { - labels.push("Reasoning"); + labels.push("No reasoning"); } return labels; } @@ -144,6 +155,7 @@ export function ProviderModelsSection({ const hiddenCount = displayModels.filter( (model) => !model.isCustom && hiddenModelSet.has(model.slug), ).length; + const flagMissingReasoning = models.some(hasReasoningControl); const showFilter = models.length > FILTER_THRESHOLD; const normalizedFilter = filter.trim().toLowerCase(); const isFiltering = showFilter && normalizedFilter.length > 0; @@ -278,6 +290,7 @@ export function ProviderModelsSection({ canMoveUp={!isFiltering && previousModel !== undefined && groupOf(previousModel) === group} canMoveDown={!isFiltering && nextModel !== undefined && groupOf(nextModel) === group} isFiltering={isFiltering} + flagMissingReasoning={flagMissingReasoning} canWritePreferences={canWritePreferences} canManageCustomModels={canManageCustomModels} {...rowHandlers} @@ -437,6 +450,7 @@ function ProviderModelRow({ canMoveUp, canMoveDown, isFiltering, + flagMissingReasoning, canWritePreferences, canManageCustomModels, onToggleFavorite, @@ -451,6 +465,7 @@ function ProviderModelRow({ readonly canMoveUp: boolean; readonly canMoveDown: boolean; readonly isFiltering: boolean; + readonly flagMissingReasoning: boolean; readonly canWritePreferences: boolean; readonly canManageCustomModels: boolean; readonly onToggleFavorite: (slug: string) => void; @@ -459,7 +474,7 @@ function ProviderModelRow({ readonly onToggleEdit: (slug: string) => void; readonly onRemove: (slug: string) => void; }) { - const capLabels = describeModelCapabilities(model); + const capLabels = describeModelCapabilities(model, flagMissingReasoning); const nameClassName = cn("text-xs", isHidden ? "text-muted-foreground" : "text-foreground/90"); const pickerTooltip = model.isCustom ? "Custom models are always shown in the picker"