import Foundation public enum ClaudeProviderDescriptor { public static let descriptor: ProviderDescriptor = Self.makeDescriptor() static func makeDescriptor() -> ProviderDescriptor { ProviderDescriptor( id: .claude, metadata: ProviderMetadata( id: .claude, displayName: "Claude", sessionLabel: "Session", weeklyLabel: "Weekly", opusLabel: "Sonnet", supportsOpus: true, supportsCredits: false, creditsHint: "", toggleTitle: "Show Claude Code usage", cliName: "claude", defaultEnabled: false, isPrimaryProvider: true, usesAccountFallback: false, browserCookieOrder: ProviderBrowserCookieDefaults.defaultImportOrder, dashboardURL: "https://console.anthropic.com/settings/billing", subscriptionDashboardURL: "https://claude.ai/settings/usage", changelogURL: "https://github.com/anthropics/claude-code/releases", statusPageURL: "https://status.claude.com/"), branding: ProviderBranding( iconStyle: .claude, iconResourceName: "ProviderIcon-claude", color: ProviderColor(red: 204 / 255, green: 124 / 255, blue: 94 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: true, noDataMessage: self.noDataMessage), fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .api, .web, .cli, .oauth], pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), cli: ProviderCLIConfig( name: "claude", versionDetector: { browserDetection in ClaudeUsageFetcher(browserDetection: browserDetection).detectVersion() })) } private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] { if context.sourceMode == .api || self.hasAutoAdminAPIKey(context: context) { return [ClaudeAdminAPIFetchStrategy()] } if ClaudeAdminAPIFetchStrategy.isSelectedAdminAPIAccount(context: context) { return [ClaudeAdminAPIFetchStrategy()] } let planningInput = await Self.makePlanningInput(context: context) let plan = ClaudeSourcePlanner.resolve(input: planningInput) let manualCookieHeader = Self.manualCookieHeader(from: context) return plan.orderedSteps.map { step in let strategy: any ProviderFetchStrategy = switch step.dataSource { case .api: ClaudeAdminAPIFetchStrategy() case .oauth: ClaudeOAuthFetchStrategy() case .web: ClaudeWebFetchStrategy(browserDetection: context.browserDetection) case .cli: ClaudeCLIFetchStrategy( useWebExtras: context.runtime == .app && planningInput.webExtrasEnabled, manualCookieHeader: manualCookieHeader, browserDetection: context.browserDetection) case .auto: fatalError("Planner must not emit .auto as an executable step.") } return ClaudePlannedFetchStrategy(base: strategy, plannedStep: step) } } private static func hasAutoAdminAPIKey(context: ProviderFetchContext) -> Bool { context.sourceMode == .auto && ClaudeAdminAPISettingsReader.apiKey(environment: context.env) != nil } private static func makePlanningInput(context: ProviderFetchContext) async -> ClaudeSourcePlanningInput { let webExtrasEnabled = context.settings?.claude?.webExtrasEnabled ?? false let needsOAuthAvailability = context.runtime == .app && context.sourceMode == .auto return ClaudeSourcePlanningInput( runtime: context.runtime, selectedDataSource: Self.sourceDataSource(from: context.sourceMode), webExtrasEnabled: webExtrasEnabled, hasWebSession: ClaudeWebFetchStrategy.isAvailableForFallback( context: context, browserDetection: context.browserDetection), hasCLI: ClaudeCLIResolver.isAvailable(environment: context.env), hasOAuthCredentials: needsOAuthAvailability && ClaudeOAuthPlanningAvailability.isAvailable( runtime: context.runtime, sourceMode: context.sourceMode, environment: context.env)) } private static func manualCookieHeader(from context: ProviderFetchContext) -> String? { guard context.settings?.claude?.cookieSource == .manual else { return nil } return CookieHeaderNormalizer.normalize(context.settings?.claude?.manualCookieHeader) } private static func noDataMessage() -> String { "No Claude usage logs found in ~/.config/claude/projects or ~/.claude/projects." } public static func resolveUsageStrategy( selectedDataSource: ClaudeUsageDataSource, webExtrasEnabled: Bool, hasWebSession: Bool, hasCLI: Bool, hasOAuthCredentials: Bool) -> ClaudeUsageStrategy { let plan = ClaudeSourcePlanner.resolve(input: ClaudeSourcePlanningInput( runtime: .app, selectedDataSource: selectedDataSource, webExtrasEnabled: webExtrasEnabled, hasWebSession: hasWebSession, hasCLI: hasCLI, hasOAuthCredentials: hasOAuthCredentials)) return plan.compatibilityStrategy ?? ClaudeUsageStrategy(dataSource: selectedDataSource, useWebExtras: false) } private static func sourceDataSource(from mode: ProviderSourceMode) -> ClaudeUsageDataSource { switch mode { case .auto: .auto case .api: .api case .web: .web case .cli: .cli case .oauth: .oauth } }} public struct ClaudeUsageStrategy: Equatable, Sendable { public let dataSource: ClaudeUsageDataSource public let useWebExtras: Bool} public enum ClaudeOAuthPlanningAvailability { public static func isAvailable( runtime: ProviderRuntime, sourceMode: ProviderSourceMode, environment: [String: String]) -> Bool { ClaudeOAuthFetchStrategy.isPlausiblyAvailable( runtime: runtime, sourceMode: sourceMode, environment: environment) }} private struct ClaudePlannedFetchStrategy: ProviderFetchStrategy { let base: any ProviderFetchStrategy let plannedStep: ClaudeFetchPlanStep var id: String { self.base.id } var kind: ProviderFetchKind { self.base.kind } func isAvailable(_ context: ProviderFetchContext) async -> Bool { if context.sourceMode == .auto { return self.plannedStep.isPlausiblyAvailable } return await self.base.isAvailable(context) } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { try await self.base.fetch(context) } func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool { self.base.shouldFallback(on: error, context: context) }} struct ClaudeAdminAPIFetchStrategy: ProviderFetchStrategy { let id: String = "claude.admin-api" let kind: ProviderFetchKind = .apiToken let usageFetcher: @Sendable (String) async throws -> ClaudeAdminAPIUsageSnapshot init( usageFetcher: @escaping @Sendable (String) async throws -> ClaudeAdminAPIUsageSnapshot = { apiKey in try await ClaudeAdminAPIUsageFetcher.fetchUsage(apiKey: apiKey) }) { self.usageFetcher = usageFetcher } static func isSelectedAdminAPIAccount(context: ProviderFetchContext) -> Bool { guard context.selectedTokenAccountID != nil else { return false } return self.resolveToken(environment: context.env) != nil } func isAvailable(_ context: ProviderFetchContext) async -> Bool { Self.resolveToken(environment: context.env) != nil } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { guard let apiKey = Self.resolveToken(environment: context.env) else { throw ClaudeAdminAPISettingsError.missingToken } let usage = try await self.usageFetcher(apiKey) return self.makeResult( usage: usage.toUsageSnapshot(), sourceLabel: "admin-api") } func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool { context.runtime == .app && context.sourceMode == .auto && !Self.isSelectedAdminAPIAccount(context: context) } private static func resolveToken(environment: [String: String]) -> String? { ProviderTokenResolver.claudeAdminAPIToken(environment: environment) }} struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { let id: String = "claude.oauth" let kind: ProviderFetchKind = .oauth #if DEBUG @TaskLocal static var nonInteractiveCredentialRecordOverride: ClaudeOAuthCredentialRecord? @TaskLocal static var claudeCLIAvailableOverride: Bool? #endif private func loadNonInteractiveCredentialRecord(environment: [String: String]) -> ClaudeOAuthCredentialRecord? { #if DEBUG if let override = Self.nonInteractiveCredentialRecordOverride { return override } #endif return try? ClaudeOAuthCredentialsStore.loadRecord( environment: environment, allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: false) } private func isClaudeCLIAvailable(environment: [String: String]) -> Bool { #if DEBUG if let override = Self.claudeCLIAvailableOverride { return override } #endif return ClaudeCLIResolver.isAvailable(environment: environment) } static func isPlausiblyAvailable( runtime: ProviderRuntime, sourceMode: ProviderSourceMode, environment: [String: String]) -> Bool { let hasEnvironmentOAuthToken = !(environment[ClaudeOAuthCredentialsStore.environmentTokenKey]? .trimmingCharacters(in: .whitespacesAndNewlines) .isEmpty ?? true) if hasEnvironmentOAuthToken { return true } let strategy = ClaudeOAuthFetchStrategy() let nonInteractiveRecord = strategy.loadNonInteractiveCredentialRecord(environment: environment) let nonInteractiveCredentials = nonInteractiveRecord?.credentials let hasRequiredScopeWithoutPrompt = nonInteractiveCredentials?.scopes.contains("user:profile") == true if hasRequiredScopeWithoutPrompt, nonInteractiveCredentials?.isExpired == false { return true } let claudeCLIAvailable = strategy.isClaudeCLIAvailable(environment: environment) if let nonInteractiveRecord, hasRequiredScopeWithoutPrompt, nonInteractiveRecord.credentials.isExpired { switch nonInteractiveRecord.owner { case .codexbar: let refreshToken = nonInteractiveRecord.credentials.refreshToken? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if sourceMode == .auto { return !refreshToken.isEmpty } return true case .claudeCLI: if sourceMode == .auto { return claudeCLIAvailable } return true case .environment: return sourceMode != .auto } } guard sourceMode == .auto else { return true } let fallbackPromptMode = ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode() let promptPolicyApplicable = ClaudeOAuthKeychainPromptPreference.isApplicable() if ProviderInteractionContext.current == .userInitiated { _ = ClaudeOAuthKeychainAccessGate.clearDenied() } let shouldAllowStartupBootstrap = runtime == .app && ProviderRefreshContext.current == .startup && ProviderInteractionContext.current == .background && fallbackPromptMode == .onlyOnUserAction && !ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: environment) if shouldAllowStartupBootstrap { return ClaudeOAuthKeychainAccessGate.shouldAllowPrompt() } if promptPolicyApplicable, !ClaudeOAuthKeychainAccessGate.shouldAllowPrompt() { return false } return ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt() } func isAvailable(_ context: ProviderFetchContext) async -> Bool { Self.isPlausiblyAvailable( runtime: context.runtime, sourceMode: context.sourceMode, environment: context.env) } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { let fetcher = ClaudeUsageFetcher( browserDetection: context.browserDetection, environment: context.env, dataSource: .oauth, oauthKeychainPromptCooldownEnabled: context.sourceMode == .auto, allowBackgroundDelegatedRefresh: context.runtime == .cli, allowStartupBootstrapPrompt: context.runtime == .app && (context.sourceMode == .auto || context.sourceMode == .oauth), useWebExtras: false) let usage = try await fetcher.loadLatestUsage(model: "sonnet") return self.makeResult( usage: Self.snapshot(from: usage), sourceLabel: "oauth") } func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool { // In Auto mode, fall back to the next strategy (cli/web) if OAuth fails (e.g. user cancels keychain prompt // or auth breaks). context.runtime == .app && context.sourceMode == .auto } fileprivate static func snapshot(from usage: ClaudeUsageSnapshot) -> UsageSnapshot { let identity = ProviderIdentitySnapshot( providerID: .claude, accountEmail: usage.accountEmail, accountOrganization: usage.accountOrganization, loginMethod: usage.loginMethod) let primary = usage.primaryWindowKind == .spendLimit ? nil : usage.primary return UsageSnapshot( primary: primary, secondary: usage.secondary, tertiary: usage.opus, extraRateWindows: usage.extraRateWindows.isEmpty ? nil : usage.extraRateWindows, providerCost: usage.providerCost, updatedAt: usage.updatedAt, identity: identity) } static func _snapshotForTesting(from usage: ClaudeUsageSnapshot) -> UsageSnapshot { self.snapshot(from: usage) }} struct ClaudeWebFetchStrategy: ProviderFetchStrategy { let id: String = "claude.web" let kind: ProviderFetchKind = .web let browserDetection: BrowserDetection func isAvailable(_ context: ProviderFetchContext) async -> Bool { Self.isAvailableForFallback(context: context, browserDetection: self.browserDetection) } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { let fetcher = ClaudeUsageFetcher( browserDetection: browserDetection, dataSource: .web, useWebExtras: false, manualCookieHeader: Self.manualCookieHeader(from: context), webOrganizationID: context.settings?.claude?.organizationID) let usage = try await fetcher.loadLatestUsage(model: "sonnet") return self.makeResult( usage: ClaudeOAuthFetchStrategy.snapshot(from: usage), sourceLabel: "web") } func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool { guard context.sourceMode == .auto else { return false } _ = error // In CLI runtime auto mode, web comes before CLI so fallback is required. // In app runtime auto mode, web is terminal and should surface its concrete error. return context.runtime == .cli } fileprivate static func isAvailableForFallback( context: ProviderFetchContext, browserDetection: BrowserDetection) -> Bool { if let header = self.manualCookieHeader(from: context) { return ClaudeWebAPIFetcher.hasSessionKey(cookieHeader: header) } guard context.settings?.claude?.cookieSource != .off else { return false } return ClaudeWebAPIFetcher.hasSessionKey(browserDetection: browserDetection) } private static func manualCookieHeader(from context: ProviderFetchContext) -> String? { guard context.settings?.claude?.cookieSource == .manual else { return nil } return CookieHeaderNormalizer.normalize(context.settings?.claude?.manualCookieHeader) }} struct ClaudeCLIFetchStrategy: ProviderFetchStrategy { let id: String = "claude.cli" let kind: ProviderFetchKind = .cli let useWebExtras: Bool let manualCookieHeader: String? let browserDetection: BrowserDetection func isAvailable(_: ProviderFetchContext) async -> Bool { true } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { let keepAlive = context.settings?.debugKeepCLISessionsAlive ?? false let fetcher = ClaudeUsageFetcher( browserDetection: browserDetection, environment: context.env, dataSource: .cli, useWebExtras: self.useWebExtras, manualCookieHeader: self.manualCookieHeader, webOrganizationID: context.settings?.claude?.organizationID, keepCLISessionsAlive: keepAlive) let usage = try await fetcher.loadLatestUsage(model: "sonnet") return self.makeResult( usage: ClaudeOAuthFetchStrategy.snapshot(from: usage), sourceLabel: "claude") } func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool { guard context.runtime == .app, context.sourceMode == .auto else { return false } // Only fall through when web is actually available; otherwise preserve actionable CLI errors. return ClaudeWebFetchStrategy.isAvailableForFallback( context: context, browserDetection: self.browserDetection) }}
import Foundation
public enum ClaudeProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .claude,
metadata: ProviderMetadata(
id: .claude,
displayName: "Claude",
sessionLabel: "Session",
weeklyLabel: "Weekly",
opusLabel: "Sonnet",
supportsOpus: true,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Claude Code usage",
cliName: "claude",
defaultEnabled: false,
isPrimaryProvider: true,
usesAccountFallback: false,
browserCookieOrder: ProviderBrowserCookieDefaults.defaultImportOrder,
dashboardURL: "https://console.anthropic.com/settings/billing",
subscriptionDashboardURL: "https://claude.ai/settings/usage",
changelogURL: "https://github.com/anthropics/claude-code/releases",
statusPageURL: "https://status.claude.com/"),
branding: ProviderBranding(
iconStyle: .claude,
iconResourceName: "ProviderIcon-claude",
color: ProviderColor(red: 204 / 255, green: 124 / 255, blue: 94 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: true,
noDataMessage: self.noDataMessage),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api, .web, .cli, .oauth],
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
cli: ProviderCLIConfig(
name: "claude",
versionDetector: { browserDetection in
ClaudeUsageFetcher(browserDetection: browserDetection).detectVersion()
}))
}
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
if context.sourceMode == .api || self.hasAutoAdminAPIKey(context: context) {
return [ClaudeAdminAPIFetchStrategy()]
}
if ClaudeAdminAPIFetchStrategy.isSelectedAdminAPIAccount(context: context) {
return [ClaudeAdminAPIFetchStrategy()]
}
let planningInput = await Self.makePlanningInput(context: context)
let plan = ClaudeSourcePlanner.resolve(input: planningInput)
let manualCookieHeader = Self.manualCookieHeader(from: context)
return plan.orderedSteps.map { step in
let strategy: any ProviderFetchStrategy = switch step.dataSource {
case .api:
ClaudeAdminAPIFetchStrategy()
case .oauth:
ClaudeOAuthFetchStrategy()
case .web:
ClaudeWebFetchStrategy(browserDetection: context.browserDetection)
case .cli:
ClaudeCLIFetchStrategy(
useWebExtras: context.runtime == .app
&& planningInput.webExtrasEnabled,
manualCookieHeader: manualCookieHeader,
browserDetection: context.browserDetection)
case .auto:
fatalError("Planner must not emit .auto as an executable step.")
}
return ClaudePlannedFetchStrategy(base: strategy, plannedStep: step)
}
}
private static func hasAutoAdminAPIKey(context: ProviderFetchContext) -> Bool {
context.sourceMode == .auto && ClaudeAdminAPISettingsReader.apiKey(environment: context.env) != nil
}
private static func makePlanningInput(context: ProviderFetchContext) async -> ClaudeSourcePlanningInput {
let webExtrasEnabled = context.settings?.claude?.webExtrasEnabled ?? false
let needsOAuthAvailability = context.runtime == .app && context.sourceMode == .auto
return ClaudeSourcePlanningInput(
runtime: context.runtime,
selectedDataSource: Self.sourceDataSource(from: context.sourceMode),
webExtrasEnabled: webExtrasEnabled,
hasWebSession: ClaudeWebFetchStrategy.isAvailableForFallback(
context: context,
browserDetection: context.browserDetection),
hasCLI: ClaudeCLIResolver.isAvailable(environment: context.env),
hasOAuthCredentials: needsOAuthAvailability && ClaudeOAuthPlanningAvailability.isAvailable(
runtime: context.runtime,
sourceMode: context.sourceMode,
environment: context.env))
}
private static func manualCookieHeader(from context: ProviderFetchContext) -> String? {
guard context.settings?.claude?.cookieSource == .manual else { return nil }
return CookieHeaderNormalizer.normalize(context.settings?.claude?.manualCookieHeader)
}
private static func noDataMessage() -> String {
"No Claude usage logs found in ~/.config/claude/projects or ~/.claude/projects."
}
public static func resolveUsageStrategy(
selectedDataSource: ClaudeUsageDataSource,
webExtrasEnabled: Bool,
hasWebSession: Bool,
hasCLI: Bool,
hasOAuthCredentials: Bool) -> ClaudeUsageStrategy
{
let plan = ClaudeSourcePlanner.resolve(input: ClaudeSourcePlanningInput(
runtime: .app,
selectedDataSource: selectedDataSource,
webExtrasEnabled: webExtrasEnabled,
hasWebSession: hasWebSession,
hasCLI: hasCLI,
hasOAuthCredentials: hasOAuthCredentials))
return plan.compatibilityStrategy ?? ClaudeUsageStrategy(dataSource: selectedDataSource, useWebExtras: false)
}
private static func sourceDataSource(from mode: ProviderSourceMode) -> ClaudeUsageDataSource {
switch mode {
case .auto:
.auto
case .api:
.api
case .web:
.web
case .cli:
.cli
case .oauth:
.oauth
}
}
}
public struct ClaudeUsageStrategy: Equatable, Sendable {
public let dataSource: ClaudeUsageDataSource
public let useWebExtras: Bool
}
public enum ClaudeOAuthPlanningAvailability {
public static func isAvailable(
runtime: ProviderRuntime,
sourceMode: ProviderSourceMode,
environment: [String: String]) -> Bool
{
ClaudeOAuthFetchStrategy.isPlausiblyAvailable(
runtime: runtime,
sourceMode: sourceMode,
environment: environment)
}
}
private struct ClaudePlannedFetchStrategy: ProviderFetchStrategy {
let base: any ProviderFetchStrategy
let plannedStep: ClaudeFetchPlanStep
var id: String {
self.base.id
}
var kind: ProviderFetchKind {
self.base.kind
}
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
if context.sourceMode == .auto {
return self.plannedStep.isPlausiblyAvailable
}
return await self.base.isAvailable(context)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
try await self.base.fetch(context)
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
self.base.shouldFallback(on: error, context: context)
}
}
struct ClaudeAdminAPIFetchStrategy: ProviderFetchStrategy {
let id: String = "claude.admin-api"
let kind: ProviderFetchKind = .apiToken
let usageFetcher: @Sendable (String) async throws -> ClaudeAdminAPIUsageSnapshot
init(
usageFetcher: @escaping @Sendable (String) async throws -> ClaudeAdminAPIUsageSnapshot = { apiKey in
try await ClaudeAdminAPIUsageFetcher.fetchUsage(apiKey: apiKey)
})
{
self.usageFetcher = usageFetcher
}
static func isSelectedAdminAPIAccount(context: ProviderFetchContext) -> Bool {
guard context.selectedTokenAccountID != nil else { return false }
return self.resolveToken(environment: context.env) != nil
}
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
Self.resolveToken(environment: context.env) != nil
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = Self.resolveToken(environment: context.env) else {
throw ClaudeAdminAPISettingsError.missingToken
}
let usage = try await self.usageFetcher(apiKey)
return self.makeResult(
usage: usage.toUsageSnapshot(),
sourceLabel: "admin-api")
}
func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool {
context.runtime == .app &&
context.sourceMode == .auto &&
!Self.isSelectedAdminAPIAccount(context: context)
}
private static func resolveToken(environment: [String: String]) -> String? {
ProviderTokenResolver.claudeAdminAPIToken(environment: environment)
}
}
struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy {
let id: String = "claude.oauth"
let kind: ProviderFetchKind = .oauth
#if DEBUG
@TaskLocal static var nonInteractiveCredentialRecordOverride: ClaudeOAuthCredentialRecord?
@TaskLocal static var claudeCLIAvailableOverride: Bool?
#endif
private func loadNonInteractiveCredentialRecord(environment: [String: String]) -> ClaudeOAuthCredentialRecord? {
#if DEBUG
if let override = Self.nonInteractiveCredentialRecordOverride { return override }
#endif
return try? ClaudeOAuthCredentialsStore.loadRecord(
environment: environment,
allowKeychainPrompt: false,
respectKeychainPromptCooldown: true,
allowClaudeKeychainRepairWithoutPrompt: false)
}
private func isClaudeCLIAvailable(environment: [String: String]) -> Bool {
#if DEBUG
if let override = Self.claudeCLIAvailableOverride { return override }
#endif
return ClaudeCLIResolver.isAvailable(environment: environment)
}
static func isPlausiblyAvailable(
runtime: ProviderRuntime,
sourceMode: ProviderSourceMode,
environment: [String: String]) -> Bool
{
let hasEnvironmentOAuthToken = !(environment[ClaudeOAuthCredentialsStore.environmentTokenKey]?
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty ?? true)
if hasEnvironmentOAuthToken {
return true
}
let strategy = ClaudeOAuthFetchStrategy()
let nonInteractiveRecord = strategy.loadNonInteractiveCredentialRecord(environment: environment)
let nonInteractiveCredentials = nonInteractiveRecord?.credentials
let hasRequiredScopeWithoutPrompt = nonInteractiveCredentials?.scopes.contains("user:profile") == true
if hasRequiredScopeWithoutPrompt, nonInteractiveCredentials?.isExpired == false {
return true
}
let claudeCLIAvailable = strategy.isClaudeCLIAvailable(environment: environment)
if let nonInteractiveRecord, hasRequiredScopeWithoutPrompt, nonInteractiveRecord.credentials.isExpired {
switch nonInteractiveRecord.owner {
case .codexbar:
let refreshToken = nonInteractiveRecord.credentials.refreshToken?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if sourceMode == .auto {
return !refreshToken.isEmpty
}
return true
case .claudeCLI:
if sourceMode == .auto {
return claudeCLIAvailable
}
return true
case .environment:
return sourceMode != .auto
}
}
guard sourceMode == .auto else { return true }
let fallbackPromptMode = ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode()
let promptPolicyApplicable = ClaudeOAuthKeychainPromptPreference.isApplicable()
if ProviderInteractionContext.current == .userInitiated {
_ = ClaudeOAuthKeychainAccessGate.clearDenied()
}
let shouldAllowStartupBootstrap = runtime == .app &&
ProviderRefreshContext.current == .startup &&
ProviderInteractionContext.current == .background &&
fallbackPromptMode == .onlyOnUserAction &&
!ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: environment)
if shouldAllowStartupBootstrap {
return ClaudeOAuthKeychainAccessGate.shouldAllowPrompt()
}
if promptPolicyApplicable,
!ClaudeOAuthKeychainAccessGate.shouldAllowPrompt()
{
return false
}
return ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt()
}
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
Self.isPlausiblyAvailable(
runtime: context.runtime,
sourceMode: context.sourceMode,
environment: context.env)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let fetcher = ClaudeUsageFetcher(
browserDetection: context.browserDetection,
environment: context.env,
dataSource: .oauth,
oauthKeychainPromptCooldownEnabled: context.sourceMode == .auto,
allowBackgroundDelegatedRefresh: context.runtime == .cli,
allowStartupBootstrapPrompt: context.runtime == .app &&
(context.sourceMode == .auto || context.sourceMode == .oauth),
useWebExtras: false)
let usage = try await fetcher.loadLatestUsage(model: "sonnet")
return self.makeResult(
usage: Self.snapshot(from: usage),
sourceLabel: "oauth")
}
func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool {
// In Auto mode, fall back to the next strategy (cli/web) if OAuth fails (e.g. user cancels keychain prompt
// or auth breaks).
context.runtime == .app && context.sourceMode == .auto
}
fileprivate static func snapshot(from usage: ClaudeUsageSnapshot) -> UsageSnapshot {
let identity = ProviderIdentitySnapshot(
providerID: .claude,
accountEmail: usage.accountEmail,
accountOrganization: usage.accountOrganization,
loginMethod: usage.loginMethod)
let primary = usage.primaryWindowKind == .spendLimit ? nil : usage.primary
return UsageSnapshot(
primary: primary,
secondary: usage.secondary,
tertiary: usage.opus,
extraRateWindows: usage.extraRateWindows.isEmpty ? nil : usage.extraRateWindows,
providerCost: usage.providerCost,
updatedAt: usage.updatedAt,
identity: identity)
}
static func _snapshotForTesting(from usage: ClaudeUsageSnapshot) -> UsageSnapshot {
self.snapshot(from: usage)
}
}
struct ClaudeWebFetchStrategy: ProviderFetchStrategy {
let id: String = "claude.web"
let kind: ProviderFetchKind = .web
let browserDetection: BrowserDetection
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
Self.isAvailableForFallback(context: context, browserDetection: self.browserDetection)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let fetcher = ClaudeUsageFetcher(
browserDetection: browserDetection,
dataSource: .web,
useWebExtras: false,
manualCookieHeader: Self.manualCookieHeader(from: context),
webOrganizationID: context.settings?.claude?.organizationID)
let usage = try await fetcher.loadLatestUsage(model: "sonnet")
return self.makeResult(
usage: ClaudeOAuthFetchStrategy.snapshot(from: usage),
sourceLabel: "web")
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
guard context.sourceMode == .auto else { return false }
_ = error
// In CLI runtime auto mode, web comes before CLI so fallback is required.
// In app runtime auto mode, web is terminal and should surface its concrete error.
return context.runtime == .cli
}
fileprivate static func isAvailableForFallback(
context: ProviderFetchContext,
browserDetection: BrowserDetection) -> Bool
{
if let header = self.manualCookieHeader(from: context) {
return ClaudeWebAPIFetcher.hasSessionKey(cookieHeader: header)
}
guard context.settings?.claude?.cookieSource != .off else { return false }
return ClaudeWebAPIFetcher.hasSessionKey(browserDetection: browserDetection)
}
private static func manualCookieHeader(from context: ProviderFetchContext) -> String? {
guard context.settings?.claude?.cookieSource == .manual else { return nil }
return CookieHeaderNormalizer.normalize(context.settings?.claude?.manualCookieHeader)
}
}
struct ClaudeCLIFetchStrategy: ProviderFetchStrategy {
let id: String = "claude.cli"
let kind: ProviderFetchKind = .cli
let useWebExtras: Bool
let manualCookieHeader: String?
let browserDetection: BrowserDetection
func isAvailable(_: ProviderFetchContext) async -> Bool {
true
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let keepAlive = context.settings?.debugKeepCLISessionsAlive ?? false
let fetcher = ClaudeUsageFetcher(
browserDetection: browserDetection,
environment: context.env,
dataSource: .cli,
useWebExtras: self.useWebExtras,
manualCookieHeader: self.manualCookieHeader,
webOrganizationID: context.settings?.claude?.organizationID,
keepCLISessionsAlive: keepAlive)
let usage = try await fetcher.loadLatestUsage(model: "sonnet")
return self.makeResult(
usage: ClaudeOAuthFetchStrategy.snapshot(from: usage),
sourceLabel: "claude")
}
func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool {
guard context.runtime == .app, context.sourceMode == .auto else { return false }
// Only fall through when web is actually available; otherwise preserve actionable CLI errors.
return ClaudeWebFetchStrategy.isAvailableForFallback(
context: context,
browserDetection: self.browserDetection)
}
}