From a9eb6e0c89e72b041a058901f23aeb2b73740438 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Wed, 18 Mar 2026 16:31:08 +0800 Subject: [PATCH] feat: complete Novita provider integration --- pkg/config/config.go | 8 +++- pkg/config/config_test.go | 16 ++++++++ pkg/providers/factory_provider.go | 8 ++-- pkg/providers/factory_provider_test.go | 29 ++++++++++++++ pkg/providers/openai_compat/provider.go | 41 ++++++++++++++++---- pkg/providers/openai_compat/provider_test.go | 4 +- 6 files changed, 93 insertions(+), 13 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 49fb3679f..79d0196b0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -531,6 +531,7 @@ type ProvidersConfig struct { Minimax ProviderConfig `json:"minimax"` LongCat ProviderConfig `json:"longcat"` ModelScope ProviderConfig `json:"modelscope"` + Novita ProviderConfig `json:"novita"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -559,7 +560,8 @@ func (p ProvidersConfig) IsEmpty() bool { p.Avian.APIKey == "" && p.Avian.APIBase == "" && p.Minimax.APIKey == "" && p.Minimax.APIBase == "" && p.LongCat.APIKey == "" && p.LongCat.APIBase == "" && - p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" + p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" && + p.Novita.APIKey == "" && p.Novita.APIBase == "" } // MarshalJSON implements custom JSON marshaling for ProvidersConfig @@ -589,7 +591,9 @@ type OpenAIProviderConfig struct { // ModelConfig represents a model-centric provider configuration. // It allows adding new providers (especially OpenAI-compatible ones) via configuration only. // The model field uses protocol prefix format: [protocol/]model-identifier -// Supported protocols: openai, anthropic, antigravity, claude-cli, codex-cli, github-copilot +// Supported protocols include openai, anthropic, antigravity, claude-cli, +// codex-cli, github-copilot, and named OpenAI-compatible protocols such as +// groq, deepseek, modelscope, and novita. // Default protocol is "openai" if no prefix is specified. type ModelConfig struct { // Required fields diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 82a845471..588c04645 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -77,6 +77,22 @@ func TestAgentModelConfig_MarshalObject(t *testing.T) { } } +func TestProvidersConfig_IsEmpty(t *testing.T) { + var empty ProvidersConfig + if !empty.IsEmpty() { + t.Fatal("empty ProvidersConfig should report empty") + } + + novita := ProvidersConfig{ + Novita: ProviderConfig{ + APIKey: "test-key", + }, + } + if novita.IsEmpty() { + t.Fatal("ProvidersConfig with novita settings should not report empty") + } +} + func TestAgentConfig_FullParse(t *testing.T) { jsonData := `{ "agents": { diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index b7567f9fc..dbb5db5cb 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -55,8 +55,8 @@ func ExtractProtocol(model string) (protocol, modelID string) { // CreateProviderFromConfig creates a provider based on the ModelConfig. // It uses the protocol prefix in the Model field to determine which provider to create. -// Supported protocols: openai, litellm, anthropic, anthropic-messages, antigravity, -// claude-cli, codex-cli, github-copilot +// Supported protocols: openai, litellm, novita, anthropic, anthropic-messages, +// antigravity, claude-cli, codex-cli, github-copilot // Returns the provider, the model ID (without protocol prefix), and any error. func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) { if cfg == nil { @@ -116,7 +116,7 @@ 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": + "minimax", "longcat", "modelscope", "novita": // 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) @@ -219,6 +219,8 @@ func getDefaultAPIBase(protocol string) string { return "https://openrouter.ai/api/v1" case "litellm": return "http://localhost:4000/v1" + case "novita": + return "https://api.novita.ai/openai" case "groq": return "https://api.groq.com/openai/v1" case "zhipu": diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index b678a7eb6..c7629ad9d 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) { }{ {"openai", "openai"}, {"groq", "groq"}, + {"novita", "novita"}, {"openrouter", "openrouter"}, {"cerebras", "cerebras"}, {"vivgrid", "vivgrid"}, @@ -222,6 +223,34 @@ func TestGetDefaultAPIBase_ModelScope(t *testing.T) { } } +func TestCreateProviderFromConfig_Novita(t *testing.T) { + cfg := &config.ModelConfig{ + ModelName: "test-novita", + Model: "novita/deepseek/deepseek-v3.2", + 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 != "deepseek/deepseek-v3.2" { + t.Errorf("modelID = %q, want %q", modelID, "deepseek/deepseek-v3.2") + } + if _, ok := provider.(*HTTPProvider); !ok { + t.Fatalf("expected *HTTPProvider, got %T", provider) + } +} + +func TestGetDefaultAPIBase_Novita(t *testing.T) { + if got := getDefaultAPIBase("novita"); got != "https://api.novita.ai/openai" { + t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "novita", got, "https://api.novita.ai/openai") + } +} + func TestCreateProviderFromConfig_Anthropic(t *testing.T) { cfg := &config.ModelConfig{ ModelName: "test-anthropic", diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 23f64d880..4c3fa8812 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -189,13 +189,40 @@ func normalizeModel(model, apiBase string) string { } prefix := strings.ToLower(before) - switch prefix { - case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", - "openrouter", "zhipu", "mistral", "vivgrid", "minimax", "novita": - return after - default: - return model - } + switch prefix { + case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", + "openrouter", "zhipu", "mistral", "vivgrid", "minimax", "novita": + return after + default: + return model + } +} + +func buildToolsList(tools []ToolDefinition, nativeSearch bool) []any { + result := make([]any, 0, len(tools)+1) + for _, t := range tools { + if nativeSearch && strings.EqualFold(t.Function.Name, "web_search") { + continue + } + result = append(result, t) + } + if nativeSearch { + result = append(result, map[string]any{"type": "web_search_preview"}) + } + return result +} + +func (p *Provider) SupportsNativeSearch() bool { + return isNativeSearchHost(p.apiBase) +} + +func isNativeSearchHost(apiBase string) bool { + u, err := url.Parse(apiBase) + if err != nil { + return false + } + host := u.Hostname() + return host == "api.openai.com" || strings.HasSuffix(host, ".openai.azure.com") } func buildToolsList(tools []ToolDefinition, nativeSearch bool) []any { diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 31bf885b8..3db43e6aa 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -454,7 +454,6 @@ func TestProviderChat_StripsGroqOllamaDeepseekVivgridNovitaPrefixes(t *testing.T defer server.Close() p := NewProvider("key", server.URL, "") - tests := []struct { name string input string @@ -589,6 +588,9 @@ func TestNormalizeModel_UsesAPIBase(t *testing.T) { if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" { t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", got, "auto") } + if got := normalizeModel("novita/deepseek/deepseek-v3.2", "https://api.novita.ai/openai"); got != "deepseek/deepseek-v3.2" { + t.Fatalf("normalizeModel(novita) = %q, want %q", got, "deepseek/deepseek-v3.2") + } } func TestProvider_RequestTimeoutDefault(t *testing.T) {