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

344 lines
13 KiB
Swift

import Foundation
public struct CopilotUsageResponse: Sendable, Decodable {
private struct AnyCodingKey: CodingKey {
let stringValue: String
let intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
public struct QuotaSnapshot: Sendable, Decodable {
public let entitlement: Double
public let remaining: Double
public let percentRemaining: Double
public let quotaId: String
public let hasPercentRemaining: Bool
public let unlimited: Bool
private let entitlementWasDecoded: Bool
private let remainingWasDecoded: Bool
public var usedPercent: Double {
max(0, 100 - self.percentRemaining)
}
public var overQuotaUsedPercent: Double? {
self.usedPercent > 100 ? self.usedPercent : nil
}
public var isPlaceholder: Bool {
if self.unlimited {
return false
}
if self.entitlement == 0,
self.remaining == 0,
self.percentRemaining == 0,
!self.hasPercentRemaining
{
return true
}
// An explicit zero-entitlement, zero-remaining snapshot carries no usable quota signal.
// GitHub returns this shape for token-based billing / Copilot Business seats,
// sometimes as percent_remaining=100 with a non-empty quota_id, which would
// otherwise render as a misleading "0% used" (100 - 100). Treat it as a
// placeholder so the usual handling drops it instead of showing fake usage.
return self.entitlementWasDecoded && self.remainingWasDecoded && self.entitlement == 0 && self
.remaining == 0
}
private enum CodingKeys: String, CodingKey {
case entitlement
case remaining
case percentRemaining = "percent_remaining"
case quotaId = "quota_id"
case unlimited
}
public init(
entitlement: Double,
remaining: Double,
percentRemaining: Double,
quotaId: String,
hasPercentRemaining: Bool = true,
unlimited: Bool = false)
{
self.entitlement = entitlement
self.remaining = remaining
self.percentRemaining = unlimited ? 100 : percentRemaining
self.quotaId = quotaId
self.hasPercentRemaining = unlimited || hasPercentRemaining
self.unlimited = unlimited
self.entitlementWasDecoded = true
self.remainingWasDecoded = true
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let decodedEntitlement = Self.decodeNumberIfPresent(container: container, key: .entitlement)
let decodedRemaining = Self.decodeNumberIfPresent(container: container, key: .remaining)
self.entitlement = decodedEntitlement ?? 0
self.remaining = decodedRemaining ?? 0
self.entitlementWasDecoded = decodedEntitlement != nil
self.remainingWasDecoded = decodedRemaining != nil
let decodedUnlimited = try container.decodeIfPresent(Bool.self, forKey: .unlimited) ?? false
let decodedPercent = Self.decodeNumberIfPresent(container: container, key: .percentRemaining)
if decodedUnlimited {
self.percentRemaining = 100
self.hasPercentRemaining = true
} else if let decodedPercent {
self.percentRemaining = decodedPercent
self.hasPercentRemaining = true
} else if let entitlement = decodedEntitlement,
entitlement > 0,
let remaining = decodedRemaining
{
let derived = (remaining / entitlement) * 100
self.percentRemaining = derived
self.hasPercentRemaining = true
} else {
// Without percent_remaining and both inputs for derivation, the percent is unknown.
self.percentRemaining = 0
self.hasPercentRemaining = false
}
self.quotaId = try container.decodeIfPresent(String.self, forKey: .quotaId) ?? ""
self.unlimited = decodedUnlimited
}
private static func decodeNumberIfPresent(
container: KeyedDecodingContainer<CodingKeys>,
key: CodingKeys) -> Double?
{
if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
return value
}
if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
return Double(value)
}
if let value = try? container.decodeIfPresent(String.self, forKey: key) {
return Double(value)
}
return nil
}
}
public struct QuotaCounts: Sendable, Decodable {
public let chat: Double?
public let completions: Double?
private enum CodingKeys: String, CodingKey {
case chat
case completions
}
public init(chat: Double?, completions: Double?) {
self.chat = chat
self.completions = completions
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.chat = Self.decodeNumberIfPresent(container: container, key: .chat)
self.completions = Self.decodeNumberIfPresent(container: container, key: .completions)
}
private static func decodeNumberIfPresent(
container: KeyedDecodingContainer<CodingKeys>,
key: CodingKeys) -> Double?
{
if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
return value
}
if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
return Double(value)
}
if let value = try? container.decodeIfPresent(String.self, forKey: key) {
return Double(value)
}
return nil
}
}
public struct QuotaSnapshots: Sendable, Decodable {
public let premiumInteractions: QuotaSnapshot?
public let chat: QuotaSnapshot?
private enum CodingKeys: String, CodingKey {
case premiumInteractions = "premium_interactions"
case chat
}
public init(premiumInteractions: QuotaSnapshot?, chat: QuotaSnapshot?) {
self.premiumInteractions = premiumInteractions
self.chat = chat
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var premium = try container.decodeIfPresent(QuotaSnapshot.self, forKey: .premiumInteractions)
var chat = try container.decodeIfPresent(QuotaSnapshot.self, forKey: .chat)
if premium?.isPlaceholder == true {
premium = nil
}
if chat?.isPlaceholder == true {
chat = nil
}
if premium == nil || chat == nil {
let dynamic = try decoder.container(keyedBy: AnyCodingKey.self)
var fallbackPremium: QuotaSnapshot?
var fallbackChat: QuotaSnapshot?
var firstUsable: QuotaSnapshot?
for key in dynamic.allKeys {
let value: QuotaSnapshot
do {
guard let decoded = try dynamic.decodeIfPresent(QuotaSnapshot.self, forKey: key) else {
continue
}
guard !decoded.isPlaceholder else { continue }
value = decoded
} catch {
continue
}
let name = key.stringValue.lowercased()
if firstUsable == nil {
firstUsable = value
}
if fallbackChat == nil, name.contains("chat") {
fallbackChat = value
continue
}
if fallbackPremium == nil,
name.contains("premium") || name.contains("completion") || name.contains("code")
{
fallbackPremium = value
}
}
if premium == nil {
premium = fallbackPremium
}
if chat == nil {
chat = fallbackChat
}
if premium == nil, chat == nil {
// If keys are unfamiliar, still expose one usable quota instead of failing.
chat = firstUsable
}
}
self.premiumInteractions = premium
self.chat = chat
}
}
public let quotaSnapshots: QuotaSnapshots
public let copilotPlan: String
public let tokenBasedBilling: Bool
public let assignedDate: String?
public let quotaResetDate: String?
private enum CodingKeys: String, CodingKey {
case quotaSnapshots = "quota_snapshots"
case copilotPlan = "copilot_plan"
case tokenBasedBilling = "token_based_billing"
case assignedDate = "assigned_date"
case quotaResetDate = "quota_reset_date"
case monthlyQuotas = "monthly_quotas"
case limitedUserQuotas = "limited_user_quotas"
}
public init(
quotaSnapshots: QuotaSnapshots,
copilotPlan: String,
tokenBasedBilling: Bool = false,
assignedDate: String?,
quotaResetDate: String?)
{
self.quotaSnapshots = quotaSnapshots
self.copilotPlan = copilotPlan
self.tokenBasedBilling = tokenBasedBilling
self.assignedDate = assignedDate
self.quotaResetDate = quotaResetDate
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let directSnapshots = try container.decodeIfPresent(QuotaSnapshots.self, forKey: .quotaSnapshots)
let monthlyQuotas = try container.decodeIfPresent(QuotaCounts.self, forKey: .monthlyQuotas)
let limitedUserQuotas = try container.decodeIfPresent(QuotaCounts.self, forKey: .limitedUserQuotas)
let monthlyLimitedSnapshots = Self.makeQuotaSnapshots(monthly: monthlyQuotas, limited: limitedUserQuotas)
let premium = Self.usableQuotaSnapshot(from: directSnapshots?.premiumInteractions) ??
Self.usableQuotaSnapshot(from: monthlyLimitedSnapshots?.premiumInteractions)
let chat = Self.usableQuotaSnapshot(from: directSnapshots?.chat) ??
Self.usableQuotaSnapshot(from: monthlyLimitedSnapshots?.chat)
if premium != nil || chat != nil {
self.quotaSnapshots = QuotaSnapshots(premiumInteractions: premium, chat: chat)
} else {
self.quotaSnapshots = directSnapshots ?? QuotaSnapshots(premiumInteractions: nil, chat: nil)
}
self.copilotPlan = try container.decodeIfPresent(String.self, forKey: .copilotPlan) ?? "unknown"
self.tokenBasedBilling = try container.decodeIfPresent(Bool.self, forKey: .tokenBasedBilling) ?? false
self.assignedDate = try container.decodeIfPresent(String.self, forKey: .assignedDate)
self.quotaResetDate = try container.decodeIfPresent(String.self, forKey: .quotaResetDate)
}
private static func makeQuotaSnapshots(monthly: QuotaCounts?, limited: QuotaCounts?) -> QuotaSnapshots? {
let premium = Self.makeQuotaSnapshot(
monthly: monthly?.completions,
limited: limited?.completions,
quotaID: "completions")
let chat = Self.makeQuotaSnapshot(
monthly: monthly?.chat,
limited: limited?.chat,
quotaID: "chat")
guard premium != nil || chat != nil else { return nil }
return QuotaSnapshots(premiumInteractions: premium, chat: chat)
}
private static func makeQuotaSnapshot(monthly: Double?, limited: Double?, quotaID: String) -> QuotaSnapshot? {
guard monthly != nil || limited != nil else { return nil }
guard let monthly else {
// Without a monthly denominator, avoid fabricating a misleading percentage.
return nil
}
guard let limited else {
// Without the limited/remaining value, usage is unknown.
return nil
}
let entitlement = max(0, monthly)
guard entitlement > 0 else {
// A zero denominator cannot produce a meaningful percentage.
return nil
}
let remaining = max(0, limited)
let percentRemaining = max(0, min(100, (remaining / entitlement) * 100))
return QuotaSnapshot(
entitlement: entitlement,
remaining: remaining,
percentRemaining: percentRemaining,
quotaId: quotaID)
}
private static func usableQuotaSnapshot(from snapshot: QuotaSnapshot?) -> QuotaSnapshot? {
guard let snapshot, !snapshot.isPlaceholder, snapshot.hasPercentRemaining else {
return nil
}
return snapshot
}
}