From 29cef7b20a308f7b54bc8a770e482cacbfd60ab5 Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Sun, 5 Jul 2026 17:03:10 +0100 Subject: [PATCH 1/2] Reject nonfinite Kimi K2 usage --- .../Providers/KimiK2/KimiK2UsageFetcher.swift | 16 +++++++++++----- .../CodexBarTests/KimiK2UsageFetcherTests.swift | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift index 22f9255e64..8222715143 100644 --- a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift @@ -113,7 +113,10 @@ public struct KimiK2UsageFetcher: Sendable { ["lastUpdated"], ] - public static func fetchUsage(apiKey: String) async throws -> KimiK2UsageSnapshot { + public static func fetchUsage( + apiKey: String, + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> KimiK2UsageSnapshot + { guard !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw KimiK2UsageError.missingCredentials } @@ -123,7 +126,7 @@ public struct KimiK2UsageFetcher: Sendable { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") - let response = try await ProviderHTTPClient.shared.response(for: request) + let response = try await transport.response(for: request) let data = response.data guard response.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "HTTP \(response.statusCode)" @@ -241,13 +244,16 @@ public struct KimiK2UsageFetcher: Sendable { private static func double(from raw: Any) -> Double? { if let value = raw as? Double { - return value + return value.isFinite ? value : nil } if let value = raw as? Int { return Double(value) } - if let value = raw as? String { - return Double(value) + if let value = raw as? String, + let parsed = Double(value), + parsed.isFinite + { + return parsed } return nil } diff --git a/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift b/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift index 9b549bfbf7..552bf6196b 100644 --- a/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift +++ b/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift @@ -40,6 +40,23 @@ struct KimiK2UsageFetcherTests { #expect(summary.remaining == 25) } + @Test + func `ignores nonfinite usage values`() throws { + let json = """ + { + "total_credits_consumed": "NaN", + "credits_remaining": "Infinity", + "average_tokens": "1e309" + } + """ + + let summary = try KimiK2UsageFetcher._parseSummaryForTesting(Data(json.utf8)) + + #expect(summary.consumed == 0) + #expect(summary.remaining == 0) + #expect(summary.averageTokens == nil) + } + @Test func `parses numeric timestamp seconds`() throws { let json = """ From 909b415a751eb7e34af141a8a8db80e989442219 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Jul 2026 09:13:30 +0100 Subject: [PATCH 2/2] fix: harden Kimi K2 numeric parsing --- CHANGELOG.md | 1 + .../Providers/KimiK2/KimiK2UsageFetcher.swift | 22 +++++++++---------- .../KimiK2UsageFetcherTests.swift | 15 +++++++++++-- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7784b45aab..64fee9599d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed - Codex menu: hide error-only optional Credits and OpenAI web setup diagnostics while keeping them visible in provider Settings. - Codex cost history: reuse cached aggregate pricing and one pricing catalog across daily and project reports, carry fresh cache state across launches, and treat unpriced models as migrated, avoiding repeated row scans, filesystem work, and duplicate background scans on large local histories. +- Kimi K2: reject non-finite credit and token values before they reach menus, CLI output, or widgets. Thanks @joeVenner! - Kimi: show the five-hour rate limit before the weekly quota while preserving existing menu-bar metric preferences. Thanks @Zihao-Qi! ## 0.40.0 — 2026-07-05 diff --git a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift index 8222715143..70536e649b 100644 --- a/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift @@ -243,19 +243,17 @@ public struct KimiK2UsageFetcher: Sendable { } private static func double(from raw: Any) -> Double? { - if let value = raw as? Double { - return value.isFinite ? value : nil - } - if let value = raw as? Int { - return Double(value) + let value: Double? = if let raw = raw as? Double { + raw + } else if let raw = raw as? Int { + Double(raw) + } else if let raw = raw as? String { + Double(raw) + } else { + nil } - if let value = raw as? String, - let parsed = Double(value), - parsed.isFinite - { - return parsed - } - return nil + guard let value, value.isFinite else { return nil } + return value } private static func date(from raw: Any) -> Date? { diff --git a/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift b/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift index 552bf6196b..b38179bf41 100644 --- a/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift +++ b/Tests/CodexBarTests/KimiK2UsageFetcherTests.swift @@ -41,7 +41,7 @@ struct KimiK2UsageFetcherTests { } @Test - func `ignores nonfinite usage values`() throws { + func `fetch ignores non-finite usage values`() async throws { let json = """ { "total_credits_consumed": "NaN", @@ -49,8 +49,19 @@ struct KimiK2UsageFetcherTests { "average_tokens": "1e309" } """ + let transport = ProviderHTTPTransportHandler { request in + let url = try #require(request.url) + #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-key") + let response = try #require(HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: ["X-Credits-Remaining": "-Infinity"])) + return (Data(json.utf8), response) + } - let summary = try KimiK2UsageFetcher._parseSummaryForTesting(Data(json.utf8)) + let snapshot = try await KimiK2UsageFetcher.fetchUsage(apiKey: "test-key", transport: transport) + let summary = snapshot.summary #expect(summary.consumed == 0) #expect(summary.remaining == 0)