feat: Plexum-kimi-provider v0.1 — Moonshot Kimi via coding endpoint
Plexum ProviderPlugin that serves Moonshot Kimi K2.6 ("Kimi for
Coding") via Kimi's Anthropic-compatible coding endpoint at
https://api.kimi.com/coding/v1/messages. Port of openclaw's
extensions/kimi-coding/provider-catalog.ts to Go + Plexum SDK.
Repo structure parallels Plexum-minimax-provider:
- internal/anthropic/ HTTP+SSE Anthropic Messages client, with new
UserAgent field (Kimi expects "claude-code/
0.1.0" — openclaw plugin parity)
- internal/translate/ canonical ↔ Anthropic translator (re-used
shape from MiniMax — no Kimi-specific quirks
needed for v1 plain-text path)
- cmd/plexum-kimi-provider-plugin/ ProviderPlugin entry
Declared models (Kimi server accepts all three; plugin normalizes
legacy aliases to the canonical id on the wire):
kimi-for-coding (current, default)
kimi-code (legacy alias)
k2p5 (legacy alias)
HostConfig: api_key (required), base_url (override), user_agent
(default "claude-code/0.1.0"), max_tokens_default (default 8192).
End-to-end verified against the live `sk-kimi-` subscription key:
1. CLI embedded turn 1: "Hi there! I'm Kimi."
2. CLI embedded turn 2: "I said hi, I'm Kimi." (multi-turn context OK)
3. Via gateway socket: {"outcome":"text","text":"...pong"}
4. Via Fabric channel: alice → bt2-clean → kimi agent → Kimi K2.6 →
outbound REST → reply in channel seq=15:
"Concise concurrency: goroutines and channels
make parallel code readable, safe, and
efficient without the usual threading
complexity."
Test the bidirectional channel pipeline works with a fresh provider:
Fabric (channel plugin) + Kimi (provider plugin) wired through Plexum
agentloop, MiniMax-style plugin packaging.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
169
cmd/plexum-kimi-provider-plugin/main.go
Normal file
169
cmd/plexum-kimi-provider-plugin/main.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// 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-kimi-provider/internal/anthropic"
|
||||
"git.hangman-lab.top/hzhang/Plexum-kimi-provider/internal/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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user