codeburn/mac/Sources/CodeBurnMenubar/Data/CodexUsage.swift
Aditya Vikram Singh 688467e86f
Some checks are pending
CI / semgrep (push) Waiting to run
menubar: surface Codex limit-reset credits (count and next expiry) in the Plan tab (#723) (#724)
Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
2026-07-17 05:58:21 -07:00

107 lines
4.1 KiB
Swift

import Foundation
/// Codex (ChatGPT-mode) live quota snapshot returned by /backend-api/wham/usage.
/// Two windows are exposed: primary (typically the 5-hour rolling window) and
/// secondary (typically the weekly window). Window size is dynamic per
/// account `limitWindowSeconds` tells us whether it's a 5-hour or 7-day
/// boundary so we can label correctly.
struct CodexUsage: Sendable, Equatable {
enum PlanType: Sendable, Equatable {
case guest, free, go, plus, pro, prolite, freeWorkspace, team
case business, education, quorum, k12, enterprise, edu
/// Captures any plan_type string OpenAI ships that we haven't enumerated
/// yet, so the Settings/Plan UI can still show "Plan: <raw>" instead of
/// a generic "Subscription" placeholder. Preserves forward compatibility
/// without requiring a CodeBurn update for every new tier.
case unknown(String)
var displayName: String {
switch self {
case .guest: "Guest"
case .free: "Free"
case .go: "Go"
case .plus: "Plus"
case .pro: "Pro"
case .prolite: "Pro Lite"
case .freeWorkspace: "Free Workspace"
case .team: "Team"
case .business: "Business"
case .education: "Education"
case .quorum: "Quorum"
case .k12: "K-12"
case .enterprise: "Enterprise"
case .edu: "Edu"
case let .unknown(raw): raw.isEmpty ? "Subscription" : raw.capitalized
}
}
}
struct Window: Sendable, Equatable {
let usedPercent: Double // 0.0 ... 100.0
let resetsAt: Date?
let limitWindowSeconds: Int
/// Human label inferred from window size: 5h, 1d, 7d, etc.
var windowLabel: String {
switch limitWindowSeconds {
case 0..<3600: return "Hourly"
case 3600..<7200: return "Hour"
case 18000..<19000: return "5-hour"
case 86400..<87000: return "Daily"
case 604800..<605000: return "Weekly"
default:
let hours = limitWindowSeconds / 3600
if hours < 24 { return "\(hours)-hour" }
return "\(hours / 24)-day"
}
}
}
/// Additional per-model / per-feature quotas exposed by ChatGPT alongside
/// the main rate_limit (e.g. "GPT-5.3-Codex-Spark"). Each entry has its
/// own primary/secondary windows. Only ones with non-zero utilization are
/// surfaced in the popover so users on plans that don't touch these
/// features don't see clutter.
struct AdditionalLimit: Sendable, Equatable {
let name: String
let primary: Window?
let secondary: Window?
}
/// Account-level limit-reset credits: grants that restore a rate-limit
/// window early, each with its own expiry. Only what the popover renders
/// the available count and the soonest expiry among available credits.
struct ResetCredits: Sendable, Equatable {
let availableCount: Int
let nextExpiresAt: Date?
}
let plan: PlanType
let primary: Window?
let secondary: Window?
let additionalLimits: [AdditionalLimit]
let creditsBalance: Double?
let resetCredits: ResetCredits?
let fetchedAt: Date
static func planType(from raw: String?) -> PlanType {
guard let raw = raw?.lowercased() else { return .unknown("") }
switch raw {
case "guest": return .guest
case "free": return .free
case "go": return .go
case "plus": return .plus
case "pro": return .pro
case "prolite", "pro_lite", "pro-lite": return .prolite
case "free_workspace": return .freeWorkspace
case "team": return .team
case "business": return .business
case "education": return .education
case "quorum": return .quorum
case "k12": return .k12
case "enterprise": return .enterprise
case "edu": return .edu
default: return .unknown(raw)
}
}
}