Skip to content
Open
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
18 changes: 18 additions & 0 deletions FACTORY_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ VibeProxy manages OAuth tokens, auto-refreshes them, routes requests, and handle
- **Google Cloud account** with Gemini API access for Gemini 2.x models (optional)
- **GitHub Copilot** subscription for Copilot model access (optional) — gives Claude, GPT, and Gemini models via GitHub's API
- **Z.AI API key** for GLM model access (optional) - get one at [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list)
- **MiniMax API key** for MiniMax model access (optional) - get one at [platform.minimax.io](https://platform.minimax.io)
- Factory CLI installed: `curl -fsSL https://app.factory.ai/cli | sh`

## Step 1: Install VibeProxy
Expand Down Expand Up @@ -63,6 +64,10 @@ Once VibeProxy is running:
- Enter your Z.AI API key (get one at [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list))
- This provides access to **GLM-5**, **GLM-4.7**, and other GLM models
- VibeProxy will securely store your API key
9. **(Optional)** Click **"Add Account"** next to MiniMax
- Enter your MiniMax API key (get one at [platform.minimax.io](https://platform.minimax.io))
- This provides access to **MiniMax-M3**, **MiniMax-M2.7**, **MiniMax-M2.5**, and other MiniMax models
- VibeProxy will securely store your API key

✅ The server starts automatically and runs on port **8317**

Expand Down Expand Up @@ -443,6 +448,19 @@ Antigravity provides access to Claude models with a generous usage quota (shared
> [!NOTE]
> Z.AI GLM models require an API key instead of OAuth authentication. Get your API key at [z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list) and add it in VibeProxy Settings → Z.AI GLM → Add Account.

### MiniMax Models
- `MiniMax-M3` - Latest MiniMax text model
- `MiniMax-M2.7` - MiniMax M2.7 text model
- `MiniMax-M2.7-highspeed` - Faster MiniMax M2.7 variant
- `MiniMax-M2.5` - MiniMax M2.5 text model
- `MiniMax-M2.5-highspeed` - Faster MiniMax M2.5 variant
- `MiniMax-M2.1` - MiniMax M2.1 text model
- `MiniMax-M2.1-highspeed` - Faster MiniMax M2.1 variant
- `MiniMax-M2` - MiniMax M2 text model

> [!NOTE]
> MiniMax models require an API key instead of OAuth authentication. Get your API key at [platform.minimax.io](https://platform.minimax.io) and add it in VibeProxy Settings → MiniMax → Add Account.

### OpenAI Models

**GPT-5.5** (Latest):
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<a href="https://github.com/automazeio/vibeproxy"><img alt="Star this repo" src="https://img.shields.io/github/stars/automazeio/vibeproxy.svg?style=social&amp;label=Star%20this%20repo&amp;maxAge=60" style="max-width: 100%;"></a></p>
</p>

**Stop paying twice for AI.** VibeProxy is a beautiful native macOS menu bar app that lets you use your existing Claude Code, ChatGPT, **Gemini**, **Kimi**, **Qwen**, **Antigravity**, and **Z.AI GLM** subscriptions with powerful AI coding tools like **[Factory Droids](https://app.factory.ai/r/FM8BJHFQ)**.
**Stop paying twice for AI.** VibeProxy is a beautiful native macOS menu bar app that lets you use your existing Claude Code, ChatGPT, **Gemini**, **Kimi**, **Qwen**, **Antigravity**, **Z.AI GLM**, and **MiniMax** accounts or API keys with powerful AI coding tools like **[Factory Droids](https://app.factory.ai/r/FM8BJHFQ)**.

Built on [CLIProxyAPIPlus](https://github.com/router-for-me/CLIProxyAPIPlus), it handles OAuth authentication, token management, and API routing automatically. One click to authenticate, zero friction to code.

Expand All @@ -24,7 +24,7 @@ Built on [CLIProxyAPIPlus](https://github.com/router-for-me/CLIProxyAPIPlus), it
> [!TIP]
> 📣 **NEW: Vercel AI Gateway Integration!**<br>Route your Claude requests through [Vercel's officially sanctioned AI Gateway](https://vercel.com/docs/ai-gateway) for safer access to your Claude Max subscription. No more worrying about account risks from using OAuth tokens directly!
>
> **Latest models supported:** Gemini 3 Pro (via Antigravity), GPT-5.1 / GPT-5.1 Codex, Claude Sonnet 4.5 / Opus 4.5 with extended thinking, GitHub Copilot, Z.AI GLM-4.7, and Kimi! 🚀
> **Latest models supported:** Gemini 3 Pro (via Antigravity), GPT-5.1 / GPT-5.1 Codex, Claude Sonnet 4.5 / Opus 4.5 with extended thinking, GitHub Copilot, Z.AI GLM-4.7, MiniMax-M3, and Kimi! 🚀
>
> **Setup Guides:**
> - [Factory CLI Setup →](FACTORY_SETUP.md) - Use Factory Droids with your AI subscriptions
Expand All @@ -36,7 +36,7 @@ Built on [CLIProxyAPIPlus](https://github.com/router-for-me/CLIProxyAPIPlus), it

- 🎯 **Native macOS Experience** - Clean, native SwiftUI interface that feels right at home on macOS
- 🚀 **One-Click Server Management** - Start/stop the proxy server from your menu bar
- 🔐 **Easy Authentication** - Authenticate with Codex, Claude Code, Gemini, Kimi, Qwen, and Antigravity (OAuth), plus Z.AI GLM (API key) directly from the app
- 🔐 **Easy Authentication** - Authenticate with Codex, Claude Code, Gemini, Kimi, Qwen, and Antigravity (OAuth), plus Z.AI GLM and MiniMax (API keys) directly from the app
- 🛡️ **Vercel AI Gateway** - Route Claude requests through [Vercel's AI Gateway](https://vercel.com/docs/ai-gateway) for safer access to your Claude Max subscription without risking your account from direct OAuth token usage
- 👥 **Multi-Account Support** - Connect multiple accounts per provider with automatic round-robin distribution and failover when rate-limited
- 🎚️ **Provider Priority** - Enable/disable providers to control which models are available (instant hot reload)
Expand Down Expand Up @@ -72,7 +72,7 @@ Want to build it yourself? See [**INSTALLATION.md**](INSTALLATION.md) for detail
1. Launch VibeProxy - you'll see a menu bar icon
2. Click the icon and select "Open Settings"
3. The server will start automatically
4. Click "Connect" for Claude Code, Codex, Gemini, Kimi, Qwen, or Antigravity to authenticate, or "Add Account" for Z.AI GLM
4. Click "Connect" for Claude Code, Codex, Gemini, Kimi, Qwen, or Antigravity to authenticate, or "Add Account" for Z.AI GLM or MiniMax

### Authentication

Expand All @@ -82,7 +82,7 @@ When you click "Connect" for an OAuth provider:
3. VibeProxy automatically detects your credentials
4. Status updates to show you're connected

When you click "Add Account" for Z.AI GLM:
When you click "Add Account" for Z.AI GLM or MiniMax:
1. Paste your provider API key
2. VibeProxy stores it in `~/.cli-proxy-api/`
3. The provider becomes available through the proxy immediately
Expand Down
2 changes: 2 additions & 0 deletions src/Sources/AuthStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum ServiceType: String, CaseIterable {
case qwen
case antigravity
case zai
case minimax

var displayName: String {
switch self {
Expand All @@ -20,6 +21,7 @@ enum ServiceType: String, CaseIterable {
case .qwen: return "Qwen"
case .antigravity: return "Antigravity"
case .zai: return "Z.AI GLM"
case .minimax: return "MiniMax"
}
}
}
Expand Down
58 changes: 55 additions & 3 deletions src/Sources/ConfigComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ enum ConfigComposer {
seenProviderIDs.insert(providerID)
}

if reservedProviderIDs.contains(providerID), providerID != ProviderCatalog.managedZAIProviderName {
if reservedProviderIDs.contains(providerID),
!ProviderCatalog.managedOpenAICompatibleProviderNames.contains(providerID) {
errors.append("Provider '\(providerID)' is reserved and cannot be declared under openai-compatibility.")
continue
}
Expand All @@ -132,7 +133,7 @@ enum ConfigComposer {
}
}

if providerID == ProviderCatalog.managedZAIProviderName {
if ProviderCatalog.managedOpenAICompatibleProviderNames.contains(providerID) {
continue
}

Expand All @@ -151,9 +152,12 @@ enum ConfigComposer {
disabledCustomProviderIDs: Set<String>,
disabledOAuthProviderKeys: [String],
zaiAPIKeys: [String],
miniMaxAPIKeys: [String],
customProviderAuthRecords: [ConfigProviderAuthRecord],
includeManagedZAIProvider: Bool,
managedZAIProviderName: String = "zai"
includeManagedMiniMaxProvider: Bool,
managedZAIProviderName: String = "zai",
managedMiniMaxProviderName: String = "minimax"
) -> [String: Any] {
var mergedRoot = baseRoot

Expand All @@ -179,6 +183,7 @@ enum ConfigComposer {

var mergedOpenAICompatibility: [[String: Any]] = []
var managedZAIBaseEntry: [String: Any]?
var managedMiniMaxBaseEntry: [String: Any]?
for entry in stringKeyedDictionaryArray(mergedRoot["openai-compatibility"]) {
guard let providerName = normalizedProviderID(from: entry) else {
continue
Expand All @@ -190,6 +195,10 @@ enum ConfigComposer {
managedZAIBaseEntry = sanitizedEntry
continue
}
if providerName == managedMiniMaxProviderName {
managedMiniMaxBaseEntry = sanitizedEntry
continue
}

if managedCustomProviderIDs.contains(providerName) {
if disabledCustomProviderIDs.contains(providerName) {
Expand Down Expand Up @@ -217,6 +226,16 @@ enum ConfigComposer {
mergedOpenAICompatibility.append(managedZAIEntry)
}
}

if includeManagedMiniMaxProvider {
let managedMiniMaxEntry = makeMiniMaxProviderEntry(
baseEntry: managedMiniMaxBaseEntry,
apiKeys: miniMaxAPIKeys
)
if !apiKeyEntries(from: managedMiniMaxEntry).isEmpty {
mergedOpenAICompatibility.append(managedMiniMaxEntry)
}
}

if mergedOpenAICompatibility.isEmpty {
mergedRoot.removeValue(forKey: "openai-compatibility")
Expand Down Expand Up @@ -408,6 +427,26 @@ enum ConfigComposer {
return entry
}

private static func makeMiniMaxProviderEntry(baseEntry: [String: Any]?, apiKeys: [String]) -> [String: Any] {
var entry = stripCustomProviderUIMetadata(from: baseEntry ?? [:])
entry["name"] = "minimax"

if normalizedString(entry["base-url"]) == nil {
entry["base-url"] = "https://api.minimax.io/v1"
}

let inlineEntries = apiKeyEntries(from: entry)
entry["api-key-entries"] = deduplicatedAPIKeyEntries(
inlineEntries + apiKeys.map { ["api-key": $0] }
)

if stringKeyedDictionaryArray(entry["models"]).isEmpty {
entry["models"] = defaultMiniMaxModels()
}

return entry
}

private static func normalizedProviderID(from entry: [String: Any]) -> String? {
normalizedString(entry["name"])
}
Expand All @@ -432,4 +471,17 @@ enum ConfigComposer {
["name": "glm-4-flash", "alias": "glm-4-flash"]
]
}

private static func defaultMiniMaxModels() -> [[String: String]] {
[
["name": "MiniMax-M3", "alias": "MiniMax-M3"],
["name": "MiniMax-M2.7", "alias": "MiniMax-M2.7"],
["name": "MiniMax-M2.7-highspeed", "alias": "MiniMax-M2.7-highspeed"],
["name": "MiniMax-M2.5", "alias": "MiniMax-M2.5"],
["name": "MiniMax-M2.5-highspeed", "alias": "MiniMax-M2.5-highspeed"],
["name": "MiniMax-M2.1", "alias": "MiniMax-M2.1"],
["name": "MiniMax-M2.1-highspeed", "alias": "MiniMax-M2.1-highspeed"],
["name": "MiniMax-M2", "alias": "MiniMax-M2"]
]
}
}
4 changes: 3 additions & 1 deletion src/Sources/ConfigInputFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ enum ConfigInputFingerprint {
guard file.pathExtension == "json" else {
return false
}
return name.hasPrefix("zai-") || name.hasPrefix("openai-compat-")
return name.hasPrefix("zai-")
|| name.hasPrefix("minimax-")
|| name.hasPrefix("openai-compat-")
}
.sorted { $0.lastPathComponent < $1.lastPathComponent }

Expand Down
7 changes: 6 additions & 1 deletion src/Sources/ProviderCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import Foundation

enum ProviderCatalog {
static let managedZAIProviderName = "zai"
static let managedMiniMaxProviderName = "minimax"
static let managedOpenAICompatibleProviderNames: Set<String> = [
managedZAIProviderName,
managedMiniMaxProviderName
]

/// OAuth provider keys used in config.yaml oauth-excluded-models.
static let oauthProviderKeys: [String: String] = [
Expand All @@ -16,5 +21,5 @@ enum ProviderCatalog {

static let reservedCustomProviderKeys = Set(oauthProviderKeys.keys)
.union(oauthProviderKeys.values)
.union([managedZAIProviderName])
.union(managedOpenAICompatibleProviderNames)
}
41 changes: 39 additions & 2 deletions src/Sources/ServerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ServerManager: ObservableObject {
private let configInputStateQueue = DispatchQueue(label: "io.automaze.vibeproxy.config-input-state", qos: .userInitiated)
private let configResolutionQueue = DispatchQueue(label: "io.automaze.vibeproxy.config-resolution", qos: .userInitiated)
private lazy var zaiAPIKeyStore = ZAIAPIKeyStore(directoryURL: authDirectoryURL())
private lazy var miniMaxAPIKeyStore = MiniMaxAPIKeyStore(directoryURL: authDirectoryURL())
private lazy var customProviderCredentialStore = CustomProviderCredentialStore(directoryURL: authDirectoryURL())
private var activeConfigPath = ""
private var isRestartingForConfigUpdate = false
Expand Down Expand Up @@ -233,7 +234,7 @@ class ServerManager: ObservableObject {
return
}

// Use config path (merged with Z.AI if keys exist)
// Use config path merged with managed provider keys when present.
let configPath = getConfigPath()
guard !configPath.isEmpty && FileManager.default.fileExists(atPath: configPath) else {
addLog("❌ Error: \(configErrorMessage ?? "Could not resolve active config path")")
Expand Down Expand Up @@ -653,6 +654,26 @@ class ServerManager: ObservableObject {
}
}
}

/// Saves a MiniMax API key to the auth directory
func saveMiniMaxApiKey(_ apiKey: String, completion: @escaping (Bool, String) -> Void) {
credentialMutationQueue.async { [weak self] in
guard let self else { return }

do {
let filePath = try self.miniMaxAPIKeyStore.save(apiKey: apiKey)
self.addLog("✓ MiniMax API key saved to \(filePath.lastPathComponent)")
self.refreshAuthBackedConfiguration()
DispatchQueue.main.async {
completion(true, "API key saved successfully")
}
} catch {
DispatchQueue.main.async {
completion(false, error.localizedDescription)
}
}
}
}

func saveCustomProviderAPIKey(providerID: String, apiKey: String, completion: @escaping (Bool, String) -> Void) {
credentialMutationQueue.async { [weak self] in
Expand Down Expand Up @@ -831,6 +852,7 @@ class ServerManager: ObservableObject {

let authDir = authDirectoryURL()
let zaiApiKeys = loadZaiAPIKeys()
let miniMaxApiKeys = loadMiniMaxAPIKeys()
let customAuthRecords = loadCustomProviderCredentialRecords()
let managedCustomProviders = ConfigComposer.parseCustomProviders(
from: baseConfig.root,
Expand All @@ -856,6 +878,7 @@ class ServerManager: ObservableObject {
disabledCustomProviderIDs: disabledCustomProviderIDs,
disabledOAuthProviderKeys: disabledProviders,
zaiAPIKeys: zaiApiKeys,
miniMaxAPIKeys: miniMaxApiKeys,
customProviderAuthRecords: customAuthRecords.map {
ConfigProviderAuthRecord(
providerID: $0.providerID,
Expand All @@ -868,7 +891,13 @@ class ServerManager: ObservableObject {
baseConfigRoot: baseConfig.root,
enabledProviderStates: enabledProviderStates
),
managedZAIProviderName: ProviderCatalog.managedZAIProviderName
includeManagedMiniMaxProvider: isProviderEnabled(
ProviderCatalog.managedMiniMaxProviderName,
baseConfigRoot: baseConfig.root,
enabledProviderStates: enabledProviderStates
),
managedZAIProviderName: ProviderCatalog.managedZAIProviderName,
managedMiniMaxProviderName: ProviderCatalog.managedMiniMaxProviderName
)

let mergedConfigPath = authDir.appendingPathComponent(CustomProviderConstants.mergedConfigFilename)
Expand Down Expand Up @@ -1053,6 +1082,14 @@ class ServerManager: ObservableObject {
}
return loadResult.apiKeys
}

private func loadMiniMaxAPIKeys() -> [String] {
let loadResult = miniMaxAPIKeyStore.loadActiveAPIKeys()
for issue in loadResult.issues {
NSLog("[ServerManager] Ignoring MiniMax API key file at %@: %@", issue.filePath.path, issue.message)
}
return loadResult.apiKeys
}

private func loadCustomProviderCredentialRecords() -> [CustomProviderCredentialRecord] {
let loadResult = customProviderCredentialStore.loadAll()
Expand Down
3 changes: 3 additions & 0 deletions src/Sources/ServiceType+ConnectionAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ enum ServiceConnectionAction: Equatable {
case authCommand(AuthCommand)
case promptForQwenEmail
case promptForZAIAPIKey
case promptForMiniMaxAPIKey
}

extension ServiceType {
Expand All @@ -23,6 +24,8 @@ extension ServiceType {
return .authCommand(.antigravityLogin)
case .zai:
return .promptForZAIAPIKey
case .minimax:
return .promptForMiniMaxAPIKey
}
}
}
Loading