diff --git a/config.example.toml b/config.example.toml
index 53bb07554..ac858f856 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -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)
@@ -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
@@ -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 ... 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"]
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 051fe0733..d8e819bb7 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -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";
@@ -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,
@@ -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,
@@ -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)]
@@ -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,
@@ -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,
@@ -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(),
@@ -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,
@@ -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,
@@ -4281,6 +4304,7 @@ struct EnvRuntimeOverrides {
verbosity: Option,
http_headers: Option>,
deepseek_base_url: Option,
+ deepseek_anthropic_base_url: Option,
nvidia_base_url: Option,
openai_base_url: Option,
atlascloud_base_url: Option,
@@ -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"))
@@ -4552,6 +4580,7 @@ impl EnvRuntimeOverrides {
// values (`providers..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(),
diff --git a/crates/config/src/provider.rs b/crates/config/src/provider.rs
index 862be6651..20a7346f4 100644
--- a/crates/config/src/provider.rs
+++ b/crates/config/src/provider.rs
@@ -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,
@@ -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"]
+ }
+
+ 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,
@@ -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;
@@ -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,
diff --git a/crates/config/src/route/tests.rs b/crates/config/src/route/tests.rs
index e1eede93a..e74f4e9a6 100644
--- a/crates/config/src/route/tests.rs
+++ b/crates/config/src/route/tests.rs
@@ -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");
diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs
index f369360a8..859455b5a 100644
--- a/crates/config/src/tests.rs
+++ b/crates/config/src/tests.rs
@@ -536,6 +536,8 @@ fn config_store_secures_persisted_permissions_file() {
struct EnvGuard {
deepseek_api_key: Option,
deepseek_base_url: Option,
+ deepseek_anthropic_base_url: Option,
+ deepseek_claude_base_url: Option,
deepseek_http_headers: Option,
deepseek_model: Option,
deepseek_default_text_model: Option,
@@ -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"),
@@ -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");
@@ -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(
@@ -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();
@@ -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);
diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs
index 96f3981dc..a670782a5 100644
--- a/crates/tui/src/client.rs
+++ b/crates/tui/src/client.rs
@@ -20,7 +20,9 @@ use crate::llm_client::{
sanitize_http_error_body, with_retry,
};
use crate::logging;
-use crate::models::{MessageRequest, MessageResponse, ServerToolUsage, SystemPrompt, Usage};
+use crate::models::{
+ ContentBlock, Message, MessageRequest, MessageResponse, ServerToolUsage, SystemPrompt, Usage,
+};
pub(super) fn to_api_tool_name(name: &str) -> String {
let mut out = String::new();
@@ -777,7 +779,7 @@ fn build_default_headers(
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let api_key = api_key.trim();
- if api_provider == ApiProvider::Anthropic {
+ if api_provider_uses_anthropic_messages(api_provider) {
// #3014: the Messages API authenticates with `x-api-key` (never
// `Authorization: Bearer`) and pins the wire contract via
// `anthropic-version`.
@@ -786,19 +788,20 @@ fn build_default_headers(
HeaderValue::from_static("2023-06-01"),
);
}
- let auth_header_name = if !api_key.is_empty() && api_provider == ApiProvider::Anthropic {
- Some(HeaderName::from_static("x-api-key"))
- } else if !api_key.is_empty()
- && api_provider == ApiProvider::XiaomiMimo
- && (xiaomi_mimo_base_url_uses_token_plan(base_url)
- || xiaomi_mimo_api_key_uses_token_plan(api_key))
- {
- Some(HeaderName::from_static("api-key"))
- } else if !api_key.is_empty() {
- Some(AUTHORIZATION)
- } else {
- None
- };
+ let auth_header_name =
+ if !api_key.is_empty() && api_provider_uses_anthropic_messages(api_provider) {
+ Some(HeaderName::from_static("x-api-key"))
+ } else if !api_key.is_empty()
+ && api_provider == ApiProvider::XiaomiMimo
+ && (xiaomi_mimo_base_url_uses_token_plan(base_url)
+ || xiaomi_mimo_api_key_uses_token_plan(api_key))
+ {
+ Some(HeaderName::from_static("api-key"))
+ } else if !api_key.is_empty() {
+ Some(AUTHORIZATION)
+ } else {
+ None
+ };
if let Some(header_name) = auth_header_name.as_ref() {
let header_value = if *header_name == AUTHORIZATION {
HeaderValue::from_str(&format!("Bearer {api_key}"))?
@@ -832,6 +835,75 @@ fn is_auth_dialect_header(header_name: &HeaderName) -> bool {
|| header_name == HeaderName::from_static("x-api-key")
}
+fn api_provider_uses_anthropic_messages(api_provider: ApiProvider) -> bool {
+ matches!(
+ api_provider,
+ ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic
+ )
+}
+
+fn api_provider_skips_models_probe(api_provider: ApiProvider) -> bool {
+ matches!(api_provider, ApiProvider::DeepseekAnthropic)
+}
+
+fn translation_system_prompt(target_language: &str) -> String {
+ format!(
+ "You are a professional translator. Your ONLY task is to translate text to {target_language}. \
+ Rules:\n\
+ 1. Output ONLY the translation, nothing else — no explanations, no notes, no quotes.\n\
+ 2. Preserve all code blocks (```...```), URLs, file paths, command names, \
+ and technical terms like API names, function names, and library names untranslated.\n\
+ 3. Keep Markdown formatting (headings, lists, bold, italics, links) intact.\n\
+ 4. Translate all natural-language prose naturally and professionally.\n\
+ 5. Do NOT add any prefix, suffix, or commentary.\n\
+ 6. If the input is already in {target_language} or contains no prose to translate, \
+ return it as-is."
+ )
+}
+
+fn translation_message_request(text: &str, model: String, target_language: &str) -> MessageRequest {
+ MessageRequest {
+ model,
+ messages: vec![Message {
+ role: "user".to_string(),
+ content: vec![ContentBlock::Text {
+ text: text.to_string(),
+ cache_control: None,
+ }],
+ }],
+ max_tokens: 4096,
+ system: Some(SystemPrompt::Text(translation_system_prompt(
+ target_language,
+ ))),
+ tools: None,
+ tool_choice: None,
+ metadata: None,
+ thinking: None,
+ reasoning_effort: Some("off".to_string()),
+ stream: Some(false),
+ temperature: Some(0.1),
+ top_p: None,
+ }
+}
+
+fn translation_text_from_response(response: &MessageResponse) -> Result {
+ let translated = response
+ .content
+ .iter()
+ .filter_map(|block| match block {
+ ContentBlock::Text { text, .. } => Some(text.as_str()),
+ _ => None,
+ })
+ .collect::>()
+ .join("")
+ .trim()
+ .to_string();
+ if translated.is_empty() {
+ bail!("translate: Anthropic Messages response did not contain text content");
+ }
+ Ok(translated)
+}
+
fn xiaomi_mimo_base_url_uses_token_plan(base_url: &str) -> bool {
let normalized = base_url.trim().to_ascii_lowercase();
let without_scheme = normalized
@@ -873,29 +945,25 @@ impl DeepSeekClient {
model: &str,
target_language: &str,
) -> Result {
+ let model = wire_model_for_provider(self.api_provider, model);
+ if api_provider_uses_anthropic_messages(self.api_provider) {
+ let response = self
+ .handle_anthropic_message(translation_message_request(text, model, target_language))
+ .await?;
+ return translation_text_from_response(&response);
+ }
+
let url = api_url_with_suffix(
&self.base_url,
"chat/completions",
self.path_suffix.as_deref(),
);
- let model = wire_model_for_provider(self.api_provider, model);
let mut body = serde_json::json!({
"model": model,
"messages": [
{
"role": "system",
- "content": format!(
- "You are a professional translator. Your ONLY task is to translate text to {target_language}. \
- Rules:\n\
- 1. Output ONLY the translation, nothing else — no explanations, no notes, no quotes.\n\
- 2. Preserve all code blocks (```...```), URLs, file paths, command names, \
- and technical terms like API names, function names, and library names untranslated.\n\
- 3. Keep Markdown formatting (headings, lists, bold, italics, links) intact.\n\
- 4. Translate all natural-language prose naturally and professionally.\n\
- 5. Do NOT add any prefix, suffix, or commentary.\n\
- 6. If the input is already in {target_language} or contains no prose to translate, \
- return it as-is."
- )
+ "content": translation_system_prompt(target_language)
},
{
"role": "user",
@@ -1072,6 +1140,11 @@ impl DeepSeekClient {
if !should_probe {
return;
}
+ if api_provider_skips_models_probe(self.api_provider) {
+ self.mark_request_success().await;
+ logging::info("Skipping /models recovery probe for provider without a models endpoint");
+ return;
+ }
let health_url = api_url(&self.base_url, "models");
let probe = self.http_client.get(health_url).send().await;
match probe {
@@ -1202,6 +1275,10 @@ impl LlmClient for DeepSeekClient {
}
async fn health_check(&self) -> Result {
+ if api_provider_skips_models_probe(self.api_provider) {
+ self.mark_request_success().await;
+ return Ok(true);
+ }
let health_url = api_url(&self.base_url, "models");
self.wait_for_rate_limit().await;
let response = self.http_client.get(health_url).send().await;
@@ -1229,7 +1306,7 @@ impl LlmClient for DeepSeekClient {
if self.api_provider == ApiProvider::OpenaiCodex {
return self.handle_responses_message(request).await;
}
- if self.api_provider == ApiProvider::Anthropic {
+ if api_provider_uses_anthropic_messages(self.api_provider) {
return self.handle_anthropic_message(request).await;
}
self.create_message_chat(&request).await
@@ -1242,7 +1319,7 @@ impl LlmClient for DeepSeekClient {
if self.api_provider == ApiProvider::OpenaiCodex {
return self.handle_responses_stream(request).await;
}
- if self.api_provider == ApiProvider::Anthropic {
+ if api_provider_uses_anthropic_messages(self.api_provider) {
return self.handle_anthropic_stream(request).await;
}
self.handle_chat_completion_stream(request).await
@@ -1364,7 +1441,7 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama OpenAI-compat endpoint accepts think param.
body["think"] = json!(false);
}
- ApiProvider::Anthropic => {
+ ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic => {
// #3014: thinking/effort shaping happens natively inside
// client/anthropic.rs (adaptive thinking + output_config),
// not via OpenAI-dialect fields.
@@ -1444,7 +1521,7 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama think param.
body["think"] = json!(true);
}
- ApiProvider::Anthropic => {
+ ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic => {
// #3014: thinking/effort shaping happens natively inside
// client/anthropic.rs (adaptive thinking + output_config),
// not via OpenAI-dialect fields.
@@ -1511,7 +1588,7 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama think param.
body["think"] = json!(true);
}
- ApiProvider::Anthropic => {
+ ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic => {
// #3014: thinking/effort shaping happens natively inside
// client/anthropic.rs (adaptive thinking + output_config),
// not via OpenAI-dialect fields.
@@ -1616,6 +1693,12 @@ impl DeepSeekClient {
suffix: &str,
max_tokens: u32,
) -> anyhow::Result {
+ if api_provider_uses_anthropic_messages(self.api_provider) {
+ bail!(
+ "FIM completion is not supported for {} because it uses the Anthropic Messages protocol",
+ self.api_provider.display_name()
+ );
+ }
let url = api_url_with_suffix(&self.base_url, "beta/completions", None);
let model = wire_model_for_provider(self.api_provider, model);
let body = json!({
@@ -1679,10 +1762,13 @@ mod tests {
parse_chat_message, parse_sse_chunk, sanitize_thinking_mode_messages, tool_to_chat,
tool_to_chat_for_base_url,
};
+ use crate::config::{ProviderConfig, ProvidersConfig};
use crate::models::{
ContentBlock, ContentBlockStart, Delta, Message, MessageRequest, StreamEvent, Tool,
};
use serde_json::json;
+ use wiremock::matchers::{method, path};
+ use wiremock::{Mock, MockServer, ResponseTemplate};
fn test_tool(name: &str) -> Tool {
Tool {
@@ -1701,6 +1787,22 @@ mod tests {
}
}
+ fn deepseek_anthropic_client(server: &MockServer) -> DeepSeekClient {
+ let _ = rustls::crypto::ring::default_provider().install_default();
+ let mut providers = ProvidersConfig::default();
+ providers.deepseek_anthropic = ProviderConfig {
+ api_key: Some("ds-test".to_string()),
+ base_url: Some(server.uri()),
+ ..ProviderConfig::default()
+ };
+ DeepSeekClient::new(&Config {
+ provider: Some("deepseek-anthropic".to_string()),
+ providers: Some(providers),
+ ..Config::default()
+ })
+ .expect("deepseek anthropic client")
+ }
+
#[test]
fn parse_speech_audio_response_accepts_message_audio() {
let encoded = general_purpose::STANDARD.encode(b"hi");
@@ -2082,6 +2184,126 @@ mod tests {
assert!(headers.get("x-api-key").is_none());
}
+ #[test]
+ fn deepseek_anthropic_uses_anthropic_header_dialect() {
+ let mut extra = HashMap::new();
+ extra.insert("Authorization".to_string(), "Bearer wrong".to_string());
+ extra.insert("api-key".to_string(), "wrong".to_string());
+ let headers = DeepSeekClient::default_headers_for_provider(
+ "ds-test",
+ &extra,
+ ApiProvider::DeepseekAnthropic,
+ crate::config::DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
+ )
+ .expect("headers");
+
+ assert_eq!(
+ headers
+ .get("x-api-key")
+ .and_then(|value| value.to_str().ok()),
+ Some("ds-test")
+ );
+ assert_eq!(
+ headers
+ .get("anthropic-version")
+ .and_then(|value| value.to_str().ok()),
+ Some("2023-06-01")
+ );
+ assert!(
+ headers.get(AUTHORIZATION).is_none(),
+ "Anthropic-compatible DeepSeek route must not use Bearer auth"
+ );
+ assert!(
+ headers.get("api-key").is_none(),
+ "Anthropic-compatible DeepSeek route must not inherit MiMo auth headers"
+ );
+ }
+
+ #[tokio::test]
+ async fn deepseek_anthropic_translate_uses_messages_endpoint() {
+ let server = MockServer::start().await;
+ Mock::given(method("POST"))
+ .and(path("/v1/messages"))
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
+ "id": "msg_1",
+ "type": "message",
+ "role": "assistant",
+ "content": [{"type": "text", "text": "Hola"}],
+ "model": "deepseek-chat",
+ "stop_reason": "end_turn",
+ "stop_sequence": null,
+ "usage": {"input_tokens": 3, "output_tokens": 1}
+ })))
+ .expect(1)
+ .mount(&server)
+ .await;
+
+ let client = deepseek_anthropic_client(&server);
+ let translated = client
+ .translate("Hello", "deepseek-chat", "Spanish")
+ .await
+ .expect("translation succeeds");
+
+ assert_eq!(translated, "Hola");
+ let requests = server.received_requests().await.expect("recorded requests");
+ assert_eq!(requests.len(), 1);
+ let body: Value = serde_json::from_slice(&requests[0].body).expect("json body");
+ assert_eq!(
+ body.pointer("/messages/0/role").and_then(Value::as_str),
+ Some("user")
+ );
+ assert_eq!(
+ body.pointer("/messages/0/content/0/text")
+ .and_then(Value::as_str),
+ Some("Hello")
+ );
+ assert!(
+ body.get("thinking").is_none(),
+ "translation disables thinking: {body}"
+ );
+ assert!(
+ body.get("system")
+ .and_then(Value::as_str)
+ .is_some_and(|system| system.contains("Spanish")),
+ "target language should be in system prompt: {body}"
+ );
+ }
+
+ #[tokio::test]
+ async fn deepseek_anthropic_health_check_skips_models_probe() {
+ let server = MockServer::start().await;
+ let client = deepseek_anthropic_client(&server);
+
+ assert!(client.health_check().await.expect("health check"));
+ let requests = server.received_requests().await.expect("recorded requests");
+ assert!(
+ requests.is_empty(),
+ "DeepSeek Anthropic-compatible route must not probe /models"
+ );
+ }
+
+ #[tokio::test]
+ async fn deepseek_anthropic_fim_fails_without_http_request() {
+ let server = MockServer::start().await;
+ let client = deepseek_anthropic_client(&server);
+
+ let err = client
+ .fim_completion("deepseek-chat", "fn main() {", "}", 16)
+ .await
+ .expect_err("FIM is unsupported");
+ let message = err.to_string();
+ assert!(
+ message.contains("FIM completion is not supported"),
+ "{message}"
+ );
+ assert!(message.contains("Anthropic Messages protocol"), "{message}");
+ let requests = server.received_requests().await.expect("recorded requests");
+ assert!(
+ requests.is_empty(),
+ "unsupported FIM should fail locally before any HTTP call"
+ );
+ }
+
#[test]
fn custom_api_key_header_is_allowed_without_primary_provider_key() {
let mut extra = HashMap::new();
diff --git a/crates/tui/src/client/anthropic.rs b/crates/tui/src/client/anthropic.rs
index 4a21517b3..ec506b5d0 100644
--- a/crates/tui/src/client/anthropic.rs
+++ b/crates/tui/src/client/anthropic.rs
@@ -954,5 +954,9 @@ mod tests {
anthropic_messages_url("https://gateway.example/v1"),
"https://gateway.example/v1/messages"
);
+ assert_eq!(
+ anthropic_messages_url("https://api.deepseek.com/anthropic"),
+ "https://api.deepseek.com/anthropic/v1/messages"
+ );
}
}
diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs
index 143347d5b..29e21a289 100644
--- a/crates/tui/src/config.rs
+++ b/crates/tui/src/config.rs
@@ -79,6 +79,8 @@ fn resolve_subagent_heartbeat_timeout_secs(raw: Option, api_timeout_secs: u
pub const DEFAULT_TEXT_MODEL: &str = "deepseek-v4-pro";
pub const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/beta";
+pub const DEFAULT_DEEPSEEK_ANTHROPIC_MODEL: &str = DEFAULT_TEXT_MODEL;
+pub const DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL: &str = "https://api.deepseek.com/anthropic";
pub const DEFAULT_NVIDIA_NIM_MODEL: &str = "deepseek-ai/deepseek-v4-pro";
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";
@@ -234,6 +236,7 @@ pub const DEFAULT_MINIMAX_BASE_URL: &str = "https://api.minimax.io/v1";
pub enum ApiProvider {
Deepseek,
DeepseekCN,
+ DeepseekAnthropic,
NvidiaNim,
Openai,
Atlascloud,
@@ -359,7 +362,9 @@ impl ApiProvider {
#[must_use]
pub fn credential_url(self) -> Option<&'static str> {
Some(match self {
- Self::Deepseek | Self::DeepseekCN => "https://platform.deepseek.com/api_keys",
+ Self::Deepseek | Self::DeepseekCN | Self::DeepseekAnthropic => {
+ "https://platform.deepseek.com/api_keys"
+ }
Self::NvidiaNim => "https://build.nvidia.com/settings/api-keys",
Self::Openai => "https://platform.openai.com/api-keys",
Self::Atlascloud => "https://atlascloud.ai/docs/en/api-keys",
@@ -392,9 +397,10 @@ impl ApiProvider {
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
- const KIND_LOOKUP: [Option; 27] = [
+ const KIND_LOOKUP: [Option; 28] = [
Some(codewhale_config::ProviderKind::Deepseek),
None, // DeepseekCN
+ Some(codewhale_config::ProviderKind::DeepseekAnthropic),
Some(codewhale_config::ProviderKind::NvidiaNim),
Some(codewhale_config::ProviderKind::Openai),
Some(codewhale_config::ProviderKind::Atlascloud),
@@ -423,8 +429,9 @@ impl ApiProvider {
];
/// `ProviderKind` discriminant → `ApiProvider` lookup.
- const FROM_KIND_LOOKUP: [Self; 26] = [
+ const FROM_KIND_LOOKUP: [Self; 27] = [
Self::Deepseek,
+ Self::DeepseekAnthropic,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
@@ -497,6 +504,10 @@ fn subagent_provider_key_matches(key: &str, provider: ApiProvider) -> bool {
normalized.as_str(),
"deepseek_cn" | "deepseek_china" | "deepseekcn"
),
+ ApiProvider::DeepseekAnthropic => matches!(
+ normalized.as_str(),
+ "deepseek_anthropic" | "deepseek_claude" | "deepseek_anthropic_api"
+ ),
ApiProvider::Openrouter => matches!(normalized.as_str(), "openrouter" | "open_router"),
ApiProvider::OpenaiCodex => matches!(
normalized.as_str(),
@@ -645,7 +656,10 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
}
let model_lower = resolved_model.to_ascii_lowercase();
- let alias_deprecation = if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
+ let alias_deprecation = if matches!(
+ provider,
+ ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic
+ ) {
deepseek_alias_deprecation(&model_lower)
} else {
None
@@ -695,8 +709,11 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
| ApiProvider::Volcengine
);
- // Request payload mode: all current providers use chat completions.
- let request_payload_mode = RequestPayloadMode::ChatCompletions;
+ let request_payload_mode = if matches!(provider, ApiProvider::DeepseekAnthropic) {
+ RequestPayloadMode::AnthropicMessages
+ } else {
+ RequestPayloadMode::ChatCompletions
+ };
ProviderCapability {
provider,
@@ -787,7 +804,9 @@ pub(crate) fn normalize_custom_model_id(model: &str) -> Option {
#[must_use]
pub fn requested_model_for_provider(provider: ApiProvider, model: &str) -> Option {
match provider {
- ApiProvider::Deepseek | ApiProvider::DeepseekCN => normalize_model_name(model),
+ ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic => {
+ normalize_model_name(model)
+ }
_ => normalize_custom_model_id(model),
}
}
@@ -1155,8 +1174,10 @@ pub fn normalize_model_name_for_provider(provider: ApiProvider, model: &str) ->
return Some(provider_model);
}
}
- if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN)
- && let Some(canonical) = canonical_official_deepseek_model_id(&normalized)
+ if matches!(
+ provider,
+ ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic
+ ) && let Some(canonical) = canonical_official_deepseek_model_id(&normalized)
{
// When the user's input already matches a known model id
// case-insensitively, keep their original casing; only rewrite
@@ -1202,7 +1223,9 @@ pub fn wire_model_for_provider(provider: ApiProvider, model: &str) -> String {
#[must_use]
pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'static str> {
match provider {
- ApiProvider::Deepseek | ApiProvider::DeepseekCN => OFFICIAL_DEEPSEEK_MODELS.to_vec(),
+ ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic => {
+ OFFICIAL_DEEPSEEK_MODELS.to_vec()
+ }
ApiProvider::NvidiaNim => vec![DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_NVIDIA_NIM_FLASH_MODEL],
ApiProvider::Openrouter => {
let mut models = vec![DEFAULT_OPENROUTER_MODEL, DEFAULT_OPENROUTER_FLASH_MODEL];
@@ -2635,6 +2658,14 @@ pub struct ProvidersConfig {
pub deepseek: ProviderConfig,
#[serde(default, alias = "deepseekCn")]
pub deepseek_cn: ProviderConfig,
+ #[serde(
+ default,
+ alias = "deepseek-anthropic",
+ alias = "deepseekAnthropic",
+ alias = "deepseek-claude",
+ alias = "deepseek_claude"
+ )]
+ pub deepseek_anthropic: ProviderConfig,
#[serde(default, alias = "nvidiaNim")]
pub nvidia_nim: ProviderConfig,
#[serde(default)]
@@ -2973,6 +3004,7 @@ impl Config {
Some(match provider {
ApiProvider::Deepseek => &providers.deepseek,
ApiProvider::DeepseekCN => &providers.deepseek_cn,
+ ApiProvider::DeepseekAnthropic => &providers.deepseek_anthropic,
ApiProvider::NvidiaNim => &providers.nvidia_nim,
ApiProvider::Openai => &providers.openai,
ApiProvider::Atlascloud => &providers.atlascloud,
@@ -3016,6 +3048,7 @@ impl Config {
match provider {
ApiProvider::Deepseek => &mut providers.deepseek,
ApiProvider::DeepseekCN => &mut providers.deepseek_cn,
+ ApiProvider::DeepseekAnthropic => &mut providers.deepseek_anthropic,
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
@@ -3165,6 +3198,7 @@ impl Config {
match provider {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => DEFAULT_TEXT_MODEL,
+ ApiProvider::DeepseekAnthropic => DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_MODEL,
ApiProvider::Openai => DEFAULT_OPENAI_MODEL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
@@ -3205,6 +3239,7 @@ impl Config {
// entries or the corresponding `*_BASE_URL` env var.
let root_base = match provider {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => self.base_url.clone(),
+ ApiProvider::DeepseekAnthropic => None,
ApiProvider::NvidiaNim => self
.base_url
.as_ref()
@@ -3254,6 +3289,7 @@ impl Config {
match provider {
ApiProvider::Deepseek => DEFAULT_DEEPSEEK_BASE_URL,
ApiProvider::DeepseekCN => DEFAULT_DEEPSEEKCN_BASE_URL,
+ ApiProvider::DeepseekAnthropic => DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
ApiProvider::NvidiaNim => DEFAULT_NVIDIA_NIM_BASE_URL,
ApiProvider::Openai => DEFAULT_OPENAI_BASE_URL,
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
@@ -4348,6 +4384,13 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {
config.base_url = Some(value);
}
+ ApiProvider::DeepseekAnthropic => {
+ config
+ .providers
+ .get_or_insert_with(ProvidersConfig::default)
+ .deepseek_anthropic
+ .base_url = Some(value);
+ }
ApiProvider::NvidiaNim => {
config
.providers
@@ -4710,6 +4753,7 @@ fn apply_env_overrides(config: &mut Config) {
let entry = match provider {
ApiProvider::Deepseek => &mut providers.deepseek,
ApiProvider::DeepseekCN => &mut providers.deepseek_cn,
+ ApiProvider::DeepseekAnthropic => &mut providers.deepseek_anthropic,
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
@@ -4900,14 +4944,19 @@ fn apply_env_overrides(config: &mut Config) {
// (issue #1714). Mirror the OPENAI_MODEL branch above for every
// non-DeepSeek provider.
let provider = config.api_provider();
- if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
+ if matches!(
+ provider,
+ ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic
+ ) {
config.default_text_model = Some(value);
} else {
let providers = config
.providers
.get_or_insert_with(ProvidersConfig::default);
let entry = match provider {
- ApiProvider::Deepseek | ApiProvider::DeepseekCN => unreachable!(
+ ApiProvider::Deepseek
+ | ApiProvider::DeepseekCN
+ | ApiProvider::DeepseekAnthropic => unreachable!(
"DeepSeek providers are handled in the if branch above (issue #1714)"
),
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
@@ -5639,6 +5688,10 @@ fn merge_providers(
(Some(base), Some(override_cfg)) => Some(ProvidersConfig {
deepseek: merge_provider_config(base.deepseek, override_cfg.deepseek),
deepseek_cn: merge_provider_config(base.deepseek_cn, override_cfg.deepseek_cn),
+ deepseek_anthropic: merge_provider_config(
+ base.deepseek_anthropic,
+ override_cfg.deepseek_anthropic,
+ ),
nvidia_nim: merge_provider_config(base.nvidia_nim, override_cfg.nvidia_nim),
openai: merge_provider_config(base.openai, override_cfg.openai),
anthropic: merge_provider_config(base.anthropic, override_cfg.anthropic),
diff --git a/crates/tui/src/config/tests.rs b/crates/tui/src/config/tests.rs
index 9fb1bb1f2..c372e2b1d 100644
--- a/crates/tui/src/config/tests.rs
+++ b/crates/tui/src/config/tests.rs
@@ -6127,6 +6127,26 @@ fn provider_capability_deepseek_v4_pro_has_1m_window_and_thinking() {
);
}
+#[test]
+fn provider_capability_deepseek_anthropic_uses_messages_payload() {
+ let cap = provider_capability(
+ ApiProvider::DeepseekAnthropic,
+ DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
+ );
+ assert_eq!(
+ cap.context_window,
+ crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS
+ );
+ assert_eq!(cap.max_output, 384_000);
+ assert!(cap.thinking_supported);
+ assert!(!cap.cache_telemetry_supported);
+ assert_eq!(
+ cap.request_payload_mode,
+ RequestPayloadMode::AnthropicMessages
+ );
+ assert!(cap.alias_deprecation.is_none());
+}
+
#[test]
fn provider_capability_deepseek_v4_flash_has_1m_window_and_thinking() {
let cap = provider_capability(ApiProvider::Deepseek, "deepseek-v4-flash");
diff --git a/crates/tui/src/config_persistence.rs b/crates/tui/src/config_persistence.rs
index 39d00bbf0..0731e8616 100644
--- a/crates/tui/src/config_persistence.rs
+++ b/crates/tui/src/config_persistence.rs
@@ -290,6 +290,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {
anyhow::bail!("DeepSeek uses the root base_url setting")
}
+ ApiProvider::DeepseekAnthropic => Ok("deepseek_anthropic"),
ApiProvider::NvidiaNim => Ok("nvidia_nim"),
ApiProvider::Openai => Ok("openai"),
ApiProvider::Anthropic => Ok("anthropic"),
diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs
index fd62d94ad..5629d980c 100644
--- a/crates/tui/src/tui/ui.rs
+++ b/crates/tui/src/tui/ui.rs
@@ -8278,6 +8278,7 @@ fn render(f: &mut Frame, app: &mut App) {
let provider_label = match app.api_provider {
crate::config::ApiProvider::Deepseek => None,
crate::config::ApiProvider::DeepseekCN => None,
+ crate::config::ApiProvider::DeepseekAnthropic => Some("DS-A"),
crate::config::ApiProvider::NvidiaNim => Some("NIM"),
crate::config::ApiProvider::Openai => Some("OpenAI"),
crate::config::ApiProvider::Anthropic => Some("Claude"),
@@ -9416,6 +9417,7 @@ async fn apply_provider_picker_api_key(
// Guarded by the outer `if` above; safety net against refactors.
return;
}
+ ApiProvider::DeepseekAnthropic => &mut providers.deepseek_anthropic,
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
@@ -9481,6 +9483,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
.get_or_insert_with(ProvidersConfig::default);
let entry: &mut ProviderConfig = match provider {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => return,
+ ApiProvider::DeepseekAnthropic => &mut providers.deepseek_anthropic,
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 35ca775f3..43002cbcb 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -453,13 +453,14 @@ aliases. When both forms are set the `CODEWHALE_*` value wins; the
`DEEPSEEK_*` form is kept for older shells:
- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —
- `deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|huggingface|together|qianfan|openai-codex|anthropic|zai|stepfun|minimax|deepinfra`
+ `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|huggingface|together|qianfan|openai-codex|anthropic|zai|stepfun|minimax|deepinfra`
- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider
- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider
Remaining variables:
- `DEEPSEEK_API_KEY`
+- `DEEPSEEK_ANTHROPIC_BASE_URL`
- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)
- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)
- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `300`, clamped to `1..=3600`)
@@ -1014,14 +1015,14 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
-- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`, `zai`, `stepfun`, `minimax`, or `deepinfra`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys; set `base_url` explicitly if your Token Plan account uses the China region; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/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`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets ChatGPT/Codex OAuth; `anthropic` targets Claude's native Messages API; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`.
+- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`, `zai`, `stepfun`, `minimax`, or `deepinfra`. 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. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys; set `base_url` explicitly if your Token Plan account uses the China region; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/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`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets ChatGPT/Codex OAuth; `anthropic` targets Claude's native Messages API; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`.
- `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. Other defaults are `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://router.huggingface.co/v1` for `huggingface`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://chatgpt.com/backend-api` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `base_url = "https://token-plan-cn.xiaomimimo.com/v1"` explicitly if your Xiaomi MiMo Token Plan account is provisioned in the China region. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
+- `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. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://router.huggingface.co/v1` for `huggingface`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://chatgpt.com/backend-api` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `base_url = "https://token-plan-cn.xiaomimimo.com/v1"` explicitly if your Xiaomi MiMo Token Plan account is provisioned in the China region. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = "/chat/completions"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.
- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text.
- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = "command"` stores a command argv plus optional `timeout_ms`; `source = "secret"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.
- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.
-- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.2` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-5.5`; `anthropic` defaults to `claude-sonnet-4-6`. 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, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k2.7-code`, `kimi-k2.6`, and Kimi Code's stable `kimi-for-coding`; direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.5-pro` and `mimo-v2.5`, while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `zai`, `stepfun`, `qianfan`, and Ollama model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
+- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek, `deepseek-anthropic`, and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.2` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-5.5`; `anthropic` defaults to `claude-sonnet-4-6`. 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, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k2.7-code`, `kimi-k2.6`, and Kimi Code's stable `kimi-for-coding`; direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.5-pro` and `mimo-v2.5`, while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `zai`, `stepfun`, `qianfan`, and Ollama model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, `max`, `xhigh`, or `ultracode`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the
default conversational prompt. `concise` appends a prompt discipline block
diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md
index e93b7f391..84884f981 100644
--- a/docs/PROVIDERS.md
+++ b/docs/PROVIDERS.md
@@ -28,11 +28,11 @@ Sources to keep in sync:
The canonical provider IDs are:
-`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,
-`openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`,
-`siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`,
-`together`, `qianfan`, `openai-codex`, `anthropic`, `zai`, `stepfun`,
-`minimax`, and `deepinfra`.
+`deepseek`, `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`,
+`wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`,
+`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,
+`ollama`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`,
+`zai`, `stepfun`, `minimax`, and `deepinfra`.
Use any of these surfaces to select a provider:
@@ -45,6 +45,12 @@ Use any of these surfaces to select a provider:
as legacy aliases for `deepseek`. They do not select a different official host;
DeepSeek uses the same official API host worldwide.
+`deepseek_anthropic`, `deepseek-claude`, and `deepseek_claude` select
+`deepseek-anthropic`, the opt-in DeepSeek route that speaks the Anthropic
+Messages API at `https://api.deepseek.com/anthropic`. It keeps the normal
+DeepSeek API key path but uses `x-api-key` plus `anthropic-version: 2023-06-01`
+instead of Bearer auth.
+
`huggingface`, `hugging-face`, `hugging_face`, and `hf` all select the
Hugging Face Inference Providers route. This is the OpenAI-compatible router
path for chat/inference, not Hub browsing, model-card inspection, uploads, or
@@ -72,6 +78,7 @@ the listed provider env vars.
| Provider ID | TOML table | Wire protocol | Auth env vars |
| --- | --- | --- | --- |
| `deepseek` | `[providers.deepseek]` | OpenAI Chat Completions | `DEEPSEEK_API_KEY` |
+| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | Anthropic Messages | `DEEPSEEK_API_KEY` |
| `nvidia-nim` | `[providers.nvidia_nim]` | OpenAI Chat Completions | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY`, `DEEPSEEK_API_KEY` |
| `openai` | `[providers.openai]` | OpenAI Chat Completions | `OPENAI_API_KEY` |
| `atlascloud` | `[providers.atlascloud]` | OpenAI Chat Completions | `ATLASCLOUD_API_KEY` |
@@ -101,8 +108,8 @@ the listed provider env vars.
Default base URLs and models for each route are listed in the shipped provider
table below. The wire protocol values above are derived from
`crates/config/src/provider.rs`: `ChatCompletions` is the default,
-`openai-codex` overrides to `Responses`, and `anthropic` overrides to
-`AnthropicMessages`.
+`openai-codex` overrides to `Responses`, and `deepseek-anthropic` plus
+`anthropic` override to `AnthropicMessages`.
## Auth And Env Rules
@@ -227,6 +234,7 @@ the same links where possible.
| Provider ID | TOML table | Auth env | Base URL env and default | Default or static models | Notes |
| --- | --- | --- | --- | --- | --- |
| `deepseek` | `[providers.deepseek]` | `DEEPSEEK_API_KEY` | `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`; default `https://api.deepseek.com/beta` | `deepseek-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. Set `https://api.deepseek.com` or `/v1` explicitly to opt out of beta-only features. |
+| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | `DEEPSEEK_API_KEY` | `DEEPSEEK_ANTHROPIC_BASE_URL`; default `https://api.deepseek.com/anthropic` | `deepseek-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | Opt-in DeepSeek route for the Anthropic Messages wire protocol. Uses `/v1/messages`, `x-api-key`, and `anthropic-version: 2023-06-01`. Keep `provider = "deepseek"` for the default Chat Completions path. |
| `nvidia-nim` | `[providers.nvidia_nim]` | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY`, fallback `DEEPSEEK_API_KEY` | `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, `NVIDIA_BASE_URL`; default `https://integrate.api.nvidia.com/v1` | `deepseek-ai/deepseek-v4-pro`, `deepseek-ai/deepseek-v4-flash` | Hosted DeepSeek V4 through NVIDIA NIM. `NVIDIA_NIM_MODEL` is accepted by the TUI config path. |
| `openai` | `[providers.openai]` | `OPENAI_API_KEY` | `OPENAI_BASE_URL`; default `https://api.openai.com/v1` | Registry entries: `deepseek-v4-pro`, `deepseek-v4-flash`; default config model `deepseek-v4-pro` | Generic OpenAI-compatible route for gateways and custom endpoints, including Alibaba Bailian / Model Studio DashScope when configured with that endpoint. Use this for explicit third-party OpenAI-compatible routes instead of inventing a new provider ID. `OPENAI_MODEL` is accepted. |
| `atlascloud` | `[providers.atlascloud]` | `ATLASCLOUD_API_KEY` | `ATLASCLOUD_BASE_URL`; default `https://api.atlascloud.ai/v1` | Default `deepseek-ai/deepseek-v4-flash`; explicit `vendor/model-id` values pass through when AtlasCloud is selected | OpenAI-compatible hosted route. `ATLASCLOUD_MODEL` is accepted by the TUI config path, the static `ModelRegistry` keeps DeepSeek V4 fallback rows, and provider-hinted CLI model IDs are sent to AtlasCloud exactly as requested. Use Atlas Cloud's own catalog or Coding Plan page for the current provider-owned model list and pricing. |
diff --git a/scripts/check-provider-registry.py b/scripts/check-provider-registry.py
index 4d41ce566..34e504d07 100644
--- a/scripts/check-provider-registry.py
+++ b/scripts/check-provider-registry.py
@@ -126,8 +126,10 @@ def provider_kind_ids(config_rs: str) -> dict[str, str]:
provider_rs,
)
ids: dict[str, str] = {variant: provider_id for variant, provider_id in pairs}
- # OpenaiCodex and Anthropic use manual impls rather than the provider!() macro
+ # OpenaiCodex, Anthropic, and DeepseekAnthropic use manual impls rather
+ # than the provider!() macro.
for variant_name, id_literal in [
+ ("DeepseekAnthropic", "deepseek-anthropic"),
("OpenaiCodex", "openai-codex"),
("Anthropic", "anthropic"),
]:
diff --git a/web/lib/facts-drift.ts b/web/lib/facts-drift.ts
index 8eddad71f..a1a78eebb 100644
--- a/web/lib/facts-drift.ts
+++ b/web/lib/facts-drift.ts
@@ -77,6 +77,7 @@ function deriveProvidersFromConfig(cfg: string): ProviderFact[] {
// so the binary rejects it — keep it out of the docs. Issue #1104.
const labelMap: Record = {
Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
+ DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" },
NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" },
Openai: { id: "openai", label: "OpenAI-compatible", env: "OPENAI_API_KEY" },
Atlascloud: { id: "atlascloud", label: "AtlasCloud", env: "ATLASCLOUD_API_KEY" },
diff --git a/web/scripts/derive-facts.mjs b/web/scripts/derive-facts.mjs
index 0338ea45b..694deeb0f 100644
--- a/web/scripts/derive-facts.mjs
+++ b/web/scripts/derive-facts.mjs
@@ -66,6 +66,7 @@ function deriveProviders() {
// shared ProviderKind, so we exclude it until that lands. Issue #1104.
const labelMap = {
Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
+ DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" },
NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" },
Openai: { id: "openai", label: "OpenAI-compatible", env: "OPENAI_API_KEY" },
Atlascloud: { id: "atlascloud", label: "AtlasCloud", env: "ATLASCLOUD_API_KEY" },