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:
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user