feat: complete Novita provider integration
This commit is contained in:
parent
50c87dd5b7
commit
a9eb6e0c89
6 changed files with 93 additions and 13 deletions
|
|
@ -531,6 +531,7 @@ type ProvidersConfig struct {
|
||||||
Minimax ProviderConfig `json:"minimax"`
|
Minimax ProviderConfig `json:"minimax"`
|
||||||
LongCat ProviderConfig `json:"longcat"`
|
LongCat ProviderConfig `json:"longcat"`
|
||||||
ModelScope ProviderConfig `json:"modelscope"`
|
ModelScope ProviderConfig `json:"modelscope"`
|
||||||
|
Novita ProviderConfig `json:"novita"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEmpty checks if all provider configs are empty (no API keys or API bases set)
|
// 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.Avian.APIKey == "" && p.Avian.APIBase == "" &&
|
||||||
p.Minimax.APIKey == "" && p.Minimax.APIBase == "" &&
|
p.Minimax.APIKey == "" && p.Minimax.APIBase == "" &&
|
||||||
p.LongCat.APIKey == "" && p.LongCat.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
|
// MarshalJSON implements custom JSON marshaling for ProvidersConfig
|
||||||
|
|
@ -589,7 +591,9 @@ type OpenAIProviderConfig struct {
|
||||||
// ModelConfig represents a model-centric provider configuration.
|
// ModelConfig represents a model-centric provider configuration.
|
||||||
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
|
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
|
||||||
// The model field uses protocol prefix format: [protocol/]model-identifier
|
// 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.
|
// Default protocol is "openai" if no prefix is specified.
|
||||||
type ModelConfig struct {
|
type ModelConfig struct {
|
||||||
// Required fields
|
// Required fields
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestAgentConfig_FullParse(t *testing.T) {
|
||||||
jsonData := `{
|
jsonData := `{
|
||||||
"agents": {
|
"agents": {
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ func ExtractProtocol(model string) (protocol, modelID string) {
|
||||||
|
|
||||||
// CreateProviderFromConfig creates a provider based on the ModelConfig.
|
// CreateProviderFromConfig creates a provider based on the ModelConfig.
|
||||||
// It uses the protocol prefix in the Model field to determine which provider to create.
|
// It uses the protocol prefix in the Model field to determine which provider to create.
|
||||||
// Supported protocols: openai, litellm, anthropic, anthropic-messages, antigravity,
|
// Supported protocols: openai, litellm, novita, anthropic, anthropic-messages,
|
||||||
// claude-cli, codex-cli, github-copilot
|
// antigravity, claude-cli, codex-cli, github-copilot
|
||||||
// Returns the provider, the model ID (without protocol prefix), and any error.
|
// Returns the provider, the model ID (without protocol prefix), and any error.
|
||||||
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
|
func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
|
@ -116,7 +116,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
||||||
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
||||||
"vivgrid", "volcengine", "vllm", "qwen", "mistral", "avian",
|
"vivgrid", "volcengine", "vllm", "qwen", "mistral", "avian",
|
||||||
"minimax", "longcat", "modelscope":
|
"minimax", "longcat", "modelscope", "novita":
|
||||||
// All other OpenAI-compatible HTTP providers
|
// All other OpenAI-compatible HTTP providers
|
||||||
if cfg.APIKey == "" && cfg.APIBase == "" {
|
if cfg.APIKey == "" && cfg.APIBase == "" {
|
||||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
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"
|
return "https://openrouter.ai/api/v1"
|
||||||
case "litellm":
|
case "litellm":
|
||||||
return "http://localhost:4000/v1"
|
return "http://localhost:4000/v1"
|
||||||
|
case "novita":
|
||||||
|
return "https://api.novita.ai/openai"
|
||||||
case "groq":
|
case "groq":
|
||||||
return "https://api.groq.com/openai/v1"
|
return "https://api.groq.com/openai/v1"
|
||||||
case "zhipu":
|
case "zhipu":
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"openai", "openai"},
|
{"openai", "openai"},
|
||||||
{"groq", "groq"},
|
{"groq", "groq"},
|
||||||
|
{"novita", "novita"},
|
||||||
{"openrouter", "openrouter"},
|
{"openrouter", "openrouter"},
|
||||||
{"cerebras", "cerebras"},
|
{"cerebras", "cerebras"},
|
||||||
{"vivgrid", "vivgrid"},
|
{"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) {
|
func TestCreateProviderFromConfig_Anthropic(t *testing.T) {
|
||||||
cfg := &config.ModelConfig{
|
cfg := &config.ModelConfig{
|
||||||
ModelName: "test-anthropic",
|
ModelName: "test-anthropic",
|
||||||
|
|
|
||||||
|
|
@ -189,13 +189,40 @@ func normalizeModel(model, apiBase string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := strings.ToLower(before)
|
prefix := strings.ToLower(before)
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google",
|
case "litellm", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google",
|
||||||
"openrouter", "zhipu", "mistral", "vivgrid", "minimax", "novita":
|
"openrouter", "zhipu", "mistral", "vivgrid", "minimax", "novita":
|
||||||
return after
|
return after
|
||||||
default:
|
default:
|
||||||
return model
|
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 {
|
func buildToolsList(tools []ToolDefinition, nativeSearch bool) []any {
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,6 @@ func TestProviderChat_StripsGroqOllamaDeepseekVivgridNovitaPrefixes(t *testing.T
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
p := NewProvider("key", server.URL, "")
|
p := NewProvider("key", server.URL, "")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input 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" {
|
if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" {
|
||||||
t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", 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) {
|
func TestProvider_RequestTimeoutDefault(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue