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 @@ -16,6 +16,7 @@

### Fixed
- Localization: translate the Default Terminal setting across every supported app language. Thanks @Zihao-Qi!
- z.ai: parse successful BigModel CN quota responses that omit the optional message field, while preserving useful API-code errors. Thanks @joeVenner!
- Claude: block background delegated CLI OAuth refresh when the keychain holds MCP-only state (`mcpOAuth` without `claudeAiOauth`) while preserving explicit Refresh recovery (#1844). Thanks @Yuxin-Qiao!

## 0.38.0 — 2026-07-03
Expand Down
10 changes: 8 additions & 2 deletions Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,19 @@ extension ZaiUsageSnapshot {
/// Z.ai quota limit API response
private struct ZaiQuotaLimitResponse: Decodable {
let code: Int
let msg: String
let msg: String?
let data: ZaiQuotaLimitData?
let success: Bool

var isSuccess: Bool {
self.success && self.code == 200
}

var errorMessage: String {
let message = self.msg?.trimmingCharacters(in: .whitespacesAndNewlines)
if let message, !message.isEmpty { return message }
return "Z.ai quota API returned code \(self.code)"
}
}

private struct ZaiQuotaLimitData: Decodable {
Expand Down Expand Up @@ -462,7 +468,7 @@ public struct ZaiUsageFetcher: Sendable {
let apiResponse = try decoder.decode(ZaiQuotaLimitResponse.self, from: data)

guard apiResponse.isSuccess else {
throw ZaiUsageError.apiError(apiResponse.msg)
throw ZaiUsageError.apiError(apiResponse.errorMessage)
}

guard let responseData = apiResponse.data else {
Expand Down
65 changes: 65 additions & 0 deletions Tests/CodexBarTests/ZaiProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,20 @@ struct ZaiUsageParsingTests {
}
}

@Test
func `failed response without message reports the API code`() {
let json = """
{ "code": 1001, "success": false }
"""

#expect {
_ = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
} throws: { error in
guard case let ZaiUsageError.apiError(message) = error else { return false }
return message == "Z.ai quota API returned code 1001"
}
}

@Test
func `success without data returns parse failed`() {
let json = """
Expand Down Expand Up @@ -387,6 +401,57 @@ struct ZaiUsageParsingTests {
#expect(snapshot.tokenLimit?.windowMinutes == 300)
#expect(snapshot.timeLimit?.usage == 100)
}

@Test
func `parses BigModel CN quota response without message`() throws {
let json = """
{
"code": 200,
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 1000,
"currentValue": 147,
"remaining": 853,
"percentage": 14,
"nextResetTime": 1784706344993,
"usageDetails": [
{ "modelCode": "search-prime", "usage": 84 },
{ "modelCode": "web-reader", "usage": 41 },
{ "modelCode": "zread", "usage": 8 }
]
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 8,
"nextResetTime": 1783049703178
},
{
"type": "TOKENS_LIMIT",
"unit": 6,
"number": 1,
"percentage": 7,
"nextResetTime": 1783496744998
}
],
"level": "pro"
},
"success": true
}
"""

let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
let usage = snapshot.toUsageSnapshot()

#expect(usage.primary?.usedPercent == 7)
#expect(usage.secondary?.usedPercent == 14.7)
#expect(usage.tertiary?.usedPercent == 8)
}
}

struct ZaiBigModelTeamScopeTests {
Expand Down