chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,554 @@
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
// MARK: - API response types
|
||||
|
||||
/// A flexible number type that can decode from both JSON integers and floats.
|
||||
/// The StepFun API returns `five_hour_usage_left_rate: 1` (int) or `0.99781543` (float).
|
||||
public struct StepFunFlexibleNumber: Decodable, Sendable {
|
||||
public let value: Double
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let intVal = try? container.decode(Int.self) {
|
||||
self.value = Double(intVal)
|
||||
} else if let doubleVal = try? container.decode(Double.self) {
|
||||
self.value = doubleVal
|
||||
} else {
|
||||
self.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ value: Double) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/// A flexible timestamp type that can decode from both JSON strings and integers.
|
||||
/// The StepFun API returns timestamps as strings like `"1777528800"`.
|
||||
public struct StepFunFlexibleTimestamp: Decodable, Sendable {
|
||||
public let value: Int64
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let strVal = try? container.decode(String.self), let parsed = Int64(strVal) {
|
||||
self.value = parsed
|
||||
} else if let intVal = try? container.decode(Int64.self) {
|
||||
self.value = intVal
|
||||
} else {
|
||||
self.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ value: Int64) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
public struct StepFunRateLimitResponse: Decodable, Sendable {
|
||||
public let status: Int?
|
||||
public let code: Int?
|
||||
public let message: String?
|
||||
public let desc: String?
|
||||
public let fiveHourUsageLeftRate: StepFunFlexibleNumber?
|
||||
public let weeklyUsageLeftRate: StepFunFlexibleNumber?
|
||||
public let fiveHourUsageResetTime: StepFunFlexibleTimestamp?
|
||||
public let weeklyUsageResetTime: StepFunFlexibleTimestamp?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case code
|
||||
case message
|
||||
case desc
|
||||
case fiveHourUsageLeftRate = "five_hour_usage_left_rate"
|
||||
case weeklyUsageLeftRate = "weekly_usage_left_rate"
|
||||
case fiveHourUsageResetTime = "five_hour_usage_reset_time"
|
||||
case weeklyUsageResetTime = "weekly_usage_reset_time"
|
||||
}
|
||||
|
||||
public var isSuccess: Bool {
|
||||
self.status == 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plan status response types
|
||||
|
||||
struct StepFunPlanStatusResponse: Decodable {
|
||||
let status: Int?
|
||||
let subscription: StepFunSubscription?
|
||||
|
||||
var planName: String? {
|
||||
self.subscription?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
||||
struct StepFunSubscription: Decodable {
|
||||
let name: String?
|
||||
let planType: Int?
|
||||
let planStatus: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case planType = "plan_type"
|
||||
case planStatus = "status"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Auth response types
|
||||
|
||||
struct StepFunRegisterDeviceResponse: Decodable {
|
||||
let accessToken: StepFunTokenPair?
|
||||
let refreshToken: StepFunTokenPair?
|
||||
}
|
||||
|
||||
struct StepFunLoginResponse: Decodable {
|
||||
let accessToken: StepFunTokenPair?
|
||||
let refreshToken: StepFunTokenPair?
|
||||
}
|
||||
|
||||
struct StepFunRefreshTokenResponse: Decodable {
|
||||
let accessToken: StepFunTokenPair?
|
||||
let refreshToken: StepFunTokenPair?
|
||||
}
|
||||
|
||||
struct StepFunTokenPair: Decodable {
|
||||
let raw: String
|
||||
}
|
||||
|
||||
// MARK: - Domain snapshot
|
||||
|
||||
public struct StepFunUsageSnapshot: Sendable {
|
||||
public let fiveHourUsageLeftRate: Double
|
||||
public let weeklyUsageLeftRate: Double
|
||||
public let fiveHourUsageResetTime: Date
|
||||
public let weeklyUsageResetTime: Date
|
||||
public let planName: String?
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
fiveHourUsageLeftRate: Double,
|
||||
weeklyUsageLeftRate: Double,
|
||||
fiveHourUsageResetTime: Date,
|
||||
weeklyUsageResetTime: Date,
|
||||
planName: String? = nil,
|
||||
updatedAt: Date)
|
||||
{
|
||||
self.fiveHourUsageLeftRate = fiveHourUsageLeftRate
|
||||
self.weeklyUsageLeftRate = weeklyUsageLeftRate
|
||||
self.fiveHourUsageResetTime = fiveHourUsageResetTime
|
||||
self.weeklyUsageResetTime = weeklyUsageResetTime
|
||||
self.planName = planName
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
public func toUsageSnapshot() -> UsageSnapshot {
|
||||
// Five-hour window: primary
|
||||
let fiveHourUsedPercent = max(0, min(100, (1.0 - self.fiveHourUsageLeftRate) * 100))
|
||||
let fiveHourResetDescription = UsageFormatter.resetDescription(from: self.fiveHourUsageResetTime)
|
||||
let fiveHourWindow = RateWindow(
|
||||
usedPercent: fiveHourUsedPercent,
|
||||
windowMinutes: 300,
|
||||
resetsAt: self.fiveHourUsageResetTime,
|
||||
resetDescription: fiveHourResetDescription)
|
||||
|
||||
// Weekly window: secondary
|
||||
let weeklyUsedPercent = max(0, min(100, (1.0 - self.weeklyUsageLeftRate) * 100))
|
||||
let weeklyResetDescription = UsageFormatter.resetDescription(from: self.weeklyUsageResetTime)
|
||||
let weeklyWindow = RateWindow(
|
||||
usedPercent: weeklyUsedPercent,
|
||||
windowMinutes: 10080,
|
||||
resetsAt: self.weeklyUsageResetTime,
|
||||
resetDescription: weeklyResetDescription)
|
||||
|
||||
let trimmedPlan = self.planName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let loginMethod = (trimmedPlan?.isEmpty ?? true) ? "password" : trimmedPlan
|
||||
|
||||
let identity = ProviderIdentitySnapshot(
|
||||
providerID: .stepfun,
|
||||
accountEmail: nil,
|
||||
accountOrganization: nil,
|
||||
loginMethod: loginMethod)
|
||||
|
||||
return UsageSnapshot(
|
||||
primary: fiveHourWindow,
|
||||
secondary: weeklyWindow,
|
||||
tertiary: nil,
|
||||
updatedAt: self.updatedAt,
|
||||
identity: identity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum StepFunUsageError: LocalizedError, Sendable {
|
||||
case missingCredentials
|
||||
case missingToken
|
||||
case networkError(String)
|
||||
case apiError(String)
|
||||
case parseFailed(String)
|
||||
case loginFailed(String)
|
||||
case tokenRefreshFailed(String)
|
||||
case deviceRegistrationFailed(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .missingCredentials:
|
||||
"Missing StepFun username or password. Set STEPFUN_USERNAME and STEPFUN_PASSWORD environment variables."
|
||||
case .missingToken:
|
||||
"Missing StepFun authentication token."
|
||||
case let .networkError(message):
|
||||
"StepFun network error: \(message)"
|
||||
case let .apiError(message):
|
||||
"StepFun API error: \(message)"
|
||||
case let .parseFailed(message):
|
||||
"Failed to parse StepFun response: \(message)"
|
||||
case let .loginFailed(message):
|
||||
"StepFun login failed: \(message)"
|
||||
case let .tokenRefreshFailed(message):
|
||||
"StepFun token refresh failed: \(message)"
|
||||
case let .deviceRegistrationFailed(message):
|
||||
"StepFun device registration failed: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetcher
|
||||
|
||||
public struct StepFunUsageFetcher: Sendable {
|
||||
private static let log = CodexBarLog.logger(LogCategories.stepfunUsage)
|
||||
private static let platformURL = URL(string: "https://platform.stepfun.com")!
|
||||
private static let apiURL =
|
||||
URL(string: "https://platform.stepfun.com/api/step.openapi.devcenter.Dashboard/QueryStepPlanRateLimit")!
|
||||
private static let planStatusURL =
|
||||
URL(string: "https://platform.stepfun.com/api/step.openapi.devcenter.Dashboard/GetStepPlanStatus")!
|
||||
private static let registerDeviceURL =
|
||||
URL(string: "https://platform.stepfun.com/passport/proto.api.passport.v1.PassportService/RegisterDevice")!
|
||||
private static let loginURL =
|
||||
URL(string: "https://platform.stepfun.com/passport/proto.api.passport.v1.PassportService/SignInByPassword")!
|
||||
private static let refreshTokenURL =
|
||||
URL(string: "https://platform.stepfun.com/passport/proto.api.passport.v1.PassportService/RefreshToken")!
|
||||
private static let timeoutSeconds: TimeInterval = 15
|
||||
|
||||
private static let webID = "c8a1002d2c457e758785a9979832217c7c0b884c"
|
||||
private static let appID = "10300"
|
||||
|
||||
private static let baseHeaders: [String: String] = [
|
||||
"content-type": "application/json",
|
||||
"oasis-appid": appID,
|
||||
"oasis-platform": "web",
|
||||
"oasis-webid": webID,
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
|
||||
]
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Perform the full login flow (username + password → Oasis-Token) and return the token.
|
||||
/// Does NOT fetch usage — the caller should cache the token and then call `fetchUsage(token:)`.
|
||||
public static func login(username: String, password: String) async throws -> String {
|
||||
try await self.fullLogin(username: username, password: password)
|
||||
}
|
||||
|
||||
/// Refresh an existing Oasis-Token and return a fresh access + refresh token pair.
|
||||
public static func refreshToken(token: String) async throws -> String {
|
||||
try await self.refreshOasisToken(token: token)
|
||||
}
|
||||
|
||||
/// Fetch usage data using an existing Oasis-Token (from env var or cached).
|
||||
public static func fetchUsage(token: String) async throws -> StepFunUsageSnapshot {
|
||||
guard !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw StepFunUsageError.missingToken
|
||||
}
|
||||
return try await self.queryUsage(token: token)
|
||||
}
|
||||
|
||||
/// Full login flow: username + password → token, then fetch usage.
|
||||
public static func fetchUsage(username: String, password: String) async throws -> StepFunUsageSnapshot {
|
||||
let token = try await self.fullLogin(username: username, password: password)
|
||||
return try await self.queryUsage(token: token)
|
||||
}
|
||||
|
||||
// MARK: - Login
|
||||
|
||||
private static func fullLogin(username: String, password: String) async throws -> String {
|
||||
// Step 1: Get INGRESSCOOKIE by visiting the platform homepage
|
||||
let (ingressCookie, _) = try await self.getIngressCookie()
|
||||
|
||||
// Step 2: RegisterDevice → get anonymous token
|
||||
let anonToken = try await self.registerDevice(ingressCookie: ingressCookie)
|
||||
|
||||
// Step 3: SignInByPassword → get authenticated token
|
||||
return try await self.signInByPassword(
|
||||
username: username,
|
||||
password: password,
|
||||
ingressCookie: ingressCookie,
|
||||
anonToken: anonToken)
|
||||
}
|
||||
|
||||
private static func getIngressCookie() async throws -> (String, HTTPURLResponse) {
|
||||
var request = URLRequest(url: self.platformURL)
|
||||
request.httpMethod = "GET"
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
let httpResponse = response.response
|
||||
|
||||
// Extract INGRESSCOOKIE from Set-Cookie headers
|
||||
let setCookieHeaders = httpResponse.allHeaderFields.filter { ($0.key as? String)?.lowercased() == "set-cookie" }
|
||||
var ingressCookie = ""
|
||||
for (_, value) in setCookieHeaders {
|
||||
let cookieString = "\(value)"
|
||||
if cookieString.contains("INGRESSCOOKIE=") {
|
||||
let parts = cookieString.components(separatedBy: "INGRESSCOOKIE=")
|
||||
if parts.count > 1 {
|
||||
let valuePart = parts[1].components(separatedBy: ";").first ?? ""
|
||||
ingressCookie = valuePart.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check cookies from the URLSession cookie store
|
||||
if ingressCookie.isEmpty {
|
||||
let cookies = HTTPCookieStorage.shared.cookies(for: self.platformURL) ?? []
|
||||
for cookie in cookies where cookie.name == "INGRESSCOOKIE" {
|
||||
ingressCookie = cookie.value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard !ingressCookie.isEmpty else {
|
||||
throw StepFunUsageError.loginFailed("Could not obtain INGRESSCOOKIE")
|
||||
}
|
||||
|
||||
return (ingressCookie, httpResponse)
|
||||
}
|
||||
|
||||
private static func registerDevice(ingressCookie: String) async throws -> String {
|
||||
var request = URLRequest(url: self.registerDeviceURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = Data("{}".utf8)
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.setValue("INGRESSCOOKIE=\(ingressCookie)", forHTTPHeaderField: "Cookie")
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
let data = response.data
|
||||
guard response.statusCode == 200 else {
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
Self.log.error("StepFun RegisterDevice returned \(response.statusCode): \(body)")
|
||||
throw StepFunUsageError.deviceRegistrationFailed("HTTP \(response.statusCode)")
|
||||
}
|
||||
|
||||
let decoded: StepFunRegisterDeviceResponse
|
||||
do {
|
||||
decoded = try JSONDecoder().decode(StepFunRegisterDeviceResponse.self, from: data)
|
||||
} catch {
|
||||
throw StepFunUsageError.parseFailed("RegisterDevice response: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
guard let accessToken = decoded.accessToken?.raw, !accessToken.isEmpty else {
|
||||
throw StepFunUsageError.deviceRegistrationFailed("No access token in RegisterDevice response")
|
||||
}
|
||||
|
||||
return self.combinedToken(accessToken: accessToken, refreshToken: decoded.refreshToken?.raw)
|
||||
}
|
||||
|
||||
private static func signInByPassword(
|
||||
username: String,
|
||||
password: String,
|
||||
ingressCookie: String,
|
||||
anonToken: String) async throws -> String
|
||||
{
|
||||
var request = URLRequest(url: self.loginURL)
|
||||
request.httpMethod = "POST"
|
||||
let body: [String: String] = ["username": username, "password": password]
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.setValue(
|
||||
"Oasis-Token=\(anonToken); Oasis-Webid=\(self.webID); INGRESSCOOKIE=\(ingressCookie)",
|
||||
forHTTPHeaderField: "Cookie")
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
let data = response.data
|
||||
guard response.statusCode == 200 else {
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
Self.log.error("StepFun SignInByPassword returned \(response.statusCode): \(body)")
|
||||
throw StepFunUsageError.loginFailed("HTTP \(response.statusCode)")
|
||||
}
|
||||
|
||||
let decoded: StepFunLoginResponse
|
||||
do {
|
||||
decoded = try JSONDecoder().decode(StepFunLoginResponse.self, from: data)
|
||||
} catch {
|
||||
throw StepFunUsageError.parseFailed("SignInByPassword response: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
guard let accessToken = decoded.accessToken?.raw, !accessToken.isEmpty else {
|
||||
throw StepFunUsageError.loginFailed("No access token in login response")
|
||||
}
|
||||
|
||||
return self.combinedToken(accessToken: accessToken, refreshToken: decoded.refreshToken?.raw)
|
||||
}
|
||||
|
||||
private static func refreshOasisToken(token: String) async throws -> String {
|
||||
let normalized = StepFunTokenNormalizer.normalize(token)
|
||||
guard !normalized.isEmpty else {
|
||||
throw StepFunUsageError.missingToken
|
||||
}
|
||||
|
||||
var request = URLRequest(url: self.refreshTokenURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = Data("{}".utf8)
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.setValue(normalized, forHTTPHeaderField: "Oasis-Token")
|
||||
request.setValue(
|
||||
"Oasis-Token=\(normalized); Oasis-Webid=\(self.webID)",
|
||||
forHTTPHeaderField: "Cookie")
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
let data = response.data
|
||||
guard response.statusCode == 200 else {
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
Self.log.error("StepFun RefreshToken returned \(response.statusCode): \(body)")
|
||||
throw StepFunUsageError.tokenRefreshFailed("HTTP \(response.statusCode)")
|
||||
}
|
||||
|
||||
let decoded: StepFunRefreshTokenResponse
|
||||
do {
|
||||
decoded = try JSONDecoder().decode(StepFunRefreshTokenResponse.self, from: data)
|
||||
} catch {
|
||||
throw StepFunUsageError.parseFailed("RefreshToken response: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
guard let accessToken = decoded.accessToken?.raw, !accessToken.isEmpty else {
|
||||
throw StepFunUsageError.tokenRefreshFailed("No access token in refresh response")
|
||||
}
|
||||
|
||||
return self.combinedToken(accessToken: accessToken, refreshToken: decoded.refreshToken?.raw)
|
||||
}
|
||||
|
||||
private static func combinedToken(accessToken: String, refreshToken: String?) -> String {
|
||||
guard let refreshToken, !refreshToken.isEmpty else {
|
||||
return accessToken
|
||||
}
|
||||
return "\(accessToken)...\(refreshToken)"
|
||||
}
|
||||
|
||||
// MARK: - Query usage
|
||||
|
||||
private static func queryUsage(token: String) async throws -> StepFunUsageSnapshot {
|
||||
var request = URLRequest(url: self.apiURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = Data("{}".utf8)
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.setValue("Oasis-Token=\(token); Oasis-Webid=\(self.webID)", forHTTPHeaderField: "Cookie")
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
let data = response.data
|
||||
guard response.statusCode == 200 else {
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
Self.log.error("StepFun API returned \(response.statusCode): \(body)")
|
||||
throw StepFunUsageError.apiError("HTTP \(response.statusCode)")
|
||||
}
|
||||
|
||||
if let jsonString = String(data: data, encoding: .utf8) {
|
||||
Self.log.debug("StepFun API response: \(jsonString)")
|
||||
}
|
||||
|
||||
var snapshot = try self.parseSnapshot(data: data)
|
||||
|
||||
// Fetch plan name in parallel is not needed — just do it sequentially.
|
||||
// If plan status fails, we still return usage data without plan name.
|
||||
if let planName = try? await self.queryPlanStatus(token: token) {
|
||||
snapshot = StepFunUsageSnapshot(
|
||||
fiveHourUsageLeftRate: snapshot.fiveHourUsageLeftRate,
|
||||
weeklyUsageLeftRate: snapshot.weeklyUsageLeftRate,
|
||||
fiveHourUsageResetTime: snapshot.fiveHourUsageResetTime,
|
||||
weeklyUsageResetTime: snapshot.weeklyUsageResetTime,
|
||||
planName: planName,
|
||||
updatedAt: snapshot.updatedAt)
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// MARK: - Plan Status
|
||||
|
||||
private static func queryPlanStatus(token: String) async throws -> String? {
|
||||
var request = URLRequest(url: self.planStatusURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = Data("{}".utf8)
|
||||
for (key, value) in self.baseHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.setValue("Oasis-Token=\(token); Oasis-Webid=\(self.webID)", forHTTPHeaderField: "Cookie")
|
||||
request.timeoutInterval = self.timeoutSeconds
|
||||
|
||||
let response = try await ProviderHTTPClient.shared.response(for: request)
|
||||
guard response.statusCode == 200 else {
|
||||
Self.log.debug("StepFun plan status request failed, skipping plan name")
|
||||
return nil
|
||||
}
|
||||
|
||||
let decoded: StepFunPlanStatusResponse
|
||||
do {
|
||||
decoded = try JSONDecoder().decode(StepFunPlanStatusResponse.self, from: response.data)
|
||||
} catch {
|
||||
Self.log.debug("StepFun plan status parse failed: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
|
||||
return decoded.planName
|
||||
}
|
||||
|
||||
public static func _parseSnapshotForTesting(_ data: Data) throws -> StepFunUsageSnapshot {
|
||||
try self.parseSnapshot(data: data)
|
||||
}
|
||||
|
||||
private static func parseSnapshot(data: Data) throws -> StepFunUsageSnapshot {
|
||||
let decoded: StepFunRateLimitResponse
|
||||
do {
|
||||
decoded = try JSONDecoder().decode(StepFunRateLimitResponse.self, from: data)
|
||||
} catch {
|
||||
throw StepFunUsageError.parseFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
guard decoded.isSuccess else {
|
||||
let msg = [decoded.message, decoded.desc]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.first { !$0.isEmpty } ?? decoded.code.map(String.init) ?? "unknown"
|
||||
throw StepFunUsageError.apiError(msg)
|
||||
}
|
||||
|
||||
guard let fiveHourRate = decoded.fiveHourUsageLeftRate,
|
||||
let weeklyRate = decoded.weeklyUsageLeftRate,
|
||||
let fiveHourReset = decoded.fiveHourUsageResetTime,
|
||||
let weeklyReset = decoded.weeklyUsageResetTime
|
||||
else {
|
||||
throw StepFunUsageError.parseFailed("Missing usage rate or reset time fields")
|
||||
}
|
||||
|
||||
return StepFunUsageSnapshot(
|
||||
fiveHourUsageLeftRate: fiveHourRate.value,
|
||||
weeklyUsageLeftRate: weeklyRate.value,
|
||||
fiveHourUsageResetTime: Date(timeIntervalSince1970: TimeInterval(fiveHourReset.value)),
|
||||
weeklyUsageResetTime: Date(timeIntervalSince1970: TimeInterval(weeklyReset.value)),
|
||||
updatedAt: Date())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user