Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion crates/config/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) ────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions crates/provider-setup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,15 @@ pub fn known_providers() -> Vec<KnownProvider> {
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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
];
Expand Down Expand Up @@ -4022,6 +4033,7 @@ mod tests {
"moonshot",
"zai",
"kimi-code",
"novita",
"venice",
"ollama",
] {
Expand Down Expand Up @@ -4059,6 +4071,7 @@ mod tests {
"moonshot",
"zai",
"kimi-code",
"novita",
"venice",
"ollama",
"github-copilot",
Expand Down
53 changes: 53 additions & 0 deletions crates/providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://novita.ai/docs/llm-api>
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: <https://ai.google.dev/gemini-api/docs/models>
const GEMINI_MODELS: &[(&str, &str)] = &[
Expand Down Expand Up @@ -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,
},
Comment on lines +1041 to +1050
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Provider ordering inconsistent with known_providers()

In provider-setup/src/lib.rs, the new KnownProvider entry is inserted between kimi-code and venice (matching the alphabetical-ish ordering of the surrounding list). Here in OPENAI_COMPAT_PROVIDERS the same provider is appended at the very end, after gemini.

Keeping the two lists in the same relative order makes it easier to cross-reference them and reduces the risk of accidentally missing a provider when adding the next one. Consider moving this entry to follow zai / moonshot, where the other same-region providers live:

// after the existing `zai` entry and before `venice`
OpenAiCompatDef {
    config_name: "novita",
    ...
},

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

];

#[cfg(any(feature = "provider-openai-codex", feature = "provider-github-copilot"))]
Expand Down Expand Up @@ -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]
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Comment on lines +4092 to +4101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing context-window assertion for deepseek/deepseek-v3.2

The novita_context_windows test deliberately covers two of the three Novita models (with explanatory comments about their capability IDs), but silently omits deepseek/deepseek-v3.2.

capability_model_id("deepseek/deepseek-v3.2") strips the org-prefix to "deepseek-v3.2", which does not match any of the rules in context_window_for_model — so the function falls through to the 200 k default. DeepSeek V3 is typically deployed with a 64 k context window on most platforms; reporting 200 k to the UI means users can construct prompts that will be rejected by the actual API.

Either add a deepseek- prefix rule to context_window_for_model (consistent with how kimi-, glm-, etc. are handled) or add a test assertion that explicitly documents the intended fallback value, so the behaviour is visible and deliberate:

Suggested change
#[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);
}
#[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);
// deepseek/deepseek-v3.2 — capability ID is "deepseek-v3.2" → falls
// back to 200k default; update if a tighter window is confirmed.
assert_eq!(context_window_for_model("deepseek/deepseek-v3.2"), 200_000);
}

}