From bd1411028b32b9b0351393e09cee7c0f3b5e1c78 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 29 Mar 2026 11:15:14 +0800 Subject: [PATCH] feat(providers): add Novita AI as new LLM provider (#444) Add Novita AI as a new LLM provider with OpenAI-compatible API endpoint. - Add ProviderNovita constant and valid type entry - Add Novita to ProvidersConfig with APIBaseForType and HasAnyProvider - Register Novita provider in gateway startup (config and DB paths) - Add Novita case in HTTP handler's registerInMemory - Add Novita to UI provider selection dropdown Default model: moonshotai/kimi-k2.5 Default endpoint: https://api.novita.ai/openai --- cmd/gateway_providers.go | 16 ++++++++++++++++ internal/config/config_channels.go | 6 +++++- internal/http/providers.go | 6 ++++++ internal/store/provider_store.go | 2 ++ ui/web/src/constants/providers.ts | 1 + 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/cmd/gateway_providers.go b/cmd/gateway_providers.go index b2740239..95715266 100644 --- a/cmd/gateway_providers.go +++ b/cmd/gateway_providers.go @@ -136,6 +136,16 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) { slog.Info("registered provider", "name", "ollama-cloud") } + // Novita AI — OpenAI-compatible endpoint. + if cfg.Providers.Novita.APIKey != "" { + base := cfg.Providers.Novita.APIBase + if base == "" { + base = "https://api.novita.ai/openai" + } + registry.Register(providers.NewOpenAIProvider("novita", cfg.Providers.Novita.APIKey, base, "moonshotai/kimi-k2.5")) + slog.Info("registered provider", "name", "novita") + } + // Claude CLI provider (subscription-based, no API key needed) if cfg.Providers.ClaudeCLI.CLIPath != "" { cliPath := cfg.Providers.ClaudeCLI.CLIPath @@ -341,6 +351,12 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, "") prov.WithProviderType(p.ProviderType) registry.RegisterForTenant(p.TenantID, prov) + case store.ProviderNovita: + base := p.APIBase + if base == "" { + base = "https://api.novita.ai/openai" + } + registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "moonshotai/kimi-k2.5")) default: prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "") prov.WithProviderType(p.ProviderType) diff --git a/internal/config/config_channels.go b/internal/config/config_channels.go index b29112f9..db310a85 100644 --- a/internal/config/config_channels.go +++ b/internal/config/config_channels.go @@ -213,6 +213,7 @@ type ProvidersConfig struct { OllamaCloud ProviderConfig `json:"ollama_cloud"` // Ollama Cloud (API key required) ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` ACP ACPConfig `json:"acp"` + Novita ProviderConfig `json:"novita"` // Novita AI (OpenAI-compatible endpoint) } // OllamaConfig configures a local (or self-hosted) Ollama instance. @@ -281,6 +282,8 @@ func (p *ProvidersConfig) APIBaseForType(providerType string) string { return p.ZaiCoding.APIBase case "ollama_cloud": return p.OllamaCloud.APIBase + case "novita": + return p.Novita.APIBase default: return "" } @@ -307,7 +310,8 @@ func (c *Config) HasAnyProvider() bool { p.Ollama.Host != "" || p.OllamaCloud.APIKey != "" || p.ClaudeCLI.CLIPath != "" || - p.ACP.Binary != "" + p.ACP.Binary != "" || + p.Novita.APIKey != "" } // QuotaWindow defines request limits per time window. Zero means unlimited. diff --git a/internal/http/providers.go b/internal/http/providers.go index f329f985..21d41c55 100644 --- a/internal/http/providers.go +++ b/internal/http/providers.go @@ -176,6 +176,12 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) { base = "https://coding-intl.dashscope.aliyuncs.com/v1" } h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "qwen3.5-plus")) + case store.ProviderNovita: + base := apiBase + if base == "" { + base = "https://api.novita.ai/openai" + } + h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "moonshotai/kimi-k2.5")) default: prov := providers.NewOpenAIProvider(p.Name, p.APIKey, apiBase, "") if p.ProviderType == store.ProviderMiniMax { diff --git a/internal/store/provider_store.go b/internal/store/provider_store.go index 053b9b55..ba4f2e95 100644 --- a/internal/store/provider_store.go +++ b/internal/store/provider_store.go @@ -31,6 +31,7 @@ const ( ProviderOllama = "ollama" // local or self-hosted Ollama (no API key) ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required) ProviderACP = "acp" // ACP (Agent Client Protocol) agent subprocess + ProviderNovita = "novita" // Novita AI (OpenAI-compatible endpoint) ) // ValidProviderTypes lists all accepted provider_type values. @@ -57,6 +58,7 @@ var ValidProviderTypes = map[string]bool{ ProviderOllama: true, ProviderOllamaCloud: true, ProviderACP: true, + ProviderNovita: true, } // LLMProviderData represents an LLM provider configuration. diff --git a/ui/web/src/constants/providers.ts b/ui/web/src/constants/providers.ts index 40fcdb2c..b751dcda 100644 --- a/ui/web/src/constants/providers.ts +++ b/ui/web/src/constants/providers.ts @@ -22,6 +22,7 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [ { value: "mistral", label: "Mistral AI", apiBase: "https://api.mistral.ai/v1", placeholder: "" }, { value: "xai", label: "xAI (Grok)", apiBase: "https://api.x.ai/v1", placeholder: "" }, { value: "minimax_native", label: "MiniMax (Native)", apiBase: "https://api.minimax.io/v1", placeholder: "" }, + { value: "novita", label: "Novita AI", apiBase: "https://api.novita.ai/openai", placeholder: "" }, { value: "cohere", label: "Cohere", apiBase: "https://api.cohere.ai/compatibility/v1", placeholder: "" }, { value: "perplexity", label: "Perplexity", apiBase: "https://api.perplexity.ai", placeholder: "" }, { value: "dashscope", label: "DashScope (Qwen)", apiBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", placeholder: "" },