From 4f01c4dba19ede57572f532d8b873b83c472e843 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 26 Jun 2026 14:22:13 -0700 Subject: [PATCH] FN-7075: backfill session models for token analytics Backfill resolved pi session models so token analytics can show every model bucket.\n\n- Mirror explicit model overrides onto sessions that do not expose a model snapshot.\n- Cover multi-model token grouping totals and Command Center pie/table rendering.\n- Add a patch changeset for the dashboard token breakdown fix.\n\nFiles changed:\n .changeset/tidy-token-model-buckets.md | 7 ++++++\n .../core/src/__tests__/token-analytics.test.ts | 5 ++++\n .../areas/__tests__/TokensArea.test.tsx | 5 ++++\n .../src/__tests__/pi-create-fn-agent.test.ts | 28 ++++++++++++++++++++++\n packages/engine/src/pi.ts | 10 +++++++-\n 5 files changed, 54 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7075 Fusion-Task-Lineage: 5f026435-5c3c-4e83-aab4-c1d7a487685a --- .changeset/tidy-token-model-buckets.md | 7 +++++ .../src/__tests__/token-analytics.test.ts | 5 ++++ .../areas/__tests__/TokensArea.test.tsx | 5 ++++ .../src/__tests__/pi-create-fn-agent.test.ts | 28 +++++++++++++++++++ packages/engine/src/pi.ts | 10 ++++++- 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-token-model-buckets.md 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 => {