feat: Add support for Kimi For Coding model
This commit is contained in:
parent
86da6a7d56
commit
59cf349543
6 changed files with 195 additions and 5 deletions
|
|
@ -43,19 +43,26 @@ func createCodexAuthProvider() (LLMProvider, error) {
|
||||||
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
|
// - "openai/gpt-4o" -> ("openai", "gpt-4o")
|
||||||
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
|
// - "anthropic/claude-sonnet-4.6" -> ("anthropic", "claude-sonnet-4.6")
|
||||||
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
|
// - "gpt-4o" -> ("openai", "gpt-4o") // default protocol
|
||||||
|
// - "kimi-for-coding" -> ("kimi-code", "kimi-for-coding") // special case
|
||||||
func ExtractProtocol(model string) (protocol, modelID string) {
|
func ExtractProtocol(model string) (protocol, modelID string) {
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
protocol, modelID, found := strings.Cut(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 "openai", model
|
||||||
}
|
}
|
||||||
return protocol, modelID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateProviderFromConfig creates a provider based on the ModelConfig.
|
// CreateProviderFromConfig creates a provider based on the Model field to determine which provider to create.
|
||||||
// It uses the protocol prefix in the Model field to determine which provider to create.
|
|
||||||
// Supported protocols: openai, litellm, anthropic, anthropic-messages, antigravity,
|
// 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.
|
// Returns the provider, the model ID (without protocol prefix), and any error.
|
||||||
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
|
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
|
@ -82,6 +89,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
if cfg.APIKey == "" && cfg.APIBase == "" {
|
if cfg.APIKey == "" && cfg.APIBase == "" {
|
||||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiBase := cfg.APIBase
|
apiBase := cfg.APIBase
|
||||||
if apiBase == "" {
|
if apiBase == "" {
|
||||||
apiBase = getDefaultAPIBase(protocol)
|
apiBase = getDefaultAPIBase(protocol)
|
||||||
|
|
@ -114,6 +122,22 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
cfg.RequestTimeout,
|
cfg.RequestTimeout,
|
||||||
), modelID, nil
|
), 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":
|
case "anthropic":
|
||||||
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
||||||
// Use OAuth credentials from auth store
|
// Use OAuth credentials from auth store
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,12 @@ func TestExtractProtocol(t *testing.T) {
|
||||||
wantProtocol: "nvidia",
|
wantProtocol: "nvidia",
|
||||||
wantModelID: "meta/llama-3.1-8b",
|
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 {
|
for _, tt := range tests {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,18 @@ func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField st
|
||||||
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
apiKey, apiBase, proxy, maxTokensField string,
|
apiKey, apiBase, proxy, maxTokensField string,
|
||||||
requestTimeoutSeconds int,
|
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 {
|
) *HTTPProvider {
|
||||||
return &HTTPProvider{
|
return &HTTPProvider{
|
||||||
delegate: openai_compat.NewProvider(
|
delegate: openai_compat.NewProvider(
|
||||||
|
|
@ -38,6 +50,7 @@ func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
proxy,
|
proxy,
|
||||||
openai_compat.WithMaxTokensField(maxTokensField),
|
openai_compat.WithMaxTokensField(maxTokensField),
|
||||||
openai_compat.WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
|
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
|
apiBase string
|
||||||
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
headers map[string]string // Custom headers to add to requests
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*Provider)
|
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 {
|
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: defaultRequestTimeout,
|
Timeout: defaultRequestTimeout,
|
||||||
|
|
@ -179,6 +188,11 @@ func (p *Provider) Chat(
|
||||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
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)
|
resp, err := p.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue