mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
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:
parent
739e6cbbf8
commit
75af913ba6
3 changed files with 70 additions and 1 deletions
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue