Skip to content

feat(usage): sync Go usage from the official /zen/go/v1/usage endpoint (#130) - #132

Merged
ltmoerdani merged 23 commits into
ltmoerdani:mainfrom
Fahad090NP:feat/issue-130-go-usage-sync
Aug 12, 2026
Merged

feat(usage): sync Go usage from the official /zen/go/v1/usage endpoint (#130)#132
ltmoerdani merged 23 commits into
ltmoerdani:mainfrom
Fahad090NP:feat/issue-130-go-usage-sync

Conversation

@Fahad090NP

Copy link
Copy Markdown
Contributor

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/usage with 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 + resetsAt onto the local summary; spent is 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:

  • Startup sync with the stored key; per-request re-sync using the exact key the request ran under (covers native BYOK group keys); background sync on status-bar refresh with repaint on new data.

Dialog fixes found along the way:

  • "Reset tracked usage data" was never wired — picking it did nothing. Now: modal confirm → tracker.clear() (also clears session costs) → repaint.
  • Reset previously collapsed the card into the first-run "No data" state; a persisted everTracked flag keeps the card with zeroed local values instead.
  • Quick-pick "Open OpenCode console" was also dead — now opens the console; a "Synced from opencode.ai" note appears when server meters are active.
  • Usage panel + hover card: stable geometry (fixed width/columns, no layout jumps), consistent 14/16px gutters, always-visible Today/Yesterday rows; the webview is a clean compact card.
  • Resolved the always-true/false TS hints (redundant cancellation guards, dead if (tracker), and two wrong types: GO_MODEL_PRICING and the metadata providers map are genuinely partial at runtime).

Verification

  • Endpoint verified live in production (401 shape matches upstream route source; upstream feat(console): add go usage endpoint anomalyco/opencode#16513 merged).
  • npm run lint — all 7 steps green; npm test 188/188.
  • Branch merged with current main (incl. the strict-lint toolchain) — mergeable, no conflicts.

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.
@ltmoerdani

Copy link
Copy Markdown
Owner

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.

  1. Multi-profile sync key. refreshGoUsageStatusBar() and the startup sync in activate() both read secrets.get(SECRET_KEY) and push that key onto the active tracker. With a single account that's the same key, no problem. With multiple profiles, switching profiles without a request in between pulls the wrong account's meters onto the active profile. And when a request does land, the status-bar sync and the per-request sync race, both write the snapshot, so the wrong-key result can win. The request path already resolves the exact key, so maybe the refresh path can do the same instead of reading SECRET_KEY. Am I missing something in how profile keys resolve here?

  2. hasData ignores the server snapshot. The status bar returns "OpenCode Go" and the tooltip says "No usage data yet" whenever there's no local tracking, even when the server just reported real account-wide meters. That's basically the exact case [FEATURE] Sync Go usage from official /zen/go/v1/usage endpoint #130 is about: someone with CLI usage installs the extension fresh and sees nothing until they send their first request. The quick pick and webview show the meters fine, so it's only the status bar and tooltip. Flipping hasData when a server snapshot exists would fix it.

Minor: syncServerUsage() has no in-flight dedup, so startup and a status-bar refresh at the same moment can fetch twice. Benign, last write wins, but a cheap guard would tidy it up.

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.

@Fahad090NP

Copy link
Copy Markdown
Contributor Author

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.
@Fahad090NP

Copy link
Copy Markdown
Contributor Author

All three points addressed and pushed (commits 172c2e9, 98efb6b, 0bff8b7):

  1. Multi-profile sync key — fixed. The extension now remembers which API key owns each profile fingerprint (ensureProfileForApiKey records it), and the status-bar background sync uses the active profile's own key, falling back to the extension secret only when unknown. Switching profiles without a request now syncs the right account's meters, and the refresh path no longer races the per-request sync with a different key (both resolve the same key for the same tracker).

  2. hasData ignoring the server snapshot — fixed. mergeServerUsage now flips hasData to true whenever a server snapshot is applied, so a fresh install with CLI usage shows the real meters on the status bar and tooltip instead of "OpenCode Go" / "No usage data yet". Regression test added (189/189 passing).

  3. In-flight dedup — fixed. Concurrent syncServerUsage calls for the same key share a single in-flight promise (startup + status-bar refresh no longer double-fetch); failures stay TTL-paced as before.

Ready to merge whenever you are — merge commit works for me.

@Fahad090NP
Fahad090NP marked this pull request as draft August 12, 2026 17:13
@Fahad090NP
Fahad090NP marked this pull request as ready for review August 12, 2026 17:20
@ltmoerdani

Copy link
Copy Markdown
Owner

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 hasData in mergeServerUsage is minimal and does the job.

One heads-up rather than a blocker: the PR picked up two files outside the usage scope (.husky/pre-commit and the lint-staged config in package.json, now check-only). It makes sense as its own follow-up, and it's consistent with how we treat the pre-commit gate, so I'm fine keeping it in this PR. Just flagging so it stays intentional.

Merge commit as usual.

@ltmoerdani
ltmoerdani merged commit 4a14b1e into ltmoerdani:main Aug 12, 2026
2 checks passed
ltmoerdani added a commit that referenced this pull request Aug 12, 2026
- 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
@Fahad090NP
Fahad090NP deleted the feat/issue-130-go-usage-sync branch August 13, 2026 02:09
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.

[FEATURE] Sync Go usage from official /zen/go/v1/usage endpoint

2 participants