1use crate::catalog::ModelFacts;
14use crate::client::env::{self, EnvError};
15use crate::completion::options::FinalBody;
16use crate::completion::{CompletionRequest, ProviderCapabilities};
17use crate::error::EncodeError;
18use crate::operation::Completion;
19use crate::providers::internal::named_dialect;
20use crate::wire::{Body, Capabilities, Descriptor, Encoded, Framing, Mode, Secret, Wire};
21use serde::{Deserialize, Serialize};
22
23use super::completion::{
24 CacheTtl, default_max_tokens_for_model, document_source, image_source,
25 rejects_forced_tool_choice,
26};
27use super::streaming::MessagesDecoder;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Dialect {
34 pub name: &'static str,
36 pub base_url: &'static str,
38 pub api_key_env: &'static str,
40 pub base_url_env: Option<&'static str>,
43 pub request_id_header: Option<&'static str>,
45 pub quirks: Quirks,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct Quirks {
53 pub max_tokens: MaxTokens,
57 pub strict_tool_schemas: bool,
60 pub unsigned_thinking: bool,
64 pub tool_input_streaming: ToolInputStreaming,
66}
67
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ToolInputStreaming {
76 #[default]
78 Eager,
79 BetaHeader,
82 Off,
84}
85
86impl Quirks {
87 pub const fn anthropic() -> Self {
89 Self {
90 max_tokens: MaxTokens::ByModel,
91 strict_tool_schemas: true,
92 unsigned_thinking: false,
93 tool_input_streaming: ToolInputStreaming::Eager,
94 }
95 }
96
97 pub const fn gateway() -> Self {
101 Self {
102 max_tokens: MaxTokens::Fixed(4096),
103 strict_tool_schemas: false,
104 unsigned_thinking: false,
105 tool_input_streaming: ToolInputStreaming::Eager,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub enum MaxTokens {
113 ByModel,
115 Fixed(u64),
117}
118
119pub(crate) const MESSAGES_API: &str = "anthropic.messages";
121
122pub const ANTHROPIC: Dialect = Dialect {
124 name: super::PROVIDER_NAME,
125 base_url: "https://api.anthropic.com",
126 api_key_env: "ANTHROPIC_API_KEY",
127 base_url_env: Some("ANTHROPIC_BASE_URL"),
128 request_id_header: Some("request-id"),
129 quirks: Quirks::anthropic(),
130};
131
132const ALL: &[&Dialect] = &[&ANTHROPIC, &ZAI, &MINIMAX, &MOONSHOT, &XIAOMIMIMO];
134
135pub fn all() -> impl Iterator<Item = &'static Dialect> {
137 ALL.iter().copied()
138}
139
140impl Dialect {
141 pub fn by_name(name: &str) -> Option<Self> {
143 all().copied().find(|dialect| dialect.name == name)
144 }
145}
146
147impl Serialize for Dialect {
148 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149 let registered = Self::by_name(self.name).as_ref() == Some(self);
150 named_dialect::serialize(serializer, "Anthropic", self.name, registered)
151 }
152}
153
154impl<'de> Deserialize<'de> for Dialect {
155 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
156 named_dialect::deserialize(deserializer, "Anthropic", Self::by_name)
157 }
158}
159
160pub const fn compatible(
163 name: &'static str,
164 base_url: &'static str,
165 api_key_env: &'static str,
166 base_url_env: Option<&'static str>,
167) -> Dialect {
168 Dialect {
169 name,
170 base_url,
171 api_key_env,
172 base_url_env,
173 request_id_header: Some("request-id"),
174 quirks: Quirks::gateway(),
175 }
176}
177
178impl Dialect {
179 pub fn default_max_tokens(&self, facts: &ModelFacts, model: &str) -> Option<u64> {
182 match self.quirks.max_tokens {
183 MaxTokens::ByModel => default_max_tokens_for_model(facts, model),
184 MaxTokens::Fixed(tokens) => Some(tokens),
185 }
186 }
187}
188
189pub const ZAI: Dialect = compatible(
191 crate::providers::zai::PROVIDER_NAME,
192 "https://api.z.ai/api/anthropic",
193 "ZAI_API_KEY",
194 Some("ZAI_ANTHROPIC_API_BASE"),
195);
196
197pub const MINIMAX: Dialect = compatible(
199 crate::providers::minimax::PROVIDER_NAME,
200 "https://api.minimax.io/anthropic",
201 "MINIMAX_API_KEY",
202 Some("MINIMAX_ANTHROPIC_API_BASE"),
203);
204
205pub const MOONSHOT: Dialect = Dialect {
208 quirks: Quirks {
209 unsigned_thinking: true,
210 ..Quirks::gateway()
211 },
212 ..compatible(
213 crate::providers::moonshot::PROVIDER_NAME,
214 "https://api.moonshot.ai/anthropic",
215 "MOONSHOT_API_KEY",
216 Some("MOONSHOT_ANTHROPIC_API_BASE"),
217 )
218};
219
220pub const XIAOMIMIMO: Dialect = compatible(
222 crate::providers::xiaomimimo::PROVIDER_NAME,
223 "https://api.xiaomimimo.com/anthropic",
224 "XIAOMI_MIMO_API_KEY",
225 Some("XIAOMI_MIMO_ANTHROPIC_API_BASE"),
226);
227
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(deny_unknown_fields)]
233pub struct AnthropicConfig {
234 pub api_key: Secret,
236 pub base_url: String,
238 pub version: String,
240 pub betas: Vec<String>,
242 pub dialect: Dialect,
244 #[serde(default, skip_serializing_if = "Addressing::is_body")]
246 pub addressing: Addressing,
247}
248
249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum Addressing {
253 #[default]
256 Body,
257 Url,
261}
262
263impl Addressing {
264 fn is_body(&self) -> bool {
266 *self == Self::Body
267 }
268}
269
270impl AnthropicConfig {
271 pub fn new(api_key: impl Into<Secret>) -> Self {
273 Self::with_key(&ANTHROPIC, api_key)
274 }
275
276 pub fn with_key(dialect: &Dialect, api_key: impl Into<Secret>) -> Self {
280 Self {
281 api_key: api_key.into(),
282 base_url: normalize_base_url(dialect.base_url),
283 version: super::completion::ANTHROPIC_VERSION_LATEST.to_owned(),
284 betas: Vec::new(),
285 dialect: *dialect,
286 addressing: Addressing::Body,
287 }
288 }
289
290 pub fn from_env() -> Result<Self, EnvError> {
292 Self::from_env_with(&ANTHROPIC)
293 }
294
295 pub fn from_env_with(dialect: &Dialect) -> Result<Self, EnvError> {
297 Self::from_env_with_credential(dialect, env::required(dialect.api_key_env)?)
298 }
299
300 pub(crate) fn from_env_with_credential(
303 dialect: &Dialect,
304 api_key: impl Into<Secret>,
305 ) -> Result<Self, EnvError> {
306 let mut provider = Self::with_key(dialect, api_key);
307 if let Some(name) = dialect.base_url_env
308 && let Some(base_url) = env::optional(name)?
309 {
310 provider.base_url = normalize_base_url(&base_url);
311 }
312 Ok(provider)
313 }
314
315 pub fn with_version(mut self, version: impl Into<String>) -> Self {
317 self.version = version.into();
318 self
319 }
320
321 pub fn with_beta(mut self, beta: impl Into<String>) -> Self {
323 self.betas.push(beta.into());
324 self
325 }
326
327 pub fn with_base_url(mut self, base_url: impl AsRef<str>) -> Self {
329 self.base_url = normalize_base_url(base_url.as_ref());
330 self
331 }
332
333 pub fn completion(&self, model: impl Into<String>) -> Messages {
335 let model = model.into();
336 Messages {
337 default_max_tokens: self
338 .dialect
339 .default_max_tokens(ModelFacts::builtin(), &model),
340 provider: self.clone(),
341 model,
342 prompt_caching: false,
343 static_prefix_cache_ttl: None,
344 strict_tools: false,
345 tool_input_streaming: self.dialect.quirks.tool_input_streaming,
346 facts: ModelFacts::default(),
347 }
348 }
349
350 pub(super) fn headers(&self, builder: http::request::Builder) -> http::request::Builder {
352 self.headers_with(builder, &self.betas)
353 }
354
355 fn headers_with(
357 &self,
358 builder: http::request::Builder,
359 betas: &[String],
360 ) -> http::request::Builder {
361 let builder = builder
362 .header("x-api-key", self.api_key.expose())
363 .header("anthropic-version", &self.version);
364 if betas.is_empty() {
365 builder
366 } else {
367 builder.header("anthropic-beta", betas.join(","))
368 }
369 }
370
371 fn betas_with(&self, extra: &[&str]) -> Vec<String> {
374 let mut betas = self.betas.clone();
375 for extra in extra {
376 if !betas.iter().any(|beta| beta == extra) {
377 betas.push((*extra).to_owned());
378 }
379 }
380 betas
381 }
382}
383
384pub fn normalize_base_url(base_url: &str) -> String {
387 let trimmed = base_url.trim_end_matches('/');
388 for suffix in ["/v1/messages", "/messages", "/v1"] {
389 if let Some(stripped) = trimmed.strip_suffix(suffix) {
390 return stripped.to_owned();
391 }
392 }
393 trimmed.to_owned()
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct Messages {
399 pub provider: AnthropicConfig,
401 pub model: String,
403 pub default_max_tokens: Option<u64>,
407 pub prompt_caching: bool,
413 pub static_prefix_cache_ttl: Option<CacheTtl>,
416 pub strict_tools: bool,
418 #[serde(default)]
421 pub tool_input_streaming: ToolInputStreaming,
422 #[serde(skip)]
424 pub facts: ModelFacts,
425}
426
427impl Messages {
428 pub fn with_facts(mut self, facts: ModelFacts) -> Self {
433 self.default_max_tokens = self
434 .provider
435 .dialect
436 .default_max_tokens(&facts, &self.model);
437 self.facts = facts;
438 self
439 }
440
441 pub fn with_default_max_tokens(mut self, tokens: u64) -> Self {
443 self.default_max_tokens = Some(tokens);
444 self
445 }
446
447 pub fn with_prompt_caching(mut self) -> Self {
452 self.prompt_caching = true;
453 self
454 }
455
456 pub fn with_static_prefix_cache_ttl(mut self, ttl: CacheTtl) -> Self {
479 self.static_prefix_cache_ttl = Some(ttl);
480 self
481 }
482
483 pub fn with_strict_tools(mut self) -> Self {
498 self.strict_tools = true;
499 self
500 }
501
502 pub fn with_tool_input_streaming(mut self, streaming: ToolInputStreaming) -> Self {
518 self.tool_input_streaming = streaming;
519 self
520 }
521}
522
523#[derive(Debug, Clone, PartialEq)]
526#[non_exhaustive]
527pub struct Prepared {
528 pub model: String,
530 pub body: FinalBody,
532 pub betas: Vec<String>,
535}
536
537impl Messages {
538 pub fn encode_with(
549 &self,
550 request: CompletionRequest,
551 mode: Mode,
552 envelope: impl FnOnce(Prepared) -> Result<http::Request<Body>, EncodeError>,
553 ) -> Result<Encoded, EncodeError> {
554 let model = request.model.clone().unwrap_or_else(|| self.model.clone());
555 let fine_grained = !request.tools.is_empty()
556 && self.tool_input_streaming == ToolInputStreaming::BetaHeader;
557 let body = super::completion::body(self, &request, mode)?;
558 let betas: Vec<&str> = [
559 super::completion::drops_unbound_thinking(self, &model, body.get("thinking"))
560 .then_some(super::completion::THINKING_BINDING_BETA),
561 fine_grained.then_some(super::completion::FINE_GRAINED_TOOL_STREAMING_BETA),
562 ]
563 .into_iter()
564 .flatten()
565 .collect();
566 crate::providers::internal::trace_json(
567 crate::providers::internal::LogTarget::Completions,
568 "Anthropic completion request",
569 &body,
570 );
571 let request = envelope(Prepared {
572 model,
573 body,
574 betas: self.provider.betas_with(&betas),
575 })?;
576 Ok(Encoded::new(
577 request,
578 match mode {
579 Mode::Unary => Framing::Whole,
580 Mode::Streaming => Framing::Sse,
581 },
582 )
583 .with_request_id_header(self.provider.dialect.request_id_header)
584 .with_projection(MessagesDecoder::project))
585 }
586}
587
588impl Wire for Messages {
589 type Op = Completion;
590 type Payload = crate::wire::Encoded;
591 type Frame = crate::wire::WireFrame;
592 type Event = super::streaming::MessagesEvent;
593 type Decoder<'id> = MessagesDecoder;
594 type Reassembler = super::streaming::document::Message;
595
596 fn describe(&self) -> Descriptor<'_> {
598 Descriptor::new(self.provider.dialect.name)
599 .model(self.model.as_str())
600 .facts(&self.facts)
601 .capabilities(Capabilities::completion(
602 ProviderCapabilities::default()
603 .with_native_output_tool_composition(true)
604 .with_forced_tool_choice_rejected(rejects_forced_tool_choice(
605 &self.facts,
606 &self.model,
607 )),
608 ))
609 .replay(self)
610 }
611
612 fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
613 self.encode_with(request, mode, |prepared| {
614 Ok(self
615 .provider
616 .headers_with(
617 http::Request::post(format!("{}/v1/messages", self.provider.base_url)),
618 &prepared.betas,
619 )
620 .header(http::header::CONTENT_TYPE, "application/json")
621 .body(prepared.body.into_body())?)
622 })
623 }
624
625 fn decoder<'id>(&self) -> Self::Decoder<'id> {
626 MessagesDecoder::new(self.provider.dialect.quirks.unsigned_thinking)
627 }
628}
629
630impl crate::completion::ReplayTarget for Messages {
631 fn map_options(
633 &self,
634 request: &CompletionRequest,
635 fields: crate::completion::options::OptionFields<'_>,
636 ) -> crate::completion::options::OptionMap {
637 super::options::map_options(self, request, fields)
638 }
639
640 fn facts(&self) -> Option<&ModelFacts> {
641 Some(&self.facts)
642 }
643
644 fn api(&self) -> crate::message::Api {
645 crate::message::Api::from_static(MESSAGES_API)
646 }
647
648 fn provider(&self) -> &str {
649 self.provider.dialect.name
650 }
651
652 fn model(&self) -> &str {
653 &self.model
654 }
655
656 fn accepts(&self, model: &str) -> crate::completion::Accepts {
662 let rule: Option<fn(&str) -> bool> = match self.provider.dialect.name {
663 name if name == ANTHROPIC.name => None,
664 name if name == ZAI.name => Some(crate::providers::zai::reads_images),
665 name if name == MOONSHOT.name => Some(crate::providers::moonshot::reads_images),
666 name if name == MINIMAX.name => Some(crate::providers::minimax::reads_images),
667 name if name == XIAOMIMIMO.name => Some(crate::providers::xiaomimimo::reads_images),
668 _ => Some(|_| true),
669 };
670 let images = rule.is_none_or(|rule| {
671 self.facts
672 .reads_images_or(self.provider.dialect.name, model, rule)
673 });
674 crate::completion::Accepts {
675 user_images: images,
676 assistant_images: false,
677 tool_result_images: images,
678 tools: true,
679 foreign_reasoning: false,
681 }
682 }
683
684 fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
689 use crate::completion::{Media, Place};
690 match media {
691 Media::Image(image, Place::User | Place::ToolResult) => image_source(image).is_some(),
692 Media::Document(document) => document_source(document).is_some(),
693 Media::Image(_, Place::Assistant) | Media::Audio(_) | Media::Video(_) => false,
694 }
695 }
696
697 fn normalize_tool_call_id(
699 &self,
700 id: &str,
701 _model: &str,
702 _source: Option<&crate::message::Origin>,
703 ) -> String {
704 crate::providers::internal::wire_ids::legal_call_id(id, 64)
705 }
706
707 fn identity(&self, item: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
710 item.get("caller")
711 .filter(|_| item.get("type").and_then(serde_json::Value::as_str) == Some("tool_use"))
712 .map(|caller| serde_json::Map::from_iter([("caller".to_owned(), caller.clone())]))
713 .unwrap_or_default()
714 }
715
716 fn later_system(&self, model: &str) -> crate::completion::LaterSystem {
719 if super::completion::takes_mid_conversation_system(&self.facts, model) {
720 crate::completion::LaterSystem::InPlace
721 } else {
722 crate::completion::LaterSystem::Leading
723 }
724 }
725
726 fn binds_context(&self, model: &str) -> bool {
731 self.provider.dialect.name == ANTHROPIC.name
732 && super::completion::binds_context(&self.facts, model)
733 }
734
735 fn drops_unbound_items(&self, request: &CompletionRequest) -> bool {
740 let model = request.model.as_deref().unwrap_or(&self.model);
741 let thinking = crate::completion::options::param(self, request, "thinking");
742 super::completion::drops_unbound_thinking(self, model, thinking.as_ref())
743 }
744
745 fn sends_alone(&self, block: &crate::message::AssistantContent) -> bool {
747 let ids = crate::providers::internal::wire_ids::WireIds::default();
748 super::completion::assistant_part(block, self, &ids).is_some()
749 }
750
751 fn call_id_slot(&self) -> Option<&'static str> {
752 Some("/id")
753 }
754
755 fn hosted_pair(
758 &self,
759 item: &serde_json::Value,
760 ) -> Option<(crate::completion::Pairing, String)> {
761 use crate::completion::Pairing;
762 let kind = item.get("type")?.as_str()?;
763 let (side, key) = if kind.ends_with("_tool_use") {
764 (Pairing::Use, "id")
765 } else if kind.ends_with("_tool_result") {
766 (Pairing::Result, "tool_use_id")
767 } else {
768 return None;
769 };
770 Some((side, item.get(key)?.as_str()?.to_owned()))
771 }
772}
773
774#[cfg(test)]
775mod tests;