Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.27.0 — Unreleased

### Added
- Providers: route app-owned provider HTTP calls through a shared transport seam for cleaner proxy and test support (#892). Thanks @serezha93!
- Website: replace provider-letter tiles with brand logos, add light/dark landing-page themes, and collapse OpenCode/OpenCode Go into one company entry (#989). Thanks @pasangimhana!
- Claude: add an Anthropic Admin API source and allow `sk-ant-admin...` keys in Claude token accounts for API spend/token tracking (#966).
- Grok: add xAI Grok provider support with local identity detection and billing decoding for the Grok CLI integration (#965). Thanks @taibaran!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ enum AntigravityLoginRunner {
"grant_type": "authorization_code",
])

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw AntigravityLoginError.failed("Invalid token response.")
}
Expand All @@ -171,7 +171,7 @@ enum AntigravityLoginRunner {
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
return nil
}
Expand Down
17 changes: 13 additions & 4 deletions Sources/CodexBar/UsageStore+Status.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import CodexBarCore
import Foundation

extension UsageStore {
static func fetchStatus(from baseURL: URL) async throws -> ProviderStatus {
static func fetchStatus(
from baseURL: URL,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
async throws -> ProviderStatus
{
let apiURL = baseURL.appendingPathComponent("api/v2/status.json")
var request = URLRequest(url: apiURL)
request.timeoutInterval = 10

let (data, _) = try await URLSession.shared.data(for: request, delegate: nil)
let (data, _) = try await transport.data(for: request)

struct Response: Decodable {
struct Status: Decodable {
Expand Down Expand Up @@ -46,13 +51,17 @@ extension UsageStore {
updatedAt: response.page?.updatedAt)
}

static func fetchWorkspaceStatus(productID: String) async throws -> ProviderStatus {
static func fetchWorkspaceStatus(
productID: String,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
async throws -> ProviderStatus
{
guard let url = URL(string: "https://www.google.com/appsstatus/dashboard/incidents.json") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let (data, _) = try await URLSession.shared.data(for: request, delegate: nil)
let (data, _) = try await transport.data(for: request)
return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID)
}

Expand Down
24 changes: 24 additions & 0 deletions Sources/CodexBarCore/ProviderHTTPClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public protocol ProviderHTTPTransport: Sendable {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}

extension URLSession: ProviderHTTPTransport {}

public final class ProviderHTTPClient: ProviderHTTPTransport, @unchecked Sendable {
public static let shared = ProviderHTTPClient()

private let session: URLSession

public init(session: URLSession = .shared) {
self.session = session
}

public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
try await self.session.data(for: request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public enum AbacusUsageFetcher {
request.httpBody = Data("{}".utf8)
}

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw AbacusUsageError.networkError("Invalid response from \(url.lastPathComponent)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin")
request.setValue(region.dashboardURL.absoluteString, forHTTPHeaderField: "Referer")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw AlibabaCodingPlanUsageError.networkError("Invalid response")
}
Expand Down Expand Up @@ -158,7 +158,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin")
request.setValue(region.consoleRefererURL.absoluteString, forHTTPHeaderField: "Referer")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw AlibabaCodingPlanUsageError.networkError("Invalid response")
}
Expand Down Expand Up @@ -338,7 +338,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
forHTTPHeaderField: "Accept")

if let (data, response) = try? await URLSession.shared.data(for: request),
if let (data, response) = try? await ProviderHTTPClient.shared.data(for: request),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let html = String(data: data, encoding: .utf8),
Expand Down Expand Up @@ -404,7 +404,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
.absoluteString + "/"
request.setValue(referer, forHTTPHeaderField: "Referer")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public struct AntigravityRemoteUsageFetcher: Sendable {
homeDirectory: String = NSHomeDirectory(),
environment: [String: String] = ProcessInfo.processInfo.environment,
dataLoader: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse) = { request in
try await URLSession.shared.data(for: request)
try await ProviderHTTPClient.shared.data(for: request)
},
oauthClientResolver: @escaping @Sendable () -> AntigravityOAuthClient? = {
AntigravityOAuthConfig.resolvedClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public final class AugmentSessionKeepalive {
request.setValue("https://app.augmentcode.com", forHTTPHeaderField: "Referer")

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
self.log(" ✗ Invalid response type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ public struct AugmentStatusProbe: Sendable {
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw AugmentStatusProbeError.networkError("Invalid response")
Expand Down Expand Up @@ -535,7 +535,7 @@ public struct AugmentStatusProbe: Sendable {
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw AugmentStatusProbeError.networkError("Invalid response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ enum BedrockUsageFetcher {
region: ceRegion,
service: "ce")

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw BedrockUsageError.networkError("Invalid response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public enum ClaudeAdminAPIUsageFetcher {
apiKey: String,
costURL: URL = Self.costReportURL,
messagesURL: URL = Self.messagesUsageURL,
session: URLSession = .shared,
session: any ProviderHTTPTransport = ProviderHTTPClient.shared,
now: Date = Date()) async throws -> ClaudeAdminAPIUsageSnapshot
{
let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -75,7 +75,7 @@ public enum ClaudeAdminAPIUsageFetcher {
apiKey: String,
baseURL: URL,
range: DateRange,
session: URLSession) async throws -> CostReportResponse
session: any ProviderHTTPTransport) async throws -> CostReportResponse
{
let url = Self.url(
baseURL: baseURL,
Expand All @@ -91,7 +91,7 @@ public enum ClaudeAdminAPIUsageFetcher {
apiKey: String,
baseURL: URL,
range: DateRange,
session: URLSession) async throws -> MessagesUsageResponse
session: any ProviderHTTPTransport) async throws -> MessagesUsageResponse
{
let url = Self.url(
baseURL: baseURL,
Expand All @@ -107,7 +107,7 @@ public enum ClaudeAdminAPIUsageFetcher {
url: URL,
apiKey: String,
endpoint: String,
session: URLSession) async throws -> Data
session: any ProviderHTTPTransport) async throws -> Data
{
var request = URLRequest(url: url)
request.httpMethod = "GET"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ public enum ClaudeOAuthCredentialsStore {
]
request.httpBody = (components.percentEncodedQuery ?? "").data(using: .utf8)

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let http = response as? HTTPURLResponse else {
throw ClaudeOAuthCredentialsError.refreshFailed("Invalid response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ enum ClaudeOAuthUsageFetcher {
request.setValue(Self.claudeCodeUserAgent(), forHTTPHeaderField: "User-Agent")

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw ClaudeOAuthFetchError.invalidResponse
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public enum ClaudeWebAPIFetcher {
request.timeoutInterval = 20

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
let http = response as? HTTPURLResponse
let contentType = http?.allHeaderFields["Content-Type"] as? String
let truncated = data.prefix(Self.maxProbeBytes)
Expand Down Expand Up @@ -428,7 +428,7 @@ public enum ClaudeWebAPIFetcher {
request.httpMethod = "GET"
request.timeoutInterval = 15

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw FetchError.invalidResponse
Expand Down Expand Up @@ -458,7 +458,7 @@ public enum ClaudeWebAPIFetcher {
request.httpMethod = "GET"
request.timeoutInterval = 15

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw FetchError.invalidResponse
Expand Down Expand Up @@ -662,7 +662,7 @@ public enum ClaudeWebAPIFetcher {
request.timeoutInterval = 15

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { return nil }
logger?("Account API status: \(httpResponse.statusCode)")
guard httpResponse.statusCode == 200 else { return nil }
Expand Down Expand Up @@ -921,7 +921,7 @@ private enum ClaudeWebExtraUsageCost {
request.timeoutInterval = 15

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { return nil }
logger?("Overage API status: \(httpResponse.statusCode)")
guard httpResponse.statusCode == 200 else { return nil }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public enum CodebuffUsageFetcher {
apiKey: String,
environment: [String: String] = ProcessInfo.processInfo.environment,
includeSubscription: Bool = true,
session: URLSession = .shared) async throws -> CodebuffUsageSnapshot
session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> CodebuffUsageSnapshot
{
let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
Expand Down Expand Up @@ -52,7 +52,7 @@ public enum CodebuffUsageFetcher {
apiKey: String,
baseURL: URL,
includeSubscription: Bool,
session: URLSession) async throws -> (UsagePayload, SubscriptionPayload?)
session: any ProviderHTTPTransport) async throws -> (UsagePayload, SubscriptionPayload?)
{
try await withThrowingTaskGroup(of: FetchResult.self) { group in
group.addTask {
Expand Down Expand Up @@ -223,7 +223,7 @@ public enum CodebuffUsageFetcher {
private static func fetchUsagePayload(
apiKey: String,
baseURL: URL,
session: URLSession) async throws -> UsagePayload
session: any ProviderHTTPTransport) async throws -> UsagePayload
{
var request = URLRequest(url: self.usageURL(baseURL: baseURL))
request.httpMethod = "POST"
Expand All @@ -246,7 +246,7 @@ public enum CodebuffUsageFetcher {
private static func fetchSubscriptionPayload(
apiKey: String,
baseURL: URL,
session: URLSession) async throws -> SubscriptionPayload
session: any ProviderHTTPTransport) async throws -> SubscriptionPayload
{
var request = URLRequest(url: self.subscriptionURL(baseURL: baseURL))
request.httpMethod = "GET"
Expand All @@ -266,7 +266,7 @@ public enum CodebuffUsageFetcher {

private static func send(
request: URLRequest,
session: URLSession) async throws -> (Data, HTTPURLResponse)
session: any ProviderHTTPTransport) async throws -> (Data, HTTPURLResponse)
{
do {
let (data, response) = try await session.data(for: request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public enum CodexOAuthUsageFetcher {
}

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw CodexOAuthFetchError.invalidResponse
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public enum CodexTokenRefresher {
request.httpBody = try JSONSerialization.data(withJSONObject: body)

do {
let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await ProviderHTTPClient.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw RefreshError.invalidResponse("No HTTP response")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public enum CodexOpenAIWorkspaceResolver {

public static func resolve(
credentials: CodexOAuthCredentials,
session: URLSession = .shared) async throws -> CodexOpenAIWorkspaceIdentity?
session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> CodexOpenAIWorkspaceIdentity?
{
guard let workspaceAccountID = normalizeWorkspaceAccountID(credentials.accountId) else {
return nil
Expand All @@ -56,7 +56,7 @@ public enum CodexOpenAIWorkspaceResolver {

public static func listWorkspaces(
credentials: CodexOAuthCredentials,
session: URLSession = .shared) async throws -> [CodexOpenAIWorkspaceIdentity]
session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> [CodexOpenAIWorkspaceIdentity]
{
var request = URLRequest(url: self.accountsURL)
request.httpMethod = "GET"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum CommandCodeUsageFetcher {

public static func fetchUsage(
cookieHeader: String,
session: URLSession = .shared,
session: any ProviderHTTPTransport = ProviderHTTPClient.shared,
now: Date = Date()) async throws -> CommandCodeUsageSnapshot
{
async let creditsResult = self.fetchCredits(cookieHeader: cookieHeader, session: session)
Expand Down Expand Up @@ -66,7 +66,7 @@ public enum CommandCodeUsageFetcher {

private static func fetchCredits(
cookieHeader: String,
session: URLSession) async throws -> CreditsPayload
session: any ProviderHTTPTransport) async throws -> CreditsPayload
{
let url = self.apiBase.appendingPathComponent(self.creditsPath)
let data = try await self.send(url: url, cookieHeader: cookieHeader, session: session)
Expand All @@ -75,7 +75,7 @@ public enum CommandCodeUsageFetcher {

private static func fetchSubscription(
cookieHeader: String,
session: URLSession) async throws -> SubscriptionPayload?
session: any ProviderHTTPTransport) async throws -> SubscriptionPayload?
{
let url = self.apiBase.appendingPathComponent(self.subscriptionsPath)
let data = try await self.send(url: url, cookieHeader: cookieHeader, session: session)
Expand All @@ -85,7 +85,7 @@ public enum CommandCodeUsageFetcher {
private static func send(
url: URL,
cookieHeader: String,
session: URLSession) async throws -> Data
session: any ProviderHTTPTransport) async throws -> Data
{
var request = URLRequest(url: url)
request.httpMethod = "GET"
Expand Down
Loading