Skip to main content

rig_core/providers/anthropic/
wire.rs

1//! Configuration and wires for Messages-format completion, model listing,
2//! and credential verification. [`Dialect`] describes endpoint defaults and capabilities.
3//!
4//! ```no_run
5//! use rig_core::providers::anthropic::{Anthropic, completion::CLAUDE_SONNET_4_6};
6//!
7//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
8//! let wire = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
9//! # Ok(())
10//! # }
11//! ```
12
13use 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/// Endpoint defaults and capabilities for a Messages-format provider.
30/// Serializes by registered name. Serialization rejects modified or unregistered
31/// dialects; deserialization rejects unknown names.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Dialect {
34    /// The provider descriptor name, as records and telemetry spell it.
35    pub name: &'static str,
36    /// The default base URL.
37    pub base_url: &'static str,
38    /// The environment variable carrying the API key.
39    pub api_key_env: &'static str,
40    /// The environment variable overriding the base URL, when the provider
41    /// documents one.
42    pub base_url_env: Option<&'static str>,
43    /// The reply header carrying the provider's transport request id.
44    pub request_id_header: Option<&'static str>,
45    /// Everything that is not identity.
46    pub quirks: Quirks,
47}
48
49/// Token defaults, schema capabilities and request envelope for a
50/// Messages-format dialect.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[non_exhaustive]
53pub struct Quirks {
54    /// How `max_tokens` is defaulted when the caller sets none. Anthropic
55    /// requires the field, so a wire that cannot supply it fails the request
56    /// rather than sending one the API rejects.
57    pub max_tokens: MaxTokens,
58    /// Whether the provider implements Anthropic's constrained tool schemas.
59    /// A gateway that does not leaves Rig-generated tools unchanged.
60    pub strict_tool_schemas: bool,
61    /// Whether the provider takes thinking back without a signature, as
62    /// Kimi does. Elsewhere unsigned thinking
63    /// replays as text.
64    pub unsigned_thinking: bool,
65    /// How a request with tools asks the provider to stream tool input.
66    pub tool_input_streaming: ToolInputStreaming,
67    /// How the credential is sent.
68    pub auth: Auth,
69    /// The header every completion request carries a session id in: the
70    /// request's cache key when it has one, else a fresh id.
71    pub session_header: Option<&'static str>,
72    /// Whether `output_schema` maps to `output_config.format`. A dialect
73    /// that does not read it gets no schema, with a warning. An agent run
74    /// with tools then enforces the output through its output tool; one
75    /// without tools gets unconstrained output.
76    pub supports_output_format: bool,
77    /// Whether the catalog's per-token API prices apply to replies. A
78    /// server that is usually local has none.
79    pub catalog_priced: bool,
80    /// Whether a reply's cache writes are priced by the TTL split it
81    /// reports, at Anthropic's list rates: five-minute writes at the
82    /// catalog's write rate, one-hour writes at twice the input rate.
83    pub cache_ttl_pricing: bool,
84}
85
86/// How a Messages-format provider takes its credential.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub enum Auth {
89    /// The `x-api-key` header, always sent.
90    #[default]
91    ApiKey,
92    /// `Authorization: Bearer`, sent only when there is a credential, for a
93    /// local server that needs none and a hosted one that refuses
94    /// `x-api-key`.
95    OptionalBearer,
96}
97
98/// How a request with tools asks a Messages-format provider to stream each
99/// call's input as the model writes it. Without it, Anthropic holds a long
100/// argument value back and sends it in one burst. Unary requests ask too, so
101/// a tool definition, which a prompt cache keys on, is the same whether a
102/// turn streams.
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum ToolInputStreaming {
106    /// `eager_input_streaming: true` on each Rig tool definition.
107    #[default]
108    Eager,
109    /// The `fine-grained-tool-streaming-2025-05-14` beta flag, for a
110    /// provider that rejects the per-tool field.
111    BetaHeader,
112    /// Neither: the provider streams tool input as it chooses.
113    Off,
114}
115
116impl Quirks {
117    /// Anthropic's own contract.
118    pub const fn anthropic() -> Self {
119        Self {
120            max_tokens: MaxTokens::ByModel,
121            strict_tool_schemas: true,
122            unsigned_thinking: false,
123            tool_input_streaming: ToolInputStreaming::Eager,
124            auth: Auth::ApiKey,
125            session_header: None,
126            supports_output_format: true,
127            catalog_priced: true,
128            cache_ttl_pricing: true,
129        }
130    }
131
132    /// Default to 4096 output tokens without constrained tool schemas, with
133    /// eager tool input, which a Messages-format provider takes unless it is
134    /// known to reject it.
135    pub const fn gateway() -> Self {
136        Self {
137            max_tokens: MaxTokens::Fixed(4096),
138            strict_tool_schemas: false,
139            unsigned_thinking: false,
140            tool_input_streaming: ToolInputStreaming::Eager,
141            auth: Auth::ApiKey,
142            session_header: None,
143            supports_output_format: true,
144            catalog_priced: true,
145            cache_ttl_pricing: false,
146        }
147    }
148}
149
150/// Where a dialect's `max_tokens` default comes from.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub enum MaxTokens {
153    /// Anthropic's published per-model output limits.
154    ByModel,
155    /// One ceiling for every model, which is all a gateway documents.
156    Fixed(u64),
157}
158
159/// The API name the Messages wire reports for every dialect.
160pub(crate) const MESSAGES_API: &str = "anthropic.messages";
161
162/// Anthropic itself.
163pub const ANTHROPIC: Dialect = Dialect {
164    name: super::PROVIDER_NAME,
165    base_url: "https://api.anthropic.com",
166    api_key_env: "ANTHROPIC_API_KEY",
167    base_url_env: Some("ANTHROPIC_BASE_URL"),
168    request_id_header: Some("request-id"),
169    quirks: Quirks::anthropic(),
170};
171
172/// Registered Messages-format dialects in declaration order.
173const ALL: &[&Dialect] = &[
174    &ANTHROPIC,
175    &ZAI,
176    &MINIMAX,
177    &MOONSHOT,
178    &XIAOMIMIMO,
179    &OLLAMA,
180    &OPENCODE,
181    &OPENCODE_GO,
182];
183
184/// Every Messages-format dialect this build knows, in declaration order.
185pub fn all() -> impl Iterator<Item = &'static Dialect> {
186    ALL.iter().copied()
187}
188
189impl Dialect {
190    /// The dialect this crate ships under `name`.
191    pub fn by_name(name: &str) -> Option<Self> {
192        all().copied().find(|dialect| dialect.name == name)
193    }
194}
195
196impl Serialize for Dialect {
197    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198        let registered = Self::by_name(self.name).as_ref() == Some(self);
199        named_dialect::serialize(serializer, "Anthropic", self.name, registered)
200    }
201}
202
203impl<'de> Deserialize<'de> for Dialect {
204    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
205        named_dialect::deserialize(deserializer, "Anthropic", Self::by_name)
206    }
207}
208
209/// Build a Messages-format dialect with `request-id` response headers and
210/// [`Quirks::gateway`] defaults. Only registered dialects support serialization.
211pub const fn compatible(
212    name: &'static str,
213    base_url: &'static str,
214    api_key_env: &'static str,
215    base_url_env: Option<&'static str>,
216) -> Dialect {
217    Dialect {
218        name,
219        base_url,
220        api_key_env,
221        base_url_env,
222        request_id_header: Some("request-id"),
223        quirks: Quirks::gateway(),
224    }
225}
226
227impl Dialect {
228    /// The `max_tokens` this dialect defaults `model` to, by the output
229    /// limit `facts` give it where the dialect defaults by model.
230    pub fn default_max_tokens(&self, facts: &ModelFacts, model: &str) -> Option<u64> {
231        match self.quirks.max_tokens {
232            MaxTokens::ByModel => default_max_tokens_for_model(facts, model),
233            MaxTokens::Fixed(tokens) => Some(tokens),
234        }
235    }
236}
237
238/// Z.AI's Anthropic-format endpoint.
239pub const ZAI: Dialect = compatible(
240    crate::providers::zai::PROVIDER_NAME,
241    "https://api.z.ai/api/anthropic",
242    "ZAI_API_KEY",
243    Some("ZAI_ANTHROPIC_API_BASE"),
244);
245
246/// MiniMax's Anthropic-format endpoint.
247pub const MINIMAX: Dialect = compatible(
248    crate::providers::minimax::PROVIDER_NAME,
249    "https://api.minimax.io/anthropic",
250    "MINIMAX_API_KEY",
251    Some("MINIMAX_ANTHROPIC_API_BASE"),
252);
253
254/// Moonshot's Anthropic-format endpoint, which sends and takes back
255/// thinking without a signature.
256pub const MOONSHOT: Dialect = Dialect {
257    quirks: Quirks {
258        unsigned_thinking: true,
259        ..Quirks::gateway()
260    },
261    ..compatible(
262        crate::providers::moonshot::PROVIDER_NAME,
263        "https://api.moonshot.ai/anthropic",
264        "MOONSHOT_API_KEY",
265        Some("MOONSHOT_ANTHROPIC_API_BASE"),
266    )
267};
268
269/// Xiaomi MiMo's Anthropic-format endpoint.
270pub const XIAOMIMIMO: Dialect = compatible(
271    crate::providers::xiaomimimo::PROVIDER_NAME,
272    "https://api.xiaomimimo.com/anthropic",
273    "XIAOMI_MIMO_API_KEY",
274    Some("XIAOMI_MIMO_ANTHROPIC_API_BASE"),
275);
276
277/// An Ollama daemon's Messages-format endpoint. A local daemon takes no
278/// credential; Ollama's cloud takes its key only as a bearer token. Its
279/// thinking carries no signature, and it constrains no output to a schema.
280/// It lists no models in Anthropic's shape: list them through the
281/// [`Ollama`](crate::providers::ollama::Ollama) client.
282pub const OLLAMA: Dialect = Dialect {
283    quirks: Quirks {
284        auth: Auth::OptionalBearer,
285        unsigned_thinking: true,
286        // Its `output_config` reads only `effort`.
287        supports_output_format: false,
288        // A daemon is usually local; the catalog's prices are Ollama Cloud's.
289        catalog_priced: false,
290        ..Quirks::gateway()
291    },
292    ..compatible(
293        crate::providers::ollama::PROVIDER_NAME,
294        "http://localhost:11434",
295        "OLLAMA_API_KEY",
296        Some("OLLAMA_API_BASE_URL"),
297    )
298};
299
300/// OpenCode Zen's Messages-format endpoint, for the models its catalog
301/// serves there.
302pub const OPENCODE: Dialect = compatible(
303    crate::providers::opencode::ZEN,
304    "https://opencode.ai/inference/anthropic",
305    "OPENCODE_API_KEY",
306    Some("OPENCODE_ANTHROPIC_BASE_URL"),
307);
308
309/// OpenCode Go's Messages-format endpoint, which refuses a request without
310/// an `x-opencode-session` header.
311pub const OPENCODE_GO: Dialect = Dialect {
312    quirks: Quirks {
313        session_header: Some(crate::providers::opencode::SESSION_HEADER),
314        ..Quirks::gateway()
315    },
316    ..compatible(
317        crate::providers::opencode::GO,
318        "https://opencode.ai/inference/go/anthropic",
319        "OPENCODE_API_KEY",
320        Some("OPENCODE_GO_ANTHROPIC_BASE_URL"),
321    )
322};
323
324/// The settings of a Messages-format provider: serializable, and the
325/// credential is never serialized. [`connect`](Self::connect) puts it on a
326/// transport as an [`Anthropic`](super::Anthropic) client.
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct AnthropicConfig {
330    /// The API key, sent as the dialect's [`Auth`] says.
331    pub api_key: Secret,
332    /// The API root, without a trailing `/v1`.
333    pub base_url: String,
334    /// The `anthropic-version` header.
335    pub version: String,
336    /// The `anthropic-beta` flags, joined with commas when non-empty.
337    pub betas: Vec<String>,
338    /// Which Messages-format provider this is.
339    pub dialect: Dialect,
340    /// Where a request names its model and API version.
341    #[serde(default, skip_serializing_if = "Addressing::is_body")]
342    pub addressing: Addressing,
343}
344
345/// Where a Messages request names its model and API version.
346#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub enum Addressing {
349    /// `model` in the body and `version` as the `anthropic-version` header,
350    /// as Anthropic's own API takes them.
351    #[default]
352    Body,
353    /// No `model` in the body, because the URL names it, and `version` as
354    /// the body's `anthropic_version`, as cloud platforms take them. Such a
355    /// platform builds its URL with [`Messages::encode_with`].
356    Url,
357}
358
359impl Addressing {
360    /// Whether this is Anthropic's own [`Addressing::Body`].
361    fn is_body(&self) -> bool {
362        *self == Self::Body
363    }
364}
365
366impl AnthropicConfig {
367    /// Anthropic itself, with default settings.
368    pub fn new(api_key: impl Into<Secret>) -> Self {
369        Self::with_key(&ANTHROPIC, api_key)
370    }
371
372    /// `dialect` with `api_key`, at the dialect's default base URL and
373    /// with default settings. The base URL is normalized as
374    /// [`with_base_url`](Self::with_base_url) does.
375    pub fn with_key(dialect: &Dialect, api_key: impl Into<Secret>) -> Self {
376        Self {
377            api_key: api_key.into(),
378            base_url: normalize_base_url(dialect.base_url),
379            version: super::completion::ANTHROPIC_VERSION_LATEST.to_owned(),
380            betas: Vec::new(),
381            dialect: *dialect,
382            addressing: Addressing::Body,
383        }
384    }
385
386    /// Anthropic from `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`.
387    pub fn from_env() -> Result<Self, EnvError> {
388        Self::from_env_with(&ANTHROPIC)
389    }
390
391    /// A Messages-format provider from the variables its dialect names. A
392    /// dialect whose credential is optional ([`Auth::OptionalBearer`]) takes
393    /// an unset key as none.
394    pub fn from_env_with(dialect: &Dialect) -> Result<Self, EnvError> {
395        let api_key = match dialect.quirks.auth {
396            Auth::ApiKey => env::required(dialect.api_key_env)?,
397            Auth::OptionalBearer => env::optional(dialect.api_key_env)?.unwrap_or_default(),
398        };
399        Self::from_env_with_credential(dialect, api_key)
400    }
401
402    /// `dialect` with `api_key` and the base URL its environment variable
403    /// names, if set.
404    pub(crate) fn from_env_with_credential(
405        dialect: &Dialect,
406        api_key: impl Into<Secret>,
407    ) -> Result<Self, EnvError> {
408        let mut provider = Self::with_key(dialect, api_key);
409        if let Some(name) = dialect.base_url_env
410            && let Some(base_url) = env::optional(name)?
411        {
412            provider.base_url = normalize_base_url(&base_url);
413        }
414        Ok(provider)
415    }
416
417    /// Pin the `anthropic-version` header.
418    pub fn with_version(mut self, version: impl Into<String>) -> Self {
419        self.version = version.into();
420        self
421    }
422
423    /// Request an `anthropic-beta` flag.
424    pub fn with_beta(mut self, beta: impl Into<String>) -> Self {
425        self.betas.push(beta.into());
426        self
427    }
428
429    /// Point the wire at another API root.
430    pub fn with_base_url(mut self, base_url: impl AsRef<str>) -> Self {
431        self.base_url = normalize_base_url(base_url.as_ref());
432        self
433    }
434
435    /// The Messages wire for `model`.
436    pub fn completion(&self, model: impl Into<String>) -> Messages {
437        let model = model.into();
438        Messages {
439            default_max_tokens: self
440                .dialect
441                .default_max_tokens(ModelFacts::builtin(), &model),
442            provider: self.clone(),
443            model,
444            prompt_caching: false,
445            static_prefix_cache_ttl: None,
446            strict_tools: false,
447            tool_input_streaming: self.dialect.quirks.tool_input_streaming,
448            facts: ModelFacts::default(),
449        }
450    }
451
452    /// The request headers every Messages-format endpoint takes.
453    pub(super) fn headers(&self, builder: http::request::Builder) -> http::request::Builder {
454        self.headers_with(builder, &self.betas)
455    }
456
457    /// [`Self::headers`], with `betas` as the `anthropic-beta` flags.
458    fn headers_with(
459        &self,
460        builder: http::request::Builder,
461        betas: &[String],
462    ) -> http::request::Builder {
463        let builder = match self.dialect.quirks.auth {
464            Auth::ApiKey => builder.header("x-api-key", self.api_key.expose()),
465            Auth::OptionalBearer if self.api_key.is_empty() => builder,
466            Auth::OptionalBearer => builder.header(
467                http::header::AUTHORIZATION,
468                format!("Bearer {}", self.api_key.expose()),
469            ),
470        };
471        let builder = builder.header("anthropic-version", &self.version);
472        if betas.is_empty() {
473            builder
474        } else {
475            builder.header("anthropic-beta", betas.join(","))
476        }
477    }
478
479    /// The configured `anthropic-beta` flags, then each of `extra` not
480    /// among them.
481    fn betas_with(&self, extra: &[&str]) -> Vec<String> {
482        let mut betas = self.betas.clone();
483        for extra in extra {
484            if !betas.iter().any(|beta| beta == extra) {
485                betas.push((*extra).to_owned());
486            }
487        }
488        betas
489    }
490}
491
492/// Trim the suffixes a user may paste from the API docs, so a base URL that
493/// already names the endpoint does not produce `/v1/messages/v1/messages`.
494pub fn normalize_base_url(base_url: &str) -> String {
495    let trimmed = base_url.trim_end_matches('/');
496    for suffix in ["/v1/messages", "/messages", "/v1"] {
497        if let Some(stripped) = trimmed.strip_suffix(suffix) {
498            return stripped.to_owned();
499        }
500    }
501    trimmed.to_owned()
502}
503
504/// The Messages wire: `POST /v1/messages`, SSE when streamed.
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506pub struct Messages {
507    /// The provider this wire speaks to.
508    pub provider: AnthropicConfig,
509    /// The model to address.
510    pub model: String,
511    /// What `max_tokens` defaults to when the caller sets none. Anthropic
512    /// requires the field; `None` means a request without one is rejected
513    /// here rather than by the API.
514    pub default_max_tokens: Option<u64>,
515    /// Manual prompt caching: `cache_control` breakpoints on the system
516    /// prompt, the last tool definition, and the last content block of the
517    /// last message. The request's
518    /// [`cache`](crate::completion::GenerationOptions::cache) sets the
519    /// top-level marker and its TTL.
520    pub prompt_caching: bool,
521    /// TTL for the static prefix (tools + system), independent of the
522    /// conversation tail. `None` inherits the top-level TTL.
523    pub static_prefix_cache_ttl: Option<CacheTtl>,
524    /// Whether Rig-generated tools request the provider's strict validation.
525    pub strict_tools: bool,
526    /// How a request with tools asks for tool input as it is written; the
527    /// dialect's by default.
528    #[serde(default)]
529    pub tool_input_streaming: ToolInputStreaming,
530    /// The model facts the encoder reads and replies are priced by.
531    #[serde(skip)]
532    pub facts: ModelFacts,
533}
534
535impl Messages {
536    /// The same wire, encoding with `facts` and pricing its replies by
537    /// them. The `max_tokens` a request without one defaults to becomes the
538    /// dialect's default for the model under `facts`; set another with
539    /// [`Self::with_default_max_tokens`] after this.
540    pub fn with_facts(mut self, facts: ModelFacts) -> Self {
541        self.default_max_tokens = self
542            .provider
543            .dialect
544            .default_max_tokens(&facts, &self.model);
545        self.facts = facts;
546        self
547    }
548
549    /// Set the `max_tokens` a request without one defaults to.
550    pub fn with_default_max_tokens(mut self, tokens: u64) -> Self {
551        self.default_max_tokens = Some(tokens);
552        self
553    }
554
555    /// Enable cache breakpoints on the system prompt, final tool, and final message block.
556    /// With a request [`cache`](crate::completion::GenerationOptions::cache), the
557    /// provider owns the moving message breakpoint and the markers take its TTL.
558    /// Existing tool markers are preserved and count toward the four-breakpoint budget.
559    pub fn with_prompt_caching(mut self) -> Self {
560        self.prompt_caching = true;
561        self
562    }
563
564    /// Cache the static prefix (tool definitions and system prompt) at its
565    /// own TTL, independent of the conversation tail's.
566    ///
567    /// Mark the final tool definition and system prompt at `ttl` without
568    /// changing the conversation tail's TTL.
569    ///
570    /// ```no_run
571    /// use rig_core::providers::anthropic::completion::{CLAUDE_SONNET_4_6, CacheTtl};
572    /// use rig_core::providers::anthropic::Anthropic;
573    ///
574    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
575    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
576    /// messages.wire = messages.wire.with_static_prefix_cache_ttl(CacheTtl::OneHour);
577    /// # Ok(())
578    /// # }
579    /// ```
580    ///
581    /// One-hour markers must precede five-minute markers. A five-minute
582    /// prefix under a request with
583    /// [`CacheRetention::Long`](crate::completion::CacheRetention::Long) fails
584    /// during encoding.
585    /// Each marker must meet the model's minimum cacheable prompt length.
586    pub fn with_static_prefix_cache_ttl(mut self, ttl: CacheTtl) -> Self {
587        self.static_prefix_cache_ttl = Some(ttl);
588        self
589    }
590
591    /// Request Anthropic's strict tool use for every Rig-generated tool.
592    ///
593    /// Anthropic constrains tool inputs to the supported JSON Schema subset
594    /// when `strict: true` is present on a tool definition. Rig sanitizes
595    /// each generated schema for that subset and leaves provider-specific
596    /// tools supplied through `additional_params` unchanged. Unsupported
597    /// validation keywords are retained only as model guidance in schema
598    /// descriptions; neither Anthropic nor Rig enforces them, so validate
599    /// tool inputs before execution when those constraints matter.
600    ///
601    /// Anthropic caches compiled schemas for up to 24 hours: do not include
602    /// PHI in schema property names, enum or const values, or regex
603    /// patterns. A dialect that does not implement constrained tool schemas
604    /// ignores this.
605    pub fn with_strict_tools(mut self) -> Self {
606        self.strict_tools = true;
607        self
608    }
609
610    /// Choose how a request with tools asks for tool input as the model
611    /// writes it, for a gateway whose support differs from its dialect's:
612    /// [`ToolInputStreaming::BetaHeader`] for one that rejects the per-tool
613    /// field, [`ToolInputStreaming::Off`] for one that rejects both.
614    ///
615    /// ```no_run
616    /// use rig_core::providers::anthropic::{Anthropic, completion::CLAUDE_SONNET_4_6};
617    /// use rig_core::providers::anthropic::ToolInputStreaming;
618    ///
619    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
620    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
621    /// messages.wire = messages.wire.with_tool_input_streaming(ToolInputStreaming::BetaHeader);
622    /// # Ok(())
623    /// # }
624    /// ```
625    pub fn with_tool_input_streaming(mut self, streaming: ToolInputStreaming) -> Self {
626        self.tool_input_streaming = streaming;
627        self
628    }
629}
630
631/// A Messages request encoded but not yet addressed or authenticated: what
632/// a platform serving the Messages API under its own envelope sends.
633#[derive(Debug, Clone, PartialEq)]
634#[non_exhaustive]
635pub struct Prepared {
636    /// The model the request addresses.
637    pub model: String,
638    /// The request body.
639    pub body: FinalBody,
640    /// The `anthropic-beta` flags: the configured ones, then the ones the
641    /// request needs.
642    pub betas: Vec<String>,
643}
644
645impl Messages {
646    /// Encode `request` as [`Wire::encode`] does, with `envelope` building
647    /// the HTTP request in place of `POST /v1/messages` with an API key.
648    /// The framing, request-id header and projection stay this wire's, so
649    /// a platform that serves the Messages API at another address, with
650    /// other credentials or another body envelope, reads replies as this
651    /// wire does.
652    ///
653    /// # Errors
654    ///
655    /// When the request does not encode, or `envelope` fails.
656    pub fn encode_with(
657        &self,
658        request: CompletionRequest,
659        mode: Mode,
660        envelope: impl FnOnce(Prepared) -> Result<http::Request<Body>, EncodeError>,
661    ) -> Result<Encoded, EncodeError> {
662        let model = request.model.clone().unwrap_or_else(|| self.model.clone());
663        let fine_grained = !request.tools.is_empty()
664            && self.tool_input_streaming == ToolInputStreaming::BetaHeader;
665        let body = super::completion::body(self, &request, mode)?;
666        let betas: Vec<&str> = [
667            body.get("thinking")
668                .and_then(|thinking| thinking.get("block_binding"))
669                .is_some_and(serde_json::Value::is_object)
670                .then_some(super::completion::THINKING_BINDING_BETA),
671            fine_grained.then_some(super::completion::FINE_GRAINED_TOOL_STREAMING_BETA),
672        ]
673        .into_iter()
674        .flatten()
675        .collect();
676        crate::providers::internal::trace_json(
677            crate::providers::internal::LogTarget::Completions,
678            "Anthropic completion request",
679            &body,
680        );
681        let request = envelope(Prepared {
682            model,
683            body,
684            betas: self.provider.betas_with(&betas),
685        })?;
686        Ok(Encoded::new(
687            request,
688            match mode {
689                Mode::Unary => Framing::Whole,
690                Mode::Streaming => Framing::Sse,
691            },
692        )
693        .with_request_id_header(self.provider.dialect.request_id_header)
694        .with_projection(MessagesDecoder::project))
695    }
696}
697
698impl Wire for Messages {
699    type Op = Completion;
700    type Payload = crate::wire::Encoded;
701    type Frame = crate::wire::WireFrame;
702    type Event = super::streaming::MessagesEvent;
703    type Decoder<'id> = MessagesDecoder;
704    type Reassembler = super::streaming::document::Message;
705
706    /// Constrained output decoding does not suppress strict tool calls. A
707    /// dialect that reads no output format leaves the output to the agent's
708    /// tool.
709    fn describe(&self) -> Descriptor<'_> {
710        Descriptor::new(self.provider.dialect.name)
711            .model(self.model.as_str())
712            .facts(&self.facts)
713            .capabilities(Capabilities::completion(
714                ProviderCapabilities::default()
715                    .with_native_output_tool_composition(
716                        self.provider.dialect.quirks.supports_output_format,
717                    )
718                    .with_forced_tool_choice_rejected(rejects_forced_tool_choice(
719                        &self.facts,
720                        &self.model,
721                    )),
722            ))
723            .replay(self)
724    }
725
726    fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
727        let session = self.provider.dialect.quirks.session_header.map(|name| {
728            let id = crate::providers::internal::request_session_id(&request);
729            (name, id)
730        });
731        self.encode_with(request, mode, |prepared| {
732            let mut builder = self
733                .provider
734                .headers_with(
735                    http::Request::post(format!("{}/v1/messages", self.provider.base_url)),
736                    &prepared.betas,
737                )
738                .header(http::header::CONTENT_TYPE, "application/json");
739            if let Some((name, id)) = session {
740                builder = builder.header(name, id);
741            }
742            Ok(builder.body(prepared.body.into_body())?)
743        })
744    }
745
746    fn decoder<'id>(&self) -> Self::Decoder<'id> {
747        MessagesDecoder::new(self.provider.dialect.quirks)
748    }
749}
750
751impl crate::completion::ReplayTarget for Messages {
752    /// Whether the catalog prices this dialect's replies
753    /// ([`Quirks::catalog_priced`]).
754    fn catalog_priced(&self) -> bool {
755        self.provider.dialect.quirks.catalog_priced
756    }
757
758    /// Section 6.1 of the typed-options design, by dialect and model class.
759    fn map_options(
760        &self,
761        request: &CompletionRequest,
762        fields: crate::completion::options::OptionFields<'_>,
763    ) -> crate::completion::options::OptionMap {
764        super::options::map_options(self, request, fields)
765    }
766
767    fn facts(&self) -> Option<&ModelFacts> {
768        Some(&self.facts)
769    }
770
771    fn api(&self) -> crate::message::Api {
772        crate::message::Api::from_static(MESSAGES_API)
773    }
774
775    fn provider(&self) -> &str {
776        self.provider.dialect.name
777    }
778
779    fn model(&self) -> &str {
780        &self.model
781    }
782
783    /// Messages takes images in user turns and tool results, never in
784    /// assistant turns, on models that read images: every Claude model, and
785    /// on another dialect each model its catalog entry lists as reading
786    /// them, or for a model the catalog does not list each dialect's
787    /// documented vision naming.
788    fn accepts(&self, model: &str) -> crate::completion::Accepts {
789        let rule: Option<fn(&str) -> bool> = match self.provider.dialect.name {
790            name if name == ANTHROPIC.name => None,
791            name if name == ZAI.name => Some(crate::providers::zai::reads_images),
792            name if name == MOONSHOT.name => Some(crate::providers::moonshot::reads_images),
793            name if name == MINIMAX.name => Some(crate::providers::minimax::reads_images),
794            name if name == XIAOMIMIMO.name => Some(crate::providers::xiaomimimo::reads_images),
795            _ => Some(|_| true),
796        };
797        let images = rule.is_none_or(|rule| {
798            self.facts
799                .reads_images_or(self.provider.dialect.name, model, rule)
800        });
801        crate::completion::Accepts {
802            user_images: images,
803            assistant_images: false,
804            tool_result_images: images,
805            tools: true,
806            // Thinking replays only with its signature.
807            foreign_reasoning: false,
808        }
809    }
810
811    /// The encoder carries images by typed base64, URL or file id in user
812    /// turns and tool results, and documents by file id, PDF data or URL,
813    /// or the text they hold. Assistant images, audio and video it never
814    /// carries.
815    fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
816        use crate::completion::{Media, Place};
817        match media {
818            Media::Image(image, Place::User | Place::ToolResult) => image_source(image).is_some(),
819            Media::Document(document) => document_source(document).is_some(),
820            Media::Image(_, Place::Assistant) | Media::Audio(_) | Media::Video(_) => false,
821        }
822    }
823
824    /// Anthropic takes call ids of `[a-zA-Z0-9_-]`, at most 64 long.
825    fn normalize_tool_call_id(
826        &self,
827        id: &str,
828        _model: &str,
829        _source: Option<&crate::message::Origin>,
830    ) -> String {
831        crate::providers::internal::wire_ids::legal_call_id(id, 64)
832    }
833
834    /// An edited call keeps the `caller` that ties it to the code
835    /// execution that made it.
836    fn identity(&self, item: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
837        item.get("caller")
838            .filter(|_| item.get("type").and_then(serde_json::Value::as_str) == Some("tool_use"))
839            .map(|caller| serde_json::Map::from_iter([("caller".to_owned(), caller.clone())]))
840            .unwrap_or_default()
841    }
842
843    /// A model that takes no system message inside `messages` gets every
844    /// one folded into the leading system prompt.
845    fn later_system(&self, model: &str) -> crate::completion::LaterSystem {
846        if super::completion::takes_mid_conversation_system(&self.facts, model) {
847            crate::completion::LaterSystem::InPlace
848        } else {
849            crate::completion::LaterSystem::Leading
850        }
851    }
852
853    /// A container item is request state, never content, and unsigned
854    /// thinking that is redacted or blank has nothing to send.
855    /// Claude models whose thinking binds to the tools and system prompt it
856    /// was made under.
857    fn binds_context(&self, model: &str) -> bool {
858        self.provider.dialect.name == ANTHROPIC.name
859            && super::completion::binds_context(&self.facts, model)
860    }
861
862    /// A request in adaptive thinking asks Anthropic to drop a block bound
863    /// to another context (`drop_block`), so its turns replay verbatim.
864    /// The thinking read is what the body sends: the mapped `reasoning`,
865    /// with `additional_params` over it.
866    fn drops_unbound_items(&self, request: &CompletionRequest) -> bool {
867        let model = request.model.as_deref().unwrap_or(&self.model);
868        let thinking = crate::completion::options::param(self, request, "thinking");
869        super::completion::drops_unbound_thinking(self, model, thinking.as_ref())
870    }
871
872    /// What the encoder sends for the block, so the two never disagree.
873    fn sends_alone(&self, block: &crate::message::AssistantContent) -> bool {
874        let ids = crate::providers::internal::wire_ids::WireIds::default();
875        super::completion::assistant_part(block, self, &ids).is_some()
876    }
877
878    fn call_id_slot(&self) -> Option<&'static str> {
879        Some("/id")
880    }
881
882    fn blob_slots(&self) -> &'static [&'static str] {
883        &["/signature", "/data"]
884    }
885
886    /// A server tool's use (`server_tool_use`, `mcp_tool_use`) and the
887    /// `*_tool_result` that answers it by `tool_use_id`.
888    fn hosted_pair(
889        &self,
890        item: &serde_json::Value,
891    ) -> Option<(crate::completion::Pairing, String)> {
892        use crate::completion::Pairing;
893        let kind = item.get("type")?.as_str()?;
894        let (side, key) = if kind.ends_with("_tool_use") {
895            (Pairing::Use, "id")
896        } else if kind.ends_with("_tool_result") {
897            (Pairing::Result, "tool_use_id")
898        } else {
899            return None;
900        };
901        Some((side, item.get(key)?.as_str()?.to_owned()))
902    }
903}
904
905#[cfg(test)]
906mod tests;