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
3 changes: 3 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ OPENAI_API_KEY=sk-....
ANTHROPIC_API_KEY=sk-ant-....
GEMINI_API_KEY=AIza....
MISTRAL_API_KEY=....
MINIMAX_API_KEY=....
MINIMAX_BASE_URL=https://api.minimax.io/v1
# Mainland China endpoint: https://api.minimaxi.com/v1

# 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
21 changes: 21 additions & 0 deletions src/backend/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"reflect"
"strings"
"testing"

"github.com/dataiku/kiji-proxy/src/backend/providers"
)

func TestValidatePort(t *testing.T) {
Expand Down Expand Up @@ -362,6 +364,25 @@ func TestDefaultConfig_Detectors(t *testing.T) {
}
}

func TestDefaultConfig_MiniMaxProvider(t *testing.T) {
cfg := DefaultConfig()
if got := cfg.Providers.MiniMaxProviderConfig.APIDomain; got != providers.ProviderAPIDomainMiniMax {
t.Errorf("MiniMax APIDomain = %q, want %q", got, providers.ProviderAPIDomainMiniMax)
}

wantDomain := "api.minimax.io"
found := false
for _, domain := range cfg.Providers.GetInterceptDomains() {
if domain == wantDomain {
found = true
break
}
}
if !found {
t.Errorf("GetInterceptDomains() does not contain %q", wantDomain)
}
}

// An absent "detectors" key must not clobber the default, while an explicit value
// overrides it. This mirrors how config files are decoded over DefaultConfig.
func TestConfig_DetectorsDecode(t *testing.T) {
Expand Down
11 changes: 11 additions & 0 deletions src/backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ 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))
} else {
log.Printf("Warning: MINIMAX_API_KEY is empty or not set")
}

if variant := os.Getenv("MODEL_VARIANT"); variant != "" {
cfg.ModelVariant = variant
log.Printf("Loaded MODEL_VARIANT from environment: %s", variant)
Expand Down
15 changes: 15 additions & 0 deletions src/backend/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,18 @@ func TestLoadApplicationConfigUnixSocket(t *testing.T) {
t.Errorf("UnixSocketPath = %q, want %q", cfg.UnixSocketPath, "/tmp/kiji-proxy.sock")
}
}

func TestLoadApplicationConfigMiniMax(t *testing.T) {
t.Setenv("MINIMAX_API_KEY", "test-minimax-key")
t.Setenv("MINIMAX_BASE_URL", "https://api.minimaxi.com/v1")

cfg := config.DefaultConfig()
loadApplicationConfig(cfg)

if got := cfg.Providers.MiniMaxProviderConfig.APIKey; got != "test-minimax-key" {
t.Errorf("MiniMax APIKey = %q, want %q", got, "test-minimax-key")
}
if got := cfg.Providers.MiniMaxProviderConfig.APIDomain; got != "https://api.minimaxi.com/v1" {
t.Errorf("MiniMax APIDomain = %q, want %q", got, "https://api.minimaxi.com/v1")
}
}
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)
}
158 changes: 158 additions & 0 deletions src/backend/providers/minimax_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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_ExtractMultimodalRequestText(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)
data := map[string]interface{}{
"model": "MiniMax-M3",
"messages": []interface{}{
map[string]interface{}{
"role": "user",
"content": []interface{}{
map[string]interface{}{"type": "text", "text": "Hello MiniMax"},
map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "https://example.com/image.png"}},
map[string]interface{}{"type": "video_url", "video_url": map[string]interface{}{"url": "https://example.com/video.mp4"}},
},
},
},
}

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_CreateMaskedMultimodalRequest(t *testing.T) {
p := NewMiniMaxProvider("api.minimax.io/v1", "key", nil)
imagePart := map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{"url": "https://example.com/image.png"},
}
textPart := map[string]interface{}{"type": "text", "text": "Hello John Doe"}
data := map[string]interface{}{
"model": "MiniMax-M3",
"messages": []interface{}{
map[string]interface{}{
"role": "user",
"content": []interface{}{textPart, imagePart},
},
},
}

mapping, entities, err := p.CreateMaskedRequest(data, replaceMaskPII)
if err != nil {
t.Fatalf("CreateMaskedRequest() error = %v", err)
}
if textPart["text"] != "Hello Jane Smith" {
t.Errorf("text content = %q, want %q", textPart["text"], "Hello Jane Smith")
}
if len(mapping) == 0 || entities == nil || len(*entities) == 0 {
t.Fatal("expected masked mapping and entities")
}
if imagePart["image_url"].(map[string]interface{})["url"] != "https://example.com/image.png" {
t.Error("image content should remain unchanged")
}
}

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")
}
}
Loading
Loading