Files
steipete--codexbar/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift
T
2026-07-13 12:22:33 +08:00

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
}
}