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
This commit is contained in:
gsxdsm
2026-06-26 14:22:13 -07:00
parent e89a58f276
commit 4f01c4dba1
5 changed files with 54 additions and 1 deletions

View File

@@ -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.

View File

@@ -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", () => {

View File

@@ -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)");
});
});

View File

@@ -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 = {

View File

@@ -2293,7 +2293,15 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
].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<void> => {