diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 4f867a05..f6e0f8ca 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -86,6 +86,10 @@ jobs: env: XAI_API_KEY: ${{ secrets.XAI_API_KEY }} + - name: Xiaomi MiMo + run: go run ./cmd/xiaomi-mimo/main.go + continue-on-error: true + - name: Create Pull Request uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: diff --git a/Taskfile.yaml b/Taskfile.yaml index 07ba2dd9..1218517c 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -73,6 +73,7 @@ tasks: - task: gen:venice - task: gen:vercel - task: gen:xai + - task: gen:xiaomi-mimo gen:aihubmix: desc: Generate aihubmix provider configurations @@ -154,6 +155,11 @@ tasks: cmds: - go run cmd/xai/main.go + gen:xiaomi-mimo: + desc: Generate Xiaomi MiMo provider configurations + cmds: + - go run cmd/xiaomi-mimo/main.go + update: desc: Trigger the update workflow on GitHub cmds: diff --git a/cmd/xiaomi-mimo/main.go b/cmd/xiaomi-mimo/main.go new file mode 100644 index 00000000..85bd9148 --- /dev/null +++ b/cmd/xiaomi-mimo/main.go @@ -0,0 +1,282 @@ +// Package main provides a command-line tool to fetch models from Xiaomi MiMo +// and generate configuration files for the provider. +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "slices" + "strings" + "time" + + "charm.land/catwalk/pkg/catwalk" +) + +// ModelsResponse is the OpenAI-compatible /v1/models response. +type ModelsResponse struct { + Data []MiMoModel `json:"data"` +} + +// MiMoModel represents a single model entry in the /v1/models response. +type MiMoModel struct { + ID string `json:"id"` +} + +// modelSpec holds metadata for each known MiMo model. The /v1/models endpoint +// only returns model IDs, so pricing and capabilities are sourced from here. +type modelSpec struct { + Name string + CostPer1MIn float64 + CostPer1MOut float64 + CostPer1MInCached float64 + ContextWindow int64 + DefaultMaxTokens int64 + CanReason bool + SupportsImages bool +} + +var reasoningLevels = []string{"low", "medium", "high"} + +// knownModels maps model IDs to their metadata. Only models present in both +// the API response and this map are included in the generated configs. +var knownModels = map[string]modelSpec{ + "mimo-v2.5-pro": { + Name: "MiMo V2.5 Pro", + CostPer1MIn: 0.435, + CostPer1MOut: 0.87, + CostPer1MInCached: 0.0036, + ContextWindow: 1_048_576, + DefaultMaxTokens: 131_072, + CanReason: true, + SupportsImages: false, + }, + "mimo-v2.5": { + Name: "MiMo V2.5", + CostPer1MIn: 0.14, + CostPer1MOut: 0.28, + CostPer1MInCached: 0.0028, + ContextWindow: 1_048_576, + DefaultMaxTokens: 131_072, + CanReason: true, + SupportsImages: true, + }, + "mimo-v2-flash": { + Name: "MiMo V2 Flash", + CostPer1MIn: 0.1, + CostPer1MOut: 0.3, + CostPer1MInCached: 0.01, + ContextWindow: 262_144, + DefaultMaxTokens: 65_536, + CanReason: true, + SupportsImages: false, + }, +} + +type providerConfig struct { + name string + id catwalk.InferenceProvider + apiKey string + apiEndpoint string + tokenPlan bool +} + +type providerFile struct { + path string + config providerConfig +} + +func fetchModels(endpoint, apiKey string) ([]string, error) { + client := &http.Client{Timeout: 30 * time.Second} + req, _ := http.NewRequestWithContext( + context.Background(), + "GET", + endpoint+"/models", + nil, + ) + req.Header.Set("User-Agent", "Crush-Client/1.0") + req.Header.Set("Authorization", "Bearer "+apiKey) + + resp, err := client.Do(req) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer resp.Body.Close() //nolint:errcheck + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body) + } + + _ = os.MkdirAll("tmp", 0o700) + _ = os.WriteFile("tmp/xiaomi-mimo-response.json", body, 0o600) + + var mr ModelsResponse + if err := json.Unmarshal(body, &mr); err != nil { + return nil, err //nolint:wrapcheck + } + + ids := make([]string, len(mr.Data)) + for i, m := range mr.Data { + ids[i] = m.ID + } + return ids, nil +} + +func buildModels(apiModelIDs []string, zeroCost bool) []catwalk.Model { + var models []catwalk.Model + for _, id := range apiModelIDs { + spec, ok := knownModels[id] + if !ok { + continue + } + + var reasoningEffort string + var levels []string + if spec.CanReason { + levels = reasoningLevels + reasoningEffort = "medium" + } + + m := catwalk.Model{ + ID: id, + Name: spec.Name, + CostPer1MIn: spec.CostPer1MIn, + CostPer1MOut: spec.CostPer1MOut, + CostPer1MInCached: spec.CostPer1MInCached, + CostPer1MOutCached: 0, + ContextWindow: spec.ContextWindow, + DefaultMaxTokens: spec.DefaultMaxTokens, + CanReason: spec.CanReason, + ReasoningLevels: levels, + DefaultReasoningEffort: reasoningEffort, + SupportsImages: spec.SupportsImages, + } + + if zeroCost { + m.CostPer1MIn = 0 + m.CostPer1MOut = 0 + m.CostPer1MInCached = 0 + } + + models = append(models, m) + } + + slices.SortFunc(models, func(a, b catwalk.Model) int { + return strings.Compare(a.ID, b.ID) + }) + + return models +} + +func writeProvider(path string, provider catwalk.Provider) { + data, err := json.MarshalIndent(provider, "", " ") + if err != nil { + log.Fatalf("Error marshaling %s: %v", provider.Name, err) + } + data = append(data, '\n') + + if err := os.WriteFile(path, data, 0o600); err != nil { + log.Fatalf("Error writing %s: %v", path, err) + } + + fmt.Printf("Generated %s with %d models\n", path, len(provider.Models)) +} + +func main() { + apiKey := os.Getenv("MIMO_API_KEY") + if apiKey == "" { + log.Fatal("MIMO_API_KEY environment variable is not set") + } + + // Default to the pay-as-you-go endpoint; allow override (e.g. for token + // plan keys that only work on regional endpoints). + fetchEndpoint := os.Getenv("MIMO_API_ENDPOINT") + if fetchEndpoint == "" { + fetchEndpoint = "https://api.xiaomimimo.com/v1" + } + + apiModelIDs, err := fetchModels(fetchEndpoint, apiKey) + if err != nil { + log.Fatal("Error fetching Xiaomi MiMo models:", err) + } + + knownCount := 0 + for _, id := range apiModelIDs { + if _, ok := knownModels[id]; ok { + knownCount++ + } else { + log.Printf("Warning: model %q found in API but not in known models map; skipping", id) + } + } + if knownCount == 0 { + log.Fatal("No known models found in API response") + } + + providers := []providerFile{ + { + path: "internal/providers/configs/xiaomi-mimo.json", + config: providerConfig{ + name: "Xiaomi MiMo", + id: catwalk.InferenceProviderXiaomiMiMo, + apiKey: "$MIMO_API_KEY", + apiEndpoint: "https://api.xiaomimimo.com/v1", + }, + }, + { + path: "internal/providers/configs/xiaomi-mimo-token-plan-ams.json", + config: providerConfig{ + name: "Xiaomi MiMo Token Plan (Europe)", + id: catwalk.InferenceProviderXiaomiMiMoTokenPlanAMS, + apiKey: "$MIMO_TOKEN_PLAN_API_KEY", + apiEndpoint: "https://token-plan-ams.xiaomimimo.com/v1", + tokenPlan: true, + }, + }, + { + path: "internal/providers/configs/xiaomi-mimo-token-plan-cn.json", + config: providerConfig{ + name: "Xiaomi MiMo Token Plan (China)", + id: catwalk.InferenceProviderXiaomiMiMoTokenPlanCN, + apiKey: "$MIMO_TOKEN_PLAN_API_KEY", + apiEndpoint: "https://token-plan-cn.xiaomimimo.com/v1", + tokenPlan: true, + }, + }, + { + path: "internal/providers/configs/xiaomi-mimo-token-plan-sgp.json", + config: providerConfig{ + name: "Xiaomi MiMo Token Plan (Singapore)", + id: catwalk.InferenceProviderXiaomiMiMoTokenPlanSGP, + apiKey: "$MIMO_TOKEN_PLAN_API_KEY", + apiEndpoint: "https://token-plan-sgp.xiaomimimo.com/v1", + tokenPlan: true, + }, + }, + } + + for _, pf := range providers { + models := buildModels(apiModelIDs, pf.config.tokenPlan) + + p := catwalk.Provider{ + Name: pf.config.name, + ID: pf.config.id, + APIKey: pf.config.apiKey, + APIEndpoint: pf.config.apiEndpoint, + Type: catwalk.TypeOpenAICompat, + DefaultLargeModelID: "mimo-v2.5-pro", + DefaultSmallModelID: "mimo-v2.5", + Models: models, + } + + writeProvider(pf.path, p) + } +} diff --git a/internal/providers/configs/xiaomi-mimo-token-plan-ams.json b/internal/providers/configs/xiaomi-mimo-token-plan-ams.json new file mode 100644 index 00000000..4e24433e --- /dev/null +++ b/internal/providers/configs/xiaomi-mimo-token-plan-ams.json @@ -0,0 +1,47 @@ +{ + "name": "Xiaomi MiMo Token Plan (Europe)", + "id": "xiaomi-mimo-token-plan-ams", + "api_key": "$MIMO_TOKEN_PLAN_API_KEY", + "api_endpoint": "https://token-plan-ams.xiaomimimo.com/v1", + "type": "openai-compat", + "default_large_model_id": "mimo-v2.5-pro", + "default_small_model_id": "mimo-v2.5", + "models": [ + { + "id": "mimo-v2.5", + "name": "MiMo V2.5", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": true + }, + { + "id": "mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": false + } + ] +} diff --git a/internal/providers/configs/xiaomi-mimo-token-plan-cn.json b/internal/providers/configs/xiaomi-mimo-token-plan-cn.json new file mode 100644 index 00000000..0ee1de27 --- /dev/null +++ b/internal/providers/configs/xiaomi-mimo-token-plan-cn.json @@ -0,0 +1,47 @@ +{ + "name": "Xiaomi MiMo Token Plan (China)", + "id": "xiaomi-mimo-token-plan-cn", + "api_key": "$MIMO_TOKEN_PLAN_API_KEY", + "api_endpoint": "https://token-plan-cn.xiaomimimo.com/v1", + "type": "openai-compat", + "default_large_model_id": "mimo-v2.5-pro", + "default_small_model_id": "mimo-v2.5", + "models": [ + { + "id": "mimo-v2.5", + "name": "MiMo V2.5", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": true + }, + { + "id": "mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": false + } + ] +} diff --git a/internal/providers/configs/xiaomi-mimo-token-plan-sgp.json b/internal/providers/configs/xiaomi-mimo-token-plan-sgp.json new file mode 100644 index 00000000..39aac0ee --- /dev/null +++ b/internal/providers/configs/xiaomi-mimo-token-plan-sgp.json @@ -0,0 +1,47 @@ +{ + "name": "Xiaomi MiMo Token Plan (Singapore)", + "id": "xiaomi-mimo-token-plan-sgp", + "api_key": "$MIMO_TOKEN_PLAN_API_KEY", + "api_endpoint": "https://token-plan-sgp.xiaomimimo.com/v1", + "type": "openai-compat", + "default_large_model_id": "mimo-v2.5-pro", + "default_small_model_id": "mimo-v2.5", + "models": [ + { + "id": "mimo-v2.5", + "name": "MiMo V2.5", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": true + }, + { + "id": "mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": false + } + ] +} diff --git a/internal/providers/configs/xiaomi-mimo.json b/internal/providers/configs/xiaomi-mimo.json new file mode 100644 index 00000000..31b2a8ed --- /dev/null +++ b/internal/providers/configs/xiaomi-mimo.json @@ -0,0 +1,47 @@ +{ + "name": "Xiaomi MiMo", + "id": "xiaomi-mimo", + "api_key": "$MIMO_API_KEY", + "api_endpoint": "https://api.xiaomimimo.com/v1", + "type": "openai-compat", + "default_large_model_id": "mimo-v2.5-pro", + "default_small_model_id": "mimo-v2.5", + "models": [ + { + "id": "mimo-v2.5", + "name": "MiMo V2.5", + "cost_per_1m_in": 0.14, + "cost_per_1m_out": 0.28, + "cost_per_1m_in_cached": 0.0028, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": true + }, + { + "id": "mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "cost_per_1m_in": 0.435, + "cost_per_1m_out": 0.87, + "cost_per_1m_in_cached": 0.0036, + "cost_per_1m_out_cached": 0, + "context_window": 1048576, + "default_max_tokens": 131072, + "can_reason": true, + "reasoning_levels": [ + "low", + "medium", + "high" + ], + "default_reasoning_effort": "medium", + "supports_attachments": false + } + ] +} diff --git a/internal/providers/providers.go b/internal/providers/providers.go index c2cd88df..8550398f 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -105,6 +105,18 @@ var vertexAIConfig []byte //go:embed configs/xai.json var xAIConfig []byte +//go:embed configs/xiaomi-mimo.json +var xiaomiMiMoConfig []byte + +//go:embed configs/xiaomi-mimo-token-plan-ams.json +var xiaomiMiMoTokenPlanAMSConfig []byte + +//go:embed configs/xiaomi-mimo-token-plan-cn.json +var xiaomiMiMoTokenPlanCNConfig []byte + +//go:embed configs/xiaomi-mimo-token-plan-sgp.json +var xiaomiMiMoTokenPlanSGPConfig []byte + //go:embed configs/zai.json var zAIConfig []byte @@ -154,6 +166,10 @@ var providerRegistry = []ProviderFunc{ vercelProvider, veniceProvider, vertexAIProvider, + xiaomiMiMoProvider, + xiaomiMiMoTokenPlanAMSProvider, + xiaomiMiMoTokenPlanCNProvider, + xiaomiMiMoTokenPlanSGPProvider, zhipuProvider, zhipuCodingProvider, } @@ -304,6 +320,22 @@ func xAIProvider() catwalk.Provider { return loadProviderFromConfig(xAIConfig) } +func xiaomiMiMoProvider() catwalk.Provider { + return loadProviderFromConfig(xiaomiMiMoConfig) +} + +func xiaomiMiMoTokenPlanAMSProvider() catwalk.Provider { + return loadProviderFromConfig(xiaomiMiMoTokenPlanAMSConfig) +} + +func xiaomiMiMoTokenPlanCNProvider() catwalk.Provider { + return loadProviderFromConfig(xiaomiMiMoTokenPlanCNConfig) +} + +func xiaomiMiMoTokenPlanSGPProvider() catwalk.Provider { + return loadProviderFromConfig(xiaomiMiMoTokenPlanSGPConfig) +} + func zAIProvider() catwalk.Provider { return loadProviderFromConfig(zAIConfig) } diff --git a/pkg/catwalk/provider.go b/pkg/catwalk/provider.go index 55835825..5b3a5c1e 100644 --- a/pkg/catwalk/provider.go +++ b/pkg/catwalk/provider.go @@ -21,40 +21,44 @@ type InferenceProvider string // All the inference providers supported by the system. const ( - InferenceProviderOpenAI InferenceProvider = "openai" - InferenceProviderAnthropic InferenceProvider = "anthropic" - InferenceProviderSynthetic InferenceProvider = "synthetic" - InferenceProviderGemini InferenceProvider = "gemini" - InferenceProviderAzure InferenceProvider = "azure" - InferenceProviderBedrock InferenceProvider = "bedrock" - InferenceProviderBedrockEurope InferenceProvider = "bedrock-europe" - InferenceProviderVertexAI InferenceProvider = "vertexai" - InferenceProviderXAI InferenceProvider = "xai" - InferenceProviderZAI InferenceProvider = "zai" - InferenceProviderDeepSeek InferenceProvider = "deepseek" - InferenceProviderZhipu InferenceProvider = "zhipu" - InferenceProviderZhipuCoding InferenceProvider = "zhipu-coding" - InferenceProviderGROQ InferenceProvider = "groq" - InferenceProviderOpenRouter InferenceProvider = "openrouter" - InferenceProviderCerebras InferenceProvider = "cerebras" - InferenceProviderVenice InferenceProvider = "venice" - InferenceProviderChutes InferenceProvider = "chutes" - InferenceProviderHuggingFace InferenceProvider = "huggingface" - InferenceAIHubMix InferenceProvider = "aihubmix" - InferenceKimiCoding InferenceProvider = "kimi-coding" - InferenceProviderCopilot InferenceProvider = "copilot" - InferenceProviderCortecs InferenceProvider = "cortecs" - InferenceProviderVercel InferenceProvider = "vercel" - InferenceProviderMiniMax InferenceProvider = "minimax" - InferenceProviderMiniMaxChina InferenceProvider = "minimax-china" - InferenceProviderIoNet InferenceProvider = "ionet" - InferenceProviderQiniuCloud InferenceProvider = "qiniucloud" - InferenceProviderAvian InferenceProvider = "avian" - InferenceProviderNebius InferenceProvider = "nebius" - InferenceProviderNeuralwatt InferenceProvider = "neuralwatt" - InferenceProviderOpenCodeZen InferenceProvider = "opencode-zen" - InferenceProviderOpenCodeGo InferenceProvider = "opencode-go" - InferenceProviderAlibabaSingapore InferenceProvider = "alibaba-singapore" + InferenceProviderOpenAI InferenceProvider = "openai" + InferenceProviderAnthropic InferenceProvider = "anthropic" + InferenceProviderSynthetic InferenceProvider = "synthetic" + InferenceProviderGemini InferenceProvider = "gemini" + InferenceProviderAzure InferenceProvider = "azure" + InferenceProviderBedrock InferenceProvider = "bedrock" + InferenceProviderBedrockEurope InferenceProvider = "bedrock-europe" + InferenceProviderVertexAI InferenceProvider = "vertexai" + InferenceProviderXAI InferenceProvider = "xai" + InferenceProviderZAI InferenceProvider = "zai" + InferenceProviderDeepSeek InferenceProvider = "deepseek" + InferenceProviderZhipu InferenceProvider = "zhipu" + InferenceProviderZhipuCoding InferenceProvider = "zhipu-coding" + InferenceProviderGROQ InferenceProvider = "groq" + InferenceProviderOpenRouter InferenceProvider = "openrouter" + InferenceProviderCerebras InferenceProvider = "cerebras" + InferenceProviderVenice InferenceProvider = "venice" + InferenceProviderChutes InferenceProvider = "chutes" + InferenceProviderHuggingFace InferenceProvider = "huggingface" + InferenceAIHubMix InferenceProvider = "aihubmix" + InferenceKimiCoding InferenceProvider = "kimi-coding" + InferenceProviderCopilot InferenceProvider = "copilot" + InferenceProviderCortecs InferenceProvider = "cortecs" + InferenceProviderVercel InferenceProvider = "vercel" + InferenceProviderMiniMax InferenceProvider = "minimax" + InferenceProviderMiniMaxChina InferenceProvider = "minimax-china" + InferenceProviderIoNet InferenceProvider = "ionet" + InferenceProviderQiniuCloud InferenceProvider = "qiniucloud" + InferenceProviderAvian InferenceProvider = "avian" + InferenceProviderNebius InferenceProvider = "nebius" + InferenceProviderNeuralwatt InferenceProvider = "neuralwatt" + InferenceProviderOpenCodeZen InferenceProvider = "opencode-zen" + InferenceProviderOpenCodeGo InferenceProvider = "opencode-go" + InferenceProviderAlibabaSingapore InferenceProvider = "alibaba-singapore" + InferenceProviderXiaomiMiMo InferenceProvider = "xiaomi-mimo" + InferenceProviderXiaomiMiMoTokenPlanAMS InferenceProvider = "xiaomi-mimo-token-plan-ams" // #nosec G101 -- provider ID, not a credential. + InferenceProviderXiaomiMiMoTokenPlanCN InferenceProvider = "xiaomi-mimo-token-plan-cn" // #nosec G101 -- provider ID, not a credential. + InferenceProviderXiaomiMiMoTokenPlanSGP InferenceProvider = "xiaomi-mimo-token-plan-sgp" // #nosec G101 -- provider ID, not a credential. ) // Provider represents an AI provider configuration. @@ -131,6 +135,10 @@ func KnownProviders() []InferenceProvider { InferenceProviderNeuralwatt, InferenceProviderOpenCodeZen, InferenceProviderOpenCodeGo, + InferenceProviderXiaomiMiMo, + InferenceProviderXiaomiMiMoTokenPlanAMS, + InferenceProviderXiaomiMiMoTokenPlanCN, + InferenceProviderXiaomiMiMoTokenPlanSGP, } }