136 lines
5.6 KiB
Swift
136 lines
5.6 KiB
Swift
import Foundation
|
|
|
|
public struct KimiUsageSnapshot: Sendable {
|
|
public let weekly: KimiUsageDetail
|
|
public let rateLimit: KimiUsageDetail?
|
|
public let updatedAt: Date
|
|
let subscriptionBalance: KimiSubscriptionBalance?
|
|
let subscriptionCodeWeeklyLimit: KimiSubscriptionRateLimit?
|
|
|
|
public init(weekly: KimiUsageDetail, rateLimit: KimiUsageDetail?, updatedAt: Date) {
|
|
self.weekly = weekly
|
|
self.rateLimit = rateLimit
|
|
self.updatedAt = updatedAt
|
|
self.subscriptionBalance = nil
|
|
self.subscriptionCodeWeeklyLimit = nil
|
|
}
|
|
|
|
init(
|
|
weekly: KimiUsageDetail,
|
|
rateLimit: KimiUsageDetail?,
|
|
subscriptionBalance: KimiSubscriptionBalance?,
|
|
subscriptionCodeWeeklyLimit: KimiSubscriptionRateLimit? = nil,
|
|
updatedAt: Date)
|
|
{
|
|
self.weekly = weekly
|
|
self.rateLimit = rateLimit
|
|
self.subscriptionBalance = subscriptionBalance
|
|
self.subscriptionCodeWeeklyLimit = subscriptionCodeWeeklyLimit
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
private static func parseDate(_ dateString: String?) -> Date? {
|
|
guard let dateString else { return nil }
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
if let date = formatter.date(from: dateString) {
|
|
return date
|
|
}
|
|
let fallback = ISO8601DateFormatter()
|
|
fallback.formatOptions = [.withInternetDateTime]
|
|
return fallback.date(from: dateString)
|
|
}
|
|
|
|
private static func minutesFromNow(_ date: Date?) -> Int? {
|
|
guard let date else { return nil }
|
|
let minutes = Int(date.timeIntervalSince(Date()) / 60)
|
|
return minutes > 0 ? minutes : nil
|
|
}
|
|
|
|
private static func clampedPercent(_ value: Double) -> Double {
|
|
min(100, max(0, value))
|
|
}
|
|
}
|
|
|
|
extension KimiUsageSnapshot {
|
|
public func toUsageSnapshot() -> UsageSnapshot {
|
|
// Parse weekly quota
|
|
let weeklyLimit = Int(weekly.limit) ?? 0
|
|
let weeklyRemaining = Int(weekly.remaining ?? "")
|
|
let weeklyUsed = Int(weekly.used ?? "") ?? {
|
|
guard let remaining = weeklyRemaining else { return 0 }
|
|
return max(0, weeklyLimit - remaining)
|
|
}()
|
|
|
|
let weeklyPercent = weeklyLimit > 0 ? Self.clampedPercent(Double(weeklyUsed) / Double(weeklyLimit) * 100) : 0
|
|
|
|
let weeklyWindow = RateWindow(
|
|
usedPercent: weeklyPercent,
|
|
windowMinutes: nil, // Weekly doesn't have a fixed window like rate limit
|
|
resetsAt: Self.parseDate(self.weekly.resetTime),
|
|
resetDescription: "\(weeklyUsed)/\(weeklyLimit) requests")
|
|
|
|
// Parse rate limit if available
|
|
var rateLimitWindow: RateWindow?
|
|
if let rateLimit = self.rateLimit {
|
|
let rateLimitValue = Int(rateLimit.limit) ?? 0
|
|
if rateLimitValue > 0 {
|
|
let rateRemaining = Int(rateLimit.remaining ?? "")
|
|
let rateUsed = Int(rateLimit.used ?? "") ?? {
|
|
guard let remaining = rateRemaining else { return 0 }
|
|
return max(0, rateLimitValue - remaining)
|
|
}()
|
|
let ratePercent = Self.clampedPercent(Double(rateUsed) / Double(rateLimitValue) * 100)
|
|
|
|
rateLimitWindow = RateWindow(
|
|
usedPercent: ratePercent,
|
|
windowMinutes: 300, // 300 minutes = 5 hours
|
|
resetsAt: Self.parseDate(rateLimit.resetTime),
|
|
resetDescription: "Rate: \(rateUsed)/\(rateLimitValue) per 5 hours")
|
|
}
|
|
}
|
|
|
|
let monthlyWindow = self.subscriptionBalance.flatMap { balance -> NamedRateWindow? in
|
|
// Monthly = shared subscription pool (`amountUsedRatio`), not the Code-only `kimiCodeUsedRatio`:
|
|
// the pool is shared across features, so amountUsedRatio is the real "subscription remaining".
|
|
guard balance.feature == nil || balance.feature == "FEATURE_OMNI" else { return nil }
|
|
guard balance.type == nil || balance.type == "SUBSCRIPTION" else { return nil }
|
|
guard let ratio = balance.amountUsedRatio, ratio.isFinite else { return nil }
|
|
let window = RateWindow(
|
|
usedPercent: Self.clampedPercent(ratio * 100),
|
|
windowMinutes: nil,
|
|
resetsAt: Self.parseDate(balance.expireTime),
|
|
resetDescription: nil)
|
|
return NamedRateWindow(id: "kimi-monthly", title: "Monthly", window: window)
|
|
}
|
|
|
|
let subscriptionCodeWeeklyWindow = self.subscriptionCodeWeeklyLimit.flatMap { limit -> NamedRateWindow? in
|
|
guard limit.enabled != false else { return nil }
|
|
guard let ratio = limit.ratio, ratio.isFinite else { return nil }
|
|
let window = RateWindow(
|
|
usedPercent: Self.clampedPercent(ratio * 100),
|
|
windowMinutes: 7 * 24 * 60,
|
|
resetsAt: Self.parseDate(limit.resetTime),
|
|
resetDescription: nil)
|
|
return NamedRateWindow(id: "kimi-code-7d", title: "Code 7-day", window: window)
|
|
}
|
|
|
|
let extraRateWindows = [monthlyWindow, subscriptionCodeWeeklyWindow].compactMap(\.self)
|
|
|
|
let identity = ProviderIdentitySnapshot(
|
|
providerID: .kimi,
|
|
accountEmail: nil,
|
|
accountOrganization: nil,
|
|
loginMethod: nil)
|
|
|
|
return UsageSnapshot(
|
|
primary: weeklyWindow,
|
|
secondary: rateLimitWindow,
|
|
tertiary: nil,
|
|
extraRateWindows: extraRateWindows.isEmpty ? nil : extraRateWindows,
|
|
providerCost: nil,
|
|
updatedAt: self.updatedAt,
|
|
identity: identity)
|
|
}
|
|
}
|