-
Notifications
You must be signed in to change notification settings - Fork 6
fix: support CREDIT_LIMIT quotas for Z.AI Coding Plan #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d16c14
4516b42
a43e1f7
437835e
2eeee24
e437791
3bbf266
2746ecd
1da2b59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,15 +11,15 @@ | |
| case usage | ||
| } | ||
|
|
||
| private enum UsageDisplayWindowPriority: Int, CaseIterable { | ||
| enum UsageDisplayWindowPriority: Int, CaseIterable { | ||
| case weekly = 0 | ||
| case monthly = 1 | ||
| case daily = 2 | ||
| case hourly = 3 | ||
| case fallback = 4 | ||
| } | ||
|
|
||
| private struct UsagePercentCandidate { | ||
| struct UsagePercentCandidate { | ||
| let percent: Double | ||
| let priority: UsageDisplayWindowPriority | ||
| } | ||
|
|
@@ -273,19 +273,24 @@ | |
| } | ||
| } | ||
|
|
||
| override init() { | ||
| init(startBackgroundServices: Bool) { | ||
| super.init() | ||
| debugLog("StatusBarController init started") | ||
|
|
||
| TokenManager.shared.logDebugEnvironmentInfo() | ||
| debugLog("Environment debug info logged") | ||
|
|
||
| ensureBraveRefreshModeDefault() | ||
| if startBackgroundServices { | ||
| TokenManager.shared.logDebugEnvironmentInfo() | ||
| ensureBraveRefreshModeDefault() | ||
| } | ||
|
|
||
| setupStatusItem() | ||
| debugLog("setupStatusItem completed") | ||
| setupMenu() | ||
| debugLog("setupMenu completed") | ||
| // Menu tests must not launch credential discovery, network refreshes, or modal prompts. | ||
| guard startBackgroundServices else { | ||
| logger.debug("Menu initialized without background services") | ||
| return | ||
| } | ||
| setupNotificationObservers() | ||
| debugLog("setupNotificationObservers completed") | ||
| startRefreshTimer() | ||
|
|
@@ -959,7 +964,20 @@ | |
| return details.chutesMonthlyValueUsedPercent | ||
| } | ||
|
|
||
| private func usagePercentCandidates( | ||
| /// Window percentages shown on the Z.AI top-level quota/provider row. | ||
| /// Unlike the status-bar candidate list (priority-ordered), the top-level | ||
| /// row shows every active window side by side, so the Lite weekly window | ||
| /// must be included here too — omitting it makes the row diverge from the | ||
| /// usage windows (5h session, weekly, MCP monthly). | ||
| private static func zaiCodingPlanTopLevelPercents(details: DetailedUsage?) -> [Double] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Z.AI windows are still enumerated separately for the top-level row, status-bar candidate selection, and change detection. Define one shared window descriptor with its field and display priority, then derive each ordered list from it; otherwise adding the next window needs synchronized edits and can silently make one path disagree with the others.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The top-level row now has its own Z.AI window list, while candidate selection and change detection still enumerate the fields separately. Define one ordered window descriptor and derive these consumer lists from it; otherwise the next quota window needs synchronized edits and one path can silently disagree. |
||
| [ | ||
| details?.tokenUsagePercent, | ||
| details?.weeklyUsagePercent, | ||
| details?.mcpUsagePercent | ||
| ].compactMap { $0 } | ||
| } | ||
|
|
||
| static func usagePercentCandidates( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| identifier: ProviderIdentifier, | ||
| usage: ProviderUsage, | ||
| details: DetailedUsage? | ||
|
|
@@ -1024,6 +1042,7 @@ | |
| case .zaiCodingPlan: | ||
| add(details?.mcpUsagePercent, priority: .monthly) | ||
| add(details?.tokenUsagePercent, priority: .hourly) | ||
| add(details?.weeklyUsagePercent, priority: .weekly) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This adds the weekly window to status-bar candidate selection, but the top-level Z.AI quota row still formats |
||
| case .nanoGpt: | ||
| add(details?.sevenDayUsage, priority: .weekly) | ||
| case .chutes: | ||
|
|
@@ -1046,7 +1065,7 @@ | |
| usage: ProviderUsage, | ||
| details: DetailedUsage? | ||
| ) -> Double? { | ||
| let candidates = usagePercentCandidates(identifier: identifier, usage: usage, details: details) | ||
| let candidates = Self.usagePercentCandidates(identifier: identifier, usage: usage, details: details) | ||
| guard let selectedPriority = candidates.map(\.priority.rawValue).min() else { | ||
| return nil | ||
| } | ||
|
|
@@ -1068,7 +1087,7 @@ | |
| // Main result candidates | ||
| if case .quotaBased = result.usage { | ||
| allCandidates.append(contentsOf: | ||
| usagePercentCandidates(identifier: identifier, usage: result.usage, details: result.details) | ||
| Self.usagePercentCandidates(identifier: identifier, usage: result.usage, details: result.details) | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -1077,7 +1096,7 @@ | |
| for account in accounts { | ||
| guard case .quotaBased = account.usage else { continue } | ||
| allCandidates.append(contentsOf: | ||
| usagePercentCandidates(identifier: identifier, usage: account.usage, details: account.details) | ||
| Self.usagePercentCandidates(identifier: identifier, usage: account.usage, details: account.details) | ||
| ) | ||
| } | ||
| } | ||
|
|
@@ -1103,7 +1122,7 @@ | |
| .max() | ||
| } | ||
|
|
||
| private func usedPercentsForChangeDetection(identifier: ProviderIdentifier, result: ProviderResult) -> [Double] { | ||
| static func usedPercentsForChangeDetection(identifier: ProviderIdentifier, result: ProviderResult) -> [Double] { | ||
| var usedPercents: [Double] = [] | ||
|
|
||
| func appendMetrics(usage: ProviderUsage, details: DetailedUsage?) { | ||
|
|
@@ -1126,6 +1145,7 @@ | |
| details.cursorApiUsage, | ||
| details.tokenUsagePercent, | ||
| details.mcpUsagePercent, | ||
| details.weeklyUsagePercent, | ||
| details.openCodeGoMonthlyUsage | ||
| ] | ||
| for percent in extraPercents { | ||
|
|
@@ -1163,7 +1183,7 @@ | |
| kind: .cost | ||
| ) | ||
| case .quotaBased: | ||
| let cappedPercents = usedPercentsForChangeDetection(identifier: identifier, result: result).map { min($0, 100.0) } | ||
| let cappedPercents = Self.usedPercentsForChangeDetection(identifier: identifier, result: result).map { min($0, 100.0) } | ||
| // Use aggregate quota usage for change detection so non-max windows/accounts can still trigger updates. | ||
| let aggregatePercent = cappedPercents.isEmpty | ||
| ? min(max(result.usage.usagePercentage, 0.0), 100.0) | ||
|
|
@@ -2144,7 +2164,7 @@ | |
| ].compactMap { $0 } | ||
| usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents | ||
| } else if identifier == .zaiCodingPlan { | ||
| let percents = [account.details?.tokenUsagePercent, account.details?.mcpUsagePercent].compactMap { $0 } | ||
| let percents = Self.zaiCodingPlanTopLevelPercents(details: account.details) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The account-row and single-result branches still duplicate the provider-specific percentage selection. Extract one helper accepting |
||
| usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents | ||
| } else if identifier == .chutes { | ||
| let percents = [Self.dailyPercentFromDetails(account.details), Self.chutesMonthlyPercentFromDetails(account.details)].compactMap { $0 } | ||
|
|
@@ -2230,7 +2250,7 @@ | |
| ].compactMap { $0 } | ||
| usedPercents = percents.isEmpty ? [singlePercent] : percents | ||
| } else if identifier == .zaiCodingPlan { | ||
| let percents = [result.details?.tokenUsagePercent, result.details?.mcpUsagePercent].compactMap { $0 } | ||
| let percents = Self.zaiCodingPlanTopLevelPercents(details: result.details) | ||
| usedPercents = percents.isEmpty ? [singlePercent] : percents | ||
| } else if identifier == .chutes { | ||
| let percents = [Self.dailyPercentFromDetails(result.details), Self.chutesMonthlyPercentFromDetails(result.details)].compactMap { $0 } | ||
|
|
@@ -3853,7 +3873,7 @@ | |
| // 3. OpenRouter - only has current cost, no daily history | ||
| // We'll include today's cost if available | ||
| if let routerResult = providerResults[.openRouter], | ||
| case .payAsYouGo(_, let cost, _) = routerResult.usage, | ||
|
Check warning on line 3876 in CopilotMonitor/CopilotMonitor/App/StatusBarController.swift
|
||
| let dailyCost = routerResult.details?.dailyUsage { | ||
| let today = Calendar.current.startOfDay(for: Date()) | ||
| if aggregatedDailyCosts[today] == nil { | ||
|
|
@@ -4258,22 +4278,33 @@ | |
| ) | ||
| ), | ||
| .zaiCodingPlan: ProviderResult( | ||
| usage: .quotaBased(remaining: 1, entitlement: 100, overagePermitted: false), | ||
| usage: .quotaBased(remaining: 88, entitlement: 100, overagePermitted: false), | ||
| details: DetailedUsage( | ||
| tokenUsagePercent: 99.0, | ||
| tokenUsagePercent: 12.0, | ||
| tokenUsageReset: oneDayFromNow, | ||
| tokenUsageUsed: 990_000, | ||
| tokenUsageTotal: 1_000_000, | ||
| mcpUsagePercent: 45.0, | ||
| mcpUsagePercent: 2.0, | ||
| mcpUsageReset: oneDayFromNow, | ||
| mcpUsageUsed: 45, | ||
| mcpUsageTotal: 100, | ||
| modelUsageTokens: 500_000, | ||
| modelUsageCalls: 128, | ||
| toolNetworkSearchCount: 42, | ||
| toolWebReadCount: 15, | ||
| toolZreadCount: 8 | ||
| ) | ||
| weeklyUsagePercent: 1.0, | ||
| weeklyUsageReset: sevenDaysFromNow | ||
| ), | ||
| accounts: [ | ||
| ProviderAccountResult( | ||
| accountIndex: 0, | ||
| accountId: "zai-session", | ||
| usage: .quotaBased(remaining: 88, entitlement: 100, overagePermitted: false), | ||
| details: DetailedUsage( | ||
| tokenUsagePercent: 12.0, | ||
| mcpUsagePercent: 2.0, | ||
| weeklyUsagePercent: 1.0 | ||
| ) | ||
| ), | ||
| ProviderAccountResult( | ||
| accountIndex: 1, | ||
| accountId: "zai-weekly", | ||
| usage: .quotaBased(remaining: 99, entitlement: 100, overagePermitted: false), | ||
| details: DetailedUsage(weeklyUsagePercent: 1.0) | ||
| ) | ||
| ] | ||
| ), | ||
| .geminiCLI: ProviderResult( | ||
| usage: .quotaBased(remaining: 85, entitlement: 100, overagePermitted: false), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -739,6 +739,22 @@ extension StatusBarController { | |
| submenu.addItem(item) | ||
| } | ||
|
|
||
| // === Weekly Usage (CREDIT_LIMIT unit=6, lite tier) === | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Z.AI token, MCP, and weekly branches repeat the same usage-window and limit-row construction used by sibling provider branches. Move the optional-window rendering into a small helper driven by window definitions, keeping only provider-specific labels and fields in data, so separators and reset handling do not drift across synchronized edits. |
||
| if let weeklyUsage = details.weeklyUsagePercent { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new Z.AI weekly branch repeats the optional-window/separator/row sequence already hand-written for several providers. Build a small window-descriptor list and one shared add-window routine for separator handling, leaving only provider-specific labels and fields in data; that keeps future window rows from diverging. |
||
| let items = createUsageWindowRow( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| label: "Weekly (7d)", | ||
| usagePercent: weeklyUsage, | ||
| resetDate: details.weeklyUsageReset, | ||
| windowHours: 24 * 7, | ||
| isMonthly: false | ||
| ) | ||
| items.forEach { submenu.addItem($0) } | ||
| } | ||
| if let weeklyUsed = details.weeklyUsageUsed, let weeklyTotal = details.weeklyUsageTotal { | ||
| let item = createLimitRow(label: "Weekly", used: Double(weeklyUsed), total: Double(weeklyTotal)) | ||
| submenu.addItem(item) | ||
| } | ||
|
|
||
| // === Last 24h stats (provider-specific, keep as-is) === | ||
| let numberFormatter = NumberFormatter() | ||
| numberFormatter.numberStyle = .decimal | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[3라운드째 미합의]
These controller algorithms and candidate types are made module-visible solely for direct tests. Keep them private and test the existing behavior path, or expose one narrowly scoped seam instead of widening several production APIs.