diff --git a/.changeset/fn-7217-zai-glm-icons.md b/.changeset/fn-7217-zai-glm-icons.md new file mode 100644 index 0000000000..4ef1a4938c --- /dev/null +++ b/.changeset/fn-7217-zai-glm-icons.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show Z.ai icons for GLM model rows in Command Center token analytics. +category: fix +dev: Maps standalone glm-* model labels through the shared provider-icon inference helper. diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx index 609431ea81..266d80dca3 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx @@ -106,6 +106,31 @@ function tokenFixture(totalTokens = 1_500) { }; } +function glmOverviewTokenFixture() { + const groups = [ + makeTokenGroup("glm-5.1", 1_400), + makeTokenGroup("gpt-4o", 1_200), + makeTokenGroup("claude-sonnet", 1_000), + ]; + const totals = groups.reduce( + (acc, group) => ({ + inputTokens: acc.inputTokens + group.inputTokens, + outputTokens: acc.outputTokens + group.outputTokens, + cachedTokens: acc.cachedTokens + group.cachedTokens, + cacheWriteTokens: acc.cacheWriteTokens + group.cacheWriteTokens, + totalTokens: acc.totalTokens + group.totalTokens, + nTasks: acc.nTasks + group.nTasks, + }), + { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 }, + ); + + return { + ...tokenFixture(totals.totalTokens), + totals, + groups, + }; +} + function manyModelTokenFixture() { const groups = [ makeTokenGroup("model-01", 2_000), @@ -639,6 +664,20 @@ describe("CommandCenter shell", () => { } }); + it("renders standalone GLM model rows with the Z.ai icon in Overview token bars", async () => { + mockOverviewApi({ tokens: glmOverviewTokenFixture() }); + render(); + + const overviewTokenChart = await screen.findByTestId("command-center-overview-chart-tokens"); + const glmBarLabel = within(overviewTokenChart).getByText("glm-5.1").closest(".cc-bar-label"); + expect(glmBarLabel).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "zai")).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "openai")).toBeTruthy(); + expect(providerIconIn(overviewTokenChart, "anthropic")).toBeTruthy(); + expect(screen.getByRole("img", { name: "zai glm-5.1: 1,400" })).toBeTruthy(); + expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "zai")).toBeNull(); + }); + it("renders Overview providerless and unknown model icons without touching pie labels", async () => { mockOverviewApi({ tokens: { diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx index 21486b467c..cbe814b809 100644 --- a/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx +++ b/packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx @@ -83,6 +83,36 @@ function tokenFixture() { }; } +function glmMixedProviderTokenFixture() { + const groups = [ + makeTokenGroup("glm-5.1", 2_400), + makeTokenGroup("glm-4.5-air", 2_200), + makeTokenGroup("glm-5v-turbo", 2_000), + makeTokenGroup("gpt-4o-mini", 1_800), + makeTokenGroup("claude-sonnet-4-5", 1_600), + makeTokenGroup("custom-model-v1", 1_400), + makeTokenGroup(null, 1_200), + ]; + const totals = groups.reduce( + (acc, group) => ({ + inputTokens: acc.inputTokens + group.inputTokens, + outputTokens: acc.outputTokens + group.outputTokens, + cachedTokens: acc.cachedTokens + group.cachedTokens, + cacheWriteTokens: acc.cacheWriteTokens + group.cacheWriteTokens, + totalTokens: acc.totalTokens + group.totalTokens, + nTasks: acc.nTasks + group.nTasks, + }), + { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 }, + ); + + return { + ...tokenFixture(), + totals, + cost: { usd: null, unavailable: true, stale: false }, + groups, + }; +} + function manyModelTokenFixture() { const groups = [ makeTokenGroup("claude-sonnet-4-5", 2_000), @@ -152,6 +182,29 @@ describe("TokensArea provider model icons", () => { expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("(unknown)"); }); + it("renders standalone GLM model rows with Z.ai icons across bars and table", async () => { + apiMock.mockResolvedValue(glmMixedProviderTokenFixture()); + render(); + + const table = await screen.findByTestId("cc-tokens-table"); + const byModelChart = screen.getByRole("list", { name: "Tokens by model" }); + + for (const label of ["glm-5.1", "glm-4.5-air", "glm-5v-turbo"]) { + const row = screen.getByTestId(`cc-tokens-row-${label}`); + expect(within(row).getByText(label)).toBeTruthy(); + expect(within(row).getByTestId("provider-icon-zai")).toHaveAttribute("data-provider", "zai"); + const barLabel = within(byModelChart).getByText(label).closest(".cc-bar-label"); + expect(barLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-zai"); + expect(barLabel?.firstElementChild).toHaveAttribute("data-provider", "zai"); + } + + expect(within(screen.getByTestId("cc-tokens-row-gpt-4o-mini")).getByTestId("provider-icon-openai")).toBeTruthy(); + expect(within(screen.getByTestId("cc-tokens-row-claude-sonnet-4-5")).getByTestId("provider-icon-anthropic")).toBeTruthy(); + expect(within(screen.getByTestId("cc-tokens-row-custom-model-v1")).getByTestId("provider-icon-custom-model-v1")).toBeTruthy(); + expect(within(screen.getByTestId("cc-tokens-row-unknown")).getByTestId("provider-icon-")).toBeTruthy(); + expect(table.querySelectorAll('.provider-icon[data-provider="zai"]').length).toBe(3); + }); + it("renders every analytics model group in detail bar, pie, and table even beyond the old cap", async () => { apiMock.mockResolvedValue(manyModelTokenFixture()); render(); diff --git a/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts b/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts index f39a357f33..b28691d98f 100644 --- a/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts +++ b/packages/dashboard/app/utils/__tests__/providerIconKey.test.ts @@ -13,6 +13,9 @@ describe("inferProviderIconKey", () => { ["minimax-text-01", "minimax"], ["zhipu-glm-4", "zai"], ["zai/glm-4.5", "zai"], + ["glm-5.1", "zai"], + ["glm-4.5-air", "zai"], + ["glm-5v-turbo", "zai"], ["kimi-k2", "kimi"], ["moonshot-v1", "kimi"], ["amazon-bedrock-claude", "anthropic"], @@ -27,6 +30,7 @@ describe("inferProviderIconKey", () => { it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => { expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1"); + expect(inferProviderIconKey("not-a-glmish-model")).toBe("not-a-glmish-model"); expect(inferProviderIconKey("")).toBe(""); }); }); diff --git a/packages/dashboard/app/utils/providerIconKey.ts b/packages/dashboard/app/utils/providerIconKey.ts index 79d8d2a7ea..a1b85c46d2 100644 --- a/packages/dashboard/app/utils/providerIconKey.ts +++ b/packages/dashboard/app/utils/providerIconKey.ts @@ -1,7 +1,12 @@ /* FNXC:ProviderIcons 2026-06-25-00:00: Command Center model analytics group rows by model id only, so dashboard model-name surfaces must infer the provider icon from the model/provider text before handing off to ProviderIcon's fallback-safe renderer. + +FNXC:ProviderIcons 2026-06-28-20:46: +Standalone GLM model ids such as glm-5.1, glm-4.5-air, and glm-5v-turbo are Z.ai/Zhipu-family models even when the analytics label omits zai/zhipu. Match GLM only at model-id segment boundaries so unrelated words do not hijack the Z.ai provider icon. */ +const GLM_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])glm(?:$|[\d._:-]|-[\da-z])/; + export function inferProviderIconKey(modelOrProviderName: string): string { const normalized = modelOrProviderName.toLowerCase(); @@ -21,7 +26,7 @@ export function inferProviderIconKey(modelOrProviderName: string): string { if (normalized.includes("minimax")) { return "minimax"; } - if (normalized.includes("zai") || normalized.includes("zhipu")) { + if (normalized.includes("zai") || normalized.includes("zhipu") || GLM_MODEL_SEGMENT_PATTERN.test(normalized)) { return "zai"; } if (normalized.includes("kimi") || normalized.includes("moonshot")) {