From 5d8de56c341516d251230fb71174555b6802ee0e Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Fri, 3 Jul 2026 18:19:35 +0100 Subject: [PATCH 1/2] Fix z.ai BigModel quota parsing --- .../Providers/Zai/ZaiUsageStats.swift | 4 +- Tests/CodexBarTests/ZaiProviderTests.swift | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift index 8430158090..9ec8bf5554 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift @@ -246,7 +246,7 @@ 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 @@ -462,7 +462,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.msg ?? "Unknown error") } guard let responseData = apiResponse.data else { diff --git a/Tests/CodexBarTests/ZaiProviderTests.swift b/Tests/CodexBarTests/ZaiProviderTests.swift index 8231828616..0d7b8e93db 100644 --- a/Tests/CodexBarTests/ZaiProviderTests.swift +++ b/Tests/CodexBarTests/ZaiProviderTests.swift @@ -387,6 +387,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 { From 52f523a615527066f64437b367c030aa1f2e6cbf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Jul 2026 15:10:50 -0700 Subject: [PATCH 2/2] fix: harden z.ai quota response parsing --- CHANGELOG.md | 1 + .../CodexBarCore/Providers/Zai/ZaiUsageStats.swift | 8 +++++++- Tests/CodexBarTests/ZaiProviderTests.swift | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 574840419a..093eef5c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift index 9ec8bf5554..3443271967 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiUsageStats.swift @@ -253,6 +253,12 @@ private struct ZaiQuotaLimitResponse: Decodable { 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 { @@ -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 ?? "Unknown error") + throw ZaiUsageError.apiError(apiResponse.errorMessage) } guard let responseData = apiResponse.data else { diff --git a/Tests/CodexBarTests/ZaiProviderTests.swift b/Tests/CodexBarTests/ZaiProviderTests.swift index 0d7b8e93db..ea46854179 100644 --- a/Tests/CodexBarTests/ZaiProviderTests.swift +++ b/Tests/CodexBarTests/ZaiProviderTests.swift @@ -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 = """