Skip to content

Commit dcccce8

Browse files
steipeteskyzer
andauthored
Fix Kimi subscription quota rows (#1953)
Update Kimi membership quota enrichment to the current endpoint, add the Code 7-day quota, harden ratio validation, and preserve compact widget fit. Co-authored-by: skyzer <skyzer@gmail.com>
1 parent 72aaaf3 commit dcccce8

8 files changed

Lines changed: 158 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66
- Widgets: make Kimi available with Weekly, Rate Limit, and Monthly quota rows. Thanks @joeVenner!
7+
- Kimi: show the subscription 7-day Code quota in menus and large widgets. Thanks @skyzer!
78

89
### Fixed
910
- Amp: open the current Usage page from the menu dashboard action. Thanks @3kh0!
@@ -18,6 +19,7 @@
1819
- Devin: keep exact 1% usage from being inflated to 100% while preserving fractional fallback quota semantics. Thanks @Lex-ic-on!
1920
- Kimi K2: reject invalid and out-of-range numeric timestamps while preserving valid second and millisecond values. Thanks @joeVenner!
2021
- Kimi K2: reject non-finite credit and token values before they reach menus, CLI output, or widgets. Thanks @joeVenner!
22+
- Kimi: call the current `GetSubscriptionStats` membership endpoint so the Monthly subscription quota is populated again. Thanks @skyzer!
2123
- Kimi: show the five-hour rate limit before the weekly quota while preserving existing menu-bar metric preferences. Thanks @Zihao-Qi!
2224

2325
## 0.40.0 — 2026-07-05

Sources/CodexBar/UsageStore+WidgetSnapshot.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,17 @@ extension UsageStore {
181181
title: metadata?.opusLabel ?? "Opus",
182182
percentLeft: snapshot.tertiary?.remainingPercent))
183183
}
184-
if provider == .kimi,
185-
let monthly = snapshot.extraRateWindows?.first(where: { $0.id == "kimi-monthly" }),
186-
monthly.usageKnown
187-
{
188-
rows.append(WidgetSnapshot.WidgetUsageRowSnapshot(
189-
id: monthly.id,
190-
title: monthly.title,
191-
percentLeft: monthly.window.remainingPercent))
184+
if provider == .kimi {
185+
// Keep persisted widget order stable and include only Kimi's intentional subscription lanes.
186+
let kimiWindowIDs = ["kimi-monthly", "kimi-code-7d"]
187+
rows.append(contentsOf: kimiWindowIDs.compactMap { id in
188+
guard let window = snapshot.extraRateWindows?.first(where: { $0.id == id }), window.usageKnown
189+
else { return nil }
190+
return WidgetSnapshot.WidgetUsageRowSnapshot(
191+
id: window.id,
192+
title: window.title,
193+
percentLeft: window.window.remainingPercent)
194+
})
192195
}
193196
return rows.filter { $0.percentLeft != nil }
194197
}

Sources/CodexBarCore/Providers/Kimi/KimiModels.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ struct KimiCodeAPIUsageResponse: Codable {
99
let limits: [KimiRateLimit]?
1010
}
1111

12-
struct KimiSubscriptionStatResponse: Codable {
12+
struct KimiSubscriptionStatsResponse: Codable {
1313
let subscriptionBalance: KimiSubscriptionBalance?
14+
let ratelimitCode7d: KimiSubscriptionRateLimit?
1415
}
1516

1617
struct KimiSubscriptionBalance: Codable, Sendable {
@@ -20,6 +21,12 @@ struct KimiSubscriptionBalance: Codable, Sendable {
2021
let expireTime: String?
2122
}
2223

24+
struct KimiSubscriptionRateLimit: Codable, Sendable {
25+
let ratio: Double?
26+
let enabled: Bool?
27+
let resetTime: String?
28+
}
29+
2330
struct KimiUsage: Codable {
2431
let scope: String
2532
let detail: KimiUsageDetail

Sources/CodexBarCore/Providers/Kimi/KimiUsageFetcher.swift

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public struct KimiUsageFetcher: Sendable {
99
private static let subscriptionGraceSeconds: TimeInterval = 2
1010
private static let usageURL =
1111
URL(string: "https://www.kimi.com/apiv2/kimi.gateway.billing.v1.BillingService/GetUsages")!
12-
private static let subscriptionStatURL =
13-
URL(string: "https://www.kimi.com/apiv2/kimi.gateway.membership.v2.MembershipService/GetSubscriptionStat")!
12+
private static let subscriptionStatsURL =
13+
URL(string: "https://www.kimi.com/apiv2/kimi.gateway.membership.v2.MembershipService/GetSubscriptionStats")!
1414

1515
public static func fetchCodeAPIUsage(
1616
apiKey: String,
@@ -83,8 +83,8 @@ public struct KimiUsageFetcher: Sendable {
8383
{
8484
let sessionInfo = self.decodeSessionInfo(from: authToken)
8585

86-
let subscriptionTask = Task<KimiSubscriptionStatResponse?, Error> {
87-
try await self.fetchSubscriptionStat(
86+
let subscriptionTask = Task<KimiSubscriptionStatsResponse?, Error> {
87+
try await self.fetchSubscriptionStats(
8888
authToken: authToken,
8989
sessionInfo: sessionInfo,
9090
transport: transport)
@@ -117,22 +117,23 @@ public struct KimiUsageFetcher: Sendable {
117117
}
118118
try Task.checkCancellation()
119119

120-
let subscriptionStat: KimiSubscriptionStatResponse?
120+
let subscriptionStats: KimiSubscriptionStatsResponse?
121121
switch subscriptionOutcome {
122122
case let .value(response):
123-
subscriptionStat = response
123+
subscriptionStats = response
124124
case .timedOut:
125-
Self.log.warning("Kimi subscription stat timed out")
126-
subscriptionStat = nil
125+
Self.log.warning("Kimi subscription stats timed out")
126+
subscriptionStats = nil
127127
case let .failure(error):
128-
Self.log.warning("Kimi subscription stat unavailable: \(error.localizedDescription)")
129-
subscriptionStat = nil
128+
Self.log.warning("Kimi subscription stats unavailable: \(error.localizedDescription)")
129+
subscriptionStats = nil
130130
}
131131

132132
return KimiUsageSnapshot(
133133
weekly: codingUsage.detail,
134134
rateLimit: codingUsage.limits?.first?.detail,
135-
subscriptionBalance: subscriptionStat?.subscriptionBalance,
135+
subscriptionBalance: subscriptionStats?.subscriptionBalance,
136+
subscriptionCodeWeeklyLimit: subscriptionStats?.ratelimitCode7d,
136137
updatedAt: now)
137138
}
138139

@@ -196,26 +197,26 @@ public struct KimiUsageFetcher: Sendable {
196197
.appendingPathComponent("usages")
197198
}
198199

199-
private static func fetchSubscriptionStat(
200+
private static func fetchSubscriptionStats(
200201
authToken: String,
201202
sessionInfo: SessionInfo?,
202-
transport: any ProviderHTTPTransport) async throws -> KimiSubscriptionStatResponse?
203+
transport: any ProviderHTTPTransport) async throws -> KimiSubscriptionStatsResponse?
203204
{
204-
var request = self.webRequest(url: self.subscriptionStatURL, authToken: authToken, sessionInfo: sessionInfo)
205+
var request = self.webRequest(url: self.subscriptionStatsURL, authToken: authToken, sessionInfo: sessionInfo)
205206
request.httpBody = Data("{}".utf8)
206207

207208
do {
208209
let response = try await transport.response(for: request)
209210
guard response.statusCode == 200 else {
210-
Self.log.warning("Kimi subscription stat returned \(response.statusCode)")
211+
Self.log.warning("Kimi subscription stats returned \(response.statusCode)")
211212
return nil
212213
}
213-
return try JSONDecoder().decode(KimiSubscriptionStatResponse.self, from: response.data)
214+
return try JSONDecoder().decode(KimiSubscriptionStatsResponse.self, from: response.data)
214215
} catch {
215216
if error is CancellationError || (error as? URLError)?.code == .cancelled || Task.isCancelled {
216217
throw CancellationError()
217218
}
218-
Self.log.warning("Kimi subscription stat unavailable: \(error.localizedDescription)")
219+
Self.log.warning("Kimi subscription stats unavailable: \(error.localizedDescription)")
219220
return nil
220221
}
221222
}

Sources/CodexBarCore/Providers/Kimi/KimiUsageSnapshot.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ public struct KimiUsageSnapshot: Sendable {
55
public let rateLimit: KimiUsageDetail?
66
public let updatedAt: Date
77
let subscriptionBalance: KimiSubscriptionBalance?
8+
let subscriptionCodeWeeklyLimit: KimiSubscriptionRateLimit?
89

910
public init(weekly: KimiUsageDetail, rateLimit: KimiUsageDetail?, updatedAt: Date) {
1011
self.weekly = weekly
1112
self.rateLimit = rateLimit
1213
self.updatedAt = updatedAt
1314
self.subscriptionBalance = nil
15+
self.subscriptionCodeWeeklyLimit = nil
1416
}
1517

1618
init(
1719
weekly: KimiUsageDetail,
1820
rateLimit: KimiUsageDetail?,
1921
subscriptionBalance: KimiSubscriptionBalance?,
22+
subscriptionCodeWeeklyLimit: KimiSubscriptionRateLimit? = nil,
2023
updatedAt: Date)
2124
{
2225
self.weekly = weekly
2326
self.rateLimit = rateLimit
2427
self.subscriptionBalance = subscriptionBalance
28+
self.subscriptionCodeWeeklyLimit = subscriptionCodeWeeklyLimit
2529
self.updatedAt = updatedAt
2630
}
2731

@@ -91,7 +95,7 @@ extension KimiUsageSnapshot {
9195
// the pool is shared across features, so amountUsedRatio is the real "subscription remaining".
9296
guard balance.feature == nil || balance.feature == "FEATURE_OMNI" else { return nil }
9397
guard balance.type == nil || balance.type == "SUBSCRIPTION" else { return nil }
94-
guard let ratio = balance.amountUsedRatio else { return nil }
98+
guard let ratio = balance.amountUsedRatio, ratio.isFinite else { return nil }
9599
let window = RateWindow(
96100
usedPercent: Self.clampedPercent(ratio * 100),
97101
windowMinutes: nil,
@@ -100,6 +104,19 @@ extension KimiUsageSnapshot {
100104
return NamedRateWindow(id: "kimi-monthly", title: "Monthly", window: window)
101105
}
102106

107+
let subscriptionCodeWeeklyWindow = self.subscriptionCodeWeeklyLimit.flatMap { limit -> NamedRateWindow? in
108+
guard limit.enabled != false else { return nil }
109+
guard let ratio = limit.ratio, ratio.isFinite else { return nil }
110+
let window = RateWindow(
111+
usedPercent: Self.clampedPercent(ratio * 100),
112+
windowMinutes: 7 * 24 * 60,
113+
resetsAt: Self.parseDate(limit.resetTime),
114+
resetDescription: nil)
115+
return NamedRateWindow(id: "kimi-code-7d", title: "Code 7-day", window: window)
116+
}
117+
118+
let extraRateWindows = [monthlyWindow, subscriptionCodeWeeklyWindow].compactMap(\.self)
119+
103120
let identity = ProviderIdentitySnapshot(
104121
providerID: .kimi,
105122
accountEmail: nil,
@@ -110,7 +127,7 @@ extension KimiUsageSnapshot {
110127
primary: weeklyWindow,
111128
secondary: rateLimitWindow,
112129
tertiary: nil,
113-
extraRateWindows: monthlyWindow.map { [$0] },
130+
extraRateWindows: extraRateWindows.isEmpty ? nil : extraRateWindows,
114131
providerCost: nil,
115132
updatedAt: self.updatedAt,
116133
identity: identity)

Tests/CodexBarTests/CodexBarWidgetProviderTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ struct CodexBarWidgetProviderTests {
376376
}
377377

378378
@Test
379-
func `small and medium Kimi widgets keep all three persisted lane rows`() {
379+
func `compact Kimi widgets keep established row fit while large widgets show all quotas`() {
380380
let now = Date(timeIntervalSince1970: 1_700_000_000)
381381
let entry = WidgetSnapshot.ProviderEntry(
382382
provider: .kimi,
@@ -388,6 +388,7 @@ struct CodexBarWidgetProviderTests {
388388
WidgetSnapshot.WidgetUsageRowSnapshot(id: "primary", title: "Weekly", percentLeft: 75),
389389
WidgetSnapshot.WidgetUsageRowSnapshot(id: "secondary", title: "Rate Limit", percentLeft: 50),
390390
WidgetSnapshot.WidgetUsageRowSnapshot(id: "kimi-monthly", title: "Monthly", percentLeft: 25),
391+
WidgetSnapshot.WidgetUsageRowSnapshot(id: "kimi-code-7d", title: "Code 7-day", percentLeft: 90),
391392
],
392393
creditsRemaining: nil,
393394
codeReviewRemainingPercent: nil,
@@ -400,11 +401,13 @@ struct CodexBarWidgetProviderTests {
400401
let mediumRows = WidgetUsageRow.rows(
401402
for: entry,
402403
limit: WidgetUsageRow.mediumWidgetRowLimit(for: entry))
404+
let largeRows = WidgetUsageRow.rows(for: entry)
403405

404406
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == 3)
405407
#expect(WidgetUsageRow.mediumWidgetRowLimit(for: entry) == 3)
406408
#expect(smallRows.map(\.id) == ["primary", "secondary", "kimi-monthly"])
407409
#expect(mediumRows == smallRows)
410+
#expect(largeRows.map(\.id) == ["primary", "secondary", "kimi-monthly", "kimi-code-7d"])
408411
}
409412

410413
@Test

Tests/CodexBarTests/KimiProviderTests.swift

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,12 +426,15 @@ struct KimiUsageResponseParsingTests {
426426
}
427427
"""
428428

429-
let response = try JSONDecoder().decode(KimiSubscriptionStatResponse.self, from: Data(json.utf8))
429+
let response = try JSONDecoder().decode(KimiSubscriptionStatsResponse.self, from: Data(json.utf8))
430430

431431
#expect(response.subscriptionBalance?.feature == "FEATURE_OMNI")
432432
#expect(response.subscriptionBalance?.type == "SUBSCRIPTION")
433433
#expect(response.subscriptionBalance?.amountUsedRatio == 1)
434434
#expect(response.subscriptionBalance?.expireTime == "2026-07-23T00:00:00Z")
435+
#expect(response.ratelimitCode7d?.ratio == 0.0946)
436+
#expect(response.ratelimitCode7d?.enabled == true)
437+
#expect(response.ratelimitCode7d?.resetTime == "2026-07-09T06:56:36.876796734Z")
435438
}
436439

437440
@Test
@@ -511,6 +514,11 @@ struct KimiUsageResponseParsingTests {
511514
"type": "SUBSCRIPTION",
512515
"amountUsedRatio": 0.42,
513516
"expireTime": "2026-07-23T00:00:00Z"
517+
},
518+
"ratelimitCode7d": {
519+
"ratio": 0.17,
520+
"enabled": true,
521+
"resetTime": "2026-07-13T15:28:00Z"
514522
}
515523
}
516524
"""
@@ -524,17 +532,23 @@ struct KimiUsageResponseParsingTests {
524532
if url.path.hasSuffix("/GetUsages") {
525533
return (Data(usageJSON.utf8), response)
526534
}
535+
#expect(url.path.hasSuffix("/GetSubscriptionStats"))
527536
return (Data(subscriptionJSON.utf8), response)
528537
}
529538

530539
let snapshot = try await KimiUsageFetcher._fetchUsageForTesting(
531540
authToken: "test-token",
532541
transport: transport,
533542
subscriptionGrace: .seconds(1))
534-
let monthly = try #require(snapshot.toUsageSnapshot().extraRateWindows?.first)
543+
let windows = try #require(snapshot.toUsageSnapshot().extraRateWindows)
544+
let monthly = try #require(windows.first { $0.id == "kimi-monthly" })
545+
let weeklyCode = try #require(windows.first { $0.id == "kimi-code-7d" })
535546

536547
#expect(monthly.id == "kimi-monthly")
537548
#expect(monthly.window.usedPercent == 42)
549+
#expect(weeklyCode.title == "Code 7-day")
550+
#expect(weeklyCode.window.usedPercent == 17)
551+
#expect(weeklyCode.window.windowMinutes == 7 * 24 * 60)
538552
}
539553

540554
@Test
@@ -723,6 +737,64 @@ struct KimiUsageSnapshotConversionTests {
723737
#expect(abs(monthly.window.usedPercent - 77.16) < 0.0001)
724738
}
725739

740+
@Test
741+
func `converts subscription code weekly limit to extra window`() throws {
742+
let now = Date()
743+
let weeklyDetail = KimiUsageDetail(
744+
limit: "2048",
745+
used: "375",
746+
remaining: "1673",
747+
resetTime: "2026-01-09T15:23:13.373329235Z")
748+
let subscriptionCodeWeeklyLimit = KimiSubscriptionRateLimit(
749+
ratio: 0.0946,
750+
enabled: true,
751+
resetTime: "2026-07-13T15:28:00Z")
752+
753+
let snapshot = KimiUsageSnapshot(
754+
weekly: weeklyDetail,
755+
rateLimit: nil,
756+
subscriptionBalance: nil,
757+
subscriptionCodeWeeklyLimit: subscriptionCodeWeeklyLimit,
758+
updatedAt: now)
759+
let usageSnapshot = snapshot.toUsageSnapshot()
760+
761+
let weeklyCode = try #require(usageSnapshot.extraRateWindows?.first)
762+
#expect(weeklyCode.id == "kimi-code-7d")
763+
#expect(weeklyCode.title == "Code 7-day")
764+
#expect(abs(weeklyCode.window.usedPercent - 9.46) < 0.0001)
765+
#expect(weeklyCode.window.windowMinutes == 7 * 24 * 60)
766+
#expect(weeklyCode.window.resetsAt == Self.date("2026-07-13T15:28:00Z"))
767+
}
768+
769+
@Test
770+
func `omits disabled and nonfinite subscription quota ratios`() {
771+
let weeklyDetail = KimiUsageDetail(
772+
limit: "2048",
773+
used: "375",
774+
remaining: "1673",
775+
resetTime: "2026-01-09T15:23:13.373329235Z")
776+
let invalidLimits = [
777+
KimiSubscriptionRateLimit(ratio: 0.25, enabled: false, resetTime: nil),
778+
KimiSubscriptionRateLimit(ratio: .nan, enabled: true, resetTime: nil),
779+
KimiSubscriptionRateLimit(ratio: .infinity, enabled: true, resetTime: nil),
780+
]
781+
782+
for limit in invalidLimits {
783+
let snapshot = KimiUsageSnapshot(
784+
weekly: weeklyDetail,
785+
rateLimit: nil,
786+
subscriptionBalance: KimiSubscriptionBalance(
787+
feature: "FEATURE_OMNI",
788+
type: "SUBSCRIPTION",
789+
amountUsedRatio: .nan,
790+
expireTime: nil),
791+
subscriptionCodeWeeklyLimit: limit,
792+
updatedAt: Date())
793+
794+
#expect(snapshot.toUsageSnapshot().extraRateWindows == nil)
795+
}
796+
}
797+
726798
@Test
727799
func `converts to usage snapshot without rate limit`() {
728800
let now = Date()

0 commit comments

Comments
 (0)