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:
7
.changeset/tidy-token-model-buckets.md
Normal file
7
.changeset/tidy-token-model-buckets.md
Normal 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.
|
||||||
@@ -151,7 +151,10 @@ describe("token-analytics", () => {
|
|||||||
{ provider: "openai", model: "gpt-5" },
|
{ provider: "openai", model: "gpt-5" },
|
||||||
));
|
));
|
||||||
expect(modelGroups.size).toBe(2);
|
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([...modelGroups.values()].reduce((sum, group) => sum + group.nTasks, 0)).toBe(2);
|
||||||
|
expect(byModel.totals.nTasks).toBe(1);
|
||||||
|
|
||||||
const expectedTaskCost = costFor(
|
const expectedTaskCost = costFor(
|
||||||
{ inputTokens: 950, outputTokens: 450, cachedTokens: 0, cacheWriteTokens: 0 },
|
{ 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(
|
expect(new Map(byProvider.groups.map((group) => [group.key, group.totalTokens]))).toEqual(
|
||||||
new Map([["anthropic", 1000], ["openai", 400]]),
|
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", () => {
|
it("marks unpriced per-model buckets as cost unavailable instead of zero", () => {
|
||||||
|
|||||||
@@ -101,5 +101,10 @@ describe("TokensArea provider model icons", () => {
|
|||||||
expect(gptBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-openai");
|
expect(gptBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-openai");
|
||||||
expect(unknownBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-");
|
expect(unknownBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-");
|
||||||
expect(table.querySelectorAll(".provider-icon").length).toBeGreaterThanOrEqual(3);
|
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)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
it("keeps caller customTools in readonly sessions", async () => {
|
||||||
createReadOnlyToolsMock.mockReturnValueOnce([{ name: "read" }] as any);
|
createReadOnlyToolsMock.mockReturnValueOnce([{ name: "read" }] as any);
|
||||||
const delegationTool = {
|
const delegationTool = {
|
||||||
|
|||||||
@@ -2293,7 +2293,15 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
|||||||
].sort();
|
].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> => {
|
const emitFallbackUsed = async (triggerPoint: "session-creation" | "prompt-time"): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user