Skip to content

Commit 6079e7f

Browse files
Alix-007steipete
authored andcommitted
fix(gateway): scope usage cost by agent filter
1 parent 0d7c550 commit 6079e7f

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

src/gateway/server-methods/usage.cost-usage-cache.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ describe("costUsageCache bounded growth", () => {
8686
// oldest-first, never the newest.
8787
const lastStartMs = Date.UTC(2026, 0, 1) + (ITERATIONS - 1) * DAY_MS;
8888
const lastEndMs = lastStartMs + ((ITERATIONS - 1) % 3 === 0 ? DAY_MS : 7 * DAY_MS) - 1;
89-
const lastCacheKey = `${lastStartMs}-${lastEndMs}`;
89+
const lastCacheKey = `__all__:${lastStartMs}-${lastEndMs}`;
9090
expect(testApi.costUsageCache.has(lastCacheKey)).toBe(true);
9191

9292
// Tertiary: the oldest entry must have been evicted once the cap was
9393
// exceeded. Pre-fix all 600 entries remain and this fails too.
9494
const firstStartMs = Date.UTC(2026, 0, 1);
9595
const firstEndMs = firstStartMs + DAY_MS - 1;
96-
const firstCacheKey = `${firstStartMs}-${firstEndMs}`;
96+
const firstCacheKey = `__all__:${firstStartMs}-${firstEndMs}`;
9797
expect(testApi.costUsageCache.has(firstCacheKey)).toBe(false);
9898
});
9999

@@ -125,7 +125,7 @@ describe("costUsageCache bounded growth", () => {
125125
});
126126
await Promise.resolve();
127127

128-
expect(testApi.costUsageCache.has("1-2")).toBe(true);
128+
expect(testApi.costUsageCache.has("__all__:1-2")).toBe(true);
129129
expect(mocks.loadCostUsageSummaryFromCache).toHaveBeenCalledTimes(257);
130130
void inFlight.catch(() => {});
131131
void repeated.catch(() => {});

src/gateway/server-methods/usage.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ vi.mock("../../infra/session-cost-usage.js", async () => {
1818
});
1919

2020
import { loadCostUsageSummaryFromCache } from "../../infra/session-cost-usage.js";
21-
import { testApi } from "./usage.js";
21+
import { testApi, usageHandlers } from "./usage.js";
2222

2323
describe("gateway usage helpers", () => {
2424
const dayMs = 24 * 60 * 60 * 1000;
@@ -161,4 +161,50 @@ describe("gateway usage helpers", () => {
161161
"background",
162162
);
163163
});
164+
165+
it("keeps cost usage cache entries scoped by agentId", async () => {
166+
const config = {} as OpenClawConfig;
167+
168+
await testApi.loadCostUsageSummaryCached({
169+
startMs: 1,
170+
endMs: 2,
171+
config,
172+
agentId: "main",
173+
});
174+
await testApi.loadCostUsageSummaryCached({
175+
startMs: 1,
176+
endMs: 2,
177+
config,
178+
agentId: "research",
179+
});
180+
await testApi.loadCostUsageSummaryCached({
181+
startMs: 1,
182+
endMs: 2,
183+
config,
184+
agentId: "research",
185+
});
186+
187+
expect(vi.mocked(loadCostUsageSummaryFromCache)).toHaveBeenCalledTimes(2);
188+
expect(vi.mocked(loadCostUsageSummaryFromCache).mock.calls.at(0)?.[0]).toMatchObject({
189+
agentId: "main",
190+
});
191+
expect(vi.mocked(loadCostUsageSummaryFromCache).mock.calls.at(1)?.[0]).toMatchObject({
192+
agentId: "research",
193+
});
194+
});
195+
196+
it("passes usage.cost agentId through to the cost summary loader", async () => {
197+
const respond = vi.fn();
198+
199+
await usageHandlers["usage.cost"]({
200+
respond,
201+
params: { startDate: "2026-02-01", endDate: "2026-02-02", agentId: "research" },
202+
context: { getRuntimeConfig: () => ({}) },
203+
} as unknown as Parameters<(typeof usageHandlers)["usage.cost"]>[0]);
204+
205+
expect(respond).toHaveBeenCalledWith(true, expect.any(Object), undefined);
206+
expect(vi.mocked(loadCostUsageSummaryFromCache)).toHaveBeenCalledWith(
207+
expect.objectContaining({ agentId: "research" }),
208+
);
209+
});
164210
});

src/gateway/server-methods/usage.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,9 @@ async function loadCostUsageSummaryCached(params: {
720720
startMs: number;
721721
endMs: number;
722722
config: OpenClawConfig;
723+
agentId?: string;
723724
}): Promise<CostUsageSummary> {
724-
const cacheKey = `${params.startMs}-${params.endMs}`;
725+
const cacheKey = `${params.agentId ?? "__all__"}:${params.startMs}-${params.endMs}`;
725726
const now = Date.now();
726727
const cached = costUsageCache.get(cacheKey);
727728
if (
@@ -745,6 +746,7 @@ async function loadCostUsageSummaryCached(params: {
745746
startMs: params.startMs,
746747
endMs: params.endMs,
747748
config: params.config,
749+
agentId: params.agentId,
748750
requestRefresh: true,
749751
refreshMode: "background",
750752
})
@@ -832,7 +834,8 @@ export const usageHandlers: GatewayRequestHandlers = {
832834
mode: params?.mode,
833835
utcOffset: params?.utcOffset,
834836
});
835-
const summary = await loadCostUsageSummaryCached({ startMs, endMs, config });
837+
const agentId = normalizeOptionalString(params?.agentId);
838+
const summary = await loadCostUsageSummaryCached({ startMs, endMs, config, agentId });
836839
respond(true, summary, undefined);
837840
},
838841
"sessions.usage": async ({ respond, params, context }) => {

0 commit comments

Comments
 (0)