feat: Plexum-minimax-provider v0.1 — MiniMax via Anthropic-compat endpoint
Plexum ProviderPlugin that serves MiniMax models through MiniMax's Anthropic-compatible HTTP endpoint (https://api.minimax.io/anthropic, or CN api.minimaxi.com). Inspired by openclaw's extensions/minimax provider-registration, but rewritten in Go for Plexum's SDK. internal/anthropic/ (~210 LOC + 6 tests): - minimal HTTP+SSE Anthropic Messages client (POST /v1/messages, stream:true, parses event:/data: SSE frames) - handles non-2xx as HTTP error; stream errors land as Event{Type:"error"} - 1 MiB SSE line cap; per-conn 5min timeout internal/translate/ (~220 LOC): - CanonicalToAnthropic: canonical.TurnRequest → MessagesRequest - blockToAnthropic: TextBlock / ToolUseBlock / ToolResultBlock / ThinkingBlock → loose ContentBlock map; preserves signatures + cache control - Translator: per-turn state machine; consumes anthropic.Event stream and emits canonical.TurnEvent stream (handles thinking blocks + tool_use input_json_delta accumulation + signature_delta capture) cmd/plexum-minimax-provider-plugin/: - Plugin manifest declares provider.models = [MiniMax-M2.7, MiniMax-M2.7-highspeed] - Backend fixed to "api" (per scope); region "global"|"cn" + base_url override supported via config - HostConfig from <profile>/plugins/plexum-minimax-provider/config.json {api_key, region?, base_url?, max_tokens_default?} scripts/install.sh: build + manifest emit; operator writes config.json + allows plugin + adds an agent + restarts. End-to-end verified against the real key: 1. plexum say --agent-id mini ... → "Hi, I'm MiniMax!" 2. Multi-turn continuity: agent recalled the prior reply 3. Via gateway socket: {"outcome":"text","text":"\n\npong"} 4. Via Fabric channel (alice posts → plugin inbound → mini agent → MiniMax → outbound REST → reply visible in bt2-clean seq=11): "Hi there! 👋 Fun fact: Octopuses have three hearts, blue blood, and neurons distributed throughout their arms—so their tentacles can 'think'" The MiniMax-M2.7-highspeed variant works the same way but hit a Code Plan rate-limit ceiling during testing (not a plugin issue). Deferred: - OAuth (Code Plan portal) — not in v1 scope per request - MiniMax Portal provider (separate provider id minimax-portal) - Image / TTS / video / music providers (separate plugins later) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
152
cmd/plexum-minimax-provider-plugin/main.go
Normal file
152
cmd/plexum-minimax-provider-plugin/main.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// plexum-minimax-provider-plugin is a Plexum ProviderPlugin that
|
||||
// serves MiniMax models via MiniMax's Anthropic-compatible endpoint.
|
||||
//
|
||||
// Backend is fixed to "api" (per scope): global endpoint
|
||||
// https://api.minimax.io/anthropic, CN endpoint https://api.minimaxi.com/anthropic.
|
||||
// Authentication: a single API key sourced from the plugin's config
|
||||
// file at <profile>/plugins/plexum-minimax-provider/config.json.
|
||||
//
|
||||
// Declared models (advertised in manifest.contracts.provider.models):
|
||||
// - MiniMax-M2.7
|
||||
// - MiniMax-M2.7-highspeed
|
||||
//
|
||||
// Operator points an agent at one of these via agent.json.model and
|
||||
// Plexum's ProviderRouter dispatches each turn here.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical"
|
||||
plugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin"
|
||||
|
||||
"git.hangman-lab.top/hzhang/Plexum-minimax-provider/internal/anthropic"
|
||||
"git.hangman-lab.top/hzhang/Plexum-minimax-provider/internal/translate"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "plexum-minimax-provider"
|
||||
|
||||
defaultBaseURLGlobal = "https://api.minimax.io/anthropic"
|
||||
defaultBaseURLCN = "https://api.minimaxi.com/anthropic"
|
||||
defaultMaxTokens = 4096 // MiniMax M2.7 can serve up to 131072 — but most turns want less
|
||||
)
|
||||
|
||||
// supportedModels = what the manifest's provider.models advertises.
|
||||
// Operator agent.json.model values must match one of these.
|
||||
var supportedModels = []string{"MiniMax-M2.7", "MiniMax-M2.7-highspeed"}
|
||||
|
||||
// HostConfig is the per-profile plugin config at
|
||||
// <profile>/plugins/plexum-minimax-provider/config.json:
|
||||
//
|
||||
// {
|
||||
// "api_key": "sk-cp-...", // required
|
||||
// "region": "global" | "cn", // default "global"
|
||||
// "base_url": "https://...", // optional override
|
||||
// "max_tokens_default": 4096 // optional default when TurnRequest.MaxTokens unset
|
||||
// }
|
||||
type HostConfig struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Region string `json:"region"`
|
||||
BaseURL string `json:"base_url"`
|
||||
MaxTokensDefault int `json:"max_tokens_default"`
|
||||
}
|
||||
|
||||
type minimaxPlugin struct {
|
||||
host plugin.HostAPI
|
||||
cfg HostConfig
|
||||
cli *anthropic.Client
|
||||
}
|
||||
|
||||
func (p *minimaxPlugin) Manifest() plugin.Manifest {
|
||||
return plugin.Manifest{
|
||||
Name: pluginName,
|
||||
Version: "0.1.0",
|
||||
Activation: plugin.ActivationLazy,
|
||||
Executable: "plexum-minimax-provider-plugin",
|
||||
Contracts: plugin.Contracts{
|
||||
Provider: &plugin.ProviderContract{Models: supportedModels},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *minimaxPlugin) Init(ctx context.Context, host plugin.HostAPI) error {
|
||||
p.host = host
|
||||
|
||||
profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT")
|
||||
if profileRoot == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
profileRoot = filepath.Join(home, ".plexum")
|
||||
}
|
||||
cfgPath := filepath.Join(profileRoot, "plugins", pluginName, "config.json")
|
||||
raw, err := os.ReadFile(cfgPath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("read %s: %w", cfgPath, err)
|
||||
}
|
||||
if len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &p.cfg); err != nil {
|
||||
return fmt.Errorf("parse %s: %w", cfgPath, err)
|
||||
}
|
||||
}
|
||||
if p.cfg.APIKey == "" {
|
||||
return fmt.Errorf("minimax: api_key missing in %s", cfgPath)
|
||||
}
|
||||
base := p.cfg.BaseURL
|
||||
if base == "" {
|
||||
if p.cfg.Region == "cn" {
|
||||
base = defaultBaseURLCN
|
||||
} else {
|
||||
base = defaultBaseURLGlobal
|
||||
}
|
||||
}
|
||||
if p.cfg.MaxTokensDefault <= 0 {
|
||||
p.cfg.MaxTokensDefault = defaultMaxTokens
|
||||
}
|
||||
p.cli = anthropic.New(base, p.cfg.APIKey)
|
||||
host.Log("info", "minimax provider initialized", map[string]any{
|
||||
"base": base, "models": supportedModels,
|
||||
"max_tokens_default": p.cfg.MaxTokensDefault,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream is the ProviderPlugin entrypoint. canonical.TurnRequest in,
|
||||
// channel of canonical.TurnEvent out (plugin author owns + closes the
|
||||
// channel; SDK forwards the stream over MCP notifications).
|
||||
func (p *minimaxPlugin) Stream(ctx context.Context, modelID string, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) {
|
||||
apiReq, err := translate.CanonicalToAnthropic(req, modelID, p.cfg.MaxTokensDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := p.cli.StreamMessages(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(chan canonical.TurnEvent, 32)
|
||||
go func() {
|
||||
defer close(out)
|
||||
tr := translate.NewTranslator()
|
||||
for ev := range raw {
|
||||
for _, te := range tr.Translate(ev) {
|
||||
select {
|
||||
case out <- te:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := plugin.Serve(&minimaxPlugin{}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "plexum-minimax-provider-plugin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user