mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): repair usage cost aggregation across agents (#93022)
* fix(gateway): aggregate usage cost across agents * fix(clownfish): address review for ghcrawl-157040-autonomous-smoke (1) Co-authored-by: luke-skywalker-open-claw <262978557+luke-skywalker-open-claw@users.noreply.github.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b470316fc0
commit
4c2fef4a3b
@@ -37,6 +37,39 @@ import { testApi, usageHandlers } from "./usage.js";
|
||||
|
||||
describe("gateway usage helpers", () => {
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
const costSummary = (params: { date?: string; totalTokens: number; totalCost: number }) => ({
|
||||
updatedAt: Date.now(),
|
||||
days: 1,
|
||||
daily: [
|
||||
{
|
||||
date: params.date ?? "2026-02-01",
|
||||
input: params.totalTokens,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: params.totalTokens,
|
||||
totalCost: params.totalCost,
|
||||
inputCost: params.totalCost,
|
||||
outputCost: 0,
|
||||
cacheReadCost: 0,
|
||||
cacheWriteCost: 0,
|
||||
missingCostEntries: 0,
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
input: params.totalTokens,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: params.totalTokens,
|
||||
totalCost: params.totalCost,
|
||||
inputCost: params.totalCost,
|
||||
outputCost: 0,
|
||||
cacheReadCost: 0,
|
||||
cacheWriteCost: 0,
|
||||
missingCostEntries: 0,
|
||||
},
|
||||
});
|
||||
|
||||
function expectUtcDateRange(
|
||||
range: ReturnType<typeof testApi.parseDateRange>,
|
||||
@@ -257,4 +290,64 @@ describe("gateway usage helpers", () => {
|
||||
expect.objectContaining({ agentId: "research" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("aggregates usage.cost only for explicit all-agent scope", async () => {
|
||||
vi.mocked(loadCostUsageSummaryFromCache).mockImplementation(async (params) =>
|
||||
params?.agentId === "opus"
|
||||
? costSummary({ totalTokens: 20, totalCost: 2 })
|
||||
: costSummary({ totalTokens: 10, totalCost: 1 }),
|
||||
);
|
||||
|
||||
const config = {
|
||||
agents: { list: [{ id: "main" }, { id: "opus" }] },
|
||||
session: {},
|
||||
} as OpenClawConfig;
|
||||
const context = { getRuntimeConfig: () => config };
|
||||
const params = { startDate: "2026-02-01", endDate: "2026-02-01", mode: "utc" };
|
||||
|
||||
const defaultRespond = vi.fn();
|
||||
await usageHandlers["usage.cost"]({
|
||||
respond: defaultRespond,
|
||||
params,
|
||||
context,
|
||||
} as unknown as Parameters<(typeof usageHandlers)["usage.cost"]>[0]);
|
||||
|
||||
expect(vi.mocked(loadCostUsageSummaryFromCache)).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(loadCostUsageSummaryFromCache).mock.calls[0]?.[0]?.agentId).toBeUndefined();
|
||||
expect(defaultRespond.mock.calls[0]?.[1]).toMatchObject({
|
||||
totals: { totalTokens: 10, totalCost: 1 },
|
||||
});
|
||||
|
||||
const aggregateRespond = vi.fn();
|
||||
await usageHandlers["usage.cost"]({
|
||||
respond: aggregateRespond,
|
||||
params: { ...params, agentScope: "all" },
|
||||
context,
|
||||
} as unknown as Parameters<(typeof usageHandlers)["usage.cost"]>[0]);
|
||||
|
||||
expect(vi.mocked(loadCostUsageSummaryFromCache)).toHaveBeenCalledTimes(3);
|
||||
expect(
|
||||
vi.mocked(loadCostUsageSummaryFromCache)
|
||||
.mock.calls.slice(1)
|
||||
.map((call) => call[0]?.agentId),
|
||||
).toEqual(["main", "opus"]);
|
||||
expect(aggregateRespond.mock.calls[0]?.[0]).toBe(true);
|
||||
expect(aggregateRespond.mock.calls[0]?.[1]).toMatchObject({
|
||||
totals: { totalTokens: 30, totalCost: 3 },
|
||||
daily: [{ date: "2026-02-01", totalTokens: 30, totalCost: 3 }],
|
||||
});
|
||||
|
||||
const mainRespond = vi.fn();
|
||||
await usageHandlers["usage.cost"]({
|
||||
respond: mainRespond,
|
||||
params: { ...params, agentId: "main" },
|
||||
context,
|
||||
} as unknown as Parameters<(typeof usageHandlers)["usage.cost"]>[0]);
|
||||
|
||||
expect(vi.mocked(loadCostUsageSummaryFromCache)).toHaveBeenCalledTimes(4);
|
||||
expect(vi.mocked(loadCostUsageSummaryFromCache).mock.calls[3]?.[0]?.agentId).toBe("main");
|
||||
expect(mainRespond.mock.calls[0]?.[1]).toMatchObject({
|
||||
totals: { totalTokens: 10, totalCost: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withEnvAsync } from "../../../../src/test-utils/env.js";
|
||||
import { formatDayLabel, formatFullDate } from "./usage-metrics.ts";
|
||||
|
||||
describe("usage metrics date labels", () => {
|
||||
it("formats YYYY-MM-DD values as stable calendar dates in negative UTC offsets", async () => {
|
||||
await withEnvAsync({ TZ: "America/Los_Angeles" }, async () => {
|
||||
const date = new Date(2026, 1, 1);
|
||||
expect(formatDayLabel("2026-02-01")).toBe(
|
||||
date.toLocaleDateString(undefined, { month: "short", day: "numeric" }),
|
||||
);
|
||||
expect(formatFullDate("2026-02-01")).toBe(
|
||||
date.toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves invalid day labels unchanged", () => {
|
||||
expect(formatDayLabel("2026-02-31")).toBe("2026-02-31");
|
||||
expect(formatFullDate("2026-02-31")).toBe("2026-02-31");
|
||||
});
|
||||
});
|
||||
@@ -452,8 +452,19 @@ function parseYmdDate(dateStr: string): Date | null {
|
||||
return null;
|
||||
}
|
||||
const [, y, m, d] = match;
|
||||
const date = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)));
|
||||
return Number.isNaN(date.valueOf()) ? null : date;
|
||||
const year = Number(y);
|
||||
const monthIndex = Number(m) - 1;
|
||||
const day = Number(d);
|
||||
const date = new Date(year, monthIndex, day);
|
||||
if (
|
||||
Number.isNaN(date.valueOf()) ||
|
||||
date.getFullYear() !== year ||
|
||||
date.getMonth() !== monthIndex ||
|
||||
date.getDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
function formatDayLabel(dateStr: string): string {
|
||||
|
||||
Reference in New Issue
Block a user