diff --git a/pkg/config/migration_test.go b/pkg/config/migration_test.go index db8f4657d..7fda3a1fc 100644 --- a/pkg/config/migration_test.go +++ b/pkg/config/migration_test.go @@ -132,14 +132,15 @@ func TestConvertProvidersToModelList_AllProviders(t *testing.T) { Antigravity: ProviderConfig{AuthMethod: "oauth"}, Qwen: ProviderConfig{APIKey: "key17"}, Mistral: ProviderConfig{APIKey: "key18"}, + Opencode: ProviderConfig{APIKey: "key19"}, }, } result := ConvertProvidersToModelList(cfg) - // All 18 providers should be converted - if len(result) != 18 { - t.Errorf("len(result) = %d, want 18", len(result)) + // All 19 providers should be converted + if len(result) != 19 { + t.Errorf("len(result) = %d, want 19", len(result)) } } @@ -551,6 +552,65 @@ func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) { } } +func TestConvertProvidersToModelList_Opencode(t *testing.T) { + cfg := &Config{ + Providers: ProvidersConfig{ + Opencode: ProviderConfig{ + APIKey: "oc-test-key", + APIBase: "https://custom.opencode.ai/v1", + Proxy: "http://proxy:9090", + RequestTimeout: 60, + }, + }, + } + + result := ConvertProvidersToModelList(cfg) + + if len(result) != 1 { + t.Fatalf("len(result) = %d, want 1", len(result)) + } + + mc := result[0] + if mc.ModelName != "opencode" { + t.Errorf("ModelName = %q, want %q", mc.ModelName, "opencode") + } + if mc.Model != "opencode/auto" { + t.Errorf("Model = %q, want %q", mc.Model, "opencode/auto") + } + if mc.APIKey != "oc-test-key" { + t.Errorf("APIKey = %q, want %q", mc.APIKey, "oc-test-key") + } + if mc.APIBase != "https://custom.opencode.ai/v1" { + t.Errorf("APIBase = %q, want %q", mc.APIBase, "https://custom.opencode.ai/v1") + } + if mc.Proxy != "http://proxy:9090" { + t.Errorf("Proxy = %q, want %q", mc.Proxy, "http://proxy:9090") + } + if mc.RequestTimeout != 60 { + t.Errorf("RequestTimeout = %d, want %d", mc.RequestTimeout, 60) + } +} + +func TestConvertProvidersToModelList_Opencode_APIBaseOnly(t *testing.T) { + cfg := &Config{ + Providers: ProvidersConfig{ + Opencode: ProviderConfig{ + APIBase: "https://custom.opencode.ai/v1", + }, + }, + } + + result := ConvertProvidersToModelList(cfg) + + if len(result) != 1 { + t.Fatalf("len(result) = %d, want 1 (APIBase-only should create entry)", len(result)) + } + + if result[0].ModelName != "opencode" { + t.Errorf("ModelName = %q, want %q", result[0].ModelName, "opencode") + } +} + // Test for legacy config with protocol prefix in model name func TestConvertProvidersToModelList_LegacyModelWithProtocolPrefix(t *testing.T) { cfg := &Config{ diff --git a/pkg/providers/factory.go b/pkg/providers/factory.go index a332c39ee..3f46d0f3d 100644 --- a/pkg/providers/factory.go +++ b/pkg/providers/factory.go @@ -182,7 +182,7 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { } } case "opencode": - if cfg.Providers.Opencode.APIKey != "" { + if cfg.Providers.Opencode.APIKey != "" || cfg.Providers.Opencode.APIBase != "" { sel.apiKey = cfg.Providers.Opencode.APIKey sel.apiBase = cfg.Providers.Opencode.APIBase sel.proxy = cfg.Providers.Opencode.Proxy @@ -196,7 +196,11 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { sel.apiBase = cfg.Providers.Moonshot.APIBase sel.proxy = cfg.Providers.Moonshot.Proxy if sel.apiBase == "" { - sel.apiBase = "https://api.kimi.com/coding/v1" + if providerName == "moonshot" { + sel.apiBase = "https://api.moonshot.cn/v1" + } else { + sel.apiBase = "https://api.kimi.com/coding/v1" + } } } case "github_copilot", "copilot": @@ -219,7 +223,11 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { sel.apiBase = cfg.Providers.Moonshot.APIBase sel.proxy = cfg.Providers.Moonshot.Proxy if sel.apiBase == "" { - sel.apiBase = "https://api.kimi.com/coding/v1" + if strings.Contains(lowerModel, "moonshot") || strings.HasPrefix(model, "moonshot/") { + sel.apiBase = "https://api.moonshot.cn/v1" + } else { + sel.apiBase = "https://api.kimi.com/coding/v1" + } } case strings.HasPrefix(model, "openrouter/") || strings.HasPrefix(model, "anthropic/") || diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index e0c0eddef..eccb8cd40 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -112,6 +112,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) { {"vllm", "vllm"}, {"deepseek", "deepseek"}, {"ollama", "ollama"}, + {"opencode", "opencode"}, } for _, tt := range tests { diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 636a6ae97..98e69fd2a 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -177,7 +177,7 @@ func (p *Provider) Chat( req.Header.Set("Authorization", "Bearer "+p.apiKey) } // Kimi Code API requires a coding agent User-Agent - if strings.Contains(p.apiBase, "api.kimi.com") { + if parsedURL, parseErr := url.Parse(p.apiBase); parseErr == nil && parsedURL.Hostname() == "api.kimi.com" { req.Header.Set("User-Agent", "KimiCLI/0.77") }