feat(usage): sync Go usage from the official /zen/go/v1/usage endpoint (#130) - #132
Conversation
Pure, unit-testable module for the official OpenCode Go usage endpoint (upstream anomalyco/opencode#16513, verified live in production): - fetchGoUsage(): GET /zen/go/v1/usage with the existing opencode-go key as a Bearer token; classifies failures (unauthorized / no-subscription / not-found / network / invalid) so callers can fall back to local estimates. The key is never logged or persisted. - mergeServerUsage(): overlays the server-accurate rolling/weekly/monthly percent + resetsAt onto a locally-computed UsageSummary. The endpoint does not return raw spend, so spent is derived as limit x percent / 100 to keep the status bar and tooltip consistent; today/yesterday and per-session spend stay local. - GO_LIMITS stays the source of the display limits (they match the server's ZEN_LIMITS).
GoUsageTracker gains syncServerUsage(apiKey): fetches the official usage snapshot and caches it for GO_USAGE_SYNC_TTL_MS, so it is safe to call on every request or status-bar refresh. getSummary() (still synchronous) overlays the cached snapshot onto the local estimate via mergeServerUsage — the status bar, tooltip, quick-pick and webview all pick up the account-wide percentages and reset times automatically. A failed fetch keeps the previous snapshot and falls back to the existing local estimates; today/yesterday and per-session spend stay local.
…efresh - Startup: fetch the server meters once with the stored key (TTL-guarded). - Per request: after recording a Go transport summary, re-sync with the exact key that request ran under (covers native BYOK group keys that differ from the extension secret). - Status bar refresh: background sync with the stored key; when a fresh snapshot lands the status bar repaints with the account-wide numbers. - syncTrackerUsage helper: sync + repaint, used by all three paths.
7 tests for goUsageSync (pure module, no vscode dependency): - sends the key as a Bearer header to the official endpoint URL - parses a 200 payload (including rate-limited status) - classifies 401/403/404/5xx for fallback - refuses a missing key without issuing a request - classifies malformed payloads and network errors - overlays server percent + resetsAt per period - derives spent from the authoritative percent (full limit at 100%) - keeps local today/yesterday and summary metadata
README Usage Tracking section: meters now come from the official /zen/go/v1/usage endpoint (account-wide, includes CLI/other devices), with local fallback; today/yesterday + per-session spend stay device local. CHANGELOG [Unreleased] entry for ltmoerdani#130. Also pace retries after failed syncs too: an invalid key or unreachable endpoint must not hammer the API on every request (the TTL now covers failures as well as successes).
…ctions The usage quick-pick listed "Reset tracked usage data" and "Open OpenCode console" but neither item carried an action marker, so picking them did nothing (handler only knew setUsageTargets/showUsageDetails/switchProfile). - Reset now asks for modal confirmation, calls tracker.clear() (which also clears session costs now), repaints the status bar and webview, and confirms — the 6 device-local values (Today/Yesterday/session) reset, while server-synced meters are unaffected. - Open console opens https://opencode.ai in the browser. - The quick-pick also shows a "Synced from opencode.ai" note when server-accurate meters are in effect.
tracker.clear() emptied the entries, which made getSummary() report hasData=false and collapsed the whole card into the first-run "No data yet" state — the reset wiped the display instead of zeroing the 6 local values. Add a persisted everTracked flag (set on first record and on reset, restored from globalState): after a reset the card stays up with Today/Yesterday/session at $0 · 0 req · 0 tokens while the meters keep their values (server-synced or local). Fresh installs still get the onboarding "No data" card.
- sleep(): drop the settled/redundant post-subscribe check — VS Code's cancellation tokens invoke listeners registered after cancellation (shortcutEvent), so one subscription suffices and Promises ignore double settlement. - signalFromToken(): same redundant race-check removed. - onTransportSummary: remove the always-true if (tracker) guard (ensureProfileForApiKey is non-optional). - GO_MODEL_PRICING: Record<string, ModelCost | undefined> — the table is genuinely incomplete, so the guard was right and the type was wrong. - CachedModelMetadataSnapshot.providers: Record<..., | undefined> with ?. at the two direct access sites — partial snapshots are real. - goUsageSync.test.ts: drop the always-true if (result.ok) after assert.ok() narrowing.
Container fixes for the usage dialog: - Today and Yesterday rows are always rendered (zeros included), so the card no longer shrinks when a day has no usage. - Fixed 440px card width and fixed value columns — previously the card widened and the cost column shifted when session data appeared, making the layout jump between renders. - Uniform spacing: content starts at the same 14px gutter on every side (title top, left/right), right-aligned "Resets in" and percent now sit at the card's right gutter (was 410, leaving a 30px right margin), and the meter blocks, divider and device rows follow a consistent rhythm. - No-data card sized with the same gutters. - Webview renders the card at its natural 440px size instead of stretching it to the container width. - Hover renders the image at its natural 440px width (was 420). Hover action links keep their command-link form (markdown hovers cannot render styled buttons) but now sit behind a divider with even spacing.
The usage webview now renders a compact card: 320px, 6px radius, 14/16px padding, neutral widget colors, three meter sections (label + resets row, 4px progress bar, used/percent row), a divider, Today/Yesterday as three-column stats, and a footer with Set spent targets / Rename links. Theme-aware via VS Code CSS variables.
The status-bar hover card now uses the same compact 320px layout and palette as the usage panel: 16px gutters, 13px/600 title, meter sections with a 4px progress bar (blue fill) and a used/percent row, a full-width divider, and Today/Yesterday as three-column stats.
The 320px reference-style hover card gets wrapped by VS Code's hover widget chrome and cannot look like the standalone design. Restore the previous hover card (stable 440px geometry, always-visible Today and Yesterday rows, consistent 14px gutters, divider and right-aligned resets/percent) while keeping the usage panel as the reference HTML card.
…sage-sync # Conflicts: # src/extension.ts # src/metadata.ts
The card used 14px top/bottom padding against 16px left/right. Make the padding uniform (16px) so the spacing around the content matches on all four sides.
|
Nice one @Fahad090NP, this is a clean implementation. I ran the branch locally before commenting: compile, the strict tsc check, and the full unit suite all pass (188/188), and the 8 new goUsageSync tests hold up. The endpoint was verified live, the fallback-to-local path is the right call, and wiring the two dead quick-pick actions (reset + open console) is a nice catch. Two things I want to poke at before merge.
Minor: No blockers from me. Happy to merge once you've had a look, and I'll keep it as a merge commit so your history stays intact. |
|
Let me do some more changes. I opened this PR too early. Sorry for that. |
- Remove the card's background rect: the hover widget's own background shows through, so there is no visible container edge and the fixed 4px/8px hover padding no longer looks inconsistent (the hover's padding is VS Code chrome and cannot be changed from the extension). - Tighten the Today/Yesterday/session row gap in the stats block. - The Set spent targets / Rename links start at the same left position as the card content (drop the extra indent) so the dialog uses one consistent left margin throughout.
The pre-commit gate previously auto-fixed staged files (prettier --write, eslint --fix, markdownlint --fix). Per policy, linters must never modify or rename files at commit time: a violation now fails the commit with a clear error and the user fixes it (or runs npm run format / lint:fix) explicitly. lint-staged is now prettier --check + eslint --max-warnings 0 + markdownlint without --fix; scripts/staged-lint.ts and npm run lint were already check-only. Verified: an unformatted staged file blocks the commit and stays byte-identical.
The previous commit bundled two UI changes: the hover card's background removal (keep — it resolves the hover padding mismatch) and a tightened Today/Yesterday/session row gap (revert to 24px).
Both actions are reachable from the Command Palette (opencodego.setUsageTargets, opencodego.renameActiveProfile), so the divider + Set spent targets / Rename links below the summary card are removed — the hover now shows the card only, without the bottom section.
The Session (est) row was hidden while no session was active, which made the card height and layout shift. All three rows (Session, Today, Yesterday) are now always rendered with zeros, keeping the card stable.
The panel body now fills the viewport and centers the card on both axes (align-items: center + height: 100vh), so the usage card is centered in the panel instead of pinned to the top.
A fresh install with CLI usage had no local tracking, so the status bar fell back to 'OpenCode Go' / 'No usage data yet' even though the server snapshot carried real account-wide meters. mergeServerUsage now flips hasData to true whenever a server snapshot is applied; regression test covers the fresh-install case.
Startup and a status-bar refresh can fire syncServerUsage at the same moment, issuing two identical fetches. Concurrent calls for the same key now share a single in-flight promise; failures stay TTL-paced.
The status-bar background sync read the extension secret, which can belong to a different account once multiple profiles exist — switching profiles without a request in between pulled the wrong account's meters onto the active profile, and the refresh could race the per-request sync. Profile keys are now remembered per fingerprint (ensureProfileForApiKey), and the refresh path uses the active profile's own key, falling back to the extension secret.
|
All three points addressed and pushed (commits
Ready to merge whenever you are — merge commit works for me. |
|
Nice one, all three fixes check out. I pulled the branch and ran it before commenting: compile, strict tsc, and the full suite all pass at 189/189. The new fresh-install regression test covers exactly the case I was worried about. The profile-key map is a clean solution, and sharing one in-flight promise for concurrent syncs reads well. Flipping One heads-up rather than a blocker: the PR picked up two files outside the usage scope ( Merge commit as usual. |
- docs/issues/62: PR #132 doc (server-accurate /zen/go/v1/usage sync), incl. post-review fixes (profile key, hasData, in-flight dedup), 189/189 - docs/issues/65: consolidated issue #23 timeline (Jun drift -> server sync) - docs/features/03 + docs/issues/13: mark local-only conclusion superseded, add server-accurate section - docs/issues/61: PR #129 strict lint stack + pre-commit gate - docs/issues/64 + issue 48: issue #131 per-model config duplicate models - docs/issues/63: open-PR tracker (#133/#135/#136) - CHANGELOG [Unreleased], README (server-synced meters, model names), devlog, architecture timeline updated
Closes #130
What
The status-bar Session/Weekly/Monthly meters were estimated locally (tokens × pricing + local
opencode.db), which drifted from opencode.ai because CLI, cross-device and pre-install usage was invisible (issue #23). This syncs the meters from the official endpoint instead.Sync layer (
src/goUsageSync.ts, pure + unit-tested):fetchGoUsage(apiKey)—GET /zen/go/v1/usagewith the existing Go key as a Bearer token; failures classified (unauthorized / no-subscription / not-found / network / invalid) so callers fall back to local estimates. The key is never logged or persisted.mergeServerUsage()— overlays server rolling/weekly/monthly percent +resetsAtonto the local summary;spentis derived from the authoritative percent. Today/Yesterday/per-session spend stay device-local.Tracker wiring (
src/goUsageTracker.ts):syncServerUsage(apiKey)with a 60s TTL (failures paced too, so a bad key can't hammer the API);getSummary()stays synchronous and overlays the cached snapshot.Extension wiring:
Dialog fixes found along the way:
tracker.clear()(also clears session costs) → repaint.everTrackedflag keeps the card with zeroed local values instead.if (tracker), and two wrong types:GO_MODEL_PRICINGand the metadataprovidersmap are genuinely partial at runtime).Verification
npm run lint— all 7 steps green;npm test188/188.