import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public struct MiniMaxUsageFetcher: Sendable { private static let log = CodexBarLog.logger("minimax-usage") private static let codingPlanPath = "user-center/payment/coding-plan" private static let codingPlanQuery = "cycle_type=3" private static let codingPlanRemainsPath = "v1/api/openplatform/coding_plan/remains" private static let apiRemainsURL = URL(string: "https://api.minimax.io/v1/coding_plan/remains")! private struct RemainsContext: Sendable { let authorizationToken: String? let groupID: String? } public static func fetchUsage( cookieHeader: String, authorizationToken: String? = nil, groupID: String? = nil, region: MiniMaxAPIRegion = .global, environment: [String: String] = ProcessInfo.processInfo.environment, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else { throw MiniMaxUsageError.invalidCredentials } do { return try await self.fetchCodingPlanHTML( cookie: cookie, authorizationToken: authorizationToken, region: region, environment: environment, now: now) } catch let error as MiniMaxUsageError { if case .parseFailed = error { Self.log.debug("MiniMax coding plan HTML parse failed, trying remains API") return try await self.fetchCodingPlanRemains( cookie: cookie, remainsContext: RemainsContext( authorizationToken: authorizationToken, groupID: groupID), region: region, environment: environment, now: now) } throw error } } public static func fetchUsage( apiToken: String, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { throw MiniMaxUsageError.invalidCredentials } var request = URLRequest(url: self.apiRemainsURL) request.httpMethod = "GET" request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "accept") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now) } private static func fetchCodingPlanHTML( cookie: String, authorizationToken: String?, region: MiniMaxAPIRegion, environment: [String: String], now: Date) async throws -> MiniMaxUsageSnapshot { let url = self.resolveCodingPlanURL(region: region, environment: environment) var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(cookie, forHTTPHeaderField: "Cookie") if let authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } let acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.setValue(acceptHeader, forHTTPHeaderField: "accept") let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" request.setValue(userAgent, forHTTPHeaderField: "user-agent") request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") let origin = self.originURL(from: url) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue( self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString, forHTTPHeaderField: "referer") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now) } let html = String(data: data, encoding: .utf8) ?? "" if html.contains("__NEXT_DATA__") { Self.log.debug("MiniMax coding plan HTML contains __NEXT_DATA__") } if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func fetchCodingPlanRemains( cookie: String, remainsContext: RemainsContext, region: MiniMaxAPIRegion, environment: [String: String], now: Date) async throws -> MiniMaxUsageSnapshot { let baseRemainsURL = self.resolveRemainsURL(region: region, environment: environment) let remainsURL = self.appendGroupID(remainsContext.groupID, to: baseRemainsURL) var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue(cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = remainsContext.authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } let acceptHeader = "application/json, text/plain, */*" request.setValue(acceptHeader, forHTTPHeaderField: "accept") request.setValue("XMLHttpRequest", forHTTPHeaderField: "x-requested-with") let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" request.setValue(userAgent, forHTTPHeaderField: "user-agent") request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") let origin = self.originURL(from: baseRemainsURL) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue( self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString, forHTTPHeaderField: "referer") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let payload = try MiniMaxUsageParser.decodePayload(data: data) self.logCodingPlanStatus(payload: payload) return try MiniMaxUsageParser.parseCodingPlanRemains(payload: payload, now: now) } let html = String(data: data, encoding: .utf8) ?? "" if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func appendGroupID(_ groupID: String?, to url: URL) -> URL { guard let groupID, !groupID.isEmpty else { return url } guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } var queryItems = components.queryItems ?? [] queryItems.append(URLQueryItem(name: "GroupId", value: groupID)) components.queryItems = queryItems return components.url ?? url } static func originURL(from url: URL) -> URL { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } components.path = "" components.query = nil components.fragment = nil return components.url ?? url } static func resolveCodingPlanURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) { return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanPath, query: Self.codingPlanQuery) { return hostURL } return region.codingPlanURL } static func resolveCodingPlanRefererURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) { if var components = URLComponents(url: override, resolvingAgainstBaseURL: false) { components.query = nil return components.url ?? override } return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanPath) { return hostURL } return region.codingPlanRefererURL } static func resolveRemainsURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.remainsURL(environment: environment) { return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanRemainsPath) { return hostURL } return region.remainsURL } static func url(from raw: String, path: String? = nil, query: String? = nil) -> URL? { guard let cleaned = MiniMaxSettingsReader.cleaned(raw) else { return nil } func compose(_ base: URL) -> URL? { var components = URLComponents(url: base, resolvingAgainstBaseURL: false)! if let path { components.path = "/" + path } if let query { components.query = query } return components.url } if let url = URL(string: cleaned), url.scheme != nil { if let composed = compose(url) { return composed } return url } guard let base = URL(string: "https://\(cleaned)") else { return nil } return compose(base) } private static func logCodingPlanStatus(payload: MiniMaxCodingPlanPayload) { let baseResponse = payload.data.baseResp ?? payload.baseResp guard let status = baseResponse?.statusCode else { return } let message = baseResponse?.statusMessage ?? "" if !message.isEmpty { Self.log.debug("MiniMax coding plan status \(status): \(message)") } else { Self.log.debug("MiniMax coding plan status \(status)") } } private static func looksSignedOut(html: String) -> Bool { let lower = html.lowercased() return lower.contains("sign in") || lower.contains("log in") || lower.contains("登录") || lower.contains("登入") }} struct MiniMaxCodingPlanPayload: Decodable, Sendable { let baseResp: MiniMaxBaseResponse? let data: MiniMaxCodingPlanData private enum CodingKeys: String, CodingKey { case baseResp = "base_resp" case data } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp) if container.contains(.data) { let dataDecoder = try container.superDecoder(forKey: .data) self.data = try MiniMaxCodingPlanData(from: dataDecoder) } else { self.data = try MiniMaxCodingPlanData(from: decoder) } }} struct MiniMaxCodingPlanData: Decodable, Sendable { let baseResp: MiniMaxBaseResponse? let currentSubscribeTitle: String? let planName: String? let comboTitle: String? let currentPlanTitle: String? let currentComboCard: MiniMaxComboCard? 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 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.modelRemains = try (container.decodeIfPresent([MiniMaxModelRemains].self, forKey: .modelRemains)) ?? [] }} struct MiniMaxComboCard: Decodable, Sendable { let title: String?} struct MiniMaxModelRemains: Decodable, Sendable { let currentIntervalTotalCount: Int? let currentIntervalUsageCount: Int? let startTime: Int? let endTime: Int? let remainsTime: Int? private enum CodingKeys: String, CodingKey { case currentIntervalTotalCount = "current_interval_total_count" case currentIntervalUsageCount = "current_interval_usage_count" case startTime = "start_time" case endTime = "end_time" case remainsTime = "remains_time" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) 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) }} struct MiniMaxBaseResponse: Decodable, Sendable { 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) }} 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 }} enum MiniMaxUsageParser { static func decodePayload(data: Data) throws -> MiniMaxCodingPlanPayload { let decoder = JSONDecoder() return try decoder.decode(MiniMaxCodingPlanPayload.self, from: data) } static func decodePayload(json: [String: Any]) throws -> MiniMaxCodingPlanPayload { let data = try JSONSerialization.data(withJSONObject: json, options: []) return try self.decodePayload(data: data) } static func parseCodingPlanRemains(data: Data, now: Date = Date()) throws -> MiniMaxUsageSnapshot { let payload = try self.decodePayload(data: data) return try self.parseCodingPlanRemains(payload: payload, now: now) } static func parse(html: String, now: Date = Date()) throws -> MiniMaxUsageSnapshot { if let snapshot = self.parseNextData(html: html, now: now) { return snapshot } let text = self.stripHTML(html) let planName = self.parsePlanName(html: html, text: text) let available = self.parseAvailableUsage(text: text) let usedPercent = self.parseUsedPercent(text: text) let resetsAt = self.parseResetsAt(text: text, now: now) if planName == nil, available == nil, usedPercent == nil { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } return MiniMaxUsageSnapshot( planName: planName, availablePrompts: available?.prompts, currentPrompts: nil, remainingPrompts: nil, windowMinutes: available?.windowMinutes, usedPercent: usedPercent, resetsAt: resetsAt, updatedAt: now) } static func parseCodingPlanRemains( payload: MiniMaxCodingPlanPayload, now: Date = Date()) throws -> MiniMaxUsageSnapshot { let baseResponse = payload.data.baseResp ?? payload.baseResp if let status = baseResponse?.statusCode, status != 0 { let message = baseResponse?.statusMessage ?? "status_code \(status)" let lower = message.lowercased() if status == 1004 || lower.contains("cookie") || lower.contains("log in") || lower.contains("login") { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError(message) } guard let first = payload.data.modelRemains.first else { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } let total = first.currentIntervalTotalCount let remaining = first.currentIntervalUsageCount let usedPercent = self.usedPercent(total: total, remaining: remaining) let windowMinutes = self.windowMinutes( start: self.dateFromEpoch(first.startTime), end: self.dateFromEpoch(first.endTime)) let resetsAt = self.resetsAt( end: self.dateFromEpoch(first.endTime), remains: first.remainsTime, now: now) let planName = self.parsePlanName(data: payload.data) if planName == nil, total == nil, usedPercent == nil { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } let currentPrompts: Int? = if let total, let remaining { max(0, total - remaining) } else { nil } return MiniMaxUsageSnapshot(Lines 1–500 of 816. Load more
import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public struct MiniMaxUsageFetcher: Sendable { private static let log = CodexBarLog.logger("minimax-usage") private static let codingPlanPath = "user-center/payment/coding-plan" private static let codingPlanQuery = "cycle_type=3" private static let codingPlanRemainsPath = "v1/api/openplatform/coding_plan/remains" private static let apiRemainsURL = URL(string: "https://api.minimax.io/v1/coding_plan/remains")! private struct RemainsContext: Sendable { let authorizationToken: String? let groupID: String? } public static func fetchUsage( cookieHeader: String, authorizationToken: String? = nil, groupID: String? = nil, region: MiniMaxAPIRegion = .global, environment: [String: String] = ProcessInfo.processInfo.environment, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else { throw MiniMaxUsageError.invalidCredentials } do { return try await self.fetchCodingPlanHTML( cookie: cookie, authorizationToken: authorizationToken, region: region, environment: environment, now: now) } catch let error as MiniMaxUsageError { if case .parseFailed = error { Self.log.debug("MiniMax coding plan HTML parse failed, trying remains API") return try await self.fetchCodingPlanRemains( cookie: cookie, remainsContext: RemainsContext( authorizationToken: authorizationToken, groupID: groupID), region: region, environment: environment, now: now) } throw error } } public static func fetchUsage( apiToken: String, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { throw MiniMaxUsageError.invalidCredentials } var request = URLRequest(url: self.apiRemainsURL) request.httpMethod = "GET" request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "accept") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now) } private static func fetchCodingPlanHTML( cookie: String, authorizationToken: String?, region: MiniMaxAPIRegion, environment: [String: String], now: Date) async throws -> MiniMaxUsageSnapshot { let url = self.resolveCodingPlanURL(region: region, environment: environment) var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(cookie, forHTTPHeaderField: "Cookie") if let authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } let acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.setValue(acceptHeader, forHTTPHeaderField: "accept") let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" request.setValue(userAgent, forHTTPHeaderField: "user-agent") request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") let origin = self.originURL(from: url) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue( self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString, forHTTPHeaderField: "referer") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { return try MiniMaxUsageParser.parseCodingPlanRemains(data: data, now: now) } let html = String(data: data, encoding: .utf8) ?? "" if html.contains("__NEXT_DATA__") { Self.log.debug("MiniMax coding plan HTML contains __NEXT_DATA__") } if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func fetchCodingPlanRemains( cookie: String, remainsContext: RemainsContext, region: MiniMaxAPIRegion, environment: [String: String], now: Date) async throws -> MiniMaxUsageSnapshot { let baseRemainsURL = self.resolveRemainsURL(region: region, environment: environment) let remainsURL = self.appendGroupID(remainsContext.groupID, to: baseRemainsURL) var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue(cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = remainsContext.authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } let acceptHeader = "application/json, text/plain, */*" request.setValue(acceptHeader, forHTTPHeaderField: "accept") request.setValue("XMLHttpRequest", forHTTPHeaderField: "x-requested-with") let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" request.setValue(userAgent, forHTTPHeaderField: "user-agent") request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") let origin = self.originURL(from: baseRemainsURL) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue( self.resolveCodingPlanRefererURL(region: region, environment: environment).absoluteString, forHTTPHeaderField: "referer") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw MiniMaxUsageError.networkError("Invalid response") } guard httpResponse.statusCode == 200 else { let body = String(data: data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(httpResponse.statusCode): \(body)") if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(httpResponse.statusCode)") } if let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let payload = try MiniMaxUsageParser.decodePayload(data: data) self.logCodingPlanStatus(payload: payload) return try MiniMaxUsageParser.parseCodingPlanRemains(payload: payload, now: now) } let html = String(data: data, encoding: .utf8) ?? "" if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func appendGroupID(_ groupID: String?, to url: URL) -> URL { guard let groupID, !groupID.isEmpty else { return url } guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } var queryItems = components.queryItems ?? [] queryItems.append(URLQueryItem(name: "GroupId", value: groupID)) components.queryItems = queryItems return components.url ?? url } static func originURL(from url: URL) -> URL { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } components.path = "" components.query = nil components.fragment = nil return components.url ?? url } static func resolveCodingPlanURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) { return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanPath, query: Self.codingPlanQuery) { return hostURL } return region.codingPlanURL } static func resolveCodingPlanRefererURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.codingPlanURL(environment: environment) { if var components = URLComponents(url: override, resolvingAgainstBaseURL: false) { components.query = nil return components.url ?? override } return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanPath) { return hostURL } return region.codingPlanRefererURL } static func resolveRemainsURL( region: MiniMaxAPIRegion, environment: [String: String]) -> URL { if let override = MiniMaxSettingsReader.remainsURL(environment: environment) { return override } if let host = MiniMaxSettingsReader.hostOverride(environment: environment), let hostURL = self.url(from: host, path: Self.codingPlanRemainsPath) { return hostURL } return region.remainsURL } static func url(from raw: String, path: String? = nil, query: String? = nil) -> URL? { guard let cleaned = MiniMaxSettingsReader.cleaned(raw) else { return nil } func compose(_ base: URL) -> URL? { var components = URLComponents(url: base, resolvingAgainstBaseURL: false)! if let path { components.path = "/" + path } if let query { components.query = query } return components.url } if let url = URL(string: cleaned), url.scheme != nil { if let composed = compose(url) { return composed } return url } guard let base = URL(string: "https://\(cleaned)") else { return nil } return compose(base) } private static func logCodingPlanStatus(payload: MiniMaxCodingPlanPayload) { let baseResponse = payload.data.baseResp ?? payload.baseResp guard let status = baseResponse?.statusCode else { return } let message = baseResponse?.statusMessage ?? "" if !message.isEmpty { Self.log.debug("MiniMax coding plan status \(status): \(message)") } else { Self.log.debug("MiniMax coding plan status \(status)") } } private static func looksSignedOut(html: String) -> Bool { let lower = html.lowercased() return lower.contains("sign in") || lower.contains("log in") || lower.contains("登录") || lower.contains("登入") } } struct MiniMaxCodingPlanPayload: Decodable, Sendable { let baseResp: MiniMaxBaseResponse? let data: MiniMaxCodingPlanData private enum CodingKeys: String, CodingKey { case baseResp = "base_resp" case data } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp) if container.contains(.data) { let dataDecoder = try container.superDecoder(forKey: .data) self.data = try MiniMaxCodingPlanData(from: dataDecoder) } else { self.data = try MiniMaxCodingPlanData(from: decoder) } } } struct MiniMaxCodingPlanData: Decodable, Sendable { let baseResp: MiniMaxBaseResponse? let currentSubscribeTitle: String? let planName: String? let comboTitle: String? let currentPlanTitle: String? let currentComboCard: MiniMaxComboCard? 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 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.modelRemains = try (container.decodeIfPresent([MiniMaxModelRemains].self, forKey: .modelRemains)) ?? [] } } struct MiniMaxComboCard: Decodable, Sendable { let title: String? } struct MiniMaxModelRemains: Decodable, Sendable { let currentIntervalTotalCount: Int? let currentIntervalUsageCount: Int? let startTime: Int? let endTime: Int? let remainsTime: Int? private enum CodingKeys: String, CodingKey { case currentIntervalTotalCount = "current_interval_total_count" case currentIntervalUsageCount = "current_interval_usage_count" case startTime = "start_time" case endTime = "end_time" case remainsTime = "remains_time" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) 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) } } struct MiniMaxBaseResponse: Decodable, Sendable { 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) } } 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 } } enum MiniMaxUsageParser { static func decodePayload(data: Data) throws -> MiniMaxCodingPlanPayload { let decoder = JSONDecoder() return try decoder.decode(MiniMaxCodingPlanPayload.self, from: data) } static func decodePayload(json: [String: Any]) throws -> MiniMaxCodingPlanPayload { let data = try JSONSerialization.data(withJSONObject: json, options: []) return try self.decodePayload(data: data) } static func parseCodingPlanRemains(data: Data, now: Date = Date()) throws -> MiniMaxUsageSnapshot { let payload = try self.decodePayload(data: data) return try self.parseCodingPlanRemains(payload: payload, now: now) } static func parse(html: String, now: Date = Date()) throws -> MiniMaxUsageSnapshot { if let snapshot = self.parseNextData(html: html, now: now) { return snapshot } let text = self.stripHTML(html) let planName = self.parsePlanName(html: html, text: text) let available = self.parseAvailableUsage(text: text) let usedPercent = self.parseUsedPercent(text: text) let resetsAt = self.parseResetsAt(text: text, now: now) if planName == nil, available == nil, usedPercent == nil { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } return MiniMaxUsageSnapshot( planName: planName, availablePrompts: available?.prompts, currentPrompts: nil, remainingPrompts: nil, windowMinutes: available?.windowMinutes, usedPercent: usedPercent, resetsAt: resetsAt, updatedAt: now) } static func parseCodingPlanRemains( payload: MiniMaxCodingPlanPayload, now: Date = Date()) throws -> MiniMaxUsageSnapshot { let baseResponse = payload.data.baseResp ?? payload.baseResp if let status = baseResponse?.statusCode, status != 0 { let message = baseResponse?.statusMessage ?? "status_code \(status)" let lower = message.lowercased() if status == 1004 || lower.contains("cookie") || lower.contains("log in") || lower.contains("login") { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError(message) } guard let first = payload.data.modelRemains.first else { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } let total = first.currentIntervalTotalCount let remaining = first.currentIntervalUsageCount let usedPercent = self.usedPercent(total: total, remaining: remaining) let windowMinutes = self.windowMinutes( start: self.dateFromEpoch(first.startTime), end: self.dateFromEpoch(first.endTime)) let resetsAt = self.resetsAt( end: self.dateFromEpoch(first.endTime), remains: first.remainsTime, now: now) let planName = self.parsePlanName(data: payload.data) if planName == nil, total == nil, usedPercent == nil { throw MiniMaxUsageError.parseFailed("Missing coding plan data.") } let currentPrompts: Int? = if let total, let remaining { max(0, total - remaining) } else { nil } return MiniMaxUsageSnapshot(