Skip to content

Anthropic quota fetch broken — /api/oauth/usage now returns an array for a bucket value (cannot unmarshal array into AnthropicQuotaEntry) #82

Description

@olaxe

Summary

Since 2026-06-16 ~18:05 UTC, every Anthropic quota poll fails to decode. The Anthropic agent goes healthy=0 and quota tracking stops. Auth is fine — the OAuth token refreshes normally; this is purely a response-schema decode failure.

Error (every poll, ~10 min interval, continuous)

level=ERROR msg="Failed to fetch Anthropic quotas" \
  error="anthropic: invalid response: json: cannot unmarshal array into Go value of type api.AnthropicQuotaEntry"

A daemon restart does not help — it re-fails on the first poll after boot.

Versions affected

  • Reproduced on v2.12.0.
  • Type is unchanged through v2.12.5 (latest) and main → all current versions are affected. (v2.12.1–v2.12.5 changelogs touch Codex/Antigravity/self-update, nothing on the Anthropic quota schema.)

Root cause

AnthropicClient.FetchQuotas (internal/api/anthropic_client.go) GETs https://api.anthropic.com/api/oauth/usage and decodes into:

// internal/api/anthropic_types.go:21
type AnthropicQuotaResponse map[string]*AnthropicQuotaEntry

// internal/api/anthropic_client.go (~line 156)
var quotaResp AnthropicQuotaResponse
if err := json.Unmarshal(body, &quotaResp); err != nil { ... }   // <-- fails here

The error is cannot unmarshal array into AnthropicQuotaEntry (not into map), so the top-level object is intact, but at least one bucket value, which the code expects to be a single AnthropicQuotaEntry object, is now a JSON array (Anthropic appears to have split a window into per-model entries, e.g. seven_day[ {…}, {…} ]). Because the whole map is unmarshalled in one pass, a single array-valued key fails the entire decode → all quota data is lost, not just the affected bucket.

Request used (for repro)

GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer <oauth-access-token>
Content-Type: application/json
anthropic-beta: oauth-2025-04-20
User-Agent: claude-code/2.1.69

Inspect the response: one of the top-level keys now holds [ … ] instead of { … }.

Impact

Anthropic quota tracking is fully down for OAuth/subscription users since the API change. Anything gating on onwatch_agent_healthy / quota metrics fails closed.

Suggested fix

Make the per-bucket value tolerate both shapes. Minimal approach — a wrapper type with a custom UnmarshalJSON that accepts a single object or an array of AnthropicQuotaEntry:

type quotaBucket []*AnthropicQuotaEntry   // always a slice internally

func (b *quotaBucket) UnmarshalJSON(data []byte) error {
    trimmed := bytes.TrimSpace(data)
    if len(trimmed) > 0 && trimmed[0] == '[' {
        var arr []*AnthropicQuotaEntry
        if err := json.Unmarshal(data, &arr); err != nil { return err }
        *b = arr
        return nil
    }
    var one AnthropicQuotaEntry
    if err := json.Unmarshal(data, &one); err != nil { return err }
    *b = []*AnthropicQuotaEntry{&one}
    return nil
}
// type AnthropicQuotaResponse map[string]quotaBucket

Then decide downstream semantics for multi-entry buckets (e.g. expand into per-model keys like seven_day_opus / seven_day_sonnet, or aggregate by max utilization). Happy to PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions