import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public struct CopilotUsageFetcher: Sendable { public struct GitHubUserIdentity: Decodable, Equatable, Sendable { public let id: Int64 public let login: String public init(id: Int64, login: String) { self.id = id self.login = login } } private let token: String private let enterpriseHost: String? private let transport: any ProviderHTTPTransport public init( token: String, enterpriseHost: String? = nil, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) { self.token = token self.enterpriseHost = enterpriseHost self.transport = transport } public static func apiHost(enterpriseHost: String?) -> String { let host = CopilotDeviceFlow.normalizedHost(enterpriseHost) if host == CopilotDeviceFlow.defaultHost { return "api.github.com" } if host.hasPrefix("api.") { return host } return "api.\(host)" } public static func usageURL(enterpriseHost: String?) -> URL? { CopilotDeviceFlow.makeRequestURL( host: self.apiHost(enterpriseHost: enterpriseHost), path: "/copilot_internal/user") } public func fetch() async throws -> UsageSnapshot { guard let url = Self.usageURL(enterpriseHost: self.enterpriseHost) else { throw URLError(.badURL) } var request = URLRequest(url: url) // Use the GitHub OAuth token directly, not the Copilot token. request.setValue("token \(self.token)", forHTTPHeaderField: "Authorization") self.addCommonHeaders(to: &request) let response = try await self.transport.response(for: request) if response.statusCode == 401 || response.statusCode == 403 { throw URLError(.userAuthenticationRequired) } guard response.statusCode == 200 else { throw URLError(.badServerResponse) } let usage = try JSONDecoder().decode(CopilotUsageResponse.self, from: response.data) let resetsAt = Self.parseQuotaResetDate(usage.quotaResetDate) let premium = Self.makeRateWindow(from: usage.quotaSnapshots.premiumInteractions, resetsAt: resetsAt) let chat = Self.makeRateWindow(from: usage.quotaSnapshots.chat, resetsAt: resetsAt) let primary: RateWindow? let secondary: RateWindow? if let premium { primary = premium secondary = chat } else if let chatWindow = chat { // Keep chat in the secondary slot so provider labels remain accurate // ("Premium" for primary, "Chat" for secondary) on chat-only plans. primary = nil secondary = chatWindow } else if usage.tokenBasedBilling { // Copilot Business token-based billing currently exposes zero-entitlement // placeholder quotas on this endpoint, so surface the plan without fake usage. primary = nil secondary = nil } else { throw URLError(.cannotDecodeRawData) } let identity = ProviderIdentitySnapshot( providerID: .copilot, accountEmail: nil, accountOrganization: nil, loginMethod: usage.copilotPlan.capitalized) return UsageSnapshot( primary: primary, secondary: secondary, tertiary: nil, providerCost: nil, updatedAt: Date(), identity: identity) } public static func fetchGitHubUsername(token: String) async throws -> String { try await self.fetchGitHubIdentity(token: token).login } public static func fetchGitHubIdentity( token: String, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> GitHubUserIdentity { guard let url = URL(string: "https://api.github.com/user") else { throw URLError(.badURL) } var request = URLRequest(url: url) request.setValue("token \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let response = try await transport.response(for: request) switch response.statusCode { case 200: return try JSONDecoder().decode(GitHubUserIdentity.self, from: response.data) case 401, 403: throw URLError(.userAuthenticationRequired) default: throw URLError(.badServerResponse) } } private func addCommonHeaders(to request: inout URLRequest) { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("vscode/1.96.2", forHTTPHeaderField: "Editor-Version") request.setValue("copilot-chat/0.26.7", forHTTPHeaderField: "Editor-Plugin-Version") request.setValue("GitHubCopilotChat/0.26.7", forHTTPHeaderField: "User-Agent") request.setValue("2025-04-01", forHTTPHeaderField: "X-Github-Api-Version") } static func makeRateWindow( from snapshot: CopilotUsageResponse.QuotaSnapshot?, resetsAt: Date? = nil) -> RateWindow? { guard let snapshot else { return nil } guard !snapshot.isPlaceholder else { return nil } guard snapshot.hasPercentRemaining else { return nil } let usedPercent = snapshot.usedPercent let overQuotaDescription = snapshot.overQuotaUsedPercent.map { used in String(format: "%.0f%% used", used) } let effectiveResetsAt = snapshot.unlimited ? nil : resetsAt return RateWindow( usedPercent: usedPercent, windowMinutes: nil, resetsAt: effectiveResetsAt, resetDescription: overQuotaDescription) } static func parseQuotaResetDate(_ value: String?) -> Date? { guard let raw = value?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil } let fractionalISO = ISO8601DateFormatter() fractionalISO.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = fractionalISO.date(from: raw) { return date } let internetISO = ISO8601DateFormatter() internetISO.formatOptions = [.withInternetDateTime] if let date = internetISO.date(from: raw) { return date } let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.isLenient = false formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: raw) }}
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct CopilotUsageFetcher: Sendable {
public struct GitHubUserIdentity: Decodable, Equatable, Sendable {
public let id: Int64
public let login: String
public init(id: Int64, login: String) {
self.id = id
self.login = login
}
}
private let token: String
private let enterpriseHost: String?
private let transport: any ProviderHTTPTransport
public init(
token: String,
enterpriseHost: String? = nil,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
{
self.token = token
self.enterpriseHost = enterpriseHost
self.transport = transport
}
public static func apiHost(enterpriseHost: String?) -> String {
let host = CopilotDeviceFlow.normalizedHost(enterpriseHost)
if host == CopilotDeviceFlow.defaultHost {
return "api.github.com"
}
if host.hasPrefix("api.") {
return host
}
return "api.\(host)"
}
public static func usageURL(enterpriseHost: String?) -> URL? {
CopilotDeviceFlow.makeRequestURL(
host: self.apiHost(enterpriseHost: enterpriseHost),
path: "/copilot_internal/user")
}
public func fetch() async throws -> UsageSnapshot {
guard let url = Self.usageURL(enterpriseHost: self.enterpriseHost) else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
// Use the GitHub OAuth token directly, not the Copilot token.
request.setValue("token \(self.token)", forHTTPHeaderField: "Authorization")
self.addCommonHeaders(to: &request)
let response = try await self.transport.response(for: request)
if response.statusCode == 401 || response.statusCode == 403 {
throw URLError(.userAuthenticationRequired)
}
guard response.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let usage = try JSONDecoder().decode(CopilotUsageResponse.self, from: response.data)
let resetsAt = Self.parseQuotaResetDate(usage.quotaResetDate)
let premium = Self.makeRateWindow(from: usage.quotaSnapshots.premiumInteractions, resetsAt: resetsAt)
let chat = Self.makeRateWindow(from: usage.quotaSnapshots.chat, resetsAt: resetsAt)
let primary: RateWindow?
let secondary: RateWindow?
if let premium {
primary = premium
secondary = chat
} else if let chatWindow = chat {
// Keep chat in the secondary slot so provider labels remain accurate
// ("Premium" for primary, "Chat" for secondary) on chat-only plans.
primary = nil
secondary = chatWindow
} else if usage.tokenBasedBilling {
// Copilot Business token-based billing currently exposes zero-entitlement
// placeholder quotas on this endpoint, so surface the plan without fake usage.
primary = nil
secondary = nil
} else {
throw URLError(.cannotDecodeRawData)
}
let identity = ProviderIdentitySnapshot(
providerID: .copilot,
accountEmail: nil,
accountOrganization: nil,
loginMethod: usage.copilotPlan.capitalized)
return UsageSnapshot(
primary: primary,
secondary: secondary,
tertiary: nil,
providerCost: nil,
updatedAt: Date(),
identity: identity)
}
public static func fetchGitHubUsername(token: String) async throws -> String {
try await self.fetchGitHubIdentity(token: token).login
}
public static func fetchGitHubIdentity(
token: String,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
async throws -> GitHubUserIdentity
{
guard let url = URL(string: "https://api.github.com/user") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.setValue("token \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let response = try await transport.response(for: request)
switch response.statusCode {
case 200:
return try JSONDecoder().decode(GitHubUserIdentity.self, from: response.data)
case 401, 403:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
}
private func addCommonHeaders(to request: inout URLRequest) {
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("vscode/1.96.2", forHTTPHeaderField: "Editor-Version")
request.setValue("copilot-chat/0.26.7", forHTTPHeaderField: "Editor-Plugin-Version")
request.setValue("GitHubCopilotChat/0.26.7", forHTTPHeaderField: "User-Agent")
request.setValue("2025-04-01", forHTTPHeaderField: "X-Github-Api-Version")
}
static func makeRateWindow(
from snapshot: CopilotUsageResponse.QuotaSnapshot?,
resetsAt: Date? = nil) -> RateWindow?
{
guard let snapshot else { return nil }
guard !snapshot.isPlaceholder else { return nil }
guard snapshot.hasPercentRemaining else { return nil }
let usedPercent = snapshot.usedPercent
let overQuotaDescription = snapshot.overQuotaUsedPercent.map { used in
String(format: "%.0f%% used", used)
}
let effectiveResetsAt = snapshot.unlimited ? nil : resetsAt
return RateWindow(
usedPercent: usedPercent,
windowMinutes: nil,
resetsAt: effectiveResetsAt,
resetDescription: overQuotaDescription)
}
static func parseQuotaResetDate(_ value: String?) -> Date? {
guard let raw = value?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return nil
}
let fractionalISO = ISO8601DateFormatter()
fractionalISO.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = fractionalISO.date(from: raw) {
return date
}
let internetISO = ISO8601DateFormatter()
internetISO.formatOptions = [.withInternetDateTime]
if let date = internetISO.date(from: raw) {
return date
}
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.isLenient = false
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: raw)
}
}