From a1467f533568c7ddf3f0c08e44a7f81952312c94 Mon Sep 17 00:00:00 2001 From: serezha93 Date: Sun, 10 May 2026 22:48:05 +0300 Subject: [PATCH 1/2] feat(http): add shared provider client seam --- Sources/CodexBar/UsageStore+Status.swift | 5 +- Sources/CodexBarCore/ProviderHTTPClient.swift | 18 ++++ .../Copilot/CopilotUsageFetcher.swift | 4 +- .../Factory/FactoryStatusProbe.swift | 10 +-- .../CopilotUsageFetcherTests.swift | 70 ++++++++++++++++ .../GoogleWorkspaceStatusNetworkTests.swift | 84 +++++++++++++++++++ .../ProviderHTTPClientTests.swift | 68 +++++++++++++++ 7 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 Sources/CodexBarCore/ProviderHTTPClient.swift create mode 100644 Tests/CodexBarTests/CopilotUsageFetcherTests.swift create mode 100644 Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift create mode 100644 Tests/CodexBarTests/ProviderHTTPClientTests.swift diff --git a/Sources/CodexBar/UsageStore+Status.swift b/Sources/CodexBar/UsageStore+Status.swift index abf7aee47..2c692ea78 100644 --- a/Sources/CodexBar/UsageStore+Status.swift +++ b/Sources/CodexBar/UsageStore+Status.swift @@ -1,3 +1,4 @@ +import CodexBarCore import Foundation extension UsageStore { @@ -6,7 +7,7 @@ extension UsageStore { var request = URLRequest(url: apiURL) request.timeoutInterval = 10 - let (data, _) = try await URLSession.shared.data(for: request, delegate: nil) + let (data, _) = try await ProviderHTTPClient.shared.data(for: request) struct Response: Decodable { struct Status: Decodable { @@ -52,7 +53,7 @@ extension UsageStore { } var request = URLRequest(url: url) request.timeoutInterval = 10 - let (data, _) = try await URLSession.shared.data(for: request, delegate: nil) + let (data, _) = try await ProviderHTTPClient.shared.data(for: request) return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID) } diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift new file mode 100644 index 000000000..e0f24cd7e --- /dev/null +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -0,0 +1,18 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public final class ProviderHTTPClient: @unchecked Sendable { + public static let shared = ProviderHTTPClient() + + private let session: URLSession + + public init(session: URLSession? = nil) { + self.session = session ?? URLSession.shared + } + + public func data(for request: URLRequest) async throws -> (Data, URLResponse) { + try await self.session.data(for: request) + } +} diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift index 01e1666f0..aab4a86fc 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift @@ -49,7 +49,7 @@ public struct CopilotUsageFetcher: Sendable { request.setValue("token \(self.token)", forHTTPHeaderField: "Authorization") self.addCommonHeaders(to: &request) - 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 URLError(.badServerResponse) @@ -107,7 +107,7 @@ public struct CopilotUsageFetcher: Sendable { request.setValue("token \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - 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 URLError(.badServerResponse) } diff --git a/Sources/CodexBarCore/Providers/Factory/FactoryStatusProbe.swift b/Sources/CodexBarCore/Providers/Factory/FactoryStatusProbe.swift index c4b3fe338..e3b1fb230 100644 --- a/Sources/CodexBarCore/Providers/Factory/FactoryStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Factory/FactoryStatusProbe.swift @@ -1266,7 +1266,7 @@ public struct FactoryStatusProbe: Sendable { let data: Data let response: URLResponse do { - (data, response) = try await URLSession.shared.data(for: request) + (data, response) = try await ProviderHTTPClient.shared.data(for: request) } catch { return nil } @@ -1303,7 +1303,7 @@ public struct FactoryStatusProbe: Sendable { request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization") } - 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 FactoryStatusProbeError.networkError("Invalid response") @@ -1368,7 +1368,7 @@ public struct FactoryStatusProbe: Sendable { request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization") } - 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 FactoryStatusProbeError.networkError("Invalid response") @@ -1502,7 +1502,7 @@ public struct FactoryStatusProbe: Sendable { } request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) - 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 FactoryStatusProbeError.networkError("Invalid WorkOS response") } @@ -1572,7 +1572,7 @@ public struct FactoryStatusProbe: Sendable { } request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) - 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 FactoryStatusProbeError.networkError("Invalid WorkOS response") } diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift new file mode 100644 index 000000000..316f6f985 --- /dev/null +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -0,0 +1,70 @@ +import Foundation +import Testing +@testable import CodexBarCore + +@Suite(.serialized) +struct CopilotUsageFetcherTests { + @Test + func `fetchGitHubIdentity uses shared client`() async throws { + let registered = URLProtocol.registerClass(CopilotHTTPStubURLProtocol.self) + defer { + if registered { + URLProtocol.unregisterClass(CopilotHTTPStubURLProtocol.self) + } + CopilotHTTPStubURLProtocol.handler = nil + CopilotHTTPStubURLProtocol.requests = [] + } + + CopilotHTTPStubURLProtocol.requests = [] + CopilotHTTPStubURLProtocol.handler = { request in + CopilotHTTPStubURLProtocol.requests.append(request) + guard request.value(forHTTPHeaderField: "Authorization") == "token abc123" else { + throw URLError(.userAuthenticationRequired) + } + let response = HTTPURLResponse( + url: try #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + return (Data(#"{"login":"testuser","id":123}"#.utf8), response) + } + + let identity = try await CopilotUsageFetcher.fetchGitHubIdentity(token: "abc123") + + #expect(identity.login == "testuser") + #expect(identity.id == 123) + #expect(CopilotHTTPStubURLProtocol.requests.count == 1) + #expect(CopilotHTTPStubURLProtocol.requests.first?.url?.host == "api.github.com") + } +} + +final class CopilotHTTPStubURLProtocol: URLProtocol { + nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? + nonisolated(unsafe) static var requests: [URLRequest] = [] + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard let handler = Self.handler else { + self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork)) + return + } + + do { + let (data, response) = try handler(self.request) + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift new file mode 100644 index 000000000..d6bcb42cf --- /dev/null +++ b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift @@ -0,0 +1,84 @@ +import Foundation +import Testing +@testable import CodexBar + +@Suite(.serialized) +@MainActor +struct GoogleWorkspaceStatusNetworkTests { + @Test + func `fetchWorkspaceStatus uses shared client`() async throws { + let registered = URLProtocol.registerClass(WorkspaceStatusStubURLProtocol.self) + defer { + if registered { + URLProtocol.unregisterClass(WorkspaceStatusStubURLProtocol.self) + } + WorkspaceStatusStubURLProtocol.handler = nil + WorkspaceStatusStubURLProtocol.requests = [] + } + + WorkspaceStatusStubURLProtocol.requests = [] + WorkspaceStatusStubURLProtocol.handler = { request in + WorkspaceStatusStubURLProtocol.requests.append(request) + let response = HTTPURLResponse( + url: try #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + let body = Data(#""" + [ + { + "begin": "2026-05-10T10:00:00+00:00", + "end": null, + "affected_products": [ + {"title": "Gemini", "id": "npdyhgECDJ6tB66MxXyo"} + ], + "most_recent_update": { + "when": "2026-05-10T10:15:00+00:00", + "status": "SERVICE_OUTAGE", + "text": "**Summary**\nGemini API error.\n" + } + } + ] + """#.utf8) + return (body, response) + } + + let status = try await UsageStore.fetchWorkspaceStatus(productID: "npdyhgECDJ6tB66MxXyo") + + #expect(status.indicator == .critical) + #expect(status.description == "Gemini API error.") + #expect(WorkspaceStatusStubURLProtocol.requests.count == 1) + #expect(WorkspaceStatusStubURLProtocol.requests.first?.url?.host == "www.google.com") + } +} + +final class WorkspaceStatusStubURLProtocol: URLProtocol { + nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? + nonisolated(unsafe) static var requests: [URLRequest] = [] + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard let handler = Self.handler else { + self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork)) + return + } + + do { + let (data, response) = try handler(self.request) + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/CodexBarTests/ProviderHTTPClientTests.swift b/Tests/CodexBarTests/ProviderHTTPClientTests.swift new file mode 100644 index 000000000..cb6aad354 --- /dev/null +++ b/Tests/CodexBarTests/ProviderHTTPClientTests.swift @@ -0,0 +1,68 @@ +import Foundation +import Testing +@testable import CodexBarCore + +@Suite(.serialized) +struct ProviderHTTPClientTests { + @Test + func `client loads requests through an injected session`() async throws { + StubURLProtocol.requests = [] + StubURLProtocol.handler = { request in + StubURLProtocol.requests.append(request) + let response = HTTPURLResponse( + url: try #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + return (Data(#"{"ok":true}"#.utf8), response) + } + defer { + StubURLProtocol.handler = nil + StubURLProtocol.requests = [] + } + + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [StubURLProtocol.self] + let client = ProviderHTTPClient(session: URLSession(configuration: configuration)) + let request = URLRequest(url: URL(string: "https://example.com/status")!) + + let (data, response) = try await client.data(for: request) + + let body = try #require(String(data: data, encoding: .utf8)) + #expect(body == #"{"ok":true}"#) + #expect((response as? HTTPURLResponse)?.statusCode == 200) + #expect(StubURLProtocol.requests.count == 1) + #expect(StubURLProtocol.requests.first?.url?.host == "example.com") + } +} + +final class StubURLProtocol: URLProtocol { + nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? + nonisolated(unsafe) static var requests: [URLRequest] = [] + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard let handler = Self.handler else { + self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork)) + return + } + + do { + let (data, response) = try handler(self.request) + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} From 2aecbfe3dfb6542e179338e0932bd29a9564515b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 17 May 2026 02:04:50 +0100 Subject: [PATCH 2/2] refactor: broaden provider HTTP transport seam --- CHANGELOG.md | 1 + .../Antigravity/AntigravityLoginRunner.swift | 4 +- Sources/CodexBar/UsageStore+Status.swift | 16 +++-- Sources/CodexBarCore/ProviderHTTPClient.swift | 12 +++- .../Providers/Abacus/AbacusUsageFetcher.swift | 2 +- .../AlibabaCodingPlanUsageFetcher.swift | 8 +-- .../AntigravityRemoteUsageFetcher.swift | 2 +- .../Augment/AugmentSessionKeepalive.swift | 2 +- .../Augment/AugmentStatusProbe.swift | 4 +- .../Providers/Bedrock/BedrockUsageStats.swift | 2 +- .../Claude/ClaudeAdminAPIUsageFetcher.swift | 8 +-- .../ClaudeOAuth/ClaudeOAuthCredentials.swift | 2 +- .../ClaudeOAuth/ClaudeOAuthUsageFetcher.swift | 2 +- .../ClaudeWeb/ClaudeWebAPIFetcher.swift | 10 ++-- .../Codebuff/CodebuffUsageFetcher.swift | 10 ++-- .../CodexOAuth/CodexOAuthUsageFetcher.swift | 2 +- .../CodexOAuth/CodexTokenRefresher.swift | 2 +- .../Codex/CodexOpenAIWorkspaceResolver.swift | 4 +- .../CommandCode/CommandCodeUsageFetcher.swift | 8 +-- .../Providers/Copilot/CopilotDeviceFlow.swift | 4 +- .../Copilot/CopilotUsageFetcher.swift | 8 ++- .../Providers/Crof/CrofUsageFetcher.swift | 2 +- .../Providers/Cursor/CursorStatusProbe.swift | 6 +- .../DeepSeek/DeepSeekUsageFetcher.swift | 2 +- .../Providers/Doubao/DoubaoUsageFetcher.swift | 2 +- .../ElevenLabs/ElevenLabsUsageFetcher.swift | 2 +- .../Gemini/GeminiStatusProbe+DataLoader.swift | 2 +- .../Grok/GrokWebBillingFetcher.swift | 8 +-- .../Providers/Kilo/KiloUsageFetcher.swift | 6 +- .../Providers/Kimi/KimiUsageFetcher.swift | 2 +- .../Providers/KimiK2/KimiK2UsageFetcher.swift | 2 +- .../Providers/Manus/ManusUsageFetcher.swift | 2 +- .../Providers/MiMo/MiMoUsageFetcher.swift | 2 +- .../MiniMax/MiniMaxUsageFetcher.swift | 8 +-- .../Mistral/MistralUsageFetcher.swift | 2 +- .../Moonshot/MoonshotUsageFetcher.swift | 2 +- .../OpenAIAPICreditBalanceFetcher.swift | 2 +- .../OpenAI/OpenAIAPIUsageFetcher.swift | 8 +-- .../OpenCode/OpenCodeUsageFetcher.swift | 8 +-- .../OpenRouter/OpenRouterUsageStats.swift | 4 +- .../Perplexity/PerplexityUsageFetcher.swift | 2 +- .../StepFun/StepFunUsageFetcher.swift | 10 ++-- .../Synthetic/SyntheticUsageStats.swift | 2 +- .../Providers/Venice/VeniceUsageFetcher.swift | 2 +- .../VertexAITokenRefresher.swift | 2 +- .../VertexAIOAuth/VertexAIUsageFetcher.swift | 2 +- .../Providers/Warp/WarpUsageFetcher.swift | 2 +- .../Windsurf/WindsurfWebFetcher.swift | 6 +- .../Providers/Zai/ZaiUsageStats.swift | 4 +- .../CopilotUsageFetcherTests.swift | 56 +++--------------- .../GoogleWorkspaceStatusNetworkTests.swift | 58 +++---------------- .../ProviderHTTPClientTests.swift | 10 ++-- .../ProviderHTTPTransportStub.swift | 20 +++++++ 53 files changed, 159 insertions(+), 202 deletions(-) create mode 100644 Tests/CodexBarTests/ProviderHTTPTransportStub.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5e3d242..52aab86c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! diff --git a/Sources/CodexBar/Providers/Antigravity/AntigravityLoginRunner.swift b/Sources/CodexBar/Providers/Antigravity/AntigravityLoginRunner.swift index 7578b7d7f..0ac446c87 100644 --- a/Sources/CodexBar/Providers/Antigravity/AntigravityLoginRunner.swift +++ b/Sources/CodexBar/Providers/Antigravity/AntigravityLoginRunner.swift @@ -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.") } @@ -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 } diff --git a/Sources/CodexBar/UsageStore+Status.swift b/Sources/CodexBar/UsageStore+Status.swift index 2c692ea78..4d1b85e4d 100644 --- a/Sources/CodexBar/UsageStore+Status.swift +++ b/Sources/CodexBar/UsageStore+Status.swift @@ -2,12 +2,16 @@ 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 ProviderHTTPClient.shared.data(for: request) + let (data, _) = try await transport.data(for: request) struct Response: Decodable { struct Status: Decodable { @@ -47,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 ProviderHTTPClient.shared.data(for: request) + let (data, _) = try await transport.data(for: request) return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID) } diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift index e0f24cd7e..ba6300fe5 100644 --- a/Sources/CodexBarCore/ProviderHTTPClient.swift +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -3,13 +3,19 @@ import Foundation import FoundationNetworking #endif -public final class ProviderHTTPClient: @unchecked Sendable { +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? = nil) { - self.session = session ?? URLSession.shared + public init(session: URLSession = .shared) { + self.session = session } public func data(for request: URLRequest) async throws -> (Data, URLResponse) { diff --git a/Sources/CodexBarCore/Providers/Abacus/AbacusUsageFetcher.swift b/Sources/CodexBarCore/Providers/Abacus/AbacusUsageFetcher.swift index 1042d9f60..3246a3e63 100644 --- a/Sources/CodexBarCore/Providers/Abacus/AbacusUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Abacus/AbacusUsageFetcher.swift @@ -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)") diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift index f10b0c2c0..54c3c8569 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift @@ -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") } @@ -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") } @@ -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), @@ -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 } diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityRemoteUsageFetcher.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityRemoteUsageFetcher.swift index f10808d5f..7c0774215 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityRemoteUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityRemoteUsageFetcher.swift @@ -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() diff --git a/Sources/CodexBarCore/Providers/Augment/AugmentSessionKeepalive.swift b/Sources/CodexBarCore/Providers/Augment/AugmentSessionKeepalive.swift index 002b3bcd7..40e1974a9 100644 --- a/Sources/CodexBarCore/Providers/Augment/AugmentSessionKeepalive.swift +++ b/Sources/CodexBarCore/Providers/Augment/AugmentSessionKeepalive.swift @@ -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") diff --git a/Sources/CodexBarCore/Providers/Augment/AugmentStatusProbe.swift b/Sources/CodexBarCore/Providers/Augment/AugmentStatusProbe.swift index 7c8e6bc7c..cff8957a4 100644 --- a/Sources/CodexBarCore/Providers/Augment/AugmentStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Augment/AugmentStatusProbe.swift @@ -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") @@ -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") diff --git a/Sources/CodexBarCore/Providers/Bedrock/BedrockUsageStats.swift b/Sources/CodexBarCore/Providers/Bedrock/BedrockUsageStats.swift index 494270b95..8f8a9313d 100644 --- a/Sources/CodexBarCore/Providers/Bedrock/BedrockUsageStats.swift +++ b/Sources/CodexBarCore/Providers/Bedrock/BedrockUsageStats.swift @@ -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") diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeAdminAPIUsageFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeAdminAPIUsageFetcher.swift index b41787708..dcb6da2ed 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeAdminAPIUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeAdminAPIUsageFetcher.swift @@ -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) @@ -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, @@ -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, @@ -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" diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift index 80f9039ef..4b96c1535 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift @@ -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") diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift index 33e8677e7..cdeed7020 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift @@ -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 } diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift index f270d9615..d88505db4 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift @@ -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) @@ -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 @@ -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 @@ -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 } @@ -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 } diff --git a/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift b/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift index 2041fa6ab..be481f8f2 100644 --- a/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift @@ -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 { @@ -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 { @@ -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" @@ -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" @@ -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) diff --git a/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift b/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift index 7cca42ff8..74102052a 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift @@ -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 } diff --git a/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift b/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift index 09789cadc..d76ba8a30 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift @@ -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") } diff --git a/Sources/CodexBarCore/Providers/Codex/CodexOpenAIWorkspaceResolver.swift b/Sources/CodexBarCore/Providers/Codex/CodexOpenAIWorkspaceResolver.swift index fabf412a3..d6a0892a5 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexOpenAIWorkspaceResolver.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexOpenAIWorkspaceResolver.swift @@ -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 @@ -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" diff --git a/Sources/CodexBarCore/Providers/CommandCode/CommandCodeUsageFetcher.swift b/Sources/CodexBarCore/Providers/CommandCode/CommandCodeUsageFetcher.swift index 6b49a62bd..9af940071 100644 --- a/Sources/CodexBarCore/Providers/CommandCode/CommandCodeUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/CommandCode/CommandCodeUsageFetcher.swift @@ -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) @@ -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) @@ -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) @@ -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" diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotDeviceFlow.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotDeviceFlow.swift index a407202c5..18ebe4716 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotDeviceFlow.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotDeviceFlow.swift @@ -98,7 +98,7 @@ public struct CopilotDeviceFlow: Sendable { ] postRequest.httpBody = Self.formURLEncodedBody(body) - let (data, response) = try await URLSession.shared.data(for: postRequest) + let (data, response) = try await ProviderHTTPClient.shared.data(for: postRequest) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) @@ -127,7 +127,7 @@ public struct CopilotDeviceFlow: Sendable { try await Task.sleep(nanoseconds: UInt64(interval) * 1_000_000_000) try Task.checkCancellation() - let (data, _) = try await URLSession.shared.data(for: request) + let (data, _) = try await ProviderHTTPClient.shared.data(for: request) // Check for error in JSON if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift index aab4a86fc..fec73979a 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift @@ -99,7 +99,11 @@ public struct CopilotUsageFetcher: Sendable { try await self.fetchGitHubIdentity(token: token).login } - public static func fetchGitHubIdentity(token: String) async throws -> GitHubUserIdentity { + 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) } @@ -107,7 +111,7 @@ public struct CopilotUsageFetcher: Sendable { request.setValue("token \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - let (data, response) = try await ProviderHTTPClient.shared.data(for: request) + let (data, response) = try await transport.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } diff --git a/Sources/CodexBarCore/Providers/Crof/CrofUsageFetcher.swift b/Sources/CodexBarCore/Providers/Crof/CrofUsageFetcher.swift index a917f17ed..0a76f1835 100644 --- a/Sources/CodexBarCore/Providers/Crof/CrofUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Crof/CrofUsageFetcher.swift @@ -41,7 +41,7 @@ public enum CrofUsageFetcher { public static func fetchUsage( apiKey: String, - session: URLSession = .shared) async throws -> CrofUsageSnapshot + session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> CrofUsageSnapshot { let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift index a85f9d16a..bb94fdfca 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift @@ -670,13 +670,13 @@ public struct CursorStatusProbe: Sendable { public let baseURL: URL public var timeout: TimeInterval = 15.0 private let browserDetection: BrowserDetection - private let urlSession: URLSession + private let urlSession: any ProviderHTTPTransport public init( baseURL: URL = URL(string: "https://cursor.com")!, timeout: TimeInterval = 15.0, browserDetection: BrowserDetection, - urlSession: URLSession = .shared) + urlSession: any ProviderHTTPTransport = ProviderHTTPClient.shared) { self.baseURL = baseURL self.timeout = timeout @@ -1160,7 +1160,7 @@ public struct CursorStatusProbe: Sendable { baseURL: URL = URL(string: "https://cursor.com")!, timeout: TimeInterval = 15.0, browserDetection: BrowserDetection, - urlSession: URLSession = .shared) + urlSession: any ProviderHTTPTransport = ProviderHTTPClient.shared) { _ = baseURL _ = timeout diff --git a/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift b/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift index 847cc8dfc..570385c89 100644 --- a/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift @@ -135,7 +135,7 @@ public struct DeepSeekUsageFetcher: Sendable { request.setValue("application/json", forHTTPHeaderField: "Accept") request.timeoutInterval = Self.timeoutSeconds - 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 DeepSeekUsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/Doubao/DoubaoUsageFetcher.swift b/Sources/CodexBarCore/Providers/Doubao/DoubaoUsageFetcher.swift index 617791e66..893304a0b 100644 --- a/Sources/CodexBarCore/Providers/Doubao/DoubaoUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Doubao/DoubaoUsageFetcher.swift @@ -135,7 +135,7 @@ public struct DoubaoUsageFetcher: Sendable { request.httpBody = try JSONSerialization.data(withJSONObject: body) - 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 DoubaoUsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift index 579f05b0f..0c67ea902 100644 --- a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift @@ -196,7 +196,7 @@ public struct ElevenLabsUsageFetcher: Sendable { request.setValue("application/json", forHTTPHeaderField: "Accept") request.timeoutInterval = Self.timeoutSeconds - 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 ElevenLabsUsageError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/Gemini/GeminiStatusProbe+DataLoader.swift b/Sources/CodexBarCore/Providers/Gemini/GeminiStatusProbe+DataLoader.swift index 5e85e8c9c..502f4960c 100644 --- a/Sources/CodexBarCore/Providers/Gemini/GeminiStatusProbe+DataLoader.swift +++ b/Sources/CodexBarCore/Providers/Gemini/GeminiStatusProbe+DataLoader.swift @@ -7,7 +7,7 @@ extension GeminiStatusProbe { public static func defaultDataLoader(for request: URLRequest) async throws -> (Data, URLResponse) { let loader = Self.dataLoaderWithCurlFallback( primary: { request in - try await URLSession.shared.data(for: request) + try await ProviderHTTPClient.shared.data(for: request) }, fallback: Self.curlDataLoader) return try await loader(request) diff --git a/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift b/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift index c89ad429e..03a2eedcf 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift @@ -54,7 +54,7 @@ public enum GrokWebBillingFetcher { public static func fetch( credentials: GrokCredentials, - session: URLSession = .shared, + session: any ProviderHTTPTransport = ProviderHTTPClient.shared, endpoint: URL = Self.defaultEndpoint) async throws -> GrokWebBillingSnapshot { try await self.fetch( @@ -66,7 +66,7 @@ public enum GrokWebBillingFetcher { public static func fetch( cookieHeader: String, - session: URLSession = .shared, + session: any ProviderHTTPTransport = ProviderHTTPClient.shared, endpoint: URL = Self.defaultEndpoint) async throws -> GrokWebBillingSnapshot { try await self.fetch( @@ -79,7 +79,7 @@ public enum GrokWebBillingFetcher { private static func fetch( authorizationHeader: String?, cookieHeader: String?, - session: URLSession, + session: any ProviderHTTPTransport, endpoint: URL) async throws -> GrokWebBillingSnapshot { do { @@ -100,7 +100,7 @@ public enum GrokWebBillingFetcher { private static func fetchOnce( authorizationHeader: String?, cookieHeader: String?, - session: URLSession, + session: any ProviderHTTPTransport, endpoint: URL) async throws -> GrokWebBillingSnapshot { var request = URLRequest(url: endpoint) diff --git a/Sources/CodexBarCore/Providers/Kilo/KiloUsageFetcher.swift b/Sources/CodexBarCore/Providers/Kilo/KiloUsageFetcher.swift index f069c3592..492a30c4d 100644 --- a/Sources/CodexBarCore/Providers/Kilo/KiloUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Kilo/KiloUsageFetcher.swift @@ -271,7 +271,7 @@ public struct KiloUsageFetcher: Sendable { let data: Data let response: URLResponse do { - (data, response) = try await URLSession.shared.data(for: request) + (data, response) = try await ProviderHTTPClient.shared.data(for: request) } catch { throw KiloUsageError.networkError(error.localizedDescription) } @@ -332,7 +332,7 @@ public struct KiloUsageFetcher: Sendable { let trpcRequest = try self.makeOrgListTRPCRequest(baseURL: baseURL, apiKey: apiKey) do { - let (data, response) = try await URLSession.shared.data(for: trpcRequest) + let (data, response) = try await ProviderHTTPClient.shared.data(for: trpcRequest) guard let httpResponse = response as? HTTPURLResponse else { throw KiloUsageError.networkError("Invalid response") } @@ -391,7 +391,7 @@ public struct KiloUsageFetcher: Sendable { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - 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 KiloUsageError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift b/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift index c6d6c2a2e..ec204803d 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift @@ -46,7 +46,7 @@ public struct KimiUsageFetcher: Sendable { let requestBody = ["scope": ["FEATURE_CODING"]] request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody) - 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 KimiAPIError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift index 4ff54e207..b509ea4ff 100644 --- a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift @@ -123,7 +123,7 @@ public struct KimiK2UsageFetcher: Sendable { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - 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 KimiK2UsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/Manus/ManusUsageFetcher.swift b/Sources/CodexBarCore/Providers/Manus/ManusUsageFetcher.swift index 9de2fa8cd..c8283ed53 100644 --- a/Sources/CodexBarCore/Providers/Manus/ManusUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Manus/ManusUsageFetcher.swift @@ -103,7 +103,7 @@ public enum ManusUsageFetcher { userAgent, forHTTPHeaderField: "User-Agent") - 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 ManusAPIError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/MiMo/MiMoUsageFetcher.swift b/Sources/CodexBarCore/Providers/MiMo/MiMoUsageFetcher.swift index 1770c5525..c1c253619 100644 --- a/Sources/CodexBarCore/Providers/MiMo/MiMoUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/MiMo/MiMoUsageFetcher.swift @@ -99,7 +99,7 @@ public enum MiMoUsageFetcher { "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") - 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 MiMoUsageError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift index 2a7125e99..6acd3d7ae 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift @@ -52,7 +52,7 @@ public struct MiniMaxUsageFetcher: Sendable { apiToken: String, region: MiniMaxAPIRegion = .global, now: Date = Date(), - session: URLSession = .shared) async throws -> MiniMaxUsageSnapshot + session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> MiniMaxUsageSnapshot { let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { @@ -89,7 +89,7 @@ public struct MiniMaxUsageFetcher: Sendable { apiToken: String, region: MiniMaxAPIRegion, now: Date, - session: URLSession) async throws -> MiniMaxUsageSnapshot + session: any ProviderHTTPTransport) async throws -> MiniMaxUsageSnapshot { var request = URLRequest(url: region.apiRemainsURL) request.httpMethod = "GET" @@ -146,7 +146,7 @@ public struct MiniMaxUsageFetcher: Sendable { self.resolveCodingPlanRefererURL(region: region, environment: environment).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 MiniMaxUsageError.networkError("Invalid response") } @@ -209,7 +209,7 @@ public struct MiniMaxUsageFetcher: Sendable { self.resolveCodingPlanRefererURL(region: region, environment: environment).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 MiniMaxUsageError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/Mistral/MistralUsageFetcher.swift b/Sources/CodexBarCore/Providers/Mistral/MistralUsageFetcher.swift index b1087f634..f4a297685 100644 --- a/Sources/CodexBarCore/Providers/Mistral/MistralUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Mistral/MistralUsageFetcher.swift @@ -36,7 +36,7 @@ public enum MistralUsageFetcher { request.setValue(csrfToken, forHTTPHeaderField: "X-CSRFTOKEN") } - 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 MistralUsageError.apiError("Invalid response type") diff --git a/Sources/CodexBarCore/Providers/Moonshot/MoonshotUsageFetcher.swift b/Sources/CodexBarCore/Providers/Moonshot/MoonshotUsageFetcher.swift index b3ed3ac50..427a690f0 100644 --- a/Sources/CodexBarCore/Providers/Moonshot/MoonshotUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Moonshot/MoonshotUsageFetcher.swift @@ -101,7 +101,7 @@ public struct MoonshotUsageFetcher: Sendable { public static func fetchUsage( apiKey: String, region: MoonshotRegion = .international, - session: URLSession = .shared) async throws -> MoonshotUsageSnapshot + session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> MoonshotUsageSnapshot { let cleaned = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { diff --git a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPICreditBalanceFetcher.swift b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPICreditBalanceFetcher.swift index ed4281104..f3d1ff3a8 100644 --- a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPICreditBalanceFetcher.swift +++ b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPICreditBalanceFetcher.swift @@ -121,7 +121,7 @@ public enum OpenAIAPICreditBalanceFetcher { public static func fetchBalance( apiKey: String, url: URL = Self.creditGrantsURL, - session: URLSession = .shared, + session: any ProviderHTTPTransport = ProviderHTTPClient.shared, now: Date = Date()) async throws -> OpenAIAPICreditBalanceSnapshot { let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIUsageFetcher.swift b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIUsageFetcher.swift index e4afb3741..48dccbce2 100644 --- a/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/OpenAI/OpenAIAPIUsageFetcher.swift @@ -44,7 +44,7 @@ public enum OpenAIAPIUsageFetcher { apiKey: String, costsURL: URL = Self.organizationCostsURL, completionsURL: URL = Self.organizationCompletionsUsageURL, - session: URLSession = .shared, + session: any ProviderHTTPTransport = ProviderHTTPClient.shared, now: Date = Date()) async throws -> OpenAIAPIUsageSnapshot { let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) @@ -87,7 +87,7 @@ public enum OpenAIAPIUsageFetcher { apiKey: String, baseURL: URL, range: DateRange, - session: URLSession) async throws -> CostsResponse + session: any ProviderHTTPTransport) async throws -> CostsResponse { let url = Self.url( baseURL: baseURL, @@ -103,7 +103,7 @@ public enum OpenAIAPIUsageFetcher { apiKey: String, baseURL: URL, range: DateRange, - session: URLSession) async throws -> CompletionsUsageResponse + session: any ProviderHTTPTransport) async throws -> CompletionsUsageResponse { let url = Self.url( baseURL: baseURL, @@ -119,7 +119,7 @@ public enum OpenAIAPIUsageFetcher { 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" diff --git a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeUsageFetcher.swift b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeUsageFetcher.swift index 0946a4c4f..d1f6d68c5 100644 --- a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeUsageFetcher.swift @@ -84,7 +84,7 @@ public struct OpenCodeUsageFetcher: Sendable { timeout: TimeInterval, now: Date = Date(), workspaceIDOverride: String? = nil, - session: URLSession = .shared) async throws -> OpenCodeUsageSnapshot + session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> OpenCodeUsageSnapshot { guard let requestCookieHeader = OpenCodeWebCookieSupport.requestCookieHeader(from: cookieHeader) else { throw OpenCodeUsageError.invalidCredentials @@ -108,7 +108,7 @@ public struct OpenCodeUsageFetcher: Sendable { private static func fetchWorkspaceID( cookieHeader: String, timeout: TimeInterval, - session: URLSession) async throws -> String + session: any ProviderHTTPTransport) async throws -> String { let text = try await self.fetchServerText( request: ServerRequest( @@ -157,7 +157,7 @@ public struct OpenCodeUsageFetcher: Sendable { workspaceID: String, cookieHeader: String, timeout: TimeInterval, - session: URLSession) async throws -> String + session: any ProviderHTTPTransport) async throws -> String { let referer = URL(string: "https://opencode.ai/workspace/\(workspaceID)/billing") ?? self.baseURL let text = try await self.fetchServerText( @@ -249,7 +249,7 @@ public struct OpenCodeUsageFetcher: Sendable { request serverRequest: ServerRequest, cookieHeader: String, timeout: TimeInterval, - session: URLSession) async throws -> String + session: any ProviderHTTPTransport) async throws -> String { let url = self.serverRequestURL( serverID: serverRequest.serverID, diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift index 384bc9f13..690180337 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift @@ -240,7 +240,7 @@ public struct OpenRouterUsageFetcher: Sendable { let title = Self.sanitizedHeaderValue(environment[self.clientTitleEnvKey]) ?? Self.defaultClientTitle request.setValue(title, forHTTPHeaderField: "X-Title") - 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 OpenRouterUsageError.networkError("Invalid response") @@ -350,7 +350,7 @@ public struct OpenRouterUsageFetcher: Sendable { request.timeoutInterval = timeoutSeconds 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 diff --git a/Sources/CodexBarCore/Providers/Perplexity/PerplexityUsageFetcher.swift b/Sources/CodexBarCore/Providers/Perplexity/PerplexityUsageFetcher.swift index 2a9fe5211..2f4e75a9d 100644 --- a/Sources/CodexBarCore/Providers/Perplexity/PerplexityUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Perplexity/PerplexityUsageFetcher.swift @@ -43,7 +43,7 @@ public struct PerplexityUsageFetcher: Sendable { "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" request.setValue(userAgent, forHTTPHeaderField: "User-Agent") - 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 PerplexityAPIError.networkError("Invalid response") } diff --git a/Sources/CodexBarCore/Providers/StepFun/StepFunUsageFetcher.swift b/Sources/CodexBarCore/Providers/StepFun/StepFunUsageFetcher.swift index 64e4b21e4..ab1d5787c 100644 --- a/Sources/CodexBarCore/Providers/StepFun/StepFunUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/StepFun/StepFunUsageFetcher.swift @@ -280,7 +280,7 @@ public struct StepFunUsageFetcher: Sendable { } request.timeoutInterval = self.timeoutSeconds - let (_, response) = try await URLSession.shared.data(for: request) + let (_, response) = try await ProviderHTTPClient.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw StepFunUsageError.networkError("Invalid response fetching platform page") @@ -326,7 +326,7 @@ public struct StepFunUsageFetcher: Sendable { request.setValue("INGRESSCOOKIE=\(ingressCookie)", forHTTPHeaderField: "Cookie") request.timeoutInterval = self.timeoutSeconds - 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 StepFunUsageError.networkError("Invalid response from RegisterDevice") @@ -372,7 +372,7 @@ public struct StepFunUsageFetcher: Sendable { forHTTPHeaderField: "Cookie") request.timeoutInterval = self.timeoutSeconds - 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 StepFunUsageError.networkError("Invalid response from SignInByPassword") @@ -411,7 +411,7 @@ public struct StepFunUsageFetcher: Sendable { request.setValue("Oasis-Token=\(token); Oasis-Webid=\(self.webID)", forHTTPHeaderField: "Cookie") request.timeoutInterval = self.timeoutSeconds - 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 StepFunUsageError.networkError("Invalid response") @@ -456,7 +456,7 @@ public struct StepFunUsageFetcher: Sendable { request.setValue("Oasis-Token=\(token); Oasis-Webid=\(self.webID)", forHTTPHeaderField: "Cookie") request.timeoutInterval = self.timeoutSeconds - 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 { Self.log.debug("StepFun plan status request failed, skipping plan name") diff --git a/Sources/CodexBarCore/Providers/Synthetic/SyntheticUsageStats.swift b/Sources/CodexBarCore/Providers/Synthetic/SyntheticUsageStats.swift index f3e2edff8..eacdc71ed 100644 --- a/Sources/CodexBarCore/Providers/Synthetic/SyntheticUsageStats.swift +++ b/Sources/CodexBarCore/Providers/Synthetic/SyntheticUsageStats.swift @@ -104,7 +104,7 @@ public struct SyntheticUsageFetcher: Sendable { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - 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 SyntheticUsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/Venice/VeniceUsageFetcher.swift b/Sources/CodexBarCore/Providers/Venice/VeniceUsageFetcher.swift index bd17f9aad..a28424d45 100644 --- a/Sources/CodexBarCore/Providers/Venice/VeniceUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Venice/VeniceUsageFetcher.swift @@ -172,7 +172,7 @@ public struct VeniceUsageFetcher: Sendable { request.setValue("application/json", forHTTPHeaderField: "Accept") request.timeoutInterval = Self.timeoutSeconds - 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 VeniceUsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAITokenRefresher.swift b/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAITokenRefresher.swift index b5e8e3f68..dfab37220 100644 --- a/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAITokenRefresher.swift +++ b/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAITokenRefresher.swift @@ -49,7 +49,7 @@ public enum VertexAITokenRefresher { request.httpBody = bodyString.data(using: .utf8) 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") } diff --git a/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAIUsageFetcher.swift b/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAIUsageFetcher.swift index 2c9da2033..25d690078 100644 --- a/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAIUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/VertexAI/VertexAIOAuth/VertexAIUsageFetcher.swift @@ -219,7 +219,7 @@ public enum VertexAIUsageFetcher { let response: URLResponse do { - (data, response) = try await URLSession.shared.data(for: request) + (data, response) = try await ProviderHTTPClient.shared.data(for: request) } catch { throw VertexAIFetchError.networkError(error) } diff --git a/Sources/CodexBarCore/Providers/Warp/WarpUsageFetcher.swift b/Sources/CodexBarCore/Providers/Warp/WarpUsageFetcher.swift index c5e2bf8a4..b667a8bb0 100644 --- a/Sources/CodexBarCore/Providers/Warp/WarpUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Warp/WarpUsageFetcher.swift @@ -206,7 +206,7 @@ public struct WarpUsageFetcher: Sendable { request.httpBody = try JSONSerialization.data(withJSONObject: body) - 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 WarpUsageError.networkError("Invalid response") diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift index fd102d523..2cc1519a7 100644 --- a/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift +++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift @@ -139,7 +139,7 @@ public enum WindsurfWebFetcher { manualSessionInput: String? = nil, timeout: TimeInterval = 15, logger: ((String) -> Void)? = nil, - session: URLSession = .shared) async throws -> UsageSnapshot + session: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> UsageSnapshot { let log: (String) -> Void = { msg in logger?("[windsurf-web] \(msg)") } @@ -257,7 +257,7 @@ public enum WindsurfWebFetcher { sessionInfos: [WindsurfDevinSessionImporter.SessionInfo], timeout: TimeInterval, logger log: (String) -> Void, - session: URLSession) async throws -> UsageSnapshot + session: any ProviderHTTPTransport) async throws -> UsageSnapshot { var lastError: Error? for sessionInfo in sessionInfos { @@ -316,7 +316,7 @@ public enum WindsurfWebFetcher { private static func fetchPlanStatus( auth: WindsurfDevinSessionAuth, timeout: TimeInterval, - session: URLSession) async throws -> WindsurfGetPlanStatusResponse + session: any ProviderHTTPTransport) async throws -> WindsurfGetPlanStatusResponse { guard let url = URL(string: self.getPlanStatusURL) else { throw WindsurfWebFetcherError.apiCallFailed("Invalid GetPlanStatus URL") diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift index 7d6a677d7..2938c0aa8 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift @@ -326,7 +326,7 @@ public struct ZaiUsageFetcher: Sendable { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "authorization") request.setValue("application/json", forHTTPHeaderField: "accept") - 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 ZaiUsageError.networkError("Invalid response") @@ -607,7 +607,7 @@ extension ZaiUsageFetcher { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") - 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 ZaiUsageError.networkError("Invalid response") diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift index 316f6f985..3ae186a65 100644 --- a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -2,69 +2,27 @@ import Foundation import Testing @testable import CodexBarCore -@Suite(.serialized) struct CopilotUsageFetcherTests { @Test func `fetchGitHubIdentity uses shared client`() async throws { - let registered = URLProtocol.registerClass(CopilotHTTPStubURLProtocol.self) - defer { - if registered { - URLProtocol.unregisterClass(CopilotHTTPStubURLProtocol.self) - } - CopilotHTTPStubURLProtocol.handler = nil - CopilotHTTPStubURLProtocol.requests = [] - } - - CopilotHTTPStubURLProtocol.requests = [] - CopilotHTTPStubURLProtocol.handler = { request in - CopilotHTTPStubURLProtocol.requests.append(request) + let transport = ProviderHTTPTransportStub { request in guard request.value(forHTTPHeaderField: "Authorization") == "token abc123" else { throw URLError(.userAuthenticationRequired) } - let response = HTTPURLResponse( - url: try #require(request.url), + let response = try HTTPURLResponse( + url: #require(request.url), statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])! return (Data(#"{"login":"testuser","id":123}"#.utf8), response) } - let identity = try await CopilotUsageFetcher.fetchGitHubIdentity(token: "abc123") + let identity = try await CopilotUsageFetcher.fetchGitHubIdentity(token: "abc123", transport: transport) #expect(identity.login == "testuser") #expect(identity.id == 123) - #expect(CopilotHTTPStubURLProtocol.requests.count == 1) - #expect(CopilotHTTPStubURLProtocol.requests.first?.url?.host == "api.github.com") - } -} - -final class CopilotHTTPStubURLProtocol: URLProtocol { - nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? - nonisolated(unsafe) static var requests: [URLRequest] = [] - - override class func canInit(with request: URLRequest) -> Bool { - true + let requests = await transport.requests() + #expect(requests.count == 1) + #expect(requests.first?.url?.host == "api.github.com") } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - request - } - - override func startLoading() { - guard let handler = Self.handler else { - self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork)) - return - } - - do { - let (data, response) = try handler(self.request) - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - self.client?.urlProtocol(self, didLoad: data) - self.client?.urlProtocolDidFinishLoading(self) - } catch { - self.client?.urlProtocol(self, didFailWithError: error) - } - } - - override func stopLoading() {} } diff --git a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift index d6bcb42cf..926612729 100644 --- a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift +++ b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift @@ -2,25 +2,13 @@ import Foundation import Testing @testable import CodexBar -@Suite(.serialized) @MainActor struct GoogleWorkspaceStatusNetworkTests { @Test func `fetchWorkspaceStatus uses shared client`() async throws { - let registered = URLProtocol.registerClass(WorkspaceStatusStubURLProtocol.self) - defer { - if registered { - URLProtocol.unregisterClass(WorkspaceStatusStubURLProtocol.self) - } - WorkspaceStatusStubURLProtocol.handler = nil - WorkspaceStatusStubURLProtocol.requests = [] - } - - WorkspaceStatusStubURLProtocol.requests = [] - WorkspaceStatusStubURLProtocol.handler = { request in - WorkspaceStatusStubURLProtocol.requests.append(request) - let response = HTTPURLResponse( - url: try #require(request.url), + let transport = ProviderHTTPTransportStub { request in + let response = try HTTPURLResponse( + url: #require(request.url), statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])! @@ -43,42 +31,14 @@ struct GoogleWorkspaceStatusNetworkTests { return (body, response) } - let status = try await UsageStore.fetchWorkspaceStatus(productID: "npdyhgECDJ6tB66MxXyo") + let status = try await UsageStore.fetchWorkspaceStatus( + productID: "npdyhgECDJ6tB66MxXyo", + transport: transport) #expect(status.indicator == .critical) #expect(status.description == "Gemini API error.") - #expect(WorkspaceStatusStubURLProtocol.requests.count == 1) - #expect(WorkspaceStatusStubURLProtocol.requests.first?.url?.host == "www.google.com") - } -} - -final class WorkspaceStatusStubURLProtocol: URLProtocol { - nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? - nonisolated(unsafe) static var requests: [URLRequest] = [] - - override class func canInit(with request: URLRequest) -> Bool { - true - } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - request + let requests = await transport.requests() + #expect(requests.count == 1) + #expect(requests.first?.url?.host == "www.google.com") } - - override func startLoading() { - guard let handler = Self.handler else { - self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork)) - return - } - - do { - let (data, response) = try handler(self.request) - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - self.client?.urlProtocol(self, didLoad: data) - self.client?.urlProtocolDidFinishLoading(self) - } catch { - self.client?.urlProtocol(self, didFailWithError: error) - } - } - - override func stopLoading() {} } diff --git a/Tests/CodexBarTests/ProviderHTTPClientTests.swift b/Tests/CodexBarTests/ProviderHTTPClientTests.swift index cb6aad354..8594380dd 100644 --- a/Tests/CodexBarTests/ProviderHTTPClientTests.swift +++ b/Tests/CodexBarTests/ProviderHTTPClientTests.swift @@ -9,8 +9,8 @@ struct ProviderHTTPClientTests { StubURLProtocol.requests = [] StubURLProtocol.handler = { request in StubURLProtocol.requests.append(request) - let response = HTTPURLResponse( - url: try #require(request.url), + let response = try HTTPURLResponse( + url: #require(request.url), statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])! @@ -24,7 +24,7 @@ struct ProviderHTTPClientTests { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [StubURLProtocol.self] let client = ProviderHTTPClient(session: URLSession(configuration: configuration)) - let request = URLRequest(url: URL(string: "https://example.com/status")!) + let request = try URLRequest(url: #require(URL(string: "https://example.com/status"))) let (data, response) = try await client.data(for: request) @@ -40,11 +40,11 @@ final class StubURLProtocol: URLProtocol { nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))? nonisolated(unsafe) static var requests: [URLRequest] = [] - override class func canInit(with request: URLRequest) -> Bool { + override static func canInit(with request: URLRequest) -> Bool { true } - override class func canonicalRequest(for request: URLRequest) -> URLRequest { + override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } diff --git a/Tests/CodexBarTests/ProviderHTTPTransportStub.swift b/Tests/CodexBarTests/ProviderHTTPTransportStub.swift new file mode 100644 index 000000000..1c01e75cc --- /dev/null +++ b/Tests/CodexBarTests/ProviderHTTPTransportStub.swift @@ -0,0 +1,20 @@ +import Foundation +@testable import CodexBarCore + +actor ProviderHTTPTransportStub: ProviderHTTPTransport { + private let handler: @Sendable (URLRequest) async throws -> (Data, URLResponse) + private var recordedRequests: [URLRequest] = [] + + init(handler: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse)) { + self.handler = handler + } + + func requests() -> [URLRequest] { + self.recordedRequests + } + + func data(for request: URLRequest) async throws -> (Data, URLResponse) { + self.recordedRequests.append(request) + return try await self.handler(request) + } +}