|
1 | | -# Add OpenCode Go usage limits to the dashboard |
| 1 | +# OpenCode Go usage limits |
2 | 2 |
|
3 | | -## Background & findings |
| 3 | +## Overview |
4 | 4 |
|
5 | | -- The opencode web console exposes the Go usage page as a SolidStart `"use server"` RPC at `https://opencode.ai/workspace/<id>/go`. There is no public REST API today (ref `anomalyco/opencode#16017`, `slkiser/opencode-quota#36`). |
6 | | -- The page's `queryLiteSubscription` returns three windows: **5h** (`rollingUsage`), **Weekly** (`weeklyUsage`), **Monthly** (`monthlyUsage`) — each `{ usagePercent, resetInSec }`. The page also embeds them in SSR hydration output as `rollingUsage:$R[N]={...usagePercent:N...resetInSec:N...}` plus a `data-slot="usage-item"` HTML fallback. |
7 | | -- Limits (per `packages/console/core/src/subscription.ts` + `ZEN_LIMITS` resource): rolling `$12 / 5h`, weekly `$30`, monthly `$60`. (Pricing surface can be added later; this PR is percentage-only like the rest of the panel.) |
8 | | -- The repo's existing `opencode` source row tracks *your local* OpenCode sessions; we add a **separate** `opencodeGo` row so they don't collide. |
9 | | -- A precedent with the same approach is **merged in `slkiser/opencode-quota#41`** (TypeScript). The implementation there: `GET https://opencode.ai/workspace/<workspaceId>/go` with `Cookie: auth=<cookie>` → regex-parse three windows out of the HTML. Two env vars: `OPENCODE_GO_WORKSPACE_ID` + `OPENCODE_GO_AUTH_COOKIE`. ~430 tests pass, no breakage on missing config (`{state: "none"}` → not-attempted result). |
10 | | -- This is the proven, safe approach. Mirroring the SolidStart `"use server"` RPC is possible but fragile (SolidStart uses opaque internal `_server` URLs and a per-hash function name; the dashboard approach has a stable public URL and is what the opencode team themselves eventually want — the issue is tracked). |
| 5 | +TokenTracker exposes **OpenCode Go** as a usage-limits provider separate from the ordinary local OpenCode session source. The provider reports the three subscription windows that OpenCode calculates on its servers: rolling five-hour usage, weekly usage, and monthly usage. |
11 | 6 |
|
12 | | -## Decision (user-confirmed) |
| 7 | +The preferred source is OpenCode's official authenticated usage endpoint, `GET https://opencode.ai/zen/go/v1/usage`. The endpoint is implemented upstream in [anomalyco/opencode#16513](https://github.com/anomalyco/opencode/pull/16513). The older signed-in dashboard scrape remains available for existing configurations, and a local SQLite cost calculation remains an explicit, non-authoritative estimate. |
13 | 8 |
|
14 | | -- New provider: `opencodeGo`, displayed as **"OpenCode Go"**, reusing the existing `opencode` icon (`OPENCODE` key) in `ProviderIcon.jsx`. |
15 | | -- Config: `OPENCODE_GO_WORKSPACE_ID` + `OPENCODE_GO_AUTH_COOKIE` env vars, same naming as `slkiser/opencode-quota#41` and the existing `OPENCODE_*` / `KIMI_*` / `ZAI_*` convention already in `.env.example` (no `TOKENTRACKER_` prefix — that prefix is only for our own CLI runtime, not provider auth). No macOS Keychain reading — env-only matches the rest of the dashboard's `VITE_*` pattern. The Fe26.* cookie becomes `OPENCODE_GO_AUTH_COOKIE` and is sent verbatim as `Cookie: auth=<value>`. |
16 | | -- The dashboard response is the authoritative subscription source. A page that renders `data-slot="subscribe-button"` is returned as an inactive subscription and never replaced with historical local bars. Local `opencode.db` cost aggregation is disabled by default; set `TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1` only when a clearly labeled, unverified local estimate is desired. |
| 9 | +## Data-source priority |
17 | 10 |
|
18 | | -## Files to add / modify |
| 11 | +| Priority | Source | Configuration | Semantics | |
| 12 | +|---|---|---|---| |
| 13 | +| 1 | Official OpenCode Go usage API | `OPENCODE_GO_API_KEY` | Authoritative subscription windows returned as JSON. | |
| 14 | +| 2 | Signed-in workspace dashboard scrape | `OPENCODE_GO_AUTH_COOKIE`, optionally `OPENCODE_GO_WORKSPACE_ID` | Legacy compatibility fallback for existing users. | |
| 15 | +| 3 | Local `opencode.db` cost aggregation | `TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1` | An explicitly labeled historical estimate; it cannot establish current subscription entitlement. | |
19 | 16 |
|
20 | | -### Backend (CLI) |
| 17 | +When an API key is present, TokenTracker calls the official endpoint first. A `401` or `403` response is returned directly so the user can correct the key or subscription. For a transient API failure, a configured cookie-backed scrape is attempted before the optional local estimate. |
21 | 18 |
|
22 | | -1. **`src/lib/opencode-go-limits.js` (new)** — `fetchOpencodeGoLimits({ home, env, fetchImpl, providerTimeoutMs })`: |
23 | | - - Reads `OPENCODE_GO_WORKSPACE_ID` + `OPENCODE_GO_AUTH_COOKIE` from `env` (no `TOKENTRACKER_` prefix — mirrors the `KIMI_API_KEY` / `ZAI_API_KEY` pattern already in `.env.example` and the upstream `slkiser/opencode-quota` env names). |
24 | | - - `GET https://opencode.ai/workspace/<encoded workspaceId>/go` with `Cookie: auth=<cookie>`, `User-Agent: Mozilla/5.0…` (matches PR #41, dodges some anti-bot 403s). |
25 | | - - Parses SSR-hydration regexes (3 windows × 2 field orderings) + a `data-slot` HTML fallback, ported from `slkiser/opencode-quota/src/lib/opencode-go.ts:54-126` (MIT, project allows reuse with attribution note in code). |
26 | | - - Returns `{ configured: false }` when no authoritative cookie is configured, `{ configured: true, subscription_status: "inactive" }` for an inactive Go page, or the three server windows with `subscription_status: "active"`. A local `opencode.db` estimate is returned only when `TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1`, with `source: "local-estimate"` and `subscription_status: "unknown"`. |
27 | | -2. **`src/lib/usage-limits.js`** — add `import { fetchOpencodeGoLimits }` and a `Promise.all` slot for it next to the existing 10 providers; merge with `withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go")` in the returned `data` object. `normalizePlanLabel(null, ...)` returns `null` so the rendered title stays "OpenCode Go" (the brand), not "OpenCode Go OpenCode Go". |
28 | | -3. **`src/lib/local-api.js`** — no changes (the new provider is just another key on the JSON the existing `/functions/tokentracker-usage-limits` endpoint already returns). |
29 | | -4. **`test/opencode-go-limits.test.js` (new)** — node:test, fixtures: |
30 | | - - missing env → `{ configured: false }` |
31 | | - - happy path: stubbed `fetch` returning both SSR hydration + data-slot HTML → 3 windows parsed |
32 | | - - 401/403 → `{ configured: true, error: "…" }` (treats logout the same way as the other providers) |
33 | | - - 200 but no parseable windows → `{ configured: true, error: "Could not parse any known OpenCode Go dashboard usage windows…" }` |
| 19 | +## Configuration |
34 | 20 |
|
35 | | -### Frontend (dashboard) |
| 21 | +Configure secrets only in the local environment. API keys and cookies must never be committed, logged, displayed, or placed in client-side dashboard variables. |
36 | 22 |
|
37 | | -5. **`dashboard/src/lib/limits-providers.js`** — add `"opencodeGo"` to `LIMIT_PROVIDER_IDS` (newest entry, after `zcode`); map to `OPENCODE` in `LIMIT_PROVIDER_ICON_KEYS`; add a `case "opencodeGo"` in `limitProviderName()`. |
38 | | -6. **`dashboard/src/ui/dashboard/components/usage-limits-provider-specs.js`** — add `opencodeGo` spec with 3 windows (`primary_window` = 5h, `secondary_window` = weekly, `tertiary_window` = monthly), reusing the existing `used_percent` + `reset_at` fields. Append the three new `copy("limits.label.opencode_go_*")` calls to `usageLimitsLabelCopyAnchor()`. |
39 | | -7. **`dashboard/src/content/copy.csv`** — add copy rows for `limits.provider.opencode_go` ("OpenCode Go") and the three labels (5h, Weekly, Monthly); mirrors the `zcode` block. All 5 i18n locales regenerated via the existing sync script. |
40 | | -8. **`dashboard/src/hooks/use-usage-limits.ts`** — extend the `UsageLimitsData` type to include the new `opencodeGo` field. |
41 | | -9. **`dashboard/src/pages/LimitsPage.jsx`** — pass `opencodeGo={usageLimits?.opencodeGo}` to `<UsageLimitsPanel>`. The existing `UsageLimitsPanel.jsx` and `ProviderIcon.jsx` need no changes — they iterate the `dataById` map and fall back to the shared `OPENCODE` icon. |
42 | | -10. **`dashboard/src/hooks/use-usage-limits.test.tsx`** — add `opencodeGo: { configured: false }` to the mock fixture. |
43 | | -11. **`dashboard/src/ui/dashboard/components/UsageLimitsPanel.test.jsx`** — add a test rendering the panel with the 3 mocked windows (5h 12%, Weekly 30%, Monthly 60%) and asserting the labels render. |
| 23 | +```dotenv |
| 24 | +# Preferred: official OpenCode Go usage API. |
| 25 | +OPENCODE_GO_API_KEY= |
44 | 26 |
|
45 | | -### Docs / env |
| 27 | +# Legacy compatibility fallback: signed-in workspace dashboard scrape. |
| 28 | +OPENCODE_GO_WORKSPACE_ID= |
| 29 | +OPENCODE_GO_AUTH_COOKIE= |
46 | 30 |
|
47 | | -12. **`.env.example`** — append: |
48 | | - ```dotenv |
49 | | - # OpenCode Go (https://opencode.ai/workspace/<id>/go) — optional, enables dashboard scrape |
50 | | - OPENCODE_GO_WORKSPACE_ID= |
51 | | - OPENCODE_GO_AUTH_COOKIE= |
52 | | - ``` |
53 | | -13. **`docs/`** — short note in the existing limits doc (if one exists) + a paragraph in `CLAUDE.md` under "What's where" pointing at `src/lib/opencode-go-limits.js`. No README change. |
| 31 | +# Optional and explicitly unverified local historical estimate. |
| 32 | +# TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1 |
| 33 | +``` |
54 | 34 |
|
55 | | -## Reused / shared patterns |
| 35 | +The workspace ID is optional when the legacy cookie path is used. The module will attempt to resolve it from the signed-in account, but a direct API-key configuration does not need a workspace ID or cookie. |
56 | 36 |
|
57 | | -- **Spec shape**: copies ZCode's window spec verbatim (only labels differ) — same 3 `primary/secondary/tertiary` window fields already used by `kimi`/`cursor`/`gemini`/`antigravity`. |
58 | | -- **Error/cache plumbing**: existing `withPlanLabel`, the 2-min in-memory cache, the single-flight guard, and the 15s focus-throttled refetch in `use-usage-limits.ts` all just work — no changes needed. |
59 | | -- **Validation**: no new copy strings, but the new copy.csv rows get picked up by `npm run validate:copy`. New `limits.label.opencode_go_*` are referenced through `copy(...)` only — no UI hardcode, no `validate:ui-hardcode` regressions. |
| 37 | +## Official API contract |
60 | 38 |
|
61 | | -## Risks / non-goals |
| 39 | +The request uses a standard Bearer authorization header: |
62 | 40 |
|
63 | | -- **Brittleness** (the one real concern): the opencode team can rename the SSR hydration key, drop the `data-slot` attrs, or move to a public API at any time. The PR #41 maintainers flagged the same. Mitigation: keep both parsers; if both fail, surface a clear "Could not parse OpenCode Go dashboard" error in the panel so the user knows to re-check. |
64 | | -- **Cookie rotation**: the Fe26.* cookie expires on logout. If it 401s, we surface that as the provider's `error` field — same UX as Kimi/ZCode/Copilot when their token goes stale. No automatic re-auth. |
65 | | -- **No public API yet** — when upstream ships one, this is a one-file swap in `opencode-go-limits.js`. |
| 41 | +```bash |
| 42 | +curl -sS https://opencode.ai/zen/go/v1/usage \ |
| 43 | + -H "Authorization: Bearer $OPENCODE_GO_API_KEY" |
| 44 | +``` |
66 | 45 |
|
67 | | -## Auth details (user-confirmed) |
| 46 | +A successful response contains `rollingUsage`, `weeklyUsage`, and `monthlyUsage` objects. Each includes `usagePercent` and `resetInSec`; the service may also include `status` and top-level `useBalance`. `src/lib/opencode-go-limits.js` converts those fields into the provider-panel contract: |
68 | 47 |
|
69 | | -When implementing, send `Cookie: auth=<OPENCODE_GO_AUTH_COOKIE>` exactly as PR #41 does, with no prefix manipulation. User sets `OPENCODE_GO_AUTH_COOKIE=<pasted-value>` in `.env.local`. |
| 48 | +```json |
| 49 | +{ |
| 50 | + "configured": true, |
| 51 | + "source": "api", |
| 52 | + "subscription_status": "active", |
| 53 | + "primary_window": { "used_percent": 42, "reset_at": "2026-01-01T00:00:00.000Z" }, |
| 54 | + "secondary_window": { "used_percent": 18, "reset_at": "2026-01-01T00:00:00.000Z" }, |
| 55 | + "tertiary_window": { "used_percent": 7, "reset_at": "2026-01-01T00:00:00.000Z" } |
| 56 | +} |
| 57 | +``` |
70 | 58 |
|
71 | | -## Release impact (per `CLAUDE.md`) |
| 59 | +The endpoint returns `401` when the key is absent, invalid, expired, or not entitled to an OpenCode Go subscription; early upstream versions used `401` for both authentication and entitlement failures. Newer upstream versions may return `403` for a missing Go subscription, so TokenTracker handles that response defensively as well. Both cases are surfaced with actionable provider errors. |
72 | 60 |
|
73 | | -Touches `src/` + `dashboard/` → must bump `package.json` + `TokenTrackerBar/project.yml` `MARKETING_VERSION` (×2) + `TokenTrackerWin/TokenTrackerWin.csproj` `<Version>` in lockstep, then trigger `release (macOS + Windows + Linux)`. |
| 61 | +## Implementation boundaries |
74 | 62 |
|
75 | | -## Reference (MIT-licensed reuse) |
| 63 | +The implementation lives in `src/lib/opencode-go-limits.js` and is wired into the shared provider poll in `src/lib/usage-limits.js`. The API and dashboard paths share `buildWindow()`, so dashboard rendering, cache behavior, and the existing `primary_window` / `secondary_window` / `tertiary_window` schema do not change. |
76 | 64 |
|
77 | | -`slkiser/opencode-quota` PR #41 — same approach, merged Apr 12 2026, 430 tests passing. Port the parser verbatim and credit the source in a code comment. |
| 65 | +The legacy scraper intentionally retains its SSR-hydration and `data-slot` parsers because it is a compatibility path, not a protocol dependency for API-key users. Its workspace-resolution code should not run when a valid API response is available. |
| 66 | + |
| 67 | +## Tests and validation |
| 68 | + |
| 69 | +`test/opencode-go-limits.test.js` covers the API request shape, JSON window mapping, API-key precedence, authentication errors, malformed payload handling, and a temporary-API-failure fallback to the existing dashboard path. The existing tests continue to cover legacy cookie parsing and workspace resolution. |
| 70 | + |
| 71 | +Run the targeted test during development, then execute the repository's complete local validation before requesting review: |
| 72 | + |
| 73 | +```bash |
| 74 | +node --test test/opencode-go-limits.test.js |
| 75 | +npm run ci:local |
| 76 | +``` |
| 77 | + |
| 78 | +## Release impact |
| 79 | + |
| 80 | +The implementation changes `src/`, so it follows the release workflow documented in `CLAUDE.md`: maintainers bump the shared package version and ship the updated CLI through npm and the macOS, Windows, and Linux bundles. |
0 commit comments