fix(rate-limits): read OpenCode Go usage with the Go API key (#22551)

* fix(rate-limits): read OpenCode Go usage with the account API key

Since OpenCode's console migration (upstream fe51b0b19a, "fix(console):
restrict legacy access to Black"), an account with no Black subscription
is redirected from the legacy console to /console/login, so Orca's
cookie-based workspace lookup returns nothing and the Go bar stays empty.

Fetch usage from GET https://opencode.ai/zen/go/v1/usage instead, which
authenticates with `Authorization: Bearer <key>` and needs no console
session. The key resolves in order: Orca settings override,
OPENCODE_API_KEY, then whatever OpenCode itself stored on /connect --
auth.json for 1.x, the credential table for 2.x. The cookie path stays
as the fallback so Black/legacy accounts keep working.

A 403 EntitlementError now reads as "no OpenCode Go subscription" in the
status bar instead of a generic refresh failure (#22257's reporter was
misled by exactly that).

* fix(rate-limits): prefer OpenCode's stored Go key over OPENCODE_API_KEY

OpenCode applies the key saved on /connect after the environment, so the
stored key is the one its own Go requests use. OPENCODE_API_KEY is also
the Zen provider's variable, so ranking it first could read a key that
OpenCode itself is not using for Go.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(rate-limits): name the API key when OpenCode Go usage lands on sign-in

A redirected usage request arrives as a 200 sign-in page because Electron
follows redirects; report it as a rejected key instead of a parse failure.
The cookie path's empty workspace lookup is what non-Black accounts now
hit after the console migration, so its message points at the API key
rather than only the workspace override.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(i18n): add the OpenCode Go API key strings to the English catalog

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(rate-limits): stop calling the credential table an OpenCode 2 marker

Verified on two real Windows hosts running OpenCode 1.18.16: the `credential`
table exists there too (empty, same columns), so its presence does not identify
a 2.x install. Neither host had an `auth.json` at all.

The resolution already probes both stores on every version, so only the comments
were wrong. Says so now, and records that a 2.x install which never ran the
legacy import has no `auth.json` either — which is why both tiers exist.

* refactor(shared): move GhosttyImportPreview out of global-settings-types

Adding `opencodeGoApiKey` pushed global-settings-types.ts one line past the
300-line ceiling, failing `oxlint` in CI. AGENTS.md forbids a max-lines
suppression, so split instead: the Ghostty import preview is a distinct concern
that never belonged in the settings-shape file.

Re-exported from the original module so no importer changes. 293 code lines now.

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-09-23 20:09:02 -07:00
committed by GitHub
co-authored by Jinwoo-H Claude
parent 57bf732a42
commit 5802b54579
43 changed files with 1246 additions and 80 deletions
+1
View File
@@ -210,6 +210,7 @@ export function buildDefaultSettings(args: {
defaultLinearTeamSelection: null,
opencodeSessionCookie: '',
opencodeWorkspaceId: '',
opencodeGoApiKey: '',
minimaxGroupId: '',
minimaxUsageModels: 'general',
minimaxEndpoint: 'overseas',
+11
View File
@@ -0,0 +1,11 @@
import type { GlobalSettings } from './global-settings-types'
/** What a Ghostty config import would change, shown before the user accepts it. */
export type GhosttyImportPreview = {
found: boolean
configPath?: string
configPaths?: string[]
diff: Partial<GlobalSettings>
unsupportedKeys: string[]
error?: string
}
+5 -8
View File
@@ -389,6 +389,8 @@ export type GlobalSettings = {
opencodeSessionCookie: string
/** Optional OpenCode Go workspace ID override; when set, skips the workspaces lookup and fetches usage directly. */
opencodeWorkspaceId: string
/** Optional OpenCode Go API key override. Takes precedence over OpenCode's own stored key and OPENCODE_API_KEY. Stored encrypted. */
opencodeGoApiKey: string
/** Optional MiniMax group id. When empty, the usage fetcher extracts minimax_group_id_v2 from the cookie. */
minimaxGroupId: string
/** Comma-separated MiniMax model names to show in the status bar usage window. */
@@ -525,11 +527,6 @@ export type OrcaWorkspaceLayout = {
nestWorkspaces: boolean
}
export type GhosttyImportPreview = {
found: boolean
configPath?: string
configPaths?: string[]
diff: Partial<GlobalSettings>
unsupportedKeys: string[]
error?: string
}
// Re-exported so existing importers keep one entry point; the shape lives in its
// own file because this one is at the max-lines ceiling.
export type { GhosttyImportPreview } from './ghostty-import-preview'
+1
View File
@@ -13,6 +13,7 @@ export function createEmptyRateLimitState(overrides: Partial<RateLimitState> = {
grok: null,
minimaxCookieConfigured: false,
minimaxApiKeyConfigured: false,
opencodeGoApiKeyConfigured: false,
grokAuthConfigured: false,
claudeTarget: { runtime: 'host', wslDistro: null },
codexTarget: { runtime: 'host', wslDistro: null },
+1
View File
@@ -18,6 +18,7 @@ describe('RateLimitState', () => {
grok: null,
minimaxCookieConfigured: false,
minimaxApiKeyConfigured: false,
opencodeGoApiKeyConfigured: false,
grokAuthConfigured: false,
claudeTarget: { runtime: 'host', wslDistro: null },
codexTarget: { runtime: 'host', wslDistro: null },
+9
View File
@@ -25,6 +25,8 @@ export type UsageRateLimitFailureKind =
| 'deferred-by-live-session'
| 'keychain-unavailable'
| 'missing-scope'
/** The account is authenticated but not entitled to the product being polled. */
| 'no-subscription'
| 'network'
| 'server'
| 'parse'
@@ -138,6 +140,13 @@ export type RateLimitState = {
* visible across reloads.
*/
minimaxApiKeyConfigured: boolean
/**
* True when main resolved an OpenCode Go API key (Orca settings,
* OPENCODE_API_KEY, or what OpenCode stored on /connect). The key itself
* never leaves main; the status bar ORs this with the session cookie to
* decide whether the OpenCode Go bar stays visible.
*/
opencodeGoApiKeyConfigured: boolean
/** True when main finds a Grok CLI session file (~/.grok/auth.json or GROK_HOME). */
grokAuthConfigured: boolean
claudeTarget: RateLimitRuntimeTarget