1537 lines
68 KiB
Swift
1537 lines
68 KiB
Swift
import CodexBarCore
|
|
import CryptoKit
|
|
import Foundation
|
|
|
|
struct TokenAccountUsageSnapshot: Identifiable {
|
|
let id: UUID
|
|
let account: ProviderTokenAccount
|
|
let snapshot: UsageSnapshot?
|
|
let error: String?
|
|
let sourceLabel: String?
|
|
let cacheKey: String
|
|
|
|
init(
|
|
account: ProviderTokenAccount,
|
|
snapshot: UsageSnapshot?,
|
|
error: String?,
|
|
sourceLabel: String?,
|
|
cacheKey: String)
|
|
{
|
|
self.id = account.id
|
|
self.account = account
|
|
self.snapshot = snapshot
|
|
self.error = error
|
|
self.sourceLabel = sourceLabel
|
|
self.cacheKey = cacheKey
|
|
}
|
|
}
|
|
|
|
struct CodexAccountUsageSnapshot: Identifiable {
|
|
let id: String
|
|
let account: CodexVisibleAccount
|
|
let snapshot: UsageSnapshot?
|
|
let error: String?
|
|
let sourceLabel: String?
|
|
|
|
init(account: CodexVisibleAccount, snapshot: UsageSnapshot?, error: String?, sourceLabel: String?) {
|
|
self.id = account.id
|
|
self.account = account
|
|
self.snapshot = snapshot
|
|
self.error = error
|
|
self.sourceLabel = sourceLabel
|
|
}
|
|
}
|
|
|
|
extension UsageStore {
|
|
func activateCachedTokenAccountSnapshot(provider: UsageProvider, accountID: UUID) {
|
|
self.knownLimitsAvailabilityByProvider.removeValue(forKey: provider)
|
|
self.tokenAccountLiveStateProviders.insert(provider)
|
|
guard let account = self.uniqueTokenAccount(provider: provider, accountID: accountID),
|
|
let cached = self.accountSnapshots[provider]?.first(where: {
|
|
$0.account.id == accountID && $0.cacheKey == self.tokenAccountSnapshotCacheKey(
|
|
provider: provider,
|
|
account: account)
|
|
})
|
|
else {
|
|
self.accountSnapshots[provider]?.removeAll { $0.account.id == accountID }
|
|
// Never show the previous account's usage under the newly selected account. Segmented layouts only
|
|
// fetch the active account, so an uncached selection must render as refreshing until its fetch completes.
|
|
self.clearTokenAccountLiveSnapshot(provider: provider)
|
|
return
|
|
}
|
|
|
|
if let snapshot = cached.snapshot {
|
|
self.snapshots[provider] = snapshot
|
|
self.lastKnownResetSnapshots[provider] = snapshot
|
|
} else {
|
|
self.snapshots.removeValue(forKey: provider)
|
|
self.lastKnownResetSnapshots.removeValue(forKey: provider)
|
|
}
|
|
self.errors[provider] = cached.error
|
|
if let sourceLabel = cached.sourceLabel {
|
|
self.lastSourceLabels[provider] = sourceLabel
|
|
} else {
|
|
self.lastSourceLabels.removeValue(forKey: provider)
|
|
}
|
|
}
|
|
|
|
func cacheTokenAccountSnapshot(
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount,
|
|
snapshot: UsageSnapshot,
|
|
sourceLabel: String?)
|
|
{
|
|
let cached = TokenAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: snapshot,
|
|
error: nil,
|
|
sourceLabel: sourceLabel,
|
|
cacheKey: self.tokenAccountSnapshotCacheKey(provider: provider, account: account))
|
|
var snapshots = self.accountSnapshots[provider] ?? []
|
|
if let index = snapshots.firstIndex(where: { $0.account.id == account.id }) {
|
|
snapshots[index] = cached
|
|
} else {
|
|
snapshots.append(cached)
|
|
}
|
|
self.accountSnapshots[provider] = snapshots
|
|
}
|
|
|
|
func pruneTokenAccountSnapshots(provider: UsageProvider, accounts: [ProviderTokenAccount]) {
|
|
let retained = self.validTokenAccountSnapshots(provider: provider, accounts: accounts)
|
|
if retained.isEmpty {
|
|
self.accountSnapshots.removeValue(forKey: provider)
|
|
} else {
|
|
self.accountSnapshots[provider] = retained
|
|
}
|
|
}
|
|
|
|
func reconcileSelectedTokenAccountSnapshotBeforeRefresh(
|
|
provider: UsageProvider,
|
|
accounts: [ProviderTokenAccount])
|
|
{
|
|
self.pruneTokenAccountSnapshots(provider: provider, accounts: accounts)
|
|
guard let selectedAccount = self.settings.selectedTokenAccount(for: provider) else {
|
|
if self.tokenAccountLiveStateProviders.remove(provider) != nil {
|
|
self.knownLimitsAvailabilityByProvider.removeValue(forKey: provider)
|
|
self.clearTokenAccountLiveSnapshot(provider: provider)
|
|
}
|
|
return
|
|
}
|
|
// A Settings edit can invalidate the selected credential or endpoint before its replacement refresh
|
|
// completes. Reconcile the live card now so a failed/cancelled fetch cannot retain old-account data.
|
|
self.activateCachedTokenAccountSnapshot(provider: provider, accountID: selectedAccount.id)
|
|
}
|
|
|
|
private func clearTokenAccountLiveSnapshot(provider: UsageProvider) {
|
|
self.snapshots.removeValue(forKey: provider)
|
|
self.errors.removeValue(forKey: provider)
|
|
self.lastSourceLabels.removeValue(forKey: provider)
|
|
self.lastKnownResetSnapshots.removeValue(forKey: provider)
|
|
}
|
|
|
|
func validTokenAccountSnapshots(
|
|
provider: UsageProvider,
|
|
accounts: [ProviderTokenAccount]) -> [TokenAccountUsageSnapshot]
|
|
{
|
|
let accountsByID = Dictionary(grouping: accounts, by: \.id).compactMapValues { matches in
|
|
matches.count == 1 ? matches[0] : nil
|
|
}
|
|
return (self.accountSnapshots[provider] ?? []).filter { cached in
|
|
guard let account = accountsByID[cached.account.id] else { return false }
|
|
return cached.cacheKey == self.tokenAccountSnapshotCacheKey(provider: provider, account: account)
|
|
}
|
|
}
|
|
|
|
func tokenAccountSnapshotCacheKey(provider: UsageProvider, account: ProviderTokenAccount) -> String {
|
|
var config = self.settings.configSnapshot.providerConfig(for: provider) ?? ProviderConfig(id: provider)
|
|
// Active selection and sibling accounts must not invalidate a valid per-account snapshot.
|
|
config.tokenAccounts = nil
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.sortedKeys]
|
|
var material = Data(provider.rawValue.utf8)
|
|
material.append((try? encoder.encode(config)) ?? Data())
|
|
material.append((try? encoder.encode(account)) ?? Data())
|
|
return SHA256.hash(data: material).map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
func uniqueTokenAccount(provider: UsageProvider, accountID: UUID) -> ProviderTokenAccount? {
|
|
let matches = self.settings.tokenAccounts(for: provider).filter { $0.id == accountID }
|
|
return matches.count == 1 ? matches[0] : nil
|
|
}
|
|
}
|
|
|
|
private struct TokenAccountFetchResult {
|
|
let index: Int
|
|
let account: ProviderTokenAccount
|
|
let outcome: ProviderFetchOutcome
|
|
}
|
|
|
|
private struct CodexAccountFetchResult {
|
|
let index: Int
|
|
let account: CodexVisibleAccount
|
|
let outcome: ProviderFetchOutcome?
|
|
let limitResetOwnerKey: CodexLimitResetOwnerKey?
|
|
}
|
|
|
|
private struct CodexAccountFetchRequest {
|
|
let index: Int
|
|
let account: CodexVisibleAccount
|
|
let previousSnapshot: UsageSnapshot?
|
|
let missingWindowBackfillSnapshot: UsageSnapshot?
|
|
let limitResetOwnerKey: CodexLimitResetOwnerKey?
|
|
let descriptor: ProviderDescriptor
|
|
let context: ProviderFetchContext
|
|
}
|
|
|
|
private struct CodexManagedVisibleAccountRuntimeState {
|
|
let authFingerprint: String?
|
|
let workspaceAccountID: String?
|
|
}
|
|
|
|
extension UsageStore {
|
|
static let tokenAccountMenuSnapshotLimit = 6
|
|
|
|
func freshCodexVisibleAccountsForSnapshotHydration() -> [CodexVisibleAccount] {
|
|
self.freshCodexVisibleAccountProjectionForAccountRefresh().visibleAccounts
|
|
}
|
|
|
|
func tokenAccounts(for provider: UsageProvider) -> [ProviderTokenAccount] {
|
|
guard TokenAccountSupportCatalog.support(for: provider) != nil else { return [] }
|
|
return self.settings.tokenAccounts(for: provider)
|
|
}
|
|
|
|
func shouldFetchAllTokenAccounts(provider: UsageProvider, accounts: [ProviderTokenAccount]) -> Bool {
|
|
guard TokenAccountSupportCatalog.support(for: provider) != nil else { return false }
|
|
return self.settings.multiAccountMenuLayout == .stacked && accounts.count > 1
|
|
}
|
|
|
|
func shouldFetchAllCodexVisibleAccounts() -> Bool {
|
|
let projection = self.freshCodexVisibleAccountProjectionForAccountRefresh()
|
|
return self.settings.multiAccountMenuLayout == .stacked &&
|
|
projection.visibleAccounts.count > 1
|
|
}
|
|
|
|
func refreshCodexVisibleAccountsForMenu(generation: UInt64? = nil) async {
|
|
let projection = self.freshCodexVisibleAccountProjectionForAccountRefresh()
|
|
let accounts = self.limitedCodexVisibleAccounts(
|
|
projection.visibleAccounts,
|
|
snapshots: self.codexAccountSnapshots,
|
|
activeVisibleAccountID: projection.activeVisibleAccountID)
|
|
guard accounts.count > 1 else {
|
|
self.codexAccountSnapshots = []
|
|
return
|
|
}
|
|
let managedAccountIDsWithReadableAuthAtStart = self.codexManagedAccountIDsWithReadableAuth()
|
|
|
|
let originalVisibleAccountID = projection.activeVisibleAccountID
|
|
let originalSelectionSource = originalVisibleAccountID.flatMap {
|
|
projection.source(forVisibleAccountID: $0)
|
|
}
|
|
let originalVisibleAccount = originalVisibleAccountID.flatMap { id in
|
|
accounts.first { $0.id == id }
|
|
}
|
|
let priorSnapshots = self.codexAccountSnapshots
|
|
var snapshots: [CodexAccountUsageSnapshot] = []
|
|
var selectedOutcome: ProviderFetchOutcome?
|
|
var selectedAccount: CodexVisibleAccount?
|
|
var selectedSnapshot: UsageSnapshot?
|
|
var selectedSourceLabel: String?
|
|
var selectedLimitResetOwnerKey: CodexLimitResetOwnerKey?
|
|
|
|
let results = await self.fetchCodexVisibleAccountOutcomes(
|
|
accounts,
|
|
allVisibleAccounts: projection.visibleAccounts,
|
|
priorSnapshots: priorSnapshots,
|
|
activeVisibleAccountID: originalVisibleAccountID)
|
|
for result in results {
|
|
let account = result.account
|
|
let priorSnapshot = Self.codexPriorAccountSnapshot(
|
|
matching: account,
|
|
in: priorSnapshots)
|
|
guard let outcome = result.outcome else {
|
|
if let priorSnapshot {
|
|
snapshots.append(priorSnapshot)
|
|
}
|
|
if account.id == originalVisibleAccountID {
|
|
selectedAccount = account
|
|
selectedLimitResetOwnerKey = result.limitResetOwnerKey
|
|
}
|
|
continue
|
|
}
|
|
let resolved = self.resolveCodexAccountOutcome(
|
|
outcome,
|
|
account: account,
|
|
priorSnapshot: priorSnapshot,
|
|
resetBackfillSnapshots: result.limitResetOwnerKey == nil
|
|
? []
|
|
: self.codexResetBackfillSnapshots(
|
|
for: account,
|
|
priorSnapshot: priorSnapshot,
|
|
activeVisibleAccountID: originalVisibleAccountID))
|
|
if let snapshot = resolved.snapshot {
|
|
snapshots.append(snapshot)
|
|
}
|
|
if account.id == originalVisibleAccountID {
|
|
selectedOutcome = outcome
|
|
selectedAccount = account
|
|
selectedSnapshot = resolved.usage
|
|
selectedSourceLabel = resolved.sourceLabel
|
|
selectedLimitResetOwnerKey = result.limitResetOwnerKey
|
|
}
|
|
}
|
|
|
|
let currentProjection = self.freshCodexVisibleAccountProjectionForAccountRefresh(
|
|
requireLiveManagedAuthFor: managedAccountIDsWithReadableAuthAtStart)
|
|
guard self.isCurrentProviderRefreshGeneration(.codex, generation: generation) else { return }
|
|
let currentSnapshots = snapshots.compactMap { snapshot -> CodexAccountUsageSnapshot? in
|
|
guard let currentAccount = Self.currentCodexVisibleAccount(
|
|
matching: snapshot.account,
|
|
projection: currentProjection,
|
|
allowProviderAccountAuthFingerprintMismatch: snapshot.error == nil)
|
|
else {
|
|
return nil
|
|
}
|
|
guard currentAccount != snapshot.account else { return snapshot }
|
|
return CodexAccountUsageSnapshot(
|
|
account: currentAccount,
|
|
snapshot: Self.codexVisibleAccountSnapshotRelabeledForCurrentProjection(
|
|
snapshot.snapshot,
|
|
account: currentAccount),
|
|
error: snapshot.error,
|
|
sourceLabel: snapshot.sourceLabel)
|
|
}
|
|
self.codexAccountSnapshots = currentSnapshots
|
|
self.codexAccountUsageSnapshotStore?.store(currentSnapshots)
|
|
|
|
let selectionStillMatches = self.codexVisibleSelectionStillMatches(
|
|
originalVisibleAccountID: originalVisibleAccountID,
|
|
originalSelectionSource: originalSelectionSource,
|
|
originalAccount: originalVisibleAccount,
|
|
currentProjection: currentProjection)
|
|
guard let selectedOutcome, let selectedAccount else {
|
|
if selectionStillMatches,
|
|
let selectedID = currentProjection.activeVisibleAccountID,
|
|
let preserved = currentSnapshots.first(where: { $0.id == selectedID }),
|
|
let snapshot = preserved.snapshot
|
|
{
|
|
self.snapshots[.codex] = snapshot
|
|
self.lastKnownResetSnapshots[.codex] = snapshot
|
|
let publicationGuard = Self.codexScopedRefreshGuard(for: preserved.account)
|
|
self.lastCodexUsagePublicationGuard = publicationGuard
|
|
self.lastCodexAccountScopedRefreshGuard = publicationGuard
|
|
} else if !selectionStillMatches {
|
|
self.reconcileCodexAccountStateForUsageOwner(self.freshCodexAccountScopedRefreshGuard())
|
|
}
|
|
return
|
|
}
|
|
guard selectionStillMatches else {
|
|
self.reconcileCodexAccountStateForUsageOwner(self.freshCodexAccountScopedRefreshGuard())
|
|
return
|
|
}
|
|
|
|
let allowSelectedAuthFingerprintMismatch = switch selectedOutcome.result {
|
|
case .success:
|
|
true
|
|
case .failure:
|
|
false
|
|
}
|
|
let currentSelectedAccount = Self.currentCodexVisibleAccount(
|
|
matching: selectedAccount,
|
|
projection: currentProjection,
|
|
allowProviderAccountAuthFingerprintMismatch: allowSelectedAuthFingerprintMismatch)
|
|
if let currentSelectedAccount {
|
|
let currentSelectedSnapshot = Self.codexVisibleAccountSnapshotRelabeledForCurrentProjection(
|
|
selectedSnapshot,
|
|
account: currentSelectedAccount)
|
|
if self.shouldApplySelectedCodexVisibleAccountOutcome(
|
|
selectedOutcome,
|
|
snapshot: currentSelectedSnapshot)
|
|
{
|
|
await self.applySelectedCodexVisibleAccountOutcome(
|
|
selectedOutcome,
|
|
account: currentSelectedAccount,
|
|
snapshot: currentSelectedSnapshot,
|
|
sourceLabel: selectedSourceLabel,
|
|
limitResetOwnerKey: selectedLimitResetOwnerKey,
|
|
generation: generation)
|
|
}
|
|
} else {
|
|
self.reconcileCodexAccountStateForUsageOwner(self.freshCodexAccountScopedRefreshGuard())
|
|
}
|
|
}
|
|
|
|
func codexVisibleSelectionStillMatches(
|
|
originalVisibleAccountID: String?,
|
|
originalSelectionSource: CodexActiveSource?,
|
|
originalAccount: CodexVisibleAccount? = nil,
|
|
currentProjection: CodexVisibleAccountProjection? = nil) -> Bool
|
|
{
|
|
let currentProjection = currentProjection ?? self.settings.codexVisibleAccountProjection
|
|
let currentActiveAccount = currentProjection.activeVisibleAccountID.flatMap { id in
|
|
currentProjection.visibleAccounts.first { $0.id == id }
|
|
}
|
|
let currentSelectionSource = currentActiveAccount?.selectionSource
|
|
if currentProjection.activeVisibleAccountID == originalVisibleAccountID,
|
|
currentSelectionSource == originalSelectionSource
|
|
{
|
|
guard let originalAccount else { return true }
|
|
guard let currentActiveAccount else { return false }
|
|
return Self.codexVisibleAccountMatchesCurrentProjection(
|
|
originalAccount,
|
|
account: currentActiveAccount)
|
|
}
|
|
guard let originalAccount, let currentActiveAccount, currentSelectionSource == originalSelectionSource else {
|
|
return false
|
|
}
|
|
return Self.codexVisibleAccountMatchesCurrentProjection(originalAccount, account: currentActiveAccount)
|
|
}
|
|
|
|
private func freshCodexVisibleAccountProjectionForAccountRefresh(
|
|
requireLiveManagedAuthFor accountIDs: Set<UUID> = []) -> CodexVisibleAccountProjection
|
|
{
|
|
// Auth files can change while account fetches are in flight, so account refreshes bypass the
|
|
// short-lived reconciliation cache used for normal menu rendering and stale-result guards.
|
|
self.settings.invalidateCodexAccountReconciliationSnapshotCache()
|
|
let snapshot = self.settings.codexAccountReconciliationSnapshot
|
|
return Self.codexVisibleAccountProjectionWithFreshManagedAuthFingerprints(
|
|
CodexVisibleAccountProjection.make(from: snapshot),
|
|
snapshot: snapshot,
|
|
requireLiveManagedAuthFor: accountIDs)
|
|
}
|
|
|
|
private func codexManagedAccountIDsWithReadableAuth() -> Set<UUID> {
|
|
Set(self.settings.codexAccountReconciliationSnapshot.storedAccounts.compactMap { account in
|
|
CodexAuthFingerprint.fingerprint(homePath: account.managedHomePath) == nil ? nil : account.id
|
|
})
|
|
}
|
|
|
|
private nonisolated static func codexVisibleAccountProjectionWithFreshManagedAuthFingerprints(
|
|
_ projection: CodexVisibleAccountProjection,
|
|
snapshot: CodexAccountReconciliationSnapshot,
|
|
requireLiveManagedAuthFor accountIDs: Set<UUID> = []) -> CodexVisibleAccountProjection
|
|
{
|
|
let managedRuntimeStates = Dictionary(
|
|
uniqueKeysWithValues: snapshot.storedAccounts.map { account in
|
|
let workspaceAccountID: String? = switch snapshot.runtimeIdentity(for: account) {
|
|
case let .providerAccount(id):
|
|
id
|
|
case .emailOnly, .unresolved:
|
|
nil
|
|
}
|
|
let authFingerprint = CodexAuthFingerprint.fingerprint(homePath: account.managedHomePath)
|
|
let requiresLiveAuth = accountIDs.contains(account.id)
|
|
return (account.id, CodexManagedVisibleAccountRuntimeState(
|
|
authFingerprint: authFingerprint ?? (requiresLiveAuth ? nil : account.authFingerprint),
|
|
workspaceAccountID: authFingerprint == nil && requiresLiveAuth
|
|
? nil
|
|
: (workspaceAccountID ?? account.workspaceAccountID)))
|
|
})
|
|
let visibleAccounts = projection.visibleAccounts.map { account in
|
|
guard case let .managedAccount(id) = account.selectionSource else { return account }
|
|
let accountWorkspaceAccountID = account.workspaceAccountID
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
let runtimeWorkspaceAccountID = managedRuntimeStates[id]?.workspaceAccountID
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
guard let runtimeState = managedRuntimeStates[id],
|
|
runtimeState.authFingerprint != account.authFingerprint ||
|
|
runtimeWorkspaceAccountID != accountWorkspaceAccountID
|
|
else {
|
|
return account
|
|
}
|
|
return CodexVisibleAccount(
|
|
id: account.id,
|
|
email: account.email,
|
|
workspaceLabel: account.workspaceLabel,
|
|
workspaceAccountID: runtimeState.workspaceAccountID,
|
|
authFingerprint: runtimeState.authFingerprint,
|
|
storedAccountID: account.storedAccountID,
|
|
selectionSource: account.selectionSource,
|
|
isActive: account.isActive,
|
|
isLive: account.isLive,
|
|
canReauthenticate: account.canReauthenticate,
|
|
canRemove: account.canRemove)
|
|
}
|
|
return CodexVisibleAccountProjection(
|
|
visibleAccounts: visibleAccounts,
|
|
activeVisibleAccountID: projection.activeVisibleAccountID,
|
|
liveVisibleAccountID: projection.liveVisibleAccountID,
|
|
hasUnreadableAddedAccountStore: projection.hasUnreadableAddedAccountStore)
|
|
}
|
|
|
|
private static func currentCodexVisibleAccount(
|
|
matching account: CodexVisibleAccount,
|
|
projection: CodexVisibleAccountProjection,
|
|
allowProviderAccountAuthFingerprintMismatch: Bool = true) -> CodexVisibleAccount?
|
|
{
|
|
if let currentAccount = projection.visibleAccounts.first(where: { $0.id == account.id }),
|
|
self.codexVisibleAccountMatchesCurrentProjection(
|
|
account,
|
|
account: currentAccount,
|
|
allowProviderAccountAuthFingerprintMismatch: allowProviderAccountAuthFingerprintMismatch)
|
|
{
|
|
return currentAccount
|
|
}
|
|
return projection.visibleAccounts.first {
|
|
self.codexVisibleAccountMatchesCurrentProjection(
|
|
account,
|
|
account: $0,
|
|
allowProviderAccountAuthFingerprintMismatch: allowProviderAccountAuthFingerprintMismatch)
|
|
}
|
|
}
|
|
|
|
private static func codexVisibleAccountSnapshotRelabeledForCurrentProjection(
|
|
_ snapshot: UsageSnapshot?,
|
|
account: CodexVisibleAccount) -> UsageSnapshot?
|
|
{
|
|
guard let snapshot else { return nil }
|
|
let existing = snapshot.identity(for: .codex)
|
|
return snapshot.withIdentity(ProviderIdentitySnapshot(
|
|
providerID: .codex,
|
|
accountEmail: account.email,
|
|
accountOrganization: existing?.accountOrganization,
|
|
loginMethod: existing?.loginMethod ?? account.workspaceLabel))
|
|
}
|
|
|
|
private static func codexVisibleAccountMatchesCurrentProjection(
|
|
_ prior: CodexVisibleAccount,
|
|
account: CodexVisibleAccount,
|
|
allowProviderAccountAuthFingerprintMismatch: Bool = true) -> Bool
|
|
{
|
|
guard prior.selectionSource == account.selectionSource else { return false }
|
|
|
|
guard let priorEmail = CodexIdentityResolver.normalizeEmail(prior.email),
|
|
let accountEmail = CodexIdentityResolver.normalizeEmail(account.email),
|
|
priorEmail == accountEmail
|
|
else {
|
|
return false
|
|
}
|
|
|
|
let priorWorkspaceID = self.normalizedCodexVisibleAccountText(prior.workspaceAccountID)
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
let accountWorkspaceID = self.normalizedCodexVisibleAccountText(account.workspaceAccountID)
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
if priorWorkspaceID != nil || accountWorkspaceID != nil {
|
|
guard priorWorkspaceID == accountWorkspaceID else { return false }
|
|
if !allowProviderAccountAuthFingerprintMismatch {
|
|
guard self.codexVisibleAccountAuthFingerprintMatches(prior, account: account) else { return false }
|
|
}
|
|
return true
|
|
}
|
|
|
|
let priorAuthFingerprint = CodexAuthFingerprint.normalize(prior.authFingerprint)
|
|
let accountAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
|
|
if priorAuthFingerprint != nil || accountAuthFingerprint != nil {
|
|
guard priorAuthFingerprint == accountAuthFingerprint else { return false }
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private static func codexVisibleAccountAuthFingerprintMatches(
|
|
_ prior: CodexVisibleAccount,
|
|
account: CodexVisibleAccount) -> Bool
|
|
{
|
|
let priorAuthFingerprint = CodexAuthFingerprint.normalize(prior.authFingerprint)
|
|
let accountAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
|
|
if priorAuthFingerprint != nil || accountAuthFingerprint != nil {
|
|
return priorAuthFingerprint == accountAuthFingerprint
|
|
}
|
|
return true
|
|
}
|
|
|
|
func shouldApplySelectedCodexVisibleAccountOutcome(
|
|
_ outcome: ProviderFetchOutcome,
|
|
snapshot: UsageSnapshot?) -> Bool
|
|
{
|
|
switch outcome.result {
|
|
case .success:
|
|
snapshot != nil
|
|
case .failure:
|
|
true
|
|
}
|
|
}
|
|
|
|
func refreshTokenAccounts(
|
|
provider: UsageProvider,
|
|
accounts: [ProviderTokenAccount],
|
|
generation: UInt64? = nil) async
|
|
{
|
|
let selectedAccount = self.settings.selectedTokenAccount(for: provider)
|
|
let limitedAccounts = self.limitedTokenAccounts(accounts, selected: selectedAccount)
|
|
let effectiveSelected = selectedAccount ?? limitedAccounts.first
|
|
|
|
// Capture the prior per-account snapshot state so we can preserve last-good
|
|
// data when an in-flight refresh is cancelled (e.g. menu tab switches). Without
|
|
// this, cancellation produces empty/error snapshots and the menu briefly shows
|
|
// misleading cards for accounts that previously had valid data.
|
|
let priorSnapshots = await MainActor.run {
|
|
self.pruneTokenAccountSnapshots(provider: provider, accounts: accounts)
|
|
if let effectiveSelected {
|
|
self.activateCachedTokenAccountSnapshot(provider: provider, accountID: effectiveSelected.id)
|
|
}
|
|
return self.accountSnapshots[provider] ?? []
|
|
}
|
|
let priorByAccountID = Dictionary(uniqueKeysWithValues: priorSnapshots.map { ($0.account.id, $0) })
|
|
|
|
var snapshots: [TokenAccountUsageSnapshot] = []
|
|
var historySamples: [(account: ProviderTokenAccount, snapshot: UsageSnapshot)] = []
|
|
var selectedOutcome: ProviderFetchOutcome?
|
|
var selectedSnapshot: UsageSnapshot?
|
|
var sawAnyNonCancellationOutcome = false
|
|
|
|
let results = await self.fetchTokenAccountOutcomes(provider: provider, accounts: limitedAccounts)
|
|
guard self.isCurrentProviderRefreshGeneration(provider, generation: generation) else { return }
|
|
for result in results {
|
|
guard let account = self.uniqueTokenAccount(provider: provider, accountID: result.account.id)
|
|
else { continue }
|
|
let outcome = result.outcome
|
|
let isCancellation = Self.outcomeIsCancellation(outcome)
|
|
if !isCancellation {
|
|
sawAnyNonCancellationOutcome = true
|
|
}
|
|
let resolved = self.resolveAccountOutcome(
|
|
outcome,
|
|
provider: provider,
|
|
account: account,
|
|
priorSnapshot: priorByAccountID[account.id])
|
|
if let snapshot = resolved.snapshot {
|
|
snapshots.append(snapshot)
|
|
}
|
|
if let usage = resolved.usage {
|
|
historySamples.append((account: account, snapshot: usage))
|
|
}
|
|
if account.id == effectiveSelected?.id {
|
|
selectedOutcome = outcome
|
|
selectedSnapshot = resolved.usage
|
|
}
|
|
}
|
|
|
|
// If every fetch was cancelled (e.g. the user closed/reopened the menu mid-flight)
|
|
// and we have no usable snapshots, leave the prior per-account state alone.
|
|
// Wiping it would produce a menu of useless "cancelled" placeholders.
|
|
let shouldPreservePriorState = !sawAnyNonCancellationOutcome &&
|
|
snapshots.allSatisfy { $0.snapshot == nil }
|
|
if !shouldPreservePriorState {
|
|
await MainActor.run {
|
|
self.accountSnapshots[provider] = snapshots
|
|
}
|
|
}
|
|
|
|
if let selectedOutcome {
|
|
await self.applySelectedOutcome(
|
|
selectedOutcome,
|
|
provider: provider,
|
|
account: effectiveSelected,
|
|
fallbackSnapshot: selectedSnapshot,
|
|
generation: generation)
|
|
}
|
|
|
|
guard self.isCurrentProviderRefreshGeneration(provider, generation: generation) else { return }
|
|
await self.recordFetchedTokenAccountPlanUtilizationHistory(
|
|
provider: provider,
|
|
samples: historySamples,
|
|
selectedAccount: effectiveSelected)
|
|
}
|
|
|
|
private static func outcomeIsCancellation(_ outcome: ProviderFetchOutcome) -> Bool {
|
|
if case let .failure(error) = outcome.result, error is CancellationError {
|
|
return true
|
|
}
|
|
if case let .failure(error) = outcome.result {
|
|
return self.errorIsCancellation(error)
|
|
}
|
|
return false
|
|
}
|
|
|
|
private nonisolated static func codexUsageOutcomeMatchesVisibleAccount(
|
|
_ outcome: ProviderFetchOutcome,
|
|
account: CodexVisibleAccount) -> Bool
|
|
{
|
|
guard case let .success(result) = outcome.result else { return true }
|
|
guard let resultEmail = CodexIdentityResolver.normalizeEmail(
|
|
result.usage.scoped(to: .codex).accountEmail(for: .codex))
|
|
else {
|
|
return true
|
|
}
|
|
return resultEmail == CodexIdentityResolver.normalizeEmail(account.email)
|
|
}
|
|
|
|
nonisolated static func errorIsCancellation(_ error: any Error) -> Bool {
|
|
if error is CancellationError {
|
|
return true
|
|
}
|
|
if let urlError = error as? URLError, urlError.code == .cancelled {
|
|
return true
|
|
}
|
|
let message = error.localizedDescription
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.lowercased()
|
|
return message == "cancelled" ||
|
|
message.contains("cancellationerror") ||
|
|
message.contains("cancelled")
|
|
}
|
|
|
|
func limitedTokenAccounts(
|
|
_ accounts: [ProviderTokenAccount],
|
|
selected: ProviderTokenAccount?) -> [ProviderTokenAccount]
|
|
{
|
|
let limit = Self.tokenAccountMenuSnapshotLimit
|
|
if accounts.count <= limit {
|
|
return accounts
|
|
}
|
|
var limited = Array(accounts.prefix(limit))
|
|
if let selected, !limited.contains(where: { $0.id == selected.id }) {
|
|
limited.removeLast()
|
|
limited.append(selected)
|
|
}
|
|
return limited
|
|
}
|
|
|
|
func limitedCodexVisibleAccounts(
|
|
_ accounts: [CodexVisibleAccount],
|
|
snapshots: [CodexAccountUsageSnapshot] = [],
|
|
activeVisibleAccountID: String?) -> [CodexVisibleAccount]
|
|
{
|
|
let accounts = CodexAccountPresentationOrdering.orderedAccounts(
|
|
accounts,
|
|
snapshots: snapshots,
|
|
activeVisibleAccountID: activeVisibleAccountID)
|
|
let limit = Self.tokenAccountMenuSnapshotLimit
|
|
if accounts.count <= limit {
|
|
return accounts
|
|
}
|
|
var limited = Array(accounts.prefix(limit))
|
|
if let activeVisibleAccountID,
|
|
let active = accounts.first(where: { $0.id == activeVisibleAccountID }),
|
|
!limited.contains(where: { $0.id == activeVisibleAccountID })
|
|
{
|
|
limited.removeLast()
|
|
limited.append(active)
|
|
}
|
|
return limited
|
|
}
|
|
|
|
func fetchOutcome(
|
|
provider: UsageProvider,
|
|
override: TokenAccountOverride?,
|
|
codexActiveSourceOverride: CodexActiveSource? = nil) async -> ProviderFetchOutcome
|
|
{
|
|
let descriptor = self.providerSpecs[provider]?.descriptor ?? ProviderDescriptorRegistry
|
|
.descriptor(for: provider)
|
|
let context = self.makeFetchContext(
|
|
provider: provider,
|
|
override: override,
|
|
codexActiveSourceOverride: codexActiveSourceOverride)
|
|
let outcome = await descriptor.fetchOutcome(context: context)
|
|
guard provider == .codex else { return outcome }
|
|
return await Self.attachingCodexResetCreditsIfNeeded(
|
|
to: outcome,
|
|
env: context.env,
|
|
fetcher: self.codexResetCreditsFetcher())
|
|
}
|
|
|
|
private func fetchTokenAccountOutcomes(
|
|
provider: UsageProvider,
|
|
accounts: [ProviderTokenAccount]) async -> [TokenAccountFetchResult]
|
|
{
|
|
let requests: [(
|
|
index: Int,
|
|
account: ProviderTokenAccount,
|
|
descriptor: ProviderDescriptor,
|
|
context: ProviderFetchContext)] =
|
|
accounts.enumerated().map { index, account in
|
|
let override = TokenAccountOverride(provider: provider, account: account)
|
|
let descriptor = self.providerSpecs[provider]?.descriptor ?? ProviderDescriptorRegistry
|
|
.descriptor(for: provider)
|
|
let context = self.makeFetchContext(provider: provider, override: override)
|
|
return (index, account, descriptor, context)
|
|
}
|
|
|
|
return await withTaskGroup(
|
|
of: TokenAccountFetchResult.self,
|
|
returning: [TokenAccountFetchResult].self)
|
|
{ group in
|
|
for request in requests {
|
|
group.addTask {
|
|
let outcome = await request.descriptor.fetchOutcome(context: request.context)
|
|
return TokenAccountFetchResult(
|
|
index: request.index,
|
|
account: request.account,
|
|
outcome: outcome)
|
|
}
|
|
}
|
|
|
|
var results: [TokenAccountFetchResult] = []
|
|
results.reserveCapacity(requests.count)
|
|
for await result in group {
|
|
results.append(result)
|
|
}
|
|
return results.sorted { $0.index < $1.index }
|
|
}
|
|
}
|
|
|
|
private func fetchCodexVisibleAccountOutcomes(
|
|
_ accounts: [CodexVisibleAccount],
|
|
allVisibleAccounts: [CodexVisibleAccount],
|
|
priorSnapshots: [CodexAccountUsageSnapshot],
|
|
activeVisibleAccountID: String?) async
|
|
-> [CodexAccountFetchResult] {
|
|
let resetCreditsFetcher = self.codexResetCreditsFetcher()
|
|
let requests: [CodexAccountFetchRequest] = accounts.enumerated().map { index, account in
|
|
let descriptor = self.providerSpecs[.codex]?.descriptor ?? ProviderDescriptorRegistry
|
|
.descriptor(for: .codex)
|
|
let context = self.makeFetchContext(
|
|
provider: .codex,
|
|
override: nil,
|
|
codexActiveSourceOverride: account.selectionSource)
|
|
let limitResetOwnerKey = self.codexLimitResetOwnerKey(
|
|
forVisibleAccount: account,
|
|
visibleAccounts: allVisibleAccounts)
|
|
let priorSnapshot = Self.codexPriorAccountSnapshot(
|
|
matching: account,
|
|
in: priorSnapshots)
|
|
let trustedBackfillSnapshots = limitResetOwnerKey == nil
|
|
? []
|
|
: self.codexResetBackfillSnapshots(
|
|
for: account,
|
|
priorSnapshot: priorSnapshot,
|
|
activeVisibleAccountID: activeVisibleAccountID)
|
|
let missingWindowBackfillSnapshot = Self.codexMergedResetBackfillSnapshot(trustedBackfillSnapshots)
|
|
return CodexAccountFetchRequest(
|
|
index: index,
|
|
account: account,
|
|
previousSnapshot: limitResetOwnerKey == nil ? nil : priorSnapshot?.snapshot,
|
|
missingWindowBackfillSnapshot: missingWindowBackfillSnapshot,
|
|
limitResetOwnerKey: limitResetOwnerKey,
|
|
descriptor: descriptor,
|
|
context: context)
|
|
}
|
|
|
|
return await withTaskGroup(
|
|
of: CodexAccountFetchResult.self,
|
|
returning: [CodexAccountFetchResult].self)
|
|
{ group in
|
|
for request in requests {
|
|
group.addTask {
|
|
let fetchOutcome: CodexWeeklyConfirmationFetch = {
|
|
let baseOutcome = await request.descriptor.fetchOutcome(context: request.context)
|
|
return await Self.attachingCodexResetCreditsIfNeeded(
|
|
to: baseOutcome,
|
|
env: request.context.env,
|
|
fetcher: resetCreditsFetcher)
|
|
}
|
|
let initialOutcome = await fetchOutcome()
|
|
let outcome: ProviderFetchOutcome? = if Self.codexUsageOutcomeMatchesVisibleAccount(
|
|
initialOutcome,
|
|
account: request.account)
|
|
{
|
|
if let admitted = await Self.codexOutcomeAdmittedForPublication(
|
|
initialOutcome: initialOutcome,
|
|
previousSnapshot: request.previousSnapshot,
|
|
missingWindowBackfillSnapshot: request.missingWindowBackfillSnapshot,
|
|
fetchConfirmation: fetchOutcome),
|
|
Self.codexUsageOutcomeMatchesVisibleAccount(admitted, account: request.account)
|
|
{
|
|
admitted
|
|
} else {
|
|
nil
|
|
}
|
|
} else {
|
|
nil
|
|
}
|
|
return CodexAccountFetchResult(
|
|
index: request.index,
|
|
account: request.account,
|
|
outcome: outcome,
|
|
limitResetOwnerKey: request.limitResetOwnerKey)
|
|
}
|
|
}
|
|
|
|
var results: [CodexAccountFetchResult] = []
|
|
results.reserveCapacity(requests.count)
|
|
for await result in group {
|
|
results.append(result)
|
|
}
|
|
return results.sorted { $0.index < $1.index }
|
|
}
|
|
}
|
|
|
|
func makeFetchContext(
|
|
provider: UsageProvider,
|
|
override: TokenAccountOverride?,
|
|
codexActiveSourceOverride: CodexActiveSource? = nil,
|
|
includeCredits: Bool = false) -> ProviderFetchContext
|
|
{
|
|
let account = ProviderTokenAccountSelection.selectedAccount(
|
|
provider: provider,
|
|
settings: self.settings,
|
|
override: override)
|
|
let sourceMode = self.sourceMode(for: provider)
|
|
let snapshot = ProviderRegistry.makeSettingsSnapshot(
|
|
settings: self.settings,
|
|
tokenOverride: override,
|
|
codexActiveSourceOverride: codexActiveSourceOverride)
|
|
let env = ProviderRegistry.makeEnvironment(
|
|
base: self.environmentBase,
|
|
provider: provider,
|
|
settings: self.settings,
|
|
tokenOverride: override,
|
|
codexActiveSourceOverride: codexActiveSourceOverride)
|
|
let fetcher = ProviderRegistry.makeFetcher(base: self.codexFetcher, provider: provider, env: env)
|
|
let verbose = self.settings.isVerboseLoggingEnabled
|
|
let contextProvider = provider
|
|
let publicationGeneration = self.providerRefreshPublicationContexts[provider]?.generation
|
|
let contextConfigRevision = self.settings.providerConfigRevision(for: provider)
|
|
let originalAccountToken = account?.token
|
|
let originalManualToken = provider == .stepfun ? self.settings.stepfunToken : nil
|
|
return ProviderFetchContext(
|
|
runtime: .app,
|
|
sourceMode: sourceMode,
|
|
includeCredits: includeCredits,
|
|
includeOptionalUsage: self.settings.showOptionalCreditsAndExtraUsage,
|
|
webTimeout: 60,
|
|
webDebugDumpHTML: false,
|
|
verbose: verbose,
|
|
env: env,
|
|
settings: snapshot,
|
|
fetcher: fetcher,
|
|
claudeFetcher: self.claudeFetcher,
|
|
browserDetection: self.browserDetection,
|
|
selectedTokenAccountID: account?.id,
|
|
tokenAccountTokenUpdater: { [weak self] provider, accountID, token in
|
|
await MainActor.run {
|
|
guard let self, provider == contextProvider,
|
|
self.settings.tokenAccounts(for: provider)
|
|
.first(where: { $0.id == accountID })?.token == originalAccountToken
|
|
else {
|
|
return
|
|
}
|
|
guard self.providerConfigMutationIsCurrent(
|
|
provider: provider,
|
|
generation: publicationGeneration,
|
|
originalConfigRevision: contextConfigRevision)
|
|
else { return }
|
|
self.settings.updateTokenAccount(
|
|
provider: provider,
|
|
accountID: accountID,
|
|
token: token)
|
|
self.advanceProviderRefreshConfigRevision(
|
|
provider: provider,
|
|
generation: publicationGeneration)
|
|
}
|
|
},
|
|
providerManualTokenUpdater: { [weak self] provider, token in
|
|
await MainActor.run {
|
|
guard let self, provider == .stepfun,
|
|
self.settings.stepfunToken == originalManualToken
|
|
else { return }
|
|
guard self.providerConfigMutationIsCurrent(
|
|
provider: provider,
|
|
generation: publicationGeneration,
|
|
originalConfigRevision: contextConfigRevision)
|
|
else { return }
|
|
self.settings.stepfunToken = token
|
|
self.advanceProviderRefreshConfigRevision(
|
|
provider: provider,
|
|
generation: publicationGeneration)
|
|
}
|
|
},
|
|
costUsageHistoryDays: self.settings.costUsageHistoryDays,
|
|
persistsCLISessions: true,
|
|
persistentCLISessionIdleWindow: ProviderRegistry.persistentCLISessionIdleWindow(
|
|
refreshInterval: self.normalRefreshIntervalForHeuristics()))
|
|
}
|
|
|
|
private func providerConfigMutationIsCurrent(
|
|
provider: UsageProvider,
|
|
generation: UInt64?,
|
|
originalConfigRevision: UInt64) -> Bool
|
|
{
|
|
guard let generation else { return true }
|
|
let currentConfigRevision = self.settings.providerConfigRevision(for: provider)
|
|
guard let publication = self.providerRefreshPublicationContexts[provider] else { return false }
|
|
if publication.generation == generation {
|
|
return publication.configRevision == currentConfigRevision
|
|
}
|
|
// A replacement waits for its predecessor before capturing fetch inputs. Let the predecessor persist an
|
|
// authorized refresh token while its original config is unchanged; the replacement will then start from it.
|
|
return originalConfigRevision == currentConfigRevision
|
|
}
|
|
|
|
private func advanceProviderRefreshConfigRevision(provider: UsageProvider, generation: UInt64?) {
|
|
guard let generation,
|
|
var publication = self.providerRefreshPublicationContexts[provider],
|
|
publication.generation == generation
|
|
else { return }
|
|
publication.configRevision = self.settings.providerConfigRevision(for: provider)
|
|
self.providerRefreshPublicationContexts[provider] = publication
|
|
}
|
|
|
|
func sourceMode(for provider: UsageProvider) -> ProviderSourceMode {
|
|
ProviderCatalog.implementation(for: provider)?
|
|
.sourceMode(context: ProviderSourceModeContext(provider: provider, settings: self.settings))
|
|
?? .auto
|
|
}
|
|
|
|
private struct ResolvedAccountOutcome {
|
|
let snapshot: TokenAccountUsageSnapshot?
|
|
let usage: UsageSnapshot?
|
|
}
|
|
|
|
private struct ResolvedCodexAccountOutcome {
|
|
let snapshot: CodexAccountUsageSnapshot?
|
|
let usage: UsageSnapshot?
|
|
let sourceLabel: String?
|
|
}
|
|
|
|
func tokenAccountErrorMessage(_ error: any Error) -> String? {
|
|
guard !Self.errorIsCancellation(error) else { return nil }
|
|
let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return message.isEmpty ? nil : message
|
|
}
|
|
|
|
/// Per-account snapshot error text. Cancellation is handled before this path so
|
|
/// transient menu refresh cancellation does not render as a user-facing error.
|
|
func tokenAccountSnapshotErrorMessage(_ error: any Error) -> String {
|
|
let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return message.isEmpty ? "Refresh failed" : message
|
|
}
|
|
|
|
private func codexResetBackfillSnapshots(
|
|
for account: CodexVisibleAccount,
|
|
priorSnapshot: CodexAccountUsageSnapshot?,
|
|
activeVisibleAccountID: String?) -> [UsageSnapshot]
|
|
{
|
|
var snapshots: [UsageSnapshot] = []
|
|
if let priorSnapshot,
|
|
Self.codexPriorSnapshotAccountMatches(priorSnapshot.account, account: account),
|
|
let prior = priorSnapshot.snapshot
|
|
{
|
|
snapshots.append(prior)
|
|
}
|
|
if account.id == activeVisibleAccountID,
|
|
let lastKnown = self.codexLastKnownResetSnapshot(for: account)
|
|
{
|
|
snapshots.append(lastKnown)
|
|
}
|
|
// Plan history remains display-only: its legacy provider and email keys cannot prove
|
|
// the composite publication owner required for quota state.
|
|
return snapshots
|
|
}
|
|
|
|
private func codexLastKnownResetSnapshot(for account: CodexVisibleAccount) -> UsageSnapshot? {
|
|
guard let snapshot = self.lastKnownResetSnapshots[.codex],
|
|
Self.codexVisibleAccountEmailMatches(snapshot: snapshot, account: account),
|
|
Self.codexScopedGuard(self.lastCodexUsagePublicationGuard, matches: account)
|
|
else {
|
|
return nil
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func codexLastKnownResetSnapshot(matching guardValue: CodexAccountScopedRefreshGuard?) -> UsageSnapshot? {
|
|
guard let guardValue,
|
|
let lastGuard = self.lastCodexUsagePublicationGuard,
|
|
Self.codexScopedRefreshGuardAllowsResetBackfill(lastGuard, matching: guardValue)
|
|
else {
|
|
return nil
|
|
}
|
|
return self.lastKnownResetSnapshots[.codex]
|
|
}
|
|
|
|
private nonisolated static func codexVisibleAccountEmailMatches(
|
|
snapshot: UsageSnapshot,
|
|
account: CodexVisibleAccount) -> Bool
|
|
{
|
|
guard let identity = snapshot.identity(for: .codex),
|
|
let identityEmail = CodexIdentityResolver.normalizeEmail(identity.accountEmail),
|
|
let accountEmail = CodexIdentityResolver.normalizeEmail(account.email),
|
|
identityEmail == accountEmail
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
nonisolated static func codexPriorSnapshotAccountMatches(
|
|
_ prior: CodexVisibleAccount,
|
|
account: CodexVisibleAccount) -> Bool
|
|
{
|
|
guard let priorEmail = CodexIdentityResolver.normalizeEmail(prior.email),
|
|
let accountEmail = CodexIdentityResolver.normalizeEmail(account.email),
|
|
priorEmail == accountEmail
|
|
else {
|
|
return false
|
|
}
|
|
|
|
let priorWorkspaceID = self.normalizedCodexVisibleAccountText(prior.workspaceAccountID)
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
let accountWorkspaceID = self.normalizedCodexVisibleAccountText(account.workspaceAccountID)
|
|
.map(CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID)
|
|
if priorWorkspaceID != nil || accountWorkspaceID != nil {
|
|
return priorWorkspaceID == accountWorkspaceID
|
|
}
|
|
|
|
let priorAuthFingerprint = CodexAuthFingerprint.normalize(prior.authFingerprint)
|
|
let accountAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
|
|
if priorAuthFingerprint != nil || accountAuthFingerprint != nil {
|
|
guard priorAuthFingerprint == accountAuthFingerprint else { return false }
|
|
}
|
|
|
|
if prior.selectionSource == account.selectionSource {
|
|
switch account.selectionSource {
|
|
case .managedAccount:
|
|
return true
|
|
case .liveSystem:
|
|
return prior.id == account.id
|
|
case .profileHome:
|
|
return true
|
|
}
|
|
}
|
|
|
|
guard prior.id != prior.email, account.id != account.email else { return false }
|
|
return prior.id == account.id
|
|
}
|
|
|
|
private nonisolated static func codexPriorAccountSnapshot(
|
|
matching account: CodexVisibleAccount,
|
|
in snapshots: [CodexAccountUsageSnapshot]) -> CodexAccountUsageSnapshot?
|
|
{
|
|
if let exact = snapshots.first(where: { $0.id == account.id }),
|
|
self.codexPriorSnapshotAccountMatches(exact.account, account: account)
|
|
{
|
|
return exact
|
|
}
|
|
let matches = snapshots.filter {
|
|
self.codexPriorSnapshotAccountMatches($0.account, account: account)
|
|
}
|
|
guard matches.count == 1 else { return nil }
|
|
return matches[0]
|
|
}
|
|
|
|
private nonisolated static func codexScopedGuard(
|
|
_ guardValue: CodexAccountScopedRefreshGuard?,
|
|
matches account: CodexVisibleAccount) -> Bool
|
|
{
|
|
guard let guardValue, guardValue.source == account.selectionSource else { return false }
|
|
let guardAuthFingerprint = CodexAuthFingerprint.normalize(guardValue.authFingerprint)
|
|
let accountAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
|
|
if guardAuthFingerprint != nil || accountAuthFingerprint != nil {
|
|
guard guardAuthFingerprint == accountAuthFingerprint else { return false }
|
|
}
|
|
let identity = self.codexVisibleAccountIdentity(for: account)
|
|
if identity != .unresolved {
|
|
return guardValue.identity == identity
|
|
}
|
|
guard let accountKey = CodexIdentityResolver.normalizeEmail(account.email) else { return false }
|
|
return guardValue.accountKey == accountKey
|
|
}
|
|
|
|
private nonisolated static func codexScopedRefreshGuardAllowsResetBackfill(
|
|
_ lastGuard: CodexAccountScopedRefreshGuard,
|
|
matching expectedGuard: CodexAccountScopedRefreshGuard) -> Bool
|
|
{
|
|
self.codexScopedRefreshGuardsMatchAccount(lastGuard, expectedGuard)
|
|
}
|
|
|
|
private nonisolated static func codexScopedRefreshGuard(for account: CodexVisibleAccount)
|
|
-> CodexAccountScopedRefreshGuard
|
|
{
|
|
let accountEmail = CodexIdentityResolver.normalizeEmail(account.email)
|
|
return CodexAccountScopedRefreshGuard(
|
|
source: account.selectionSource,
|
|
identity: self.codexVisibleAccountIdentity(for: account),
|
|
accountKey: accountEmail,
|
|
authFingerprint: account.authFingerprint)
|
|
}
|
|
|
|
private nonisolated static func codexVisibleAccountIdentity(for account: CodexVisibleAccount) -> CodexIdentity {
|
|
if let workspaceAccountID = self.normalizedCodexVisibleAccountText(account.workspaceAccountID) {
|
|
return .providerAccount(id: CodexOpenAIWorkspaceIdentity.normalizeWorkspaceAccountID(workspaceAccountID))
|
|
}
|
|
return CodexIdentityResolver.resolve(accountId: nil, email: account.email)
|
|
}
|
|
|
|
private nonisolated static func normalizedCodexVisibleAccountText(_ text: String?) -> String? {
|
|
guard let trimmed = text?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
|
|
return nil
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
nonisolated static func codexBackfillingResetWindows(
|
|
_ snapshot: UsageSnapshot,
|
|
from cached: UsageSnapshot) -> UsageSnapshot
|
|
{
|
|
let primary = self.codexBackfillingResetWindow(
|
|
CodexConsumerProjection.sourceRateWindow(for: .session, snapshot: snapshot),
|
|
from: CodexConsumerProjection.sourceRateWindow(for: .session, snapshot: cached))
|
|
let secondary = self.codexBackfillingResetWindow(
|
|
CodexConsumerProjection.sourceRateWindow(for: .weekly, snapshot: snapshot),
|
|
from: CodexConsumerProjection.sourceRateWindow(for: .weekly, snapshot: cached))
|
|
guard primary != snapshot.primary || secondary != snapshot.secondary else { return snapshot }
|
|
return snapshot.with(primary: primary, secondary: secondary)
|
|
}
|
|
|
|
nonisolated static func codexMergedResetBackfillSnapshot(
|
|
_ snapshots: [UsageSnapshot],
|
|
now: Date = Date()) -> UsageSnapshot?
|
|
{
|
|
let primary = self.codexPreferredResetBackfillWindow(
|
|
snapshots.enumerated().compactMap { index, snapshot in
|
|
CodexConsumerProjection.sourceRateWindow(for: .session, snapshot: snapshot)
|
|
.map { (window: $0, updatedAt: snapshot.updatedAt, priority: index) }
|
|
},
|
|
now: now)
|
|
let secondary = self.codexPreferredResetBackfillWindow(
|
|
snapshots.enumerated().compactMap { index, snapshot in
|
|
CodexConsumerProjection.sourceRateWindow(for: .weekly, snapshot: snapshot)
|
|
.map { (window: $0, updatedAt: snapshot.updatedAt, priority: index) }
|
|
},
|
|
now: now)
|
|
guard primary != nil || secondary != nil else { return nil }
|
|
return UsageSnapshot(
|
|
primary: primary,
|
|
secondary: secondary,
|
|
updatedAt: snapshots.map(\.updatedAt).max() ?? now)
|
|
}
|
|
|
|
private nonisolated static func codexPreferredResetBackfillWindow(
|
|
_ windows: [(window: RateWindow, updatedAt: Date, priority: Int)],
|
|
now: Date) -> RateWindow?
|
|
{
|
|
windows
|
|
.filter { ($0.window.resetsAt ?? .distantPast) > now }
|
|
.max { lhs, rhs in
|
|
if lhs.updatedAt != rhs.updatedAt {
|
|
return lhs.updatedAt < rhs.updatedAt
|
|
}
|
|
if lhs.priority != rhs.priority {
|
|
return lhs.priority < rhs.priority
|
|
}
|
|
let lhsReset = lhs.window.resetsAt ?? .distantPast
|
|
let rhsReset = rhs.window.resetsAt ?? .distantPast
|
|
if lhsReset != rhsReset {
|
|
return lhsReset < rhsReset
|
|
}
|
|
return (lhs.window.windowMinutes ?? 0) < (rhs.window.windowMinutes ?? 0)
|
|
}
|
|
.map(\.window)
|
|
}
|
|
|
|
private nonisolated static func codexBackfillingResetWindow(
|
|
_ window: RateWindow?,
|
|
from cached: RateWindow?) -> RateWindow?
|
|
{
|
|
guard let cached,
|
|
let resetsAt = cached.resetsAt,
|
|
resetsAt > Date()
|
|
else {
|
|
return window
|
|
}
|
|
if let window {
|
|
return window.backfillingResetTime(from: cached)
|
|
}
|
|
guard let windowMinutes = cached.windowMinutes, windowMinutes > 0 else { return nil }
|
|
return RateWindow(
|
|
usedPercent: cached.usedPercent,
|
|
windowMinutes: windowMinutes,
|
|
resetsAt: resetsAt,
|
|
resetDescription: cached.resetDescription)
|
|
}
|
|
|
|
func recordFetchedTokenAccountPlanUtilizationHistory(
|
|
provider: UsageProvider,
|
|
samples: [(account: ProviderTokenAccount, snapshot: UsageSnapshot)],
|
|
selectedAccount: ProviderTokenAccount?) async
|
|
{
|
|
for sample in samples where sample.account.id != selectedAccount?.id {
|
|
await self.recordPlanUtilizationHistorySample(
|
|
provider: provider,
|
|
snapshot: sample.snapshot,
|
|
account: sample.account,
|
|
shouldUpdatePreferredAccountKey: false,
|
|
shouldAdoptUnscopedHistory: false)
|
|
}
|
|
}
|
|
|
|
private func resolveAccountOutcome(
|
|
_ outcome: ProviderFetchOutcome,
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount,
|
|
priorSnapshot: TokenAccountUsageSnapshot? = nil) -> ResolvedAccountOutcome
|
|
{
|
|
switch outcome.result {
|
|
case let .success(result):
|
|
let scoped = result.usage.scoped(to: provider)
|
|
let labeled = self.applyAccountLabel(scoped, provider: provider, account: account)
|
|
let snapshot = TokenAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: labeled,
|
|
error: nil,
|
|
sourceLabel: result.sourceLabel,
|
|
cacheKey: self.tokenAccountSnapshotCacheKey(provider: provider, account: account))
|
|
return ResolvedAccountOutcome(snapshot: snapshot, usage: labeled)
|
|
case let .failure(error):
|
|
// Preserve the last-good snapshot when the refresh was cancelled (e.g. the
|
|
// user switched menu tabs mid-flight). Without this the per-account list
|
|
// would briefly render error chips for accounts that already had data.
|
|
if Self.errorIsCancellation(error) {
|
|
if let priorSnapshot, priorSnapshot.snapshot != nil {
|
|
return ResolvedAccountOutcome(snapshot: priorSnapshot, usage: priorSnapshot.snapshot)
|
|
}
|
|
// No usable prior data: skip this row entirely. The caller will
|
|
// either preserve the existing per-account state or fall back to
|
|
// the single live card. Rendering a "cancelled" placeholder here
|
|
// produces visually duplicate cards with no useful data.
|
|
return ResolvedAccountOutcome(snapshot: nil, usage: nil)
|
|
}
|
|
let snapshot = TokenAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: nil,
|
|
error: self.tokenAccountSnapshotErrorMessage(error),
|
|
sourceLabel: nil,
|
|
cacheKey: self.tokenAccountSnapshotCacheKey(provider: provider, account: account))
|
|
return ResolvedAccountOutcome(snapshot: snapshot, usage: nil)
|
|
}
|
|
}
|
|
|
|
private func resolveCodexAccountOutcome(
|
|
_ outcome: ProviderFetchOutcome,
|
|
account: CodexVisibleAccount,
|
|
priorSnapshot: CodexAccountUsageSnapshot? = nil,
|
|
resetBackfillSnapshots: [UsageSnapshot] = []) -> ResolvedCodexAccountOutcome
|
|
{
|
|
switch outcome.result {
|
|
case let .success(result):
|
|
let scoped = result.usage.scoped(to: .codex)
|
|
if let resultEmail = CodexIdentityResolver.normalizeEmail(scoped.accountEmail(for: .codex)),
|
|
resultEmail != CodexIdentityResolver.normalizeEmail(account.email)
|
|
{
|
|
return ResolvedCodexAccountOutcome(
|
|
snapshot: priorSnapshot,
|
|
usage: nil,
|
|
sourceLabel: priorSnapshot?.sourceLabel)
|
|
}
|
|
let labeled = self.applyCodexVisibleAccountLabel(scoped, account: account)
|
|
let backfilled = Self.codexMergedResetBackfillSnapshot(resetBackfillSnapshots)
|
|
.map { Self.codexBackfillingResetWindows(labeled, from: $0) } ?? labeled
|
|
let snapshot = CodexAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: backfilled,
|
|
error: nil,
|
|
sourceLabel: result.sourceLabel)
|
|
return ResolvedCodexAccountOutcome(
|
|
snapshot: snapshot,
|
|
usage: backfilled,
|
|
sourceLabel: result.sourceLabel)
|
|
case let .failure(error):
|
|
if Self.errorIsCancellation(error) {
|
|
if let priorSnapshot, priorSnapshot.snapshot != nil {
|
|
return ResolvedCodexAccountOutcome(
|
|
snapshot: priorSnapshot,
|
|
usage: priorSnapshot.snapshot,
|
|
sourceLabel: priorSnapshot.sourceLabel)
|
|
}
|
|
return ResolvedCodexAccountOutcome(snapshot: nil, usage: nil, sourceLabel: nil)
|
|
}
|
|
let errorMessage = self.tokenAccountSnapshotErrorMessage(error)
|
|
if Self.shouldPreserveCodexAccountSnapshotOnFailure(errorMessage),
|
|
let priorSnapshot,
|
|
let priorUsage = priorSnapshot.snapshot
|
|
{
|
|
let snapshot = CodexAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: priorUsage,
|
|
error: errorMessage,
|
|
sourceLabel: priorSnapshot.sourceLabel)
|
|
return ResolvedCodexAccountOutcome(
|
|
snapshot: snapshot,
|
|
usage: priorUsage,
|
|
sourceLabel: priorSnapshot.sourceLabel)
|
|
}
|
|
let snapshot = CodexAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: nil,
|
|
error: errorMessage,
|
|
sourceLabel: nil)
|
|
return ResolvedCodexAccountOutcome(snapshot: snapshot, usage: nil, sourceLabel: nil)
|
|
}
|
|
}
|
|
|
|
private static func shouldPreserveCodexAccountSnapshotOnFailure(_ message: String) -> Bool {
|
|
guard CodexAccountHealth.status(forError: message) == .unavailable else { return false }
|
|
let normalized = message.lowercased()
|
|
return normalized.contains("network") ||
|
|
normalized.contains("internet connection") ||
|
|
normalized.contains("offline") ||
|
|
normalized.contains("timed out") ||
|
|
normalized.contains("timeout") ||
|
|
normalized.contains("connection was lost") ||
|
|
normalized.contains("could not connect") ||
|
|
normalized.contains("not connected") ||
|
|
normalized.contains("hostname") ||
|
|
normalized.contains("dns") ||
|
|
normalized.contains("temporarily unavailable")
|
|
}
|
|
|
|
func applySelectedCodexVisibleAccountOutcome(
|
|
_ outcome: ProviderFetchOutcome,
|
|
account: CodexVisibleAccount,
|
|
snapshot: UsageSnapshot?,
|
|
sourceLabel: String?,
|
|
limitResetOwnerKey: CodexLimitResetOwnerKey?,
|
|
generation: UInt64? = nil) async
|
|
{
|
|
guard self.isCurrentProviderRefreshGeneration(.codex, generation: generation) else { return }
|
|
switch outcome.result {
|
|
case .success:
|
|
guard let snapshot else { return }
|
|
let publicationGuard = Self.codexScopedRefreshGuard(for: account)
|
|
self.lastFetchAttempts[.codex] = outcome.attempts
|
|
self.handleCodexResetCreditNotifications(snapshot: snapshot)
|
|
self.handleSessionQuotaTransition(
|
|
provider: .codex,
|
|
snapshot: snapshot,
|
|
codexOwnerKey: Self.codexSessionQuotaOwnerKey(for: publicationGuard))
|
|
self.handlePredictivePaceWarningTransitions(provider: .codex, snapshot: snapshot)
|
|
self.lastKnownResetSnapshots[.codex] = snapshot
|
|
self.lastCodexUsagePublicationGuard = publicationGuard
|
|
self.lastCodexAccountScopedRefreshGuard = publicationGuard
|
|
self.snapshots[.codex] = snapshot
|
|
if let sourceLabel {
|
|
self.lastSourceLabels[.codex] = sourceLabel
|
|
}
|
|
self.errors[.codex] = nil
|
|
self.failureGates[.codex]?.recordSuccess()
|
|
self.rememberLiveSystemCodexEmailIfNeeded(snapshot.accountEmail(for: .codex))
|
|
self.seedCodexAccountScopedRefreshGuard(accountEmail: account.email)
|
|
await self.recordPlanUtilizationHistorySample(
|
|
provider: .codex,
|
|
snapshot: snapshot,
|
|
codexLimitResetOwnerKey: limitResetOwnerKey)
|
|
guard self.isCurrentProviderRefreshGeneration(.codex, generation: generation) else { return }
|
|
self.recordCodexHistoricalSampleIfNeeded(snapshot: snapshot)
|
|
case let .failure(error):
|
|
guard let message = self.tokenAccountErrorMessage(error) else {
|
|
self.errors[.codex] = nil
|
|
return
|
|
}
|
|
let publicationGuard = Self.codexScopedRefreshGuard(for: account)
|
|
self.lastCodexUsagePublicationGuard = publicationGuard
|
|
self.lastCodexAccountScopedRefreshGuard = publicationGuard
|
|
self.lastFetchAttempts[.codex] = outcome.attempts
|
|
let hadPriorData = self.snapshots[.codex] != nil
|
|
let shouldSurface =
|
|
self.failureGates[.codex]?
|
|
.shouldSurfaceError(onFailureWithPriorData: hadPriorData) ?? true
|
|
if shouldSurface {
|
|
self.errors[.codex] = message
|
|
self.snapshots.removeValue(forKey: .codex)
|
|
} else {
|
|
self.errors[.codex] = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func applySelectedOutcome(
|
|
_ outcome: ProviderFetchOutcome,
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount?,
|
|
fallbackSnapshot: UsageSnapshot?,
|
|
generation: UInt64? = nil) async
|
|
{
|
|
await MainActor.run {
|
|
guard self.isCurrentProviderRefreshGeneration(provider, generation: generation) else { return }
|
|
self.lastFetchAttempts[provider] = outcome.attempts
|
|
}
|
|
guard self.isCurrentProviderRefreshGeneration(provider, generation: generation) else { return }
|
|
switch outcome.result {
|
|
case let .success(result):
|
|
let scoped = result.usage.scoped(to: provider)
|
|
let labeled: UsageSnapshot = if let account {
|
|
self.applyAccountLabel(scoped, provider: provider, account: account)
|
|
} else {
|
|
scoped
|
|
}
|
|
let backfilled = await MainActor.run {
|
|
guard self.isCurrentProviderRefreshGeneration(provider, generation: generation) else {
|
|
return nil as UsageSnapshot?
|
|
}
|
|
let backfilled = labeled.backfillingResetTimes(from: self.lastKnownResetSnapshots[provider])
|
|
let predictivePaceWarningAccountDiscriminatorOverride: String? = if provider == .claude {
|
|
Self.predictivePaceWarningTokenAccountDiscriminator(account)
|
|
} else {
|
|
nil
|
|
}
|
|
self.handleQuotaWarningTransitions(provider: provider, snapshot: backfilled)
|
|
self.handleSessionQuotaTransition(provider: provider, snapshot: backfilled)
|
|
self.handlePredictivePaceWarningTransitions(
|
|
provider: provider,
|
|
snapshot: backfilled,
|
|
accountDiscriminatorOverride: predictivePaceWarningAccountDiscriminatorOverride)
|
|
self.lastKnownResetSnapshots[provider] = backfilled
|
|
self.snapshots[provider] = backfilled
|
|
self.lastSourceLabels[provider] = result.sourceLabel
|
|
self.errors[provider] = nil
|
|
self.knownLimitsAvailabilityByProvider.removeValue(forKey: provider)
|
|
self.failureGates[provider]?.recordSuccess()
|
|
return backfilled
|
|
}
|
|
guard let backfilled else { return }
|
|
await self.recordPlanUtilizationHistorySample(
|
|
provider: provider,
|
|
snapshot: backfilled,
|
|
account: account)
|
|
case let .failure(error):
|
|
await MainActor.run {
|
|
self.knownLimitsAvailabilityByProvider.removeValue(forKey: provider)
|
|
guard let message = self.tokenAccountErrorMessage(error) else {
|
|
self.errors[provider] = nil
|
|
return
|
|
}
|
|
let hadPriorData = self.snapshots[provider] != nil || fallbackSnapshot != nil
|
|
let shouldSurface = self.failureGates[provider]?
|
|
.shouldSurfaceError(onFailureWithPriorData: hadPriorData) ?? true
|
|
if shouldSurface {
|
|
self.errors[provider] = message
|
|
self.snapshots.removeValue(forKey: provider)
|
|
} else {
|
|
self.errors[provider] = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyAccountLabel(
|
|
_ snapshot: UsageSnapshot,
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount) -> UsageSnapshot
|
|
{
|
|
let label = account.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !label.isEmpty else { return snapshot }
|
|
let existing = snapshot.identity(for: provider)
|
|
let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let resolvedEmail = (email?.isEmpty ?? true) ? label : email
|
|
let identity = ProviderIdentitySnapshot(
|
|
providerID: provider,
|
|
accountEmail: resolvedEmail,
|
|
accountOrganization: existing?.accountOrganization,
|
|
loginMethod: existing?.loginMethod)
|
|
return snapshot.withIdentity(identity)
|
|
}
|
|
|
|
func applyCodexVisibleAccountLabel(_ snapshot: UsageSnapshot, account: CodexVisibleAccount) -> UsageSnapshot {
|
|
let existing = snapshot.identity(for: .codex)
|
|
let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let resolvedEmail = (email?.isEmpty ?? true) ? account.email : email
|
|
let loginMethod = existing?.loginMethod ?? account.workspaceLabel
|
|
let identity = ProviderIdentitySnapshot(
|
|
providerID: .codex,
|
|
accountEmail: resolvedEmail,
|
|
accountOrganization: existing?.accountOrganization,
|
|
loginMethod: loginMethod)
|
|
return snapshot.withIdentity(identity)
|
|
}
|
|
}
|