Skip to content

Commit 8274022

Browse files
authored
feat: add Anthropic and OpenAI cost history (#100672)
* feat(providers): add admin cost history * fix(ui): sync cron raw-copy baseline * fix(usage): harden provider cost credentials * fix(ui): refresh raw-copy baseline * docs: refresh documentation map * fix(ci): sync provider usage baselines * fix(ui): hide zero-cost history bars * docs(changelog): defer provider cost note
1 parent 41bea62 commit 8274022

97 files changed

Lines changed: 2705 additions & 164 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/concepts/usage-tracking.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ title: "Usage tracking"
88

99
## What it is
1010

11-
- Pulls provider usage/quota directly from each provider's usage endpoint. No estimated provider billing; only provider-reported plan names, quota windows, balances, spend, budgets, or account-state summaries.
11+
- Pulls provider usage/quota directly from each provider's usage endpoint. No estimated provider billing; only provider-reported plan names, quota windows, balances, spend, budgets, daily cost history, token/model attribution, or account-state summaries.
1212
- Human-readable quota-window output is normalized to `X% left`, even when a provider reports consumed quota, remaining quota, or only raw counts. Providers without resettable quota windows show provider summary text instead (for example a balance).
1313
- Session-level `/status` and the `session_status` tool fall back to the session's transcript log when the live session snapshot is missing token/model data. That fallback fills missing token/cache counters, can recover the active runtime model label, and prefers the larger prompt-oriented total when session metadata is missing or smaller (`totalTokensFresh !== true`, zero, or below the transcript-derived value). Nonzero live values always win over the fallback.
1414

@@ -19,11 +19,20 @@ title: "Usage tracking"
1919
- `/usage cost` in chats: local cost summary aggregated from OpenClaw session logs.
2020
- CLI: `openclaw status --usage` prints a full per-provider usage/quota breakdown.
2121
- CLI: `openclaw models status` lists OAuth/token auth profiles and shows a usage-window summary next to each provider that has one.
22-
- Control UI: **Usage** shows provider plan and billing cards above OpenClaw's session-derived token and estimated-cost analysis.
22+
- Control UI: **Usage** shows provider plan and billing cards above OpenClaw's session-derived token and estimated-cost analysis. Anthropic and OpenAI Admin API credentials add provider-reported today, 7-day, and 30-day spend, daily trends, token totals, top models, and cost categories.
2323
- macOS menu bar: a root "Usage" section appears below Context when provider usage snapshots are available. See [Menu bar](/platforms/mac/menu-bar).
2424

2525
`openclaw channels list` no longer prints provider usage; it points users to `openclaw status` or `openclaw models list` instead.
2626

27+
## Anthropic and OpenAI cost history
28+
29+
Subscription quota and API billing are different provider surfaces:
30+
31+
- Anthropic subscription/setup credentials continue to show Claude quota windows and optional extra-usage budgets. Set `ANTHROPIC_ADMIN_KEY` or `ANTHROPIC_ADMIN_API_KEY` to show organization Usage and Cost API history instead. An Anthropic provider credential beginning with `sk-ant-admin` is detected automatically.
32+
- OpenAI ChatGPT/Codex OAuth continues to show plan, quota windows, and credit balance. Set `OPENAI_ADMIN_KEY` to show organization cost and completions-usage history instead; optionally set `OPENAI_PROJECT_ID` to scope it to one project. OpenClaw never sends inference credentials from `OPENAI_API_KEY`, provider config, or auth profiles to organization APIs because those keys may belong to custom endpoints.
33+
34+
Admin credentials take precedence because they provide actual organization billing. OpenClaw does not combine these provider-reported totals with its local session estimates; the two sections intentionally answer different questions.
35+
2736
## Default usage footer mode
2837

2938
`/usage off|tokens|full` sets the footer for a session and is remembered for that
@@ -296,7 +305,9 @@ provider-neutral for CLI, app, and Control UI consumers.
296305
`user:profile` scope, falls back to a `claude.ai` web session (`CLAUDE_AI_SESSION_KEY`,
297306
`CLAUDE_WEB_SESSION_KEY`, or a `sessionKey=` cookie in `CLAUDE_WEB_COOKIE`) when set.
298307
Model-scoped limits and enabled extra-usage monthly spend/budgets are included
299-
when Anthropic reports them.
308+
when Anthropic reports them. An explicit Anthropic Admin API key, or an
309+
auto-detected `sk-ant-admin...` provider profile, instead shows 30-day
310+
organization cost and Messages API history.
300311
- **ClawRouter**: API key (`CLAWROUTER_API_KEY`). Shows a monthly budget window
301312
and typed USD budget when configured; otherwise shows aggregate spend and a
302313
request/token/cost summary.
@@ -323,8 +334,9 @@ provider-neutral for CLI, app, and Control UI consumers.
323334
- **OpenAI (Codex/ChatGPT plan)**: OAuth tokens in auth profiles (`ChatGPT-Account-Id`
324335
header sent when an account id is present). Shows the ChatGPT plan, resettable
325336
Codex windows, and a credit balance when reported. Credits remain provider
326-
credits; OpenClaw does not label them as dollars. API-key-only OpenAI usage is
327-
not tracked.
337+
credits; OpenClaw does not label them as dollars. `OPENAI_ADMIN_KEY` adds
338+
30-day organization cost and completions-usage history when the key has Usage
339+
Dashboard access. Inference credentials are never forwarded to organization APIs.
328340
- **OpenRouter**: API key or OAuth-backed API key (`OPENROUTER_API_KEY` or an auth
329341
profile). Combines the account credits endpoint with the key quota endpoint,
330342
so account balance/spend, key budget, and daily/weekly/monthly usage appear

docs/docs_map.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,6 +2914,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
29142914
- Headings:
29152915
- H2: What it is
29162916
- H2: Where it shows up
2917+
- H2: Anthropic and OpenAI cost history
29172918
- H2: Default usage footer mode
29182919
- H3: Three distinct session states
29192920
- H3: Precedence
@@ -7200,6 +7201,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
72007201

72017202
- Route: /providers/anthropic
72027203
- Headings:
7204+
- H2: Usage and cost tracking
72037205
- H2: Getting started
72047206
- H2: Thinking defaults (Claude Fable 5, 4.8, and 4.6)
72057207
- H2: Safety refusal fallback (Claude Fable 5)
@@ -7656,6 +7658,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
76567658

76577659
- Route: /providers/openai
76587660
- Headings:
7661+
- H2: Usage and cost tracking
76597662
- H2: Quick choice
76607663
- H2: Naming map
76617664
- H2: GPT-5.6 limited preview

docs/plugins/architecture-internals.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ fall back to generic credential resolution for usage/status surfaces. Return
330330
must suppress generic API-key/OAuth fallback, and return `null` or `undefined`
331331
when the provider did not handle usage auth.
332332

333+
Declare organization or billing credentials in manifest
334+
`providerUsageAuthEnvVars`. This lets generic discovery and secret-scrubbing
335+
surfaces recognize them without making them inference auth candidates.
336+
333337
### Provider example
334338

335339
```ts

docs/plugins/manifest.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ See [Plugins](/tools/plugin) for the full plugin system guide, and [Capability m
158158
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
159159
| `commandAliases` | No | `object[]` | Command names owned by this plugin that should produce plugin-aware config and CLI diagnostics before runtime loads. |
160160
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Deprecated compatibility env metadata for provider auth/status lookup. Prefer `setup.providers[].envVars` for new plugins; OpenClaw still reads this during the deprecation window. |
161+
| `providerUsageAuthEnvVars` | No | `Record<string, string[]>` | Usage/billing-only provider credentials. OpenClaw uses these names for usage discovery and secret scrubbing but never for inference auth. |
161162
| `providerAuthAliases` | No | `Record<string, string>` | Provider ids that should reuse another provider id for auth lookup, for example a coding provider that shares the base provider API key and auth profiles. |
162163
| `channelEnvVars` | No | `Record<string, string[]>` | Cheap channel env metadata that OpenClaw can inspect without loading plugin code. Use this for env-driven channel setup or auth surfaces that generic startup/config helpers should see. |
163164
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
@@ -458,6 +459,8 @@ When present, `setup.providers` and `setup.cliBackends` are the preferred descri
458459

459460
OpenClaw also includes `setup.providers[].envVars` in generic provider auth and env-var lookups. `providerAuthEnvVars` remains supported through a compatibility adapter during the deprecation window, but non-bundled plugins that still use it receive a manifest diagnostic. New plugins should put setup/status env metadata on `setup.providers[].envVars`.
460461

462+
Use `providerUsageAuthEnvVars` when a billing or organization-level credential must activate `resolveUsageAuth` without becoming an inference credential. These names join workspace dotenv blocking, ACP child-process stripping, sandbox secret filtering, and broad secret scrubbing. The provider runtime still reads and classifies the value inside `resolveUsageAuth`.
463+
461464
OpenClaw can also derive simple setup choices from `setup.providers[].authMethods` when no setup entry is available, or when `setup.requiresRuntime: false` declares setup runtime unnecessary. Explicit `providerAuthChoices` entries stay preferred for custom labels, CLI flags, onboarding scope, and assistant metadata.
462465

463466
Set `requiresRuntime: false` only when those descriptors are sufficient for the setup surface. OpenClaw treats explicit `false` as a descriptor-only contract and will not execute `setup-api` or `openclaw.setupEntry` for setup lookup. If a descriptor-only plugin still ships one of those setup runtime entries, OpenClaw reports an additive diagnostic and continues ignoring it. Omitted `requiresRuntime` keeps legacy fallback behavior so existing plugins that added descriptors without the flag do not break.

docs/providers/anthropic.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ Anthropic builds the **Claude** model family. OpenClaw supports two auth routes:
1010
- **API key** - direct Anthropic API access with usage-based billing (`anthropic/*` models)
1111
- **Claude CLI** - reuse an existing Claude Code login on the same host
1212

13+
## Usage and cost tracking
14+
15+
OpenClaw detects the available Anthropic credential and selects the matching usage surface:
16+
17+
- Claude subscription/setup credentials show quota windows and optional extra-usage budget.
18+
- `ANTHROPIC_ADMIN_KEY` or `ANTHROPIC_ADMIN_API_KEY` shows 30 days of provider-reported organization cost and Messages API usage in Control UI **Usage**, including daily spend, token/cache totals, top models, and cost categories.
19+
- An `sk-ant-admin...` credential stored in the Anthropic provider profile is detected as an Admin API key automatically.
20+
21+
Admin API cost history comes from Anthropic's [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api). It is actual provider billing, separate from OpenClaw's session-derived estimated cost.
22+
1323
<Warning>
1424
OpenClaw's Claude CLI backend runs the installed Claude Code CLI in
1525
non-interactive print mode (`claude -p`). Anthropic's current Claude Code docs

docs/providers/openai.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ compatibility route for agent turns.
2525
OpenAI explicitly supports subscription OAuth usage in external tools and
2626
workflows like OpenClaw.
2727

28+
## Usage and cost tracking
29+
30+
OpenClaw keeps subscription quota and Platform API billing distinct:
31+
32+
- ChatGPT/Codex OAuth shows the subscription plan, quota windows, and credit balance.
33+
- `OPENAI_ADMIN_KEY` shows 30 days of provider-reported organization cost and completions usage in Control UI **Usage**, including daily spend, request/token totals, top models, and cost categories.
34+
- `OPENAI_PROJECT_ID` optionally scopes Admin API history to one project.
35+
- OpenClaw never sends `OPENAI_API_KEY` or an `openai` inference profile to organization APIs; those credentials may belong to custom, Azure, or agent-local endpoints.
36+
37+
An explicit Admin key takes precedence over OAuth. Provider-reported history is not merged with OpenClaw's session-derived estimated cost; it can include API activity from other clients and provider-side billing adjustments.
38+
39+
OpenAI's [API Usage Dashboard](https://help.openai.com/en/articles/10478918) documentation describes the organization-owner and explicit Usage Dashboard permission requirements for usage data.
40+
2841
Provider, model, runtime, and channel are separate layers. If those labels are
2942
getting mixed together, read [Agent runtimes](/concepts/agent-runtimes) before
3043
changing config.

extensions/anthropic/openclaw.plugin.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
},
77
"enabledByDefault": true,
88
"providers": ["anthropic"],
9+
"providerUsageAuthEnvVars": {
10+
"anthropic": ["ANTHROPIC_ADMIN_KEY", "ANTHROPIC_ADMIN_API_KEY"]
11+
},
912
"providerCatalogEntry": "./provider-discovery.ts",
1013
"modelCatalog": {
1114
"runtimeAugment": true,

extensions/anthropic/register.runtime.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import type {
1010
ProviderAuthMethodNonInteractiveContext,
1111
ProviderResolveDynamicModelContext,
1212
ProviderNormalizeResolvedModelContext,
13-
ProviderResolveUsageAuthContext,
14-
ProviderResolvedUsageAuth,
1513
ProviderRuntimeModel,
1614
} from "openclaw/plugin-sdk/plugin-entry";
1715
import {
@@ -37,7 +35,6 @@ import {
3735
supportsClaudeNativeMaxEffort,
3836
supportsClaudeNativeXhighEffort,
3937
} from "openclaw/plugin-sdk/provider-model-shared";
40-
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
4138
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
4239
import * as claudeCliAuth from "./cli-auth-seam.js";
4340
import { buildAnthropicCliBackend } from "./cli-backend.js";
@@ -55,6 +52,7 @@ import {
5552
} from "./config-defaults.js";
5653
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
5754
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
55+
import { fetchAnthropicUsage, resolveAnthropicUsageAuth } from "./usage.js";
5856

5957
const PROVIDER_ID = "anthropic";
6058
type UpsertAuthProfileParams = Parameters<typeof upsertAuthProfileWithLock>[0];
@@ -716,22 +714,6 @@ async function runAnthropicCliMigrationNonInteractive(ctx: {
716714
};
717715
}
718716

719-
async function resolveAnthropicUsageAuth(
720-
ctx: ProviderResolveUsageAuthContext,
721-
): Promise<ProviderResolvedUsageAuth> {
722-
const oauthToken = await ctx.resolveOAuthToken();
723-
if (oauthToken) {
724-
return oauthToken;
725-
}
726-
727-
const apiKey = ctx.resolveApiKeyFromConfigAndStore();
728-
if (apiKey && validateAnthropicSetupToken(apiKey) === undefined) {
729-
return { token: apiKey };
730-
}
731-
732-
return { handled: true };
733-
}
734-
735717
/** Build the full Anthropic provider descriptor used by runtime registration. */
736718
export function buildAnthropicProvider(): ProviderPlugin {
737719
const providerId = "anthropic";
@@ -857,8 +839,7 @@ export function buildAnthropicProvider(): ProviderPlugin {
857839
},
858840
wrapStreamFn: wrapAnthropicProviderStream,
859841
resolveUsageAuth: resolveAnthropicUsageAuth,
860-
fetchUsageSnapshot: async (ctx) =>
861-
await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
842+
fetchUsageSnapshot: fetchAnthropicUsage,
862843
isCacheTtlEligible: () => true,
863844
buildAuthDoctorHint: (ctx) =>
864845
buildAnthropicAuthDoctorHint({

extensions/anthropic/usage.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { fetchAnthropicAdminUsage, resolveAnthropicUsageAuth } from "./usage.js";
3+
4+
function requestUrl(input: string | URL | Request): URL {
5+
return new URL(input instanceof Request ? input.url : input);
6+
}
7+
8+
describe("Anthropic provider usage", () => {
9+
it("aggregates provider-reported costs, cache tokens, models, and categories", async () => {
10+
const fetchFn = vi.fn(async (input: string | URL | Request, _init?: RequestInit) => {
11+
const url = requestUrl(input);
12+
if (url.pathname.endsWith("/organizations/cost_report")) {
13+
return new Response(
14+
JSON.stringify({
15+
data: [
16+
{
17+
starting_at: "2026-07-06T00:00:00Z",
18+
ending_at: "2026-07-07T00:00:00Z",
19+
results: [{ amount: "1234", currency: "USD", description: "Claude API" }],
20+
},
21+
],
22+
has_more: false,
23+
}),
24+
{ status: 200 },
25+
);
26+
}
27+
return new Response(
28+
JSON.stringify({
29+
data: [
30+
{
31+
starting_at: "2026-07-06T00:00:00Z",
32+
ending_at: "2026-07-07T00:00:00Z",
33+
results: [
34+
{
35+
uncached_input_tokens: 1_000,
36+
cache_creation: {
37+
ephemeral_1h_input_tokens: 100,
38+
ephemeral_5m_input_tokens: 50,
39+
},
40+
cache_read_input_tokens: 300,
41+
output_tokens: 250,
42+
model: "claude-opus-4-8",
43+
},
44+
],
45+
},
46+
],
47+
has_more: false,
48+
}),
49+
{ status: 200 },
50+
);
51+
});
52+
53+
const result = await fetchAnthropicAdminUsage({
54+
apiKey: "sk-ant-admin-test",
55+
timeoutMs: 5_000,
56+
fetchFn: fetchFn as typeof fetch,
57+
now: Date.parse("2026-07-06T12:00:00Z"),
58+
periodDays: 2,
59+
});
60+
61+
expect(result).toMatchObject({
62+
provider: "anthropic",
63+
plan: "Admin API",
64+
billing: [{ type: "spend", amount: 12.34, unit: "USD", period: "2d" }],
65+
costHistory: {
66+
unit: "USD",
67+
periodDays: 2,
68+
daily: [
69+
{
70+
date: "2026-07-06",
71+
amount: 12.34,
72+
inputTokens: 1_000,
73+
cacheWriteTokens: 150,
74+
cacheReadTokens: 300,
75+
outputTokens: 250,
76+
totalTokens: 1_700,
77+
},
78+
],
79+
models: [{ name: "claude-opus-4-8", totalTokens: 1_700 }],
80+
categories: [{ name: "Claude API", amount: 12.34 }],
81+
},
82+
});
83+
for (const [input, init] of fetchFn.mock.calls) {
84+
const url = requestUrl(input);
85+
expect(url.searchParams.get("bucket_width")).toBe("1d");
86+
expect((init as RequestInit).headers).toMatchObject({
87+
"anthropic-version": "2023-06-01",
88+
"x-api-key": "sk-ant-admin-test",
89+
});
90+
}
91+
});
92+
93+
it("uses explicit Admin API credentials before Claude OAuth", async () => {
94+
const result = await resolveAnthropicUsageAuth({
95+
config: {},
96+
env: { ANTHROPIC_ADMIN_API_KEY: "sk-ant-admin-explicit" },
97+
provider: "anthropic",
98+
resolveApiKeyFromConfigAndStore: () => "sk-ant-oat01-fallback",
99+
resolveOAuthToken: async () => ({ token: "oauth-token" }),
100+
});
101+
expect(result).toEqual({
102+
token: 'openclaw:anthropic-admin:v1:{"token":"sk-ant-admin-explicit"}',
103+
});
104+
});
105+
106+
it("auto-detects an Admin API key stored in the Anthropic provider profile", async () => {
107+
const result = await resolveAnthropicUsageAuth({
108+
config: {},
109+
env: {},
110+
provider: "anthropic",
111+
resolveApiKeyFromConfigAndStore: () => "sk-ant-admin-profile",
112+
resolveOAuthToken: async () => null,
113+
});
114+
expect(result).toEqual({
115+
token: 'openclaw:anthropic-admin:v1:{"token":"sk-ant-admin-profile"}',
116+
});
117+
});
118+
119+
it("prefers a stored Admin API key when normal API and OAuth credentials coexist", async () => {
120+
const result = await resolveAnthropicUsageAuth({
121+
config: {},
122+
env: {},
123+
provider: "anthropic",
124+
resolveApiKeyFromConfigAndStore: () => "sk-ant-api03-inference",
125+
resolveApiKeyCandidatesFromConfigAndStore: async () => [
126+
"sk-ant-api03-inference",
127+
"sk-ant-admin-billing",
128+
],
129+
resolveOAuthToken: async () => ({ token: "oauth-token" }),
130+
});
131+
expect(result).toEqual({
132+
token: 'openclaw:anthropic-admin:v1:{"token":"sk-ant-admin-billing"}',
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)