Skip to content

Commit ce39b11

Browse files
authored
Merge PR #3054 from Hmbown: native Anthropic Messages API adapter
feat(client): native Anthropic Messages API adapter — cache_control, thinking blocks, tool streaming (#3014)
2 parents f87245f + d940b78 commit ce39b11

34 files changed

Lines changed: 1281 additions & 33 deletions

config.example.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# `api_key` / `base_url` are
2121
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
2222
# (backward compatibility).
23-
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | sglang | vllm | ollama | huggingface
23+
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | sglang | vllm | ollama | huggingface | together | openai-codex | anthropic
2424
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
2525
base_url = "https://api.deepseek.com/beta"
2626
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
@@ -440,6 +440,17 @@ max_subagents = 10 # optional (1-20)
440440
# base_url = "https://chatgpt.com/backend-api"
441441
# model = "gpt-5.5"
442442

443+
# ─────────────────────────────────────────────────────────────────────────────────
444+
# Anthropic Provider (native Messages API)
445+
# Talks to https://api.anthropic.com/v1/messages with x-api-key auth — not an
446+
# OpenAI-compatible route. Models: claude-opus-4-8, claude-sonnet-4-6 (default),
447+
# claude-haiku-4-5. Env vars: ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL,
448+
# ANTHROPIC_MODEL.
449+
[providers.anthropic]
450+
# api_key = "sk-ant-..."
451+
# base_url = "https://api.anthropic.com"
452+
# model = "claude-sonnet-4-6"
453+
443454
# ─────────────────────────────────────────────────────────────────────────────────
444455
# Web Search Provider
445456
# ─────────────────────────────────────────────────────────────────────────────────

crates/agent/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,28 @@ impl Default for ModelRegistry {
607607
supports_tools: true,
608608
supports_reasoning: true,
609609
},
610+
// Anthropic native Messages API models (#3014)
611+
ModelInfo {
612+
id: "claude-opus-4-8".to_string(),
613+
provider: ProviderKind::Anthropic,
614+
aliases: vec!["opus".to_string(), "claude-opus".to_string()],
615+
supports_tools: true,
616+
supports_reasoning: true,
617+
},
618+
ModelInfo {
619+
id: "claude-sonnet-4-6".to_string(),
620+
provider: ProviderKind::Anthropic,
621+
aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
622+
supports_tools: true,
623+
supports_reasoning: true,
624+
},
625+
ModelInfo {
626+
id: "claude-haiku-4-5".to_string(),
627+
provider: ProviderKind::Anthropic,
628+
aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
629+
supports_tools: true,
630+
supports_reasoning: false,
631+
},
610632
// MiniMax 2.7 (OpenRouter)
611633
ModelInfo {
612634
id: "minimax/minimax-2.7".to_string(),

crates/cli/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
771771
ProviderKind::Huggingface => "huggingface",
772772
ProviderKind::Together => "together",
773773
ProviderKind::OpenaiCodex => "openai-codex",
774+
ProviderKind::Anthropic => "anthropic",
774775
}
775776
}
776777

@@ -895,6 +896,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
895896
],
896897
ProviderKind::Together => &["TOGETHER_API_KEY"],
897898
ProviderKind::OpenaiCodex => &["OPENAI_CODEX_ACCESS_TOKEN", "CODEX_ACCESS_TOKEN"],
899+
ProviderKind::Anthropic => &["ANTHROPIC_API_KEY"],
898900
}
899901
}
900902

crates/config/src/lib.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const DEFAULT_OPENAI_MODEL: &str = "deepseek-v4-pro";
2727
const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/beta";
2828
const DEFAULT_NVIDIA_NIM_BASE_URL: &str = "https://integrate.api.nvidia.com/v1";
2929
const DEFAULT_OPENAI_CODEX_MODEL: &str = "gpt-5.5";
30+
const DEFAULT_ANTHROPIC_MODEL: &str = "claude-sonnet-4-6";
31+
const DEFAULT_ANTHROPIC_BASE_URL: &str = "https://api.anthropic.com";
3032
const DEFAULT_OPENAI_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api";
3133
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
3234
const DEFAULT_ATLASCLOUD_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
@@ -152,10 +154,12 @@ pub enum ProviderKind {
152154
alias = "chatgpt_codex"
153155
)]
154156
OpenaiCodex,
157+
#[serde(alias = "claude")]
158+
Anthropic,
155159
}
156160

157161
impl ProviderKind {
158-
pub const ALL: [Self; 20] = [
162+
pub const ALL: [Self; 21] = [
159163
Self::Deepseek,
160164
Self::NvidiaNim,
161165
Self::Openai,
@@ -176,6 +180,7 @@ impl ProviderKind {
176180
Self::Huggingface,
177181
Self::Together,
178182
Self::OpenaiCodex,
183+
Self::Anthropic,
179184
];
180185

181186
#[must_use]
@@ -201,6 +206,7 @@ impl ProviderKind {
201206
Self::Huggingface => "huggingface",
202207
Self::Together => "together",
203208
Self::OpenaiCodex => "openai-codex",
209+
Self::Anthropic => "anthropic",
204210
}
205211
}
206212

@@ -231,6 +237,7 @@ impl ProviderKind {
231237
"ollama" | "ollama-local" => Some(Self::Ollama),
232238
"huggingface" | "hugging-face" | "hugging_face" | "hf" => Some(Self::Huggingface),
233239
"together" | "together-ai" | "together_ai" => Some(Self::Together),
240+
"anthropic" | "claude" => Some(Self::Anthropic),
234241
"openai-codex" | "openai_codex" | "openaicodex" | "codex" | "chatgpt"
235242
| "chatgpt-codex" | "chatgpt_codex" | "chatgptcodex" => Some(Self::OpenaiCodex),
236243
_ => None,
@@ -312,6 +319,8 @@ pub struct ProvidersToml {
312319
alias = "chatgpt-codex"
313320
)]
314321
pub openai_codex: ProviderConfigToml,
322+
#[serde(default)]
323+
pub anthropic: ProviderConfigToml,
315324
}
316325

317326
/// Sibling `permissions.toml` schema.
@@ -361,6 +370,7 @@ impl ProvidersToml {
361370
ProviderKind::Huggingface => &self.huggingface,
362371
ProviderKind::Together => &self.together,
363372
ProviderKind::OpenaiCodex => &self.openai_codex,
373+
ProviderKind::Anthropic => &self.anthropic,
364374
}
365375
}
366376

@@ -385,6 +395,7 @@ impl ProvidersToml {
385395
ProviderKind::Huggingface => &mut self.huggingface,
386396
ProviderKind::Together => &mut self.together,
387397
ProviderKind::OpenaiCodex => &mut self.openai_codex,
398+
ProviderKind::Anthropic => &mut self.anthropic,
388399
}
389400
}
390401
}
@@ -2022,6 +2033,7 @@ impl ConfigToml {
20222033
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_BASE_URL.to_string(),
20232034
ProviderKind::Together => DEFAULT_TOGETHER_BASE_URL.to_string(),
20242035
ProviderKind::OpenaiCodex => DEFAULT_OPENAI_CODEX_BASE_URL.to_string(),
2036+
ProviderKind::Anthropic => DEFAULT_ANTHROPIC_BASE_URL.to_string(),
20252037
})
20262038
};
20272039
// CLI flag wins outright. Otherwise: config-file → injected secrets/env.
@@ -2454,6 +2466,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
24542466
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_MODEL,
24552467
ProviderKind::Together => DEFAULT_TOGETHER_MODEL,
24562468
ProviderKind::OpenaiCodex => DEFAULT_OPENAI_CODEX_MODEL,
2469+
ProviderKind::Anthropic => DEFAULT_ANTHROPIC_MODEL,
24572470
}
24582471
}
24592472

@@ -2479,6 +2492,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
24792492
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_BASE_URL,
24802493
ProviderKind::Together => DEFAULT_TOGETHER_BASE_URL,
24812494
ProviderKind::OpenaiCodex => DEFAULT_OPENAI_CODEX_BASE_URL,
2495+
ProviderKind::Anthropic => DEFAULT_ANTHROPIC_BASE_URL,
24822496
}
24832497
}
24842498

@@ -3231,6 +3245,8 @@ struct EnvRuntimeOverrides {
32313245
together_model: Option<String>,
32323246
openai_codex_base_url: Option<String>,
32333247
openai_codex_model: Option<String>,
3248+
anthropic_base_url: Option<String>,
3249+
anthropic_model: Option<String>,
32343250
}
32353251

32363252
impl EnvRuntimeOverrides {
@@ -3394,6 +3410,12 @@ impl EnvRuntimeOverrides {
33943410
.or_else(|_| std::env::var("CODEX_MODEL"))
33953411
.ok()
33963412
.filter(|v| !v.trim().is_empty()),
3413+
anthropic_base_url: std::env::var("ANTHROPIC_BASE_URL")
3414+
.ok()
3415+
.filter(|v| !v.trim().is_empty()),
3416+
anthropic_model: std::env::var("ANTHROPIC_MODEL")
3417+
.ok()
3418+
.filter(|v| !v.trim().is_empty()),
33973419
}
33983420
}
33993421

@@ -3422,6 +3444,7 @@ impl EnvRuntimeOverrides {
34223444
ProviderKind::Huggingface => self.huggingface_base_url.clone(),
34233445
ProviderKind::Together => self.together_base_url.clone(),
34243446
ProviderKind::OpenaiCodex => self.openai_codex_base_url.clone(),
3447+
ProviderKind::Anthropic => self.anthropic_base_url.clone(),
34253448
}
34263449
}
34273450

@@ -3441,6 +3464,7 @@ impl EnvRuntimeOverrides {
34413464
ProviderKind::Huggingface => self.huggingface_model.clone(),
34423465
ProviderKind::Together => self.together_model.clone(),
34433466
ProviderKind::OpenaiCodex => self.openai_codex_model.clone(),
3467+
ProviderKind::Anthropic => self.anthropic_model.clone(),
34443468
_ => None,
34453469
}?;
34463470

@@ -5132,10 +5156,12 @@ unix_socket_path = "/tmp/cw-hooks.sock"
51325156
);
51335157
assert!(!provider.display_name().trim().is_empty());
51345158
assert!(!provider.env_vars().is_empty());
5135-
// OpenAI Codex (ChatGPT) speaks the Responses API; every other
5136-
// built-in provider is OpenAI-compatible Chat Completions.
5159+
// OpenAI Codex (ChatGPT) speaks the Responses API and Anthropic
5160+
// speaks the native Messages API; every other built-in provider
5161+
// is OpenAI-compatible Chat Completions.
51375162
let expected_wire = match kind {
51385163
ProviderKind::OpenaiCodex => provider::WireFormat::Responses,
5164+
ProviderKind::Anthropic => provider::WireFormat::AnthropicMessages,
51395165
_ => provider::WireFormat::ChatCompletions,
51405166
};
51415167
assert_eq!(provider.wire(), expected_wire);

crates/config/src/provider.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub enum WireFormat {
2727
ChatCompletions,
2828
/// OpenAI Responses API (`/responses`).
2929
Responses,
30+
/// Native Anthropic Messages API (`/v1/messages`).
31+
AnthropicMessages,
3032
}
3133

3234
/// Static metadata for a built-in model provider.
@@ -320,6 +322,39 @@ impl Provider for OpenaiCodex {
320322
}
321323
}
322324

325+
/// Native Anthropic Messages API provider (#3014).
326+
pub struct Anthropic;
327+
328+
impl Provider for Anthropic {
329+
fn kind(&self) -> ProviderKind {
330+
ProviderKind::Anthropic
331+
}
332+
333+
fn display_name(&self) -> &'static str {
334+
"Anthropic"
335+
}
336+
337+
fn default_base_url(&self) -> &'static str {
338+
crate::DEFAULT_ANTHROPIC_BASE_URL
339+
}
340+
341+
fn default_model(&self) -> &'static str {
342+
crate::DEFAULT_ANTHROPIC_MODEL
343+
}
344+
345+
fn env_vars(&self) -> &'static [&'static str] {
346+
&["ANTHROPIC_API_KEY"]
347+
}
348+
349+
fn provider_config_key(&self) -> &'static str {
350+
"anthropic"
351+
}
352+
353+
fn wire(&self) -> WireFormat {
354+
WireFormat::AnthropicMessages
355+
}
356+
}
357+
323358
static DEEPSEEK: Deepseek = Deepseek;
324359
static NVIDIA_NIM: NvidiaNim = NvidiaNim;
325360
static OPENAI: Openai = Openai;
@@ -340,8 +375,9 @@ static OLLAMA: Ollama = Ollama;
340375
static HUGGINGFACE: Huggingface = Huggingface;
341376
static TOGETHER: Together = Together;
342377
static OPENAI_CODEX: OpenaiCodex = OpenaiCodex;
378+
static ANTHROPIC: Anthropic = Anthropic;
343379

344-
static PROVIDER_REGISTRY: [&dyn Provider; 20] = [
380+
static PROVIDER_REGISTRY: [&dyn Provider; 21] = [
345381
&DEEPSEEK,
346382
&NVIDIA_NIM,
347383
&OPENAI,
@@ -362,6 +398,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 20] = [
362398
&HUGGINGFACE,
363399
&TOGETHER,
364400
&OPENAI_CODEX,
401+
&ANTHROPIC,
365402
];
366403

367404
/// Return all built-in provider metadata entries in `ProviderKind::ALL` order.
@@ -410,5 +447,6 @@ pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
410447
ProviderKind::Huggingface => &HUGGINGFACE,
411448
ProviderKind::Together => &TOGETHER,
412449
ProviderKind::OpenaiCodex => &OPENAI_CODEX,
450+
ProviderKind::Anthropic => &ANTHROPIC,
413451
}
414452
}

crates/secrets/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ pub fn env_for(name: &str) -> Option<String> {
754754
"vllm" | "v-llm" => &["VLLM_API_KEY"],
755755
"ollama" | "ollama-local" => &["OLLAMA_API_KEY"],
756756
"openai" => &["OPENAI_API_KEY"],
757+
"anthropic" | "claude" => &["ANTHROPIC_API_KEY"],
757758
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => &["ATLASCLOUD_API_KEY"],
758759
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark"
759760
| "volcengineark" => &[

0 commit comments

Comments
 (0)