366 lines
14 KiB
Swift
366 lines
14 KiB
Swift
import Foundation
|
|
|
|
public enum MiniMaxProviderDescriptor {
|
|
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
|
|
|
|
static func makeDescriptor() -> ProviderDescriptor {
|
|
ProviderDescriptor(
|
|
id: .minimax,
|
|
metadata: ProviderMetadata(
|
|
id: .minimax,
|
|
displayName: "MiniMax",
|
|
sessionLabel: "Prompts",
|
|
weeklyLabel: "Window",
|
|
opusLabel: nil,
|
|
supportsOpus: false,
|
|
supportsCredits: false,
|
|
creditsHint: "",
|
|
toggleTitle: "Show MiniMax usage",
|
|
cliName: "minimax",
|
|
defaultEnabled: false,
|
|
isPrimaryProvider: false,
|
|
usesAccountFallback: false,
|
|
browserCookieOrder: ProviderBrowserCookieDefaults.defaultImportOrder,
|
|
dashboardURL: "https://platform.minimax.io/user-center/payment/coding-plan?cycle_type=3",
|
|
statusPageURL: nil),
|
|
branding: ProviderBranding(
|
|
iconStyle: .minimax,
|
|
iconResourceName: "ProviderIcon-minimax",
|
|
color: ProviderColor(red: 254 / 255, green: 96 / 255, blue: 60 / 255)),
|
|
tokenCost: ProviderTokenCostConfig(
|
|
supportsTokenCost: false,
|
|
noDataMessage: { "MiniMax cost summary is not supported." }),
|
|
fetchPlan: ProviderFetchPlan(
|
|
sourceModes: [.auto, .web, .api],
|
|
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
|
|
cli: ProviderCLIConfig(
|
|
name: "minimax",
|
|
aliases: ["mini-max"],
|
|
versionDetector: nil))
|
|
}
|
|
|
|
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
|
|
switch context.sourceMode {
|
|
case .web:
|
|
return [MiniMaxCodingPlanFetchStrategy()]
|
|
case .api:
|
|
return [MiniMaxAPIFetchStrategy()]
|
|
case .cli, .oauth:
|
|
return []
|
|
case .auto:
|
|
break
|
|
}
|
|
let apiToken = ProviderTokenResolver.minimaxToken(environment: context.env)
|
|
let apiKeyKind = MiniMaxAPISettingsReader.apiKeyKind(token: apiToken)
|
|
let authMode = MiniMaxAuthMode.resolve(
|
|
apiToken: apiToken,
|
|
cookieHeader: ProviderTokenResolver.minimaxCookie(environment: context.env))
|
|
if authMode.usesAPIToken {
|
|
if apiKeyKind == .standard {
|
|
return [MiniMaxCodingPlanFetchStrategy()]
|
|
}
|
|
return [MiniMaxAPIFetchStrategy(), MiniMaxCodingPlanFetchStrategy()]
|
|
}
|
|
return [MiniMaxCodingPlanFetchStrategy()]
|
|
}
|
|
}
|
|
|
|
struct MiniMaxAPIFetchStrategy: ProviderFetchStrategy {
|
|
let id: String = "minimax.api"
|
|
let kind: ProviderFetchKind = .apiToken
|
|
|
|
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
|
|
let authMode = MiniMaxAuthMode.resolve(
|
|
apiToken: ProviderTokenResolver.minimaxToken(environment: context.env),
|
|
cookieHeader: ProviderTokenResolver.minimaxCookie(environment: context.env))
|
|
if let kind = MiniMaxAPISettingsReader.apiKeyKind(environment: context.env),
|
|
kind == .standard
|
|
{
|
|
return false
|
|
}
|
|
return authMode.usesAPIToken
|
|
}
|
|
|
|
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
|
|
guard let apiToken = ProviderTokenResolver.minimaxToken(environment: context.env) else {
|
|
throw MiniMaxAPISettingsError.missingToken
|
|
}
|
|
let region = context.settings?.minimax?.apiRegion ?? .global
|
|
let usage = try await MiniMaxUsageFetcher.fetchUsage(apiToken: apiToken, region: region)
|
|
return self.makeResult(
|
|
usage: usage.toUsageSnapshot(),
|
|
sourceLabel: "api")
|
|
}
|
|
|
|
func shouldFallback(on error: Error, context _: ProviderFetchContext) -> Bool {
|
|
guard let minimaxError = error as? MiniMaxUsageError else { return false }
|
|
switch minimaxError {
|
|
case .invalidCredentials:
|
|
return true
|
|
case let .apiError(message):
|
|
return message.contains("HTTP 404")
|
|
case .networkError, .parseFailed:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MiniMaxCodingPlanFetchStrategy: ProviderFetchStrategy {
|
|
let id: String = "minimax.web"
|
|
let kind: ProviderFetchKind = .web
|
|
private static let log = CodexBarLog.logger(LogCategories.minimaxWeb)
|
|
|
|
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
|
|
if Self.resolveCookieOverride(context: context) != nil {
|
|
return true
|
|
}
|
|
#if os(macOS)
|
|
if let cached = CookieHeaderCache.load(provider: .minimax),
|
|
!cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
{
|
|
return true
|
|
}
|
|
guard Self.allowsBrowserCookieImport(context: context) else {
|
|
return false
|
|
}
|
|
return MiniMaxCookieImporter.hasSession(browserDetection: context.browserDetection)
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
|
|
let fetchContext = FetchContext(
|
|
region: context.settings?.minimax?.apiRegion ?? .global,
|
|
environment: context.env,
|
|
includeBillingHistory: context.includeOptionalUsage)
|
|
if let override = Self.resolveCookieOverride(context: context) {
|
|
Self.log.debug("Using MiniMax cookie header from settings/env")
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: override.cookieHeader,
|
|
authorizationToken: override.authorizationToken,
|
|
groupID: override.groupID,
|
|
region: fetchContext.region,
|
|
environment: fetchContext.environment,
|
|
includeBillingHistory: context.includeOptionalUsage)
|
|
return self.makeResult(
|
|
usage: snapshot.toUsageSnapshot(),
|
|
sourceLabel: "web")
|
|
}
|
|
|
|
#if os(macOS)
|
|
let tokenContext = Self.loadTokenContext(browserDetection: context.browserDetection)
|
|
|
|
var lastError: Error?
|
|
if let cached = CookieHeaderCache.load(provider: .minimax),
|
|
!cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
{
|
|
switch await Self.attemptFetch(
|
|
cookieHeader: cached.cookieHeader,
|
|
sourceLabel: cached.sourceLabel,
|
|
tokenContext: tokenContext,
|
|
logLabel: "cached",
|
|
fetchContext: fetchContext)
|
|
{
|
|
case let .success(snapshot):
|
|
return self.makeResult(
|
|
usage: snapshot.toUsageSnapshot(),
|
|
sourceLabel: "web")
|
|
case let .failure(error):
|
|
lastError = error
|
|
if Self.shouldTryNextBrowser(for: error) {
|
|
CookieHeaderCache.clear(provider: .minimax)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
guard Self.allowsBrowserCookieImport(context: context) else {
|
|
if let lastError { throw lastError }
|
|
throw MiniMaxSettingsError.missingCookie
|
|
}
|
|
|
|
let sessions = (try? MiniMaxCookieImporter.importSessions(
|
|
browserDetection: context.browserDetection)) ?? []
|
|
guard !sessions.isEmpty else {
|
|
if let lastError { throw lastError }
|
|
throw MiniMaxSettingsError.missingCookie
|
|
}
|
|
|
|
for session in sessions {
|
|
switch await Self.attemptFetch(
|
|
cookieHeader: session.cookieHeader,
|
|
sourceLabel: session.sourceLabel,
|
|
tokenContext: tokenContext,
|
|
logLabel: "",
|
|
fetchContext: fetchContext)
|
|
{
|
|
case let .success(snapshot):
|
|
CookieHeaderCache.store(
|
|
provider: .minimax,
|
|
cookieHeader: session.cookieHeader,
|
|
sourceLabel: session.sourceLabel)
|
|
return self.makeResult(
|
|
usage: snapshot.toUsageSnapshot(),
|
|
sourceLabel: "web")
|
|
case let .failure(error):
|
|
lastError = error
|
|
if Self.shouldTryNextBrowser(for: error) {
|
|
Self.log.debug("MiniMax cookies invalid from \(session.sourceLabel), trying next browser")
|
|
continue
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
if let lastError {
|
|
throw lastError
|
|
}
|
|
#endif
|
|
|
|
throw MiniMaxSettingsError.missingCookie
|
|
}
|
|
|
|
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
|
|
false
|
|
}
|
|
|
|
static func allowsBrowserCookieImport(context: ProviderFetchContext) -> Bool {
|
|
context.runtime == .app && ProviderInteractionContext.current == .userInitiated
|
|
}
|
|
|
|
private struct TokenContext {
|
|
let tokensByLabel: [String: [String]]
|
|
let groupIDByLabel: [String: String]
|
|
}
|
|
|
|
private struct FetchContext {
|
|
let region: MiniMaxAPIRegion
|
|
let environment: [String: String]
|
|
let includeBillingHistory: Bool
|
|
}
|
|
|
|
private enum FetchAttemptResult {
|
|
case success(MiniMaxUsageSnapshot)
|
|
case failure(Error)
|
|
}
|
|
|
|
private static func resolveCookieOverride(context: ProviderFetchContext) -> MiniMaxCookieOverride? {
|
|
if let settings = context.settings?.minimax {
|
|
guard settings.cookieSource == .manual else { return nil }
|
|
return MiniMaxCookieHeader.override(from: settings.manualCookieHeader)
|
|
}
|
|
guard let raw = ProviderTokenResolver.minimaxCookie(environment: context.env) else {
|
|
return nil
|
|
}
|
|
return MiniMaxCookieHeader.override(from: raw)
|
|
}
|
|
|
|
private static func normalizeStorageLabel(_ label: String) -> String {
|
|
let suffixes = [" (Session Storage)", " (IndexedDB)"]
|
|
for suffix in suffixes where label.hasSuffix(suffix) {
|
|
return String(label.dropLast(suffix.count))
|
|
}
|
|
return label
|
|
}
|
|
|
|
private static func loadTokenContext(browserDetection: BrowserDetection) -> TokenContext {
|
|
#if os(macOS)
|
|
let tokenLog: (String) -> Void = { msg in Self.log.debug(msg) }
|
|
let accessTokens = MiniMaxLocalStorageImporter.importAccessTokens(
|
|
browserDetection: browserDetection,
|
|
logger: tokenLog)
|
|
let groupIDs = MiniMaxLocalStorageImporter.importGroupIDs(
|
|
browserDetection: browserDetection,
|
|
logger: tokenLog)
|
|
var tokensByLabel: [String: [String]] = [:]
|
|
var groupIDByLabel: [String: String] = [:]
|
|
for token in accessTokens {
|
|
let normalized = Self.normalizeStorageLabel(token.sourceLabel)
|
|
tokensByLabel[normalized, default: []].append(token.accessToken)
|
|
if let groupID = token.groupID, groupIDByLabel[normalized] == nil {
|
|
groupIDByLabel[normalized] = groupID
|
|
}
|
|
}
|
|
for (label, groupID) in groupIDs {
|
|
let normalized = Self.normalizeStorageLabel(label)
|
|
if groupIDByLabel[normalized] == nil {
|
|
groupIDByLabel[normalized] = groupID
|
|
}
|
|
}
|
|
return TokenContext(tokensByLabel: tokensByLabel, groupIDByLabel: groupIDByLabel)
|
|
#else
|
|
_ = browserDetection
|
|
return TokenContext(tokensByLabel: [:], groupIDByLabel: [:])
|
|
#endif
|
|
}
|
|
|
|
private static func attemptFetch(
|
|
cookieHeader: String,
|
|
sourceLabel: String,
|
|
tokenContext: TokenContext,
|
|
logLabel: String,
|
|
fetchContext: FetchContext) async -> FetchAttemptResult
|
|
{
|
|
let normalizedLabel = Self.normalizeStorageLabel(sourceLabel)
|
|
let tokenCandidates = tokenContext.tokensByLabel[normalizedLabel] ?? []
|
|
let groupID = tokenContext.groupIDByLabel[normalizedLabel]
|
|
let cookieToken = Self.cookieValue(named: "HERTZ-SESSION", in: cookieHeader)
|
|
var attempts: [String?] = tokenCandidates.map(\.self)
|
|
if let cookieToken, !tokenCandidates.contains(cookieToken) {
|
|
attempts.append(cookieToken)
|
|
}
|
|
attempts.append(nil)
|
|
|
|
let prefix = logLabel.isEmpty ? "" : "\(logLabel) "
|
|
var lastError: Error?
|
|
for token in attempts {
|
|
let tokenLabel: String = {
|
|
guard let token else { return "" }
|
|
if token == cookieToken { return " + HERTZ-SESSION bearer" }
|
|
return " + access token"
|
|
}()
|
|
Self.log.debug("Trying MiniMax \(prefix)cookies from \(sourceLabel)\(tokenLabel)")
|
|
do {
|
|
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
|
|
cookieHeader: cookieHeader,
|
|
authorizationToken: token,
|
|
groupID: groupID,
|
|
region: fetchContext.region,
|
|
environment: fetchContext.environment,
|
|
includeBillingHistory: fetchContext.includeBillingHistory)
|
|
Self.log.debug("MiniMax \(prefix)cookies valid from \(sourceLabel)")
|
|
return .success(snapshot)
|
|
} catch {
|
|
lastError = error
|
|
if Self.shouldTryNextBrowser(for: error) {
|
|
continue
|
|
}
|
|
return .failure(error)
|
|
}
|
|
}
|
|
|
|
if let lastError {
|
|
return .failure(lastError)
|
|
}
|
|
return .failure(MiniMaxSettingsError.missingCookie)
|
|
}
|
|
|
|
private static func cookieValue(named name: String, in header: String) -> String? {
|
|
let parts = header.split(separator: ";")
|
|
for part in parts {
|
|
let trimmed = part.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard trimmed.lowercased().hasPrefix("\(name.lowercased())=") else { continue }
|
|
return String(trimmed.dropFirst(name.count + 1))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func shouldTryNextBrowser(for error: Error) -> Bool {
|
|
if case MiniMaxUsageError.invalidCredentials = error { return true }
|
|
if case MiniMaxUsageError.parseFailed = error { return true }
|
|
return false
|
|
}
|
|
}
|