136 lines
5.7 KiB
Swift
136 lines
5.7 KiB
Swift
import Foundation
|
|
|
|
public enum CopilotProviderDescriptor {
|
|
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
|
|
|
|
static func makeDescriptor() -> ProviderDescriptor {
|
|
ProviderDescriptor(
|
|
id: .copilot,
|
|
metadata: ProviderMetadata(
|
|
id: .copilot,
|
|
displayName: "Copilot",
|
|
sessionLabel: "Premium",
|
|
weeklyLabel: "Chat",
|
|
opusLabel: nil,
|
|
supportsOpus: false,
|
|
supportsCredits: false,
|
|
creditsHint: "",
|
|
toggleTitle: "Show Copilot usage",
|
|
cliName: "copilot",
|
|
defaultEnabled: false,
|
|
isPrimaryProvider: false,
|
|
usesAccountFallback: false,
|
|
browserCookieOrder: ProviderBrowserCookieDefaults.copilotCookieImportOrder,
|
|
dashboardURL: "https://github.com/settings/copilot",
|
|
statusPageURL: "https://www.githubstatus.com/"),
|
|
branding: ProviderBranding(
|
|
iconStyle: .copilot,
|
|
iconResourceName: "ProviderIcon-copilot",
|
|
color: ProviderColor(red: 168 / 255, green: 85 / 255, blue: 247 / 255)),
|
|
tokenCost: ProviderTokenCostConfig(
|
|
supportsTokenCost: false,
|
|
noDataMessage: { "Copilot cost summary is not supported." }),
|
|
fetchPlan: ProviderFetchPlan(
|
|
sourceModes: [.auto, .api],
|
|
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [CopilotAPIFetchStrategy()] })),
|
|
cli: ProviderCLIConfig(
|
|
name: "copilot",
|
|
versionDetector: nil))
|
|
}
|
|
}
|
|
|
|
struct CopilotAPIFetchStrategy: ProviderFetchStrategy {
|
|
let id: String = "copilot.api"
|
|
let kind: ProviderFetchKind = .apiToken
|
|
|
|
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
|
|
Self.resolveToken(context: context) != nil
|
|
}
|
|
|
|
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
|
|
guard let token = Self.resolveToken(context: context), !token.isEmpty else {
|
|
throw URLError(.userAuthenticationRequired)
|
|
}
|
|
let fetcher = CopilotUsageFetcher(
|
|
token: token,
|
|
enterpriseHost: context.settings?.copilot?.enterpriseHost)
|
|
let usage = try await fetcher.fetch()
|
|
let snap = await self.addBudgetWindowsIfNeeded(to: usage, token: token, context: context)
|
|
return self.makeResult(
|
|
usage: snap,
|
|
sourceLabel: "api")
|
|
}
|
|
|
|
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
|
|
false
|
|
}
|
|
|
|
private static func resolveToken(context: ProviderFetchContext) -> String? {
|
|
ProviderTokenResolver.copilotToken(environment: context.env)
|
|
?? ProviderTokenResolver.copilotResolution(environment: [
|
|
"COPILOT_API_TOKEN": context.settings?.copilot?.apiToken ?? "",
|
|
])?.token
|
|
}
|
|
|
|
private func addBudgetWindowsIfNeeded(
|
|
to usage: UsageSnapshot,
|
|
token: String,
|
|
context: ProviderFetchContext) async -> UsageSnapshot
|
|
{
|
|
guard let settings = context.settings?.copilot,
|
|
settings.budgetExtrasEnabled,
|
|
settings.budgetCookieSource != .off
|
|
else { return usage }
|
|
|
|
let manualCookieHeader = Self.budgetCookieHeaderOverride(from: settings)
|
|
if settings.budgetCookieSource == .manual, manualCookieHeader == nil {
|
|
return usage
|
|
}
|
|
do {
|
|
let expectedAccountIdentifier = try await self.expectedBudgetAccountIdentifier(
|
|
token: token,
|
|
settings: settings)
|
|
let extraRateWindows = try await CopilotBudgetWebFetcher(
|
|
cookieHeaderOverride: manualCookieHeader,
|
|
expectedGitHubAccountIdentifier: expectedAccountIdentifier,
|
|
browserDetection: context.browserDetection)
|
|
.fetchBudgetWindows()
|
|
guard !extraRateWindows.isEmpty else { return usage }
|
|
return usage.with(extraRateWindows: extraRateWindows)
|
|
} catch {
|
|
CodexBarLog.logger(LogCategories.providers).warning(
|
|
"Copilot budget extras unavailable",
|
|
metadata: ["error": "\(error.localizedDescription)"])
|
|
return usage
|
|
}
|
|
}
|
|
|
|
static func budgetCookieHeaderOverride(
|
|
from settings: ProviderSettingsSnapshot.CopilotProviderSettings) -> String?
|
|
{
|
|
guard settings.budgetCookieSource == .manual else { return nil }
|
|
return CookieHeaderNormalizer.normalize(settings.manualBudgetCookieHeader)
|
|
}
|
|
|
|
private func expectedBudgetAccountIdentifier(
|
|
token: String,
|
|
settings: ProviderSettingsSnapshot.CopilotProviderSettings) async throws -> String
|
|
{
|
|
let identity = try await CopilotUsageFetcher.fetchGitHubIdentity(token: token)
|
|
let tokenIdentifier = CopilotBudgetWebFetcher.normalizedGitHubAccountIdentifier(for: identity)
|
|
if let selectedIdentifier = Self.normalizedBudgetAccountIdentifier(settings.selectedAccountExternalIdentifier),
|
|
selectedIdentifier != tokenIdentifier.lowercased(),
|
|
selectedIdentifier != identity.login.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
{
|
|
CodexBarLog.logger(LogCategories.providers).warning(
|
|
"Ignoring stale Copilot account identifier")
|
|
}
|
|
return tokenIdentifier
|
|
}
|
|
|
|
private static func normalizedBudgetAccountIdentifier(_ identifier: String?) -> String? {
|
|
let trimmed = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
return trimmed.isEmpty ? nil : trimmed.lowercased()
|
|
}
|
|
}
|