feat(providers): add AtlasCloud as a first-class provider

AtlasCloud (https://atlascloud.ai) hosts the V4 family on its own
DeepSeek-compatible endpoint at `https://api.atlascloud.ai/v1`, and
several contributors had been running it through the
OpenAI-compatible passthrough with manual `base_url` / model
overrides. Selecting `provider = "atlascloud"` in
`~/.deepseek/config.toml` (or via `DEEPSEEK_PROVIDER=atlascloud`)
now wires up:

- documented `DEFAULT_ATLASCLOUD_BASE_URL` /
  `DEFAULT_ATLASCLOUD_MODEL` defaults so a fresh install needs
  only the api_key
- a `[providers.atlascloud]` config block with the same fields
  every other named provider exposes (api_key / base_url / model
  / http_headers)
- `ATLASCLOUD_API_KEY` env var path, including the secrets test
  cleanup loop so per-test env hygiene continues to work
- the provider-picker / `/provider` slash command entries so the
  provider is reachable from the runtime UI, not just config
- the env-driven `*_BASE_URL` override branch so users who pin a
  proxy can still flip it without editing config.toml

Trust-boundary pins held: AtlasCloud is opt-in (default remains
DeepSeek), no API keys are hardcoded, the api_key resolution flows
through the same `secrets` crate path every other provider uses,
and the provider-config base_url stays settable per environment.

Resolved 3-way merge conflicts in `crates/secrets/src/lib.rs` (env
cleanup loop) and `crates/tui/src/config.rs` (per-provider
base_url match arm + `provider_passes_model_through` predicate)
so the contributor's AtlasCloud branch coexists with the v0.8.x
provider expansion already on `main`. Added the missing match arm
in `validate_provider_base_url` so the non-exhaustive-pattern
check passes after the new variant lands.

Harvested from PR #1436 by @lucaszhu-hue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-12 00:40:43 -05:00
parent 40df46c73d
commit 8f33e4bd48
13 changed files with 292 additions and 21 deletions
+15
View File
@@ -39,6 +39,21 @@ real world uses."
### Added
- **AtlasCloud is now a first-class provider** (harvested from
PR #1436 by **@lucaszhu-hue**). AtlasCloud hosts the V4 family
(and other DeepSeek-compatible models) on its own endpoint at
`https://api.atlascloud.ai/v1`, and several contributors had
been running it through the OpenAI-compatible passthrough with
manual `base_url` / model overrides. Selecting
`provider = "atlascloud"` in `~/.deepseek/config.toml` (or via
`DEEPSEEK_PROVIDER=atlascloud`) now wires up the documented
defaults, a `[providers.atlascloud]` config block for per-user
api_key / base_url / model / http_headers overrides, the
`ATLASCLOUD_API_KEY` env var path, and the
provider-picker / `/provider` slash command entries — same
shape as the existing NVIDIA NIM / Fireworks / OpenAI provider
rows. Default remains DeepSeek; nothing changes for installs
that don't opt in.
- **`web_search` supports Tavily and Bocha as configurable
backends** (harvested from PR #1294 by **@sandofree**). DuckDuckGo
with Bing fallback remains the default — no API key required —
+9 -2
View File
@@ -16,10 +16,10 @@
# `/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 | openrouter | novita | fireworks | sglang | vllm | ollama
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | 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" # mainland China preset (official https://api.deepseek.com)
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
# base_url = "https://api.deepseek.com" # opt out of DeepSeek beta features
# Optional custom model request headers for OpenAI-compatible gateways.
# Authorization and Content-Type are managed by the client and cannot be overridden here.
@@ -34,6 +34,7 @@ base_url = "https://api.deepseek.com/beta"
# deepseek-ai/deepseek-v4-pro — NVIDIA NIM-hosted Pro model ID
# 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
# 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
@@ -183,6 +184,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.openai.com/v1"
# model = "gpt-4.1"
# AtlasCloud OpenAI-compatible endpoint (https://www.atlascloud.ai/docs/models/llm)
[providers.atlascloud]
# api_key = "YOUR_ATLASCLOUD_API_KEY"
# base_url = "https://api.atlascloud.ai/v1"
# model = "deepseek-ai/deepseek-v4-flash"
# Fireworks AI-hosted DeepSeek V4 (https://fireworks.ai)
[providers.fireworks]
# api_key = "YOUR_FIREWORKS_API_KEY"
+15 -3
View File
@@ -26,6 +26,7 @@ enum ProviderArg {
Deepseek,
NvidiaNim,
Openai,
Atlascloud,
Openrouter,
Novita,
Fireworks,
@@ -40,6 +41,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Deepseek => ProviderKind::Deepseek,
ProviderArg::NvidiaNim => ProviderKind::NvidiaNim,
ProviderArg::Openai => ProviderKind::Openai,
ProviderArg::Atlascloud => ProviderKind::Atlascloud,
ProviderArg::Openrouter => ProviderKind::Openrouter,
ProviderArg::Novita => ProviderKind::Novita,
ProviderArg::Fireworks => ProviderKind::Fireworks,
@@ -673,6 +675,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
ProviderKind::Deepseek => "deepseek",
ProviderKind::NvidiaNim => "nvidia-nim",
ProviderKind::Openai => "openai",
ProviderKind::Atlascloud => "atlascloud",
ProviderKind::Openrouter => "openrouter",
ProviderKind::Novita => "novita",
ProviderKind::Fireworks => "fireworks",
@@ -683,16 +686,17 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
}
/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 9] = [
const PROVIDER_LIST: [ProviderKind; 10] = [
ProviderKind::Deepseek,
ProviderKind::NvidiaNim,
ProviderKind::Openai,
ProviderKind::Atlascloud,
ProviderKind::Openrouter,
ProviderKind::Novita,
ProviderKind::Fireworks,
ProviderKind::Sglang,
ProviderKind::Vllm,
ProviderKind::Ollama,
ProviderKind::Openai,
];
#[cfg(test)]
@@ -748,6 +752,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
ProviderKind::Vllm => &["VLLM_API_KEY"],
ProviderKind::Ollama => &["OLLAMA_API_KEY"],
ProviderKind::Openai => &["OPENAI_API_KEY"],
ProviderKind::Atlascloud => &["ATLASCLOUD_API_KEY"],
}
}
@@ -1390,6 +1395,7 @@ fn build_tui_command(
ProviderKind::Deepseek
| ProviderKind::NvidiaNim
| ProviderKind::Openai
| ProviderKind::Atlascloud
| ProviderKind::Openrouter
| ProviderKind::Novita
| ProviderKind::Fireworks
@@ -1398,7 +1404,7 @@ fn build_tui_command(
| ProviderKind::Ollama
) {
bail!(
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, 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, OpenRouter, Novita, Fireworks, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `deepseek model ...` for provider registry inspection.",
resolved_runtime.provider.as_str()
);
}
@@ -1420,6 +1426,9 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Openai {
cmd.env("OPENAI_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
let source = resolved_runtime
.api_key_source
.unwrap_or(RuntimeApiKeySource::Env)
@@ -1453,6 +1462,9 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Openai {
cmd.env("OPENAI_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
cmd.env("DEEPSEEK_API_KEY_SOURCE", "cli");
}
if let Some(base_url) = cli.base_url.as_ref() {
+56 -1
View File
@@ -21,6 +21,8 @@ const DEFAULT_OPENAI_MODEL: &str = "gpt-4.1";
const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/beta";
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_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";
@@ -45,6 +47,7 @@ pub enum ProviderKind {
Deepseek,
NvidiaNim,
Openai,
Atlascloud,
Openrouter,
Novita,
Fireworks,
@@ -60,6 +63,7 @@ impl ProviderKind {
Self::Deepseek => "deepseek",
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -75,6 +79,7 @@ impl ProviderKind {
"deepseek" | "deep-seek" => Some(Self::Deepseek),
"nvidia" | "nvidia-nim" | "nvidia_nim" | "nim" => Some(Self::NvidiaNim),
"openai" | "open-ai" => Some(Self::Openai),
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -104,6 +109,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub openai: ProviderConfigToml,
#[serde(default)]
pub atlascloud: ProviderConfigToml,
#[serde(default)]
pub openrouter: ProviderConfigToml,
#[serde(default)]
pub novita: ProviderConfigToml,
@@ -124,6 +131,7 @@ impl ProvidersToml {
ProviderKind::Deepseek => &self.deepseek,
ProviderKind::NvidiaNim => &self.nvidia_nim,
ProviderKind::Openai => &self.openai,
ProviderKind::Atlascloud => &self.atlascloud,
ProviderKind::Openrouter => &self.openrouter,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
@@ -138,6 +146,7 @@ impl ProvidersToml {
ProviderKind::Deepseek => &mut self.deepseek,
ProviderKind::NvidiaNim => &mut self.nvidia_nim,
ProviderKind::Openai => &mut self.openai,
ProviderKind::Atlascloud => &mut self.atlascloud,
ProviderKind::Openrouter => &mut self.openrouter,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
@@ -349,6 +358,10 @@ impl ConfigToml {
&project.providers.nvidia_nim,
);
merge_provider_config(&mut self.providers.openai, &project.providers.openai);
merge_provider_config(
&mut self.providers.atlascloud,
&project.providers.atlascloud,
);
merge_provider_config(
&mut self.providers.openrouter,
&project.providers.openrouter,
@@ -411,6 +424,12 @@ impl ConfigToml {
"providers.openai.http_headers" => {
serialize_http_headers(&self.providers.openai.http_headers)
}
"providers.atlascloud.api_key" => self.providers.atlascloud.api_key.clone(),
"providers.atlascloud.base_url" => self.providers.atlascloud.base_url.clone(),
"providers.atlascloud.model" => self.providers.atlascloud.model.clone(),
"providers.atlascloud.http_headers" => {
serialize_http_headers(&self.providers.atlascloud.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(),
@@ -509,6 +528,18 @@ impl ConfigToml {
"providers.openai.http_headers" => {
self.providers.openai.http_headers = parse_http_headers(value)?;
}
"providers.atlascloud.api_key" => {
self.providers.atlascloud.api_key = Some(value.to_string());
}
"providers.atlascloud.base_url" => {
self.providers.atlascloud.base_url = Some(value.to_string());
}
"providers.atlascloud.model" => {
self.providers.atlascloud.model = Some(value.to_string());
}
"providers.atlascloud.http_headers" => {
self.providers.atlascloud.http_headers = parse_http_headers(value)?;
}
"providers.nvidia_nim.api_key" => {
self.providers.nvidia_nim.api_key = Some(value.to_string());
}
@@ -637,6 +668,10 @@ impl ConfigToml {
"providers.openai.base_url" => self.providers.openai.base_url = None,
"providers.openai.model" => self.providers.openai.model = None,
"providers.openai.http_headers" => self.providers.openai.http_headers.clear(),
"providers.atlascloud.api_key" => self.providers.atlascloud.api_key = None,
"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.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,
@@ -740,6 +775,18 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.openai.http_headers) {
out.insert("providers.openai.http_headers".to_string(), v);
}
if let Some(v) = self.providers.atlascloud.api_key.as_ref() {
out.insert("providers.atlascloud.api_key".to_string(), redact_secret(v));
}
if let Some(v) = self.providers.atlascloud.base_url.as_ref() {
out.insert("providers.atlascloud.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.atlascloud.model.as_ref() {
out.insert("providers.atlascloud.model".to_string(), v.clone());
}
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.nvidia_nim.api_key.as_ref() {
out.insert("providers.nvidia_nim.api_key".to_string(), redact_secret(v));
}
@@ -897,6 +944,7 @@ impl ConfigToml {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL.to_string(),
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::Openrouter => DEFAULT_OPENROUTER_BASE_URL.to_string(),
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
@@ -1009,7 +1057,7 @@ pub fn load_project_config(workspace: &Path) -> Option<ConfigToml> {
}
fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
if matches!(provider, ProviderKind::Ollama) {
if matches!(provider, ProviderKind::Atlascloud | ProviderKind::Ollama) {
return model.to_string();
}
@@ -1067,6 +1115,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_MODEL,
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ProviderKind::Openai => DEFAULT_OPENAI_MODEL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_MODEL,
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
@@ -1081,6 +1130,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL,
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -1369,6 +1419,7 @@ struct EnvRuntimeOverrides {
deepseek_base_url: Option<String>,
nvidia_base_url: Option<String>,
openai_base_url: Option<String>,
atlascloud_base_url: Option<String>,
openrouter_base_url: Option<String>,
novita_base_url: Option<String>,
fireworks_base_url: Option<String>,
@@ -1410,6 +1461,9 @@ impl EnvRuntimeOverrides {
openai_base_url: std::env::var("OPENAI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
atlascloud_base_url: std::env::var("ATLASCLOUD_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()),
@@ -1438,6 +1492,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Deepseek => self.deepseek_base_url.clone(),
ProviderKind::NvidiaNim => self.nvidia_base_url.clone(),
ProviderKind::Openai => self.openai_base_url.clone(),
ProviderKind::Atlascloud => self.atlascloud_base_url.clone(),
ProviderKind::Openrouter => self.openrouter_base_url.clone(),
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
+17 -1
View File
@@ -479,7 +479,8 @@ impl Secrets {
/// Resolve a secret with `secret store → env → none` precedence.
///
/// `name` is the canonical provider name (`"deepseek"`,
/// `"openrouter"`, `"novita"`, `"nvidia"`/`"nvidia-nim"`, `"openai"`).
/// `"openrouter"`, `"novita"`, `"nvidia"`/`"nvidia-nim"`, `"openai"`,
/// or `"atlascloud"`).
/// Empty strings on either layer are treated as "not set".
#[must_use]
pub fn resolve(&self, name: &str) -> Option<String> {
@@ -532,6 +533,7 @@ pub fn env_for(name: &str) -> Option<String> {
"vllm" | "v-llm" => &["VLLM_API_KEY"],
"ollama" | "ollama-local" => &["OLLAMA_API_KEY"],
"openai" => &["OPENAI_API_KEY"],
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => &["ATLASCLOUD_API_KEY"],
_ => return None,
};
for var in candidates {
@@ -570,6 +572,7 @@ mod tests {
"VLLM_API_KEY",
"OLLAMA_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
SECRET_BACKEND_ENV,
] {
// Safety: tests serialise on env_lock(); the broader
@@ -721,6 +724,19 @@ mod tests {
unsafe { std::env::remove_var("NVIDIA_NIM_API_KEY") };
}
#[test]
fn atlascloud_env_aliases_resolve() {
let _guard = env_lock();
clear_known_envs();
unsafe { std::env::set_var("ATLASCLOUD_API_KEY", "atlas-key") };
assert_eq!(env_for("atlascloud").as_deref(), Some("atlas-key"));
assert_eq!(env_for("atlas").as_deref(), Some("atlas-key"));
assert_eq!(env_for("atlas-cloud").as_deref(), Some("atlas-key"));
clear_known_envs();
}
#[test]
fn fireworks_env_aliases_resolve() {
let _lock = env_lock();
+3 -3
View File
@@ -837,7 +837,7 @@ pub(super) fn apply_reasoning_effort(
| ApiProvider::Vllm => {
body["thinking"] = json!({ "type": "disabled" });
}
ApiProvider::Openai | ApiProvider::Ollama => {}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": false,
@@ -855,7 +855,7 @@ pub(super) fn apply_reasoning_effort(
body["reasoning_effort"] = json!("high");
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Openai | ApiProvider::Ollama => {}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": true,
@@ -874,7 +874,7 @@ pub(super) fn apply_reasoning_effort(
body["reasoning_effort"] = json!("max");
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Openai | ApiProvider::Ollama => {}
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
"thinking": true,
+14 -1
View File
@@ -27,7 +27,7 @@ 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, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, openrouter, novita, fireworks, sglang, vllm, or ollama."
));
};
@@ -129,6 +129,19 @@ mod tests {
}
}
#[test]
fn switch_to_atlascloud_emits_action() {
let mut app = create_test_app();
let result = provider(&mut app, Some("atlascloud"));
match result.action {
Some(AppAction::SwitchProvider { provider, model }) => {
assert_eq!(provider, ApiProvider::Atlascloud);
assert_eq!(model, None);
}
other => panic!("expected SwitchProvider, got {other:?}"),
}
}
#[test]
fn switch_to_novita_emits_action() {
let mut app = create_test_app();
+131 -3
View File
@@ -26,6 +26,8 @@ pub const DEFAULT_NVIDIA_NIM_FLASH_MODEL: &str = "deepseek-ai/deepseek-v4-flash"
pub const DEFAULT_NVIDIA_NIM_BASE_URL: &str = "https://integrate.api.nvidia.com/v1";
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_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";
@@ -66,6 +68,7 @@ pub enum ApiProvider {
DeepseekCN,
NvidiaNim,
Openai,
Atlascloud,
Openrouter,
Novita,
Fireworks,
@@ -84,6 +87,7 @@ 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),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -101,6 +105,7 @@ impl ApiProvider {
Self::DeepseekCN => "deepseek-cn",
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -118,6 +123,7 @@ impl ApiProvider {
Self::DeepseekCN => "DeepSeek (legacy alias)",
Self::NvidiaNim => "NVIDIA NIM",
Self::Openai => "OpenAI-compatible",
Self::Atlascloud => "AtlasCloud",
Self::Openrouter => "OpenRouter",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
@@ -134,6 +140,7 @@ impl ApiProvider {
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::Openrouter,
Self::Novita,
Self::Fireworks,
@@ -205,7 +212,7 @@ pub enum RequestPayloadMode {
/// in the API payload (after normalization / provider-specific mapping).
#[must_use]
pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> ProviderCapability {
if matches!(provider, ApiProvider::Openai) {
if matches!(provider, ApiProvider::Openai | ApiProvider::Atlascloud) {
return ProviderCapability {
provider,
resolved_model: resolved_model.to_string(),
@@ -1068,6 +1075,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub openai: ProviderConfig,
#[serde(default)]
pub atlascloud: ProviderConfig,
#[serde(default)]
pub openrouter: ProviderConfig,
#[serde(default)]
pub novita: ProviderConfig,
@@ -1167,6 +1176,7 @@ impl Config {
}
let table = match provider {
ApiProvider::Openai => "providers.openai",
ApiProvider::Atlascloud => "providers.atlascloud",
ApiProvider::Openrouter => "providers.openrouter",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
@@ -1189,7 +1199,7 @@ impl Config {
&& ApiProvider::parse(provider).is_none()
{
anyhow::bail!(
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, openrouter, novita, fireworks, sglang, vllm, or ollama."
);
}
if let Some(ref key) = self.api_key
@@ -1306,6 +1316,7 @@ impl Config {
ApiProvider::DeepseekCN => &providers.deepseek_cn,
ApiProvider::NvidiaNim => &providers.nvidia_nim,
ApiProvider::Openai => &providers.openai,
ApiProvider::Atlascloud => &providers.atlascloud,
ApiProvider::Openrouter => &providers.openrouter,
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
@@ -1369,6 +1380,7 @@ impl Config {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => DEFAULT_TEXT_MODEL,
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ApiProvider::Openai => DEFAULT_OPENAI_MODEL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_MODEL,
ApiProvider::Novita => DEFAULT_NOVITA_MODEL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_MODEL,
@@ -1398,6 +1410,7 @@ impl Config {
.filter(|base| base.contains("integrate.api.nvidia.com"))
.cloned(),
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
@@ -1411,6 +1424,7 @@ impl Config {
ApiProvider::DeepseekCN => DEFAULT_DEEPSEEKCN_BASE_URL,
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ApiProvider::Openai => DEFAULT_OPENAI_BASE_URL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -1442,6 +1456,7 @@ impl Config {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => "deepseek",
ApiProvider::NvidiaNim => "nvidia-nim",
ApiProvider::Openai => "openai",
ApiProvider::Atlascloud => "atlascloud",
ApiProvider::Openrouter => "openrouter",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
@@ -1502,6 +1517,10 @@ impl Config {
"OpenAI-compatible API key not found. Run 'deepseek auth set --provider openai', \
set OPENAI_API_KEY, or add [providers.openai] api_key in ~/.deepseek/config.toml."
),
ApiProvider::Atlascloud => anyhow::bail!(
"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::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."
@@ -2039,6 +2058,13 @@ fn apply_env_overrides(config: &mut Config) {
.ollama
.base_url = Some(value);
}
ApiProvider::Atlascloud => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.atlascloud
.base_url = Some(value);
}
}
}
if matches!(config.api_provider(), ApiProvider::NvidiaNim)
@@ -2065,6 +2091,16 @@ fn apply_env_overrides(config: &mut Config) {
.openai
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Atlascloud)
&& let Ok(value) = std::env::var("ATLASCLOUD_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.atlascloud
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Openrouter)
&& let Ok(value) = std::env::var("OPENROUTER_BASE_URL")
&& !value.trim().is_empty()
@@ -2132,6 +2168,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::DeepseekCN => &mut providers.deepseek_cn,
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
@@ -2177,6 +2214,11 @@ fn apply_env_overrides(config: &mut Config) {
.openai
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Atlascloud)
&& let Ok(value) = std::env::var("ATLASCLOUD_MODEL")
{
config.default_text_model = Some(value);
}
if let Ok(value) =
std::env::var("DEEPSEEK_MODEL").or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL"))
{
@@ -2425,7 +2467,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::Ollama)
matches!(
provider,
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama
)
}
fn provider_entry_uses_custom_base_url(provider: ApiProvider, entry: &ProviderConfig) -> bool {
@@ -2441,6 +2486,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::DeepseekCN => DEFAULT_DEEPSEEKCN_BASE_URL,
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ApiProvider::Openai => DEFAULT_OPENAI_BASE_URL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -2645,6 +2691,7 @@ fn merge_providers(
deepseek_cn: merge_provider_config(base.deepseek_cn, override_cfg.deepseek_cn),
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),
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),
@@ -3047,6 +3094,9 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
|| std::env::var("NVIDIA_NIM_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Openai => std::env::var("OPENAI_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Atlascloud => {
std::env::var("ATLASCLOUD_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())
}
@@ -3074,6 +3124,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => "DEEPSEEK_API_KEY",
ApiProvider::NvidiaNim => "NVIDIA_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
@@ -3142,6 +3193,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::Openrouter => "providers.openrouter",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
@@ -3176,6 +3228,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::Openrouter => "openrouter",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
@@ -3320,6 +3373,9 @@ mod tests {
openai_api_key: Option<OsString>,
openai_base_url: Option<OsString>,
openai_model: Option<OsString>,
atlascloud_api_key: Option<OsString>,
atlascloud_base_url: Option<OsString>,
atlascloud_model: Option<OsString>,
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
novita_api_key: Option<OsString>,
@@ -3360,6 +3416,9 @@ mod tests {
let openai_api_key_prev = env::var_os("OPENAI_API_KEY");
let openai_base_url_prev = env::var_os("OPENAI_BASE_URL");
let openai_model_prev = env::var_os("OPENAI_MODEL");
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 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");
@@ -3395,6 +3454,9 @@ mod tests {
env::remove_var("OPENAI_API_KEY");
env::remove_var("OPENAI_BASE_URL");
env::remove_var("OPENAI_MODEL");
env::remove_var("ATLASCLOUD_API_KEY");
env::remove_var("ATLASCLOUD_BASE_URL");
env::remove_var("ATLASCLOUD_MODEL");
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("NOVITA_API_KEY");
@@ -3430,6 +3492,9 @@ mod tests {
openai_api_key: openai_api_key_prev,
openai_base_url: openai_base_url_prev,
openai_model: openai_model_prev,
atlascloud_api_key: atlascloud_api_key_prev,
atlascloud_base_url: atlascloud_base_url_prev,
atlascloud_model: atlascloud_model_prev,
openrouter_api_key: openrouter_api_key_prev,
openrouter_base_url: openrouter_base_url_prev,
novita_api_key: novita_api_key_prev,
@@ -3474,6 +3539,9 @@ mod tests {
Self::restore_var("OPENAI_API_KEY", self.openai_api_key.take());
Self::restore_var("OPENAI_BASE_URL", self.openai_base_url.take());
Self::restore_var("OPENAI_MODEL", self.openai_model.take());
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("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());
@@ -4706,6 +4774,50 @@ http_headers = { "X-Model-Provider-Id" = "from-file" }
Ok(())
}
#[test]
fn atlascloud_provider_uses_documented_defaults() -> Result<()> {
let config = Config {
provider: Some("atlascloud".to_string()),
..Default::default()
};
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::Atlascloud);
assert_eq!(config.default_model(), DEFAULT_ATLASCLOUD_MODEL);
assert_eq!(config.deepseek_base_url(), DEFAULT_ATLASCLOUD_BASE_URL);
Ok(())
}
#[test]
fn atlascloud_env_overrides_provider_base_url_and_model() -> 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-atlascloud-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "atlascloud");
env::set_var("ATLASCLOUD_API_KEY", "atlascloud-env-key");
env::set_var("ATLASCLOUD_BASE_URL", "https://api.atlascloud.ai/v1");
env::set_var("ATLASCLOUD_MODEL", "deepseek-ai/deepseek-v4-flash");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Atlascloud);
assert_eq!(config.deepseek_api_key()?, "atlascloud-env-key");
assert_eq!(config.deepseek_base_url(), "https://api.atlascloud.ai/v1");
assert_eq!(config.default_model(), "deepseek-ai/deepseek-v4-flash");
Ok(())
}
#[test]
fn openai_provider_accepts_custom_model_and_base_url() -> Result<()> {
let _lock = lock_test_env();
@@ -5672,6 +5784,22 @@ model = "deepseek-ai/deepseek-v4-pro"
);
}
#[test]
fn provider_capability_atlascloud_custom_model_is_chat_completions_without_thinking() {
let cap = provider_capability(ApiProvider::Atlascloud, "deepseek-ai/deepseek-v4-flash");
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
@@ -379,6 +379,7 @@ impl Engine {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => "DEEPSEEK_API_KEY",
ApiProvider::NvidiaNim => "NVIDIA_API_KEY/NVIDIA_NIM_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
+5
View File
@@ -1366,6 +1366,10 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"OPENAI_API_KEY",
"deepseek auth set --provider openai --api-key \"...\"",
),
crate::config::ApiProvider::Atlascloud => (
"ATLASCLOUD_API_KEY",
"deepseek auth set --provider atlascloud --api-key \"...\"",
),
crate::config::ApiProvider::Openrouter => (
"OPENROUTER_API_KEY",
"deepseek auth set --provider openrouter --api-key \"...\"",
@@ -1399,6 +1403,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
match config.api_provider() {
crate::config::ApiProvider::NvidiaNim => "nvidia_nim",
crate::config::ApiProvider::Openai => "openai",
crate::config::ApiProvider::Atlascloud => "atlascloud",
crate::config::ApiProvider::Openrouter => "openrouter",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
+2
View File
@@ -89,6 +89,7 @@ impl ProviderPickerView {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => "DEEPSEEK_API_KEY",
ApiProvider::NvidiaNim => "NVIDIA_API_KEY",
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
@@ -393,6 +394,7 @@ mod tests {
"DeepSeek",
"NVIDIA NIM",
"OpenAI-compatible",
"AtlasCloud",
"OpenRouter",
"Novita AI",
"Fireworks AI",
+4
View File
@@ -5295,6 +5295,8 @@ async fn execute_command_input(
providers.deepseek.api_key = None;
providers.deepseek_cn.api_key = None;
providers.nvidia_nim.api_key = None;
providers.openai.api_key = None;
providers.atlascloud.api_key = None;
providers.openrouter.api_key = None;
providers.novita.api_key = None;
providers.fireworks.api_key = None;
@@ -5675,6 +5677,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::DeepseekCN => None,
crate::config::ApiProvider::NvidiaNim => Some("NIM"),
crate::config::ApiProvider::Openai => Some("OpenAI"),
crate::config::ApiProvider::Atlascloud => Some("Atlas"),
crate::config::ApiProvider::Openrouter => Some("OR"),
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
@@ -6390,6 +6393,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::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
+20 -7
View File
@@ -62,16 +62,19 @@ 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"`, `"fireworks"`, `"sglang"`, `"vllm"`, or
`"ollama"` or pass `deepseek --provider <name>`. The facade saves provider
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"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 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. SGLang, vLLM, and Ollama are
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
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.
@@ -124,6 +127,13 @@ provider = "openai"
base_url = "https://openai-compatible.example/v4"
model = "glm-5"
[profiles.atlascloud]
provider = "atlascloud"
[profiles.atlascloud.providers.atlascloud]
base_url = "https://api.atlascloud.ai/v1"
model = "deepseek-ai/deepseek-v4-flash"
[profiles.sglang]
provider = "sglang"
base_url = "http://localhost:30000/v1"
@@ -155,7 +165,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|openrouter|novita|fireworks|sglang|vllm|ollama`)
- `DEEPSEEK_PROVIDER` (`deepseek|nvidia-nim|openai|atlascloud|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)
@@ -165,6 +175,9 @@ fallbacks after saved config and keyring credentials:
- `OPENAI_API_KEY`
- `OPENAI_BASE_URL`
- `OPENAI_MODEL`
- `ATLASCLOUD_API_KEY`
- `ATLASCLOUD_BASE_URL`
- `ATLASCLOUD_MODEL`
- `OPENROUTER_API_KEY`
- `OPENROUTER_BASE_URL`
- `NOVITA_API_KEY`
@@ -362,10 +375,10 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `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`; `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`, `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`.
- `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"`, 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, `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` 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"`, 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.
- `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.