Files
2026-07-13 12:22:33 +08:00

72 lines
3.3 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct PerplexityUsageFetcher: Sendable {
private static let log = CodexBarLog.logger(LogCategories.perplexityAPI)
private static let creditsURL =
URL(string: "https://www.perplexity.ai/rest/billing/credits?version=2.18&source=default")!
@TaskLocal static var fetchCreditsOverride:
(@Sendable (String, String, Date) async throws -> PerplexityUsageSnapshot)?
/// Testing hook: parse a raw JSON response without making network calls.
public static func _parseResponseForTesting(_ data: Data, now: Date = Date()) throws -> PerplexityUsageSnapshot {
do {
let decoded = try JSONDecoder().decode(PerplexityCreditsResponse.self, from: data)
return PerplexityUsageSnapshot(response: decoded, now: now)
} catch {
throw PerplexityAPIError.parseFailed(error.localizedDescription)
}
}
public static func fetchCredits(
sessionToken: String,
cookieName: String = PerplexityCookieHeader.defaultSessionCookieName,
now: Date = Date()) async throws -> PerplexityUsageSnapshot
{
if let override = self.fetchCreditsOverride {
return try await override(sessionToken, cookieName, now)
}
var request = URLRequest(url: self.creditsURL)
request.httpMethod = "GET"
request.timeoutInterval = 15
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(
"\(cookieName)=\(sessionToken)",
forHTTPHeaderField: "Cookie")
request.setValue("https://www.perplexity.ai", forHTTPHeaderField: "Origin")
request.setValue("https://www.perplexity.ai/account/usage", forHTTPHeaderField: "Referer")
let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
let response = try await ProviderHTTPClient.shared.response(for: request)
let data = response.data
guard response.statusCode == 200 else {
let statusCode = response.statusCode
let body = String(data: data, encoding: .utf8) ?? "<binary>"
let truncated = body.count > 200 ? String(body.prefix(200)) + "…" : body
Self.log.error("Perplexity API returned \(statusCode): \(truncated)")
if statusCode == 401 || statusCode == 403 {
throw PerplexityAPIError.invalidToken
}
throw PerplexityAPIError.apiError("HTTP \(statusCode)")
}
do {
let decoded = try JSONDecoder().decode(PerplexityCreditsResponse.self, from: data)
let snapshot = PerplexityUsageSnapshot(response: decoded, now: now)
Self.log.debug(
"Perplexity credits parsed balance=\(snapshot.balanceCents) totalUsage=\(snapshot.totalUsageCents)")
return snapshot
} catch {
let preview = String(data: data.prefix(500), encoding: .utf8) ?? "<binary>"
Self.log.error("Perplexity parse failed: \(error) — response: \(preview)")
throw PerplexityAPIError.parseFailed(error.localizedDescription)
}
}
}