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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# `api_key` / `base_url` are
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
# (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | deepinfra
provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | deepinfra
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
base_url = "https://api.deepseek.com/beta"
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
Expand Down Expand Up @@ -276,6 +276,7 @@ max_subagents = 10 # optional (1-20)
# `--provider siliconflow` / `/provider arcee` / `/provider moonshot`
# switches between them without having to re-enter keys. Env vars override anything set here:
# DeepSeek: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL
# DeepSeek Anthropic-compatible: DEEPSEEK_API_KEY, DEEPSEEK_ANTHROPIC_BASE_URL
# NIM: NVIDIA_API_KEY (or NVIDIA_NIM_API_KEY), NIM_BASE_URL
# (or NVIDIA_NIM_BASE_URL / NVIDIA_BASE_URL), NVIDIA_NIM_MODEL
# OpenAI-compatible: OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
Expand Down Expand Up @@ -320,6 +321,12 @@ max_subagents = 10 # optional (1-20)
# http_headers = { "X-Model-Provider-Id" = "your-model-provider" } # optional custom request headers
# path_suffix = "/chat/completions" # override the API path; skips /v1 versioning when set
# reasoning_stream_style = "inline_tags" # route <think>...</think> content into Thinking cells

# DeepSeek Anthropic-compatible Messages route (opt-in)
# [providers.deepseek_anthropic]
# api_key = "YOUR_DEEPSEEK_API_KEY"
# base_url = "https://api.deepseek.com/anthropic"
# model = "deepseek-v4-pro"
# [providers.deepseek.auth] # provider-scoped auth source metadata; command execution lands in a follow-up slice
# source = "command"
# command = ["secret-tool", "lookup", "service", "codewhale-deepseek"]
Expand Down
31 changes: 30 additions & 1 deletion crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
pub const CONFIG_FILE_NAME: &str = "config.toml";
pub const PERMISSIONS_FILE_NAME: &str = "permissions.toml";
const DEFAULT_DEEPSEEK_MODEL: &str = "deepseek-v4-pro";
const DEFAULT_DEEPSEEK_ANTHROPIC_MODEL: &str = DEFAULT_DEEPSEEK_MODEL;
const DEFAULT_NVIDIA_NIM_MODEL: &str = "deepseek-ai/deepseek-v4-pro";
const DEFAULT_NVIDIA_NIM_FLASH_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
const DEFAULT_OPENAI_MODEL: &str = "deepseek-v4-pro";
const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/beta";
const DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL: &str = "https://api.deepseek.com/anthropic";
const DEFAULT_NVIDIA_NIM_BASE_URL: &str = "https://integrate.api.nvidia.com/v1";
const DEFAULT_OPENAI_CODEX_MODEL: &str = "gpt-5.5";
const DEFAULT_ANTHROPIC_MODEL: &str = "claude-sonnet-4-6";
Expand Down Expand Up @@ -151,6 +153,13 @@ pub enum ProviderKind {
alias = "deepseek-china"
)]
Deepseek,
#[serde(
alias = "deepseek-anthropic",
alias = "deepseek_anthropic",
alias = "deepseek-claude",
alias = "deepseek_claude"
)]
DeepseekAnthropic,
NvidiaNim,
#[serde(alias = "open-ai")]
Openai,
Expand Down Expand Up @@ -216,8 +225,9 @@ pub enum ProviderKind {
}

impl ProviderKind {
pub const ALL: [Self; 26] = [
pub const ALL: [Self; 27] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Expand Down Expand Up @@ -310,6 +320,14 @@ pub struct ProviderConfigToml {
pub struct ProvidersToml {
#[serde(default)]
pub deepseek: ProviderConfigToml,
#[serde(
default,
alias = "deepseek-anthropic",
alias = "deepseekAnthropic",
alias = "deepseek-claude",
alias = "deepseek_claude"
)]
pub deepseek_anthropic: ProviderConfigToml,
#[serde(default)]
pub nvidia_nim: ProviderConfigToml,
#[serde(default)]
Expand Down Expand Up @@ -411,6 +429,7 @@ impl ProvidersToml {
pub fn for_provider(&self, provider: ProviderKind) -> &ProviderConfigToml {
match provider {
ProviderKind::Deepseek => &self.deepseek,
ProviderKind::DeepseekAnthropic => &self.deepseek_anthropic,
ProviderKind::NvidiaNim => &self.nvidia_nim,
ProviderKind::Openai => &self.openai,
ProviderKind::Atlascloud => &self.atlascloud,
Expand Down Expand Up @@ -442,6 +461,7 @@ impl ProvidersToml {
pub fn for_provider_mut(&mut self, provider: ProviderKind) -> &mut ProviderConfigToml {
match provider {
ProviderKind::Deepseek => &mut self.deepseek,
ProviderKind::DeepseekAnthropic => &mut self.deepseek_anthropic,
ProviderKind::NvidiaNim => &mut self.nvidia_nim,
ProviderKind::Openai => &mut self.openai,
ProviderKind::Atlascloud => &mut self.atlascloud,
Expand Down Expand Up @@ -2227,6 +2247,7 @@ impl ConfigToml {
} else {
configured_base_url.unwrap_or_else(|| match provider {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL.to_string(),
ProviderKind::DeepseekAnthropic => DEFAULT_DEEPSEEK_ANTHROPIC_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(),
Expand Down Expand Up @@ -2803,6 +2824,7 @@ fn canonical_openrouter_recent_model_id(model: &str) -> Option<&'static str> {
fn default_model_for_provider(provider: ProviderKind) -> &'static str {
match provider {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_MODEL,
ProviderKind::DeepseekAnthropic => DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ProviderKind::Openai => DEFAULT_OPENAI_MODEL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
Expand Down Expand Up @@ -2833,6 +2855,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
match provider {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL,
ProviderKind::DeepseekAnthropic => DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
ProviderKind::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
Expand Down Expand Up @@ -4281,6 +4304,7 @@ struct EnvRuntimeOverrides {
verbosity: Option<String>,
http_headers: Option<BTreeMap<String, String>>,
deepseek_base_url: Option<String>,
deepseek_anthropic_base_url: Option<String>,
nvidia_base_url: Option<String>,
openai_base_url: Option<String>,
atlascloud_base_url: Option<String>,
Expand Down Expand Up @@ -4402,6 +4426,10 @@ impl EnvRuntimeOverrides {
.or_else(|_| std::env::var("DEEPSEEK_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
deepseek_anthropic_base_url: std::env::var("DEEPSEEK_ANTHROPIC_BASE_URL")
.or_else(|_| std::env::var("DEEPSEEK_CLAUDE_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
nvidia_base_url: std::env::var("NVIDIA_NIM_BASE_URL")
.or_else(|_| std::env::var("NIM_BASE_URL"))
.or_else(|_| std::env::var("NVIDIA_BASE_URL"))
Expand Down Expand Up @@ -4552,6 +4580,7 @@ impl EnvRuntimeOverrides {
// values (`providers.<name>.base_url`) still win when env is unset.
match provider {
ProviderKind::Deepseek => self.deepseek_base_url.clone(),
ProviderKind::DeepseekAnthropic => self.deepseek_anthropic_base_url.clone(),
ProviderKind::NvidiaNim => self.nvidia_base_url.clone(),
ProviderKind::Openai => self.openai_base_url.clone(),
ProviderKind::Atlascloud => self.atlascloud_base_url.clone(),
Expand Down
46 changes: 45 additions & 1 deletion crates/config/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use super::{
DEFAULT_ARCEE_BASE_URL, DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL,
DEFAULT_ATLASCLOUD_MODEL, DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL,
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL,
DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MOONSHOT_BASE_URL,
Expand Down Expand Up @@ -134,6 +135,47 @@ provider!(
"deepseek",
aliases: ["deep-seek", "deepseek-cn", "deepseek_china", "deepseekcn", "deepseek-china"]
);

/// Opt-in DeepSeek route that speaks the Anthropic Messages wire protocol.
pub struct DeepseekAnthropic;

impl Provider for DeepseekAnthropic {
fn id(&self) -> &'static str {
"deepseek-anthropic"
}

fn kind(&self) -> ProviderKind {
ProviderKind::DeepseekAnthropic
}

fn display_name(&self) -> &'static str {
"DeepSeek (Anthropic-compatible)"
}

fn default_base_url(&self) -> &'static str {
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
}

fn default_model(&self) -> &'static str {
DEFAULT_DEEPSEEK_ANTHROPIC_MODEL
}

fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY"]
}
Comment on lines +163 to +165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

Including ANTHROPIC_API_KEY in the list of environment variables for DeepseekAnthropic poses a security risk. If a user has ANTHROPIC_API_KEY set in their environment (which is common for Anthropic users) but has not set DEEPSEEK_API_KEY, the application will fall back to using the Anthropic API key and send it to DeepSeek's servers (https://api.deepseek.com/anthropic). This results in credential exposure to a third party. Since DeepSeek's Anthropic-compatible endpoint requires a DeepSeek API key and cannot authenticate with an Anthropic key, ANTHROPIC_API_KEY should be removed from this list.

Suggested change
fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY"]
}
fn env_vars(&self) -> &'static [&'static str] {
&["DEEPSEEK_API_KEY"]
}


fn provider_config_key(&self) -> &'static str {
"deepseek_anthropic"
}

fn aliases(&self) -> &'static [&'static str] {
&["deepseek_anthropic", "deepseek-claude", "deepseek_claude"]
}

fn wire(&self) -> WireFormat {
WireFormat::AnthropicMessages
}
}
provider!(
NvidiaNim,
NvidiaNim,
Expand Down Expand Up @@ -499,6 +541,7 @@ provider!(
);

static DEEPSEEK: Deepseek = Deepseek;
static DEEPSEEK_ANTHROPIC: DeepseekAnthropic = DeepseekAnthropic;
static NVIDIA_NIM: NvidiaNim = NvidiaNim;
static OPENAI: Openai = Openai;
static ATLASCLOUD: Atlascloud = Atlascloud;
Expand All @@ -525,8 +568,9 @@ static STEPFUN: Stepfun = Stepfun;
static MINIMAX: Minimax = Minimax;
static DEEPINFRA: Deepinfra = Deepinfra;

static PROVIDER_REGISTRY: [&dyn Provider; 26] = [
static PROVIDER_REGISTRY: [&dyn Provider; 27] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
&OPENAI,
&ATLASCLOUD,
Expand Down
4 changes: 3 additions & 1 deletion crates/config/src/route/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ fn descriptor_protocol_matches_provider_wire() {
);
let expected = match kind {
ProviderKind::OpenaiCodex => RequestProtocol::Responses,
ProviderKind::Anthropic => RequestProtocol::AnthropicMessages,
ProviderKind::DeepseekAnthropic | ProviderKind::Anthropic => {
RequestProtocol::AnthropicMessages
}
_ => RequestProtocol::ChatCompletions,
};
assert_eq!(d.protocol(), expected, "{kind:?} protocol mismatch");
Expand Down
73 changes: 72 additions & 1 deletion crates/config/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ fn config_store_secures_persisted_permissions_file() {
struct EnvGuard {
deepseek_api_key: Option<OsString>,
deepseek_base_url: Option<OsString>,
deepseek_anthropic_base_url: Option<OsString>,
deepseek_claude_base_url: Option<OsString>,
deepseek_http_headers: Option<OsString>,
deepseek_model: Option<OsString>,
deepseek_default_text_model: Option<OsString>,
Expand Down Expand Up @@ -627,6 +629,8 @@ impl EnvGuard {
let guard = Self {
deepseek_api_key: env::var_os("DEEPSEEK_API_KEY"),
deepseek_base_url: env::var_os("DEEPSEEK_BASE_URL"),
deepseek_anthropic_base_url: env::var_os("DEEPSEEK_ANTHROPIC_BASE_URL"),
deepseek_claude_base_url: env::var_os("DEEPSEEK_CLAUDE_BASE_URL"),
deepseek_http_headers: env::var_os("DEEPSEEK_HTTP_HEADERS"),
deepseek_model: env::var_os("DEEPSEEK_MODEL"),
deepseek_default_text_model: env::var_os("DEEPSEEK_DEFAULT_TEXT_MODEL"),
Expand Down Expand Up @@ -716,6 +720,8 @@ impl EnvGuard {
unsafe {
env::remove_var("DEEPSEEK_API_KEY");
env::remove_var("DEEPSEEK_BASE_URL");
env::remove_var("DEEPSEEK_ANTHROPIC_BASE_URL");
env::remove_var("DEEPSEEK_CLAUDE_BASE_URL");
env::remove_var("DEEPSEEK_HTTP_HEADERS");
env::remove_var("DEEPSEEK_MODEL");
env::remove_var("DEEPSEEK_DEFAULT_TEXT_MODEL");
Expand Down Expand Up @@ -819,6 +825,14 @@ impl Drop for EnvGuard {
unsafe {
Self::restore_var("DEEPSEEK_API_KEY", self.deepseek_api_key.take());
Self::restore_var("DEEPSEEK_BASE_URL", self.deepseek_base_url.take());
Self::restore_var(
"DEEPSEEK_ANTHROPIC_BASE_URL",
self.deepseek_anthropic_base_url.take(),
);
Self::restore_var(
"DEEPSEEK_CLAUDE_BASE_URL",
self.deepseek_claude_base_url.take(),
);
Self::restore_var("DEEPSEEK_HTTP_HEADERS", self.deepseek_http_headers.take());
Self::restore_var("DEEPSEEK_MODEL", self.deepseek_model.take());
Self::restore_var(
Expand Down Expand Up @@ -2791,6 +2805,61 @@ fn provider_kind_accepts_legacy_deepseek_cn_aliases() {
}
}

#[test]
fn deepseek_anthropic_route_defaults_to_anthropic_endpoint() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
for alias in [
"deepseek-anthropic",
"deepseek_anthropic",
"deepseek-claude",
"deepseek_claude",
] {
assert_eq!(
ProviderKind::parse(alias),
Some(ProviderKind::DeepseekAnthropic)
);

let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("deepseek anthropic alias");
assert_eq!(parsed.provider, ProviderKind::DeepseekAnthropic);
}

let provider = provider::resolve_provider("deepseek-anthropic")
.expect("deepseek anthropic metadata resolves");
assert_eq!(provider.kind(), ProviderKind::DeepseekAnthropic);
assert_eq!(provider.provider_config_key(), "deepseek_anthropic");
assert_eq!(provider.default_model(), DEFAULT_DEEPSEEK_ANTHROPIC_MODEL);
assert_eq!(
provider.default_base_url(),
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
);
assert_eq!(provider.env_vars(), &["DEEPSEEK_API_KEY"]);
assert_eq!(provider.wire(), provider::WireFormat::AnthropicMessages);

let config = ConfigToml {
provider: ProviderKind::DeepseekAnthropic,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());

assert_eq!(resolved.provider, ProviderKind::DeepseekAnthropic);
assert_eq!(resolved.base_url, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL);
assert_eq!(resolved.model, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL);

unsafe {
std::env::set_var(
"DEEPSEEK_ANTHROPIC_BASE_URL",
"https://gateway.example.test/anthropic",
);
}
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.base_url, "https://gateway.example.test/anthropic");
unsafe {
std::env::remove_var("DEEPSEEK_ANTHROPIC_BASE_URL");
}
}

#[test]
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
Expand Down Expand Up @@ -2878,7 +2947,9 @@ fn provider_metadata_defaults_match_runtime_helpers() {
// is OpenAI-compatible Chat Completions.
let expected_wire = match kind {
ProviderKind::OpenaiCodex => provider::WireFormat::Responses,
ProviderKind::Anthropic => provider::WireFormat::AnthropicMessages,
ProviderKind::Anthropic | ProviderKind::DeepseekAnthropic => {
provider::WireFormat::AnthropicMessages
}
_ => provider::WireFormat::ChatCompletions,
};
assert_eq!(provider.wire(), expected_wire);
Expand Down
Loading
Loading