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
1 change: 1 addition & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ OPENAI_API_KEY=sk-....
ANTHROPIC_API_KEY=sk-ant-....
GEMINI_API_KEY=AIza....
MISTRAL_API_KEY=....
MINIMAX_API_KEY=....

# UI access control (HTTP Basic Auth) — for the Linux/server deployment.
# Protects the dashboard UI and the /api/* admin endpoints (mappings, dashboard,
Expand Down
10 changes: 10 additions & 0 deletions src/backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type ProvidersConfig struct {
AnthropicProviderConfig ProviderConfig `json:"anthropic_provider_config"`
GeminiProviderConfig ProviderConfig `json:"gemini_provider_config"`
MistralProviderConfig ProviderConfig `json:"mistral_provider_config"`
MiniMaxProviderConfig ProviderConfig `json:"minimax_provider_config"`
CustomProviderConfig ProviderConfig `json:"custom_provider_config"`
}

Expand Down Expand Up @@ -184,6 +185,9 @@ func (c *Config) ValidateConfig() error {
if err := validateProviderConfig(c.Providers.MistralProviderConfig, "Mistral"); err != nil {
errs = append(errs, err.Error())
}
if err := validateProviderConfig(c.Providers.MiniMaxProviderConfig, "MiniMax"); err != nil {
errs = append(errs, err.Error())
}
if err := validateProviderConfig(c.Providers.CustomProviderConfig, "Custom"); err != nil {
errs = append(errs, err.Error())
}
Expand Down Expand Up @@ -287,6 +291,10 @@ func DefaultConfig() *Config {
APIDomain: providers.ProviderAPIDomainMistral,
AdditionalHeaders: map[string]string{},
}
defaultMiniMaxProviderConfig := ProviderConfig{
APIDomain: providers.ProviderAPIDomainMiniMax,
AdditionalHeaders: map[string]string{},
}
defaultCustomProviderConfig := ProviderConfig{
APIDomain: providers.ProviderAPIDomainCustom,
AdditionalHeaders: map[string]string{},
Expand All @@ -305,6 +313,7 @@ func DefaultConfig() *Config {
AnthropicProviderConfig: defaultAnthropicProviderConfig,
GeminiProviderConfig: defaultGeminiProviderConfig,
MistralProviderConfig: defaultMistralProviderConfig,
MiniMaxProviderConfig: defaultMiniMaxProviderConfig,
CustomProviderConfig: defaultCustomProviderConfig,
},
ProxyPort: DefaultForwardProxyPort,
Expand Down Expand Up @@ -348,6 +357,7 @@ func (pc ProvidersConfig) GetInterceptDomains() []string {
interceptDomain(pc.OpenAIProviderConfig.APIDomain),
interceptDomain(pc.GeminiProviderConfig.APIDomain),
interceptDomain(pc.MistralProviderConfig.APIDomain),
interceptDomain(pc.MiniMaxProviderConfig.APIDomain),
interceptDomain(pc.CustomProviderConfig.APIDomain),
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ func loadApplicationConfig(cfg *config.Config) {
log.Printf("Warning: CUSTOM_API_KEY is empty or not set")
}

// Override MiniMax provider config with environment variables
if minimaxURL := os.Getenv("MINIMAX_BASE_URL"); minimaxURL != "" {
cfg.Providers.MiniMaxProviderConfig.APIDomain = minimaxURL
}
if minimaxApiKey := os.Getenv("MINIMAX_API_KEY"); minimaxApiKey != "" {
cfg.Providers.MiniMaxProviderConfig.APIKey = minimaxApiKey
log.Printf("Loaded MINIMAX_API_KEY from environment (length: %d)", len(minimaxApiKey))
}

if variant := os.Getenv("MODEL_VARIANT"); variant != "" {
cfg.ModelVariant = variant
log.Printf("Loaded MODEL_VARIANT from environment: %s", variant)
Expand Down
43 changes: 43 additions & 0 deletions src/backend/providers/minimax.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package providers

import (
"net/http"
"strings"
)

const (
ProviderTypeMiniMax ProviderType = "minimax"
ProviderAPIDomainMiniMax string = "api.minimax.io/v1"
ProviderNameMiniMax string = "MiniMax"
)

// MiniMaxProvider uses the OpenAI-compatible chat completions API shape.
type MiniMaxProvider struct {
*OpenAIProvider
}

func NewMiniMaxProvider(apiDomain string, apiKey string, additionalHeaders map[string]string) *MiniMaxProvider {
return &MiniMaxProvider{
OpenAIProvider: NewOpenAIProvider(apiDomain, apiKey, additionalHeaders),
}
}

func (p *MiniMaxProvider) GetName() string {
return ProviderNameMiniMax
}

func (p *MiniMaxProvider) GetType() ProviderType {
return ProviderTypeMiniMax
}

func (p *MiniMaxProvider) SetAuthHeaders(req *http.Request) {
if apiKey := req.Header.Get("Authorization"); apiKey != "" {
return
}

if strings.TrimSpace(p.apiKey) == "" {
return
}

req.Header.Set("Authorization", "Bearer "+p.apiKey)
}
101 changes: 101 additions & 0 deletions src/backend/providers/minimax_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package providers

import (
"context"
"net/http"
"testing"
)

func TestMiniMaxProvider_GetName(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)
if got := p.GetName(); got != ProviderNameMiniMax {
t.Errorf("GetName() = %q, want %q", got, ProviderNameMiniMax)
}
}

func TestMiniMaxProvider_GetType(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)
if got := p.GetType(); got != ProviderTypeMiniMax {
t.Errorf("GetType() = %q, want %q", got, ProviderTypeMiniMax)
}
}

func TestMiniMaxProvider_GetBaseURL(t *testing.T) {
tests := []struct {
name string
apiDomain string
useHttps bool
want string
}{
{"full URL", "https://api.minimax.io/v1", true, "https://api.minimax.io/v1"},
{"bare domain with path", "api.minimax.io/v1", true, "https://api.minimax.io/v1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewMiniMaxProvider(tt.apiDomain, "key", nil)
if got := p.GetBaseURL(tt.useHttps); got != tt.want {
t.Errorf("GetBaseURL(%v) = %q, want %q", tt.useHttps, got, tt.want)
}
})
}
}

func TestMiniMaxProvider_SetAuthHeaders(t *testing.T) {
t.Run("sets Authorization header", func(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "mm-test-key", nil)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "https://api.minimax.io/v1/chat/completions", nil)
p.SetAuthHeaders(req)
if got := req.Header.Get("Authorization"); got != "Bearer mm-test-key" {
t.Errorf("Authorization = %q, want %q", got, "Bearer mm-test-key")
}
})

t.Run("does not override existing Authorization", func(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "mm-test-key", nil)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "https://api.minimax.io/v1/chat/completions", nil)
req.Header.Set("Authorization", "Bearer existing")
p.SetAuthHeaders(req)
if got := req.Header.Get("Authorization"); got != "Bearer existing" {
t.Errorf("Authorization = %q, want %q", got, "Bearer existing")
}
})

t.Run("empty key does not set header", func(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "", nil)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "https://api.minimax.io/v1/chat/completions", nil)
p.SetAuthHeaders(req)
if got := req.Header.Get("Authorization"); got != "" {
t.Errorf("Authorization should be empty, got %q", got)
}
})
}

func TestMiniMaxProvider_ExtractRequestText(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)

data := makeOpenAIRequest([]map[string]interface{}{
{"role": "user", "content": "Hello MiniMax"},
})
got, err := p.ExtractRequestText(data)
if err != nil {
t.Fatalf("ExtractRequestText() error = %v", err)
}
if got != "Hello MiniMax\n" {
t.Errorf("ExtractRequestText() = %q, want %q", got, "Hello MiniMax\n")
}
}

func TestMiniMaxProvider_ExtractResponseText(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)

data := makeOpenAIResponse([]map[string]interface{}{
{"message": map[string]interface{}{"role": "assistant", "content": "Hello from MiniMax"}},
})
got, err := p.ExtractResponseText(data)
if err != nil {
t.Fatalf("ExtractResponseText() error = %v", err)
}
if got != "Hello from MiniMax\n" {
t.Errorf("ExtractResponseText() = %q, want %q", got, "Hello from MiniMax\n")
}
}
5 changes: 5 additions & 0 deletions src/backend/providers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type Providers struct {
AnthropicProvider *AnthropicProvider
GeminiProvider *GeminiProvider
MistralProvider *MistralProvider
MiniMaxProvider *MiniMaxProvider
CustomProvider *CustomProvider
}

Expand Down Expand Up @@ -118,6 +119,8 @@ func (p *Providers) GetProviderFromPath(host string, path string, body *[]byte,
provider = p.GeminiProvider
case ProviderTypeMistral:
provider = p.MistralProvider
case ProviderTypeMiniMax:
provider = p.MiniMaxProvider
case ProviderTypeCustom:
provider = p.CustomProvider
default:
Expand Down Expand Up @@ -183,6 +186,8 @@ func (p *Providers) GetProviderFromHost(host string, logPrefix string) (*Provide
provider = p.GeminiProvider
case p.MistralProvider != nil && providerHostMatches(host, p.MistralProvider.apiDomain):
provider = p.MistralProvider
case p.MiniMaxProvider != nil && providerHostMatches(host, p.MiniMaxProvider.apiDomain):
provider = p.MiniMaxProvider
case p.CustomProvider != nil && providerHostMatches(host, p.CustomProvider.apiDomain):
provider = p.CustomProvider
default:
Expand Down
8 changes: 8 additions & 0 deletions src/backend/proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ func providerFromModel(model string) string {
return "gemini"
case strings.Contains(m, "mistral"), strings.Contains(m, "mixtral"):
return "mistral"
case strings.Contains(m, "abab"), strings.Contains(m, "minimax"):
return "minimax"
case strings.Contains(m, "gpt"), strings.Contains(m, "davinci"),
strings.HasPrefix(m, "o1"), strings.HasPrefix(m, "o3"), strings.HasPrefix(m, "o4"):
return "openai"
Expand Down Expand Up @@ -920,6 +922,11 @@ func NewHandler(cfg *config.Config) (*Handler, error) {
cfg.Providers.MistralProviderConfig.APIKey,
cfg.Providers.MistralProviderConfig.AdditionalHeaders,
)
miniMaxProvider := providers.NewMiniMaxProvider(
cfg.Providers.MiniMaxProviderConfig.APIDomain,
cfg.Providers.MiniMaxProviderConfig.APIKey,
cfg.Providers.MiniMaxProviderConfig.AdditionalHeaders,
)
customProvider := providers.NewCustomProvider(
cfg.Providers.CustomProviderConfig.APIDomain,
cfg.Providers.CustomProviderConfig.APIKey,
Expand All @@ -939,6 +946,7 @@ func NewHandler(cfg *config.Config) (*Handler, error) {
AnthropicProvider: anthropicProvider,
GeminiProvider: geminiProvider,
MistralProvider: mistralProvider,
MiniMaxProvider: miniMaxProvider,
CustomProvider: customProvider,
}

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/components/privacy-proxy-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export default function PrivacyProxyUI({
"anthropic",
"gemini",
"mistral",
"minimax",
"custom",
] as ProviderType[]
).map((provider) => (
Expand Down
16 changes: 16 additions & 0 deletions src/frontend/src/components/settings/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const PROVIDER_INFO: Record<
placeholder: "...",
helpLink: "https://console.mistral.ai/api-keys",
},
minimax: {
name: "MiniMax",
defaultModel: "MiniMax-M3",
placeholder: "...",
helpLink: "https://platform.minimax.chat/user-center/basic-information/interface-key",
},
custom: {
name: "Custom Provider",
defaultModel: "your-model-id",
Expand All @@ -76,6 +82,7 @@ const PROVIDER_ORDER: ProviderType[] = [
"anthropic",
"gemini",
"mistral",
"minimax",
"custom",
];

Expand All @@ -101,6 +108,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: { hasApiKey: false, model: "" },
gemini: { hasApiKey: false, model: "" },
mistral: { hasApiKey: false, model: "" },
minimax: { hasApiKey: false, model: "" },
custom: { hasApiKey: false, model: "", baseUrl: "" },
},
});
Expand All @@ -118,6 +126,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: false,
gemini: false,
mistral: false,
minimax: false,
custom: false,
});

Expand All @@ -129,6 +138,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
});

Expand All @@ -139,6 +149,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
});

Expand All @@ -149,6 +160,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
});

Expand All @@ -166,13 +178,15 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
};
const baseUrls: Record<ProviderType, string> = {
openai: "",
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
};
for (const provider of PROVIDER_ORDER) {
Expand All @@ -188,6 +202,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
});
} catch (error) {
Expand Down Expand Up @@ -278,6 +293,7 @@ export default function ProvidersSection({ onSaved }: ProvidersSectionProps) {
anthropic: "",
gemini: "",
mistral: "",
minimax: "",
custom: "",
});

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/electron/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type ProviderType =
| "anthropic"
| "gemini"
| "mistral"
| "minimax"
| "custom";

interface ProviderSettings {
Expand Down
Loading
Loading