Files
Plexum-kimi-provider/cmd/plexum-kimi-provider-plugin/main.go
hzhang 0a889250a6 refactor: import shared Plexum-anthropic-compat-client lib
Same refactor as Plexum-minimax-provider — drop the duplicated
internal/{anthropic,translate}, import from the shared
Plexum-anthropic-compat-client repo. UserAgent + ExtraHeaders fields
the Kimi plugin needs already live on Client there.

No behavioral change. Live re-verified after refactor: plexum say →
"ready" (via Kimi K2.6).
2026-05-31 20:35:11 +01:00

170 lines
5.0 KiB
Go

// 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
// <profile>/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)
}
}