From 1e8db1b117a22a2347fc23e6b99dd3c7c2baac1f Mon Sep 17 00:00:00 2001 From: Arthur Vervaet Date: Fri, 7 Aug 2026 11:39:43 +0200 Subject: [PATCH 1/5] [front] feat: wire Models tab of the usage filter panel to real, period-scoped data Models now come from the same period-ranked consumption data the Attribution table uses (useConsumptionTop), instead of the mock, period-unaware catalog. The maker (lab) and Fast/Standard/Complex quick-filter tier are also real now, resolved server-side from the model config and its pricing tier (ModelsTierResource.getTierForModel) via resolveConsumptionGroupLabels, instead of random mock values. "More models" now browses the workspace's real, period-independent model catalog (useModels) grouped by maker, instead of the mock list. Also wires the page's UsageFilter state into the actual consumption queries for the "model" dimension, which it never did before. --- .../workspace/AnalyticsConsumptionPage.tsx | 1 + .../workspace/analytics/UsageFilterPanel.tsx | 93 +++++++++++++++++-- .../workspace/analytics/usageFilter.ts | 36 ++++++- .../analytics/usageFilterMockData.ts | 50 +--------- .../UsageFilterModelComplexityControls.tsx | 8 +- front/hooks/useConsumptionTop.ts | 20 ++++ front/lib/api/analytics/consumption/labels.ts | 51 ++++++++-- .../lib/api/analytics/consumption/top.test.ts | 16 +++- .../api/analytics/consumption/top_models.ts | 6 ++ 9 files changed, 208 insertions(+), 73 deletions(-) diff --git a/front/components/pages/workspace/AnalyticsConsumptionPage.tsx b/front/components/pages/workspace/AnalyticsConsumptionPage.tsx index 5197ba2ecf17..17eccd188fa0 100644 --- a/front/components/pages/workspace/AnalyticsConsumptionPage.tsx +++ b/front/components/pages/workspace/AnalyticsConsumptionPage.tsx @@ -91,6 +91,7 @@ export function AnalyticsConsumptionPage() {
( () => workspaceGroups.map((group) => ({ @@ -217,6 +243,29 @@ export function UsageFilterPanel({ [agentConfigurations] ); + // Same client-side search caveat as members/agents: a model outside the + // top 100 by credits over the period will not be searchable here (it's + // still reachable through "More models", which browses the full catalog). + // Rows whose model maker can't be resolved are dropped — they'd have no + // logo and no tier bucket to live in. + const modelOptions = useMemo( + () => + topModelRows.flatMap((row) => + row.modelMaker + ? [ + { + id: row.id, + name: row.name, + kind: "model" as const, + lab: row.modelMaker, + tier: usageModelTierFromModelsTierName(row.tier), + }, + ] + : [] + ), + [topModelRows] + ); + const resolvedCategoryOptions = useMemo<{ [C in UsageFilterCategory]: UsageFilterOptionForCategory[]; }>( @@ -225,8 +274,15 @@ export function UsageFilterPanel({ member: accumulatedMemberOptions, team: teamOptions, agent: agentOptions, + model: modelOptions, }), - [categoryOptions, accumulatedMemberOptions, teamOptions, agentOptions] + [ + categoryOptions, + accumulatedMemberOptions, + teamOptions, + agentOptions, + modelOptions, + ] ); const activeOptions = resolvedCategoryOptions[activeCategory]; @@ -313,6 +369,25 @@ export function UsageFilterPanel({ handleLoadMoreStaticOptions, ]); + // "More models" browses the workspace's full model catalog grouped by + // maker, independent of the Fast/Standard/Complex quick filter above and of + // the period (unlike the primary checklist above it, sourced from + // `topModelRows`). Search/grouping over this catalog is handled inside + // UsageFilterModelComplexityControls itself. + const moreModelsCatalog = useMemo( + () => + modelCatalog + .filter((model) => !isModelStreamId(model.modelId)) + .map((model) => ({ + id: model.modelId, + name: model.displayName, + kind: "model" as const, + lab: getModelMaker(model), + tier: undefined, + })), + [modelCatalog] + ); + const selectedIdsForActiveCategory = useMemo( () => new Set((draftFilter[activeCategory] ?? []).map((option) => option.id)), @@ -431,7 +506,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..4d0964903bcc 100644 --- a/front/components/workspace/analytics/usageFilter.ts +++ b/front/components/workspace/analytics/usageFilter.ts @@ -1,4 +1,5 @@ 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"; @@ -71,7 +72,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 +150,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 +173,30 @@ 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 { + switch (tier) { + case "cost_efficient": + return "fast"; + case "balanced": + return "standard"; + case "premium": + return "complex"; + default: + return undefined; + } +} diff --git a/front/components/workspace/analytics/usageFilterMockData.ts b/front/components/workspace/analytics/usageFilterMockData.ts index 806ace632ba2..f11b7a21b12f 100644 --- a/front/components/workspace/analytics/usageFilterMockData.ts +++ b/front/components/workspace/analytics/usageFilterMockData.ts @@ -1,47 +1,16 @@ 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 +// useConsumptionTop. 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 +69,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 +86,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..25a7bbe6ac9f 100644 --- a/front/hooks/useConsumptionTop.ts +++ b/front/hooks/useConsumptionTop.ts @@ -9,7 +9,9 @@ import type { GetConsumptionTopSourcesResponse } from "@app/lib/api/analytics/co import type { GetConsumptionTopTeamsResponse } from "@app/lib/api/analytics/consumption/top_teams"; import type { GetConsumptionTopToolsResponse } from "@app/lib/api/analytics/consumption/top_tools"; import type { GetConsumptionTopUsersResponse } from "@app/lib/api/analytics/consumption/top_users"; +import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import { emptyArray, useFetcher, useSWRWithDefaults } from "@app/lib/swr/swr"; +import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import { assertNeverAndIgnore } from "@app/types/shared/utils/assert_never"; import { useMemo } from "react"; import type { Fetcher } from "swr"; @@ -28,6 +30,10 @@ export type ConsumptionTopRow = { id: string; name: string; pictureUrl: string | null; + // Only models have one; null for every other dimension. + modelMaker: ModelMakerIdType | null; + // Only models have one; null for every other dimension. + tier: ModelsTierName | null; credits: number; avgCredits: number; }; @@ -50,6 +56,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.agentId, name: row.name, pictureUrl: row.pictureUrl, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -59,6 +67,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.userId, name: row.name, pictureUrl: row.pictureUrl, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -68,6 +78,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.teamId, name: row.name, pictureUrl: null, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -77,6 +89,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.modelId, name: row.name, pictureUrl: null, + modelMaker: row.modelMaker, + tier: row.tier, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -86,6 +100,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.serverName, name: row.name, pictureUrl: null, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerInvocation, })); @@ -95,6 +111,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.skillId, name: row.name, pictureUrl: null, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerInvocation, })); @@ -104,6 +122,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.source, name: row.name, pictureUrl: null, + modelMaker: null, + tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); diff --git a/front/lib/api/analytics/consumption/labels.ts b/front/lib/api/analytics/consumption/labels.ts index 6427c94e06ff..ba0f51a182af 100644 --- a/front/lib/api/analytics/consumption/labels.ts +++ b/front/lib/api/analytics/consumption/labels.ts @@ -3,11 +3,15 @@ import { sourceLabelForOrigin } from "@app/lib/api/analytics/source_labels"; import { resolveAnalyticsAgentLabels } from "@app/lib/api/assistant/observability/agent_labels"; import { getUserDisplayName } from "@app/lib/api/assistant/observability/credit_labels"; import { resolveServerDisplayNames } from "@app/lib/api/assistant/observability/tool_usage"; +import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import type { Authenticator } from "@app/lib/auth"; import { getModelConfigByModelId } from "@app/lib/llms/model_configurations"; import { GroupResource } from "@app/lib/resources/group_resource"; +import { ModelsTierResource } from "@app/lib/resources/models_tier_resource"; import { SkillResource } from "@app/lib/resources/skill/skill_resource"; import { UserResource } from "@app/lib/resources/user_resource"; +import { getModelMaker } from "@app/types/assistant/models/providers"; +import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import { CAP_ELIGIBLE_GROUP_KINDS } from "@app/types/groups"; import { assertNever } from "@app/types/shared/utils/assert_never"; @@ -29,13 +33,20 @@ export type DimensionLabel = { name: string; // Only agents and users have one; null for every other dimension. pictureUrl: string | null; + // Only models have one; null for every other dimension. + modelMaker: ModelMakerIdType | null; + // Only models have one; null for every other dimension. + tier: ModelsTierName | null; }; function labelsFromNames( names: Map ): Map { return new Map( - [...names].map(([key, name]) => [key, { name, pictureUrl: null }]) + [...names].map(([key, name]) => [ + key, + { name, pictureUrl: null, modelMaker: null, tier: null }, + ]) ); } @@ -54,7 +65,15 @@ export async function resolveDimensionLabels( return new Map( keys.map((key) => { const label = labels.get(key) ?? { name: key, pictureUrl: null }; - return [key, { name: label.name, pictureUrl: label.pictureUrl }]; + return [ + key, + { + name: label.name, + pictureUrl: label.pictureUrl, + modelMaker: null, + tier: null, + }, + ]; }) ); } @@ -70,6 +89,8 @@ export async function resolveDimensionLabels( { name: getUserDisplayName(user), pictureUrl: user?.imageUrl ?? null, + modelMaker: null, + tier: null, }, ]; }) @@ -86,15 +107,27 @@ export async function resolveDimensionLabels( ); } - case "model": - return labelsFromNames( - new Map( - keys.map((key) => [ + case "model": { + return new Map( + keys.map((key) => { + const config = getModelConfigByModelId(key); + return [ key, - getModelConfigByModelId(key)?.displayName ?? key, - ]) - ) + { + name: config?.displayName ?? key, + pictureUrl: null, + modelMaker: config ? getModelMaker(config) : null, + tier: config + ? ModelsTierResource.getTierForModel( + config.modelId, + config.defaultReasoningEffort + ) + : null, + }, + ]; + }) ); + } case "tool": { // The key is the MCP server name. Internal servers get their display name diff --git a/front/lib/api/analytics/consumption/top.test.ts b/front/lib/api/analytics/consumption/top.test.ts index b5f3d33db4f5..8c29abd4ed83 100644 --- a/front/lib/api/analytics/consumption/top.test.ts +++ b/front/lib/api/analytics/consumption/top.test.ts @@ -54,7 +54,7 @@ function mockLabels(labels: Record) { new Map( Object.entries(labels).map(([key, name]) => [ key, - { name, pictureUrl: null }, + { name, pictureUrl: null, modelMaker: null, tier: null }, ]) ) ); @@ -81,7 +81,17 @@ describe("consumption top rankings", () => { it("ranks agents on gross credits and averages over distinct messages", async () => { const { auth } = await setup(); vi.mocked(resolveDimensionLabels).mockResolvedValue( - new Map([["agent1", { name: "@dust", pictureUrl: "http://pic/dust" }]]) + new Map([ + [ + "agent1", + { + name: "@dust", + pictureUrl: "http://pic/dust", + modelMaker: null, + tier: null, + }, + ], + ]) ); mockAggs({ buckets: [ @@ -286,6 +296,8 @@ describe("consumption top rankings", () => { expect(models.value.models[0]).toEqual({ modelId: "key1", name: "Claude 4 Sonnet", + modelMaker: null, + tier: null, credits: 2, messageCount: 4, avgCreditsPerMessage: 0.5, diff --git a/front/lib/api/analytics/consumption/top_models.ts b/front/lib/api/analytics/consumption/top_models.ts index 80b4688aa22d..9c8f7d9f61a5 100644 --- a/front/lib/api/analytics/consumption/top_models.ts +++ b/front/lib/api/analytics/consumption/top_models.ts @@ -4,8 +4,10 @@ import { avgCreditsPerUnit, fetchConsumptionTopGroups, } from "@app/lib/api/analytics/consumption/top"; +import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import type { ElasticsearchError } from "@app/lib/api/elasticsearch"; import type { Authenticator } from "@app/lib/auth"; +import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import type { Result } from "@app/types/shared/result"; import { Ok } from "@app/types/shared/result"; import { resolveDimensionLabels } from "./labels"; @@ -20,6 +22,8 @@ import { resolveDimensionLabels } from "./labels"; export type ConsumptionTopModelRow = { modelId: string; name: string; + modelMaker: ModelMakerIdType | null; + tier: ModelsTierName | null; credits: number; messageCount: number; avgCreditsPerMessage: number; @@ -70,6 +74,8 @@ export async function fetchConsumptionTopModels( models: groups.map((group) => ({ modelId: group.key, name: labels.get(group.key)?.name ?? group.key, + modelMaker: labels.get(group.key)?.modelMaker ?? null, + tier: labels.get(group.key)?.tier ?? null, credits: group.credits, messageCount: group.count, avgCreditsPerMessage: avgCreditsPerUnit(group.credits, group.count), From dfc9560ddba6d2d82d11878b9f44c0db8aa3bf58 Mon Sep 17 00:00:00 2001 From: Arthur Vervaet Date: Mon, 10 Aug 2026 15:54:21 +0200 Subject: [PATCH 2/5] [front] fix: list every model in the usage filter panel, not just the period's top 100 The Models tab was built on the same period-ranked top-N data as the Attribution table, so a model outside the top 100 by credits for the selected period couldn't be searched or filtered on at all. Switch it to useModels, the same workspace-wide model catalog used by the model picker elsewhere in the app, so every enabled model is listable and searchable regardless of the period. Tier is now derived client-side via the existing getModelEffortTier mirror instead of a backend round-trip, which also lets "More models" and the primary checklist share a single source list. Also addresses the outstanding review comment on usageModelTierFromModelsTierName: separate the null/undefined case from the switch and use assertNeverAndIgnore so an unknown backend tier value is ignored instead of silently swallowed by the default case. --- .../workspace/AnalyticsConsumptionPage.tsx | 1 - .../workspace/analytics/UsageFilterPanel.tsx | 93 ++++++------------- .../workspace/analytics/usageFilter.ts | 5 + .../analytics/usageFilterMockData.ts | 5 +- front/hooks/useConsumptionTop.ts | 18 ---- front/lib/api/analytics/consumption/labels.ts | 51 ++-------- .../lib/api/analytics/consumption/top.test.ts | 11 +-- .../api/analytics/consumption/top_models.ts | 6 -- 8 files changed, 48 insertions(+), 142 deletions(-) diff --git a/front/components/pages/workspace/AnalyticsConsumptionPage.tsx b/front/components/pages/workspace/AnalyticsConsumptionPage.tsx index 17eccd188fa0..5197ba2ecf17 100644 --- a/front/components/pages/workspace/AnalyticsConsumptionPage.tsx +++ b/front/components/pages/workspace/AnalyticsConsumptionPage.tsx @@ -91,7 +91,6 @@ export function AnalyticsConsumptionPage() {
( + // 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( () => - topModelRows.flatMap((row) => - row.modelMaker - ? [ - { - id: row.id, - name: row.name, - kind: "model" as const, - lab: row.modelMaker, - tier: usageModelTierFromModelsTierName(row.tier), - }, - ] - : [] - ), - [topModelRows] + 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<{ @@ -274,14 +260,14 @@ export function UsageFilterPanel({ member: accumulatedMemberOptions, team: teamOptions, agent: agentOptions, - model: modelOptions, + model: modelCatalogOptions, }), [ categoryOptions, accumulatedMemberOptions, teamOptions, agentOptions, - modelOptions, + modelCatalogOptions, ] ); @@ -369,25 +355,6 @@ export function UsageFilterPanel({ handleLoadMoreStaticOptions, ]); - // "More models" browses the workspace's full model catalog grouped by - // maker, independent of the Fast/Standard/Complex quick filter above and of - // the period (unlike the primary checklist above it, sourced from - // `topModelRows`). Search/grouping over this catalog is handled inside - // UsageFilterModelComplexityControls itself. - const moreModelsCatalog = useMemo( - () => - modelCatalog - .filter((model) => !isModelStreamId(model.modelId)) - .map((model) => ({ - id: model.modelId, - name: model.displayName, - kind: "model" as const, - lab: getModelMaker(model), - tier: undefined, - })), - [modelCatalog] - ); - const selectedIdsForActiveCategory = useMemo( () => new Set((draftFilter[activeCategory] ?? []).map((option) => option.id)), @@ -506,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 4d0964903bcc..19838ce3d587 100644 --- a/front/components/workspace/analytics/usageFilter.ts +++ b/front/components/workspace/analytics/usageFilter.ts @@ -3,6 +3,7 @@ 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", @@ -189,6 +190,9 @@ export function toConsumptionScopeFilter( export function usageModelTierFromModelsTierName( tier: ModelsTierName | null | undefined ): UsageModelTier | undefined { + if (tier === null || tier === undefined) { + return undefined; + } switch (tier) { case "cost_efficient": return "fast"; @@ -197,6 +201,7 @@ export function usageModelTierFromModelsTierName( 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 f11b7a21b12f..ac849a634c64 100644 --- a/front/components/workspace/analytics/usageFilterMockData.ts +++ b/front/components/workspace/analytics/usageFilterMockData.ts @@ -7,9 +7,8 @@ import type { ConnectorProvider } from "@app/types/data_source"; // Placeholder data for categories not yet wired to a real backend endpoint. // Agents are fetched live in UsageFilterPanel (useAgentConfigurations); -// members via useSearchMembers; groups via useGroups; models via -// useConsumptionTop. Lists are long enough to exercise scrolling in the -// preview. +// members via useSearchMembers; groups via useGroups; models via useModels. +// Lists are long enough to exercise scrolling in the preview. const MOCK_ENTITY_NAMES = { tool: [ "web_search", diff --git a/front/hooks/useConsumptionTop.ts b/front/hooks/useConsumptionTop.ts index 25a7bbe6ac9f..a82bed4a9be1 100644 --- a/front/hooks/useConsumptionTop.ts +++ b/front/hooks/useConsumptionTop.ts @@ -9,9 +9,7 @@ import type { GetConsumptionTopSourcesResponse } from "@app/lib/api/analytics/co import type { GetConsumptionTopTeamsResponse } from "@app/lib/api/analytics/consumption/top_teams"; import type { GetConsumptionTopToolsResponse } from "@app/lib/api/analytics/consumption/top_tools"; import type { GetConsumptionTopUsersResponse } from "@app/lib/api/analytics/consumption/top_users"; -import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import { emptyArray, useFetcher, useSWRWithDefaults } from "@app/lib/swr/swr"; -import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import { assertNeverAndIgnore } from "@app/types/shared/utils/assert_never"; import { useMemo } from "react"; import type { Fetcher } from "swr"; @@ -30,10 +28,6 @@ export type ConsumptionTopRow = { id: string; name: string; pictureUrl: string | null; - // Only models have one; null for every other dimension. - modelMaker: ModelMakerIdType | null; - // Only models have one; null for every other dimension. - tier: ModelsTierName | null; credits: number; avgCredits: number; }; @@ -56,8 +50,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.agentId, name: row.name, pictureUrl: row.pictureUrl, - modelMaker: null, - tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -67,8 +59,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.userId, name: row.name, pictureUrl: row.pictureUrl, - modelMaker: null, - tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -89,8 +79,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.modelId, name: row.name, pictureUrl: null, - modelMaker: row.modelMaker, - tier: row.tier, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); @@ -100,8 +88,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.serverName, name: row.name, pictureUrl: null, - modelMaker: null, - tier: null, credits: row.credits, avgCredits: row.avgCreditsPerInvocation, })); @@ -111,8 +97,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.skillId, name: row.name, pictureUrl: null, - modelMaker: null, - tier: null, credits: row.credits, avgCredits: row.avgCreditsPerInvocation, })); @@ -122,8 +106,6 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] { id: row.source, name: row.name, pictureUrl: null, - modelMaker: null, - tier: null, credits: row.credits, avgCredits: row.avgCreditsPerMessage, })); diff --git a/front/lib/api/analytics/consumption/labels.ts b/front/lib/api/analytics/consumption/labels.ts index ba0f51a182af..6427c94e06ff 100644 --- a/front/lib/api/analytics/consumption/labels.ts +++ b/front/lib/api/analytics/consumption/labels.ts @@ -3,15 +3,11 @@ import { sourceLabelForOrigin } from "@app/lib/api/analytics/source_labels"; import { resolveAnalyticsAgentLabels } from "@app/lib/api/assistant/observability/agent_labels"; import { getUserDisplayName } from "@app/lib/api/assistant/observability/credit_labels"; import { resolveServerDisplayNames } from "@app/lib/api/assistant/observability/tool_usage"; -import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import type { Authenticator } from "@app/lib/auth"; import { getModelConfigByModelId } from "@app/lib/llms/model_configurations"; import { GroupResource } from "@app/lib/resources/group_resource"; -import { ModelsTierResource } from "@app/lib/resources/models_tier_resource"; import { SkillResource } from "@app/lib/resources/skill/skill_resource"; import { UserResource } from "@app/lib/resources/user_resource"; -import { getModelMaker } from "@app/types/assistant/models/providers"; -import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import { CAP_ELIGIBLE_GROUP_KINDS } from "@app/types/groups"; import { assertNever } from "@app/types/shared/utils/assert_never"; @@ -33,20 +29,13 @@ export type DimensionLabel = { name: string; // Only agents and users have one; null for every other dimension. pictureUrl: string | null; - // Only models have one; null for every other dimension. - modelMaker: ModelMakerIdType | null; - // Only models have one; null for every other dimension. - tier: ModelsTierName | null; }; function labelsFromNames( names: Map ): Map { return new Map( - [...names].map(([key, name]) => [ - key, - { name, pictureUrl: null, modelMaker: null, tier: null }, - ]) + [...names].map(([key, name]) => [key, { name, pictureUrl: null }]) ); } @@ -65,15 +54,7 @@ export async function resolveDimensionLabels( return new Map( keys.map((key) => { const label = labels.get(key) ?? { name: key, pictureUrl: null }; - return [ - key, - { - name: label.name, - pictureUrl: label.pictureUrl, - modelMaker: null, - tier: null, - }, - ]; + return [key, { name: label.name, pictureUrl: label.pictureUrl }]; }) ); } @@ -89,8 +70,6 @@ export async function resolveDimensionLabels( { name: getUserDisplayName(user), pictureUrl: user?.imageUrl ?? null, - modelMaker: null, - tier: null, }, ]; }) @@ -107,27 +86,15 @@ export async function resolveDimensionLabels( ); } - case "model": { - return new Map( - keys.map((key) => { - const config = getModelConfigByModelId(key); - return [ + case "model": + return labelsFromNames( + new Map( + keys.map((key) => [ key, - { - name: config?.displayName ?? key, - pictureUrl: null, - modelMaker: config ? getModelMaker(config) : null, - tier: config - ? ModelsTierResource.getTierForModel( - config.modelId, - config.defaultReasoningEffort - ) - : null, - }, - ]; - }) + getModelConfigByModelId(key)?.displayName ?? key, + ]) + ) ); - } case "tool": { // The key is the MCP server name. Internal servers get their display name diff --git a/front/lib/api/analytics/consumption/top.test.ts b/front/lib/api/analytics/consumption/top.test.ts index 8c29abd4ed83..283998e51c76 100644 --- a/front/lib/api/analytics/consumption/top.test.ts +++ b/front/lib/api/analytics/consumption/top.test.ts @@ -54,7 +54,7 @@ function mockLabels(labels: Record) { new Map( Object.entries(labels).map(([key, name]) => [ key, - { name, pictureUrl: null, modelMaker: null, tier: null }, + { name, pictureUrl: null }, ]) ) ); @@ -84,12 +84,7 @@ describe("consumption top rankings", () => { new Map([ [ "agent1", - { - name: "@dust", - pictureUrl: "http://pic/dust", - modelMaker: null, - tier: null, - }, + { name: "@dust", pictureUrl: "http://pic/dust" }, ], ]) ); @@ -296,8 +291,6 @@ describe("consumption top rankings", () => { expect(models.value.models[0]).toEqual({ modelId: "key1", name: "Claude 4 Sonnet", - modelMaker: null, - tier: null, credits: 2, messageCount: 4, avgCreditsPerMessage: 0.5, diff --git a/front/lib/api/analytics/consumption/top_models.ts b/front/lib/api/analytics/consumption/top_models.ts index 9c8f7d9f61a5..80b4688aa22d 100644 --- a/front/lib/api/analytics/consumption/top_models.ts +++ b/front/lib/api/analytics/consumption/top_models.ts @@ -4,10 +4,8 @@ import { avgCreditsPerUnit, fetchConsumptionTopGroups, } from "@app/lib/api/analytics/consumption/top"; -import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers"; import type { ElasticsearchError } from "@app/lib/api/elasticsearch"; import type { Authenticator } from "@app/lib/auth"; -import type { ModelMakerIdType } from "@app/types/assistant/models/types"; import type { Result } from "@app/types/shared/result"; import { Ok } from "@app/types/shared/result"; import { resolveDimensionLabels } from "./labels"; @@ -22,8 +20,6 @@ import { resolveDimensionLabels } from "./labels"; export type ConsumptionTopModelRow = { modelId: string; name: string; - modelMaker: ModelMakerIdType | null; - tier: ModelsTierName | null; credits: number; messageCount: number; avgCreditsPerMessage: number; @@ -74,8 +70,6 @@ export async function fetchConsumptionTopModels( models: groups.map((group) => ({ modelId: group.key, name: labels.get(group.key)?.name ?? group.key, - modelMaker: labels.get(group.key)?.modelMaker ?? null, - tier: labels.get(group.key)?.tier ?? null, credits: group.credits, messageCount: group.count, avgCreditsPerMessage: avgCreditsPerUnit(group.credits, group.count), From 30690a6fa0977d785bc5e3d2745c35e3fa857f09 Mon Sep 17 00:00:00 2001 From: Arthur Vervaet Date: Mon, 10 Aug 2026 18:25:57 +0200 Subject: [PATCH 3/5] Update front/components/workspace/analytics/usageFilter.ts Co-authored-by: dust-agent[bot] <195764847+dust-agent[bot]@users.noreply.github.com> --- front/components/workspace/analytics/usageFilter.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/front/components/workspace/analytics/usageFilter.ts b/front/components/workspace/analytics/usageFilter.ts index 19838ce3d587..95607cb960a4 100644 --- a/front/components/workspace/analytics/usageFilter.ts +++ b/front/components/workspace/analytics/usageFilter.ts @@ -193,6 +193,16 @@ export function usageModelTierFromModelsTierName( if (tier === null || tier === undefined) { return undefined; } + switch (tier) { + case "cost_efficient": + return "fast"; + case "balanced": + return "standard"; + case "premium": + return "complex"; + if (tier === null || tier === undefined) { + return undefined; + } switch (tier) { case "cost_efficient": return "fast"; @@ -203,5 +213,8 @@ export function usageModelTierFromModelsTierName( default: assertNeverAndIgnore(tier); return undefined; + } + assertNeverAndIgnore(tier); + return undefined; } } From deaad8a70c61d47490a2bf6ebb9ee11b7b5d1871 Mon Sep 17 00:00:00 2001 From: Arthur Vervaet Date: Mon, 10 Aug 2026 18:30:21 +0200 Subject: [PATCH 4/5] fixing test --- front/components/workspace/analytics/usageFilter.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/front/components/workspace/analytics/usageFilter.ts b/front/components/workspace/analytics/usageFilter.ts index 95607cb960a4..19838ce3d587 100644 --- a/front/components/workspace/analytics/usageFilter.ts +++ b/front/components/workspace/analytics/usageFilter.ts @@ -193,16 +193,6 @@ export function usageModelTierFromModelsTierName( if (tier === null || tier === undefined) { return undefined; } - switch (tier) { - case "cost_efficient": - return "fast"; - case "balanced": - return "standard"; - case "premium": - return "complex"; - if (tier === null || tier === undefined) { - return undefined; - } switch (tier) { case "cost_efficient": return "fast"; @@ -213,8 +203,5 @@ export function usageModelTierFromModelsTierName( default: assertNeverAndIgnore(tier); return undefined; - } - assertNeverAndIgnore(tier); - return undefined; } } From f2d68c8008bcda1d4c1c7f0e16050705f913a195 Mon Sep 17 00:00:00 2001 From: Arthur Vervaet Date: Tue, 11 Aug 2026 09:31:28 +0200 Subject: [PATCH 5/5] fix --- front/lib/api/analytics/consumption/top.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/front/lib/api/analytics/consumption/top.test.ts b/front/lib/api/analytics/consumption/top.test.ts index 283998e51c76..b5f3d33db4f5 100644 --- a/front/lib/api/analytics/consumption/top.test.ts +++ b/front/lib/api/analytics/consumption/top.test.ts @@ -81,12 +81,7 @@ describe("consumption top rankings", () => { it("ranks agents on gross credits and averages over distinct messages", async () => { const { auth } = await setup(); vi.mocked(resolveDimensionLabels).mockResolvedValue( - new Map([ - [ - "agent1", - { name: "@dust", pictureUrl: "http://pic/dust" }, - ], - ]) + new Map([["agent1", { name: "@dust", pictureUrl: "http://pic/dust" }]]) ); mockAggs({ buckets: [