Harvest quota headers on relay transports - #130
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 30 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
participant Plugin as Plugin (onResponseHeaders)
participant Relay as sendViaRelay
participant Worker as Relay Worker
participant Anthropic as Anthropic API
participant QuotaMgr as QuotaManager
participant Storage as AccountStorage
participant Sidebar as Sidebar State
Note over Plugin,Sidebar: NEW: Harvest quota headers from relay transports
Plugin->>Relay: request() with onResponseHeaders callback
Note over Relay: transports: HTTP, WebSocket, or fallback
alt HTTP relay
Relay->>Worker: POST /v1/messages (patched body)
Worker->>Anthropic: forward request
Anthropic-->>Worker: response with upstream headers (e.g. anthropic-ratelimit-unified-*)
Worker-->>Relay: relay response (status, headers, body)
Note over Relay: CHANGED: onResponseHeaders fired with relay response headers
Relay->>Relay: onResponseHeaders(response.headers)
Relay-->>Plugin: Response (via sendViaRelay return)
else WebSocket relay
Relay->>Worker: WebSocket connect + send payload
Worker->>Anthropic: forward request
Anthropic-->>Worker: upstream response headers
Worker-->>Relay: response_start control message (status, headers)
Note over Relay: CHANGED: onResponseHeaders fired once from response_start headers
Relay->>Relay: onResponseHeaders(new Headers(message.headers))
Worker-->>Relay: stream body (SSE)
Relay-->>Plugin: Response (optimistic for early resolution)
else relay-to-direct fallback
Note over Relay: No onResponseHeaders call (direct path harvests independently)
Relay->>Plugin: fallback() Response (direct Anthropic request)
Note over Plugin: Existing direct harvest path continues unchanged
end
Note over Plugin,Storage: Header harvest (fired inside onResponseHeaders callback)
Plugin->>Plugin: isQuotaBearingHeaderFrame(headers) ?
alt Has quota headers
Plugin->>Plugin: normalizeQuotaHeaders(headers)
Note over Plugin: Merge header snapshot with existing poll state<br/>Preserve scoped, extraUsage, poll bindingWindow
Plugin->>QuotaMgr: pushMainFromHeaders(accessToken, incoming)
QuotaMgr->>QuotaMgr: mergeHeaderQuotaSnapshot(existing, incoming)
QuotaMgr->>Storage: persist quota (mainQuota)
Plugin->>Storage: persist quota for fallback accounts (if applicable)
Plugin->>Sidebar: refresh sidebar state
else No quota headers
Plugin->>Plugin: trace skip (no harvest)
end
Note over Plugin,Storage: Profile hydration (for tier labels)
opt On boot or /claude-quota command
Plugin->>Plugin: ensureProfilesForQuotaDisplay()
Plugin->>Anthropic: GET /api/oauth/profile (with access token)
Anthropic-->>Plugin: profile data (tier, orgType)
Plugin->>Storage: persist mainProfile / account profiles
Plugin->>Sidebar: update tierLabel
end
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
bf8749c to
e3f231c
Compare
|
The relay propagation design is directionally sound. I tested it stacked on #129 and the current sticky-routing work across HTTP relay, optimistic WebSocket relay, relay-to-direct fallback, typecheck, build, unit tests, and E2E. This should remain blocked on #129 because it depends on that quota-harvesting API and inherits the unresolved persistence/test-isolation issues there. Please also make Once #129 is corrected and rebased, I will re-check this stack together. |
25f0b82 to
a58b9c7
Compare
|
Done in a58b9c7, rebased onto the corrected #129 (9f1b85a).
Regression covers your exact sequence: One declared edge: if the connection drops after |
e856931 to
2f07769
Compare
|
The updated callback semantics resolve my previous finding. Headers are staged until the response commits, a pre-byte recovery replaces them with the retry response's headers, duplicate I validated the #129/#130 stack with two consecutive full unit-suite runs (1,032 passing each), typecheck, and an isolated E2E run (23 passing). I found no additional #130-specific blocker. Please keep this stacked on #129 and rebase it after #129 replaces the broad fallback-account profile save with the token-checked profile-only mutation described there. I will revalidate both together after that update. |
2f07769 to
62118a4
Compare
|
No new relay-specific blocker was found in the latest review. PR #128 has now merged, and #129 has one remaining conditional profile-clear race documented in the new review comment there. Please restack this branch after #129 fixes that race and rebases onto current |
62118a4 to
e4e2a55
Compare
|
Restacked on the corrected #129 head (which is now rebased onto current Gates green on the restacked head: typecheck · opencode 955 · core 58 · pi 53 · e2e 23 · lint clean. Ready for the combined #129/#130 gate. |
Harvest anthropic-ratelimit-unified-* headers in the fetch/relay path and normalize them into the shared OAuthQuotaSnapshot alongside poll data; surface account tier/credits/failover metadata in the expanded sidebar. Token-fenced profile-only persistence with delayed-clear protection. Profile persistence is detached from the /claude-quota and /claude-account display path (render immediately, persist fire-and-forget) so an optional tier-label save can never block a command on the account write lock.
e4e2a55 to
fa083f7
Compare
|
Resolved the profile-persistence-blocks-display finding on the #129 base (this branch is restacked on it). Restacked head gated green: typecheck · opencode 956 · e2e 23 · lint clean. |
Relay responses now feed the passive quota-header harvester: HTTP relay responses expose upstream headers directly, and the WebSocket relay delivers genuine upstream headers from response_start control messages via an onResponseHeaders callback in sendViaRelay. Synthetic optimistic response headers are never harvested; relay-to-direct fallback keeps header harvesting on the direct path. The WebSocket callback is idempotent per logical request with last-accepted-response wins: headers are staged at response_start and fire exactly once when the response commits (first downstream bytes or completion). A pre-byte recovery that resends full_sync replaces the staged headers with the retry's frame — the response that actually streams is also the fresher quota evidence — and duplicate response_start frames within one attempt are ignored.
fa083f7 to
9a1a72f
Compare
|
Integrated the relay-only commit onto Post-integration validation passed: typecheck, lint, format, 956 OpenCode/core tests, 53 Pi tests, 23 end-to-end tests, and the packed TUI smoke test. Closing this PR as integrated. |
Stacked on #129 (first 14 commits are that PR; merge it first — this diff then reduces to 5 commits).
#129 gated quota-header harvesting to direct fetches. This extends it to both relay transports, closing the gap where enabling the relay silently stops passive quota freshening:
sendViaRelaygains an optionalonResponseHeaderscallback that delivers genuine upstream response headers to the caller:response_startcontrol message (the Worker already forwards real upstream headers there). The optimistically-resolvedResponsekeeps its synthetic headers; the callback is the only channel that carries the real ones. Fires at most once per request, survives retry-before-response, never fires on error paths, and a throwing callback cannot break the stream.source: headersbound to the served account, and header-less relay responses leave the store untouched. The previous direct-only e2e assertion was timing-dependent (it read state before async persistence); it now uses a condition-based wait.Tests: core relay callback lifecycle (once-per-request, retry carry-over, error paths, optimistic mode), opencode wiring (single push on fallback, synthetic headers never harvested), Miniflare Worker round-trip asserting
response_startcarries the unified quota headers, e2e both transports. Full suites green (core 49, opencode 876, pi 48, e2e 6).Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Extends passive quota-header harvesting to relay transports and direct fetches, and normalizes Anthropic unified headers into the shared quota store. Surfaces tier/plan, credits, binding-window, and fallback advice across the UI with token-fenced, fire-and-forget profile persistence so commands never block on profile writes.
New Features
sendViaRelayexposes genuine upstream headers (HTTP + WebSocketresponse_start) viaonResponseHeaders; fires once on commit; ignored on errors, relay→direct fallback, and optimistic frames; retry swaps staged headers so last-accepted wins.quota-headersnormalizesanthropic-ratelimit-unified-*, derives binding-window and fallback advice;QuotaManagermerges header pushes with poll (headers replace five_hour/seven_day; poll remains source for scoped windows, idle fallbacks, and boot).oauth-profilefetch with 7‑day, token‑fingerprint TTL; UI shows tier labels on account lists/dialogs/sidebar/TUI, extra‑usage credits with stable currency formatting and exhaustion, plus a binding marker and fallback advice in quota rows.Bug Fixes
Written for commit 9a1a72f. Summary will update on new commits.
Greptile Summary
This PR extends quota and account metadata handling across relay and display paths. The main changes are:
Confidence Score: 5/5
This looks safe to merge.
Important Files Changed
Reviews (11): Last reviewed commit: "feat: harvest quota headers on relay tra..." | Re-trigger Greptile
Context used (5)