mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +00:00
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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user