Files
orca/src/main/rate-limits/opencode-go-usage-source-selection.ts
T
Neilandmmarabel 2b0ce17514 Show provider credit balances alongside usage limits (#25408)
Display provider credit and currency balances separately from rate limits. Preserve quotas when balance data is unavailable and translate complete labels.

Supersedes #9363; credit to @mmarabel for the original implementation.

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-05 00:07:23 -07:00

153 lines
5.3 KiB
TypeScript

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 { makeOpenCodeGoZenBalance, type OpenCodeGoUsageWindows } from './opencode-go-status-parsing'
import type { OpenCodeCredentialBackend } from '../opencode/opencode-credential-backend'
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
environment?: NodeJS.ProcessEnv
backend?: OpenCodeCredentialBackend
cwd?: 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,
extraUsage: makeOpenCodeGoZenBalance(null, 'api-key-source'),
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 context = input.settingsApiKey?.trim()
? {}
: {
...(input.environment ? { environment: input.environment } : {}),
...(input.backend ? { backend: input.backend } : {}),
...(input.cwd ? { cwd: input.cwd } : {})
}
const apiKeyResolution = await resolveOpenCodeGoApiKey({
settingsOverride: input.settingsApiKey,
...context
})
input.onApiKeyResolved?.(apiKeyResolution)
const hasCookie = Boolean(normalizeCookieInput(input.cookie))
if (apiKeyResolution.status === 'credential-database-unreadable' && !hasCookie) {
return emptyResult(
"Could not read OpenCode's credential database, so OPENCODE_API_KEY was not used; it may be a Zen key. Add your OpenCode Go key in Settings, or retry.",
'error',
{ failureKind: 'usage-unavailable' }
)
}
if (apiKeyResolution.status !== 'found') {
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)
}