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
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuBarMetricWindowResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ enum MenuBarMetricWindowResolver {
{
return primary.usedPercent >= secondary.usedPercent ? primary : secondary
}
if provider == .cursor {
if provider == .cursor || provider == .minimax {
return Self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ extension UsageMenuCardView.Model {
percentLine: nil)
}

if provider == .minimax, cost.period == "MiniMax points balance" {
let balance = String(format: "%.0f", cost.used)
return ProviderCostSection(
title: L("Credits"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}

if provider == .openai || provider == .claude, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
Expand Down
74 changes: 61 additions & 13 deletions Sources/CodexBar/MenuCardView+MiniMax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ import Foundation
extension UsageMenuCardView.Model {
static func minimaxMetrics(services: [MiniMaxServiceUsage], input: Input) -> [Metric] {
let percentStyle: PercentStyle = .used
let textGenerationCount = services.count { $0.displayName == "Text Generation" }
let displayNameCounts = Dictionary(grouping: services.map(\.displayName), by: { $0 }).mapValues(\.count)

return services.enumerated().map { index, service in
let used = service.usage
let displayPercent = min(100, max(0, service.percent))
let usageLabel = String(
format: L("minimax_usage_amount_format"),
used.formatted(),
service.limit.formatted())
let usedLabel = String(
format: L("minimax_used_percent_format"),
String(format: "%.0f%%", displayPercent))
let usageLabel = if service.isUnlimited {
nil as String?
} else {
String(
format: L("minimax_usage_amount_format"),
used.formatted(),
service.limit.formatted())
}
let localizedName = Self.localizedMiniMaxServiceName(service.displayName)
let title = if localizedName == L("minimax_service_text_generation"), textGenerationCount > 1 {
"\(L("minimax_service_text_generation")) · \(Self.displayWindowBadge(for: service.windowType))"
let title = if (displayNameCounts[service.displayName] ?? 0) > 1 {
"\(localizedName) · \(Self.displayWindowBadge(for: service.windowType))"
} else {
localizedName
}
Expand All @@ -28,13 +29,60 @@ extension UsageMenuCardView.Model {
title: title,
percent: displayPercent,
percentStyle: percentStyle,
statusText: service.isUnlimited ? "∞ Unlimited" : nil,
resetText: Self.localizedMiniMaxResetDescription(service.resetDescription),
detailText: service.timeRange,
detailText: nil,
detailLeftText: usageLabel,
detailRightText: usedLabel,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true,
cardStyle: true)
warningMarkerPercents: service.isUnlimited
? []
: Self.miniMaxWarningMarkerPercents(service: service, input: input),
cardStyle: false)
}
}

private static func miniMaxWarningMarkerPercents(service: MiniMaxServiceUsage, input: Input) -> [Double] {
switch self.miniMaxQuotaWarningWindow(for: service) {
case .session:
warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.session],
showUsed: true)
case .weekly:
markerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: true,
workDays: input.workDaysPerWeek,
windowMinutes: self.miniMaxWindowMinutes(for: service.windowType),
includeWorkdayMarkers: true)
}
}

private static func miniMaxQuotaWarningWindow(for service: MiniMaxServiceUsage) -> QuotaWarningWindow {
service.windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "weekly" ? .weekly : .session
}

private static func miniMaxWindowMinutes(for windowType: String) -> Int? {
let normalized = windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if normalized == "weekly" {
return 7 * 24 * 60
}
if normalized == "today" || normalized == "daily" {
return 24 * 60
}
if normalized == "5h" {
return 5 * 60
}
let pieces = normalized.split(separator: " ")
guard pieces.count >= 2, let value = Int(pieces[0]) else { return nil }
switch pieces[1] {
case "hour", "hours", "hr", "hrs":
return value * 60
case "minute", "minutes", "min", "mins":
return value
default:
return nil
}
}

Expand Down
63 changes: 56 additions & 7 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -845,8 +845,10 @@ extension UsageMenuCardView.Model {
}

private static func usageNotes(input: Input) -> [String] {
let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)

if input.provider == .kiro {
return kiroUsageNotes(input: input)
return kiroUsageNotes(input: input) + subscriptionNotes
}

if input.provider == .kilo {
Expand All @@ -860,24 +862,24 @@ extension UsageMenuCardView.Model {
{
notes.append(L("Using CLI fallback"))
}
return notes
return notes + subscriptionNotes
}

if input.provider == .mimo, input.snapshot != nil {
return [
L("Balance updates in near-real time (up to 5 min lag)"),
L("Daily billing data finalizes at 07:00 UTC"),
]
] + subscriptionNotes
}

if let notes = apiProviderUsageNotes(input: input) {
return notes
return notes + subscriptionNotes
}

guard input.provider == .openrouter,
let openRouter = input.snapshot?.openRouterUsage
else {
return []
return subscriptionNotes
}

var notes = Self.openRouterSpendNotes(openRouter)
Expand All @@ -889,7 +891,35 @@ extension UsageMenuCardView.Model {
case .unavailable:
notes.append(L("API key limit unavailable right now"))
}
return notes
return notes + subscriptionNotes
}

private static func subscriptionMetadataNotes(snapshot: UsageSnapshot?, provider: UsageProvider) -> [String] {
guard let snapshot else { return [] }
if let renewsAt = snapshot.subscriptionRenewsAt {
return [String(format: L("Renews: %@"), self.subscriptionDateString(renewsAt, provider: provider))]
}
if let expiresAt = snapshot.subscriptionExpiresAt {
return [String(format: L("Plan expires: %@"), self.subscriptionDateString(expiresAt, provider: provider))]
}
return []
}

private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
}

private static func subscriptionDateTimeZone(provider: UsageProvider) -> TimeZone {
switch provider {
case .minimax:
TimeZone(identifier: "Asia/Shanghai") ?? .current
default:
.current
}
}

private static func openRouterSpendNotes(_ usage: OpenRouterUsageSnapshot) -> [String] {
Expand Down Expand Up @@ -952,6 +982,9 @@ extension UsageMenuCardView.Model {
}

private static func planDisplay(_ text: String, for provider: UsageProvider) -> String {
if provider == .minimax {
return self.miniMaxPlanDisplay(text)
}
let cleaned = if provider == .codex {
CodexPlanFormatting.displayName(text) ?? UsageFormatter.cleanPlanName(text)
} else {
Expand All @@ -960,6 +993,21 @@ extension UsageMenuCardView.Model {
return cleaned.isEmpty ? text : cleaned
}

private static func miniMaxPlanDisplay(_ text: String) -> String {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.lowercased()
if normalized.contains("tokenplanplus") || normalized.contains("token plan plus") {
return "Plus"
}
if normalized.contains("tokenplanmax") || normalized.contains("token plan max") {
return "Max"
}
if normalized.contains("tokenplanultra") || normalized.contains("token plan ultra") {
return "Ultra"
}
return trimmed
}

private static func kiloLoginPass(snapshot: UsageSnapshot?) -> String? {
self.kiloLoginParts(snapshot: snapshot).pass
}
Expand Down Expand Up @@ -1057,7 +1105,8 @@ extension UsageMenuCardView.Model {
}
if input.provider == .minimax {
if let minimaxUsage = snapshot.minimaxUsage {
if let services = minimaxUsage.services, !services.isEmpty {
let services = minimaxUsage.orderedQuotaServices
if !services.isEmpty {
return Self.minimaxMetrics(services: services, input: input)
}
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxAPIRegion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum MiniMaxAPIRegion: String, CaseIterable, Sendable {
private static let codingPlanPath = "user-center/payment/coding-plan"
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 billingHistoryPath = "account/amount"

public var displayName: String {
Expand Down Expand Up @@ -57,6 +58,10 @@ public enum MiniMaxAPIRegion: String, CaseIterable, Sendable {
URL(string: self.apiBaseURLString)!.appendingPathComponent(Self.remainsPath)
}

public var tokenPlanRemainsURL: URL {
URL(string: self.apiBaseURLString)!.appendingPathComponent(Self.tokenPlanRemainsPath)
}

public var dashboardURL: URL {
var components = URLComponents(string: self.baseURLString)!
components.path = "/" + Self.codingPlanPath
Expand Down
17 changes: 15 additions & 2 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxCookieHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public enum MiniMaxCookieHeader {
#"(?i)(?:--cookie|-b)\s*([^\s]+)"#,
]
private static let authorizationPattern = #"(?i)\bauthorization:\s*bearer\s+([A-Za-z0-9._\-+=/]+)"#
private static let groupIDPattern = #"(?i)\bgroup[_]?id=([0-9]{4,})"#
private static let groupIDPatterns = [
#"(?i)\bx-group-id:\s*([0-9]{4,})"#,
#"(?i)\bminimax_group_id_v2=([0-9]{4,})"#,
#"(?i)\bgroup[_]?id=([0-9]{4,})"#,
]

public static func override(from raw: String?) -> MiniMaxCookieOverride? {
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
Expand All @@ -34,7 +38,7 @@ public enum MiniMaxCookieHeader {
}
guard let cookie = self.normalized(from: raw) else { return nil }
let authorizationToken = self.extractFirst(pattern: self.authorizationPattern, text: raw)
let groupID = self.extractFirst(pattern: self.groupIDPattern, text: raw)
let groupID = self.extractFirst(patterns: self.groupIDPatterns, text: raw)
return MiniMaxCookieOverride(
cookieHeader: cookie,
authorizationToken: authorizationToken,
Expand Down Expand Up @@ -102,4 +106,13 @@ public enum MiniMaxCookieHeader {
let value = text[captureRange].trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : String(value)
}

private static func extractFirst(patterns: [String], text: String) -> String? {
for pattern in patterns {
if let value = self.extractFirst(pattern: pattern, text: text) {
return value
}
}
return nil
}
}
46 changes: 46 additions & 0 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxDecoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

enum MiniMaxDecoding {
static func decodeInt<K: CodingKey>(_ container: KeyedDecodingContainer<K>, forKey key: K) -> Int? {
if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
return value
}
if let value = try? container.decodeIfPresent(Int64.self, forKey: key) {
return Int(value)
}
if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
return Int(value)
}
if let value = try? container.decodeIfPresent(String.self, forKey: key) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return Int(trimmed)
}
return nil
}

static func decodeDouble<K: CodingKey>(_ container: KeyedDecodingContainer<K>, forKey key: K) -> Double? {
if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
return value
}
if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
return Double(value)
}
if let value = try? container.decodeIfPresent(Int64.self, forKey: key) {
return Double(value)
}
if let value = try? container.decodeIfPresent(String.self, forKey: key) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return Double(trimmed)
}
return nil
}

static func decodeDouble<K: CodingKey>(_ container: KeyedDecodingContainer<K>, forKeys keys: [K]) -> Double? {
for key in keys {
if let value = self.decodeDouble(container, forKey: key) {
return value
}
}
return nil
}
}
Loading