feat(provider): add Wanjie Ark support

This commit is contained in:
Hunter Bown
2026-05-21 00:02:02 +08:00
parent 938d681edb
commit 8597afc076
14 changed files with 574 additions and 31 deletions
+10 -6
View File
@@ -255,6 +255,10 @@ deepseek --provider nvidia-nim
deepseek auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"
deepseek --provider atlascloud
# Wanjie Ark
deepseek auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"
deepseek --provider wanjie-ark --model deepseek-reasoner
# OpenRouter
deepseek auth set --provider openrouter --api-key "YOUR_OPENROUTER_API_KEY"
deepseek --provider openrouter --model deepseek/deepseek-v4-pro
@@ -283,10 +287,9 @@ deepseek --provider ollama --model deepseek-coder:1.3b
```
Inside the TUI, `/provider` opens the provider picker and `/model` opens the
model picker. `/provider openrouter` and `/model <id>` switch directly, while
`/models` lists live API models. The `/model` picker uses the active provider's
live model catalog when the provider exposes one, with provider-aware defaults
as a fallback.
local model/thinking picker. `/provider openrouter` and `/model <id>` switch
directly, while `/models` explicitly fetches and lists live API models when the
active provider supports model listing.
---
@@ -411,13 +414,14 @@ Key environment variables:
| `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | Default model |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` |
| `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama` |
| `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama` |
| `DEEPSEEK_PROFILE` | Config profile name |
| `DEEPSEEK_MEMORY` | Set to `on` to enable user memory |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud endpoint and model override |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark endpoint and model override |
| `OPENROUTER_BASE_URL` | OpenRouter endpoint override |
| `NOVITA_BASE_URL` | Novita endpoint override |
| `FIREWORKS_BASE_URL` | Fireworks endpoint override |
+15 -6
View File
@@ -12,11 +12,12 @@
# Choose which provider to use by default. Per-provider credentials live in the
# `[providers.*]` sections near the bottom of
# this file — keeping both stored at once means `/provider deepseek` and
# `/provider nvidia-nim` (or `--provider openai`, `--provider fireworks`,
# `/provider sglang`, `/provider vllm`, `/provider ollama`) toggle without having to
# re-enter keys. Top-level `api_key` / `base_url` are still read as DeepSeek
# defaults when `[providers.deepseek]` is absent (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | openrouter | novita | fireworks | sglang | vllm | ollama
# `/provider nvidia-nim` (or `--provider openai`, `--provider wanjie-ark`,
# `--provider fireworks`, `/provider sglang`, `/provider vllm`, `/provider ollama`)
# toggle without having to re-enter keys. Top-level `api_key` / `base_url` are
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
# (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | openrouter | novita | fireworks | sglang | vllm | ollama
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
base_url = "https://api.deepseek.com/beta"
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
@@ -35,6 +36,7 @@ base_url = "https://api.deepseek.com/beta"
# deepseek-ai/deepseek-v4-flash — NVIDIA NIM-hosted Flash model ID
# gpt-4.1 — default generic OpenAI-compatible model ID
# deepseek-ai/deepseek-v4-flash — default AtlasCloud model ID
# deepseek-reasoner — default Wanjie Ark model ID
# accounts/fireworks/models/deepseek-v4-pro — Fireworks AI Pro model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
@@ -154,12 +156,13 @@ max_subagents = 10 # optional (1-20)
# ─────────────────────────────────────────────────────────────────────────────────
# Providers can be stored at once; `provider = "..."` (top of file) or
# `/provider deepseek` / `/provider nvidia-nim` / `--provider openai` /
# `/provider fireworks` switches between them without
# `--provider wanjie-ark` / `/provider fireworks` switches between them without
# having to re-enter keys. Env vars override anything set here:
# DeepSeek: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL
# NIM: NVIDIA_API_KEY (or NVIDIA_NIM_API_KEY), NIM_BASE_URL
# (or NVIDIA_NIM_BASE_URL / NVIDIA_BASE_URL), NVIDIA_NIM_MODEL
# OpenAI-compatible: OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
# Wanjie Ark: WANJIE_ARK_API_KEY (or WANJIE_API_KEY), WANJIE_ARK_BASE_URL, WANJIE_ARK_MODEL
# Fireworks: FIREWORKS_API_KEY, FIREWORKS_BASE_URL
# SGLang: SGLANG_BASE_URL, SGLANG_MODEL, optional SGLANG_API_KEY
# vLLM: VLLM_BASE_URL, VLLM_MODEL, optional VLLM_API_KEY
@@ -193,6 +196,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.atlascloud.ai/v1"
# model = "deepseek-ai/deepseek-v4-flash"
# Wanjie Ark / 万界方舟 OpenAI-compatible endpoint
[providers.wanjie_ark]
# api_key = "YOUR_WANJIE_API_KEY"
# base_url = "https://maas-openapi.wanjiedata.com/api/v1"
# model = "deepseek-reasoner" # or the exact model ID enabled on your Wanjie account
# Fireworks AI-hosted DeepSeek V4 (https://fireworks.ai)
[providers.fireworks]
# api_key = "YOUR_FIREWORKS_API_KEY"
+20
View File
@@ -87,6 +87,16 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "deepseek-reasoner".to_string(),
provider: ProviderKind::WanjieArk,
aliases: vec![
"wanjie-deepseek-reasoner".to_string(),
"ark-wanjie-deepseek-reasoner".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek/deepseek-v4-pro".to_string(),
provider: ProviderKind::Openrouter,
@@ -361,6 +371,16 @@ mod tests {
assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
}
#[test]
fn wanjie_ark_default_uses_reasoner_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
assert_eq!(resolved.resolved.id, "deepseek-reasoner");
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn novita_default_uses_namespaced_model_id() {
let registry = ModelRegistry::default();
+30 -2
View File
@@ -27,6 +27,7 @@ enum ProviderArg {
NvidiaNim,
Openai,
Atlascloud,
WanjieArk,
Openrouter,
Novita,
Fireworks,
@@ -42,6 +43,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::NvidiaNim => ProviderKind::NvidiaNim,
ProviderArg::Openai => ProviderKind::Openai,
ProviderArg::Atlascloud => ProviderKind::Atlascloud,
ProviderArg::WanjieArk => ProviderKind::WanjieArk,
ProviderArg::Openrouter => ProviderKind::Openrouter,
ProviderArg::Novita => ProviderKind::Novita,
ProviderArg::Fireworks => ProviderKind::Fireworks,
@@ -685,6 +687,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
ProviderKind::NvidiaNim => "nvidia-nim",
ProviderKind::Openai => "openai",
ProviderKind::Atlascloud => "atlascloud",
ProviderKind::WanjieArk => "wanjie-ark",
ProviderKind::Openrouter => "openrouter",
ProviderKind::Novita => "novita",
ProviderKind::Fireworks => "fireworks",
@@ -695,11 +698,12 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
}
/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 10] = [
const PROVIDER_LIST: [ProviderKind; 11] = [
ProviderKind::Deepseek,
ProviderKind::NvidiaNim,
ProviderKind::Openai,
ProviderKind::Atlascloud,
ProviderKind::WanjieArk,
ProviderKind::Openrouter,
ProviderKind::Novita,
ProviderKind::Fireworks,
@@ -762,6 +766,11 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
ProviderKind::Ollama => &["OLLAMA_API_KEY"],
ProviderKind::Openai => &["OPENAI_API_KEY"],
ProviderKind::Atlascloud => &["ATLASCLOUD_API_KEY"],
ProviderKind::WanjieArk => &[
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
],
}
}
@@ -1405,6 +1414,7 @@ fn build_tui_command(
| ProviderKind::NvidiaNim
| ProviderKind::Openai
| ProviderKind::Atlascloud
| ProviderKind::WanjieArk
| ProviderKind::Openrouter
| ProviderKind::Novita
| ProviderKind::Fireworks
@@ -1413,7 +1423,7 @@ fn build_tui_command(
| ProviderKind::Ollama
) {
bail!(
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, OpenRouter, Novita, Fireworks, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `deepseek model ...` for provider registry inspection.",
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, Wanjie Ark, OpenRouter, Novita, Fireworks, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `deepseek model ...` for provider registry inspection.",
resolved_runtime.provider.as_str()
);
}
@@ -1438,6 +1448,9 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::WanjieArk {
cmd.env("WANJIE_ARK_API_KEY", api_key);
}
let source = resolved_runtime
.api_key_source
.unwrap_or(RuntimeApiKeySource::Env)
@@ -1474,6 +1487,9 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::WanjieArk {
cmd.env("WANJIE_ARK_API_KEY", api_key);
}
cmd.env("DEEPSEEK_API_KEY_SOURCE", "cli");
}
if let Some(base_url) = cli.base_url.as_ref() {
@@ -2062,6 +2078,18 @@ mod tests {
}))
));
let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "wanjie-ark"]);
assert!(matches!(
cli.command,
Some(Commands::Auth(AuthArgs {
command: AuthCommand::Set {
provider: ProviderArg::WanjieArk,
api_key: None,
api_key_stdin: false,
}
}))
));
let cli = parse_ok(&["deepseek", "auth", "get", "--provider", "sglang"]);
assert!(matches!(
cli.command,
+164 -1
View File
@@ -23,6 +23,8 @@ const DEFAULT_NVIDIA_NIM_BASE_URL: &str = "https://integrate.api.nvidia.com/v1";
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
const DEFAULT_ATLASCLOUD_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
const DEFAULT_ATLASCLOUD_BASE_URL: &str = "https://api.atlascloud.ai/v1";
const DEFAULT_WANJIE_ARK_MODEL: &str = "deepseek-reasoner";
const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.com/api/v1";
const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_NOVITA_MODEL: &str = "deepseek/deepseek-v4-pro";
@@ -54,6 +56,15 @@ pub enum ProviderKind {
NvidiaNim,
Openai,
Atlascloud,
#[serde(
alias = "wanjie",
alias = "wanjie_ark",
alias = "ark-wanjie",
alias = "ark_wanjie",
alias = "wanjie-maas",
alias = "wanjie_maas"
)]
WanjieArk,
Openrouter,
Novita,
Fireworks,
@@ -70,6 +81,7 @@ impl ProviderKind {
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -87,6 +99,8 @@ impl ProviderKind {
"nvidia" | "nvidia-nim" | "nvidia_nim" | "nim" => Some(Self::NvidiaNim),
"openai" | "open-ai" => Some(Self::Openai),
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -118,6 +132,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub atlascloud: ProviderConfigToml,
#[serde(default)]
pub wanjie_ark: ProviderConfigToml,
#[serde(default)]
pub openrouter: ProviderConfigToml,
#[serde(default)]
pub novita: ProviderConfigToml,
@@ -139,6 +155,7 @@ impl ProvidersToml {
ProviderKind::NvidiaNim => &self.nvidia_nim,
ProviderKind::Openai => &self.openai,
ProviderKind::Atlascloud => &self.atlascloud,
ProviderKind::WanjieArk => &self.wanjie_ark,
ProviderKind::Openrouter => &self.openrouter,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
@@ -154,6 +171,7 @@ impl ProvidersToml {
ProviderKind::NvidiaNim => &mut self.nvidia_nim,
ProviderKind::Openai => &mut self.openai,
ProviderKind::Atlascloud => &mut self.atlascloud,
ProviderKind::WanjieArk => &mut self.wanjie_ark,
ProviderKind::Openrouter => &mut self.openrouter,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
@@ -369,6 +387,10 @@ impl ConfigToml {
&mut self.providers.atlascloud,
&project.providers.atlascloud,
);
merge_provider_config(
&mut self.providers.wanjie_ark,
&project.providers.wanjie_ark,
);
merge_provider_config(
&mut self.providers.openrouter,
&project.providers.openrouter,
@@ -437,6 +459,12 @@ impl ConfigToml {
"providers.atlascloud.http_headers" => {
serialize_http_headers(&self.providers.atlascloud.http_headers)
}
"providers.wanjie_ark.api_key" => self.providers.wanjie_ark.api_key.clone(),
"providers.wanjie_ark.base_url" => self.providers.wanjie_ark.base_url.clone(),
"providers.wanjie_ark.model" => self.providers.wanjie_ark.model.clone(),
"providers.wanjie_ark.http_headers" => {
serialize_http_headers(&self.providers.wanjie_ark.http_headers)
}
"providers.openrouter.api_key" => self.providers.openrouter.api_key.clone(),
"providers.openrouter.base_url" => self.providers.openrouter.base_url.clone(),
"providers.openrouter.model" => self.providers.openrouter.model.clone(),
@@ -547,6 +575,18 @@ impl ConfigToml {
"providers.atlascloud.http_headers" => {
self.providers.atlascloud.http_headers = parse_http_headers(value)?;
}
"providers.wanjie_ark.api_key" => {
self.providers.wanjie_ark.api_key = Some(value.to_string());
}
"providers.wanjie_ark.base_url" => {
self.providers.wanjie_ark.base_url = Some(value.to_string());
}
"providers.wanjie_ark.model" => {
self.providers.wanjie_ark.model = Some(value.to_string());
}
"providers.wanjie_ark.http_headers" => {
self.providers.wanjie_ark.http_headers = parse_http_headers(value)?;
}
"providers.nvidia_nim.api_key" => {
self.providers.nvidia_nim.api_key = Some(value.to_string());
}
@@ -679,6 +719,12 @@ impl ConfigToml {
"providers.atlascloud.base_url" => self.providers.atlascloud.base_url = None,
"providers.atlascloud.model" => self.providers.atlascloud.model = None,
"providers.atlascloud.http_headers" => self.providers.atlascloud.http_headers.clear(),
"providers.wanjie_ark.api_key" => self.providers.wanjie_ark.api_key = None,
"providers.wanjie_ark.base_url" => self.providers.wanjie_ark.base_url = None,
"providers.wanjie_ark.model" => self.providers.wanjie_ark.model = None,
"providers.wanjie_ark.http_headers" => {
self.providers.wanjie_ark.http_headers.clear();
}
"providers.nvidia_nim.api_key" => self.providers.nvidia_nim.api_key = None,
"providers.nvidia_nim.base_url" => self.providers.nvidia_nim.base_url = None,
"providers.nvidia_nim.model" => self.providers.nvidia_nim.model = None,
@@ -794,6 +840,18 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.atlascloud.http_headers) {
out.insert("providers.atlascloud.http_headers".to_string(), v);
}
if let Some(v) = self.providers.wanjie_ark.api_key.as_ref() {
out.insert("providers.wanjie_ark.api_key".to_string(), redact_secret(v));
}
if let Some(v) = self.providers.wanjie_ark.base_url.as_ref() {
out.insert("providers.wanjie_ark.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.wanjie_ark.model.as_ref() {
out.insert("providers.wanjie_ark.model".to_string(), v.clone());
}
if let Some(v) = serialize_http_headers(&self.providers.wanjie_ark.http_headers) {
out.insert("providers.wanjie_ark.http_headers".to_string(), v);
}
if let Some(v) = self.providers.nvidia_nim.api_key.as_ref() {
out.insert("providers.nvidia_nim.api_key".to_string(), redact_secret(v));
}
@@ -932,6 +990,7 @@ impl ConfigToml {
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL.to_string(),
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL.to_string(),
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL.to_string(),
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL.to_string(),
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL.to_string(),
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
@@ -974,6 +1033,7 @@ impl ConfigToml {
let explicit_model = cli.model.is_some()
|| env.model.is_some()
|| env.model_for(provider).is_some()
|| provider_cfg.model.is_some()
|| root_deepseek_model.is_some()
|| self.model.is_some();
@@ -981,6 +1041,7 @@ impl ConfigToml {
.model
.clone()
.or_else(|| env.model.clone())
.or_else(|| env.model_for(provider))
.or_else(|| provider_cfg.model.clone())
.or(root_deepseek_model)
.or_else(|| self.model.clone())
@@ -1071,7 +1132,10 @@ pub fn load_project_config(workspace: &Path) -> Option<ConfigToml> {
}
fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
if matches!(provider, ProviderKind::Atlascloud | ProviderKind::Ollama) {
if matches!(
provider,
ProviderKind::Atlascloud | ProviderKind::WanjieArk | ProviderKind::Ollama
) {
return model.to_string();
}
@@ -1130,6 +1194,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ProviderKind::Openai => DEFAULT_OPENAI_MODEL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_MODEL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_MODEL,
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
@@ -1145,6 +1210,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -1491,6 +1557,7 @@ fn normalize_config_file_path(path: PathBuf) -> Result<PathBuf> {
struct EnvRuntimeOverrides {
provider: Option<ProviderKind>,
model: Option<String>,
wanjie_ark_model: Option<String>,
output_mode: Option<String>,
auth_mode: Option<String>,
log_level: Option<String>,
@@ -1503,6 +1570,7 @@ struct EnvRuntimeOverrides {
nvidia_base_url: Option<String>,
openai_base_url: Option<String>,
atlascloud_base_url: Option<String>,
wanjie_ark_base_url: Option<String>,
openrouter_base_url: Option<String>,
novita_base_url: Option<String>,
fireworks_base_url: Option<String>,
@@ -1518,6 +1586,11 @@ impl EnvRuntimeOverrides {
.ok()
.and_then(|v| ProviderKind::parse(&v)),
model: std::env::var("DEEPSEEK_MODEL").ok(),
wanjie_ark_model: std::env::var("WANJIE_ARK_MODEL")
.or_else(|_| std::env::var("WANJIE_MODEL"))
.or_else(|_| std::env::var("WANJIE_MAAS_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
output_mode: std::env::var("DEEPSEEK_OUTPUT_MODE").ok(),
auth_mode: std::env::var("DEEPSEEK_AUTH_MODE").ok(),
log_level: std::env::var("DEEPSEEK_LOG_LEVEL").ok(),
@@ -1547,6 +1620,11 @@ impl EnvRuntimeOverrides {
atlascloud_base_url: std::env::var("ATLASCLOUD_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
wanjie_ark_base_url: std::env::var("WANJIE_ARK_BASE_URL")
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
.or_else(|_| std::env::var("WANJIE_MAAS_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
openrouter_base_url: std::env::var("OPENROUTER_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -1576,6 +1654,7 @@ impl EnvRuntimeOverrides {
ProviderKind::NvidiaNim => self.nvidia_base_url.clone(),
ProviderKind::Openai => self.openai_base_url.clone(),
ProviderKind::Atlascloud => self.atlascloud_base_url.clone(),
ProviderKind::WanjieArk => self.wanjie_ark_base_url.clone(),
ProviderKind::Openrouter => self.openrouter_base_url.clone(),
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
@@ -1584,6 +1663,13 @@ impl EnvRuntimeOverrides {
ProviderKind::Ollama => self.ollama_base_url.clone(),
}
}
fn model_for(&self, provider: ProviderKind) -> Option<String> {
match provider {
ProviderKind::WanjieArk => self.wanjie_ark_model.clone(),
_ => None,
}
}
}
#[cfg(test)]
@@ -1628,6 +1714,13 @@ mod tests {
nvidia_nim_base_url: Option<OsString>,
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
wanjie_ark_api_key: Option<OsString>,
wanjie_ark_base_url: Option<OsString>,
wanjie_base_url: Option<OsString>,
wanjie_maas_base_url: Option<OsString>,
wanjie_ark_model: Option<OsString>,
wanjie_model: Option<OsString>,
wanjie_maas_model: Option<OsString>,
novita_api_key: Option<OsString>,
novita_base_url: Option<OsString>,
fireworks_api_key: Option<OsString>,
@@ -1656,6 +1749,13 @@ mod tests {
nvidia_nim_base_url: env::var_os("NVIDIA_NIM_BASE_URL"),
openrouter_api_key: env::var_os("OPENROUTER_API_KEY"),
openrouter_base_url: env::var_os("OPENROUTER_BASE_URL"),
wanjie_ark_api_key: env::var_os("WANJIE_ARK_API_KEY"),
wanjie_ark_base_url: env::var_os("WANJIE_ARK_BASE_URL"),
wanjie_base_url: env::var_os("WANJIE_BASE_URL"),
wanjie_maas_base_url: env::var_os("WANJIE_MAAS_BASE_URL"),
wanjie_ark_model: env::var_os("WANJIE_ARK_MODEL"),
wanjie_model: env::var_os("WANJIE_MODEL"),
wanjie_maas_model: env::var_os("WANJIE_MAAS_MODEL"),
novita_api_key: env::var_os("NOVITA_API_KEY"),
novita_base_url: env::var_os("NOVITA_BASE_URL"),
fireworks_api_key: env::var_os("FIREWORKS_API_KEY"),
@@ -1682,6 +1782,13 @@ mod tests {
env::remove_var("NVIDIA_NIM_BASE_URL");
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("WANJIE_ARK_API_KEY");
env::remove_var("WANJIE_ARK_BASE_URL");
env::remove_var("WANJIE_BASE_URL");
env::remove_var("WANJIE_MAAS_BASE_URL");
env::remove_var("WANJIE_ARK_MODEL");
env::remove_var("WANJIE_MODEL");
env::remove_var("WANJIE_MAAS_MODEL");
env::remove_var("NOVITA_API_KEY");
env::remove_var("NOVITA_BASE_URL");
env::remove_var("FIREWORKS_API_KEY");
@@ -1722,6 +1829,13 @@ mod tests {
Self::restore_var("NVIDIA_NIM_BASE_URL", self.nvidia_nim_base_url.take());
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("WANJIE_ARK_API_KEY", self.wanjie_ark_api_key.take());
Self::restore_var("WANJIE_ARK_BASE_URL", self.wanjie_ark_base_url.take());
Self::restore_var("WANJIE_BASE_URL", self.wanjie_base_url.take());
Self::restore_var("WANJIE_MAAS_BASE_URL", self.wanjie_maas_base_url.take());
Self::restore_var("WANJIE_ARK_MODEL", self.wanjie_ark_model.take());
Self::restore_var("WANJIE_MODEL", self.wanjie_model.take());
Self::restore_var("WANJIE_MAAS_MODEL", self.wanjie_maas_model.take());
Self::restore_var("NOVITA_API_KEY", self.novita_api_key.take());
Self::restore_var("NOVITA_BASE_URL", self.novita_base_url.take());
Self::restore_var("FIREWORKS_API_KEY", self.fireworks_api_key.take());
@@ -2136,6 +2250,18 @@ mod tests {
ProviderKind::parse("ollama-local"),
Some(ProviderKind::Ollama)
);
assert_eq!(
ProviderKind::parse("wanjie-ark"),
Some(ProviderKind::WanjieArk)
);
assert_eq!(
ProviderKind::parse("ark_wanjie"),
Some(ProviderKind::WanjieArk)
);
let parsed: ConfigToml =
toml::from_str("provider = \"ark-wanjie\"").expect("wanjie provider alias");
assert_eq!(parsed.provider, ProviderKind::WanjieArk);
}
#[test]
@@ -2202,6 +2328,22 @@ mod tests {
assert_eq!(resolved.model, DEFAULT_FIREWORKS_MODEL);
}
#[test]
fn wanjie_ark_provider_defaults_to_openai_compatible_endpoint_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::WanjieArk,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::WanjieArk);
assert_eq!(resolved.base_url, DEFAULT_WANJIE_ARK_BASE_URL);
assert_eq!(resolved.model, DEFAULT_WANJIE_ARK_MODEL);
}
#[test]
fn sglang_provider_defaults_to_local_endpoint_and_model() {
let _lock = env_lock();
@@ -2412,6 +2554,27 @@ mod tests {
assert_eq!(resolved.base_url, DEFAULT_FIREWORKS_BASE_URL);
}
#[test]
fn wanjie_ark_env_api_key_and_base_url_fall_back_when_config_missing() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "wanjie-ark");
env::set_var("WANJIE_ARK_API_KEY", "wanjie-env-key");
env::set_var("WANJIE_ARK_BASE_URL", "https://wanjie.example/api/v1");
env::set_var("WANJIE_ARK_MODEL", "account-model-id");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::WanjieArk);
assert_eq!(resolved.api_key.as_deref(), Some("wanjie-env-key"));
assert_eq!(resolved.base_url, "https://wanjie.example/api/v1");
assert_eq!(resolved.model, "account-model-id");
}
#[test]
fn openrouter_provider_normalizes_flash_aliases() {
let _lock = env_lock();
+22
View File
@@ -540,6 +540,12 @@ pub fn env_for(name: &str) -> Option<String> {
"ollama" | "ollama-local" => &["OLLAMA_API_KEY"],
"openai" => &["OPENAI_API_KEY"],
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => &["ATLASCLOUD_API_KEY"],
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => &[
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
],
_ => return None,
};
for var in candidates {
@@ -579,6 +585,9 @@ mod tests {
"OLLAMA_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
SECRET_BACKEND_ENV,
] {
// Safety: tests serialise on env_lock(); the broader
@@ -743,6 +752,19 @@ mod tests {
clear_known_envs();
}
#[test]
fn wanjie_ark_env_aliases_resolve() {
let _guard = env_lock();
clear_known_envs();
unsafe { std::env::set_var("WANJIE_API_KEY", "wanjie-key") };
assert_eq!(env_for("wanjie-ark").as_deref(), Some("wanjie-key"));
assert_eq!(env_for("ark_wanjie").as_deref(), Some("wanjie-key"));
assert_eq!(env_for("wanjie-maas").as_deref(), Some("wanjie-key"));
clear_known_envs();
}
#[test]
fn fireworks_env_aliases_resolve() {
let _lock = env_lock();
+12 -3
View File
@@ -905,7 +905,10 @@ pub(super) fn apply_reasoning_effort(
"enable_thinking": false,
});
}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": false,
@@ -930,7 +933,10 @@ pub(super) fn apply_reasoning_effort(
});
body["reasoning_effort"] = json!("high");
}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": true,
@@ -956,7 +962,10 @@ pub(super) fn apply_reasoning_effort(
});
body["reasoning_effort"] = json!("max");
}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": true,
+16 -3
View File
@@ -4,7 +4,7 @@
//! `/provider` with no args opens the picker modal (#52). `/provider <name>`
//! keeps the v0.6.6 CLI form for muscle-memory + scripted use.
use crate::config::{ApiProvider, normalize_model_name};
use crate::config::{ApiProvider, normalize_model_name, provider_passes_model_through};
use crate::tui::app::{App, AppAction};
use super::CommandResult;
@@ -27,13 +27,13 @@ pub fn provider(app: &mut App, args: Option<&str>) -> CommandResult {
let Some(target) = ApiProvider::parse(name) else {
return CommandResult::error(format!(
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, novita, fireworks, sglang, vllm, or ollama."
));
};
let model = match model_arg {
None => None,
Some(raw) if target == ApiProvider::Ollama => Some(raw.trim().to_string()),
Some(raw) if provider_passes_model_through(target) => Some(raw.trim().to_string()),
Some(raw) => match normalize_model_name(&expand_model_alias(raw)) {
Some(normalized) => Some(normalized),
None => {
@@ -142,6 +142,19 @@ mod tests {
}
}
#[test]
fn switch_to_wanjie_ark_preserves_model_id() {
let mut app = create_test_app();
let result = provider(&mut app, Some("ark-wanjie account-model-id"));
match result.action {
Some(AppAction::SwitchProvider { provider, model }) => {
assert_eq!(provider, ApiProvider::WanjieArk);
assert_eq!(model.as_deref(), Some("account-model-id"));
}
other => panic!("expected SwitchProvider, got {other:?}"),
}
}
#[test]
fn switch_to_novita_emits_action() {
let mut app = create_test_app();
+242 -4
View File
@@ -28,6 +28,8 @@ pub const DEFAULT_OPENAI_MODEL: &str = "gpt-4.1";
pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
pub const DEFAULT_ATLASCLOUD_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
pub const DEFAULT_ATLASCLOUD_BASE_URL: &str = "https://api.atlascloud.ai/v1";
pub const DEFAULT_WANJIE_ARK_MODEL: &str = "deepseek-reasoner";
pub const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.com/api/v1";
pub const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
pub const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
pub const DEFAULT_OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
@@ -70,6 +72,7 @@ pub enum ApiProvider {
NvidiaNim,
Openai,
Atlascloud,
WanjieArk,
Openrouter,
Novita,
Fireworks,
@@ -89,6 +92,8 @@ impl ApiProvider {
"nvidia" | "nvidia-nim" | "nvidia_nim" | "nim" => Some(Self::NvidiaNim),
"openai" | "open-ai" => Some(Self::Openai),
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -107,6 +112,7 @@ impl ApiProvider {
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -125,6 +131,7 @@ impl ApiProvider {
Self::NvidiaNim => "NVIDIA NIM",
Self::Openai => "OpenAI-compatible",
Self::Atlascloud => "AtlasCloud",
Self::WanjieArk => "Wanjie Ark",
Self::Openrouter => "OpenRouter",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
@@ -142,6 +149,7 @@ impl ApiProvider {
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Openrouter,
Self::Novita,
Self::Fireworks,
@@ -250,6 +258,8 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
|| model_lower == "deepseek-v4flash"
|| model_lower == "deepseek-v4"
|| alias_deprecation.is_some();
let is_reasoner = matches!(provider, ApiProvider::WanjieArk)
&& (model_lower.contains("reasoner") || model_lower.contains("r1"));
// Context window: V4-class models get 1M, everything else falls through
// to the model's own lookup or a default.
@@ -270,7 +280,7 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
// Thinking support: V4 models support thinking on all providers, but
// only when the model name matches the V4 family.
let thinking_supported = is_v4_pro || is_v4_flash;
let thinking_supported = is_v4_pro || is_v4_flash || is_reasoner;
// Cache telemetry: returned only by DeepSeek-native and NVIDIA NIM endpoints.
let cache_telemetry_supported = matches!(
@@ -398,6 +408,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::Openrouter => vec![DEFAULT_OPENROUTER_MODEL, DEFAULT_OPENROUTER_FLASH_MODEL],
ApiProvider::Novita => vec![DEFAULT_NOVITA_MODEL, DEFAULT_NOVITA_FLASH_MODEL],
ApiProvider::Fireworks => vec![DEFAULT_FIREWORKS_MODEL],
ApiProvider::WanjieArk => vec![DEFAULT_WANJIE_ARK_MODEL],
ApiProvider::Sglang => vec![DEFAULT_SGLANG_MODEL, DEFAULT_SGLANG_FLASH_MODEL],
ApiProvider::Vllm => vec![DEFAULT_VLLM_MODEL, DEFAULT_VLLM_FLASH_MODEL],
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {
@@ -1194,6 +1205,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub atlascloud: ProviderConfig,
#[serde(default)]
pub wanjie_ark: ProviderConfig,
#[serde(default)]
pub openrouter: ProviderConfig,
#[serde(default)]
pub novita: ProviderConfig,
@@ -1306,6 +1319,7 @@ impl Config {
let table = match provider {
ApiProvider::Openai => "providers.openai",
ApiProvider::Atlascloud => "providers.atlascloud",
ApiProvider::WanjieArk => "providers.wanjie_ark",
ApiProvider::Openrouter => "providers.openrouter",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
@@ -1328,7 +1342,7 @@ impl Config {
&& ApiProvider::parse(provider).is_none()
{
anyhow::bail!(
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, novita, fireworks, sglang, vllm, or ollama."
);
}
if let Some(ref key) = self.api_key
@@ -1446,6 +1460,7 @@ impl Config {
ApiProvider::NvidiaNim => &providers.nvidia_nim,
ApiProvider::Openai => &providers.openai,
ApiProvider::Atlascloud => &providers.atlascloud,
ApiProvider::WanjieArk => &providers.wanjie_ark,
ApiProvider::Openrouter => &providers.openrouter,
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
@@ -1521,6 +1536,7 @@ impl Config {
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ApiProvider::Openai => DEFAULT_OPENAI_MODEL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_MODEL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_MODEL,
ApiProvider::Novita => DEFAULT_NOVITA_MODEL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_MODEL,
@@ -1551,6 +1567,7 @@ impl Config {
.cloned(),
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
@@ -1565,6 +1582,7 @@ impl Config {
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ApiProvider::Openai => DEFAULT_OPENAI_BASE_URL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -1597,6 +1615,7 @@ impl Config {
ApiProvider::NvidiaNim => "nvidia-nim",
ApiProvider::Openai => "openai",
ApiProvider::Atlascloud => "atlascloud",
ApiProvider::WanjieArk => "wanjie-ark",
ApiProvider::Openrouter => "openrouter",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
@@ -1665,6 +1684,11 @@ impl Config {
"AtlasCloud API key not found. Run 'deepseek auth set --provider atlascloud', \
set ATLASCLOUD_API_KEY, or add [providers.atlascloud] api_key in ~/.deepseek/config.toml."
),
ApiProvider::WanjieArk => anyhow::bail!(
"Wanjie Ark API key not found. Run 'deepseek auth set --provider wanjie-ark', \
set WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY, or add \
[providers.wanjie_ark] api_key in ~/.deepseek/config.toml."
),
ApiProvider::Openrouter => anyhow::bail!(
"OpenRouter API key not found. Run 'deepseek auth set --provider openrouter', \
set OPENROUTER_API_KEY, or add [providers.openrouter] api_key in ~/.deepseek/config.toml."
@@ -2177,6 +2201,13 @@ fn apply_env_overrides(config: &mut Config) {
.openrouter
.base_url = Some(value);
}
ApiProvider::WanjieArk => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.wanjie_ark
.base_url = Some(value);
}
ApiProvider::Novita => {
config
.providers
@@ -2265,6 +2296,18 @@ fn apply_env_overrides(config: &mut Config) {
.openrouter
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::WanjieArk)
&& let Ok(value) = std::env::var("WANJIE_ARK_BASE_URL")
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
.or_else(|_| std::env::var("WANJIE_MAAS_BASE_URL"))
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.wanjie_ark
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Novita)
&& let Ok(value) = std::env::var("NOVITA_BASE_URL")
&& !value.trim().is_empty()
@@ -2323,6 +2366,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
@@ -2373,6 +2417,17 @@ fn apply_env_overrides(config: &mut Config) {
{
config.default_text_model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::WanjieArk)
&& let Ok(value) = std::env::var("WANJIE_ARK_MODEL")
.or_else(|_| std::env::var("WANJIE_MODEL"))
.or_else(|_| std::env::var("WANJIE_MAAS_MODEL"))
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.wanjie_ark
.model = Some(value);
}
if let Ok(value) =
std::env::var("DEEPSEEK_MODEL").or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL"))
{
@@ -2398,6 +2453,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
@@ -2653,7 +2709,10 @@ fn normalize_model_for_provider(provider: ApiProvider, model: &str) -> Option<St
pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
matches!(
provider,
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Ollama
)
}
@@ -2671,6 +2730,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ApiProvider::Openai => DEFAULT_OPENAI_BASE_URL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -2901,6 +2961,7 @@ fn merge_providers(
nvidia_nim: merge_provider_config(base.nvidia_nim, override_cfg.nvidia_nim),
openai: merge_provider_config(base.openai, override_cfg.openai),
atlascloud: merge_provider_config(base.atlascloud, override_cfg.atlascloud),
wanjie_ark: merge_provider_config(base.wanjie_ark, override_cfg.wanjie_ark),
openrouter: merge_provider_config(base.openrouter, override_cfg.openrouter),
novita: merge_provider_config(base.novita, override_cfg.novita),
fireworks: merge_provider_config(base.fireworks, override_cfg.fireworks),
@@ -3306,6 +3367,11 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
ApiProvider::Atlascloud => {
std::env::var("ATLASCLOUD_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::WanjieArk => {
std::env::var("WANJIE_ARK_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("WANJIE_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("WANJIE_MAAS_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Openrouter => {
std::env::var("OPENROUTER_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
@@ -3334,6 +3400,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::NvidiaNim => "NVIDIA_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
@@ -3349,6 +3416,12 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
{
return true;
}
if matches!(provider, ApiProvider::WanjieArk)
&& (std::env::var("WANJIE_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("WANJIE_MAAS_API_KEY").is_ok_and(|k| !k.trim().is_empty()))
{
return true;
}
// Self-hosted providers typically run without authentication.
if matches!(
@@ -3407,6 +3480,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::NvidiaNim => "providers.nvidia_nim",
ApiProvider::Openai => "providers.openai",
ApiProvider::Atlascloud => "providers.atlascloud",
ApiProvider::WanjieArk => "providers.wanjie_ark",
ApiProvider::Openrouter => "providers.openrouter",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
@@ -3442,6 +3516,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::NvidiaNim => "nvidia_nim",
ApiProvider::Openai => "openai",
ApiProvider::Atlascloud => "atlascloud",
ApiProvider::WanjieArk => "wanjie_ark",
ApiProvider::Openrouter => "openrouter",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
@@ -3610,6 +3685,15 @@ mod tests {
atlascloud_api_key: Option<OsString>,
atlascloud_base_url: Option<OsString>,
atlascloud_model: Option<OsString>,
wanjie_ark_api_key: Option<OsString>,
wanjie_api_key: Option<OsString>,
wanjie_maas_api_key: Option<OsString>,
wanjie_ark_base_url: Option<OsString>,
wanjie_base_url: Option<OsString>,
wanjie_maas_base_url: Option<OsString>,
wanjie_ark_model: Option<OsString>,
wanjie_model: Option<OsString>,
wanjie_maas_model: Option<OsString>,
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
novita_api_key: Option<OsString>,
@@ -3653,6 +3737,15 @@ mod tests {
let atlascloud_api_key_prev = env::var_os("ATLASCLOUD_API_KEY");
let atlascloud_base_url_prev = env::var_os("ATLASCLOUD_BASE_URL");
let atlascloud_model_prev = env::var_os("ATLASCLOUD_MODEL");
let wanjie_ark_api_key_prev = env::var_os("WANJIE_ARK_API_KEY");
let wanjie_api_key_prev = env::var_os("WANJIE_API_KEY");
let wanjie_maas_api_key_prev = env::var_os("WANJIE_MAAS_API_KEY");
let wanjie_ark_base_url_prev = env::var_os("WANJIE_ARK_BASE_URL");
let wanjie_base_url_prev = env::var_os("WANJIE_BASE_URL");
let wanjie_maas_base_url_prev = env::var_os("WANJIE_MAAS_BASE_URL");
let wanjie_ark_model_prev = env::var_os("WANJIE_ARK_MODEL");
let wanjie_model_prev = env::var_os("WANJIE_MODEL");
let wanjie_maas_model_prev = env::var_os("WANJIE_MAAS_MODEL");
let openrouter_api_key_prev = env::var_os("OPENROUTER_API_KEY");
let openrouter_base_url_prev = env::var_os("OPENROUTER_BASE_URL");
let novita_api_key_prev = env::var_os("NOVITA_API_KEY");
@@ -3691,6 +3784,15 @@ mod tests {
env::remove_var("ATLASCLOUD_API_KEY");
env::remove_var("ATLASCLOUD_BASE_URL");
env::remove_var("ATLASCLOUD_MODEL");
env::remove_var("WANJIE_ARK_API_KEY");
env::remove_var("WANJIE_API_KEY");
env::remove_var("WANJIE_MAAS_API_KEY");
env::remove_var("WANJIE_ARK_BASE_URL");
env::remove_var("WANJIE_BASE_URL");
env::remove_var("WANJIE_MAAS_BASE_URL");
env::remove_var("WANJIE_ARK_MODEL");
env::remove_var("WANJIE_MODEL");
env::remove_var("WANJIE_MAAS_MODEL");
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("NOVITA_API_KEY");
@@ -3729,6 +3831,15 @@ mod tests {
atlascloud_api_key: atlascloud_api_key_prev,
atlascloud_base_url: atlascloud_base_url_prev,
atlascloud_model: atlascloud_model_prev,
wanjie_ark_api_key: wanjie_ark_api_key_prev,
wanjie_api_key: wanjie_api_key_prev,
wanjie_maas_api_key: wanjie_maas_api_key_prev,
wanjie_ark_base_url: wanjie_ark_base_url_prev,
wanjie_base_url: wanjie_base_url_prev,
wanjie_maas_base_url: wanjie_maas_base_url_prev,
wanjie_ark_model: wanjie_ark_model_prev,
wanjie_model: wanjie_model_prev,
wanjie_maas_model: wanjie_maas_model_prev,
openrouter_api_key: openrouter_api_key_prev,
openrouter_base_url: openrouter_base_url_prev,
novita_api_key: novita_api_key_prev,
@@ -3776,6 +3887,15 @@ mod tests {
Self::restore_var("ATLASCLOUD_API_KEY", self.atlascloud_api_key.take());
Self::restore_var("ATLASCLOUD_BASE_URL", self.atlascloud_base_url.take());
Self::restore_var("ATLASCLOUD_MODEL", self.atlascloud_model.take());
Self::restore_var("WANJIE_ARK_API_KEY", self.wanjie_ark_api_key.take());
Self::restore_var("WANJIE_API_KEY", self.wanjie_api_key.take());
Self::restore_var("WANJIE_MAAS_API_KEY", self.wanjie_maas_api_key.take());
Self::restore_var("WANJIE_ARK_BASE_URL", self.wanjie_ark_base_url.take());
Self::restore_var("WANJIE_BASE_URL", self.wanjie_base_url.take());
Self::restore_var("WANJIE_MAAS_BASE_URL", self.wanjie_maas_base_url.take());
Self::restore_var("WANJIE_ARK_MODEL", self.wanjie_ark_model.take());
Self::restore_var("WANJIE_MODEL", self.wanjie_model.take());
Self::restore_var("WANJIE_MAAS_MODEL", self.wanjie_maas_model.take());
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("NOVITA_API_KEY", self.novita_api_key.take());
@@ -4061,6 +4181,9 @@ mod tests {
"openai" => {
providers.openai.api_key = Some(api_key.to_string());
}
"wanjie-ark" => {
providers.wanjie_ark.api_key = Some(api_key.to_string());
}
"openrouter" => {
providers.openrouter.api_key = Some(api_key.to_string());
}
@@ -4091,7 +4214,7 @@ mod tests {
#[test]
fn has_api_key_uses_active_provider_scoped_config_key() {
for provider in ["openai", "openrouter", "novita", "fireworks"] {
for provider in ["openai", "wanjie-ark", "openrouter", "novita", "fireworks"] {
let config = config_with_provider_scoped_key(provider, "provider-config-key");
assert!(
@@ -4106,6 +4229,7 @@ mod tests {
let _lock = lock_test_env();
for (provider, env_var) in [
("openai", "OPENAI_API_KEY"),
("wanjie-ark", "WANJIE_ARK_API_KEY"),
("openrouter", "OPENROUTER_API_KEY"),
("novita", "NOVITA_API_KEY"),
("fireworks", "FIREWORKS_API_KEY"),
@@ -5101,6 +5225,89 @@ http_headers = { "X-Model-Provider-Id" = "from-file" }
Ok(())
}
#[test]
fn wanjie_ark_provider_uses_documented_defaults() -> Result<()> {
let config = Config {
provider: Some("wanjie-ark".to_string()),
..Default::default()
};
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::WanjieArk);
assert_eq!(config.default_model(), DEFAULT_WANJIE_ARK_MODEL);
assert_eq!(config.deepseek_base_url(), DEFAULT_WANJIE_ARK_BASE_URL);
Ok(())
}
#[test]
fn wanjie_ark_env_overrides_provider_base_url_model_and_key() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"deepseek-tui-wanjie-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "ark-wanjie");
env::set_var("WANJIE_ARK_API_KEY", "wanjie-env-key");
env::set_var("WANJIE_ARK_BASE_URL", "https://wanjie.example/api/v1");
env::set_var("WANJIE_ARK_MODEL", "wanjie-model-id");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::WanjieArk);
assert_eq!(config.deepseek_api_key()?, "wanjie-env-key");
assert_eq!(config.deepseek_base_url(), "https://wanjie.example/api/v1");
assert_eq!(config.default_model(), "wanjie-model-id");
Ok(())
}
#[test]
fn wanjie_ark_provider_accepts_custom_model_and_table_key() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"deepseek-tui-wanjie-table-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
let config_path = temp_root.join(".deepseek").join("config.toml");
ensure_parent_dir(&config_path)?;
fs::write(
&config_path,
r#"provider = "wanjie-ark"
[providers.wanjie_ark]
api_key = "wanjie-table-key"
base_url = "https://maas-openapi.wanjiedata.com/api/v1"
model = "account-model-id"
"#,
)?;
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::WanjieArk);
assert_eq!(config.deepseek_api_key()?, "wanjie-table-key");
assert_eq!(
config.deepseek_base_url(),
"https://maas-openapi.wanjiedata.com/api/v1"
);
assert_eq!(config.default_model(), "account-model-id");
Ok(())
}
#[test]
fn openai_provider_accepts_custom_model_and_base_url() -> Result<()> {
let _lock = lock_test_env();
@@ -5699,6 +5906,7 @@ api_key = "novita-table-key"
let mut config = Config::default();
assert!(!has_api_key_for(&config, ApiProvider::Openai));
assert!(!has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
assert!(
has_api_key_for(&config, ApiProvider::Sglang),
@@ -5713,8 +5921,10 @@ api_key = "novita-table-key"
unsafe {
env::set_var("OPENROUTER_API_KEY", "or-env");
env::set_var("OPENAI_API_KEY", "openai-env");
env::set_var("WANJIE_API_KEY", "wanjie-env");
}
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::Openrouter));
assert!(!has_api_key_for(&config, ApiProvider::Novita));
@@ -5722,12 +5932,15 @@ api_key = "novita-table-key"
unsafe {
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENAI_API_KEY");
env::remove_var("WANJIE_API_KEY");
}
let mut providers = ProvidersConfig::default();
providers.openai.api_key = Some("file-openai".to_string());
providers.wanjie_ark.api_key = Some("file-wanjie".to_string());
providers.novita.api_key = Some("file-novita".to_string());
config.providers = Some(providers);
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::Novita));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
Ok(())
@@ -5818,6 +6031,7 @@ api_key = "novita-table-key"
Some("novita-saved-key")
);
save_api_key_for(ApiProvider::Openai, "openai-saved-key")?;
save_api_key_for(ApiProvider::WanjieArk, "wanjie-saved-key")?;
save_api_key_for(ApiProvider::Fireworks, "fireworks-saved-key")?;
save_api_key_for(ApiProvider::Sglang, "sglang-saved-key")?;
let contents = fs::read_to_string(&path)?;
@@ -5830,6 +6044,14 @@ api_key = "novita-table-key"
.and_then(toml::Value::as_str),
Some("openai-saved-key")
);
assert_eq!(
parsed
.get("providers")
.and_then(|p| p.get("wanjie_ark"))
.and_then(|t| t.get("api_key"))
.and_then(toml::Value::as_str),
Some("wanjie-saved-key")
);
assert_eq!(
parsed
.get("providers")
@@ -6141,6 +6363,22 @@ model = "deepseek-ai/deepseek-v4-pro"
);
}
#[test]
fn provider_capability_wanjie_ark_reasoner_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::WanjieArk, DEFAULT_WANJIE_ARK_MODEL);
assert_eq!(
cap.context_window,
crate::models::LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS
);
assert_eq!(cap.max_output, 4096);
assert!(cap.thinking_supported);
assert!(!cap.cache_telemetry_supported);
assert_eq!(
cap.request_payload_mode,
RequestPayloadMode::ChatCompletions
);
}
#[test]
fn provider_capability_ollama_is_openai_compatible_without_thinking() {
let cap = provider_capability(ApiProvider::Ollama, "deepseek-v3.1:671b");
+1
View File
@@ -359,6 +359,7 @@ impl Engine {
ApiProvider::NvidiaNim => "NVIDIA_API_KEY/NVIDIA_NIM_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
+24
View File
@@ -1473,6 +1473,10 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"ATLASCLOUD_API_KEY",
"deepseek auth set --provider atlascloud --api-key \"...\"",
),
crate::config::ApiProvider::WanjieArk => (
"WANJIE_ARK_API_KEY",
"deepseek auth set --provider wanjie-ark --api-key \"...\"",
),
crate::config::ApiProvider::Openrouter => (
"OPENROUTER_API_KEY",
"deepseek auth set --provider openrouter --api-key \"...\"",
@@ -1507,6 +1511,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::NvidiaNim => "nvidia_nim",
crate::config::ApiProvider::Openai => "openai",
crate::config::ApiProvider::Atlascloud => "atlascloud",
crate::config::ApiProvider::WanjieArk => "wanjie_ark",
crate::config::ApiProvider::Openrouter => "openrouter",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
@@ -1718,6 +1723,25 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
"nvidia-nim",
&["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY"][..],
),
(
crate::config::ApiProvider::Openai,
"openai",
&["OPENAI_API_KEY"][..],
),
(
crate::config::ApiProvider::Atlascloud,
"atlascloud",
&["ATLASCLOUD_API_KEY"][..],
),
(
crate::config::ApiProvider::WanjieArk,
"wanjie-ark",
&[
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
][..],
),
(
crate::config::ApiProvider::Openrouter,
"openrouter",
+2
View File
@@ -90,6 +90,7 @@ impl ProviderPickerView {
ApiProvider::NvidiaNim => "NVIDIA_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
@@ -395,6 +396,7 @@ mod tests {
"NVIDIA NIM",
"OpenAI-compatible",
"AtlasCloud",
"Wanjie Ark",
"OpenRouter",
"Novita AI",
"Fireworks AI",
+2
View File
@@ -5431,6 +5431,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::NvidiaNim => Some("NIM"),
crate::config::ApiProvider::Openai => Some("OpenAI"),
crate::config::ApiProvider::Atlascloud => Some("Atlas"),
crate::config::ApiProvider::WanjieArk => Some("Wanjie"),
crate::config::ApiProvider::Openrouter => Some("OR"),
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
@@ -6194,6 +6195,7 @@ async fn apply_provider_picker_api_key(
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
+14 -6
View File
@@ -62,19 +62,24 @@ label without printing the key itself. The command only probes the active
provider's keyring entry.
For hosted, generic OpenAI-compatible, or self-hosted providers, set
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"fireworks"`,
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"wanjie-ark"`, `"fireworks"`,
`"sglang"`, `"vllm"`, or `"ollama"` or pass `deepseek --provider <name>`. The facade saves provider
credentials to the shared user config and forwards the resolved key, base URL,
provider, and model to the TUI process. Use
`deepseek auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY"` or
`deepseek auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"` or
`deepseek auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"` or
`deepseek auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"` or
`deepseek auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` to
save provider keys through the facade. The generic `openai` provider defaults
to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and passes model IDs
through unchanged for OpenAI-compatible gateways. `atlascloud` defaults to
`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses
`deepseek-ai/deepseek-v4-flash` as its default model. SGLang, vLLM, and Ollama are
`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets
Wanjie Ark's OpenAI-compatible endpoint at
`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,
and passes model IDs through unchanged because Wanjie model access is
account-scoped. SGLang, vLLM, and Ollama are
self-hosted and can run without an API key by default. Ollama defaults to
`http://localhost:11434/v1` and sends model tags such as `deepseek-coder:1.3b`
or `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom
@@ -197,7 +202,7 @@ fallbacks after saved config and keyring credentials:
- `DEEPSEEK_API_KEY`
- `DEEPSEEK_BASE_URL`
- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)
- `DEEPSEEK_PROVIDER` (`deepseek|nvidia-nim|openai|atlascloud|openrouter|novita|fireworks|sglang|vllm|ollama`)
- `DEEPSEEK_PROVIDER` (`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|novita|fireworks|sglang|vllm|ollama`)
- `DEEPSEEK_MODEL` or `DEEPSEEK_DEFAULT_TEXT_MODEL`
- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `300`, clamped to `1..=3600`)
- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout)
@@ -210,6 +215,9 @@ fallbacks after saved config and keyring credentials:
- `ATLASCLOUD_API_KEY`
- `ATLASCLOUD_BASE_URL`
- `ATLASCLOUD_MODEL`
- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`
- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`
- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`
- `OPENROUTER_API_KEY`
- `OPENROUTER_BASE_URL`
- `NOVITA_API_KEY`
@@ -418,10 +426,10 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs, `https://api.openai.com/v1` for `provider = "openai"`, `https://api.atlascloud.ai/v1` for `provider = "atlascloud"`, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `gpt-4.1` for generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `deepseek models` to discover live IDs from your configured endpoint. `DEEPSEEK_MODEL` overrides this for a single process.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs, `https://api.openai.com/v1` for `provider = "openai"`, `https://api.atlascloud.ai/v1` for `provider = "atlascloud"`, `https://maas-openapi.wanjiedata.com/api/v1` for `provider = "wanjie-ark"`, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `gpt-4.1` for generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `deepseek models` to discover live IDs from your configured endpoint. `DEEPSEEK_MODEL` overrides this for a single process.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `allow_shell` (bool, optional): defaults to `true` (sandboxed).
- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.