From 8597afc0763e3d345a295b4d62b6effda634d246 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Thu, 21 May 2026 00:02:02 +0800 Subject: [PATCH] feat(provider): add Wanjie Ark support --- README.md | 16 +- config.example.toml | 21 ++- crates/agent/src/lib.rs | 20 +++ crates/cli/src/lib.rs | 32 +++- crates/config/src/lib.rs | 165 ++++++++++++++++- crates/secrets/src/lib.rs | 22 +++ crates/tui/src/client.rs | 15 +- crates/tui/src/commands/provider.rs | 19 +- crates/tui/src/config.rs | 246 +++++++++++++++++++++++++- crates/tui/src/core/engine.rs | 1 + crates/tui/src/main.rs | 24 +++ crates/tui/src/tui/provider_picker.rs | 2 + crates/tui/src/tui/ui.rs | 2 + docs/CONFIGURATION.md | 20 ++- 14 files changed, 574 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 84f1171c..a740cf7c 100644 --- a/README.md +++ b/README.md @@ -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 ` 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 ` 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 | diff --git a/config.example.toml b/config.example.toml index 6aceae34..5e0762ed 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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" diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 61f99973..30a1caaa 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -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(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 4a4af662..8b64dc5a 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -27,6 +27,7 @@ enum ProviderArg { NvidiaNim, Openai, Atlascloud, + WanjieArk, Openrouter, Novita, Fireworks, @@ -42,6 +43,7 @@ impl From 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, diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d3d7123e..81ca4221 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -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 { } 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 { struct EnvRuntimeOverrides { provider: Option, model: Option, + wanjie_ark_model: Option, output_mode: Option, auth_mode: Option, log_level: Option, @@ -1503,6 +1570,7 @@ struct EnvRuntimeOverrides { nvidia_base_url: Option, openai_base_url: Option, atlascloud_base_url: Option, + wanjie_ark_base_url: Option, openrouter_base_url: Option, novita_base_url: Option, fireworks_base_url: Option, @@ -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 { + match provider { + ProviderKind::WanjieArk => self.wanjie_ark_model.clone(), + _ => None, + } + } } #[cfg(test)] @@ -1628,6 +1714,13 @@ mod tests { nvidia_nim_base_url: Option, openrouter_api_key: Option, openrouter_base_url: Option, + wanjie_ark_api_key: Option, + wanjie_ark_base_url: Option, + wanjie_base_url: Option, + wanjie_maas_base_url: Option, + wanjie_ark_model: Option, + wanjie_model: Option, + wanjie_maas_model: Option, novita_api_key: Option, novita_base_url: Option, fireworks_api_key: Option, @@ -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(); diff --git a/crates/secrets/src/lib.rs b/crates/secrets/src/lib.rs index 1b42f690..f2616391 100644 --- a/crates/secrets/src/lib.rs +++ b/crates/secrets/src/lib.rs @@ -540,6 +540,12 @@ pub fn env_for(name: &str) -> Option { "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(); diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 1009d848..20755bea 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -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, diff --git a/crates/tui/src/commands/provider.rs b/crates/tui/src/commands/provider.rs index 24f4c5a4..915cce8c 100644 --- a/crates/tui/src/commands/provider.rs +++ b/crates/tui/src/commands/provider.rs @@ -4,7 +4,7 @@ //! `/provider` with no args opens the picker modal (#52). `/provider ` //! 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(); diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index ecc94792..d366888a 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -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 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 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 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, atlascloud_base_url: Option, atlascloud_model: Option, + wanjie_ark_api_key: Option, + wanjie_api_key: Option, + wanjie_maas_api_key: Option, + wanjie_ark_base_url: Option, + wanjie_base_url: Option, + wanjie_maas_base_url: Option, + wanjie_ark_model: Option, + wanjie_model: Option, + wanjie_maas_model: Option, openrouter_api_key: Option, openrouter_base_url: Option, novita_api_key: Option, @@ -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"); diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 5bc237ba..407e6346 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -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", diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 85736310..1bbd18e3 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -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", diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index e81cb54f..ecf9f722 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -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", diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 2b87287e..203073c0 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -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, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 774c82c5..a0052c89 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 `. 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.