diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index dbb5db5cb2..a7fef8f5b1 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -115,8 +115,9 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia", "ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", - "vivgrid", "volcengine", "vllm", "qwen", "mistral", "avian", - "minimax", "longcat", "modelscope", "novita": + "vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl", + "qwen-us", "dashscope-us", "mistral", "avian", "minimax", "longcat", "modelscope", "novita", + "coding-plan", "alibaba-coding", "qwen-coding": // All other OpenAI-compatible HTTP providers if cfg.APIKey == "" && cfg.APIBase == "" { return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol) @@ -173,6 +174,21 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err cfg.RequestTimeout, ), modelID, nil + case "coding-plan-anthropic", "alibaba-coding-anthropic": + // Alibaba Coding Plan with Anthropic-compatible API + apiBase := cfg.APIBase + if apiBase == "" { + apiBase = getDefaultAPIBase(protocol) + } + if cfg.APIKey == "" { + return nil, "", fmt.Errorf("api_key is required for %q protocol (model: %s)", protocol, cfg.Model) + } + return anthropicmessages.NewProviderWithTimeout( + cfg.APIKey, + apiBase, + cfg.RequestTimeout, + ), modelID, nil + case "antigravity": return NewAntigravityProvider(), modelID, nil @@ -245,6 +261,14 @@ func getDefaultAPIBase(protocol string) string { return "https://ark.cn-beijing.volces.com/api/v3" case "qwen": return "https://dashscope.aliyuncs.com/compatible-mode/v1" + case "qwen-intl", "qwen-international", "dashscope-intl": + return "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" + case "qwen-us", "dashscope-us": + return "https://dashscope-us.aliyuncs.com/compatible-mode/v1" + case "coding-plan", "alibaba-coding", "qwen-coding": + return "https://coding-intl.dashscope.aliyuncs.com/v1" + case "coding-plan-anthropic", "alibaba-coding-anthropic": + return "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic" case "vllm": return "http://localhost:8000/v1" case "mistral": diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index c7629ad9df..8b9ddeecdd 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -472,3 +472,134 @@ func TestCreateProviderFromConfig_AzureMissingAPIBase(t *testing.T) { t.Fatal("CreateProviderFromConfig() expected error for missing API base") } } + +func TestCreateProviderFromConfig_QwenInternationalAlias(t *testing.T) { + tests := []struct { + name string + protocol string + }{ + {"qwen-international", "qwen-international"}, + {"dashscope-intl", "dashscope-intl"}, + {"qwen-intl", "qwen-intl"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.ModelConfig{ + ModelName: "test-" + tt.protocol, + Model: tt.protocol + "/qwen-max", + APIKey: "test-key", + } + + provider, modelID, err := CreateProviderFromConfig(cfg) + if err != nil { + t.Fatalf("CreateProviderFromConfig() error = %v", err) + } + if provider == nil { + t.Fatal("CreateProviderFromConfig() returned nil provider") + } + if modelID != "qwen-max" { + t.Errorf("modelID = %q, want %q", modelID, "qwen-max") + } + if _, ok := provider.(*HTTPProvider); !ok { + t.Fatalf("expected *HTTPProvider, got %T", provider) + } + }) + } +} + +func TestCreateProviderFromConfig_QwenUSAlias(t *testing.T) { + tests := []struct { + name string + protocol string + }{ + {"qwen-us", "qwen-us"}, + {"dashscope-us", "dashscope-us"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.ModelConfig{ + ModelName: "test-" + tt.protocol, + Model: tt.protocol + "/qwen-max", + APIKey: "test-key", + } + + provider, modelID, err := CreateProviderFromConfig(cfg) + if err != nil { + t.Fatalf("CreateProviderFromConfig() error = %v", err) + } + if provider == nil { + t.Fatal("CreateProviderFromConfig() returned nil provider") + } + if modelID != "qwen-max" { + t.Errorf("modelID = %q, want %q", modelID, "qwen-max") + } + if _, ok := provider.(*HTTPProvider); !ok { + t.Fatalf("expected *HTTPProvider, got %T", provider) + } + }) + } +} + +func TestCreateProviderFromConfig_CodingPlanAnthropic(t *testing.T) { + tests := []struct { + name string + protocol string + }{ + {"coding-plan-anthropic", "coding-plan-anthropic"}, + {"alibaba-coding-anthropic", "alibaba-coding-anthropic"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.ModelConfig{ + ModelName: "test-" + tt.protocol, + Model: tt.protocol + "/claude-sonnet-4-20250514", + APIKey: "test-key", + } + + provider, modelID, err := CreateProviderFromConfig(cfg) + if err != nil { + t.Fatalf("CreateProviderFromConfig() error = %v", err) + } + if provider == nil { + t.Fatal("CreateProviderFromConfig() returned nil provider") + } + if modelID != "claude-sonnet-4-20250514" { + t.Errorf("modelID = %q, want %q", modelID, "claude-sonnet-4-20250514") + } + // coding-plan-anthropic uses Anthropic Messages provider + // Verify it's the anthropic messages provider by checking interface + var _ LLMProvider = provider + }) + } +} + +func TestGetDefaultAPIBase_CodingPlanAnthropic(t *testing.T) { + expectedURL := "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic" + if got := getDefaultAPIBase("coding-plan-anthropic"); got != expectedURL { + t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "coding-plan-anthropic", got, expectedURL) + } + if got := getDefaultAPIBase("alibaba-coding-anthropic"); got != expectedURL { + t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "alibaba-coding-anthropic", got, expectedURL) + } +} + +func TestGetDefaultAPIBase_QwenIntlAliases(t *testing.T) { + expectedURL := "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" + for _, protocol := range []string{"qwen-intl", "qwen-international", "dashscope-intl"} { + if got := getDefaultAPIBase(protocol); got != expectedURL { + t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", protocol, got, expectedURL) + } + } +} + +func TestGetDefaultAPIBase_QwenUSAliases(t *testing.T) { + expectedURL := "https://dashscope-us.aliyuncs.com/compatible-mode/v1" + for _, protocol := range []string{"qwen-us", "dashscope-us"} { + if got := getDefaultAPIBase(protocol); got != expectedURL { + t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", protocol, got, expectedURL) + } + } +} diff --git a/pkg/providers/model_ref.go b/pkg/providers/model_ref.go index 0d1b02d168..be9f63bc6b 100644 --- a/pkg/providers/model_ref.go +++ b/pkg/providers/model_ref.go @@ -53,6 +53,14 @@ func NormalizeProvider(provider string) string { return "zhipu" case "google": return "gemini" + case "alibaba-coding", "qwen-coding": + return "coding-plan" + case "alibaba-coding-anthropic": + return "coding-plan-anthropic" + case "qwen-international", "dashscope-intl": + return "qwen-intl" + case "dashscope-us": + return "qwen-us" } return p diff --git a/pkg/providers/model_ref_test.go b/pkg/providers/model_ref_test.go index 6dd25167f6..040c511bab 100644 --- a/pkg/providers/model_ref_test.go +++ b/pkg/providers/model_ref_test.go @@ -73,6 +73,14 @@ func TestNormalizeProvider(t *testing.T) { {"glm", "zhipu"}, {"google", "gemini"}, {"groq", "groq"}, + // Alibaba Coding Plan aliases + {"alibaba-coding", "coding-plan"}, + {"qwen-coding", "coding-plan"}, + {"alibaba-coding-anthropic", "coding-plan-anthropic"}, + // Qwen international aliases + {"qwen-international", "qwen-intl"}, + {"dashscope-intl", "qwen-intl"}, + {"dashscope-us", "qwen-us"}, {"", ""}, }