Files
steipete--codexbar/Sources/CodexBar/UsageStore+CodexResetCredits.swift
T
2026-07-13 12:22:33 +08:00

134 lines
5.8 KiB
Swift

import CodexBarCore
import Foundation
extension UsageStore {
typealias CodexResetCreditsFetcher = @Sendable ([String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?
func codexResetCreditsFetcher() -> CodexResetCreditsFetcher {
if let override = self._test_codexResetCreditsFetcherOverride {
return override
}
return { env in
try await Self.fetchCodexResetCredits(env: env)
}
}
func handleCodexResetCreditNotifications(snapshot: UsageSnapshot) {
guard self.settings.showOptionalCreditsAndExtraUsage,
let resetCredits = snapshot.codexResetCredits
else {
return
}
CodexResetCreditExpiryNotifier().postExpiringCreditsIfNeeded(
snapshot: resetCredits,
resetStyle: self.settings.resetTimeDisplayStyle)
}
nonisolated static func attachingCodexResetCreditsIfNeeded(
to outcome: ProviderFetchOutcome,
env: [String: String],
fetcher: @escaping CodexResetCreditsFetcher) async -> ProviderFetchOutcome
{
guard case let .success(result) = outcome.result else { return outcome }
let requiresResetCreditRescue = Self.requiresResetCreditRescue(result)
if result.usage.codexResetCredits != nil {
return outcome
}
do {
try Task.checkCancellation()
let resetCredits = try await fetcher(env)
try Task.checkCancellation()
if requiresResetCreditRescue,
(resetCredits?.availableInventory(at: result.usage.updatedAt).count ?? 0) == 0
{
return outcome.replacingResult(with: .failure(UsageError.noRateLimitsFound))
}
return outcome.replacingUsage(result.usage.withCodexResetCredits(resetCredits))
} catch {
if error is CancellationError || Task.isCancelled {
return ProviderFetchOutcome(result: .failure(CancellationError()), attempts: outcome.attempts)
}
if requiresResetCreditRescue {
return outcome.replacingResult(with: .failure(UsageError.noRateLimitsFound))
}
// A successful usage refresh must not retain reset-credit inventory from an older snapshot.
return outcome.replacingUsage(result.usage.withCodexResetCredits(nil))
}
}
private nonisolated static func requiresResetCreditRescue(_ result: ProviderFetchResult) -> Bool {
result.strategyID == "codex.oauth"
&& result.credits == nil
&& result.usage.primary == nil
&& result.usage.secondary == nil
&& result.usage.tertiary == nil
&& (result.usage.extraRateWindows?.isEmpty ?? true)
}
nonisolated static func fetchCodexResetCredits(
env: [String: String]) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try Task.checkCancellation()
let credentials = try CodexOAuthCredentialsStore.loadOAuthTokens(env: env)
return try await Self.fetchCodexResetCredits(
credentials: credentials,
env: env,
request: { accessToken, accountId, requestEnvironment in
try await CodexOAuthUsageFetcher.fetchRateLimitResetCredits(
accessToken: accessToken,
accountId: accountId,
env: requestEnvironment)
})
}
private nonisolated static func fetchCodexResetCredits(
credentials: CodexOAuthCredentials,
env: [String: String],
request: @escaping @Sendable (String, String?, [String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try Task.checkCancellation()
// Supplemental inventory is strictly read-only. The main OAuth usage strategy owns token refreshes;
// CLI/web winners with stale credentials simply skip this best-effort GET.
guard !credentials.needsRefresh else { return nil }
return try await request(credentials.accessToken, credentials.accountId, env)
}
nonisolated static func _fetchCodexResetCreditsForTesting(
credentials: CodexOAuthCredentials,
env: [String: String] = [:],
request: @escaping @Sendable (String, String?, [String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try await self.fetchCodexResetCredits(credentials: credentials, env: env, request: request)
}
}
extension ProviderFetchOutcome {
func replacingUsage(_ usage: UsageSnapshot) -> ProviderFetchOutcome {
guard case let .success(result) = self.result else { return self }
return ProviderFetchOutcome(
result: .success(ProviderFetchResult(
usage: usage,
credits: result.credits,
dashboard: result.dashboard,
sourceLabel: result.sourceLabel,
strategyID: result.strategyID,
strategyKind: result.strategyKind,
claudeOAuthKeychainPersistentRefHash: result.claudeOAuthKeychainPersistentRefHash,
claudeOAuthHistoryOwnerIdentifier: result.claudeOAuthHistoryOwnerIdentifier,
claudeOAuthKeychainCredentialMismatch: result.claudeOAuthKeychainCredentialMismatch,
claudeOAuthKeychainCredentialAbsent: result.claudeOAuthKeychainCredentialAbsent,
claudeOAuthKeychainCredentialUnavailable: result.claudeOAuthKeychainCredentialUnavailable)),
attempts: self.attempts)
}
fileprivate func replacingResult(
with result: Result<ProviderFetchResult, Error>) -> ProviderFetchOutcome
{
ProviderFetchOutcome(result: result, attempts: self.attempts)
}
}