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
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,8 @@ enum MiniMaxUsageParser {
windowMinutes: available?.windowMinutes,
usedPercent: usedPercent,
resetsAt: resetsAt,
updatedAt: now)
updatedAt: now,
planPeriodEndsAt: nil)
}

static func parseCodingPlanRemains(
Expand Down Expand Up @@ -1278,7 +1279,8 @@ enum MiniMaxUsageParser {
usedPercent: nil,
resetsAt: nil,
updatedAt: now,
services: services)
services: services,
planPeriodEndsAt: nil)
}

private static func parseResetsAtFromTimeRange(timeRange: String, windowType: String, now: Date) -> Date? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public struct MiniMaxUsageSnapshot: Sendable {
public let updatedAt: Date
public let services: [MiniMaxServiceUsage]?
public let billingSummary: MiniMaxBillingSummary?
/// The end of the current billing period for the MiniMax Token Plan subscription.
/// Derived from `MiniMaxModelRemains.endTime` — NOT the quota reset time.
/// Use this for "Plan period ends: Jun 22, 2026" display.
public let planPeriodEndsAt: Date?

public var primaryService: MiniMaxServiceUsage? {
// Priority: "Text Generation" > first service
Expand Down Expand Up @@ -55,7 +59,8 @@ public struct MiniMaxUsageSnapshot: Sendable {
resetsAt: Date?,
updatedAt: Date,
services: [MiniMaxServiceUsage]? = nil,
billingSummary: MiniMaxBillingSummary? = nil)
billingSummary: MiniMaxBillingSummary? = nil,
planPeriodEndsAt: Date? = nil)
{
self.planName = planName
self.availablePrompts = availablePrompts
Expand All @@ -67,6 +72,7 @@ public struct MiniMaxUsageSnapshot: Sendable {
self.updatedAt = updatedAt
self.services = services
self.billingSummary = billingSummary
self.planPeriodEndsAt = planPeriodEndsAt
}

public func withBillingSummary(_ billingSummary: MiniMaxBillingSummary?) -> MiniMaxUsageSnapshot {
Expand All @@ -80,7 +86,8 @@ public struct MiniMaxUsageSnapshot: Sendable {
resetsAt: self.resetsAt,
updatedAt: self.updatedAt,
services: self.services,
billingSummary: billingSummary)
billingSummary: billingSummary,
planPeriodEndsAt: self.planPeriodEndsAt)
}
}

Expand Down
62 changes: 62 additions & 0 deletions Tests/CodexBarTests/MiniMaxMenuCardModelPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,66 @@ struct MiniMaxMenuCardModelPlanTests {

#expect(model.planText == nil)
}

@Test
func `minimax menu card omits plan period ends metric when planPeriodEndsAt is nil`() throws {
// Without a reliable billing-period source, planPeriodEndsAt stays nil from parsing.
// The menu card should not render a plan-period row when planPeriodEndsAt is nil.
let now = Date()
let minimax = MiniMaxUsageSnapshot(
planName: "Max",
availablePrompts: nil,
currentPrompts: nil,
remainingPrompts: nil,
windowMinutes: nil,
usedPercent: nil,
resetsAt: nil,
updatedAt: now,
services: [
MiniMaxServiceUsage(
serviceType: "text-generation",
windowType: "5 hours",
timeRange: "10:00-15:00(UTC+8)",
usage: 2,
limit: 10,
percent: 20,
resetsAt: now.addingTimeInterval(3600),
resetDescription: "Resets in 1 hour"),
],
planPeriodEndsAt: nil)
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
secondary: nil,
minimaxUsage: minimax,
updatedAt: now,
identity: ProviderIdentitySnapshot(
providerID: .minimax,
accountEmail: nil,
accountOrganization: nil,
loginMethod: "Max"))
let metadata = try #require(ProviderDefaults.metadata[.minimax])

let model = try #require(UsageMenuCardView.Model.make(.init(
provider: .minimax,
metadata: metadata,
snapshot: snapshot,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: true,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now)))

let planPeriodMetric = model.metrics.first { $0.id == "minimax-plan-period" }
#expect(planPeriodMetric == nil)
}
}
127 changes: 127 additions & 0 deletions Tests/CodexBarTests/MiniMaxProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1051,3 +1051,130 @@ struct MiniMaxAPIRegionTests {
#expect(origin.absoluteString == "https://api.minimaxi.com")
}
}

struct MiniMaxPlanPeriodParserTests {
@Test
func `parseCodingPlanRemains does NOT derive planPeriodEndsAt from end_time`() throws {
// end_time represents the quota reset (5-hour window), NOT billing period end.
// planPeriodEndsAt should remain nil since no reliable billing-period source exists.
let json = """
{
"base_resp": { "status_code": 0 },
"data": {
"plan_name": "Max",
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 100,
"current_interval_usage_count": 50,
"start_time": 1779019200,
"end_time": 1782086400,
"remains_time": 86400
}
]
}
}
"""

let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))

let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(
data: Data(json.utf8),
now: now)

// end_time is quota reset; planPeriodEndsAt must stay nil without a true billing-period field
#expect(snapshot.planPeriodEndsAt == nil)
}

@Test
func `parseCodingPlanRemains does NOT use earliest endTime for planPeriodEndsAt`() throws {
// Even with multiple model_remains, end_time fields are quota resets.
// planPeriodEndsAt must remain nil since end_time represents quota, not billing.
let json = """
{
"base_resp": { "status_code": 0 },
"data": {
"plan_name": "Max",
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 100,
"current_interval_usage_count": 50,
"start_time": 1779019200,
"end_time": 1782979200,
"remains_time": 86400
},
{
"model_name": "MiniMax-M2",
"current_interval_total_count": 50,
"current_interval_usage_count": 10,
"start_time": 1781404800,
"end_time": 1782086400,
"remains_time": 172800
}
]
}
}
"""

let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))

let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(
data: Data(json.utf8),
now: now)

#expect(snapshot.planPeriodEndsAt == nil)
}

@Test
func `parseCodingPlanRemains leaves planPeriodEndsAt nil when end_time missing`() throws {
// end_time is absent from model_remains
let json = """
{
"base_resp": { "status_code": 0 },
"data": {
"plan_name": "Max",
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 100,
"current_interval_usage_count": 50,
"start_time": 1779019200,
"remains_time": 86400
}
]
}
}
"""

let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))

let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(
data: Data(json.utf8),
now: now)

#expect(snapshot.planPeriodEndsAt == nil)
}

@Test
func `withBillingSummary preserves planPeriodEndsAt`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let planDate = try #require(calendar.date(from: DateComponents(year: 2026, month: 6, day: 22)))

let original = MiniMaxUsageSnapshot(
planName: "Max",
availablePrompts: 100,
currentPrompts: 50,
remainingPrompts: 50,
windowMinutes: nil,
usedPercent: nil,
resetsAt: nil,
updatedAt: now,
planPeriodEndsAt: planDate)

let withBilling = original.withBillingSummary(nil)
#expect(withBilling.planPeriodEndsAt == planDate)
}
}