Skip to content

feat(management): add GET /auth-files/quota for provider-reported usage windows - #5434

Open
deathemperor wants to merge 4 commits into
router-for-me:devfrom
deathemperor:feat/auth-files-quota
Open

feat(management): add GET /auth-files/quota for provider-reported usage windows#5434
deathemperor wants to merge 4 commits into
router-for-me:devfrom
deathemperor:feat/auth-files-quota

Conversation

@deathemperor

@deathemperor deathemperor commented Sep 3, 2026

Copy link
Copy Markdown

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_index query parameters narrow the set, same semantics as the other auth-files endpoints.
  • Credentials are queried concurrently (4 at a time) against api.anthropic.com/api/oauth/usage with the credential's access token and the oauth-2025-04-20 beta header.
  • Window kind keeps Anthropic's vocabulary from limits[] (session, weekly_all, weekly_scoped); a payload without limits falls back to five_hour / seven_day, mapped to the same names.
  • A failing upstream call yields a per-entry error plus the upstream status, never a failed request; upstream error bodies are not forwarded; response bodies are capped at 2 MiB.
  • Only Claude OAuth credentials are included. API-key Claude entries and other providers are skipped (the endpoint only accepts an OAuth token). Disabled credentials are included, matching the listing.

Why

The quota field 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-call with $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-call with $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. Reuses apiCallTransport from /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-level var (same pattern as antigravityOAuthTokenURL) so tests can point it at a local server.
  • internal/api/handlers/management/auth_files_quota_endpoint_test.go: httptest upstream; asserts the bearer token and beta header reach the provider, API-key and non-Claude credentials are excluded, auth_index filters, a 401 credential reports its own error while its sibling still returns windows, and the legacy five_hour/seven_day shape 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 ./..., and go test -race -count=5 on the new tests.

🤖 Generated with Claude Code

@github-actions
github-actions Bot changed the base branch from main to dev September 3, 2026 02:47
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

deathemperor and others added 2 commits September 3, 2026 13:17
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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, "")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +316 to +317
body, errFetch := o.fetchOAuthControlPlaneJSON(ctx, UsageURL, accessToken, "usage",
map[string]string{"anthropic-beta": UsageBeta})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +321 to +322
body, errFetch := o.fetchOAuthControlPlaneJSON(ctx, UsageURL, accessToken, "usage",
map[string]string{"anthropic-beta": UsageBeta})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant