diff --git a/crates/config/src/template.rs b/crates/config/src/template.rs index fb42d1a51..f3afc92f4 100644 --- a/crates/config/src/template.rs +++ b/crates/config/src/template.rs @@ -96,7 +96,7 @@ offered = ["local-llm", "github-copilot", "openai-codex", "openai", "anthropic", # All available providers: # "anthropic", "openai", "gemini", "groq", "xai", "deepseek", # "mistral", "openrouter", "cerebras", "minimax", "moonshot", -# "zai", "venice", "ollama", "local-llm", "openai-codex", +# "novita", "zai", "venice", "ollama", "local-llm", "openai-codex", # "github-copilot", "kimi-code" # ── Anthropic (Claude) ──────────────────────────────────────── @@ -164,6 +164,14 @@ models = ["kimi-k2.5"] # Preferred models shown first # base_url = "https://api.moonshot.ai/v1" # alias = "moonshot" +# ── Novita AI ───────────────────────────────────────────────── +# [providers.novita] +# enabled = true +# api_key = "..." # Or set NOVITA_API_KEY env var +# models = ["moonshotai/kimi-k2.5", "deepseek/deepseek-v3.2", "zai-org/glm-5"] +# base_url = "https://api.novita.ai/openai" +# alias = "novita" + [providers.ollama] # base_url = "http://localhost:11434" # models = ["llama3.2", "qwen2.5:7b"] # Optional preferred models; installed models are discovered dynamically diff --git a/crates/provider-setup/src/lib.rs b/crates/provider-setup/src/lib.rs index 5db6bb29d..223b7c11e 100644 --- a/crates/provider-setup/src/lib.rs +++ b/crates/provider-setup/src/lib.rs @@ -844,6 +844,15 @@ pub fn known_providers() -> Vec { requires_model: false, key_optional: false, }, + KnownProvider { + name: "novita", + display_name: "Novita AI", + auth_type: AuthType::ApiKey, + env_key: Some("NOVITA_API_KEY"), + default_base_url: Some("https://api.novita.ai/openai"), + requires_model: false, + key_optional: false, + }, KnownProvider { name: "venice", display_name: "Venice", @@ -3964,6 +3973,7 @@ mod tests { assert!(names.contains(&"moonshot"), "missing moonshot"); assert!(names.contains(&"zai"), "missing zai"); assert!(names.contains(&"kimi-code"), "missing kimi-code"); + assert!(names.contains(&"novita"), "missing novita"); assert!(names.contains(&"venice"), "missing venice"); assert!(names.contains(&"ollama"), "missing ollama"); // OAuth providers @@ -3991,6 +4001,7 @@ mod tests { ("moonshot", "MOONSHOT_API_KEY"), ("zai", "Z_API_KEY"), ("kimi-code", "KIMI_API_KEY"), + ("novita", "NOVITA_API_KEY"), ("venice", "VENICE_API_KEY"), ("ollama", "OLLAMA_API_KEY"), ]; @@ -4022,6 +4033,7 @@ mod tests { "moonshot", "zai", "kimi-code", + "novita", "venice", "ollama", ] { @@ -4059,6 +4071,7 @@ mod tests { "moonshot", "zai", "kimi-code", + "novita", "venice", "ollama", "github-copilot", diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index 362a6639b..d27441d36 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -886,6 +886,14 @@ const DEEPSEEK_MODELS: &[(&str, &str)] = &[ /// Known Moonshot models. const MOONSHOT_MODELS: &[(&str, &str)] = &[("kimi-k2.5", "Kimi K2.5")]; +/// Known Novita AI models. +/// See: +const NOVITA_MODELS: &[(&str, &str)] = &[ + ("moonshotai/kimi-k2.5", "Kimi K2.5 (Novita)"), + ("deepseek/deepseek-v3.2", "DeepSeek V3.2 (Novita)"), + ("zai-org/glm-5", "GLM-5 (Novita)"), +]; + /// Known Google Gemini models. /// See: const GEMINI_MODELS: &[(&str, &str)] = &[ @@ -1030,6 +1038,16 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[ requires_api_key: true, local_only: false, }, + OpenAiCompatDef { + config_name: "novita", + env_key: "NOVITA_API_KEY", + env_base_url_key: "NOVITA_BASE_URL", + default_base_url: "https://api.novita.ai/openai", + models: NOVITA_MODELS, + supports_model_discovery: true, + requires_api_key: true, + local_only: false, + }, ]; #[cfg(any(feature = "provider-openai-codex", feature = "provider-github-copilot"))] @@ -2789,6 +2807,7 @@ mod tests { assert!(!ZAI_MODELS.is_empty()); assert!(!MOONSHOT_MODELS.is_empty()); assert!(!GEMINI_MODELS.is_empty()); + assert!(!NOVITA_MODELS.is_empty()); } #[test] @@ -2811,6 +2830,7 @@ mod tests { ZAI_MODELS, MOONSHOT_MODELS, GEMINI_MODELS, + NOVITA_MODELS, ] { let mut ids: Vec<&str> = models.iter().map(|(id, _)| *id).collect(); ids.sort(); @@ -4046,4 +4066,37 @@ mod tests { assert!(!supports_reasoning_for_model("gpt-5.2")); assert!(!supports_reasoning_for_model("claude-3-haiku-20240307")); } + + #[test] + fn novita_provider_is_registered() { + let def = OPENAI_COMPAT_PROVIDERS + .iter() + .find(|d| d.config_name == "novita") + .expect("novita not in OPENAI_COMPAT_PROVIDERS"); + assert_eq!(def.env_key, "NOVITA_API_KEY"); + assert_eq!(def.default_base_url, "https://api.novita.ai/openai"); + assert!(def.requires_api_key); + assert!(!def.local_only); + } + + #[test] + fn novita_model_ids_are_chat_capable() { + for (model_id, _) in NOVITA_MODELS { + assert!( + is_chat_capable_model(model_id), + "novita model {model_id} should be chat capable" + ); + } + } + + #[test] + fn novita_context_windows() { + // moonshotai/kimi-k2.5 — capability ID is "kimi-k2.5" → 128k + assert_eq!( + context_window_for_model("moonshotai/kimi-k2.5"), + 128_000 + ); + // zai-org/glm-5 — capability ID is "glm-5" → 128k + assert_eq!(context_window_for_model("zai-org/glm-5"), 128_000); + } }