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 and schema capabilities for a Messages-format dialect.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct Quirks {
53    /// How `max_tokens` is defaulted when the caller sets none. Anthropic
54    /// requires the field, so a wire that cannot supply it fails the request
55    /// rather than sending one the API rejects.
56    pub max_tokens: MaxTokens,
57    /// Whether the provider implements Anthropic's constrained tool schemas.
58    /// A gateway that does not leaves Rig-generated tools unchanged.
59    pub strict_tool_schemas: bool,
60    /// Whether the provider takes thinking back without a signature, as
61    /// Kimi does. Elsewhere unsigned thinking
62    /// replays as text.
63    pub unsigned_thinking: bool,
64    /// How a request with tools asks the provider to stream tool input.
65    pub tool_input_streaming: ToolInputStreaming,
66}
67
68/// How a request with tools asks a Messages-format provider to stream each
69/// call's input as the model writes it. Without it, Anthropic holds a long
70/// argument value back and sends it in one burst. Unary requests ask too, so
71/// a tool definition, which a prompt cache keys on, is the same whether a
72/// turn streams.
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ToolInputStreaming {
76    /// `eager_input_streaming: true` on each Rig tool definition.
77    #[default]
78    Eager,
79    /// The `fine-grained-tool-streaming-2025-05-14` beta flag, for a
80    /// provider that rejects the per-tool field.
81    BetaHeader,
82    /// Neither: the provider streams tool input as it chooses.
83    Off,
84}
85
86impl Quirks {
87    /// Anthropic's own contract.
88    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    /// Default to 4096 output tokens without constrained tool schemas, with
98    /// eager tool input, which a Messages-format provider takes unless it is
99    /// known to reject it.
100    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/// Where a dialect's `max_tokens` default comes from.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub enum MaxTokens {
113    /// Anthropic's published per-model output limits.
114    ByModel,
115    /// One ceiling for every model, which is all a gateway documents.
116    Fixed(u64),
117}
118
119/// The API name the Messages wire reports for every dialect.
120pub(crate) const MESSAGES_API: &str = "anthropic.messages";
121
122/// Anthropic itself.
123pub 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
132/// Registered Messages-format dialects in declaration order.
133const ALL: &[&Dialect] = &[&ANTHROPIC, &ZAI, &MINIMAX, &MOONSHOT, &XIAOMIMIMO];
134
135/// Every Messages-format dialect this build knows, in declaration order.
136pub fn all() -> impl Iterator<Item = &'static Dialect> {
137    ALL.iter().copied()
138}
139
140impl Dialect {
141    /// The dialect this crate ships under `name`.
142    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
160/// Build a Messages-format dialect with `request-id` response headers and
161/// [`Quirks::gateway`] defaults. Only registered dialects support serialization.
162pub 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    /// The `max_tokens` this dialect defaults `model` to, by the output
180    /// limit `facts` give it where the dialect defaults by model.
181    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
189/// Z.AI's Anthropic-format endpoint.
190pub 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
197/// MiniMax's Anthropic-format endpoint.
198pub 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
205/// Moonshot's Anthropic-format endpoint, which sends and takes back
206/// thinking without a signature.
207pub 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
220/// Xiaomi MiMo's Anthropic-format endpoint.
221pub 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/// The settings of a Messages-format provider: serializable, and the
229/// credential is never serialized. [`connect`](Self::connect) puts it on a
230/// transport as an [`Anthropic`](super::Anthropic) client.
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(deny_unknown_fields)]
233pub struct AnthropicConfig {
234    /// The API key, sent as `x-api-key`.
235    pub api_key: Secret,
236    /// The API root, without a trailing `/v1`.
237    pub base_url: String,
238    /// The `anthropic-version` header.
239    pub version: String,
240    /// The `anthropic-beta` flags, joined with commas when non-empty.
241    pub betas: Vec<String>,
242    /// Which Messages-format provider this is.
243    pub dialect: Dialect,
244    /// Where a request names its model and API version.
245    #[serde(default, skip_serializing_if = "Addressing::is_body")]
246    pub addressing: Addressing,
247}
248
249/// Where a Messages request names its model and API version.
250#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum Addressing {
253    /// `model` in the body and `version` as the `anthropic-version` header,
254    /// as Anthropic's own API takes them.
255    #[default]
256    Body,
257    /// No `model` in the body, because the URL names it, and `version` as
258    /// the body's `anthropic_version`, as cloud platforms take them. Such a
259    /// platform builds its URL with [`Messages::encode_with`].
260    Url,
261}
262
263impl Addressing {
264    /// Whether this is Anthropic's own [`Addressing::Body`].
265    fn is_body(&self) -> bool {
266        *self == Self::Body
267    }
268}
269
270impl AnthropicConfig {
271    /// Anthropic itself, with default settings.
272    pub fn new(api_key: impl Into<Secret>) -> Self {
273        Self::with_key(&ANTHROPIC, api_key)
274    }
275
276    /// `dialect` with `api_key`, at the dialect's default base URL and
277    /// with default settings. The base URL is normalized as
278    /// [`with_base_url`](Self::with_base_url) does.
279    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    /// Anthropic from `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`.
291    pub fn from_env() -> Result<Self, EnvError> {
292        Self::from_env_with(&ANTHROPIC)
293    }
294
295    /// A Messages-format provider from the variables its dialect names.
296    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    /// `dialect` with `api_key` and the base URL its environment variable
301    /// names, if set.
302    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    /// Pin the `anthropic-version` header.
316    pub fn with_version(mut self, version: impl Into<String>) -> Self {
317        self.version = version.into();
318        self
319    }
320
321    /// Request an `anthropic-beta` flag.
322    pub fn with_beta(mut self, beta: impl Into<String>) -> Self {
323        self.betas.push(beta.into());
324        self
325    }
326
327    /// Point the wire at another API root.
328    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    /// The Messages wire for `model`.
334    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    /// The request headers every Messages-format endpoint takes.
351    pub(super) fn headers(&self, builder: http::request::Builder) -> http::request::Builder {
352        self.headers_with(builder, &self.betas)
353    }
354
355    /// [`Self::headers`], with `betas` as the `anthropic-beta` flags.
356    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    /// The configured `anthropic-beta` flags, then each of `extra` not
372    /// among them.
373    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
384/// Trim the suffixes a user may paste from the API docs, so a base URL that
385/// already names the endpoint does not produce `/v1/messages/v1/messages`.
386pub 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/// The Messages wire: `POST /v1/messages`, SSE when streamed.
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct Messages {
399    /// The provider this wire speaks to.
400    pub provider: AnthropicConfig,
401    /// The model to address.
402    pub model: String,
403    /// What `max_tokens` defaults to when the caller sets none. Anthropic
404    /// requires the field; `None` means a request without one is rejected
405    /// here rather than by the API.
406    pub default_max_tokens: Option<u64>,
407    /// Manual prompt caching: `cache_control` breakpoints on the system
408    /// prompt, the last tool definition, and the last content block of the
409    /// last message. The request's
410    /// [`cache`](crate::completion::GenerationOptions::cache) sets the
411    /// top-level marker and its TTL.
412    pub prompt_caching: bool,
413    /// TTL for the static prefix (tools + system), independent of the
414    /// conversation tail. `None` inherits the top-level TTL.
415    pub static_prefix_cache_ttl: Option<CacheTtl>,
416    /// Whether Rig-generated tools request the provider's strict validation.
417    pub strict_tools: bool,
418    /// How a request with tools asks for tool input as it is written; the
419    /// dialect's by default.
420    #[serde(default)]
421    pub tool_input_streaming: ToolInputStreaming,
422    /// The model facts the encoder reads and replies are priced by.
423    #[serde(skip)]
424    pub facts: ModelFacts,
425}
426
427impl Messages {
428    /// The same wire, encoding with `facts` and pricing its replies by
429    /// them. The `max_tokens` a request without one defaults to becomes the
430    /// dialect's default for the model under `facts`; set another with
431    /// [`Self::with_default_max_tokens`] after this.
432    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    /// Set the `max_tokens` a request without one defaults to.
442    pub fn with_default_max_tokens(mut self, tokens: u64) -> Self {
443        self.default_max_tokens = Some(tokens);
444        self
445    }
446
447    /// Enable cache breakpoints on the system prompt, final tool, and final message block.
448    /// With a request [`cache`](crate::completion::GenerationOptions::cache), the
449    /// provider owns the moving message breakpoint and the markers take its TTL.
450    /// Existing tool markers are preserved and count toward the four-breakpoint budget.
451    pub fn with_prompt_caching(mut self) -> Self {
452        self.prompt_caching = true;
453        self
454    }
455
456    /// Cache the static prefix (tool definitions and system prompt) at its
457    /// own TTL, independent of the conversation tail's.
458    ///
459    /// Mark the final tool definition and system prompt at `ttl` without
460    /// changing the conversation tail's TTL.
461    ///
462    /// ```no_run
463    /// use rig_core::providers::anthropic::completion::{CLAUDE_SONNET_4_6, CacheTtl};
464    /// use rig_core::providers::anthropic::Anthropic;
465    ///
466    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
467    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
468    /// messages.wire = messages.wire.with_static_prefix_cache_ttl(CacheTtl::OneHour);
469    /// # Ok(())
470    /// # }
471    /// ```
472    ///
473    /// One-hour markers must precede five-minute markers. A five-minute
474    /// prefix under a request with
475    /// [`CacheRetention::Long`](crate::completion::CacheRetention::Long) fails
476    /// during encoding.
477    /// Each marker must meet the model's minimum cacheable prompt length.
478    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    /// Request Anthropic's strict tool use for every Rig-generated tool.
484    ///
485    /// Anthropic constrains tool inputs to the supported JSON Schema subset
486    /// when `strict: true` is present on a tool definition. Rig sanitizes
487    /// each generated schema for that subset and leaves provider-specific
488    /// tools supplied through `additional_params` unchanged. Unsupported
489    /// validation keywords are retained only as model guidance in schema
490    /// descriptions; neither Anthropic nor Rig enforces them, so validate
491    /// tool inputs before execution when those constraints matter.
492    ///
493    /// Anthropic caches compiled schemas for up to 24 hours: do not include
494    /// PHI in schema property names, enum or const values, or regex
495    /// patterns. A dialect that does not implement constrained tool schemas
496    /// ignores this.
497    pub fn with_strict_tools(mut self) -> Self {
498        self.strict_tools = true;
499        self
500    }
501
502    /// Choose how a request with tools asks for tool input as the model
503    /// writes it, for a gateway whose support differs from its dialect's:
504    /// [`ToolInputStreaming::BetaHeader`] for one that rejects the per-tool
505    /// field, [`ToolInputStreaming::Off`] for one that rejects both.
506    ///
507    /// ```no_run
508    /// use rig_core::providers::anthropic::{Anthropic, completion::CLAUDE_SONNET_4_6};
509    /// use rig_core::providers::anthropic::ToolInputStreaming;
510    ///
511    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
512    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
513    /// messages.wire = messages.wire.with_tool_input_streaming(ToolInputStreaming::BetaHeader);
514    /// # Ok(())
515    /// # }
516    /// ```
517    pub fn with_tool_input_streaming(mut self, streaming: ToolInputStreaming) -> Self {
518        self.tool_input_streaming = streaming;
519        self
520    }
521}
522
523/// A Messages request encoded but not yet addressed or authenticated: what
524/// a platform serving the Messages API under its own envelope sends.
525#[derive(Debug, Clone, PartialEq)]
526#[non_exhaustive]
527pub struct Prepared {
528    /// The model the request addresses.
529    pub model: String,
530    /// The request body.
531    pub body: FinalBody,
532    /// The `anthropic-beta` flags: the configured ones, then the ones the
533    /// request needs.
534    pub betas: Vec<String>,
535}
536
537impl Messages {
538    /// Encode `request` as [`Wire::encode`] does, with `envelope` building
539    /// the HTTP request in place of `POST /v1/messages` with an API key.
540    /// The framing, request-id header and projection stay this wire's, so
541    /// a platform that serves the Messages API at another address, with
542    /// other credentials or another body envelope, reads replies as this
543    /// wire does.
544    ///
545    /// # Errors
546    ///
547    /// When the request does not encode, or `envelope` fails.
548    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    /// Constrained output decoding does not suppress strict tool calls.
597    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    /// Section 6.1 of the typed-options design, by dialect and model class.
632    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    /// Messages takes images in user turns and tool results, never in
657    /// assistant turns, on models that read images: every Claude model, and
658    /// on another dialect each model its catalog entry lists as reading
659    /// them, or for a model the catalog does not list each dialect's
660    /// documented vision naming.
661    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            // Thinking replays only with its signature.
680            foreign_reasoning: false,
681        }
682    }
683
684    /// The encoder carries images by typed base64, URL or file id in user
685    /// turns and tool results, and documents by file id, PDF data or URL,
686    /// or the text they hold. Assistant images, audio and video it never
687    /// carries.
688    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    /// Anthropic takes call ids of `[a-zA-Z0-9_-]`, at most 64 long.
698    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    /// An edited call keeps the `caller` that ties it to the code
708    /// execution that made it.
709    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    /// A model that takes no system message inside `messages` gets every
717    /// one folded into the leading system prompt.
718    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    /// A container item is request state, never content, and unsigned
727    /// thinking that is redacted or blank has nothing to send.
728    /// Claude models whose thinking binds to the tools and system prompt it
729    /// was made under.
730    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    /// A request in adaptive thinking asks Anthropic to drop a block bound
736    /// to another context (`drop_block`), so its turns replay verbatim.
737    /// The thinking read is what the body sends: the mapped `reasoning`,
738    /// with `additional_params` over it.
739    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    /// What the encoder sends for the block, so the two never disagree.
746    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    /// A server tool's use (`server_tool_use`, `mcp_tool_use`) and the
756    /// `*_tool_result` that answers it by `tool_use_id`.
757    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;