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 @@ -10,6 +10,7 @@
### Fixed
- Codex: accept the first click in the account switcher inside menu popovers (#1079). Thanks @ptstory!
- Codex/Claude: terminate PTY child process trees during probe cleanup so wrapper-launched CLI descendants do not linger after sessions finish (#1085). Thanks @mickobizzle!
- MiniMax: exclude explicitly failed billing-history records from token charts and model/method totals (#1089). Thanks @Yuxin-Qiao!
- OpenAI: parse Wednesday and Saturday dashboard reset lines so rate-limit reset times are not dropped on those days (#1080). Thanks @m1qaweb!
- Localization: translate provider-detail labels and empty states when Simplified Chinese is selected (#1051). Thanks @wang93wei!
- Antigravity: discover OAuth credentials from the bundled extension language server in newer IDE builds so Add Account works again (#1076). Thanks @xARSENICx!
Expand Down
41 changes: 41 additions & 0 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxBillingHistory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ struct MiniMaxBillingRecord: Decodable {
let consumeTime: String?
let method: String?
let model: String?
let result: String?
let status: String?

private enum CodingKeys: String, CodingKey {
case consumeToken = "consume_token"
Expand All @@ -97,6 +99,8 @@ struct MiniMaxBillingRecord: Decodable {
case consumeTime = "consume_time"
case method
case model
case result
case status
}

init(from decoder: Decoder) throws {
Expand All @@ -111,6 +115,18 @@ struct MiniMaxBillingRecord: Decodable {
self.consumeTime = try container.decodeIfPresent(String.self, forKey: .consumeTime)
self.method = try container.decodeIfPresent(String.self, forKey: .method)
self.model = try container.decodeIfPresent(String.self, forKey: .model)
self.result = Self.decodeOptionalScalarString(container, forKey: .result)
self.status = Self.decodeOptionalScalarString(container, forKey: .status)
}

var recordResult: String? {
if let result = self.result?.trimmingCharacters(in: .whitespacesAndNewlines), !result.isEmpty {
return result
}
if let status = self.status?.trimmingCharacters(in: .whitespacesAndNewlines), !status.isEmpty {
return status
}
return nil
}

var tokenCount: Int {
Expand All @@ -121,6 +137,25 @@ struct MiniMaxBillingRecord: Decodable {
var cashValue: Double? {
self.consumeCashAfterVoucher ?? self.consumeCash
}

private static func decodeOptionalScalarString<K: CodingKey>(
_ container: KeyedDecodingContainer<K>,
forKey key: K) -> String?
{
if let value = try? container.decodeIfPresent(String.self, forKey: key) {
return value
}
if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
return String(value)
}
if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
return String(value)
}
if let value = try? container.decodeIfPresent(Bool.self, forKey: key) {
return String(value)
}
return nil
}
}

enum MiniMaxBillingHistoryParser {
Expand Down Expand Up @@ -159,6 +194,12 @@ enum MiniMaxBillingHistoryParser {
var modelTotals: [String: (tokens: Int, cash: Double, hasCash: Bool)] = [:]

for record in records {
if let recordResult = record.recordResult,
recordResult.caseInsensitiveCompare("SUCCESS") != .orderedSame
{
Comment on lines +197 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle numeric success statuses before filtering

The new filter only keeps rows where recordResult equals SUCCESS, but this commit also decodes numeric status values into strings via decodeOptionalScalarString; as a result, records with status: 0 are always treated as non-success and skipped. If MiniMax emits numeric status codes (the same payload format already uses base_resp.status_code == 0 for success), successful billing rows will be dropped from token and breakdown aggregates, causing persistent undercounting.

Useful? React with 👍 / 👎.

continue
}

guard let date = self.recordDate(record, calendar: calendar),
date >= startOf30Days,
date <= now
Expand Down
67 changes: 67 additions & 0 deletions Tests/CodexBarTests/MiniMaxProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,73 @@ struct MiniMaxUsageParserTests {
#expect(summary.daily.map(\.day) == ["2026-05-17"])
}

@Test
func `billing history filters failed records`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let json = """
{
"base_resp": { "status_code": 0 },
"total_cnt": 5,
"charge_records": [
{
"consume_token": 1000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"result": "SUCCESS"
},
{
"consume_token": 2000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"result": "FAILED"
},
{
"consume_token": 3000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"status": "fail"
},
{
"consume_token": 4000,
"ymd": "2026-05-17",
"method": "audio",
"model": "speech-2.8"
},
{
"consume_token": 5000,
"ymd": "2026-05-17",
"method": "video",
"model": "video-1",
"status": 0
}
]
}
"""

let summary = try MiniMaxBillingHistoryParser.parse(
data: Data(json.utf8),
now: now,
calendar: calendar)

// Only SUCCESS (1000) and missing/empty result status (4000) should be included.
// FAILED (2000), status "fail" (3000), and numeric status 0 (5000) should be skipped.
#expect(summary.todayTokens == 5000)
#expect(summary.last30DaysTokens == 5000)
#expect(summary.daily.map(\.day) == ["2026-05-17"])

// Top methods should aggregate only SUCCESS/missing records.
#expect(summary.topMethods.count == 2)
#expect(summary.topMethods[0].name == "audio")
#expect(summary.topMethods[0].tokens == 4000)
#expect(summary.topMethods[1].name == "chat")
#expect(summary.topMethods[1].tokens == 1000)
}

@Test
func `web usage fetch attaches billing history when available`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
Expand Down