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
@@ -0,0 +1,132 @@
import type { NetworkProxySettings } from '../../shared/network-proxy'
import type { ProviderRateLimits, UsageRateLimitMetadata } from '../../shared/rate-limit-types'
import {
resolveOpenCodeGoApiKey,
type OpenCodeGoApiKeyResolution
} from './opencode-go-api-key-source'
import type { OpenCodeGoUsageWindows } from './opencode-go-status-parsing'
import {
fetchOpenCodeGoUsageWithApiKey,
type OpenCodeGoUsageApiOutcome
} from './opencode-go-usage-api'
import { fetchOpenCodeGoRateLimits, normalizeCookieInput } from './opencode-go-usage-fetcher'
export type OpenCodeGoUsageSourceInput = {
/** Explicit Orca override; the highest-precedence key tier. */
settingsApiKey?: string
cookie: string
workspaceIdOverride?: string
networkProxySettings?: NetworkProxySettings
signal?: AbortSignal
/** Lets the caller record key presence without handling the key itself. */
onApiKeyResolved?: (resolution: OpenCodeGoApiKeyResolution) => void
}
function emptyResult(
error: string,
status: 'error' | 'unavailable',
usageMetadata?: UsageRateLimitMetadata
): ProviderRateLimits {
return {
provider: 'opencode-go',
session: null,
weekly: null,
monthly: null,
updatedAt: Date.now(),
error,
status,
...(usageMetadata ? { usageMetadata } : {})
}
}
function usageResult(
windows: OpenCodeGoUsageWindows,
credentialSource: string
): ProviderRateLimits {
return {
provider: 'opencode-go',
session: windows.session,
weekly: windows.weekly,
monthly: windows.monthly,
updatedAt: Date.now(),
error: null,
status: 'ok',
usageMetadata: { source: 'web', credentialSource }
}
}
// Why explicit copy per verdict: #22257's reporter was misled by a generic
// "could not parse" for what was really an entitlement answer, so each verdict
// says what the account's state is and what to do about it.
function apiFailureResult(
outcome: Exclude<OpenCodeGoUsageApiOutcome, { kind: 'ok' }>,
credentialSource: string
): ProviderRateLimits {
if (outcome.kind === 'no-subscription') {
return emptyResult(
'This OpenCode account has no OpenCode Go subscription. Subscribe at opencode.ai to see Go usage.',
'error',
{ source: 'web', credentialSource, failureKind: 'no-subscription' }
)
}
if (outcome.kind === 'unauthorized') {
return emptyResult(
'The OpenCode Go API key was rejected. Run /connect in OpenCode, or replace the key in Settings.',
'error',
{ source: 'web', credentialSource, failureKind: 'stale-token' }
)
}
return emptyResult(outcome.message, 'error', {
source: 'web',
credentialSource,
failureKind: 'server'
})
}
/**
* Fetch OpenCode Go usage from the best source the account offers.
*
* The keyed `GET /zen/go/v1/usage` endpoint is tried first because it needs no
* console session; the legacy cookie path stays as the fallback for Black and
* other legacy-console accounts, which upstream `fe51b0b19a` still serves.
* @param input.settingsApiKey - Explicit Orca override, if the user set one.
* @param input.cookie - The opencode.ai cookie header from Orca settings.
* @returns A provider snapshot for the status bar.
*/
export async function fetchOpenCodeGoUsage(
input: OpenCodeGoUsageSourceInput
): Promise<ProviderRateLimits> {
const apiKeyResolution = await resolveOpenCodeGoApiKey({
settingsOverride: input.settingsApiKey
})
input.onApiKeyResolved?.(apiKeyResolution)
const hasCookie = Boolean(normalizeCookieInput(input.cookie))
if (apiKeyResolution.status === 'missing') {
return hasCookie
? fetchOpenCodeGoRateLimits(
input.cookie,
input.workspaceIdOverride,
input.networkProxySettings
)
: emptyResult('No OpenCode Go API key or session cookie configured', 'unavailable', {
failureKind: 'missing-credentials'
})
}
const { key, tier } = apiKeyResolution
const outcome = await fetchOpenCodeGoUsageWithApiKey(key, input.signal)
if (outcome.kind === 'ok') {
return usageResult(outcome.windows, tier)
}
if (!hasCookie) {
return apiFailureResult(outcome, tier)
}
// A Black-only account holds a key with no Go entitlement, so its usage still
// only exists behind the console session; keep that path working.
const cookieResult = await fetchOpenCodeGoRateLimits(
input.cookie,
input.workspaceIdOverride,
input.networkProxySettings
)
return cookieResult.status === 'ok' ? cookieResult : apiFailureResult(outcome, tier)
}