-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Decode Copilot credits_used for token-billed seats (#2593) #2613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0486221
344f432
5203c7f
7ba0ced
c48cbf7
571b1b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
| let decodedUnlimited = try container.decodeIfPresent(Bool.self, forKey: .unlimited) ?? false | ||
| let decodedPercent = Self.decodeNumberIfPresent(container: container, key: .percentRemaining) | ||
| if decodedUnlimited { | ||
|
|
@@ -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? | ||
|
|
@@ -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 | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -348,8 +397,35 @@ public struct CopilotUsageResponse: Sendable, Decodable { | |
| fallback: QuotaSnapshot?) -> QuotaSnapshot? | ||
| { | ||
| if direct?.unlimited == true, let fallback = usableQuotaSnapshot(from: fallback) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a credit-bearing direct snapshot has zero entitlement/remaining without 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When one direct lane is a zero-entitlement credit-only placeholder without 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 |
|---|---|---|
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a response contains a credit-bearing direct snapshot plus usable Useful? React with 👍 / 👎. |
||
| 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) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a token-billed response omits
unlimitedbut reports zero entitlement/remaining—as in the updated fetcher fixture withcredits_used: 31—QuotaSnapshots.initstill classifies the decoded snapshot as a placeholder and removes it. Consequently,response.quotaSnapshots.premiumInteractionsisniland the newly decoded counter remains inaccessible; credit-bearing snapshots need to survive model normalization while percentage rendering stays suppressed.Useful? React with 👍 / 👎.