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
17 changes: 17 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,23 @@ extension UsageMenuCardView.Model {
L("Daily billing data finalizes at 07:00 UTC"),
]
}
if input.provider == .minimax, let usage = input.snapshot?.minimaxUsage {
var notes = Self.apiProviderUsageNotes(input: input) ?? []
if let tier = usage.planTier?.trimmingCharacters(in: .whitespacesAndNewlines), !tier.isEmpty {
notes.append("Tier: \(tier)")
}
if let expiresAt = usage.planExpiresAt {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = "yyyy-MM-dd"
notes.append("Expires: \(formatter.string(from: expiresAt))")
}
if let total = usage.creditTotal, total > 0 {
notes.append("Credits: \(total.formatted())")
}
return notes
}

if let notes = apiProviderUsageNotes(input: input) {
return notes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
ProviderCookieSourceUI.subtitle(
source: context.settings.minimaxCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports browser cookies and local storage tokens.",
auto: "Automatic imports your browser session (after MiniMax web login) and local storage tokens.",
manual: "Paste a Cookie header or cURL capture from the Token Plan page.",
off: "MiniMax cookies are disabled.")
}
Expand All @@ -82,7 +82,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
ProviderSettingsPickerDescriptor(
id: "minimax-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports browser cookies and local storage tokens.",
subtitle: "Automatic reuses your MiniMax web-login session from supported browsers.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
Expand Down Expand Up @@ -114,10 +114,11 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
return [
ProviderSettingsFieldDescriptor(
id: "minimax-api-token",
title: "API token",
subtitle: "Stored in ~/.codexbar/config.json. Paste your MiniMax API key.",
title: "MiniMax key (optional)",
subtitle: "Stored in ~/.codexbar/config.json. Paste a Token Plan subscription key "
+ "or pay-as-you-go API key, or leave empty to reuse a MiniMax web-login session.",
kind: .secure,
placeholder: "Paste API token…",
placeholder: "Paste MiniMax key…",
binding: context.stringBinding(\.minimaxAPIToken),
actions: [
ProviderSettingsActionDescriptor(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct MiniMaxBillingSummary: Sendable {
public struct MiniMaxBillingSummary: Codable, Sendable {
public let todayTokens: Int
public let last30DaysTokens: Int
public let todayCash: Double?
Expand Down Expand Up @@ -31,7 +31,7 @@ public struct MiniMaxBillingSummary: Sendable {
}
}

public struct MiniMaxBillingDay: Sendable, Equatable {
public struct MiniMaxBillingDay: Codable, Sendable, Equatable {
public let day: String
public let tokens: Int
public let cash: Double?
Expand All @@ -43,7 +43,7 @@ public struct MiniMaxBillingDay: Sendable, Equatable {
}
}

public struct MiniMaxBillingBreakdown: Sendable, Equatable {
public struct MiniMaxBillingBreakdown: Codable, Sendable, Equatable {
public let name: String
public let tokens: Int
public let cash: Double?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
/// This struct encapsulates all the relevant details about how much of a particular
/// MiniMax service has been used within its quota window, including reset timing
/// and localized display strings.
public struct MiniMaxServiceUsage: Sendable {
public struct MiniMaxServiceUsage: Codable, Sendable {
/// The service identifier (e.g., "text-generation", "text-to-speech", "image")
public let serviceType: String

Expand Down
225 changes: 225 additions & 0 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxTokenPlanModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import Foundation

struct MiniMaxCodingPlanData: Decodable {
let baseResp: MiniMaxBaseResponse?
let currentSubscribeTitle: String?
let planName: String?
let comboTitle: String?
let currentPlanTitle: String?
let currentComboCard: MiniMaxComboCard?
let tokenPlanCredit: MiniMaxTokenPlanCredit?
let cycleResourcePackage: MiniMaxCycleResourcePackage?
let modelRemains: [MiniMaxModelRemains]

private enum CodingKeys: String, CodingKey {
case baseResp = "base_resp"
case currentSubscribeTitle = "current_subscribe_title"
case planName = "plan_name"
case comboTitle = "combo_title"
case currentPlanTitle = "current_plan_title"
case currentComboCard = "current_combo_card"
case tokenPlanCredit = "token_plan_credit"
case cycleResourcePackage = "cycle_resource_package"
case modelRemains = "model_remains"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp)
self.currentSubscribeTitle = try container.decodeIfPresent(String.self, forKey: .currentSubscribeTitle)
self.planName = try container.decodeIfPresent(String.self, forKey: .planName)
self.comboTitle = try container.decodeIfPresent(String.self, forKey: .comboTitle)
self.currentPlanTitle = try container.decodeIfPresent(String.self, forKey: .currentPlanTitle)
self.currentComboCard = try container.decodeIfPresent(MiniMaxComboCard.self, forKey: .currentComboCard)
self.tokenPlanCredit = try container.decodeIfPresent(MiniMaxTokenPlanCredit.self, forKey: .tokenPlanCredit)
self.cycleResourcePackage = try container.decodeIfPresent(
MiniMaxCycleResourcePackage.self,
forKey: .cycleResourcePackage)
self.modelRemains = try (container.decodeIfPresent([MiniMaxModelRemains].self, forKey: .modelRemains)) ?? []
}
}

struct MiniMaxComboCard: Decodable {
let title: String?
}

struct MiniMaxModelRemains: Decodable {
let modelName: String?
let currentIntervalTotalCount: Int?
let currentIntervalUsageCount: Int?
let startTime: Int?
let endTime: Int?
let remainsTime: Int?
let currentIntervalRemainingPercent: Double?
let currentWeeklyTotalCount: Int?
let currentWeeklyUsageCount: Int?
let currentWeeklyRemainingPercent: Double?
let weeklyStartTime: Int?
let weeklyEndTime: Int?
let weeklyRemainsTime: Int?

private enum CodingKeys: String, CodingKey {
case modelName = "model_name"
case currentIntervalTotalCount = "current_interval_total_count"
case currentIntervalUsageCount = "current_interval_usage_count"
case startTime = "start_time"
case endTime = "end_time"
case remainsTime = "remains_time"
case currentIntervalRemainingPercent = "current_interval_remaining_percent"
case currentWeeklyTotalCount = "current_weekly_total_count"
case currentWeeklyUsageCount = "current_weekly_usage_count"
case currentWeeklyRemainingPercent = "current_weekly_remaining_percent"
case weeklyStartTime = "weekly_start_time"
case weeklyEndTime = "weekly_end_time"
case weeklyRemainsTime = "weekly_remains_time"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.modelName = try container.decodeIfPresent(String.self, forKey: .modelName)
self.currentIntervalTotalCount = MiniMaxDecoding.decodeInt(container, forKey: .currentIntervalTotalCount)
self.currentIntervalUsageCount = MiniMaxDecoding.decodeInt(container, forKey: .currentIntervalUsageCount)
self.startTime = MiniMaxDecoding.decodeInt(container, forKey: .startTime)
self.endTime = MiniMaxDecoding.decodeInt(container, forKey: .endTime)
self.remainsTime = MiniMaxDecoding.decodeInt(container, forKey: .remainsTime)
self.currentIntervalRemainingPercent = MiniMaxDecoding.decodeDouble(
container,
forKey: .currentIntervalRemainingPercent)
self.currentWeeklyTotalCount = MiniMaxDecoding.decodeInt(container, forKey: .currentWeeklyTotalCount)
self.currentWeeklyUsageCount = MiniMaxDecoding.decodeInt(container, forKey: .currentWeeklyUsageCount)
self.currentWeeklyRemainingPercent = MiniMaxDecoding.decodeDouble(
container,
forKey: .currentWeeklyRemainingPercent)
self.weeklyStartTime = MiniMaxDecoding.decodeInt(container, forKey: .weeklyStartTime)
self.weeklyEndTime = MiniMaxDecoding.decodeInt(container, forKey: .weeklyEndTime)
self.weeklyRemainsTime = MiniMaxDecoding.decodeInt(container, forKey: .weeklyRemainsTime)
}
}

struct MiniMaxTokenPlanCredit: Decodable {
let total: Int?
let used: Int?
let remaining: Int?

private enum CodingKeys: String, CodingKey {
case total
case used
case remaining
case apiKey = "api_key"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = MiniMaxDecoding.decodeInt(container, forKey: .total)
self.used = MiniMaxDecoding.decodeInt(container, forKey: .used)
self.remaining = MiniMaxDecoding.decodeInt(container, forKey: .remaining)
_ = try? container.decodeIfPresent(String.self, forKey: .apiKey) // explicitly ignored
}
}

struct MiniMaxCycleResourcePackage: Decodable {
let title: String?
let tier: String?
let expiresAt: Date?
let creditTotal: Int?

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicCodingKey.self)
self.title = Self.decodeString(container, keys: ["title", "plan_title", "plan_name", "summary"])
self.tier = Self.decodeString(container, keys: ["tier", "plan_tier", "level"])
self.creditTotal = Self.decodeInt(container, keys: ["credit_total", "total_credit", "total_credits"])
self.expiresAt = Self.decodeDate(
container,
keys: ["end_time", "end_ts", "expire_time", "expires_at", "end_date"])
}

private static func decodeString(_ container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> String? {
for key in keys {
guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
if let value = try? container.decodeIfPresent(String.self, forKey: codingKey),
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
return value
}
}
return nil
}

private static func decodeInt(_ container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> Int? {
for key in keys {
guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
if let value = MiniMaxDecoding.decodeInt(container, forKey: codingKey) {
return value
}
}
return nil
}

private static func decodeDate(_ container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> Date? {
for key in keys {
guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
if let string = try? container.decodeIfPresent(String.self, forKey: codingKey) {
if let parsed = Self.parseDate(string) { return parsed }
}
if let intValue = MiniMaxDecoding.decodeInt(container, forKey: codingKey),
let parsed = Self.dateFromFlexibleEpoch(intValue)
{
return parsed
}
}
return nil
}

private static func parseDate(_ value: String) -> Date? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return nil }
if let intValue = Int(trimmed), let date = Self.dateFromFlexibleEpoch(intValue) {
return date
}
let formats = ["yyyy-MM-dd", "yyyy/MM/dd", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"]
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
for format in formats {
formatter.dateFormat = format
if let date = formatter.date(from: trimmed) { return date }
}
return ISO8601DateFormatter().date(from: trimmed)
}

private static func dateFromFlexibleEpoch(_ raw: Int) -> Date? {
if raw > 1_000_000_000_000 { return Date(timeIntervalSince1970: TimeInterval(raw) / 1000) }
if raw > 1_000_000_000 { return Date(timeIntervalSince1970: TimeInterval(raw)) }
return nil
}
}

struct MiniMaxBaseResponse: Decodable {
let statusCode: Int?
let statusMessage: String?

private enum CodingKeys: String, CodingKey {
case statusCode = "status_code"
case statusMessage = "status_msg"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = MiniMaxDecoding.decodeInt(container, forKey: .statusCode)
self.statusMessage = try container.decodeIfPresent(String.self, forKey: .statusMessage)
}
}

private struct DynamicCodingKey: CodingKey {
let stringValue: String
let intValue: Int?

init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}

init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
Loading