344 lines
13 KiB
Swift
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
|
|
}
|
|
}
|