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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 81 additions & 5 deletions Sources/CodexBarCore/CopilotUsageModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,9 +56,17 @@ 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
case creditsUsed = "credits_used"
case percentRemaining = "percent_remaining"
case quotaId = "quota_id"
case unlimited
Expand All @@ -68,11 +77,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
Expand All @@ -89,6 +100,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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve credits on zero-entitlement snapshots

When a token-billed response omits unlimited but reports zero entitlement/remaining—as in the updated fetcher fixture with credits_used: 31QuotaSnapshots.init still classifies the decoded snapshot as a placeholder and removes it. Consequently, response.quotaSnapshots.premiumInteractions is nil and the newly decoded counter remains inaccessible; credit-bearing snapshots need to survive model normalization while percentage rendering stays suppressed.

Useful? React with 👍 / 👎.

let decodedUnlimited = try container.decodeIfPresent(Bool.self, forKey: .unlimited) ?? false
let decodedPercent = Self.decodeNumberIfPresent(container: container, key: .percentRemaining)
if decodedUnlimited {
Expand All @@ -113,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<CodingKeys>,
key: CodingKeys) -> Double?
Expand Down Expand Up @@ -185,10 +234,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
}

Expand All @@ -204,7 +253,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
Expand Down Expand Up @@ -348,8 +397,35 @@ public struct CopilotUsageResponse: Sendable, Decodable {
fallback: QuotaSnapshot?) -> QuotaSnapshot?
{
if direct?.unlimited == true, let fallback = usableQuotaSnapshot(from: fallback) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry credits from all placeholder snapshots

When a credit-bearing direct snapshot has zero entitlement/remaining without unlimited, and usable monthly/limited fallback quotas are also present, this condition is false; the return below rejects the direct placeholder and selects the fallback without its creditsUsed, so CopilotUsageFetcher publishes no counter. Fresh evidence in this revision is the added no-unlimited fixture establishing that this direct snapshot shape is supported, while the new preservation branch still handles only unlimited snapshots.

Useful? React with 👍 / 👎.

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)
}
if let directWindow = self.usableQuotaSnapshot(from: direct) {
return directWindow
}
guard let fallback = self.usableQuotaSnapshot(from: fallback) else {
return nil
Comment on lines +408 to +409

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve credit-only snapshots when the other lane is usable

When one direct lane is a zero-entitlement credit-only placeholder without unlimited and the other lane yields a usable window, this return drops the credit-only lane; the outer initializer then rebuilds quotaSnapshots because the other lane is non-nil, so it never falls back to directSnapshots and the fetcher cannot export that counter. Fresh evidence beyond the earlier same-lane fallback reports is the mixed-lane case, which bypasses the new withCreditsUsed branch even though this revision explicitly supports the no-unlimited credit shape.

Useful? React with 👍 / 👎.

}
return self.usableQuotaSnapshot(from: direct) ?? self.usableQuotaSnapshot(from: fallback)
// 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
}
}

/// 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read credits before rate-window normalization

When a response contains a credit-bearing direct snapshot plus usable monthly_quotas/limited_user_quotas fallback data, CopilotUsageResponse.preferredQuotaSnapshot replaces the placeholder or unlimited direct snapshot with the synthesized rate-window snapshot, which has no creditsUsed; this line then sees nil and drops the real counter. The new preservation guard only covers decoding inside QuotaSnapshots, so extract the counter independently from the raw direct snapshots or carry it through top-level normalization.

Useful? React with 👍 / 👎.

let copilotCredits = creditsUsed.map {
CopilotCreditsSnapshot(creditsUsed: $0, quotaResetDate: resetsAt)
}
let hasUnlimitedQuota = premiumSnapshot?.unlimited == true || chatSnapshot?.unlimited == true

let primary: RateWindow?
Expand Down Expand Up @@ -102,6 +106,7 @@ public struct CopilotUsageFetcher: Sendable {
secondary: secondary,
tertiary: nil,
providerCost: nil,
copilotCredits: copilotCredits,
updatedAt: Date(),
identity: identity)
}
Expand Down
21 changes: 21 additions & 0 deletions Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -153,6 +154,7 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable {
case extraWindowCount
case providerCostPresent
case providerSpecificData
case copilotCredits
}

public init(from snapshot: UsageSnapshot) {
Expand Down Expand Up @@ -187,13 +189,19 @@ 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
self.windows = windows
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 {
Expand All @@ -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
}
}

Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBarCore/UsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
94 changes: 92 additions & 2 deletions Tests/CodexBarTests/CopilotUsageFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,21 @@ struct CopilotUsageFetcherTests {
{
"copilot_plan": "business",
"token_based_billing": true,
"quota_reset_date": "2026-09-01",
"quota_snapshots": {
"premium_interactions": {
"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
}
}
}
Expand All @@ -66,9 +69,96 @@ 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")
}

@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
#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
Expand Down
Loading