diff --git a/apps/picooraclaw/.env.example b/apps/picooraclaw/.env.example
index 7e6ed72b..edefd622 100644
--- a/apps/picooraclaw/.env.example
+++ b/apps/picooraclaw/.env.example
@@ -5,6 +5,7 @@
# ANTHROPIC_API_KEY=sk-ant-xxx
# OPENAI_API_KEY=sk-xxx
# GEMINI_API_KEY=xxx
+# MINIMAX_API_KEY=xxx
# ── Chat Channel ──────────────────────────
# TELEGRAM_BOT_TOKEN=123456:ABC...
diff --git a/apps/picooraclaw/README.md b/apps/picooraclaw/README.md
index b4337dd1..d2699afc 100644
--- a/apps/picooraclaw/README.md
+++ b/apps/picooraclaw/README.md
@@ -691,6 +691,32 @@ Get a key at [openrouter.ai/keys](https://openrouter.ai/keys) (200K free tokens/
+
+MiniMax (high-performance, great value)
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "provider": "minimax",
+ "model": "MiniMax-M2.7"
+ }
+ },
+ "providers": {
+ "minimax": {
+ "api_key": "your-key",
+ "api_base": "https://api.minimax.io/v1"
+ }
+ }
+}
+```
+
+Available models: `MiniMax-M2.7` (default), `MiniMax-M2.7-highspeed` (faster).
+
+Get a key at [platform.minimax.io](https://platform.minimax.io).
+
+
+
Zhipu (best for Chinese users)
@@ -728,6 +754,7 @@ Get a key at [bigmodel.cn](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys
| `gemini` | Gemini models | [aistudio.google.com](https://aistudio.google.com) |
| `groq` | Fast inference + voice transcription | [console.groq.com](https://console.groq.com) |
| `deepseek` | DeepSeek models | [platform.deepseek.com](https://platform.deepseek.com) |
+| `minimax` | MiniMax models (M2.7) | [platform.minimax.io](https://platform.minimax.io) |
| `zhipu` | Zhipu/GLM models | [bigmodel.cn](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
@@ -1001,7 +1028,7 @@ docker compose run --rm picoclaw-agent -m "What is 2+2?"
## Features
- Single static binary (~10MB RAM), runs on RISC-V/ARM64/x86_64
-- Ollama, OpenRouter, Anthropic, OpenAI, Gemini, Zhipu, DeepSeek, Groq providers
+- Ollama, OpenRouter, Anthropic, OpenAI, Gemini, Zhipu, DeepSeek, Groq, MiniMax providers
- **Default: [Oracle AI Database 26ai Free](https://www.oracle.com/database/free/)** with AI Vector Search (384-dim ONNX embeddings)
- Chat channels: Telegram, Discord, Slack, QQ, DingTalk, LINE, Feishu, WhatsApp
- Scheduled tasks via cron expressions
diff --git a/apps/picooraclaw/config/config.example.json b/apps/picooraclaw/config/config.example.json
index 01a2b37e..483845aa 100644
--- a/apps/picooraclaw/config/config.example.json
+++ b/apps/picooraclaw/config/config.example.json
@@ -108,6 +108,10 @@
"ollama": {
"api_key": "",
"api_base": "http://localhost:11434/v1"
+ },
+ "minimax": {
+ "api_key": "",
+ "api_base": "https://api.minimax.io/v1"
}
},
"tools": {
diff --git a/apps/picooraclaw/pkg/config/config.go b/apps/picooraclaw/pkg/config/config.go
index 66629357..145cb116 100644
--- a/apps/picooraclaw/pkg/config/config.go
+++ b/apps/picooraclaw/pkg/config/config.go
@@ -228,6 +228,7 @@ type ProvidersConfig struct {
Moonshot ProviderConfig `json:"moonshot"`
DeepSeek ProviderConfig `json:"deepseek"`
GitHubCopilot ProviderConfig `json:"github_copilot"`
+ MiniMax ProviderConfig `json:"minimax"`
}
type ProviderConfig struct {
@@ -369,6 +370,7 @@ func DefaultConfig() *Config {
Gemini: ProviderConfig{},
Nvidia: ProviderConfig{},
Moonshot: ProviderConfig{},
+ MiniMax: ProviderConfig{},
},
Gateway: GatewayConfig{
Host: "0.0.0.0",
diff --git a/apps/picooraclaw/pkg/providers/http_provider.go b/apps/picooraclaw/pkg/providers/http_provider.go
index 23888aea..13ce031f 100644
--- a/apps/picooraclaw/pkg/providers/http_provider.go
+++ b/apps/picooraclaw/pkg/providers/http_provider.go
@@ -57,7 +57,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
// Strip provider prefix from model name (e.g., moonshot/kimi-k2.5 -> kimi-k2.5, groq/openai/gpt-oss-120b -> openai/gpt-oss-120b, ollama/qwen2.5:14b -> qwen2.5:14b)
if idx := strings.Index(model, "/"); idx != -1 {
prefix := model[:idx]
- if prefix == "moonshot" || prefix == "nvidia" || prefix == "groq" || prefix == "ollama" {
+ if prefix == "moonshot" || prefix == "nvidia" || prefix == "groq" || prefix == "ollama" || prefix == "minimax" {
model = model[idx+1:]
}
}
@@ -86,6 +86,15 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
// Kimi k2 models only support temperature=1
if strings.Contains(lowerModel, "kimi") && strings.Contains(lowerModel, "k2") {
requestBody["temperature"] = 1.0
+ } else if strings.Contains(lowerModel, "MiniMax") || strings.Contains(lowerModel, "minimax") {
+ // MiniMax models require temperature in (0.0, 1.0]; 0 is not allowed
+ if temperature <= 0 {
+ requestBody["temperature"] = 1.0
+ } else if temperature > 1.0 {
+ requestBody["temperature"] = 1.0
+ } else {
+ requestBody["temperature"] = temperature
+ }
} else {
requestBody["temperature"] = temperature
}
@@ -467,6 +476,15 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
model = "deepseek-chat"
}
}
+ case "minimax", "MiniMax":
+ if cfg.Providers.MiniMax.APIKey != "" {
+ apiKey = cfg.Providers.MiniMax.APIKey
+ apiBase = cfg.Providers.MiniMax.APIBase
+ if apiBase == "" {
+ apiBase = "https://api.minimax.io/v1"
+ }
+ proxy = cfg.Providers.MiniMax.Proxy
+ }
case "github_copilot", "copilot":
if cfg.Providers.GitHubCopilot.APIBase != "" {
apiBase = cfg.Providers.GitHubCopilot.APIBase
@@ -544,6 +562,13 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
if apiBase == "" {
apiBase = "https://integrate.api.nvidia.com/v1"
}
+ case (strings.Contains(lowerModel, "minimax") || strings.Contains(lowerModel, "MiniMax") || strings.HasPrefix(model, "minimax/")) && cfg.Providers.MiniMax.APIKey != "":
+ apiKey = cfg.Providers.MiniMax.APIKey
+ apiBase = cfg.Providers.MiniMax.APIBase
+ proxy = cfg.Providers.MiniMax.Proxy
+ if apiBase == "" {
+ apiBase = "https://api.minimax.io/v1"
+ }
case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && cfg.Providers.Ollama.APIBase != "":
apiKey = cfg.Providers.Ollama.APIKey
if apiKey == "" {
diff --git a/apps/picooraclaw/pkg/providers/minimax_provider_test.go b/apps/picooraclaw/pkg/providers/minimax_provider_test.go
new file mode 100644
index 00000000..7e0c573b
--- /dev/null
+++ b/apps/picooraclaw/pkg/providers/minimax_provider_test.go
@@ -0,0 +1,363 @@
+package providers
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/jasperan/picooraclaw/pkg/config"
+)
+
+func TestMiniMaxProvider_ChatRoundTrip(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/chat/completions" {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ if r.Header.Get("Authorization") != "Bearer test-minimax-key" {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ var reqBody map[string]interface{}
+ json.NewDecoder(r.Body).Decode(&reqBody)
+
+ // Verify model is passed correctly
+ if reqBody["model"] != "MiniMax-M2.7" {
+ http.Error(w, "unexpected model", http.StatusBadRequest)
+ return
+ }
+
+ resp := map[string]interface{}{
+ "id": "chatcmpl-test",
+ "object": "chat.completion",
+ "model": reqBody["model"],
+ "choices": []map[string]interface{}{
+ {
+ "index": 0,
+ "message": map[string]interface{}{
+ "role": "assistant",
+ "content": "Hello from MiniMax!",
+ },
+ "finish_reason": "stop",
+ },
+ },
+ "usage": map[string]interface{}{
+ "prompt_tokens": 10,
+ "completion_tokens": 5,
+ "total_tokens": 15,
+ },
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+ }))
+ defer server.Close()
+
+ provider := NewHTTPProvider("test-minimax-key", server.URL, "")
+
+ messages := []Message{{Role: "user", Content: "Hello"}}
+ resp, err := provider.Chat(context.Background(), messages, nil, "MiniMax-M2.7", map[string]interface{}{
+ "max_tokens": 1024,
+ })
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if resp.Content != "Hello from MiniMax!" {
+ t.Errorf("Content = %q, want %q", resp.Content, "Hello from MiniMax!")
+ }
+ if resp.FinishReason != "stop" {
+ t.Errorf("FinishReason = %q, want %q", resp.FinishReason, "stop")
+ }
+ if resp.Usage.PromptTokens != 10 {
+ t.Errorf("PromptTokens = %d, want 10", resp.Usage.PromptTokens)
+ }
+ if resp.Usage.CompletionTokens != 5 {
+ t.Errorf("CompletionTokens = %d, want 5", resp.Usage.CompletionTokens)
+ }
+}
+
+func TestMiniMaxProvider_TemperatureClamp(t *testing.T) {
+ tests := []struct {
+ name string
+ temperature float64
+ wantTemp float64
+ }{
+ {"zero clamped to 1.0", 0.0, 1.0},
+ {"negative clamped to 1.0", -0.5, 1.0},
+ {"above 1.0 clamped to 1.0", 1.5, 1.0},
+ {"valid temperature passed through", 0.7, 0.7},
+ {"max valid temperature", 1.0, 1.0},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var capturedTemp float64
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody map[string]interface{}
+ json.NewDecoder(r.Body).Decode(&reqBody)
+ if temp, ok := reqBody["temperature"].(float64); ok {
+ capturedTemp = temp
+ }
+ resp := map[string]interface{}{
+ "choices": []map[string]interface{}{
+ {"message": map[string]interface{}{"content": "ok"}, "finish_reason": "stop"},
+ },
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+ }))
+ defer server.Close()
+
+ provider := NewHTTPProvider("test-key", server.URL, "")
+ messages := []Message{{Role: "user", Content: "test"}}
+ _, err := provider.Chat(context.Background(), messages, nil, "MiniMax-M2.7", map[string]interface{}{
+ "temperature": tt.temperature,
+ })
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if capturedTemp != tt.wantTemp {
+ t.Errorf("temperature = %f, want %f", capturedTemp, tt.wantTemp)
+ }
+ })
+ }
+}
+
+func TestMiniMaxProvider_ModelPrefixStripping(t *testing.T) {
+ var capturedModel string
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody map[string]interface{}
+ json.NewDecoder(r.Body).Decode(&reqBody)
+ capturedModel = reqBody["model"].(string)
+ resp := map[string]interface{}{
+ "choices": []map[string]interface{}{
+ {"message": map[string]interface{}{"content": "ok"}, "finish_reason": "stop"},
+ },
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+ }))
+ defer server.Close()
+
+ provider := NewHTTPProvider("test-key", server.URL, "")
+ messages := []Message{{Role: "user", Content: "test"}}
+
+ _, err := provider.Chat(context.Background(), messages, nil, "minimax/MiniMax-M2.7", map[string]interface{}{})
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if capturedModel != "MiniMax-M2.7" {
+ t.Errorf("model = %q, want %q (prefix should be stripped)", capturedModel, "MiniMax-M2.7")
+ }
+}
+
+func TestMiniMaxProvider_ToolCalling(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody map[string]interface{}
+ json.NewDecoder(r.Body).Decode(&reqBody)
+
+ // Verify tools are included in the request
+ if _, ok := reqBody["tools"]; !ok {
+ http.Error(w, "tools not found in request", http.StatusBadRequest)
+ return
+ }
+
+ resp := map[string]interface{}{
+ "choices": []map[string]interface{}{
+ {
+ "message": map[string]interface{}{
+ "content": "",
+ "tool_calls": []map[string]interface{}{
+ {
+ "id": "call_123",
+ "type": "function",
+ "function": map[string]interface{}{
+ "name": "get_weather",
+ "arguments": `{"city":"Tokyo"}`,
+ },
+ },
+ },
+ },
+ "finish_reason": "tool_calls",
+ },
+ },
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
+ }))
+ defer server.Close()
+
+ provider := NewHTTPProvider("test-key", server.URL, "")
+ messages := []Message{{Role: "user", Content: "What's the weather in Tokyo?"}}
+ tools := []ToolDefinition{
+ {
+ Type: "function",
+ Function: ToolFunctionDefinition{
+ Name: "get_weather",
+ Description: "Get weather for a city",
+ Parameters: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{"city": map[string]interface{}{"type": "string"}},
+ "required": []interface{}{"city"},
+ },
+ },
+ },
+ }
+
+ resp, err := provider.Chat(context.Background(), messages, tools, "MiniMax-M2.7", map[string]interface{}{})
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if len(resp.ToolCalls) != 1 {
+ t.Fatalf("len(ToolCalls) = %d, want 1", len(resp.ToolCalls))
+ }
+ if resp.ToolCalls[0].Name != "get_weather" {
+ t.Errorf("ToolCalls[0].Name = %q, want %q", resp.ToolCalls[0].Name, "get_weather")
+ }
+ if resp.ToolCalls[0].Arguments["city"] != "Tokyo" {
+ t.Errorf("ToolCalls[0].Arguments[city] = %q, want %q", resp.ToolCalls[0].Arguments["city"], "Tokyo")
+ }
+}
+
+func TestMiniMaxProvider_DefaultBaseURL(t *testing.T) {
+ provider := NewHTTPProvider("test-key", "https://api.minimax.io/v1", "")
+ if provider.apiBase != "https://api.minimax.io/v1" {
+ t.Errorf("apiBase = %q, want %q", provider.apiBase, "https://api.minimax.io/v1")
+ }
+}
+
+func TestMiniMaxProvider_Streaming(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody map[string]interface{}
+ json.NewDecoder(r.Body).Decode(&reqBody)
+
+ if reqBody["stream"] != true {
+ http.Error(w, "stream not enabled", http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+ flusher := w.(http.Flusher)
+
+ chunks := []string{
+ `data: {"choices":[{"delta":{"content":"Hello"},"index":0}]}`,
+ `data: {"choices":[{"delta":{"content":" from"},"index":0}]}`,
+ `data: {"choices":[{"delta":{"content":" MiniMax!"},"index":0,"finish_reason":"stop"}]}`,
+ `data: [DONE]`,
+ }
+ for _, chunk := range chunks {
+ w.Write([]byte(chunk + "\n\n"))
+ flusher.Flush()
+ }
+ }))
+ defer server.Close()
+
+ provider := NewHTTPProvider("test-key", server.URL, "")
+ messages := []Message{{Role: "user", Content: "test"}}
+
+ var accumulated string
+ callback := StreamCallback(func(chunk StreamChunk) {
+ accumulated += chunk.Content
+ })
+
+ resp, err := provider.Chat(context.Background(), messages, nil, "MiniMax-M2.7", map[string]interface{}{
+ "stream_callback": callback,
+ })
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if resp.Content != "Hello from MiniMax!" {
+ t.Errorf("Content = %q, want %q", resp.Content, "Hello from MiniMax!")
+ }
+ if accumulated != "Hello from MiniMax!" {
+ t.Errorf("accumulated = %q, want %q", accumulated, "Hello from MiniMax!")
+ }
+}
+
+func TestCreateProvider_MiniMax(t *testing.T) {
+ // Test that CreateProvider correctly routes to MiniMax via config
+ // This is a compile-time check that the config struct has the MiniMax field
+ // and that CreateProvider handles the "minimax" provider name
+ cfg := &config.Config{
+ Agents: config.AgentsConfig{
+ Defaults: config.AgentDefaults{
+ Provider: "minimax",
+ Model: "MiniMax-M2.7",
+ },
+ },
+ Providers: config.ProvidersConfig{
+ MiniMax: config.ProviderConfig{
+ APIKey: "test-minimax-key",
+ APIBase: "https://api.minimax.io/v1",
+ },
+ },
+ }
+
+ provider, err := CreateProvider(cfg)
+ if err != nil {
+ t.Fatalf("CreateProvider() error: %v", err)
+ }
+ httpProvider, ok := provider.(*HTTPProvider)
+ if !ok {
+ t.Fatalf("provider type = %T, want *HTTPProvider", provider)
+ }
+ if httpProvider.apiKey != "test-minimax-key" {
+ t.Errorf("apiKey = %q, want %q", httpProvider.apiKey, "test-minimax-key")
+ }
+ if httpProvider.apiBase != "https://api.minimax.io/v1" {
+ t.Errorf("apiBase = %q, want %q", httpProvider.apiBase, "https://api.minimax.io/v1")
+ }
+}
+
+func TestCreateProvider_MiniMaxDefaultBaseURL(t *testing.T) {
+ cfg := &config.Config{
+ Agents: config.AgentsConfig{
+ Defaults: config.AgentDefaults{
+ Provider: "minimax",
+ Model: "MiniMax-M2.7",
+ },
+ },
+ Providers: config.ProvidersConfig{
+ MiniMax: config.ProviderConfig{
+ APIKey: "test-minimax-key",
+ },
+ },
+ }
+
+ provider, err := CreateProvider(cfg)
+ if err != nil {
+ t.Fatalf("CreateProvider() error: %v", err)
+ }
+ httpProvider := provider.(*HTTPProvider)
+ if httpProvider.apiBase != "https://api.minimax.io/v1" {
+ t.Errorf("apiBase = %q, want default %q", httpProvider.apiBase, "https://api.minimax.io/v1")
+ }
+}
+
+func TestCreateProvider_MiniMaxAlias(t *testing.T) {
+ cfg := &config.Config{
+ Agents: config.AgentsConfig{
+ Defaults: config.AgentDefaults{
+ Provider: "MiniMax",
+ Model: "MiniMax-M2.7",
+ },
+ },
+ Providers: config.ProvidersConfig{
+ MiniMax: config.ProviderConfig{
+ APIKey: "test-minimax-key",
+ },
+ },
+ }
+
+ provider, err := CreateProvider(cfg)
+ if err != nil {
+ t.Fatalf("CreateProvider() with alias error: %v", err)
+ }
+ httpProvider := provider.(*HTTPProvider)
+ if httpProvider.apiKey != "test-minimax-key" {
+ t.Errorf("apiKey = %q, want %q", httpProvider.apiKey, "test-minimax-key")
+ }
+}