diff --git a/front/components/workspace/analytics/UsageFilterPanel.tsx b/front/components/workspace/analytics/UsageFilterPanel.tsx index dd4ca24832ad..c31658996e37 100644 --- a/front/components/workspace/analytics/UsageFilterPanel.tsx +++ b/front/components/workspace/analytics/UsageFilterPanel.tsx @@ -1,3 +1,4 @@ +import { getModelEffortTier } from "@app/components/model_picker/modelPickerUtils"; import type { UsageFilter, UsageFilterAgentOption, @@ -16,6 +17,7 @@ import { USAGE_FILTER_CATEGORIES, USAGE_FILTER_CATEGORY_LABEL, USAGE_MODEL_TIERS, + usageModelTierFromModelsTierName, } from "@app/components/workspace/analytics/usageFilter"; import { UsageFilterAgentScopeControls } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterAgentScopeControls"; import { UsageFilterCategoryNav } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterCategoryNav"; @@ -29,8 +31,11 @@ import { useToggleSelectionList } from "@app/hooks/useToggleSelectionList"; import { useAgentConfigurations } from "@app/lib/swr/assistants"; import { useGroups } from "@app/lib/swr/groups"; import { useSearchMembers } from "@app/lib/swr/memberships"; +import { useModels } from "@app/lib/swr/models"; import type { AgentConfigurationScope } from "@app/types/assistant/agent"; import { AGENT_CONFIGURATION_SCOPES } from "@app/types/assistant/agent"; +import { isModelStreamId } from "@app/types/assistant/models/auto"; +import { getModelMaker } from "@app/types/assistant/models/providers"; import { MANAGEABLE_GROUP_KINDS } from "@app/types/groups"; import { assertNever } from "@app/types/shared/utils/assert_never"; import type { LightWorkspaceType } from "@app/types/user"; @@ -56,14 +61,13 @@ interface UsageFilterPaginationState { interface UsageFilterPanelProps { owner: LightWorkspaceType; - // Models/tools/skills/sources are still mock data (see - // usageFilterMockData.ts — sources are fake connectors standing in for a - // real db call); agents come from the same workspace-wide listing the rest - // of the app uses (useAgentConfigurations), members and teams via the - // generic member search and group listing endpoints (useSearchMembers, - // useGroups). + // Tools/skills/sources are still mock data (see usageFilterMockData.ts — + // sources are fake connectors standing in for a real db call); agents come + // from useAgentConfigurations, members from useSearchMembers, teams from + // useGroups, and models from the workspace's full model catalog + // (useModels) — the same endpoint that backs the model picker elsewhere in + // the app. categoryOptions: { - model: UsageFilterModelOption[]; tool: UsageFilterToolOption[]; skill: UsageFilterSkillOption[]; source: UsageFilterSourceOption[]; @@ -106,6 +110,7 @@ export function UsageFilterPanel({ const isMemberCategoryActive = isOpen && activeCategory === "member"; const isTeamCategoryActive = isOpen && activeCategory === "team"; const isAgentCategoryActive = isOpen && activeCategory === "agent"; + const isModelCategoryActive = isOpen && activeCategory === "model"; // Every category picker supports scroll-to-load-more: const [memberPageIndex, setMemberPageIndex] = useState(0); @@ -185,6 +190,15 @@ export function UsageFilterPanel({ disabled: !isAgentCategoryActive, }); + // The workspace's full, period-independent model catalog — the same + // endpoint backing the model picker elsewhere in the app — rather than a + // period-scoped top-N, so every enabled model is listable and searchable + // regardless of the selected period. + const { models: modelCatalog } = useModels({ + owner, + disabled: !isModelCategoryActive, + }); + const groups = useMemo( () => workspaceGroups.map((group) => ({ @@ -217,6 +231,27 @@ export function UsageFilterPanel({ [agentConfigurations] ); + // Every enabled model in the workspace, regardless of the selected period. + // Excludes the auto/meta stream ids (Fast/Standard/Complex are exposed as + // the quick-filter tier buttons, not as catalog entries). Search is + // applied client-side below; tier is derived from the same static table + // ModelsTierResource.getTierForModel resolves server-side. + const modelCatalogOptions = useMemo( + () => + modelCatalog + .filter((model) => !isModelStreamId(model.modelId)) + .map((model) => ({ + id: model.modelId, + name: model.displayName, + kind: "model" as const, + lab: getModelMaker(model), + tier: usageModelTierFromModelsTierName( + getModelEffortTier(model.modelId, model.defaultReasoningEffort) + ), + })), + [modelCatalog] + ); + const resolvedCategoryOptions = useMemo<{ [C in UsageFilterCategory]: UsageFilterOptionForCategory[]; }>( @@ -225,8 +260,15 @@ export function UsageFilterPanel({ member: accumulatedMemberOptions, team: teamOptions, agent: agentOptions, + model: modelCatalogOptions, }), - [categoryOptions, accumulatedMemberOptions, teamOptions, agentOptions] + [ + categoryOptions, + accumulatedMemberOptions, + teamOptions, + agentOptions, + modelCatalogOptions, + ] ); const activeOptions = resolvedCategoryOptions[activeCategory]; @@ -431,7 +473,7 @@ export function UsageFilterPanel({ )} {activeCategory === "model" && ( toggleOption("model", model)} activeTier={activeTier} diff --git a/front/components/workspace/analytics/usageFilter.ts b/front/components/workspace/analytics/usageFilter.ts index ef2e7ab1b7ad..19838ce3d587 100644 --- a/front/components/workspace/analytics/usageFilter.ts +++ b/front/components/workspace/analytics/usageFilter.ts @@ -1,7 +1,9 @@ import type { ConsumptionScopeFilter } from "@app/lib/api/analytics/consumption/scope"; +import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import type { AgentConfigurationScope } from "@app/types/assistant/agent"; import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import type { ConnectorProvider } from "@app/types/data_source"; +import { assertNeverAndIgnore } from "@app/types/shared/utils/assert_never"; export const USAGE_FILTER_CATEGORIES = [ "agent", @@ -71,7 +73,10 @@ export interface UsageFilterSourceOption extends UsageFilterOptionBase { export interface UsageFilterModelOption extends UsageFilterOptionBase { kind: "model"; lab: ModelMakerIdType; - tier: UsageModelTier; + // Undefined for a model outside the static tier table — it doesn't match + // any Fast/Standard/Complex quick filter, so it's absent from the main + // checklist but still reachable through the "More models" browse dropdown. + tier: UsageModelTier | undefined; } export interface UsageFilterToolOption extends UsageFilterOptionBase { @@ -146,8 +151,9 @@ export function selectAllUsageFilterOptions( return { ...filter, [category]: [...current, ...additions] }; } -// Members, teams, and agents are wired to real consumption scope dimensions. -// The other categories stay mock data and are not sent as query filters yet. +// Members, teams, agents, and models are wired to real consumption scope +// dimensions. The other categories stay mock data and are not sent as query +// filters yet. export function toConsumptionScopeFilter( filter: UsageFilter ): ConsumptionScopeFilter { @@ -168,5 +174,34 @@ export function toConsumptionScopeFilter( scopeFilter.agents = agentIds; } + const modelIds = filter.model?.map((entity) => entity.id); + if (modelIds && modelIds.length > 0) { + scopeFilter.models = modelIds; + } + return scopeFilter; } + +// Maps the backend's reasoning-effort-aware pricing tier onto the filter +// panel's simpler Fast/Standard/Complex bucket. Null propagates (a model +// outside the static tier table, or a raw catalog entry with no config match) +// as "no bucket", so it's excluded from every quick-filter tier rather than +// landing in one arbitrarily. +export function usageModelTierFromModelsTierName( + tier: ModelsTierName | null | undefined +): UsageModelTier | undefined { + if (tier === null || tier === undefined) { + return undefined; + } + switch (tier) { + case "cost_efficient": + return "fast"; + case "balanced": + return "standard"; + case "premium": + return "complex"; + default: + assertNeverAndIgnore(tier); + return undefined; + } +} diff --git a/front/components/workspace/analytics/usageFilterMockData.ts b/front/components/workspace/analytics/usageFilterMockData.ts index 806ace632ba2..ac849a634c64 100644 --- a/front/components/workspace/analytics/usageFilterMockData.ts +++ b/front/components/workspace/analytics/usageFilterMockData.ts @@ -1,47 +1,15 @@ import type { - UsageFilterModelOption, UsageFilterSkillOption, UsageFilterSourceOption, UsageFilterToolOption, } from "@app/components/workspace/analytics/usageFilter"; -import { USAGE_MODEL_TIERS } from "@app/components/workspace/analytics/usageFilter"; -import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import type { ConnectorProvider } from "@app/types/data_source"; -const MOCK_MODEL_LAB: Record = { - "Claude Sonnet 5": "anthropic", - "Claude Opus 5": "anthropic", - "Claude Haiku 4.5": "anthropic", - "Claude Fable 5": "anthropic", - "GPT-5": "openai", - "GPT-5 mini": "openai", - "Gemini 3 Pro": "google_ai_studio", - "Gemini 3 Flash": "google_ai_studio", - "Llama 4 Maverick": "fireworks", - "Mistral Large 3": "mistral", - "Grok 4": "xai", - "DeepSeek V4": "deepseek", -}; - // Placeholder data for categories not yet wired to a real backend endpoint. -// Agents are fetched live in UsageFilterPanel (useConsumptionTop); members -// and groups via useSearchMembers and useGroups. Lists are long enough to -// exercise scrolling in the preview. +// Agents are fetched live in UsageFilterPanel (useAgentConfigurations); +// members via useSearchMembers; groups via useGroups; models via useModels. +// Lists are long enough to exercise scrolling in the preview. const MOCK_ENTITY_NAMES = { - model: [ - "Claude Sonnet 5", - "Claude Opus 5", - "Claude Haiku 4.5", - "Claude Fable 5", - "GPT-5", - "GPT-5 mini", - "Gemini 3 Pro", - "Gemini 3 Flash", - "Llama 4 Maverick", - "Mistral Large 3", - "Grok 4", - "DeepSeek V4", - ], tool: [ "web_search", "file_search", @@ -100,16 +68,6 @@ const MOCK_SOURCE_CONNECTORS: Array<{ { name: "Uploaded files — Legal templates", connectorProvider: undefined }, ]; -function buildModelOptions(names: string[]): UsageFilterModelOption[] { - return names.map((name, index) => ({ - id: `model_${index + 1}`, - name, - kind: "model", - lab: MOCK_MODEL_LAB[name], - tier: USAGE_MODEL_TIERS[index % USAGE_MODEL_TIERS.length], - })); -} - function buildToolOptions(names: string[]): UsageFilterToolOption[] { return names.map((name, index) => ({ id: `tool_${index + 1}`, @@ -127,7 +85,6 @@ function buildSkillOptions(names: string[]): UsageFilterSkillOption[] { } export const USAGE_FILTER_MOCK_OPTIONS = { - model: buildModelOptions(MOCK_ENTITY_NAMES.model), tool: buildToolOptions(MOCK_ENTITY_NAMES.tool), skill: buildSkillOptions(MOCK_ENTITY_NAMES.skill), source: MOCK_SOURCE_CONNECTORS.map( diff --git a/front/components/workspace/analytics/usageFilterPanel/UsageFilterModelComplexityControls.tsx b/front/components/workspace/analytics/usageFilterPanel/UsageFilterModelComplexityControls.tsx index d2e2f9278287..048d10193aff 100644 --- a/front/components/workspace/analytics/usageFilterPanel/UsageFilterModelComplexityControls.tsx +++ b/front/components/workspace/analytics/usageFilterPanel/UsageFilterModelComplexityControls.tsx @@ -39,7 +39,7 @@ const MODEL_TIER_ICON: Record = { }; interface UsageFilterModelComplexityControlsProps { - models: UsageFilterModelOption[]; + moreModelsCatalog: UsageFilterModelOption[]; selectedModelIds: Set; onToggleModel: (model: UsageFilterModelOption) => void; activeTier: UsageModelTier; @@ -47,7 +47,7 @@ interface UsageFilterModelComplexityControlsProps { } export function UsageFilterModelComplexityControls({ - models, + moreModelsCatalog, selectedModelIds, onToggleModel, activeTier, @@ -75,13 +75,13 @@ export function UsageFilterModelComplexityControls({ const isSearchingMoreModels = moreModelsQuery !== ""; const moreModelsSearchResults = isSearchingMoreModels - ? models.filter((model) => + ? moreModelsCatalog.filter((model) => model.name.toLowerCase().includes(moreModelsQuery) ) : []; const moreModelsGroups = MODEL_MAKER_IDS.flatMap((lab) => { - const labModels = models.filter((model) => model.lab === lab); + const labModels = moreModelsCatalog.filter((model) => model.lab === lab); return labModels.length > 0 ? [{ lab, models: labModels }] : []; }); diff --git a/front/hooks/useConsumptionTop.ts b/front/hooks/useConsumptionTop.ts index bab3420ffd57..a82bed4a9be1 100644 --- a/front/hooks/useConsumptionTop.ts +++ b/front/hooks/useConsumptionTop.ts @@ -68,6 +68,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.teamId, name: row.name, pictureUrl: null, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, }));