From 048622109046833ca5bd4f95ce4d4ce51f2ba6ba Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:44:00 +0800 Subject: [PATCH 1/6] Decode Copilot token-billing credits used --- Sources/CodexBarCore/CopilotUsageModels.swift | 5 +++ .../CopilotUsageModelsTests.swift | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/Sources/CodexBarCore/CopilotUsageModels.swift b/Sources/CodexBarCore/CopilotUsageModels.swift index bcfee9fe58..2886badca1 100644 --- a/Sources/CodexBarCore/CopilotUsageModels.swift +++ b/Sources/CodexBarCore/CopilotUsageModels.swift @@ -19,6 +19,7 @@ public struct CopilotUsageResponse: Sendable, Decodable { public struct QuotaSnapshot: Sendable, Decodable { public let entitlement: Double public let remaining: Double + public let creditsUsed: Double? public let percentRemaining: Double public let quotaId: String public let hasPercentRemaining: Bool @@ -58,6 +59,7 @@ public struct CopilotUsageResponse: Sendable, Decodable { private enum CodingKeys: String, CodingKey { case entitlement case remaining + case creditsUsed = "credits_used" case percentRemaining = "percent_remaining" case quotaId = "quota_id" case unlimited @@ -68,11 +70,13 @@ public struct CopilotUsageResponse: Sendable, Decodable { remaining: Double, percentRemaining: Double, quotaId: String, + creditsUsed: Double? = nil, hasPercentRemaining: Bool = true, unlimited: Bool = false) { self.entitlement = entitlement self.remaining = remaining + self.creditsUsed = creditsUsed self.percentRemaining = unlimited ? 100 : percentRemaining self.quotaId = quotaId self.hasPercentRemaining = unlimited || hasPercentRemaining @@ -89,6 +93,7 @@ public struct CopilotUsageResponse: Sendable, Decodable { self.remaining = decodedRemaining ?? 0 self.entitlementWasDecoded = decodedEntitlement != nil self.remainingWasDecoded = decodedRemaining != nil + self.creditsUsed = Self.decodeNumberIfPresent(container: container, key: .creditsUsed) let decodedUnlimited = try container.decodeIfPresent(Bool.self, forKey: .unlimited) ?? false let decodedPercent = Self.decodeNumberIfPresent(container: container, key: .percentRemaining) if decodedUnlimited { diff --git a/Tests/CodexBarTests/CopilotUsageModelsTests.swift b/Tests/CodexBarTests/CopilotUsageModelsTests.swift index 4d42656c77..4b313087c1 100644 --- a/Tests/CodexBarTests/CopilotUsageModelsTests.swift +++ b/Tests/CodexBarTests/CopilotUsageModelsTests.swift @@ -529,6 +529,42 @@ struct CopilotUsageModelsTests { #expect(!chat.isPlaceholder) } + @Test + func `decodes credits used counter from token billed business payload`() throws { + let response = try Self.decodeFixture( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "quota_reset_date": "2026-09-01", + "quota_snapshots": { + "premium_interactions": { + "unlimited": true, + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100.0, + "quota_id": "premium_interactions", + "credits_used": 31 + }, + "chat": { + "unlimited": true, + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100.0, + "quota_id": "chat", + "credits_used": 0 + } + } + } + """) + + let premium = try #require(response.quotaSnapshots.premiumInteractions) + #expect(response.tokenBasedBilling == true) + #expect(premium.creditsUsed == 31) + #expect(premium.unlimited) + #expect(premium.usedPercent == 0) + } + @Test func `flags zero entitlement snapshot as placeholder`() { let snapshot = CopilotUsageResponse.QuotaSnapshot( From 344f432b17afab03e63157babcb028deb3fbead2 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:38:43 +0800 Subject: [PATCH 2/6] Cover credits used counter in token-billing card regression --- Tests/CodexBarTests/CopilotUsageFetcherTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift index d590632e69..b3868c6f5a 100644 --- a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -47,13 +47,15 @@ struct CopilotUsageFetcherTests { "entitlement": 0, "remaining": 0, "percent_remaining": 100, - "quota_id": "premium_interactions" + "quota_id": "premium_interactions", + "credits_used": 31 }, "chat": { "entitlement": 0, "remaining": 0, "percent_remaining": 100, - "quota_id": "chat" + "quota_id": "chat", + "credits_used": 0 } } } From 5203c7feacdcf98ab555984df66a56af73245205 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:43:38 +0800 Subject: [PATCH 3/6] Keep Copilot credits counter accessible through snapshots --- Sources/CodexBarCore/CopilotUsageModels.swift | 26 ++++++++++++++++--- .../Copilot/CopilotUsageFetcher.swift | 5 ++++ Sources/CodexBarCore/UsageFetcher.swift | 5 ++++ .../CopilotUsageFetcherTests.swift | 3 +++ .../CopilotUsageModelsTests.swift | 26 +++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBarCore/CopilotUsageModels.swift b/Sources/CodexBarCore/CopilotUsageModels.swift index 2886badca1..4f4f65968e 100644 --- a/Sources/CodexBarCore/CopilotUsageModels.swift +++ b/Sources/CodexBarCore/CopilotUsageModels.swift @@ -56,6 +56,13 @@ public struct CopilotUsageResponse: Sendable, Decodable { .remaining == 0 } + /// Whether the snapshot carries a real absolute credit counter, even when + /// it lacks a usable percentage window. Such snapshots stay accessible in + /// the decoded response without ever becoming a fake percentage bar. + public var carriesCreditsCounter: Bool { + self.creditsUsed != nil + } + private enum CodingKeys: String, CodingKey { case entitlement case remaining @@ -190,10 +197,10 @@ public struct CopilotUsageResponse: Sendable, Decodable { let container = try decoder.container(keyedBy: CodingKeys.self) var premium = try container.decodeIfPresent(QuotaSnapshot.self, forKey: .premiumInteractions) var chat = try container.decodeIfPresent(QuotaSnapshot.self, forKey: .chat) - if premium?.isPlaceholder == true { + if premium?.isPlaceholder == true, premium?.carriesCreditsCounter != true { premium = nil } - if chat?.isPlaceholder == true { + if chat?.isPlaceholder == true, chat?.carriesCreditsCounter != true { chat = nil } @@ -209,7 +216,7 @@ public struct CopilotUsageResponse: Sendable, Decodable { guard let decoded = try dynamic.decodeIfPresent(QuotaSnapshot.self, forKey: key) else { continue } - guard !decoded.isPlaceholder else { continue } + guard !decoded.isPlaceholder || decoded.carriesCreditsCounter else { continue } value = decoded } catch { continue @@ -358,3 +365,16 @@ public struct CopilotUsageResponse: Sendable, Decodable { return self.usableQuotaSnapshot(from: direct) ?? self.usableQuotaSnapshot(from: fallback) } } + +/// Token-billed Copilot seats report consumption as an absolute credit counter +/// rather than a percentage window. Carried separately from rate windows so the +/// value stays accessible without inventing a fake quota denominator. +public struct CopilotCreditsSnapshot: Sendable, Codable, Equatable { + public let creditsUsed: Double + public let quotaResetDate: Date? + + public init(creditsUsed: Double, quotaResetDate: Date? = nil) { + self.creditsUsed = creditsUsed + self.quotaResetDate = quotaResetDate + } +} diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift index 756f2c59eb..268b2733e2 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift @@ -71,6 +71,10 @@ public struct CopilotUsageFetcher: Sendable { let chatSnapshot = usage.quotaSnapshots.chat let premium = Self.makeRateWindow(from: premiumSnapshot, resetsAt: resetsAt) let chat = Self.makeRateWindow(from: chatSnapshot, resetsAt: resetsAt) + let creditsUsed = premiumSnapshot?.creditsUsed ?? chatSnapshot?.creditsUsed + let copilotCredits = creditsUsed.map { + CopilotCreditsSnapshot(creditsUsed: $0, quotaResetDate: resetsAt) + } let hasUnlimitedQuota = premiumSnapshot?.unlimited == true || chatSnapshot?.unlimited == true let primary: RateWindow? @@ -102,6 +106,7 @@ public struct CopilotUsageFetcher: Sendable { secondary: secondary, tertiary: nil, providerCost: nil, + copilotCredits: copilotCredits, updatedAt: Date(), identity: identity) } diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index 8a5729fe11..45bf3d2a1d 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -171,6 +171,7 @@ public struct UsageSnapshot: Codable, Sendable { public let poeUsage: PoeUsageHistorySnapshot? public let xaiUsage: XAIUsageSnapshot? public let cursorRequests: CursorRequestUsage? + public let copilotCredits: CopilotCreditsSnapshot? /// Live-only marker for optional Command Code subscription lookup failure. public let commandCodeSubscriptionEnrichmentUnavailable: Bool /// Live-only marker that Command Code returned a recognized subscription plan. @@ -247,6 +248,7 @@ public struct UsageSnapshot: Codable, Sendable { poeUsage: PoeUsageHistorySnapshot? = nil, xaiUsage: XAIUsageSnapshot? = nil, cursorRequests: CursorRequestUsage? = nil, + copilotCredits: CopilotCreditsSnapshot? = nil, commandCodeSubscriptionEnrichmentUnavailable: Bool = false, commandCodeHasSubscriptionPlan: Bool = false, commandCodeMonthlyGrantDepleted: Bool = false, @@ -289,6 +291,7 @@ public struct UsageSnapshot: Codable, Sendable { self.poeUsage = poeUsage self.xaiUsage = xaiUsage self.cursorRequests = cursorRequests + self.copilotCredits = copilotCredits self.commandCodeSubscriptionEnrichmentUnavailable = commandCodeSubscriptionEnrichmentUnavailable self.commandCodeHasSubscriptionPlan = commandCodeHasSubscriptionPlan self.commandCodeMonthlyGrantDepleted = commandCodeMonthlyGrantDepleted @@ -364,6 +367,7 @@ public struct UsageSnapshot: Codable, Sendable { self.poeUsage = try container.decodeIfPresent(PoeUsageHistorySnapshot.self, forKey: .poeUsage) self.xaiUsage = try container.decodeIfPresent(XAIUsageSnapshot.self, forKey: .xaiUsage) self.cursorRequests = nil // Not persisted, fetched fresh each time + self.copilotCredits = nil // Not persisted, fetched fresh each time self.commandCodeSubscriptionEnrichmentUnavailable = false // Live-only fetch state self.commandCodeHasSubscriptionPlan = false // Live-only fetch state self.commandCodeMonthlyGrantDepleted = false // Live-only fetch state @@ -603,6 +607,7 @@ public struct UsageSnapshot: Codable, Sendable { poeUsage: self.poeUsage, xaiUsage: self.xaiUsage, cursorRequests: self.cursorRequests, + copilotCredits: self.copilotCredits, commandCodeSubscriptionEnrichmentUnavailable: self.commandCodeSubscriptionEnrichmentUnavailable, commandCodeHasSubscriptionPlan: self.commandCodeHasSubscriptionPlan, commandCodeMonthlyGrantDepleted: self.commandCodeMonthlyGrantDepleted, diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift index b3868c6f5a..6119521065 100644 --- a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -42,6 +42,7 @@ struct CopilotUsageFetcherTests { { "copilot_plan": "business", "token_based_billing": true, + "quota_reset_date": "2026-09-01", "quota_snapshots": { "premium_interactions": { "entitlement": 0, @@ -68,6 +69,8 @@ struct CopilotUsageFetcherTests { #expect(snapshot.primary == nil) #expect(snapshot.secondary == nil) + #expect(snapshot.copilotCredits?.creditsUsed == 31) + #expect(snapshot.copilotCredits?.quotaResetDate != nil) #expect(snapshot.identity?.loginMethod == "Business") } diff --git a/Tests/CodexBarTests/CopilotUsageModelsTests.swift b/Tests/CodexBarTests/CopilotUsageModelsTests.swift index 4b313087c1..b3116ceec1 100644 --- a/Tests/CodexBarTests/CopilotUsageModelsTests.swift +++ b/Tests/CodexBarTests/CopilotUsageModelsTests.swift @@ -565,6 +565,32 @@ struct CopilotUsageModelsTests { #expect(premium.usedPercent == 0) } + @Test + func `keeps credit counter accessible without unlimited marker`() throws { + // A zero-entitlement snapshot without `unlimited` is still a placeholder + // for percentage windows, but its absolute counter must not be lost. + let response = try Self.decodeFixture( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "quota_snapshots": { + "premium_interactions": { + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "premium_interactions", + "credits_used": 31 + } + } + } + """) + + let premium = try #require(response.quotaSnapshots.premiumInteractions) + #expect(premium.creditsUsed == 31) + #expect(premium.isPlaceholder) + } + @Test func `flags zero entitlement snapshot as placeholder`() { let snapshot = CopilotUsageResponse.QuotaSnapshot( From 7ba0cede00d7b982d6eb878605915beee10f638c Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:01:11 +0800 Subject: [PATCH 4/6] Preserve Copilot credits counter across quota fallback --- Sources/CodexBarCore/CopilotUsageModels.swift | 42 ++++++++++++++++- .../CopilotUsageFetcherTests.swift | 47 +++++++++++++++++++ .../CopilotUsageModelsTests.swift | 28 +++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/CopilotUsageModels.swift b/Sources/CodexBarCore/CopilotUsageModels.swift index 4f4f65968e..4ac848b32f 100644 --- a/Sources/CodexBarCore/CopilotUsageModels.swift +++ b/Sources/CodexBarCore/CopilotUsageModels.swift @@ -125,6 +125,43 @@ public struct CopilotUsageResponse: Sendable, Decodable { self.unlimited = decodedUnlimited } + private init( + entitlement: Double, + remaining: Double, + creditsUsed: Double?, + percentRemaining: Double, + quotaId: String, + hasPercentRemaining: Bool, + unlimited: Bool, + entitlementWasDecoded: Bool, + remainingWasDecoded: Bool) + { + self.entitlement = entitlement + self.remaining = remaining + self.creditsUsed = creditsUsed + self.percentRemaining = percentRemaining + self.quotaId = quotaId + self.hasPercentRemaining = hasPercentRemaining + self.unlimited = unlimited + self.entitlementWasDecoded = entitlementWasDecoded + self.remainingWasDecoded = remainingWasDecoded + } + + /// Returns a copy carrying `creditsUsed`, preserving the decoded-flag + /// semantics that placeholder classification depends on. + fileprivate func withCreditsUsed(_ creditsUsed: Double?) -> QuotaSnapshot { + QuotaSnapshot( + entitlement: self.entitlement, + remaining: self.remaining, + creditsUsed: creditsUsed, + percentRemaining: self.percentRemaining, + quotaId: self.quotaId, + hasPercentRemaining: self.hasPercentRemaining, + unlimited: self.unlimited, + entitlementWasDecoded: self.entitlementWasDecoded, + remainingWasDecoded: self.remainingWasDecoded) + } + private static func decodeNumberIfPresent( container: KeyedDecodingContainer, key: CodingKeys) -> Double? @@ -360,7 +397,10 @@ public struct CopilotUsageResponse: Sendable, Decodable { fallback: QuotaSnapshot?) -> QuotaSnapshot? { if direct?.unlimited == true, let fallback = usableQuotaSnapshot(from: fallback) { - return fallback + // The direct snapshot's absolute credit counter is real consumption + // even though its unlimited marker makes it ineligible for a + // percentage window; keep the counter on the selected fallback. + return fallback.withCreditsUsed(direct?.creditsUsed) } return self.usableQuotaSnapshot(from: direct) ?? self.usableQuotaSnapshot(from: fallback) } diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift index 6119521065..a9684c802e 100644 --- a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -74,6 +74,53 @@ struct CopilotUsageFetcherTests { #expect(snapshot.identity?.loginMethod == "Business") } + @Test + func `fetch retains token billed credits counter across monthly quota fallback`() async throws { + let transport = ProviderHTTPTransportStub { request in + #expect(request.value(forHTTPHeaderField: "Authorization") == "token test-token-placeholder") + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + let data = Data( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "quota_reset_date": "2026-09-01", + "monthly_quotas": { "completions": 300 }, + "limited_user_quotas": { "completions": 75 }, + "quota_snapshots": { + "premium_interactions": { + "unlimited": true, + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "premium_interactions", + "credits_used": 31 + }, + "chat": { + "unlimited": true, + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "chat", + "credits_used": 0 + } + } + } + """.utf8) + return (data, response) + } + let fetcher = CopilotUsageFetcher(token: "test-token-placeholder", transport: transport) + + let snapshot = try await fetcher.fetch() + + #expect(snapshot.primary?.usedPercent == 75) + #expect(snapshot.copilotCredits?.creditsUsed == 31) + } + @Test func `fetch omits explicitly unlimited only chat quota without failing`() async throws { let transport = ProviderHTTPTransportStub { request in diff --git a/Tests/CodexBarTests/CopilotUsageModelsTests.swift b/Tests/CodexBarTests/CopilotUsageModelsTests.swift index b3116ceec1..5fc1b08270 100644 --- a/Tests/CodexBarTests/CopilotUsageModelsTests.swift +++ b/Tests/CodexBarTests/CopilotUsageModelsTests.swift @@ -565,6 +565,34 @@ struct CopilotUsageModelsTests { #expect(premium.usedPercent == 0) } + @Test + func `keeps credit counter when unlimited direct snapshot falls back to monthly quota`() throws { + let response = try Self.decodeFixture( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "monthly_quotas": { "completions": 300 }, + "limited_user_quotas": { "completions": 75 }, + "quota_snapshots": { + "premium_interactions": { + "unlimited": true, + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "premium_interactions", + "credits_used": 31 + } + } + } + """) + + let premium = try #require(response.quotaSnapshots.premiumInteractions) + #expect(premium.creditsUsed == 31) + #expect(premium.quotaId == "completions") + #expect(premium.usedPercent == 75) + } + @Test func `keeps credit counter accessible without unlimited marker`() throws { // A zero-entitlement snapshot without `unlimited` is still a placeholder From c48cbf7a3f6defd0d0a143245e974d450056bd30 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:19:53 +0800 Subject: [PATCH 5/6] Expose Copilot credits counter in diagnostics --- .../Providers/ProviderDiagnosticExport.swift | 21 +++++++++++++++++++ .../ProviderDiagnosticExportTests.swift | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift index 5b383aa8ea..73be5b379d 100644 --- a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift @@ -145,6 +145,7 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable { public let extraWindowCount: Int public let providerCostPresent: Bool public let providerSpecificData: [String] + public let copilotCredits: ProviderDiagnosticCopilotCredits? private enum CodingKeys: String, CodingKey { case updatedAt @@ -153,6 +154,7 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable { case extraWindowCount case providerCostPresent case providerSpecificData + case copilotCredits } public init(from snapshot: UsageSnapshot) { @@ -187,6 +189,7 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable { if snapshot.deepgramUsage != nil { providerSpecificData.append("deepgramUsage") } if snapshot.xaiUsage != nil { providerSpecificData.append("xaiUsage") } if snapshot.cursorRequests != nil { providerSpecificData.append("cursorRequests") } + if snapshot.copilotCredits != nil { providerSpecificData.append("copilotCredits") } self.updatedAt = snapshot.updatedAt self.dataConfidence = snapshot.dataConfidence.rawValue @@ -194,6 +197,11 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable { self.extraWindowCount = snapshot.extraRateWindows?.count ?? 0 self.providerCostPresent = snapshot.providerCost != nil self.providerSpecificData = providerSpecificData.sorted() + self.copilotCredits = snapshot.copilotCredits.map { + ProviderDiagnosticCopilotCredits( + creditsUsed: $0.creditsUsed, + quotaResetDate: $0.quotaResetDate) + } } public init(from decoder: Decoder) throws { @@ -205,6 +213,19 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable { self.extraWindowCount = try container.decode(Int.self, forKey: .extraWindowCount) self.providerCostPresent = try container.decode(Bool.self, forKey: .providerCostPresent) self.providerSpecificData = try container.decode([String].self, forKey: .providerSpecificData) + self.copilotCredits = try container.decodeIfPresent( + ProviderDiagnosticCopilotCredits.self, + forKey: .copilotCredits) + } +} + +public struct ProviderDiagnosticCopilotCredits: Codable, Sendable { + public let creditsUsed: Double + public let quotaResetDate: Date? + + public init(creditsUsed: Double, quotaResetDate: Date?) { + self.creditsUsed = creditsUsed + self.quotaResetDate = quotaResetDate } } diff --git a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift index 5209e3596f..dc176bc016 100644 --- a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift @@ -48,6 +48,27 @@ struct ProviderDiagnosticExportTests { #expect(!json.contains("localizedDescription")) } + @Test + func `diagnostic export carries copilot credits counter`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let snapshot = UsageSnapshot( + primary: nil, + secondary: nil, + copilotCredits: CopilotCreditsSnapshot( + creditsUsed: 31, + quotaResetDate: now.addingTimeInterval(86400)), + updatedAt: now) + let summary = ProviderDiagnosticUsageSummary(from: snapshot) + + #expect(summary.copilotCredits?.creditsUsed == 31) + #expect(summary.copilotCredits?.quotaResetDate != nil) + #expect(summary.providerSpecificData.contains("copilotCredits")) + + let json = try self.json(summary) + #expect(json.contains("\"copilotCredits\"")) + #expect(json.contains("31")) + } + @Test func `diagnostic export decodes legacy schema without platform metadata`() throws { let export = ProviderDiagnosticExport( From 571b1b6cf2a991266321b3592ca42c446543d67a Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:37:18 +0800 Subject: [PATCH 6/6] Keep credits counter on zero entitlement quota fallback --- Sources/CodexBarCore/CopilotUsageModels.swift | 13 ++++++- .../CopilotUsageFetcherTests.swift | 38 +++++++++++++++++++ .../CopilotUsageModelsTests.swift | 27 +++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/CopilotUsageModels.swift b/Sources/CodexBarCore/CopilotUsageModels.swift index 4ac848b32f..4ea1cfe756 100644 --- a/Sources/CodexBarCore/CopilotUsageModels.swift +++ b/Sources/CodexBarCore/CopilotUsageModels.swift @@ -402,7 +402,18 @@ public struct CopilotUsageResponse: Sendable, Decodable { // percentage window; keep the counter on the selected fallback. return fallback.withCreditsUsed(direct?.creditsUsed) } - return self.usableQuotaSnapshot(from: direct) ?? self.usableQuotaSnapshot(from: fallback) + if let directWindow = self.usableQuotaSnapshot(from: direct) { + return directWindow + } + guard let fallback = self.usableQuotaSnapshot(from: fallback) else { + return nil + } + // A zero-entitlement placeholder can still carry a real absolute + // counter; keep it on the selected fallback instead of dropping it. + if direct?.carriesCreditsCounter == true { + return fallback.withCreditsUsed(direct?.creditsUsed) + } + return fallback } } diff --git a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift index a9684c802e..9610f50369 100644 --- a/Tests/CodexBarTests/CopilotUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CopilotUsageFetcherTests.swift @@ -74,6 +74,44 @@ struct CopilotUsageFetcherTests { #expect(snapshot.identity?.loginMethod == "Business") } + @Test + func `fetch retains token billed credits counter across zero entitlement fallback`() async throws { + let transport = ProviderHTTPTransportStub { request in + #expect(request.value(forHTTPHeaderField: "Authorization") == "token test-token-placeholder") + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + let data = Data( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "quota_reset_date": "2026-09-01", + "monthly_quotas": { "completions": 300 }, + "limited_user_quotas": { "completions": 75 }, + "quota_snapshots": { + "premium_interactions": { + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "premium_interactions", + "credits_used": 31 + } + } + } + """.utf8) + return (data, response) + } + let fetcher = CopilotUsageFetcher(token: "test-token-placeholder", transport: transport) + + let snapshot = try await fetcher.fetch() + + #expect(snapshot.primary?.usedPercent == 75) + #expect(snapshot.copilotCredits?.creditsUsed == 31) + } + @Test func `fetch retains token billed credits counter across monthly quota fallback`() async throws { let transport = ProviderHTTPTransportStub { request in diff --git a/Tests/CodexBarTests/CopilotUsageModelsTests.swift b/Tests/CodexBarTests/CopilotUsageModelsTests.swift index 5fc1b08270..35c13e5c33 100644 --- a/Tests/CodexBarTests/CopilotUsageModelsTests.swift +++ b/Tests/CodexBarTests/CopilotUsageModelsTests.swift @@ -565,6 +565,33 @@ struct CopilotUsageModelsTests { #expect(premium.usedPercent == 0) } + @Test + func `keeps credit counter when zero entitlement direct snapshot falls back to monthly quota`() throws { + let response = try Self.decodeFixture( + """ + { + "copilot_plan": "business", + "token_based_billing": true, + "monthly_quotas": { "completions": 300 }, + "limited_user_quotas": { "completions": 75 }, + "quota_snapshots": { + "premium_interactions": { + "entitlement": 0, + "remaining": 0, + "percent_remaining": 100, + "quota_id": "premium_interactions", + "credits_used": 31 + } + } + } + """) + + let premium = try #require(response.quotaSnapshots.premiumInteractions) + #expect(premium.creditsUsed == 31) + #expect(premium.quotaId == "completions") + #expect(premium.usedPercent == 75) + } + @Test func `keeps credit counter when unlimited direct snapshot falls back to monthly quota`() throws { let response = try Self.decodeFixture(