Files
steipete--codexbar/Sources/CodexBarCore/Providers/Codex/CodexReconciledState.swift
T
2026-07-13 12:22:33 +08:00

150 lines
5.7 KiB
Swift

import Foundation
public struct CodexReconciledState: Sendable {
public let session: RateWindow?
public let weekly: RateWindow?
/// Named model-specific limits (e.g. Codex Spark) surfaced through `UsageSnapshot.extraRateWindows`.
public let extraRateWindows: [NamedRateWindow]
public let identity: ProviderIdentitySnapshot?
public let updatedAt: Date
public init(
session: RateWindow?,
weekly: RateWindow?,
extraRateWindows: [NamedRateWindow] = [],
identity: ProviderIdentitySnapshot?,
updatedAt: Date)
{
self.session = session
self.weekly = weekly
self.extraRateWindows = extraRateWindows
self.identity = identity
self.updatedAt = updatedAt
}
public static func fromCLI(
primary: RateWindow?,
secondary: RateWindow?,
identity: ProviderIdentitySnapshot?,
updatedAt: Date = Date()) -> CodexReconciledState?
{
self.make(primary: primary, secondary: secondary, identity: identity, updatedAt: updatedAt)
}
public static func fromOAuth(
response: CodexUsageResponse,
credentials: CodexOAuthCredentials,
updatedAt: Date = Date()) -> CodexReconciledState?
{
self.make(
primary: self.makeWindow(response.rateLimit?.primaryWindow),
secondary: self.makeWindow(response.rateLimit?.secondaryWindow),
extraRateWindows: CodexAdditionalRateLimitMapper.extraRateWindows(
from: response.additionalRateLimits,
now: updatedAt),
identity: self.oauthIdentity(response: response, credentials: credentials),
updatedAt: updatedAt)
}
public static func fromAttachedDashboard(
snapshot: OpenAIDashboardSnapshot,
provider: UsageProvider = .codex,
accountEmail: String? = nil,
accountPlan: String? = nil) -> CodexReconciledState?
{
let resolvedEmail = accountEmail ?? snapshot.signedInEmail
let resolvedPlan = accountPlan ?? snapshot.accountPlan
let identity = ProviderIdentitySnapshot(
providerID: provider,
accountEmail: resolvedEmail,
accountOrganization: nil,
loginMethod: resolvedPlan)
return self.make(
primary: snapshot.primaryLimit,
secondary: snapshot.secondaryLimit,
extraRateWindows: snapshot.extraRateWindows ?? [],
identity: identity,
updatedAt: snapshot.updatedAt)
}
public func toUsageSnapshot() -> UsageSnapshot {
UsageSnapshot(
primary: self.session,
secondary: self.weekly,
tertiary: nil,
extraRateWindows: self.extraRateWindows.isEmpty ? nil : self.extraRateWindows,
updatedAt: self.updatedAt,
identity: self.identity)
}
public static func oauthIdentity(
response: CodexUsageResponse,
credentials: CodexOAuthCredentials) -> ProviderIdentitySnapshot
{
ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: self.resolveAccountEmail(from: credentials),
accountOrganization: nil,
loginMethod: self.resolvePlan(response: response, credentials: credentials))
}
private static func make(
primary: RateWindow?,
secondary: RateWindow?,
extraRateWindows: [NamedRateWindow] = [],
identity: ProviderIdentitySnapshot?,
updatedAt: Date) -> CodexReconciledState?
{
let normalized = CodexRateWindowNormalizer.normalize(primary: primary, secondary: secondary)
// Extra windows are supplemental, so they never resurrect a snapshot on their own: keep the
// existing primary/weekly gate to preserve current behavior when only extra limits are present.
guard normalized.primary != nil || normalized.secondary != nil else {
return nil
}
return CodexReconciledState(
session: normalized.primary,
weekly: normalized.secondary,
extraRateWindows: extraRateWindows,
identity: identity,
updatedAt: updatedAt)
}
private static func makeWindow(_ window: CodexUsageResponse.WindowSnapshot?) -> RateWindow? {
guard let window else { return nil }
let resetDate = Date(timeIntervalSince1970: TimeInterval(window.resetAt))
let resetDescription = UsageFormatter.resetDescription(from: resetDate)
return RateWindow(
usedPercent: Double(window.usedPercent),
windowMinutes: window.limitWindowSeconds / 60,
resetsAt: resetDate,
resetDescription: resetDescription)
}
private static func resolveAccountEmail(from credentials: CodexOAuthCredentials) -> String? {
guard let idToken = credentials.idToken,
let payload = UsageFetcher.parseJWT(idToken)
else {
return nil
}
let profileDict = payload["https://api.openai.com/profile"] as? [String: Any]
let email = (payload["email"] as? String) ?? (profileDict?["email"] as? String)
return email?.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func resolvePlan(response: CodexUsageResponse, credentials: CodexOAuthCredentials) -> String? {
if let plan = response.planType?.rawValue, !plan.isEmpty { return plan }
guard let idToken = credentials.idToken,
let payload = UsageFetcher.parseJWT(idToken)
else {
return nil
}
let authDict = payload["https://api.openai.com/auth"] as? [String: Any]
let plan = (authDict?["chatgpt_plan_type"] as? String) ?? (payload["chatgpt_plan_type"] as? String)
return plan?.trimmingCharacters(in: .whitespacesAndNewlines)
}
}