import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public struct MiniMaxUsageFetcher: Sendable { static let log = CodexBarLog.logger(LogCategories.minimaxUsage) 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 tokenPlanRemainsPath = "v1/token_plan/remains" private static let billingHistoryPath = "account/amount" private static let billingHistoryLimit = 100 private struct RemainsContext { let authorizationToken: String? let groupID: String? } struct WebFetchContext { let cookie: String let authorizationToken: String? let region: MiniMaxAPIRegion let environment: [String: String] let transport: any ProviderHTTPTransport } public static func fetchUsage( cookieHeader: String, authorizationToken: String? = nil, groupID: String? = nil, region: MiniMaxAPIRegion = .global, environment: [String: String] = ProcessInfo.processInfo.environment, includeBillingHistory: Bool = true, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else { throw MiniMaxUsageError.invalidCredentials } if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { throw ProviderEndpointOverrideError.minimax(rejectedKey) } let context = WebFetchContext( cookie: cookie, authorizationToken: authorizationToken, region: region, environment: environment, transport: transport) do { let snapshot = try await self.attachingSubscriptionMetadataIfAvailable( to: self.fetchCodingPlanHTML(context: context, now: now), context: context, groupID: groupID) return try await self.attachingBillingIfAvailable( to: snapshot, context: context, includeBillingHistory: includeBillingHistory, now: now) } catch let error as MiniMaxUsageError { if case .parseFailed = error { Self.log.debug("MiniMax coding plan HTML parse failed, trying remains API") let snapshot = try await self.attachingSubscriptionMetadataIfAvailable( to: self.fetchCodingPlanRemains( context: context, remainsContext: RemainsContext( authorizationToken: authorizationToken, groupID: groupID), now: now), context: context, groupID: groupID) return try await self.attachingBillingIfAvailable( to: snapshot, context: context, includeBillingHistory: includeBillingHistory, now: now) } throw error } } public static func fetchUsage( apiToken: String, region: MiniMaxAPIRegion = .global, now: Date = Date(), session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> MiniMaxUsageSnapshot { let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { throw MiniMaxUsageError.invalidCredentials } // Historically, MiniMax API token fetching used a China endpoint by default in some configurations. If the // user has no persisted region and we default to `.global`, retry the China endpoint when the global host // rejects the token so upgrades don't regress existing setups. if region != .global { return try await self.fetchUsageOnce(apiToken: cleaned, region: region, now: now, transport: transport) } do { return try await self.fetchUsageOnce( apiToken: cleaned, region: .global, now: now, transport: transport) } catch let error as MiniMaxUsageError { guard case .invalidCredentials = error else { throw error } Self.log.debug("MiniMax API token rejected for global host, retrying China mainland host") do { return try await self.fetchUsageOnce( apiToken: cleaned, region: .chinaMainland, now: now, transport: transport) } catch { // Preserve the original invalid-credentials error so the fetch pipeline can fall back to web. Self.log.debug("MiniMax China mainland retry failed, preserving global invalidCredentials") throw MiniMaxUsageError.invalidCredentials } } } private static func fetchUsageOnce( apiToken: String, region: MiniMaxAPIRegion, now: Date, transport: any ProviderHTTPTransport) async throws -> MiniMaxUsageSnapshot { var lastError: Error? var tokenPlanCredentialFailure = false for remainsURL in [region.tokenPlanRemainsURL, region.apiRemainsURL] { do { return try await self.fetchAPIUsageOnce( apiToken: apiToken, remainsURL: remainsURL, now: now, transport: transport) } catch let error as MiniMaxUsageError { lastError = error guard remainsURL == region.tokenPlanRemainsURL, self.shouldTryLegacyAPIEndpoint(after: error) else { if tokenPlanCredentialFailure { throw MiniMaxUsageError.invalidCredentials } throw error } if case .invalidCredentials = error { tokenPlanCredentialFailure = true } Self.log.debug("MiniMax token-plan API failed, trying legacy coding-plan endpoint") } } if let lastError { throw lastError } throw MiniMaxUsageError.parseFailed("Missing MiniMax API remains URL.") } private static func fetchAPIUsageOnce( apiToken: String, remainsURL: URL, now: Date, transport: any ProviderHTTPTransport) async throws -> MiniMaxUsageSnapshot { var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("CodexBar", forHTTPHeaderField: "MM-API-Source") let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw self.normalizedAPITokenError(error) } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } private static func normalizedAPITokenError(_ error: MiniMaxUsageError) -> MiniMaxUsageError { guard case let .apiError(message) = error else { return error } let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return normalizedMessage == "invalid api key" ? .invalidCredentials : error } private static func shouldTryLegacyAPIEndpoint(after error: MiniMaxUsageError) -> Bool { switch error { case .invalidCredentials: true case let .apiError(message): message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true } } private static func fetchCodingPlanHTML( context: WebFetchContext, now: Date) async throws -> MiniMaxUsageSnapshot { let url = self.resolveCodingPlanURL(region: context.region, environment: context.environment) var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(context.cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = context.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: context.region, environment: context.environment).absoluteString, forHTTPHeaderField: "referer") let response: ProviderHTTPResponse do { response = try await context.transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } if let contentType = response.response.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw error } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } let html = String(data: response.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( context: WebFetchContext, remainsContext: RemainsContext, now: Date) async throws -> MiniMaxUsageSnapshot { var lastError: Error? for baseRemainsURL in self.resolveRemainsURLs(region: context.region, environment: context.environment) { do { return try await self.fetchCodingPlanRemainsOnce( baseRemainsURL: baseRemainsURL, context: context, remainsContext: remainsContext, now: now) } catch let error as MiniMaxUsageError { lastError = error guard self.shouldTryNextRemainsURL(after: error) else { throw error } Self.log.debug("MiniMax remains API failed for \(baseRemainsURL.host ?? "unknown host"), trying next") } } if let lastError { throw lastError } throw MiniMaxUsageError.parseFailed("Missing MiniMax remains URL.") } private static func fetchCodingPlanRemainsOnce( baseRemainsURL: URL, context: WebFetchContext, remainsContext: RemainsContext, now: Date) async throws -> MiniMaxUsageSnapshot { let remainsURL = self.appendGroupID(remainsContext.groupID, to: baseRemainsURL) var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue(context.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: context.region, environment: context.environment).absoluteString, forHTTPHeaderField: "referer") let response: ProviderHTTPResponse do { response = try await context.transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } if let contentType = response.response.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw error } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } let html = String(data: response.data, encoding: .utf8) ?? "" if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func shouldTryNextRemainsURL(after error: MiniMaxUsageError) -> Bool { switch error { case .invalidCredentials: false case let .apiError(message): message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true } } private static func normalizedTransportError(_ error: Error) -> Error { if error is MiniMaxUsageError || error is CancellationError { return error } if let urlError = error as? URLError { if urlError.code == .cancelled { return error } if urlError.code == .badServerResponse { return MiniMaxUsageError.networkError("Invalid response") } return MiniMaxUsageError.networkError(urlError.localizedDescription) } return error } private static func attachingBillingIfAvailable( to snapshot: MiniMaxUsageSnapshot, context: WebFetchContext, includeBillingHistory: Bool, now: Date) async throws -> MiniMaxUsageSnapshot { guard includeBillingHistory else { return snapshot } do { let billing = try await self.fetchBillingSummary(context: context, now: now) return snapshot.withBillingSummary(billing) } catch is CancellationError { throw CancellationError() } catch let error as URLError where error.code == .cancelled { throw error } catch let error as MiniMaxUsageError { if case .invalidCredentials = error, context.authorizationToken != nil { throw error } Self.log.debug("MiniMax billing history unavailable: \(error.localizedDescription)") return snapshot } catch { Self.log.debug("MiniMax billing history unavailable: \(error.localizedDescription)") return snapshot } } private static func fetchBillingSummary(context: WebFetchContext, now: Date) async throws -> MiniMaxBillingSummary { var records: [MiniMaxBillingRecord] = [] var totalCount: Int? var page = 1 while true { let url = self.resolveBillingHistoryURL( region: context.region, environment: context.environment, page: page, limit: Self.billingHistoryLimit) let response = try await self.billingHistoryResponse(url: url, context: context) guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.debug("MiniMax billing history returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } let payload = try MiniMaxBillingHistoryParser.decodePayload(data: response.data) if let status = payload.baseResp?.statusCode, status != 0 { let message = payload.baseResp?.statusMessage ?? "status_code \(status)" throw MiniMaxUsageError.apiError(message) } totalCount = payload.totalCount ?? totalCount guard !payload.chargeRecords.isEmpty else { break } records.append(contentsOf: payload.chargeRecords) if MiniMaxBillingHistoryParser.containsRecordBefore30DayWindow(payload.chargeRecords, now: now) { break } if let totalCount, records.count >= totalCount { break } page += 1 } return MiniMaxBillingHistoryParser.aggregate(records: records, now: now) } private static func billingHistoryResponse( url: URL, context: WebFetchContext) async throws -> ProviderHTTPResponse { var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(context.cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = context.authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } request.setValue("application/json, text/plain, */*", 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: url) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue(origin.appendingPathComponent("account").absoluteString, forHTTPHeaderField: "referer") do { return try await context.transport.response(for: request) } catch let error as URLError where error.code == .badServerResponse { throw MiniMaxUsageError.networkError("Invalid response") } catch { throw error } } 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 = queryItemsLines 1–500 of 1689. Load more
import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public struct MiniMaxUsageFetcher: Sendable { static let log = CodexBarLog.logger(LogCategories.minimaxUsage) 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 tokenPlanRemainsPath = "v1/token_plan/remains" private static let billingHistoryPath = "account/amount" private static let billingHistoryLimit = 100 private struct RemainsContext { let authorizationToken: String? let groupID: String? } struct WebFetchContext { let cookie: String let authorizationToken: String? let region: MiniMaxAPIRegion let environment: [String: String] let transport: any ProviderHTTPTransport } public static func fetchUsage( cookieHeader: String, authorizationToken: String? = nil, groupID: String? = nil, region: MiniMaxAPIRegion = .global, environment: [String: String] = ProcessInfo.processInfo.environment, includeBillingHistory: Bool = true, session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, now: Date = Date()) async throws -> MiniMaxUsageSnapshot { guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else { throw MiniMaxUsageError.invalidCredentials } if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { throw ProviderEndpointOverrideError.minimax(rejectedKey) } let context = WebFetchContext( cookie: cookie, authorizationToken: authorizationToken, region: region, environment: environment, transport: transport) do { let snapshot = try await self.attachingSubscriptionMetadataIfAvailable( to: self.fetchCodingPlanHTML(context: context, now: now), context: context, groupID: groupID) return try await self.attachingBillingIfAvailable( to: snapshot, context: context, includeBillingHistory: includeBillingHistory, now: now) } catch let error as MiniMaxUsageError { if case .parseFailed = error { Self.log.debug("MiniMax coding plan HTML parse failed, trying remains API") let snapshot = try await self.attachingSubscriptionMetadataIfAvailable( to: self.fetchCodingPlanRemains( context: context, remainsContext: RemainsContext( authorizationToken: authorizationToken, groupID: groupID), now: now), context: context, groupID: groupID) return try await self.attachingBillingIfAvailable( to: snapshot, context: context, includeBillingHistory: includeBillingHistory, now: now) } throw error } } public static func fetchUsage( apiToken: String, region: MiniMaxAPIRegion = .global, now: Date = Date(), session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> MiniMaxUsageSnapshot { let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { throw MiniMaxUsageError.invalidCredentials } // Historically, MiniMax API token fetching used a China endpoint by default in some configurations. If the // user has no persisted region and we default to `.global`, retry the China endpoint when the global host // rejects the token so upgrades don't regress existing setups. if region != .global { return try await self.fetchUsageOnce(apiToken: cleaned, region: region, now: now, transport: transport) } do { return try await self.fetchUsageOnce( apiToken: cleaned, region: .global, now: now, transport: transport) } catch let error as MiniMaxUsageError { guard case .invalidCredentials = error else { throw error } Self.log.debug("MiniMax API token rejected for global host, retrying China mainland host") do { return try await self.fetchUsageOnce( apiToken: cleaned, region: .chinaMainland, now: now, transport: transport) } catch { // Preserve the original invalid-credentials error so the fetch pipeline can fall back to web. Self.log.debug("MiniMax China mainland retry failed, preserving global invalidCredentials") throw MiniMaxUsageError.invalidCredentials } } } private static func fetchUsageOnce( apiToken: String, region: MiniMaxAPIRegion, now: Date, transport: any ProviderHTTPTransport) async throws -> MiniMaxUsageSnapshot { var lastError: Error? var tokenPlanCredentialFailure = false for remainsURL in [region.tokenPlanRemainsURL, region.apiRemainsURL] { do { return try await self.fetchAPIUsageOnce( apiToken: apiToken, remainsURL: remainsURL, now: now, transport: transport) } catch let error as MiniMaxUsageError { lastError = error guard remainsURL == region.tokenPlanRemainsURL, self.shouldTryLegacyAPIEndpoint(after: error) else { if tokenPlanCredentialFailure { throw MiniMaxUsageError.invalidCredentials } throw error } if case .invalidCredentials = error { tokenPlanCredentialFailure = true } Self.log.debug("MiniMax token-plan API failed, trying legacy coding-plan endpoint") } } if let lastError { throw lastError } throw MiniMaxUsageError.parseFailed("Missing MiniMax API remains URL.") } private static func fetchAPIUsageOnce( apiToken: String, remainsURL: URL, now: Date, transport: any ProviderHTTPTransport) async throws -> MiniMaxUsageSnapshot { var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("CodexBar", forHTTPHeaderField: "MM-API-Source") let response: ProviderHTTPResponse do { response = try await transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw self.normalizedAPITokenError(error) } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } private static func normalizedAPITokenError(_ error: MiniMaxUsageError) -> MiniMaxUsageError { guard case let .apiError(message) = error else { return error } let normalizedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return normalizedMessage == "invalid api key" ? .invalidCredentials : error } private static func shouldTryLegacyAPIEndpoint(after error: MiniMaxUsageError) -> Bool { switch error { case .invalidCredentials: true case let .apiError(message): message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true } } private static func fetchCodingPlanHTML( context: WebFetchContext, now: Date) async throws -> MiniMaxUsageSnapshot { let url = self.resolveCodingPlanURL(region: context.region, environment: context.environment) var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(context.cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = context.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: context.region, environment: context.environment).absoluteString, forHTTPHeaderField: "referer") let response: ProviderHTTPResponse do { response = try await context.transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } if let contentType = response.response.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw error } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } let html = String(data: response.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( context: WebFetchContext, remainsContext: RemainsContext, now: Date) async throws -> MiniMaxUsageSnapshot { var lastError: Error? for baseRemainsURL in self.resolveRemainsURLs(region: context.region, environment: context.environment) { do { return try await self.fetchCodingPlanRemainsOnce( baseRemainsURL: baseRemainsURL, context: context, remainsContext: remainsContext, now: now) } catch let error as MiniMaxUsageError { lastError = error guard self.shouldTryNextRemainsURL(after: error) else { throw error } Self.log.debug("MiniMax remains API failed for \(baseRemainsURL.host ?? "unknown host"), trying next") } } if let lastError { throw lastError } throw MiniMaxUsageError.parseFailed("Missing MiniMax remains URL.") } private static func fetchCodingPlanRemainsOnce( baseRemainsURL: URL, context: WebFetchContext, remainsContext: RemainsContext, now: Date) async throws -> MiniMaxUsageSnapshot { let remainsURL = self.appendGroupID(remainsContext.groupID, to: baseRemainsURL) var request = URLRequest(url: remainsURL) request.httpMethod = "GET" request.setValue(context.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: context.region, environment: context.environment).absoluteString, forHTTPHeaderField: "referer") let response: ProviderHTTPResponse do { response = try await context.transport.response(for: request) } catch { throw self.normalizedTransportError(error) } guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.error("MiniMax returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } if let contentType = response.response.value(forHTTPHeaderField: "Content-Type"), contentType.lowercased().contains("application/json") { let snapshot: MiniMaxUsageSnapshot do { snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: response.data, now: now) } catch let error as MiniMaxUsageError { throw error } catch { throw MiniMaxUsageError.parseFailed(error.localizedDescription) } if let services = snapshot.services, !services.isEmpty { Self.log.debug("MiniMax multi-service response detected: \(services.count) services") } return snapshot } let html = String(data: response.data, encoding: .utf8) ?? "" if self.looksSignedOut(html: html) { throw MiniMaxUsageError.invalidCredentials } return try MiniMaxUsageParser.parse(html: html, now: now) } private static func shouldTryNextRemainsURL(after error: MiniMaxUsageError) -> Bool { switch error { case .invalidCredentials: false case let .apiError(message): message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true } } private static func normalizedTransportError(_ error: Error) -> Error { if error is MiniMaxUsageError || error is CancellationError { return error } if let urlError = error as? URLError { if urlError.code == .cancelled { return error } if urlError.code == .badServerResponse { return MiniMaxUsageError.networkError("Invalid response") } return MiniMaxUsageError.networkError(urlError.localizedDescription) } return error } private static func attachingBillingIfAvailable( to snapshot: MiniMaxUsageSnapshot, context: WebFetchContext, includeBillingHistory: Bool, now: Date) async throws -> MiniMaxUsageSnapshot { guard includeBillingHistory else { return snapshot } do { let billing = try await self.fetchBillingSummary(context: context, now: now) return snapshot.withBillingSummary(billing) } catch is CancellationError { throw CancellationError() } catch let error as URLError where error.code == .cancelled { throw error } catch let error as MiniMaxUsageError { if case .invalidCredentials = error, context.authorizationToken != nil { throw error } Self.log.debug("MiniMax billing history unavailable: \(error.localizedDescription)") return snapshot } catch { Self.log.debug("MiniMax billing history unavailable: \(error.localizedDescription)") return snapshot } } private static func fetchBillingSummary(context: WebFetchContext, now: Date) async throws -> MiniMaxBillingSummary { var records: [MiniMaxBillingRecord] = [] var totalCount: Int? var page = 1 while true { let url = self.resolveBillingHistoryURL( region: context.region, environment: context.environment, page: page, limit: Self.billingHistoryLimit) let response = try await self.billingHistoryResponse(url: url, context: context) guard response.statusCode == 200 else { let body = String(data: response.data, encoding: .utf8) ?? "" Self.log.debug("MiniMax billing history returned \(response.statusCode): \(body)") if response.statusCode == 401 || response.statusCode == 403 { throw MiniMaxUsageError.invalidCredentials } throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") } let payload = try MiniMaxBillingHistoryParser.decodePayload(data: response.data) if let status = payload.baseResp?.statusCode, status != 0 { let message = payload.baseResp?.statusMessage ?? "status_code \(status)" throw MiniMaxUsageError.apiError(message) } totalCount = payload.totalCount ?? totalCount guard !payload.chargeRecords.isEmpty else { break } records.append(contentsOf: payload.chargeRecords) if MiniMaxBillingHistoryParser.containsRecordBefore30DayWindow(payload.chargeRecords, now: now) { break } if let totalCount, records.count >= totalCount { break } page += 1 } return MiniMaxBillingHistoryParser.aggregate(records: records, now: now) } private static func billingHistoryResponse( url: URL, context: WebFetchContext) async throws -> ProviderHTTPResponse { var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(context.cookie, forHTTPHeaderField: "Cookie") if let authorizationToken = context.authorizationToken { request.setValue("Bearer \(authorizationToken)", forHTTPHeaderField: "Authorization") } request.setValue("application/json, text/plain, */*", 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: url) request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") request.setValue(origin.appendingPathComponent("account").absoluteString, forHTTPHeaderField: "referer") do { return try await context.transport.response(for: request) } catch let error as URLError where error.code == .badServerResponse { throw MiniMaxUsageError.networkError("Invalid response") } catch { throw error } } 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