From 2a2797f99dfeec869d742c465885631a812e0a90 Mon Sep 17 00:00:00 2001 From: Reginaldo Date: Mon, 23 Feb 2026 23:07:46 -0300 Subject: [PATCH] feat: add Kimi Coding compatibility and model preset --- pkg/config/defaults.go | 7 ++ pkg/providers/openai_compat/provider.go | 12 +++ pkg/providers/openai_compat/provider_test.go | 86 ++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index b96ee4d89..bdba7c9ac 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -177,6 +177,13 @@ func DefaultConfig() *Config { APIBase: "https://api.moonshot.cn/v1", APIKey: "", }, + // Kimi Plan/Coding - https://www.kimi.com/code/docs + { + ModelName: "kimi-plan-coding", + Model: "moonshot/kimi-k2-thinking", + APIBase: "https://api.kimi.com/coding/v1", + APIKey: "", + }, // Groq - https://console.groq.com/keys { diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 236a048c4..9639d2e23 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -125,6 +125,9 @@ func (p *Provider) Chat( if p.apiKey != "" { req.Header.Set("Authorization", "Bearer "+p.apiKey) } + if ua := selectUserAgent(p.apiBase); ua != "" { + req.Header.Set("User-Agent", ua) + } resp, err := p.httpClient.Do(req) if err != nil { @@ -247,6 +250,15 @@ func normalizeModel(model, apiBase string) string { } } +func selectUserAgent(apiBase string) string { + base := strings.ToLower(apiBase) + if strings.Contains(base, "api.kimi.com/coding") { + // Kimi For Coding currently restricts access to recognized coding-agent clients. + return "KimiCLI/1.3" + } + return "" +} + func asInt(v any) (int, bool) { switch val := v.(type) { case int: diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 42f9d42ab..16b48b691 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -232,6 +232,92 @@ func TestProvider_ProxyConfigured(t *testing.T) { } } +func TestProviderChat_UsesKimiUserAgentOnCodingEndpoint(t *testing.T) { + var gotUA string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotUA = r.Header.Get("User-Agent") + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + kimiBase := server.URL + "/api.kimi.com/coding/v1" + p := NewProvider("key", kimiBase, "") + _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "kimi-for-coding", nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + if gotUA != "KimiCLI/1.3" { + t.Fatalf("User-Agent = %q, want %q", gotUA, "KimiCLI/1.3") + } +} + +func TestProviderChat_KimiCodingModelAliases(t *testing.T) { + tests := []struct { + name string + model string + }{ + {name: "k2.5", model: "kimi-k2.5"}, + {name: "k2-thinking", model: "kimi-k2-thinking"}, + {name: "for-coding", model: "kimi-for-coding"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotUA string + var gotModel string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotUA = r.Header.Get("User-Agent") + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if m, ok := body["model"].(string); ok { + gotModel = m + } + + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + kimiBase := server.URL + "/api.kimi.com/coding/v1" + p := NewProvider("key", kimiBase, "") + _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, tt.model, nil) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + if gotUA != "KimiCLI/1.3" { + t.Fatalf("User-Agent = %q, want %q", gotUA, "KimiCLI/1.3") + } + if gotModel != tt.model { + t.Fatalf("model = %q, want %q", gotModel, tt.model) + } + }) + } +} + func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) { var requestBody map[string]any