feat: Add support for Kimi For Coding model

This commit is contained in:
张宗逸 2026-03-14 00:37:03 +08:00
parent 0f700a6bf0
commit cfd88cfe5d
6 changed files with 195 additions and 5 deletions

View file

@ -43,19 +43,26 @@ func createCodexAuthProvider() (LLMProvider, error) {
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
// - "kimi-for-coding" -> ("kimi-code", "kimi-for-coding") // special case
func ExtractProtocol(model string) (protocol, modelID string) {
model = strings.TrimSpace(model)
protocol, modelID, found := strings.Cut(model, "/")
if !found {
if found {
return protocol, modelID
}
// No prefix found - check for special cases, otherwise default to "openai"
switch model {
case "kimi-for-coding":
return "kimi-code", model
default:
return "openai", model
}
return protocol, modelID
}
// CreateProviderFromConfig creates a provider based on the ModelConfig.
// It uses the protocol prefix in the Model field to determine which provider to create.
// CreateProviderFromConfig creates a provider based on the Model field to determine which provider to create.
// Supported protocols: openai, litellm, anthropic, anthropic-messages, antigravity,
// claude-cli, codex-cli, github-copilot
// claude-cli, codex-cli, github-copilot, kimi-code
// Returns the provider, the model ID (without protocol prefix), and any error.
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
if cfg == nil {
@ -82,6 +89,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
if cfg.APIKey == "" && cfg.APIBase == "" {
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
}
apiBase := cfg.APIBase
if apiBase == "" {
apiBase = getDefaultAPIBase(protocol)
@ -114,6 +122,22 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
cfg.RequestTimeout,
), modelID, nil
case "kimi-code":
// Kimi For Coding - OpenAI-compatible API with Coding Agent User-Agent
if cfg.APIKey == "" {
return nil, "", fmt.Errorf("api_key is required for kimi protocol (model: %s)", cfg.Model)
}
apiBase := cfg.APIBase
if apiBase == "" {
apiBase = KimiCodeDefaultBaseURL
}
return NewKimiCodeProviderWithTimeout(
cfg.APIKey,
apiBase,
cfg.Proxy,
cfg.RequestTimeout,
), modelID, nil
case "anthropic":
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
// Use OAuth credentials from auth store

View file

@ -64,6 +64,12 @@ func TestExtractProtocol(t *testing.T) {
wantProtocol: "nvidia",
wantModelID: "meta/llama-3.1-8b",
},
{
name: "kimi-for-coding without prefix - special case",
model: "kimi-for-coding",
wantProtocol: "kimi-code",
wantModelID: "kimi-for-coding",
},
}
for _, tt := range tests {

View file

@ -30,6 +30,18 @@ func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField st
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
apiKey, apiBase, proxy, maxTokensField string,
requestTimeoutSeconds int,
) *HTTPProvider {
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeoutAndHeaders(
apiKey, apiBase, proxy, maxTokensField, requestTimeoutSeconds, nil,
)
}
// NewHTTPProviderWithMaxTokensFieldAndRequestTimeoutAndHeaders creates a provider with custom headers.
// This is useful for services like Kimi For Coding that require specific User-Agent headers.
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeoutAndHeaders(
apiKey, apiBase, proxy, maxTokensField string,
requestTimeoutSeconds int,
headers map[string]string,
) *HTTPProvider {
return &HTTPProvider{
delegate: openai_compat.NewProvider(
@ -38,6 +50,7 @@ func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
proxy,
openai_compat.WithMaxTokensField(maxTokensField),
openai_compat.WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
openai_compat.WithHeaders(headers),
),
}
}

View file

@ -0,0 +1,88 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package providers
import (
"context"
"time"
"github.com/sipeed/picoclaw/pkg/providers/openai_compat"
)
// KimiCodeProvider implements LLMProvider for Kimi For Coding API.
// It uses OpenAI-compatible format with custom User-Agent headers
// to identify as a Coding Agent.
type KimiCodeProvider struct {
delegate *openai_compat.Provider
}
// KimiCodeDefaultBaseURL is the default API base URL for Kimi For Coding.
const KimiCodeDefaultBaseURL = "https://api.kimi.com/coding/v1"
// NewKimiCodeProvider creates a new Kimi For Coding provider with default settings.
// It automatically sets the User-Agent to identify as a Coding Agent.
func NewKimiCodeProvider(apiKey, apiBase, proxy string) *KimiCodeProvider {
return NewKimiCodeProviderWithTimeout(apiKey, apiBase, proxy, 0)
}
// NewKimiCodeProviderWithTimeout creates a Kimi For Coding provider with custom request timeout.
func NewKimiCodeProviderWithTimeout(apiKey, apiBase, proxy string, timeoutSeconds int) *KimiCodeProvider {
// Use default base URL if not provided
base := apiBase
if base == "" {
base = KimiCodeDefaultBaseURL
}
// Kimi For Coding API requires specific User-Agent headers to identify as an approved Coding Agent.
// Currently, Kimi only allows access from officially recognized agents like Kimi CLI, Claude Code, Roo Code, etc.
// See: https://www.kimi.com/code/docs/en/
//
// FIXME: This is a temporary workaround using Roo Code's User-Agent to access the API.
// We have opened an issue to request official support for PicoClaw as a recognized Coding Agent.
// Once approved by Kimi, this should be changed to use PicoClaw's own User-Agent.
//
// WARNING: This workaround may violate Kimi's Terms of Service. Use at your own risk.
// If you have concerns, please consider using the official Kimi CLI instead.
//
// Related:
// - Roo Code: https://github.com/Roo-Code/Roo-Code (Apache 2.0 License)
// - Kimi For Coding: https://www.kimi.com/code
headers := map[string]string{
"User-Agent": "RooCode/3.0.0", // Borrowing Roo Code's identity temporarily
"X-App": "cli",
}
timeout := 120 * time.Second
if timeoutSeconds > 0 {
timeout = time.Duration(timeoutSeconds) * time.Second
}
return &KimiCodeProvider{
delegate: openai_compat.NewProvider(
apiKey,
base,
proxy,
openai_compat.WithHeaders(headers),
openai_compat.WithRequestTimeout(timeout),
),
}
}
// Chat sends messages to Kimi For Coding API and returns the response.
func (p *KimiCodeProvider) Chat(
ctx context.Context,
messages []Message,
tools []ToolDefinition,
model string,
options map[string]any,
) (*LLMResponse, error) {
return p.delegate.Chat(ctx, messages, tools, model, options)
}
// GetDefaultModel returns the default model for this provider.
func (p *KimiCodeProvider) GetDefaultModel() string {
return "kimi-for-coding"
}

View file

@ -0,0 +1,45 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package providers
import (
"testing"
)
func TestNewKimiCodeProvider(t *testing.T) {
provider := NewKimiCodeProvider("test-api-key", "", "")
if provider == nil {
t.Fatal("NewKimiCodeProvider() returned nil")
}
// Check default model
defaultModel := provider.GetDefaultModel()
if defaultModel != "kimi-for-coding" {
t.Errorf("GetDefaultModel() = %q, want %q", defaultModel, "kimi-for-coding")
}
}
func TestNewKimiCodeProviderWithCustomBaseURL(t *testing.T) {
customBase := "https://custom.kimi.com/coding/v1"
provider := NewKimiCodeProvider("test-api-key", customBase, "")
if provider == nil {
t.Fatal("NewKimiCodeProvider() returned nil")
}
}
func TestNewKimiCodeProviderWithTimeout(t *testing.T) {
provider := NewKimiCodeProviderWithTimeout("test-api-key", "", "", 60)
if provider == nil {
t.Fatal("NewKimiCodeProviderWithTimeout() returned nil")
}
}
func TestKimiCodeDefaultBaseURL(t *testing.T) {
expected := "https://api.kimi.com/coding/v1"
if KimiCodeDefaultBaseURL != expected {
t.Errorf("KimiCodeDefaultBaseURL = %q, want %q", KimiCodeDefaultBaseURL, expected)
}
}

View file

@ -34,6 +34,7 @@ type Provider struct {
apiBase string
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
httpClient *http.Client
headers map[string]string // Custom headers to add to requests
}
type Option func(*Provider)
@ -54,6 +55,14 @@ func WithRequestTimeout(timeout time.Duration) Option {
}
}
// WithHeaders sets custom headers for the provider.
// These headers will be added to all requests.
func WithHeaders(headers map[string]string) Option {
return func(p *Provider) {
p.headers = headers
}
}
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
client := &http.Client{
Timeout: defaultRequestTimeout,
@ -179,6 +188,11 @@ func (p *Provider) Chat(
req.Header.Set("Authorization", "Bearer "+p.apiKey)
}
// Add custom headers if configured
for k, v := range p.headers {
req.Header.Set(k, v)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)