diff --git a/.changeset/tidy-token-model-buckets.md b/.changeset/tidy-token-model-buckets.md new file mode 100644 index 0000000000..da5798bfdc --- /dev/null +++ b/.changeset/tidy-token-model-buckets.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show every token-consuming model in Command Center token breakdowns. +category: fix +dev: Backfills resolved pi session models so per-model token buckets do not fall back to unknown. diff --git a/packages/core/src/__tests__/token-analytics.test.ts b/packages/core/src/__tests__/token-analytics.test.ts index 4b86878114..5fdbda1500 100644 --- a/packages/core/src/__tests__/token-analytics.test.ts +++ b/packages/core/src/__tests__/token-analytics.test.ts @@ -151,7 +151,10 @@ describe("token-analytics", () => { { provider: "openai", model: "gpt-5" }, )); expect(modelGroups.size).toBe(2); + expect([...modelGroups.values()].reduce((sum, group) => sum + group.totalTokens, 0)).toBe(byModel.totals.totalTokens); + // FNXC:TokenAnalytics 2026-06-26-14:03: A multi-model task contributes once to grand totals but once per consumed model to grouped rows, so group nTasks may exceed the grand nTasks without double-counting total task volume. expect([...modelGroups.values()].reduce((sum, group) => sum + group.nTasks, 0)).toBe(2); + expect(byModel.totals.nTasks).toBe(1); const expectedTaskCost = costFor( { inputTokens: 950, outputTokens: 450, cachedTokens: 0, cacheWriteTokens: 0 }, @@ -164,6 +167,8 @@ describe("token-analytics", () => { expect(new Map(byProvider.groups.map((group) => [group.key, group.totalTokens]))).toEqual( new Map([["anthropic", 1000], ["openai", 400]]), ); + expect(byProvider.groups.reduce((sum, group) => sum + group.totalTokens, 0)).toBe(byProvider.totals.totalTokens); + expect(byProvider.totals.nTasks).toBe(1); }); it("marks unpriced per-model buckets as cost unavailable instead of zero", () => { 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 737595200e..fe30a9b4a2 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 @@ -101,5 +101,10 @@ describe("TokensArea provider model icons", () => { expect(gptBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-openai"); expect(unknownBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-"); expect(table.querySelectorAll(".provider-icon").length).toBeGreaterThanOrEqual(3); + + // FNXC:TokenAnalytics 2026-06-26-14:05: The Command Center must render every model bucket returned by analytics across bars, pies, and table rows; missing Claude labels recreate the production one-model breakdown. + expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("claude-sonnet-4-5"); + expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("gpt-4o-mini"); + expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("(unknown)"); }); }); diff --git a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts index 225eb1e0d1..84b327515a 100644 --- a/packages/engine/src/__tests__/pi-create-fn-agent.test.ts +++ b/packages/engine/src/__tests__/pi-create-fn-agent.test.ts @@ -1547,6 +1547,34 @@ describe("createFnAgent", () => { }); }); + it("backfills the resolved model onto sessions that do not mirror it", async () => { + const session = { + prompt: vi.fn(), + subscribe: vi.fn(), + dispose: vi.fn(), + setThinkingLevel: vi.fn(), + }; + createAgentSessionMock.mockResolvedValueOnce({ session }); + + const { createFnAgent } = await import("../pi.js"); + const result = await createFnAgent({ + cwd: "/tmp", + systemPrompt: "test", + tools: "readonly", + defaultProvider: "anthropic", + defaultModelId: "claude-sonnet-4-5", + }); + + expect(createAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({ + model: { provider: "anthropic", id: "claude-sonnet-4-5" }, + })); + expect(result.session).toBe(session); + expect((result.session as { model?: unknown }).model).toEqual({ + provider: "anthropic", + id: "claude-sonnet-4-5", + }); + }); + it("keeps caller customTools in readonly sessions", async () => { createReadOnlyToolsMock.mockReturnValueOnce([{ name: "read" }] as any); const delegationTool = { diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index 1d71c88b20..9d4108edd3 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -2293,7 +2293,15 @@ export async function createFnAgent(options: AgentOptions): Promise ].sort(); } - return createAgentSession(createSessionOptions); + const result = await createAgentSession(createSessionOptions); + /* + * FNXC:TokenAnalytics 2026-06-26-13:58: + * Token analytics depends on every resolved lane model being visible on `session.model` after session creation. Some pi providers accept the explicit model override but do not mirror it back onto the session, so backfill the snapshot here before shared token accounting reads it. + */ + if (modelOverride && !(result.session as AgentSession & { model?: unknown }).model) { + (result.session as AgentSession & { model?: typeof modelOverride }).model = modelOverride; + } + return result; }; const emitFallbackUsed = async (triggerPoint: "session-creation" | "prompt-time"): Promise => {