Skip to content

Commit 170e32f

Browse files
authored
Merge pull request #112 from opgginc/codex-minimax-percent-display-fix
Fix MiniMax usage display for active quota rows
2 parents ddd9bc3 + 1fdffeb commit 170e32f

File tree

6 files changed

+160
-13
lines changed

6 files changed

+160
-13
lines changed

CopilotMonitor/CopilotMonitor/App/StatusBarController.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,12 +1232,12 @@ final class StatusBarController: NSObject {
12321232
logger.debug(
12331233
"Recent change percent resolved: provider=\(candidate.identifier.displayName), percent=\(String(format: "%.2f", percent))"
12341234
)
1235-
return String(format: "%.0f%%", percent)
1235+
return UsagePercentDisplayFormatter.string(from: percent)
12361236
}
12371237
}
12381238

12391239
private func formatAlertText(identifier _: ProviderIdentifier, usedPercent: Double) -> String {
1240-
return String(format: "%.0f%%", usedPercent)
1240+
return UsagePercentDisplayFormatter.string(from: usedPercent)
12411241
}
12421242

12431243
private func formatProviderForStatusBar(identifier: ProviderIdentifier, result: ProviderResult) -> String {
@@ -1247,7 +1247,7 @@ final class StatusBarController: NSObject {
12471247
return costText
12481248
case .quotaBased:
12491249
let maxPercent = preferredUsedPercentForStatusBar(identifier: identifier, result: result) ?? result.usage.usagePercentage
1250-
let usageText = String(format: "%.0f%%", maxPercent)
1250+
let usageText = UsagePercentDisplayFormatter.string(from: maxPercent)
12511251
return usageText
12521252
}
12531253
}
@@ -2413,7 +2413,7 @@ final class StatusBarController: NSObject {
24132413
))
24142414

24152415
for (index, percent) in usedPercents.enumerated() {
2416-
let percentText = String(format: "%.0f%%", percent)
2416+
let percentText = UsagePercentDisplayFormatter.string(from: percent)
24172417
let percentColor = isEnabled ? colorForUsagePercent(percent) : NSColor.disabledControlTextColor
24182418
let font: NSFont = isEnabled && percent >= 100
24192419
? MenuDesignToken.Typography.monospacedBoldFont

CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1391,7 +1391,7 @@ extension StatusBarController {
13911391
attributes: [.font: NSFont.boldSystemFont(ofSize: headerFontSize), .foregroundColor: NSColor.disabledControlTextColor]
13921392
))
13931393
rightAttributedString.append(NSAttributedString(
1394-
string: String(format: "%.0f%%", usagePercent),
1394+
string: UsagePercentDisplayFormatter.string(from: usagePercent),
13951395
attributes: [.font: NSFont.boldSystemFont(ofSize: headerFontSize), .foregroundColor: valueColor]
13961396
))
13971397
rightTextField.attributedStringValue = rightAttributedString

CopilotMonitor/CopilotMonitor/Models/ProviderResult.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import Foundation
22

3+
enum UsagePercentDisplayFormatter {
4+
static func string(from percent: Double) -> String {
5+
let normalized = min(max(percent, 0.0), 999.0)
6+
if normalized > 0.0, normalized < 1.0 {
7+
return "1%"
8+
}
9+
return String(format: "%.0f%%", normalized)
10+
}
11+
12+
static func wholePercent(from percent: Double) -> Int {
13+
let normalized = min(max(percent, 0.0), 100.0)
14+
if normalized > 0.0, normalized < 1.0 {
15+
return 1
16+
}
17+
return Int(normalized.rounded())
18+
}
19+
}
20+
321
struct ProviderResult {
422
let usage: ProviderUsage
523
let details: DetailedUsage?
@@ -600,7 +618,7 @@ struct TableFormatter {
600618
guard entitlement > 0 else { return "0%" }
601619
let used = entitlement - remaining
602620
let percentage = (Double(used) / Double(entitlement)) * 100
603-
return String(format: "%.0f%%", percentage)
621+
return UsagePercentDisplayFormatter.string(from: percentage)
604622
}
605623

606624
private static func formatQuotaMetrics(remaining: Int, entitlement: Int, overagePermitted: Bool) -> String {
@@ -789,14 +807,14 @@ struct TableFormatter {
789807
if identifier == .minimaxCodingPlan {
790808
let percents = [result.details?.fiveHourUsage, result.details?.sevenDayUsage].compactMap { $0 }
791809
if percents.count == 2 {
792-
return percents.map { String(format: "%.0f%%", $0) }.joined(separator: ",")
810+
return percents.map { UsagePercentDisplayFormatter.string(from: $0) }.joined(separator: ",")
793811
}
794812
}
795813
// Z.AI: show both token and MCP percentages when both are available
796814
if identifier == .zaiCodingPlan {
797815
let percents = [result.details?.tokenUsagePercent, result.details?.mcpUsagePercent].compactMap { $0 }
798816
if percents.count == 2 {
799-
return percents.map { String(format: "%.0f%%", $0) }.joined(separator: ",")
817+
return percents.map { UsagePercentDisplayFormatter.string(from: $0) }.joined(separator: ",")
800818
}
801819
}
802820
switch result.usage {
@@ -812,7 +830,7 @@ struct TableFormatter {
812830
let label = geminiLabel(account: account)
813831
let providerPadded = label.padding(toLength: providerWidth, withPad: " ", startingAt: 0)
814832
let typePadded = "Quota-based".padding(toLength: typeWidth, withPad: " ", startingAt: 0)
815-
let usageStr = String(format: "%.0f%%", 100 - account.remainingPercentage)
833+
let usageStr = UsagePercentDisplayFormatter.string(from: 100 - account.remainingPercentage)
816834
let usagePadded = usageStr.padding(toLength: usageWidth, withPad: " ", startingAt: 0)
817835

818836
let metricsStr: String

CopilotMonitor/CopilotMonitor/Providers/MiniMaxProvider.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ final class MiniMaxProvider: ProviderProtocol {
171171
}
172172

173173
let overallUsed = max(fiveHourUsage ?? 0, weeklyUsage ?? 0)
174-
let remainingPercent = Int((100.0 - overallUsed).rounded())
174+
let aggregateUsedPercent = UsagePercentDisplayFormatter.wholePercent(from: overallUsed)
175+
let remainingPercent = max(0, 100 - aggregateUsedPercent)
175176

176177
let usage = ProviderUsage.quotaBased(
177178
remaining: remainingPercent,
@@ -256,13 +257,19 @@ final class MiniMaxProvider: ProviderProtocol {
256257
guard !quotaRows.isEmpty else { return nil }
257258

258259
return quotaRows.max { lhs, rhs in
260+
let lhsPercent = max(lhs.fiveHourUsagePercent ?? 0, lhs.weeklyUsagePercent ?? 0)
261+
let rhsPercent = max(rhs.fiveHourUsagePercent ?? 0, rhs.weeklyUsagePercent ?? 0)
262+
if lhsPercent != rhsPercent {
263+
return lhsPercent < rhsPercent
264+
}
265+
259266
if lhs.quotaScore != rhs.quotaScore {
260267
return lhs.quotaScore < rhs.quotaScore
261268
}
262269

263-
let lhsPercent = max(lhs.fiveHourUsagePercent ?? 0, lhs.weeklyUsagePercent ?? 0)
264-
let rhsPercent = max(rhs.fiveHourUsagePercent ?? 0, rhs.weeklyUsagePercent ?? 0)
265-
return lhsPercent < rhsPercent
270+
let lhsName = lhs.modelName ?? ""
271+
let rhsName = rhs.modelName ?? ""
272+
return lhsName.localizedStandardCompare(rhsName) == .orderedAscending
266273
}
267274
}
268275

CopilotMonitor/CopilotMonitorTests/MiniMaxProviderTests.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,110 @@ final class MiniMaxProviderTests: XCTestCase {
151151
XCTFail("Unexpected error: \(error)")
152152
}
153153
}
154+
155+
func testFetchPreservesSubOnePercentUsageForMiniMaxWindows() async throws {
156+
guard TokenManager.shared.getMiniMaxCodingPlanAPIKey() != nil else {
157+
throw XCTSkip("MiniMax Coding Plan API key not available; skipping fetch test.")
158+
}
159+
160+
let session = makeSession()
161+
let provider = MiniMaxProvider(tokenManager: .shared, session: session)
162+
let responseJSON = """
163+
{
164+
"model_remains": [
165+
{
166+
"start_time": 1774587600000,
167+
"end_time": 1774605600000,
168+
"remains_time": 1715317,
169+
"current_interval_total_count": 1500,
170+
"current_interval_usage_count": 1494,
171+
"model_name": "MiniMax-M*",
172+
"current_weekly_total_count": 15000,
173+
"current_weekly_usage_count": 14940,
174+
"weekly_start_time": 1774224000000,
175+
"weekly_end_time": 1774828800000,
176+
"weekly_remains_time": 224915317
177+
}
178+
],
179+
"base_resp": {
180+
"status_code": 0,
181+
"status_msg": "success"
182+
}
183+
}
184+
"""
185+
186+
MockURLProtocol.requestHandler = { request in
187+
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
188+
return (response, Data(responseJSON.utf8))
189+
}
190+
191+
let result = try await provider.fetch()
192+
193+
switch result.usage {
194+
case .quotaBased(let remaining, let entitlement, let overagePermitted):
195+
XCTAssertEqual(remaining, 99)
196+
XCTAssertEqual(entitlement, 100)
197+
XCTAssertFalse(overagePermitted)
198+
default:
199+
XCTFail("Expected quota-based usage")
200+
}
201+
202+
XCTAssertEqual(result.details?.fiveHourUsage ?? -1, 0.4, accuracy: 0.001)
203+
XCTAssertEqual(result.details?.sevenDayUsage ?? -1, 0.4, accuracy: 0.001)
204+
}
205+
206+
func testFetchPrefersUsedQuotaRowOverHigherCapacityZeroUsageRow() async throws {
207+
guard TokenManager.shared.getMiniMaxCodingPlanAPIKey() != nil else {
208+
throw XCTSkip("MiniMax Coding Plan API key not available; skipping fetch test.")
209+
}
210+
211+
let session = makeSession()
212+
let provider = MiniMaxProvider(tokenManager: .shared, session: session)
213+
let responseJSON = """
214+
{
215+
"model_remains": [
216+
{
217+
"start_time": 1774587600000,
218+
"end_time": 1774605600000,
219+
"remains_time": 1715317,
220+
"current_interval_total_count": 9000,
221+
"current_interval_usage_count": 9000,
222+
"model_name": "speech-hd",
223+
"current_weekly_total_count": 63000,
224+
"current_weekly_usage_count": 63000,
225+
"weekly_start_time": 1774224000000,
226+
"weekly_end_time": 1774828800000,
227+
"weekly_remains_time": 224915317
228+
},
229+
{
230+
"start_time": 1774587600000,
231+
"end_time": 1774605600000,
232+
"remains_time": 1715317,
233+
"current_interval_total_count": 4500,
234+
"current_interval_usage_count": 4469,
235+
"model_name": "MiniMax-M*",
236+
"current_weekly_total_count": 45000,
237+
"current_weekly_usage_count": 44659,
238+
"weekly_start_time": 1774224000000,
239+
"weekly_end_time": 1774828800000,
240+
"weekly_remains_time": 224915317
241+
}
242+
],
243+
"base_resp": {
244+
"status_code": 0,
245+
"status_msg": "success"
246+
}
247+
}
248+
"""
249+
250+
MockURLProtocol.requestHandler = { request in
251+
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
252+
return (response, Data(responseJSON.utf8))
253+
}
254+
255+
let result = try await provider.fetch()
256+
257+
XCTAssertEqual(result.details?.fiveHourUsage ?? -1, (31.0 / 4500.0) * 100.0, accuracy: 0.0001)
258+
XCTAssertEqual(result.details?.sevenDayUsage ?? -1, (341.0 / 45000.0) * 100.0, accuracy: 0.0001)
259+
}
154260
}

CopilotMonitor/CopilotMonitorTests/ProviderUsageTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ final class ProviderUsageTests: XCTestCase {
195195
XCTAssertTrue(output.contains("100%,80%"))
196196
}
197197

198+
func testUsagePercentDisplayFormatterPreservesSubOnePercentUsage() {
199+
XCTAssertEqual(UsagePercentDisplayFormatter.string(from: 0.0), "0%")
200+
XCTAssertEqual(UsagePercentDisplayFormatter.string(from: 0.4), "1%")
201+
XCTAssertEqual(UsagePercentDisplayFormatter.string(from: 1.4), "1%")
202+
XCTAssertEqual(UsagePercentDisplayFormatter.wholePercent(from: 0.4), 1)
203+
}
204+
205+
func testTableFormatterShowsOnePercentForMiniMaxSubOnePercentUsage() {
206+
let usage = ProviderUsage.quotaBased(remaining: 99, entitlement: 100, overagePermitted: false)
207+
let details = DetailedUsage(fiveHourUsage: 0.4, sevenDayUsage: 0.2)
208+
let result = ProviderResult(usage: usage, details: details)
209+
210+
let output = TableFormatter.format([.minimaxCodingPlan: result])
211+
XCTAssertTrue(output.contains("1%,1%"))
212+
}
213+
198214
func testTableFormatterShowsGeminiPercentOnlyForGeminiAccounts() {
199215
let geminiAccounts = [
200216
GeminiAccountQuota(

0 commit comments

Comments
 (0)