diff --git a/README.md b/README.md index db127a85f..c9cc28f58 100644 --- a/README.md +++ b/README.md @@ -952,6 +952,7 @@ The subagent has access to tools (message, web_search, etc.) and can communicate | `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | | `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) | | `cerebras` | LLM (Cerebras direct) | [cerebras.ai](https://cerebras.ai) | +| `vivgrid` | LLM (Vivgrid direct) | [vivgrid.com](https://vivgrid.com) | ### Model Configuration (model_list) @@ -979,11 +980,12 @@ This design also enables **multi-agent support** with flexible provider selectio | **NVIDIA** | `nvidia/` | `https://integrate.api.nvidia.com/v1` | OpenAI | [Get Key](https://build.nvidia.com) | | **Ollama** | `ollama/` | `http://localhost:11434/v1` | OpenAI | Local (no key needed) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Get Key](https://openrouter.ai/keys) | -| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1 | OpenAI | Your LiteLLM proxy key | +| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) | | **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) | | **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | +| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) | | **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | diff --git a/pkg/config/config.go b/pkg/config/config.go index 5c53c08ad..fcbfc8e78 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -459,6 +459,7 @@ type ProvidersConfig struct { ShengSuanYun ProviderConfig `json:"shengsuanyun"` DeepSeek ProviderConfig `json:"deepseek"` Cerebras ProviderConfig `json:"cerebras"` + Vivgrid ProviderConfig `json:"vivgrid"` VolcEngine ProviderConfig `json:"volcengine"` GitHubCopilot ProviderConfig `json:"github_copilot"` Antigravity ProviderConfig `json:"antigravity"` @@ -484,6 +485,7 @@ func (p ProvidersConfig) IsEmpty() bool { p.ShengSuanYun.APIKey == "" && p.ShengSuanYun.APIBase == "" && p.DeepSeek.APIKey == "" && p.DeepSeek.APIBase == "" && p.Cerebras.APIKey == "" && p.Cerebras.APIBase == "" && + p.Vivgrid.APIKey == "" && p.Vivgrid.APIBase == "" && p.VolcEngine.APIKey == "" && p.VolcEngine.APIBase == "" && p.GitHubCopilot.APIKey == "" && p.GitHubCopilot.APIBase == "" && p.Antigravity.APIKey == "" && p.Antigravity.APIBase == "" && diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 1902480c5..4eef6a79e 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -261,6 +261,14 @@ func DefaultConfig() *Config { APIKey: "", }, + // Vivgrid - https://vivgrid.com + { + ModelName: "vivgrid-auto", + Model: "vivgrid/auto", + APIBase: "https://api.vivgrid.com/v1", + APIKey: "", + }, + // Volcengine (火山引擎) - https://console.volcengine.com/ark { ModelName: "doubao-pro", diff --git a/pkg/config/migration.go b/pkg/config/migration.go index 4a17dd6c9..51f21e4f4 100644 --- a/pkg/config/migration.go +++ b/pkg/config/migration.go @@ -292,6 +292,23 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig { }, true }, }, + { + providerNames: []string{"vivgrid"}, + protocol: "vivgrid", + buildConfig: func(p ProvidersConfig) (ModelConfig, bool) { + if p.Vivgrid.APIKey == "" && p.Vivgrid.APIBase == "" { + return ModelConfig{}, false + } + return ModelConfig{ + ModelName: "vivgrid", + Model: "vivgrid/auto", + APIKey: p.Vivgrid.APIKey, + APIBase: p.Vivgrid.APIBase, + Proxy: p.Vivgrid.Proxy, + RequestTimeout: p.Vivgrid.RequestTimeout, + }, true + }, + }, { providerNames: []string{"volcengine", "doubao"}, protocol: "volcengine", diff --git a/pkg/config/migration_test.go b/pkg/config/migration_test.go index 67ad73db9..d3019aab0 100644 --- a/pkg/config/migration_test.go +++ b/pkg/config/migration_test.go @@ -155,7 +155,8 @@ func TestConvertProvidersToModelList_AllProviders(t *testing.T) { ShengSuanYun: ProviderConfig{APIKey: "key11"}, DeepSeek: ProviderConfig{APIKey: "key12"}, Cerebras: ProviderConfig{APIKey: "key13"}, - VolcEngine: ProviderConfig{APIKey: "key14"}, + Vivgrid: ProviderConfig{APIKey: "key14"}, + VolcEngine: ProviderConfig{APIKey: "key15"}, GitHubCopilot: ProviderConfig{ConnectMode: "grpc"}, Antigravity: ProviderConfig{AuthMethod: "oauth"}, Qwen: ProviderConfig{APIKey: "key17"}, @@ -166,9 +167,9 @@ func TestConvertProvidersToModelList_AllProviders(t *testing.T) { result := ConvertProvidersToModelList(cfg) - // All 20 providers should be converted - if len(result) != 20 { - t.Errorf("len(result) = %d, want 20", len(result)) + // All 21 providers should be converted + if len(result) != 21 { + t.Errorf("len(result) = %d, want 21", len(result)) } } diff --git a/pkg/providers/factory.go b/pkg/providers/factory.go index a0d09a835..25916ad03 100644 --- a/pkg/providers/factory.go +++ b/pkg/providers/factory.go @@ -153,6 +153,15 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { sel.apiBase = "https://integrate.api.nvidia.com/v1" } } + case "vivgrid": + if cfg.Providers.Vivgrid.APIKey != "" { + sel.apiKey = cfg.Providers.Vivgrid.APIKey + sel.apiBase = cfg.Providers.Vivgrid.APIBase + sel.proxy = cfg.Providers.Vivgrid.Proxy + if sel.apiBase == "" { + sel.apiBase = "https://api.vivgrid.com/v1" + } + } case "claude-cli", "claude-code", "claudecode": workspace := cfg.WorkspacePath() if workspace == "" { @@ -295,6 +304,13 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { if sel.apiBase == "" { sel.apiBase = "https://integrate.api.nvidia.com/v1" } + case strings.HasPrefix(model, "vivgrid/") && cfg.Providers.Vivgrid.APIKey != "": + sel.apiKey = cfg.Providers.Vivgrid.APIKey + sel.apiBase = cfg.Providers.Vivgrid.APIBase + sel.proxy = cfg.Providers.Vivgrid.Proxy + if sel.apiBase == "" { + sel.apiBase = "https://api.vivgrid.com/v1" + } case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && cfg.Providers.Ollama.APIKey != "": sel.apiKey = cfg.Providers.Ollama.APIKey sel.apiBase = cfg.Providers.Ollama.APIBase diff --git a/pkg/providers/factory_provider.go b/pkg/providers/factory_provider.go index c05fb0ad4..941985964 100644 --- a/pkg/providers/factory_provider.go +++ b/pkg/providers/factory_provider.go @@ -94,7 +94,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia", "ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", - "volcengine", "vllm", "qwen", "mistral", "avian": + "vivgrid", "volcengine", "vllm", "qwen", "mistral", "avian": // 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) @@ -200,6 +200,8 @@ func getDefaultAPIBase(protocol string) string { return "https://api.deepseek.com/v1" case "cerebras": return "https://api.cerebras.ai/v1" + case "vivgrid": + return "https://api.vivgrid.com/v1" case "volcengine": return "https://ark.cn-beijing.volces.com/api/v3" case "qwen": diff --git a/pkg/providers/factory_provider_test.go b/pkg/providers/factory_provider_test.go index 78389f331..17bc55d25 100644 --- a/pkg/providers/factory_provider_test.go +++ b/pkg/providers/factory_provider_test.go @@ -108,6 +108,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) { {"groq", "groq"}, {"openrouter", "openrouter"}, {"cerebras", "cerebras"}, + {"vivgrid", "vivgrid"}, {"qwen", "qwen"}, {"vllm", "vllm"}, {"deepseek", "deepseek"}, diff --git a/pkg/providers/factory_test.go b/pkg/providers/factory_test.go index f7a916d9e..36ccda4a1 100644 --- a/pkg/providers/factory_test.go +++ b/pkg/providers/factory_test.go @@ -88,6 +88,17 @@ func TestResolveProviderSelection(t *testing.T) { wantAPIBase: "https://integrate.api.nvidia.com/v1", wantProxy: "http://127.0.0.1:7890", }, + { + name: "explicit vivgrid provider uses defaults", + setup: func(cfg *config.Config) { + cfg.Agents.Defaults.Provider = "vivgrid" + cfg.Providers.Vivgrid.APIKey = "vivgrid-key" + cfg.Providers.Vivgrid.Proxy = "http://127.0.0.1:7890" + }, + wantType: providerTypeHTTPCompat, + wantAPIBase: "https://api.vivgrid.com/v1", + wantProxy: "http://127.0.0.1:7890", + }, { name: "openrouter model uses openrouter defaults", setup: func(cfg *config.Config) { diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index caee6f9ce..5c868626a 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -439,7 +439,8 @@ func normalizeModel(model, apiBase string) string { prefix := strings.ToLower(before) switch prefix { - case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "mistral": + case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", + "openrouter", "zhipu", "mistral", "vivgrid": return after default: return model diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 5c4dcd1b0..9a3a7acc5 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -382,7 +382,7 @@ func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testin } } -func TestProviderChat_StripsGroqAndOllamaPrefixes(t *testing.T) { +func TestProviderChat_StripsGroqOllamaDeepseekVivgridPrefixes(t *testing.T) { tests := []struct { name string input string @@ -408,6 +408,11 @@ func TestProviderChat_StripsGroqAndOllamaPrefixes(t *testing.T) { input: "deepseek/deepseek-chat", wantModel: "deepseek-chat", }, + { + name: "strips vivgrid prefix", + input: "vivgrid/auto", + wantModel: "auto", + }, } for _, tt := range tests { @@ -512,6 +517,12 @@ func TestNormalizeModel_UsesAPIBase(t *testing.T) { if got := normalizeModel("openrouter/auto", "https://openrouter.ai/api/v1"); got != "openrouter/auto" { t.Fatalf("normalizeModel(openrouter) = %q, want %q", got, "openrouter/auto") } + if got := normalizeModel("vivgrid/managed", "https://api.vivgrid.com/v1"); got != "managed" { + t.Fatalf("normalizeModel(vivgrid) = %q, want %q", got, "managed") + } + if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" { + t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", got, "auto") + } } func TestProvider_RequestTimeoutDefault(t *testing.T) {