feat: Add support for Kimi For Coding model
This commit is contained in:
parent
0f700a6bf0
commit
cfd88cfe5d
6 changed files with 195 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
88
pkg/providers/kimi_code_provider.go
Normal file
88
pkg/providers/kimi_code_provider.go
Normal 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"
|
||||
}
|
||||
45
pkg/providers/kimi_code_provider_test.go
Normal file
45
pkg/providers/kimi_code_provider_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue