feat(gateway-cli): scope usage-cost by agent (#94483)

* feat(gateway-cli): scope usage-cost by agent

The `gateway usage-cost` CLI only sent `{ days }` to the `usage.cost` RPC, so
callers could not break cost down per agent or aggregate across all agents the
way the Control UI can. Add `--agent <id>` (forwards `agentId`, scoping to one
agent) and `--all-agents` (forwards `agentScope: "all"`, aggregating every
agent). The two are mutually exclusive because the gateway honors `agentScope`
only when no `agentId` is set; passing both now errors instead of silently
dropping `--all-agents`. No flag keeps the existing default-agent behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gateway-cli): scope usage-cost by agent

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
This commit is contained in:
ly-wang19 2026-06-23 06:05:33 +08:00 committed by GitHub
parent 739e6cbbf8
commit 75af913ba6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 1 deletions

View file

@ -181,12 +181,20 @@ Fetch usage-cost summaries from session logs.
```bash
openclaw gateway usage-cost
openclaw gateway usage-cost --days 7
openclaw gateway usage-cost --agent work --json
openclaw gateway usage-cost --all-agents
openclaw gateway usage-cost --json
```
<ParamField path="--days <days>" type="number" default="30">
Number of days to include.
</ParamField>
<ParamField path="--agent <id>" type="string">
Scope the cost summary to one configured agent id.
</ParamField>
<ParamField path="--all-agents" type="boolean">
Aggregate the cost summary across all configured agents. Cannot be combined with `--agent`.
</ParamField>
### `gateway stability`

View file

@ -213,6 +213,55 @@ describe("gateway-cli coverage", () => {
});
});
it("scopes usage-cost to a specific agent via --agent", async () => {
callGateway.mockClear();
await runGatewayCommand(["gateway", "usage-cost", "--agent", "alpha", "--days", "7", "--json"]);
expect(callGateway).toHaveBeenCalledTimes(1);
const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown };
expect(costCall?.method).toBe("usage.cost");
expect(costCall?.params).toEqual({ days: 7, agentId: "alpha" });
});
it("omits agentId from usage-cost when --agent is absent or blank", async () => {
callGateway.mockClear();
await runGatewayCommand(["gateway", "usage-cost", "--agent", " ", "--days", "7", "--json"]);
expect(callGateway).toHaveBeenCalledTimes(1);
const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown };
expect(costCall?.method).toBe("usage.cost");
expect(costCall?.params).toEqual({ days: 7 });
});
it("aggregates usage-cost across agents via --all-agents", async () => {
callGateway.mockClear();
await runGatewayCommand(["gateway", "usage-cost", "--all-agents", "--days", "7", "--json"]);
expect(callGateway).toHaveBeenCalledTimes(1);
const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown };
expect(costCall?.method).toBe("usage.cost");
expect(costCall?.params).toEqual({ days: 7, agentScope: "all" });
});
it("rejects combining --agent with --all-agents for usage-cost", async () => {
callGateway.mockClear();
await expectGatewayExit([
"gateway",
"usage-cost",
"--agent",
"alpha",
"--all-agents",
"--json",
]);
expect(callGateway).not.toHaveBeenCalled();
expect(runtimeErrors.join("\n")).toContain("Use --agent or --all-agents, not both");
});
it("writes JSON for gateway health transport failures in JSON mode", async () => {
const error = new Error("gateway closed (1006)");
const payload = {

View file

@ -556,12 +556,24 @@ export function registerGatewayCli(program: Command) {
.command("usage-cost")
.description("Fetch usage cost summary from session logs")
.option("--days <days>", "Number of days to include", "30")
.option("--agent <id>", "Scope the cost summary to a specific agent id")
.option("--all-agents", "Aggregate the cost summary across all agents", false)
.action(async (opts, command) => {
await runGatewayCommand(
async () => {
const rpcOpts = resolveGatewayRpcOptions(opts, command);
const days = parseDaysOption(opts.days);
const result = await callGatewayCli("usage.cost", rpcOpts, { days });
const agentId = typeof opts.agent === "string" ? opts.agent.trim() : undefined;
// The gateway honors agentScope only when no agentId is set, so reject the
// ambiguous combination here instead of silently dropping --all-agents.
if (agentId && opts.allAgents) {
throw new Error("Use --agent or --all-agents, not both");
}
const result = await callGatewayCli("usage.cost", rpcOpts, {
days,
...(agentId ? { agentId } : {}),
...(opts.allAgents ? { agentScope: "all" } : {}),
});
if (rpcOpts.json) {
defaultRuntime.writeJson(result);
return;