Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions Sources/App/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ struct SettingsContentView: View {
@State private var miniMaxConfigExpanded: Bool = false
@State private var miniMaxApiKeyInput: String = ""
@State private var miniMaxAuthEnvVarInput: String = ""
@State private var miniMaxRegion: MiniMaxRegion = .china
@State private var showMiniMaxApiKey: Bool = false
@State private var isTestingMiniMax = false
@State private var miniMaxTestResult: String?
Expand Down Expand Up @@ -243,7 +244,8 @@ struct SettingsContentView: View {
kimiProbeMode = UserDefaultsProviderSettingsRepository.shared.kimiProbeMode()

// Initialize MiniMax settings
miniMaxAuthEnvVarInput = UserDefaultsProviderSettingsRepository.shared.minimaxiAuthEnvVar()
miniMaxRegion = UserDefaultsProviderSettingsRepository.shared.minimaxRegion()
miniMaxAuthEnvVarInput = UserDefaultsProviderSettingsRepository.shared.minimaxAuthEnvVar()

// Initialize Hook settings
hooksEnabled = UserDefaultsProviderSettingsRepository.shared.isHookEnabled()
Expand Down Expand Up @@ -1093,6 +1095,27 @@ struct SettingsContentView: View {

private var miniMaxConfigForm: some View {
VStack(alignment: .leading, spacing: 14) {
// Region selector (区域选择)
VStack(alignment: .leading, spacing: 6) {
Text("REGION")
.font(.system(size: 9, weight: .semibold, design: theme.fontDesign))
.foregroundStyle(theme.textSecondary)
.tracking(0.5)

Picker("", selection: $miniMaxRegion) {
ForEach(MiniMaxRegion.allCases, id: \.self) { region in
Text(region.displayName).tag(region)
}
}
.pickerStyle(.segmented)
.onChange(of: miniMaxRegion) { _, newValue in
UserDefaultsProviderSettingsRepository.shared.setMinimaxRegion(newValue)
Task {
await monitor.refresh(providerId: ProviderID.minimax)
}
}
}

// API Key input
VStack(alignment: .leading, spacing: 6) {
HStack {
Expand All @@ -1103,7 +1126,7 @@ struct SettingsContentView: View {

Spacer()

if UserDefaultsProviderSettingsRepository.shared.hasMinimaxiApiKey() {
if UserDefaultsProviderSettingsRepository.shared.hasMinimaxApiKey() {
HStack(spacing: 3) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 9))
Expand Down Expand Up @@ -1173,7 +1196,7 @@ struct SettingsContentView: View {
)
)
.onChange(of: miniMaxAuthEnvVarInput) { _, newValue in
UserDefaultsProviderSettingsRepository.shared.setMinimaxiAuthEnvVar(newValue)
UserDefaultsProviderSettingsRepository.shared.setMinimaxAuthEnvVar(newValue)
}
}

Expand Down Expand Up @@ -1232,7 +1255,7 @@ struct SettingsContentView: View {
.font(.system(size: 9, weight: .semibold, design: theme.fontDesign))
.foregroundStyle(theme.textTertiary)

Link(destination: URL(string: "https://platform.minimaxi.com/user-center/basic-information/interface-key")!) {
Link(destination: miniMaxRegion.apiKeysURL) {
HStack(spacing: 3) {
Text("Open MiniMax API Keys")
.font(.system(size: 9, weight: .semibold, design: theme.fontDesign))
Expand All @@ -1244,9 +1267,9 @@ struct SettingsContentView: View {
}

// Delete API key
if UserDefaultsProviderSettingsRepository.shared.hasMinimaxiApiKey() {
if UserDefaultsProviderSettingsRepository.shared.hasMinimaxApiKey() {
Button {
UserDefaultsProviderSettingsRepository.shared.deleteMinimaxiApiKey()
UserDefaultsProviderSettingsRepository.shared.deleteMinimaxApiKey()
miniMaxApiKeyInput = ""
miniMaxTestResult = nil
} label: {
Expand Down Expand Up @@ -2933,10 +2956,10 @@ struct SettingsContentView: View {
miniMaxTestResult = nil

// Save current inputs
UserDefaultsProviderSettingsRepository.shared.setMinimaxiAuthEnvVar(miniMaxAuthEnvVarInput)
UserDefaultsProviderSettingsRepository.shared.setMinimaxAuthEnvVar(miniMaxAuthEnvVarInput)
if !miniMaxApiKeyInput.isEmpty {
AppLog.credentials.info("Saving MiniMax API key for connection test")
UserDefaultsProviderSettingsRepository.shared.saveMinimaxiApiKey(miniMaxApiKeyInput)
UserDefaultsProviderSettingsRepository.shared.saveMinimaxApiKey(miniMaxApiKeyInput)
miniMaxApiKeyInput = ""
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Domain/Provider/MiniMax/MiniMaxProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public final class MiniMaxProvider: AIProvider, @unchecked Sendable {
public let cliCommand: String = "" // API-only provider, no CLI (纯 API 提供者,无 CLI)

public var dashboardURL: URL? {
URL(string: "https://platform.minimaxi.com/user-center/payment/coding-plan")
settingsRepository.minimaxRegion().dashboardURL
}

public var statusPageURL: URL? {
Expand Down
58 changes: 58 additions & 0 deletions Sources/Domain/Provider/MiniMax/MiniMaxRegion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

/// Represents the MiniMax API region. (MiniMax API 区域设置)
/// - international: api.minimax.io / platform.minimax.io (For International Users)
/// - china: api.minimaxi.com / platform.minimaxi.com (For Users in China)
public enum MiniMaxRegion: String, Sendable, Equatable, CaseIterable {
case international
case china

/// Display name for the region picker (区域显示名称)
public var displayName: String {
switch self {
case .international: return "International (minimax.io)"
case .china: return "China (minimaxi.com)"
}
}

/// API base URL for coding plan remains endpoint (Coding Plan API 基础 URL)
public var apiBaseURL: String {
switch self {
case .international: return "https://api.minimax.io"
case .china: return "https://api.minimaxi.com"
}
}

/// Platform URL for dashboard (平台仪表盘 URL)
public var platformURL: String {
switch self {
case .international: return "https://platform.minimax.io"
case .china: return "https://platform.minimaxi.com"
}
}

/// URL to get API keys from the platform (获取 API Key 的页面 URL)
public var apiKeysURL: URL {
switch self {
case .international:
return URL(string: "https://platform.minimax.io/user-center/basic-information/interface-key")!
case .china:
return URL(string: "https://platform.minimaxi.com/user-center/basic-information/interface-key")!
}
}

/// Dashboard URL for coding plan payment page (Coding Plan 付费页面 URL)
public var dashboardURL: URL {
switch self {
case .international:
return URL(string: "https://platform.minimax.io/user-center/payment/coding-plan")!
case .china:
return URL(string: "https://platform.minimaxi.com/user-center/payment/coding-plan")!
}
}

/// Full API URL for the coding plan remains endpoint (Coding Plan 剩余额度 API URL)
public var codingPlanRemainsURL: String {
"\(apiBaseURL)/v1/api/openplatform/coding_plan/remains"
}
}
21 changes: 14 additions & 7 deletions Sources/Domain/Provider/ProviderSettingsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,27 +191,34 @@ public protocol KimiSettingsRepository: ProviderSettingsRepository {
}

/// MiniMax-specific settings repository, extending base ProviderSettingsRepository.
/// Stores API key configuration for MiniMax Coding Plan quota monitoring.
/// Stores API key and region configuration for MiniMax Coding Plan quota monitoring.
/// Tests can use UserDefaultsProviderSettingsRepository with test UserDefaults.
/// App uses UserDefaultsProviderSettingsRepository.
public protocol MiniMaxSettingsRepository: ProviderSettingsRepository {
/// Gets the API region (international or china, default: china for legacy compatibility)
/// (获取 API 区域设置,默认中国区以兼容旧版用户)
func minimaxRegion() -> MiniMaxRegion

/// Sets the API region (设置 API 区域)
func setMinimaxRegion(_ region: MiniMaxRegion)

/// Gets the environment variable name for MiniMax API key (empty = use default MINIMAX_API_KEY)
func minimaxiAuthEnvVar() -> String
func minimaxAuthEnvVar() -> String

/// Sets the environment variable name for MiniMax API key
func setMinimaxiAuthEnvVar(_ envVar: String)
func setMinimaxAuthEnvVar(_ envVar: String)

/// Saves the MiniMax API key (for Settings UI input)
func saveMinimaxiApiKey(_ key: String)
func saveMinimaxApiKey(_ key: String)

/// Retrieves the MiniMax API key
func getMinimaxiApiKey() -> String?
func getMinimaxApiKey() -> String?

/// Deletes the MiniMax API key
func deleteMinimaxiApiKey()
func deleteMinimaxApiKey()

/// Checks if a MiniMax API key is saved
func hasMinimaxiApiKey() -> Bool
func hasMinimaxApiKey() -> Bool
}

// MARK: - Default Implementation
Expand Down
14 changes: 9 additions & 5 deletions Sources/Infrastructure/MiniMax/MiniMaxUsageProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import Foundation
import Domain

/// Probes MiniMax Coding Plan API for usage quota information.
/// Uses REST API: GET https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains
/// Supports both international (minimax.io) and China (minimaxi.com) regions.
/// (支持国际版和中国版两个区域)
/// Authentication: Bearer token from env var or stored API key.
public struct MiniMaxUsageProbe: UsageProbe {
private let networkClient: any NetworkClient
private let settingsRepository: any MiniMaxSettingsRepository
private let timeout: TimeInterval

private static let apiURL = "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains"
/// Resolves the API URL based on the configured region (根据区域配置动态选择 API URL)
var apiURL: String {
settingsRepository.minimaxRegion().codingPlanRemainsURL
}

public init(
networkClient: any NetworkClient = URLSession.shared,
Expand All @@ -25,15 +29,15 @@ public struct MiniMaxUsageProbe: UsageProbe {

func getApiKey() -> String? {
// First, check environment variable if configured
let envVarName = settingsRepository.minimaxiAuthEnvVar()
let envVarName = settingsRepository.minimaxAuthEnvVar()
let effectiveEnvVar = envVarName.isEmpty ? "MINIMAX_API_KEY" : envVarName
if let envValue = ProcessInfo.processInfo.environment[effectiveEnvVar], !envValue.isEmpty {
AppLog.probes.debug("MiniMax: Using API key from env var '\(effectiveEnvVar)'")
return envValue
}

// Fall back to stored API key
if let storedKey = settingsRepository.getMinimaxiApiKey(), !storedKey.isEmpty {
if let storedKey = settingsRepository.getMinimaxApiKey(), !storedKey.isEmpty {
AppLog.probes.debug("MiniMax: Using stored API key")
return storedKey
}
Expand All @@ -59,7 +63,7 @@ public struct MiniMaxUsageProbe: UsageProbe {

AppLog.probes.info("Starting MiniMax probe...")

guard let url = URL(string: Self.apiURL) else {
guard let url = URL(string: apiURL) else {
throw ProbeError.executionFailed("Invalid MiniMax API URL")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,27 +251,40 @@ public final class UserDefaultsProviderSettingsRepository: ZaiSettingsRepository

// MARK: - MiniMaxSettingsRepository

public func minimaxiAuthEnvVar() -> String {
public func minimaxRegion() -> MiniMaxRegion {
// Legacy compatibility: key absent means user upgraded from pre-region version,
// which only supported china (minimaxi.com). (兼容旧版:无 key 则默认中国区)
guard let rawValue = userDefaults.string(forKey: Keys.minimaxRegion) else {
return .china
}
return MiniMaxRegion(rawValue: rawValue) ?? .china
}

public func setMinimaxRegion(_ region: MiniMaxRegion) {
userDefaults.set(region.rawValue, forKey: Keys.minimaxRegion)
}

public func minimaxAuthEnvVar() -> String {
userDefaults.string(forKey: Keys.minimaxiAuthEnvVar) ?? ""
}

public func setMinimaxiAuthEnvVar(_ envVar: String) {
public func setMinimaxAuthEnvVar(_ envVar: String) {
userDefaults.set(envVar, forKey: Keys.minimaxiAuthEnvVar)
}

public func saveMinimaxiApiKey(_ key: String) {
public func saveMinimaxApiKey(_ key: String) {
userDefaults.set(key, forKey: Keys.minimaxiApiKey)
}

public func getMinimaxiApiKey() -> String? {
public func getMinimaxApiKey() -> String? {
userDefaults.string(forKey: Keys.minimaxiApiKey)
}

public func deleteMinimaxiApiKey() {
public func deleteMinimaxApiKey() {
userDefaults.removeObject(forKey: Keys.minimaxiApiKey)
}

public func hasMinimaxiApiKey() -> Bool {
public func hasMinimaxApiKey() -> Bool {
userDefaults.object(forKey: Keys.minimaxiApiKey) != nil
}

Expand Down Expand Up @@ -325,6 +338,7 @@ public final class UserDefaultsProviderSettingsRepository: ZaiSettingsRepository
static let bedrockRegions = "providerConfig.bedrockRegions"
static let bedrockDailyBudget = "providerConfig.bedrockDailyBudget"
// MiniMax settings (key strings kept for backward compatibility 保持向后兼容)
static let minimaxRegion = "providerConfig.minimaxRegion"
static let minimaxiAuthEnvVar = "providerConfig.minimaxiAuthEnvVar"
static let minimaxiApiKey = "com.claudebar.credentials.minimaxi-api-key"
// Credentials (kept compatible with old UserDefaultsCredentialRepository keys)
Expand Down
Loading
Loading