370 lines · 14.4 KB · Swift
RawDownload
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),                confettiPalette: [                    ProviderColor(hex: 0x181E25),                    ProviderColor(hex: 0x86909C),                    ProviderColor(hex: 0xF7F8FA),                ]),            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    }}