Add Xiaomi MiMo provider (mimo-v2.5) support
This commit is contained in:
@@ -18,6 +18,9 @@ DEEPSEEK_API_KEY=sk-...
|
|||||||
MOONSHOT_API_KEY=sk-...
|
MOONSHOT_API_KEY=sk-...
|
||||||
GROK_API_KEY=xai-...
|
GROK_API_KEY=xai-...
|
||||||
|
|
||||||
|
# Xiaomi MiMo
|
||||||
|
XIAOMI_API_KEY=sk-...
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type ProviderConfig struct {
|
|||||||
Moonshot MoonshotConfig `mapstructure:"moonshot"`
|
Moonshot MoonshotConfig `mapstructure:"moonshot"`
|
||||||
Grok GrokConfig `mapstructure:"grok"`
|
Grok GrokConfig `mapstructure:"grok"`
|
||||||
Ollama OllamaConfig `mapstructure:"ollama"`
|
Ollama OllamaConfig `mapstructure:"ollama"`
|
||||||
|
Xiaomi XiaomiConfig `mapstructure:"xiaomi"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIConfig struct {
|
type OpenAIConfig struct {
|
||||||
@@ -81,6 +82,13 @@ type OllamaConfig struct {
|
|||||||
Models []string `mapstructure:"models"`
|
Models []string `mapstructure:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XiaomiConfig struct {
|
||||||
|
APIKeyEnv string `mapstructure:"api_key_env"`
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
DefaultModel string `mapstructure:"default_model"`
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
|
|
||||||
@@ -120,6 +128,11 @@ func Load() (*Config, error) {
|
|||||||
v.SetDefault("providers.ollama.enabled", false)
|
v.SetDefault("providers.ollama.enabled", false)
|
||||||
v.SetDefault("providers.ollama.models", []string{})
|
v.SetDefault("providers.ollama.models", []string{})
|
||||||
|
|
||||||
|
v.SetDefault("providers.xiaomi.api_key_env", "XIAOMI_API_KEY")
|
||||||
|
v.SetDefault("providers.xiaomi.base_url", "https://api.xiaomimimo.com/v1")
|
||||||
|
v.SetDefault("providers.xiaomi.default_model", "mimo-v2.5")
|
||||||
|
v.SetDefault("providers.xiaomi.enabled", true)
|
||||||
|
|
||||||
// Environment variables
|
// Environment variables
|
||||||
v.SetEnvPrefix("LLM_PROXY")
|
v.SetEnvPrefix("LLM_PROXY")
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "__"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "__"))
|
||||||
@@ -210,6 +223,8 @@ func (c *Config) GetAPIKey(provider string) (string, error) {
|
|||||||
case "ollama":
|
case "ollama":
|
||||||
// Ollama doesn't require an API key
|
// Ollama doesn't require an API key
|
||||||
return "", nil
|
return "", nil
|
||||||
|
case "xiaomi":
|
||||||
|
envVar = c.Providers.Xiaomi.APIKeyEnv
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unknown provider: %s", provider)
|
return "", fmt.Errorf("unknown provider: %s", provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var CanonicalProviders = []string{
|
|||||||
"mistral",
|
"mistral",
|
||||||
"cohere",
|
"cohere",
|
||||||
"minimax",
|
"minimax",
|
||||||
|
"xiaomi",
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelRegistry struct {
|
type ModelRegistry struct {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"gophergate/internal/config"
|
||||||
|
"gophergate/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XiaomiProvider struct {
|
||||||
|
client *resty.Client
|
||||||
|
config config.XiaomiConfig
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXiaomiProvider(cfg config.XiaomiConfig, apiKey string) *XiaomiProvider {
|
||||||
|
return &XiaomiProvider{
|
||||||
|
client: resty.New().SetTimeout(10 * time.Minute),
|
||||||
|
config: cfg,
|
||||||
|
apiKey: strings.TrimSpace(apiKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) Name() string {
|
||||||
|
return "xiaomi"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) ChatCompletion(ctx context.Context, req *models.UnifiedRequest) (*models.ChatCompletionResponse, error) {
|
||||||
|
messagesJSON, err := MessagesToOpenAIJSON(req.Messages)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := BuildOpenAIBody(req, messagesJSON, false)
|
||||||
|
|
||||||
|
baseURL := strings.TrimRight(p.config.BaseURL, "/")
|
||||||
|
|
||||||
|
resp, err := p.client.R().
|
||||||
|
SetContext(ctx).
|
||||||
|
SetHeader("Authorization", "Bearer "+p.apiKey).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetHeader("Accept", "application/json").
|
||||||
|
SetBody(body).
|
||||||
|
Post(fmt.Sprintf("%s/chat/completions", baseURL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.IsSuccess() {
|
||||||
|
msg := resp.String()
|
||||||
|
if msg == "" {
|
||||||
|
if b := resp.Body(); len(b) > 0 {
|
||||||
|
msg = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
if body, err := io.ReadAll(resp.RawBody()); err == nil {
|
||||||
|
msg = string(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Xiaomi API error (%d): %s", resp.StatusCode(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var respJSON map[string]interface{}
|
||||||
|
if err := json.Unmarshal(resp.Body(), &respJSON); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseOpenAIResponse(respJSON, req.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) ChatCompletionStream(ctx context.Context, req *models.UnifiedRequest) (<-chan *models.ChatCompletionStreamResponse, error) {
|
||||||
|
messagesJSON, err := MessagesToOpenAIJSON(req.Messages)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := BuildOpenAIBody(req, messagesJSON, true)
|
||||||
|
|
||||||
|
baseURL := strings.TrimRight(p.config.BaseURL, "/")
|
||||||
|
|
||||||
|
resp, err := p.client.R().
|
||||||
|
SetContext(ctx).
|
||||||
|
SetHeader("Authorization", "Bearer "+p.apiKey).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetHeader("Accept", "text/event-stream").
|
||||||
|
SetBody(body).
|
||||||
|
SetDoNotParseResponse(true).
|
||||||
|
Post(fmt.Sprintf("%s/chat/completions", baseURL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.IsSuccess() {
|
||||||
|
msg := resp.String()
|
||||||
|
if msg == "" {
|
||||||
|
if body, err := io.ReadAll(resp.RawBody()); err == nil {
|
||||||
|
msg = string(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Xiaomi API error (%d): %s", resp.StatusCode(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan *models.ChatCompletionStreamResponse)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
if err := StreamOpenAI(resp.RawBody(), ch); err != nil {
|
||||||
|
fmt.Printf("Xiaomi Stream error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) ImageGeneration(ctx context.Context, req *models.ImageGenerationRequest) (*models.ImageGenerationResponse, error) {
|
||||||
|
return nil, fmt.Errorf("xiaomi does not support image generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) Responses(ctx context.Context, req *models.ResponsesRequest) (*models.ResponsesResponse, error) {
|
||||||
|
return nil, fmt.Errorf("responses API not supported by xiaomi")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaomiProvider) ResponsesStream(ctx context.Context, req *models.ResponsesRequest) (<-chan *models.ResponsesStreamChunk, error) {
|
||||||
|
return nil, fmt.Errorf("responses API not supported by xiaomi")
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ func (s *Server) handleGetModels(c *gin.Context) {
|
|||||||
"deepseek": "deepseek",
|
"deepseek": "deepseek",
|
||||||
"xai": "grok",
|
"xai": "grok",
|
||||||
"ollama": "ollama",
|
"ollama": "ollama",
|
||||||
|
"xiaomi": "xiaomi",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge registry models with DB overrides
|
// Merge registry models with DB overrides
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (s *Server) handleGetProviders(c *gin.Context) {
|
|||||||
dbMap[cfg.ID] = cfg
|
dbMap[cfg.ID] = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
providerIDs := []string{"openai", "gemini", "deepseek", "moonshot", "grok", "ollama"}
|
providerIDs := []string{"openai", "gemini", "deepseek", "moonshot", "grok", "ollama", "xiaomi"}
|
||||||
var result []gin.H
|
var result []gin.H
|
||||||
|
|
||||||
for _, id := range providerIDs {
|
for _, id := range providerIDs {
|
||||||
@@ -54,6 +54,10 @@ func (s *Server) handleGetProviders(c *gin.Context) {
|
|||||||
name = "xAI Grok"
|
name = "xAI Grok"
|
||||||
enabled = s.cfg.Providers.Grok.Enabled
|
enabled = s.cfg.Providers.Grok.Enabled
|
||||||
baseURL = s.cfg.Providers.Grok.BaseURL
|
baseURL = s.cfg.Providers.Grok.BaseURL
|
||||||
|
case "xiaomi":
|
||||||
|
name = "Xiaomi MiMo"
|
||||||
|
enabled = s.cfg.Providers.Xiaomi.Enabled
|
||||||
|
baseURL = s.cfg.Providers.Xiaomi.BaseURL
|
||||||
case "ollama":
|
case "ollama":
|
||||||
name = "Ollama"
|
name = "Ollama"
|
||||||
enabled = s.cfg.Providers.Ollama.Enabled
|
enabled = s.cfg.Providers.Ollama.Enabled
|
||||||
@@ -109,6 +113,9 @@ func (s *Server) handleGetProviders(c *gin.Context) {
|
|||||||
if id == "grok" {
|
if id == "grok" {
|
||||||
registryID = "xai"
|
registryID = "xai"
|
||||||
}
|
}
|
||||||
|
if id == "xiaomi" {
|
||||||
|
registryID = "xiaomi"
|
||||||
|
}
|
||||||
|
|
||||||
if pInfo, ok := s.registry.Providers[registryID]; ok {
|
if pInfo, ok := s.registry.Providers[registryID]; ok {
|
||||||
for mID := range pInfo.Models {
|
for mID := range pInfo.Models {
|
||||||
@@ -226,6 +233,8 @@ func (s *Server) handleTestProvider(c *gin.Context) {
|
|||||||
testReq.Model = "kimi-k2.5"
|
testReq.Model = "kimi-k2.5"
|
||||||
} else if name == "grok" {
|
} else if name == "grok" {
|
||||||
testReq.Model = "grok-4-1-fast-non-reasoning"
|
testReq.Model = "grok-4-1-fast-non-reasoning"
|
||||||
|
} else if name == "xiaomi" {
|
||||||
|
testReq.Model = "mimo-v2.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := provider.ChatCompletion(c.Request.Context(), testReq)
|
_, err := provider.ChatCompletion(c.Request.Context(), testReq)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (s *Server) RefreshProviders() error {
|
|||||||
dbMap[cfg.ID] = cfg
|
dbMap[cfg.ID] = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
providerIDs := []string{"openai", "gemini", "deepseek", "moonshot", "grok", "ollama"}
|
providerIDs := []string{"openai", "gemini", "deepseek", "moonshot", "grok", "ollama", "xiaomi"}
|
||||||
for _, id := range providerIDs {
|
for _, id := range providerIDs {
|
||||||
// Default values from config
|
// Default values from config
|
||||||
enabled := false
|
enabled := false
|
||||||
@@ -113,6 +113,10 @@ func (s *Server) RefreshProviders() error {
|
|||||||
enabled = s.cfg.Providers.Grok.Enabled
|
enabled = s.cfg.Providers.Grok.Enabled
|
||||||
baseURL = s.cfg.Providers.Grok.BaseURL
|
baseURL = s.cfg.Providers.Grok.BaseURL
|
||||||
apiKey, _ = s.cfg.GetAPIKey("grok")
|
apiKey, _ = s.cfg.GetAPIKey("grok")
|
||||||
|
case "xiaomi":
|
||||||
|
enabled = s.cfg.Providers.Xiaomi.Enabled
|
||||||
|
baseURL = s.cfg.Providers.Xiaomi.BaseURL
|
||||||
|
apiKey, _ = s.cfg.GetAPIKey("xiaomi")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overrides from DB
|
// Overrides from DB
|
||||||
@@ -167,6 +171,10 @@ func (s *Server) RefreshProviders() error {
|
|||||||
cfg := s.cfg.Providers.Ollama
|
cfg := s.cfg.Providers.Ollama
|
||||||
cfg.BaseURL = baseURL
|
cfg.BaseURL = baseURL
|
||||||
p = providers.NewOllamaProvider(cfg)
|
p = providers.NewOllamaProvider(cfg)
|
||||||
|
case "xiaomi":
|
||||||
|
cfg := s.cfg.Providers.Xiaomi
|
||||||
|
cfg.BaseURL = baseURL
|
||||||
|
p = providers.NewXiaomiProvider(cfg, apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p != nil {
|
if p != nil {
|
||||||
@@ -313,7 +321,7 @@ func (s *Server) handleResponses(c *gin.Context) {
|
|||||||
// (same pattern as handleChatCompletions).
|
// (same pattern as handleChatCompletions).
|
||||||
modelGroup := ""
|
modelGroup := ""
|
||||||
modelID := req.Model
|
modelID := req.Model
|
||||||
prefixes := []string{"gemini/", "google/", "openai/", "deepseek/", "moonshot/", "grok/", "ollama/"}
|
prefixes := []string{"gemini/", "google/", "openai/", "deepseek/", "moonshot/", "grok/", "ollama/", "xiaomi/"}
|
||||||
for _, p := range prefixes {
|
for _, p := range prefixes {
|
||||||
if strings.HasPrefix(modelID, p) {
|
if strings.HasPrefix(modelID, p) {
|
||||||
modelID = strings.TrimPrefix(modelID, p)
|
modelID = strings.TrimPrefix(modelID, p)
|
||||||
@@ -446,6 +454,7 @@ func (s *Server) handleListModels(c *gin.Context) {
|
|||||||
"xai": true, // Models from models.dev use 'xai' ID for Grok
|
"xai": true, // Models from models.dev use 'xai' ID for Grok
|
||||||
"llmgateway": true, // Catch-all for newer models
|
"llmgateway": true, // Catch-all for newer models
|
||||||
"ollama": true,
|
"ollama": true,
|
||||||
|
"xiaomi": true, // Xiaomi MiMo models
|
||||||
}
|
}
|
||||||
|
|
||||||
s.registryMu.RLock()
|
s.registryMu.RLock()
|
||||||
@@ -529,6 +538,8 @@ func (s *Server) selectProvider(modelID string) (providers.Provider, string, err
|
|||||||
strings.Contains(modelLower, "codellama") ||
|
strings.Contains(modelLower, "codellama") ||
|
||||||
strings.Contains(modelLower, "command-r") {
|
strings.Contains(modelLower, "command-r") {
|
||||||
providerName = "ollama"
|
providerName = "ollama"
|
||||||
|
} else if strings.HasPrefix(modelLower, "xiaomi/") || strings.Contains(modelLower, "mimo") || strings.Contains(modelLower, "xiaomi") {
|
||||||
|
providerName = "xiaomi"
|
||||||
}
|
}
|
||||||
|
|
||||||
p, ok := s.providers[providerName]
|
p, ok := s.providers[providerName]
|
||||||
@@ -548,7 +559,7 @@ func (s *Server) handleChatCompletions(c *gin.Context) {
|
|||||||
|
|
||||||
// Strip common prefixes and prepare model ID
|
// Strip common prefixes and prepare model ID
|
||||||
modelID := req.Model
|
modelID := req.Model
|
||||||
prefixes := []string{"gemini/", "google/", "openai/", "deepseek/", "moonshot/", "grok/", "ollama/"}
|
prefixes := []string{"gemini/", "google/", "openai/", "deepseek/", "moonshot/", "grok/", "ollama/", "xiaomi/"}
|
||||||
for _, p := range prefixes {
|
for _, p := range prefixes {
|
||||||
if strings.HasPrefix(modelID, p) {
|
if strings.HasPrefix(modelID, p) {
|
||||||
modelID = strings.TrimPrefix(modelID, p)
|
modelID = strings.TrimPrefix(modelID, p)
|
||||||
|
|||||||
Reference in New Issue
Block a user