fix(telemetry): emit per-turn cost instead of cumulative session total
extractUsageFromMessages was summing ALL assistant messages, so each $ai_generation event reported a growing cumulative total — PostHog's sum() then massively over-counted costs (e.g. $634 reported vs $73 actual).
This commit is contained in:
parent
920bb1d12a
commit
45acfbb493
@ -3,7 +3,13 @@ import type { TraceContextManager } from "./trace-context.js";
|
||||
import { readOrCreateAnonymousId, sanitizeMessages, sanitizeOutputChoices, stripSecrets } from "./privacy.js";
|
||||
|
||||
/**
|
||||
* Extract actual token counts and cost from OpenClaw's per-message usage metadata.
|
||||
* Extract token counts and cost from the LAST assistant message's usage metadata.
|
||||
*
|
||||
* Only the last assistant message is used so that each $ai_generation event
|
||||
* reports the per-turn delta rather than a cumulative session total. The
|
||||
* previous implementation summed across ALL assistant messages, which meant
|
||||
* multi-turn sessions emitted growing cumulative values on every turn and
|
||||
* PostHog's sum() massively over-counted costs.
|
||||
*/
|
||||
export function extractUsageFromMessages(messages: unknown): {
|
||||
inputTokens: number;
|
||||
@ -11,20 +17,23 @@ export function extractUsageFromMessages(messages: unknown): {
|
||||
totalCostUsd: number;
|
||||
} {
|
||||
if (!Array.isArray(messages)) return { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let totalCostUsd = 0;
|
||||
|
||||
let lastAssistantUsage: Record<string, unknown> | undefined;
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object") continue;
|
||||
const m = msg as Record<string, unknown>;
|
||||
if (m.role !== "assistant") continue;
|
||||
const usage = m.usage as Record<string, unknown> | undefined;
|
||||
if (!usage) continue;
|
||||
if (typeof usage.input === "number") inputTokens += usage.input;
|
||||
if (typeof usage.output === "number") outputTokens += usage.output;
|
||||
const cost = usage.cost as Record<string, unknown> | undefined;
|
||||
if (cost && typeof cost.total === "number") totalCostUsd += cost.total;
|
||||
if (usage) lastAssistantUsage = usage;
|
||||
}
|
||||
|
||||
if (!lastAssistantUsage) return { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
|
||||
|
||||
const inputTokens = typeof lastAssistantUsage.input === "number" ? lastAssistantUsage.input : 0;
|
||||
const outputTokens = typeof lastAssistantUsage.output === "number" ? lastAssistantUsage.output : 0;
|
||||
const cost = lastAssistantUsage.cost as Record<string, unknown> | undefined;
|
||||
const totalCostUsd = cost && typeof cost.total === "number" ? cost.total : 0;
|
||||
|
||||
return { inputTokens, outputTokens, totalCostUsd };
|
||||
}
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ describe("extractToolNamesFromMessages", () => {
|
||||
});
|
||||
|
||||
describe("extractUsageFromMessages", () => {
|
||||
it("sums input/output tokens and cost across assistant messages", () => {
|
||||
it("extracts usage from only the LAST assistant message (per-turn, not cumulative)", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "hi", usage: { input: 10, output: 50, cost: { total: 0.001 } } },
|
||||
@ -56,9 +56,9 @@ describe("extractUsageFromMessages", () => {
|
||||
{ role: "assistant", content: "sure", usage: { input: 20, output: 100, cost: { total: 0.002 } } },
|
||||
];
|
||||
const result = extractUsageFromMessages(messages);
|
||||
expect(result.inputTokens).toBe(30);
|
||||
expect(result.outputTokens).toBe(150);
|
||||
expect(result.totalCostUsd).toBeCloseTo(0.003);
|
||||
expect(result.inputTokens).toBe(20);
|
||||
expect(result.outputTokens).toBe(100);
|
||||
expect(result.totalCostUsd).toBeCloseTo(0.002);
|
||||
});
|
||||
|
||||
it("skips non-assistant messages (user, tool, system)", () => {
|
||||
@ -83,6 +83,17 @@ describe("extractUsageFromMessages", () => {
|
||||
it("returns zeros for non-array input", () => {
|
||||
expect(extractUsageFromMessages(null)).toEqual({ inputTokens: 0, outputTokens: 0, totalCostUsd: 0 });
|
||||
});
|
||||
|
||||
it("uses the last assistant with usage even if a later assistant lacks it", () => {
|
||||
const messages = [
|
||||
{ role: "assistant", content: "first", usage: { input: 50, output: 10, cost: { total: 0.01 } } },
|
||||
{ role: "assistant", content: "second (no usage)" },
|
||||
];
|
||||
const result = extractUsageFromMessages(messages);
|
||||
expect(result.inputTokens).toBe(50);
|
||||
expect(result.outputTokens).toBe(10);
|
||||
expect(result.totalCostUsd).toBe(0.01);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeOutputForPostHog", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user