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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 15 additions & 11 deletions Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)"
Expand Down Expand Up @@ -240,16 +243,17 @@ public struct KimiK2UsageFetcher: Sendable {
}

private static func double(from raw: Any) -> Double? {
if let value = raw as? Double {
return value
}
if let value = raw as? Int {
return Double(value)
}
if let value = raw as? String {
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
}
return nil
guard let value, value.isFinite else { return nil }
return value
}

private static func date(from raw: Any) -> Date? {
Expand Down
28 changes: 28 additions & 0 deletions Tests/CodexBarTests/KimiK2UsageFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ struct KimiK2UsageFetcherTests {
#expect(summary.remaining == 25)
}

@Test
func `fetch ignores non-finite usage values`() async throws {
let json = """
{
"total_credits_consumed": "NaN",
"credits_remaining": "Infinity",
"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 snapshot = try await KimiK2UsageFetcher.fetchUsage(apiKey: "test-key", transport: transport)
let summary = snapshot.summary

#expect(summary.consumed == 0)
#expect(summary.remaining == 0)
#expect(summary.averageTokens == nil)
}

@Test
func `parses numeric timestamp seconds`() throws {
let json = """
Expand Down