Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added
- Agent Sessions: list and focus live local or SSH-discovered Codex and Claude Code sessions from the menu and CLI.
- MiniMax: fetch Token Plan recharge credit balance from the console when a web session cookie is available, including MiniMax Agent desktop cookies and explicit manual or env cookies on API-token refreshes. Thanks @Yuxin-Qiao!
- MiniMax: record Session and Weekly quota utilization history with subscription utilization charts and nearest reset-time menu-bar display. Thanks @Yuxin-Qiao!

### Fixed
- Ollama: recognize current WorkOS AuthKit sessions during browser-cookie discovery and manual cookie validation. Thanks @joeVenner!
Expand Down
13 changes: 13 additions & 0 deletions Sources/CodexBar/MenuBarMetricWindowResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,19 @@ enum MenuBarMetricWindowResolver {
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}

/// The quota window whose reset is soonest among the primary, secondary, and tertiary lanes.
static func nearestResetWindow(snapshot: UsageSnapshot?, now: Date = Date()) -> RateWindow? {
guard let snapshot else { return nil }
let datedWindows = [snapshot.primary, snapshot.secondary, snapshot.tertiary]
.compactMap(\.self)
.compactMap { window -> (RateWindow, Date)? in
guard let resetsAt = window.resetsAt, resetsAt > now else { return nil }
return (window, resetsAt)
}
guard !datedWindows.isEmpty else { return nil }
return datedWindows.min(by: { $0.1 < $1.1 })?.0
}

private static func mostConstrainedCursorWindow(
total: RateWindow?,
auto: RateWindow?,
Expand Down
8 changes: 7 additions & 1 deletion Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ extension UsageMenuCardView.Model {
L("Reported by OpenAI Admin API organization usage.")
case .mistral:
L("Reported by Mistral billing usage.")
case .minimax:
L("Estimated from MiniMax usage summary and published pay-as-you-go pricing.")
default:
nil
}
Expand Down Expand Up @@ -304,11 +306,15 @@ extension UsageMenuCardView.Model {

if provider == .minimax, cost.period == "MiniMax points balance" {
let balance = String(format: "%.0f", cost.used)
let expiresLine = cost.resetsAt.map {
String(format: L("Expires: %@"), UsageFormatter.preciseDateTimeDescription(from: $0))
}
return ProviderCostSection(
title: L("Credits"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
percentLine: nil,
personalSpendLine: expiresLine)
}

if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
Expand Down
20 changes: 19 additions & 1 deletion Sources/CodexBar/MenuCardView+MiniMax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension UsageMenuCardView.Model {
percent: displayPercent,
percentStyle: percentStyle,
statusText: service.isUnlimited ? "∞ Unlimited" : nil,
resetText: Self.localizedMiniMaxResetDescription(service.resetDescription),
resetText: Self.miniMaxResetText(for: service, input: input),
detailText: nil,
detailLeftText: usageLabel,
detailRightText: nil,
Expand Down Expand Up @@ -112,6 +112,24 @@ extension UsageMenuCardView.Model {
return trimmed.isEmpty ? windowType : trimmed
}

private static func miniMaxResetText(for service: MiniMaxServiceUsage, input: Input) -> String {
if service.isUnlimited {
return self.localizedMiniMaxResetDescription(service.resetDescription)
}
if let resetsAt = service.resetsAt {
switch input.resetTimeDisplayStyle {
case .absolute:
let text = UsageFormatter.resetDescription(from: resetsAt, now: input.now)
return L("Resets %@", text)
case .countdown:
let phrase = MiniMaxServiceUsage.resetCountdownPhrase(from: resetsAt, now: input.now)
if phrase == "now" { return L("Resets now") }
return L("Resets in %@", phrase)
}
}
return Self.localizedMiniMaxResetDescription(service.resetDescription)
}

private static func localizedMiniMaxResetDescription(_ text: String) -> String {
let prefix = "Resets in "
guard text.hasPrefix(prefix) else { return text }
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ struct PlanUtilizationHistoryChartMenuView: View {
{
names.insert(.opus)
}
case .minimax:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
default:
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
+ (snapshot.extraRateWindows?.filter(\.usageKnown).map(\.window) ?? [])
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -824,9 +824,12 @@ extension StatusItemController {
self.store.weeklyPace(provider: provider, window: window, now: now)
}
case .resetTime:
let resetWindow = provider == .minimax
? self.menuBarResetTimeWindow(for: provider, snapshot: snapshot)
: percentWindow
return MenuBarDisplayText.displayText(
mode: mode,
percentWindow: percentWindow,
percentWindow: resetWindow,
showUsed: self.settings.usageBarsShowUsed,
resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle,
now: now)
Expand Down
17 changes: 14 additions & 3 deletions Sources/CodexBar/StatusItemController+CountdownRefresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import Foundation
extension StatusItemController {
private nonisolated static let menuBarCountdownRefreshEpsilon: TimeInterval = 0.05

func menuBarResetTimeWindow(for provider: UsageProvider, snapshot: UsageSnapshot?) -> RateWindow? {
if provider == .minimax {
let preference = self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot)
if preference == .automatic {
return MenuBarMetricWindowResolver.nearestResetWindow(snapshot: snapshot)
?? self.menuBarMetricWindow(for: provider, snapshot: snapshot)
}
return self.menuBarMetricWindow(for: provider, snapshot: snapshot)
}
return self.menuBarMetricWindow(for: provider, snapshot: snapshot)
}

func scheduleMenuBarCountdownRefreshIfNeeded(now: Date = .init()) {
self.menuBarCountdownRefreshTask?.cancel()
self.menuBarCountdownRefreshTask = nil
Expand All @@ -15,10 +27,9 @@ extension StatusItemController {
self.settings.resetTimeDisplayStyle == .countdown
{
let resetDates = providers.compactMap { provider in
self.menuBarMetricWindow(
self.menuBarResetTimeWindow(
for: provider,
snapshot: self.store.snapshot(for: provider),
now: now)?.resetsAt
snapshot: self.store.snapshot(for: provider))?.resetsAt
}
if let delay = Self.menuBarCountdownRefreshDelay(resetDates: resetDates, now: now) {
delays.append(delay)
Expand Down
57 changes: 57 additions & 0 deletions Sources/CodexBar/UsageStore+PlanUtilization+MiniMax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import CodexBarCore
import Foundation

extension UsageStore {
func appendMiniMaxPlanUtilizationSamples(
snapshot: UsageSnapshot,
appendWindow: (_ window: RateWindow?, _ name: PlanUtilizationSeriesName?) -> Void)
{
let services = snapshot.minimaxUsage?.services?
.filter(\.isPrimaryTextQuotaLane) ?? []
if services.isEmpty {
appendWindow(snapshot.primary, .session)
appendWindow(snapshot.secondary, .weekly)
return
}

if let weeklyService = services.first(where: {
$0.windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "weekly"
}) {
appendWindow(self.miniMaxRateWindow(for: weeklyService), .weekly)
}

let sessionService = services
.filter {
let normalized = $0.windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized != "weekly" && normalized != "today" && normalized != "今日"
}
.min {
self.miniMaxWindowMinutes(for: $0) < self.miniMaxWindowMinutes(for: $1)
}
if let sessionService {
appendWindow(self.miniMaxRateWindow(for: sessionService), .session)
}
}

private func miniMaxRateWindow(for service: MiniMaxServiceUsage) -> RateWindow {
RateWindow(
usedPercent: max(0, min(100, service.percent)),
windowMinutes: self.miniMaxWindowMinutes(for: service),
resetsAt: service.resetsAt,
resetDescription: service.resetDescription)
}

private func miniMaxWindowMinutes(for service: MiniMaxServiceUsage) -> Int {
let windowType = service.windowType.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
if windowType == "today" || windowType == "今日" {
return 24 * 60
}
if windowType == "weekly" {
return 7 * 24 * 60
}
if let parsed = MiniMaxServiceUsage.parseWindowType(service.windowType).windowMinutes {
return parsed
}
return 300
}
}
29 changes: 27 additions & 2 deletions Sources/CodexBar/UsageStore+PlanUtilization.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import CodexBarCore
import Foundation

// swiftlint:disable file_length

extension UsageStore {
private nonisolated static let limitResetThreshold = 1.0
nonisolated static let sessionLimitResetDetectorDefaultsKey = "sessionLimitResetDetectorStates"
Expand Down Expand Up @@ -42,6 +44,9 @@ extension UsageStore {
switch provider {
case .codex, .claude:
true
case .minimax:
self.planUtilizationHistory[.minimax]?.isEmpty == false
|| self.miniMaxHasRecordablePlanUtilizationSamples()
default:
if self.planUtilizationHistory[provider]?.isEmpty == false {
true
Expand Down Expand Up @@ -224,7 +229,7 @@ extension UsageStore {
}

guard !samples.isEmpty else { return }
guard self.shouldRecordPlanUtilizationHistory(for: provider) else { return }
guard self.shouldRecordPlanUtilizationHistory(for: provider, snapshot: snapshot) else { return }
guard !self.shouldDeferClaudePlanUtilizationHistory(provider: provider) else { return }

var snapshotToPersist: [UsageProvider: PlanUtilizationHistoryBuckets]?
Expand Down Expand Up @@ -261,10 +266,18 @@ extension UsageStore {
await self.planUtilizationPersistenceCoordinator.enqueue(snapshotToPersist)
}

private func shouldRecordPlanUtilizationHistory(for provider: UsageProvider) -> Bool {
private func shouldRecordPlanUtilizationHistory(
for provider: UsageProvider,
snapshot: UsageSnapshot) -> Bool
{
switch provider {
case .codex, .claude:
true
case .minimax:
!self.planUtilizationSeriesSamples(
provider: .minimax,
snapshot: snapshot,
capturedAt: snapshot.updatedAt).isEmpty
default:
self.settings.historicalTrackingEnabled
}
Expand Down Expand Up @@ -533,6 +546,14 @@ extension UsageStore {
}
}

private func miniMaxHasRecordablePlanUtilizationSamples() -> Bool {
guard let snapshot = self.snapshots[.minimax] else { return false }
return !self.planUtilizationSeriesSamples(
provider: .minimax,
snapshot: snapshot,
capturedAt: snapshot.updatedAt).isEmpty
}

private func planUtilizationSeriesSamples(
provider: UsageProvider,
snapshot: UsageSnapshot,
Expand Down Expand Up @@ -574,6 +595,10 @@ extension UsageStore {
appendWindow(snapshot.primary, name: .session)
appendWindow(snapshot.secondary, name: .weekly)
appendWindow(snapshot.tertiary, name: .opus)
case .minimax:
self.appendMiniMaxPlanUtilizationSamples(
snapshot: snapshot,
appendWindow: appendWindow)
case .antigravity:
let namedWeeklyWindows = snapshot.extraRateWindows?
.filter {
Expand Down
25 changes: 21 additions & 4 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxAPIRegion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ public enum MiniMaxAPIRegion: String, CaseIterable, Sendable {
private static let codingPlanQuery = "cycle_type=3"
private static let remainsPath = "v1/api/openplatform/coding_plan/remains"
private static let tokenPlanRemainsPath = "v1/token_plan/remains"
private static let tokenPlanCreditPath = "backend/account/token_plan_credit"
private static let tokenPlanUsageSummaryPath = "backend/account/token_plan/usage_summary"
private static let billingHistoryPath = "account/amount"
private static let usageDashboardPath = "console/usage"

public var displayName: String {
switch self {
Expand Down Expand Up @@ -37,6 +40,23 @@ public enum MiniMaxAPIRegion: String, CaseIterable, Sendable {
}
}

public var webBaseURLString: String {
switch self {
case .global:
"https://www.minimax.io"
case .chinaMainland:
"https://www.minimaxi.com"
}
}

public var tokenPlanCreditURL: URL {
URL(string: self.webBaseURLString)!.appendingPathComponent(Self.tokenPlanCreditPath)
}

public var tokenPlanUsageSummaryURL: URL {
URL(string: self.webBaseURLString)!.appendingPathComponent(Self.tokenPlanUsageSummaryPath)
}

public var codingPlanURL: URL {
var components = URLComponents(string: self.baseURLString)!
components.path = "/" + Self.codingPlanPath
Expand All @@ -63,10 +83,7 @@ public enum MiniMaxAPIRegion: String, CaseIterable, Sendable {
}

public var dashboardURL: URL {
var components = URLComponents(string: self.baseURLString)!
components.path = "/" + Self.codingPlanPath
components.query = Self.codingPlanQuery
return components.url!
URL(string: self.baseURLString)!.appendingPathComponent(Self.usageDashboardPath)
}

public func billingHistoryURL(page: Int, limit: Int) -> URL {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ public enum MiniMaxCookieImporter {
private static let cookieDomains = [
"platform.minimax.io",
"openplatform.minimax.io",
"www.minimax.io",
"minimax.io",
"platform.minimaxi.com",
"openplatform.minimaxi.com",
"www.minimaxi.com",
"minimaxi.com",
]

Expand Down
Loading