feat(management): add GET /auth-files/quota for provider-reported usage windows - #5434
feat(management): add GET /auth-files/quota for provider-reported usage windows#5434deathemperor wants to merge 4 commits into
Conversation
|
This pull request targeted The base branch has been automatically changed to |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d227ed11f7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| req.Header.Set("Accept", "application/json") | ||
|
|
||
| httpClient := &http.Client{ | ||
| Timeout: defaultAPICallTimeout, |
There was a problem hiding this comment.
Restrict the timeout to credential acquisition
When Anthropic establishes the connection but takes more than 60 seconds to send headers or finish the body, http.Client.Timeout aborts that subsequent network I/O and reports request failed for an otherwise valid credential. Repository policy permits timeouts only during credential acquisition and exempts the management APICall timeout specifically in api_tools.go, not this quota handler; rely on request cancellation or a connection/handshake-scoped timeout instead.
AGENTS.md reference: AGENTS.md:L58-L58
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 4faba38: dropped the client-wide timeout. Connection setup stays bounded by the transport's dial/TLS handshake timeouts; after that the request ends with the response or the caller's context (management client disconnecting). Branch also rebased onto dev after the retarget.
…ge windows The auth file listing already exposes a `quota` field, but it is observed from response headers as traffic flows through the proxy: it is empty for an idle credential and never carries the reset time of a window. Every client that wants to show "how much of this Claude account is left" ends up doing the same thing: list auth files, then call `/api-call` with `$TOKEN$` once per credential against api.anthropic.com/api/oauth/usage and parse the Anthropic payload itself. This adds one endpoint that does that fan-out inside the proxy and returns the result normalized: one entry per Claude OAuth credential, one window per rate-limit bucket (`kind`, `utilization`, `resets_at`, optional model `scope`). `name` and `auth_index` narrow the set like the other auth-files endpoints. A failing upstream call yields a per-entry `error` and the upstream status instead of failing the whole request; upstream error bodies are never forwarded, and the response body is capped at 2 MiB. The request reuses `apiCallTransport` and `defaultAPICallTimeout` from `/api-call`, so it goes through the same proxy resolution and timeout that management calls already use. API-key Claude credentials and other providers are skipped: only an OAuth access token can query this endpoint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Repository policy allows timeouts only during credential acquisition; the management APICall timeout is an explicit exception and this handler is not. Connection setup stays bounded by the transport's dial and TLS handshake timeouts; after that the request ends with the response or the caller's context. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d227ed1 to
4faba38
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4faba38530
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // established the request only ends with the response or the caller's | ||
| // context (the management client disconnecting). Repository policy | ||
| // allows timeouts during credential acquisition only. | ||
| httpClient := &http.Client{Transport: h.apiCallTransport(auth, "")} |
There was a problem hiding this comment.
Route usage through the Claude OAuth client
When Anthropic applies the same Cloudflare bot checks used by its other OAuth control-plane endpoints, this generic apiCallTransport request can reject valid credentials. The established path in internal/auth/claude/anthropic_auth.go:174-220 explicitly uses the Firefox-uTLS transport and Axios-shaped headers for Anthropic domains, whereas this request omits both; expose the usage fetch through that Claude OAuth client and include /api/oauth/usage in its authenticated control-plane request handling.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Done in 19cf19e: the usage request now goes through the Claude OAuth client. ClaudeAuth.FetchOAuthUsage sits next to FetchOAuthProfile/FetchOAuthRoles on the same Axios-shaped control-plane helper (Firefox-uTLS transport, Axios headers, plus the anthropic-beta header the usage endpoint needs), and the helper now returns an OAuthStatusError on non-2xx so the handler can still report the upstream status code per entry. The per-credential proxy_url is honored via NewClaudeAuthWithProxyURL. The handler-side claudeOAuthUsageURL override is gone; tests fake the client behind a package seam instead, and the claude package has its own tests for the request shape and status propagation.
The quota endpoint used a generic apiCallTransport; Anthropic's OAuth control plane expects the Firefox-uTLS transport and Axios-shaped headers the login companion calls already use. Add ClaudeAuth.FetchOAuthUsage next to FetchOAuthProfile/Roles (same helper, plus the anthropic-beta header) and an OAuthStatusError so the handler can still report the upstream status code. Per-credential proxy_url is honored through NewClaudeAuthWithProxyURL. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19cf19e9c7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| body, errFetch := o.fetchOAuthControlPlaneJSON(ctx, UsageURL, accessToken, "usage", | ||
| map[string]string{"anthropic-beta": UsageBeta}) |
There was a problem hiding this comment.
Bound usage response bodies before reading
When the usage endpoint returns an unexpectedly large or compression-bomb response, this call passes it through fetchOAuthControlPlaneJSON to readClaudeOAuthResponseBody, which uses unbounded io.ReadAll both before and after decompression. Because one management request performs up to four of these reads concurrently, it can consume unbounded memory despite the endpoint's stated 2 MiB cap; use a size-limited read path for usage responses and enforce the limit on decoded content as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 3ec7035: readClaudeOAuthResponseBodyLimited caps the body at 2 MiB (maxOAuthControlPlaneBodyBytes) both on the wire and on the decoded stream — the cap is applied through io.LimitReader at each layer, so a compression bomb is refused at the decoder rather than after inflating. Used for every control-plane JSON fetch (profile, roles, usage); an over-limit reply is an error, never a truncated success. Test covers a plain oversize body and a gzip bomb (3 MiB of zeros, a few KB on the wire).
…ression readClaudeOAuthResponseBodyLimited caps each layer at 2 MiB for the profile/roles/usage fetches, so an oversized or compression-bomb reply is refused at the decoder instead of inflating in memory — the management quota endpoint fans several of these reads out at once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ec70357c4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| body, errFetch := o.fetchOAuthControlPlaneJSON(ctx, UsageURL, accessToken, "usage", | ||
| map[string]string{"anthropic-beta": UsageBeta}) |
There was a problem hiding this comment.
Route usage through the authenticated GET header order
When Anthropic's fingerprint checks validate the wire-level Axios request shape, this usage call still uses the refresh-request header order: claudeOAuthRequestHeaderOrder in internal/auth/claude/utls_transport.go:48-61 recognizes only /api/oauth/profile and /api/oauth/claude_cli/roles, so /api/oauth/usage falls through despite carrying authorization headers. This can cause valid credentials to be rejected, and the new test cannot detect it because it inspects the pre-transport http.Request rather than serialized header order. Fresh evidence after the earlier fix is that the final code routes through ClaudeAuth but still omits the usage path from claudeOAuthInspectTargets; add that target and verify its selected wire order.
Useful? React with 👍 / 👎.
What
Adds
GET /v0/management/auth-files/quota: the provider's own usage report for each Claude OAuth credential, normalized to one window per rate-limit bucket.{ "files": [ { "name": "claude-alice.json", "auth_index": "a1b2c3", "provider": "claude", "email": "alice@example.com", "observed_at": "2026-09-03T05:12:41Z", "status_code": 200, "windows": [ {"kind": "session", "utilization": 100, "resets_at": "2026-09-02T16:19:59+00:00"}, {"kind": "weekly_all", "utilization": 52, "resets_at": "2026-09-07T14:59:59+00:00"}, {"kind": "weekly_scoped", "utilization": 51, "resets_at": "2026-09-07T14:59:59+00:00", "scope": "Opus"} ] }, { "name": "claude-bob.json", "auth_index": "d4e5f6", "provider": "claude", "observed_at": "2026-09-03T05:12:41Z", "status_code": 401, "error": "upstream returned status 401" } ] }name/auth_indexquery parameters narrow the set, same semantics as the otherauth-filesendpoints.api.anthropic.com/api/oauth/usagewith the credential's access token and theoauth-2025-04-20beta header.kindkeeps Anthropic's vocabulary fromlimits[](session,weekly_all,weekly_scoped); a payload withoutlimitsfalls back tofive_hour/seven_day, mapped to the same names.errorplus the upstream status, never a failed request; upstream error bodies are not forwarded; response bodies are capped at 2 MiB.Why
The
quotafield in the auth file listing is header-observed: it stays empty for an idle credential and never carries a reset time. Anyone who wants to show "how much of this account is left" (CPAMC, menu bar apps, dashboards) currently lists auth files, then calls/api-callwith$TOKEN$once per credential and parses the Anthropic payload client-side. Every client reimplements the same fan-out. Doing it once in the proxy makes the answer one request, with one schema, for every client.The endpoint adds no new trust surface: everything it does is already reachable through
/api-callwith$TOKEN$, and it is strictly less capable (fixed URL, fixed headers, GET only, nothing forwarded from the response but the parsed numbers).Relation to #4488
#4488 (
GET /v1/account/limits) captures Anthropic rate-limit headers on the inference port for the calling account. This is a different layer: the management API, per credential, fetched on demand from the provider, and returns reset times and scoped windows the headers don't carry. The two are complementary.Implementation
internal/api/handlers/management/auth_files_quota.go: handler, per-credential fetch, payload normalization. ReusesapiCallTransportfrom/api-call, so proxy resolution is identical to existing management calls. No client-wide timeout (repository policy: timeouts only during credential acquisition): connection setup is bounded by the transport's dial/TLS handshake timeouts, and after that the request ends with the response or the caller's context. The usage URL is a package-levelvar(same pattern asantigravityOAuthTokenURL) so tests can point it at a local server.internal/api/handlers/management/auth_files_quota_endpoint_test.go:httptestupstream; asserts the bearer token and beta header reach the provider, API-key and non-Claude credentials are excluded,auth_indexfilters, a 401 credential reports its own error while its sibling still returns windows, and the legacyfive_hour/seven_dayshape parses.internal/api/server_management.go: one route line.Verified with
gofmt -l,go vet ./internal/api/...,go build -o test-output ./cmd/server && rm test-output,go test ./..., andgo test -race -count=5on the new tests.🤖 Generated with Claude Code