// plexum-kimi-provider-plugin is a Plexum ProviderPlugin that serves // Moonshot Kimi K2.6 ("Kimi for Coding") via Kimi's Anthropic-compatible // coding endpoint (https://api.kimi.com/coding/v1/messages). // // Reference: openclaw/extensions/kimi-coding/provider-catalog.ts — // same base URL + User-Agent convention. The endpoint accepts an // `sk-kimi-` subscription API key minted from kimi.com/code. // // Declared models: // - kimi-for-coding (current; default) // - kimi-code (legacy alias; same model server-side) // - k2p5 (legacy alias; same) // // Operator agent.json.model = "kimi-for-coding" routes here via // Plexum's ProviderRouter. 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-anthropic-compat-client/anthropic" "git.hangman-lab.top/hzhang/Plexum-anthropic-compat-client/translate" ) const ( pluginName = "plexum-kimi-provider" // Kimi coding endpoint. The path /v1/messages is appended by the // anthropic client; baseURL must NOT include /v1. defaultBaseURL = "https://api.kimi.com/coding" // UA that openclaw's kimi-coding plugin sets. Likely a server-side // allowlist marker for the coding subscription auth path; omitting // it currently still works but we send it for parity. defaultUserAgent = "claude-code/0.1.0" // Kimi K2.6 supports up to 32768 output tokens per request. defaultMaxTokens = 8192 ) // supportedModels = what manifest.contracts.provider.models advertises. // The legacy aliases (kimi-code, k2p5) route the same way; Kimi server // accepts them and returns kimi-for-coding-equivalent output. var supportedModels = []string{"kimi-for-coding", "kimi-code", "k2p5"} // HostConfig is per-profile plugin config at // /plugins/plexum-kimi-provider/config.json: // // { // "api_key": "sk-kimi-...", // required // "base_url": "https://...", // optional override // "user_agent": "...", // optional; default "claude-code/0.1.0" // "max_tokens_default": 8192 // optional // } type HostConfig struct { APIKey string `json:"api_key"` BaseURL string `json:"base_url"` UserAgent string `json:"user_agent"` MaxTokensDefault int `json:"max_tokens_default"` } type kimiPlugin struct { host plugin.HostAPI cfg HostConfig cli *anthropic.Client } func (p *kimiPlugin) Manifest() plugin.Manifest { return plugin.Manifest{ Name: pluginName, Version: "0.1.0", Activation: plugin.ActivationLazy, Executable: "plexum-kimi-provider-plugin", Contracts: plugin.Contracts{ Provider: &plugin.ProviderContract{Models: supportedModels}, }, } } func (p *kimiPlugin) 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("kimi: api_key missing in %s", cfgPath) } base := p.cfg.BaseURL if base == "" { base = defaultBaseURL } ua := p.cfg.UserAgent if ua == "" { ua = defaultUserAgent } if p.cfg.MaxTokensDefault <= 0 { p.cfg.MaxTokensDefault = defaultMaxTokens } p.cli = anthropic.New(base, p.cfg.APIKey) p.cli.UserAgent = ua host.Log("info", "kimi provider initialized", map[string]any{ "base": base, "user_agent": ua, "models": supportedModels, "max_tokens_default": p.cfg.MaxTokensDefault, }) return nil } // Stream is the ProviderPlugin entrypoint. canonical.TurnRequest in, // channel of canonical.TurnEvent out. func (p *kimiPlugin) Stream(ctx context.Context, modelID string, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) { // Normalize legacy aliases to the current canonical id so the wire // model name is consistent (Kimi server accepts all 3, but this // keeps logs + telemetry tidy). wireModel := modelID if wireModel == "kimi-code" || wireModel == "k2p5" { wireModel = "kimi-for-coding" } apiReq, err := translate.CanonicalToAnthropic(req, wireModel, 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(&kimiPlugin{}); err != nil { fmt.Fprintf(os.Stderr, "plexum-kimi-provider-plugin: %v\n", err) os.Exit(1) } }