Skip to content

Commit 28c7c88

Browse files
Hu9956Hu9956
andauthored
feat: 使用 OpenCode Go 官方 API 查询套餐额度 (#457)
* feat: query OpenCode Go usage through official API * fix: clarify OpenCode Go usage API auth errors --------- Co-authored-by: Hu9956 <2.82876394e+08+Hu9956@users.noreply.github.com>
1 parent 87b1876 commit 28c7c88

5 files changed

Lines changed: 309 additions & 74 deletions

File tree

.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ INSFORGE_API_KEY=
2323
# Pricing sync / external
2424
OPENROUTER_API_KEY=
2525

26-
# OpenCode Go (https://opencode.ai/workspace/<id>/go) — optional.
27-
# Scrapes the workspace dashboard for 5h / Weekly / Monthly usage. No public
28-
# REST API yet (tracked at anomalyco/opencode#16017). Send the cookie
29-
# verbatim as `Cookie: auth=<OPENCODE_GO_AUTH_COOKIE>`.
26+
# OpenCode Go — optional. Preferred: official API for 5h / Weekly / Monthly usage.
27+
# Create a Go API key in OpenCode, then set it only in your local environment.
28+
OPENCODE_GO_API_KEY=
29+
# Legacy compatibility fallback: scrape the signed-in workspace dashboard.
30+
# Send the cookie verbatim as `Cookie: auth=<OPENCODE_GO_AUTH_COOKIE>`.
3031
OPENCODE_GO_WORKSPACE_ID=
3132
OPENCODE_GO_AUTH_COOKIE=
3233
# Optional and explicitly unverified: show a local opencode.db cost estimate.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ node bin/tracker.js serve --no-sync # local dashboard server on :7680
4040
| Add a local API endpoint | `src/lib/local-api.js` — search `/functions/tokentracker-` |
4141
| Wire a provider into sync | `src/commands/sync.js` (call site + totals aggregation) + `src/commands/status.js` (status reporting) |
4242
| Add pricing for a model | `src/lib/pricing/curated-overrides.json` **+ the canonical edge block in `dashboard/edge-patches/tokentracker-leaderboard-refresh.ts`, copied verbatim into the other 4 edge files** (account-daily / account-summary / account-model-breakdown / leaderboard-profile). `test/edge-pricing-parity.test.js` fails on any drift. Deploy the touched edge functions after editing. |
43-
| Add an OpenCode Go usage-limits row | `src/lib/opencode-go-limits.js` + provider entry in `src/lib/usage-limits.js` + `PROVIDER_LIMIT_SPECS.opencodeGo` in `dashboard/src/ui/dashboard/components/usage-limits-provider-specs.js`. **Authoritative source = the signed-in dashboard scrape** (`source:'web'`); local `opencode.db` cost ÷ Go's dollar caps ($12/5h, $30/wk, $60/mo) is only an explicit `TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1` estimate because history cannot prove an active subscription. The CLI `sk-` key auths only the inference gateway, not usage. Reads via `readSqliteJsonRowsAsync` so the limits poll never blocks the event loop. |
43+
| Add an OpenCode Go usage-limits row | `src/lib/opencode-go-limits.js` + provider entry in `src/lib/usage-limits.js` + `PROVIDER_LIMIT_SPECS.opencodeGo` in `dashboard/src/ui/dashboard/components/usage-limits-provider-specs.js`. **Authoritative source = the official Go usage API** (`source:'api'`) using `OPENCODE_GO_API_KEY`; the signed-in dashboard scrape (`source:'web'`) remains a legacy compatibility fallback. Local `opencode.db` cost ÷ Go's dollar caps ($12/5h, $30/wk, $60/mo) is only an explicit `TOKENTRACKER_OPENCODE_GO_LOCAL_ESTIMATE=1` estimate because history cannot prove an active subscription. Reads via `readSqliteJsonRowsAsync` so the limits poll never blocks the event loop. |
4444
| Add a dashboard page | `dashboard/src/pages/` (lazy-loaded via `React.lazy()` in `App.jsx`**except `NativeAuthCallbackPage`, which must stay eager-imported**, see Lessons learned) |
4545
| Add UI components | `dashboard/src/ui/dashboard/components/` |
4646
| Add a provider icon | `dashboard/src/ui/dashboard/components/ProviderIcon.jsx` (`PROVIDER_ICON_MAP` keyed by `source.toUpperCase()`) |

docs/opencode-go-limits.md

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,80 @@
1-
# Add OpenCode Go usage limits to the dashboard
1+
# OpenCode Go usage limits
22

3-
## Background & findings
3+
## Overview
44

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.
116

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.
138

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
1710

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. |
1916

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.
2118

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
3420

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.
3622

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=
4426
45-
### Docs / env
27+
# Legacy compatibility fallback: signed-in workspace dashboard scrape.
28+
OPENCODE_GO_WORKSPACE_ID=
29+
OPENCODE_GO_AUTH_COOKIE=
4630
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+
```
5434

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.
5636

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
6038

61-
## Risks / non-goals
39+
The request uses a standard Bearer authorization header:
6240

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+
```
6645

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:
6847

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+
```
7058

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.
7260

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
7462

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.
7664

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

Comments
 (0)