Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/core/src/prime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,9 @@ export type PrimeSendResult =
status?: number
ms?: number
error: string
// Discriminates the failure kind so the manager can emit the
// spec Logging table's two distinct warn events: `prime token
// refresh failed` (warn · prime · { account, error }) for
// token-refresh failures, `prime fire failed` (warn · prime ·
// { account, status?, error }) for HTTP / fetch / identity
// failures during the request itself.
reason?: 'token-refresh' | 'send'
// `vault-cold` is an expected off-path cache warm state, not evidence
// of a failed provider request, so it must not emit the fire-failure warn.
reason?: 'token-refresh' | 'vault-cold' | 'send'
}

/**
Expand Down Expand Up @@ -946,7 +942,7 @@ export class PrimeManager {
if (this.stopped) return

if (!result.ok) {
// Spec Logging table: two distinct warn events.
// `vault-cold` is a cache-warm skip; it is not a provider failure.
// - `prime token refresh failed` — token-refresh failure
// before the request fires (reason: 'token-refresh').
// - `prime fire failed` — HTTP error / fetch throw /
Expand All @@ -956,6 +952,11 @@ export class PrimeManager {
account: evaluation.label,
error: result.error,
})
} else if (result.reason === 'vault-cold') {
logger.debug('prime', 'prime vault credential cold', {
account: evaluation.label,
reason: result.reason,
})
} else {
logger.warn('prime', 'prime fire failed', {
account: evaluation.label,
Expand Down
179 changes: 106 additions & 73 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,65 @@ const anthropicAuthPlugin = async (
}
}

async function reportCapturedClaustrumAuthFailure(
served: {
accountId: string
handle: string
recordVersion: number
},
reporterSource: ClaustrumReporterSource = 'direct',
options?: { preserveServedVersion?: boolean },
): Promise<void> {
const cache = claustrumCredentialCache
if (!cache) return
if (
served.recordVersion <=
(claustrumLastReportedVersion.get(served.handle) ?? -1)
) {
return
}
if (!options?.preserveServedVersion) {
const current = cache.peek(served.handle)
// Version match makes reports single-shot per served version. Accepted
// tradeoff: an unrelated cache eviction also suppresses a genuine
// report (worst case one delayed cycle until the next served 401).
if (!current || current.recordVersion !== served.recordVersion) return
}
const key = `${served.handle}\0${served.recordVersion}`
const pending = claustrumAuthFailureReports.get(key)
if (pending) {
await pending
return
}
const report = (async () => {
try {
await cache.reportAuthFailure(
served.handle,
401,
{
recordVersion: served.recordVersion,
},
reporterSource,
)
claustrumLastReportedVersion.set(served.handle, served.recordVersion)
} catch (error) {
handleClaustrumCredentialError(served.accountId, error, served.handle)
logger.warn('claustrum', 'failed to report credential failure', {
accountId: served.accountId,
error: error instanceof Error ? error.message : String(error),
})
}
})()
claustrumAuthFailureReports.set(key, report)
try {
await report
} finally {
if (claustrumAuthFailureReports.get(key) === report) {
claustrumAuthFailureReports.delete(key)
}
}
}

async function ensureClaustrumCredentialCache(): Promise<ClaustrumCredentialCache | null> {
if (claustrumCredentialCache) return claustrumCredentialCache
const now = claustrumNow()
Expand Down Expand Up @@ -2564,6 +2623,7 @@ const anthropicAuthPlugin = async (
const start = performance.now()
let accessToken: string | undefined
let resolvedModel: string | undefined
let servedClaustrumCredential: ClaustrumAccessResolution['served']
try {
if (accountId === 'main') {
// Use the same refresh path the fresh-check uses, so a missing or
Expand Down Expand Up @@ -2598,22 +2658,41 @@ const anthropicAuthPlugin = async (
error: `prime: OAuth account ${accountId} is unavailable`,
}
}
let current = account
try {
// R2: the fire path refreshes ONLY the token, not the quota.
// The fresh-check already performed the single usage-API
// call and persisted the result; the fire path reuses the
// in-memory quota via the headers / URL contract. This keeps
// the cycle at exactly one quota API call per account.
current = await fallbackManager.refreshAccount(account, storage)
} catch (error) {
return {
ok: false,
reason: 'token-refresh',
error: error instanceof Error ? error.message : String(error),
const vaultEnabled =
Boolean(account.claustrumHandle) &&
isClaustrumEnabledForAccount(storage, account.id) &&
!claustrumBlockedAccounts.has(account.id)
if (vaultEnabled) {
const resolved = resolveClaustrumAccess(account, storage, {
warm: false,
})
if (!resolved.accessToken || !resolved.served) {
return {
ok: false,
reason: 'vault-cold',
error: 'prime: vault credential is unavailable',
}
}
accessToken = resolved.accessToken
servedClaustrumCredential = resolved.served
} else {
let current = account
try {
// R2: the fire path refreshes ONLY the token, not the quota.
// The fresh-check already performed the single usage-API
// call and persisted the result; the fire path reuses the
// in-memory quota via the headers / URL contract. This keeps
// the cycle at exactly one quota API call per account.
current = await fallbackManager.refreshAccount(account, storage)
} catch (error) {
return {
ok: false,
reason: 'token-refresh',
error: error instanceof Error ? error.message : String(error),
}
}
accessToken = current.access
}
accessToken = current.access
resolvedModel = CLAUDE_HAIKU_4_5_MODEL_ID
}

Expand Down Expand Up @@ -2657,6 +2736,13 @@ const anthropicAuthPlugin = async (
if (!response.ok) {
const reason =
(await response.text().catch(() => '')) || `HTTP ${response.status}`
if (response.status === 401 && servedClaustrumCredential) {
await reportCapturedClaustrumAuthFailure(
servedClaustrumCredential,
'direct',
{ preserveServedVersion: true },
)
}
return { ok: false, status: response.status, ms, error: reason }
}
const data = (await response.json().catch(() => null)) as Record<
Expand Down Expand Up @@ -5326,66 +5412,13 @@ const anthropicAuthPlugin = async (
recordVersion: number
},
reporterSource: ClaustrumReporterSource = 'direct',
options?: { preserveServedVersion?: boolean },
): Promise<void> {
const cache = claustrumCredentialCache
if (!cache) return
if (
served.recordVersion <=
(claustrumLastReportedVersion.get(served.handle) ?? -1)
) {
return
}
const current = cache.peek(served.handle)
// Version match makes reports single-shot per served version. Accepted
// tradeoff: an unrelated cache eviction also suppresses a genuine
// report (worst case one delayed cycle until the next served 401).
if (!current || current.recordVersion !== served.recordVersion)
return
const key = `${served.handle}\0${served.recordVersion}`
const pending = claustrumAuthFailureReports.get(key)
if (pending) {
await pending
return
}
const report = (async () => {
try {
await cache.reportAuthFailure(
served.handle,
401,
{
recordVersion: served.recordVersion,
},
reporterSource,
)
claustrumLastReportedVersion.set(
served.handle,
served.recordVersion,
)
} catch (error) {
handleClaustrumCredentialError(
served.accountId,
error,
served.handle,
)
logger.warn(
'claustrum',
'failed to report credential failure',
{
accountId: served.accountId,
error:
error instanceof Error ? error.message : String(error),
},
)
}
})()
claustrumAuthFailureReports.set(key, report)
try {
await report
} finally {
if (claustrumAuthFailureReports.get(key) === report) {
claustrumAuthFailureReports.delete(key)
}
}
return reportCapturedClaustrumAuthFailure(
served,
reporterSource,
options,
)
}

async function sendWithAccessToken(
Expand Down
Loading