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::client::env::{self, EnvError};
14use crate::completion::{CompletionRequest, ProviderCapabilities};
15use crate::error::EncodeError;
16use crate::operation::Completion;
17use crate::providers::internal::named_dialect;
18use crate::wire::{Capabilities, Descriptor, Encoded, Framing, Mode, Secret, Wire};
19use serde::{Deserialize, Serialize};
20
21use super::completion::{
22    CacheTtl, default_max_tokens_for_model, document_source, image_source,
23    rejects_forced_tool_choice,
24};
25use super::streaming::MessagesDecoder;
26
27/// Endpoint defaults and capabilities for a Messages-format provider.
28/// Serializes by registered name. Serialization rejects modified or unregistered
29/// dialects; deserialization rejects unknown names.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Dialect {
32    /// The provider descriptor name, as records and telemetry spell it.
33    pub name: &'static str,
34    /// The default base URL.
35    pub base_url: &'static str,
36    /// The environment variable carrying the API key.
37    pub api_key_env: &'static str,
38    /// The environment variable overriding the base URL, when the provider
39    /// documents one.
40    pub base_url_env: Option<&'static str>,
41    /// The reply header carrying the provider's transport request id.
42    pub request_id_header: Option<&'static str>,
43    /// Everything that is not identity.
44    pub quirks: Quirks,
45}
46
47/// Token defaults and schema capabilities for a Messages-format dialect.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct Quirks {
51    /// How `max_tokens` is defaulted when the caller sets none. Anthropic
52    /// requires the field, so a wire that cannot supply it fails the request
53    /// rather than sending one the API rejects.
54    pub max_tokens: MaxTokens,
55    /// Whether the provider implements Anthropic's constrained tool schemas.
56    /// A gateway that does not leaves Rig-generated tools unchanged.
57    pub strict_tool_schemas: bool,
58    /// Whether the provider takes thinking back without a signature, as
59    /// Kimi does (pi's `allowEmptySignature`). Elsewhere unsigned thinking
60    /// replays as text.
61    pub unsigned_thinking: bool,
62    /// How a request with tools asks the provider to stream tool input
63    /// (pi's `supportsEagerToolInputStreaming`).
64    pub tool_input_streaming: ToolInputStreaming,
65}
66
67/// How a request with tools asks a Messages-format provider to stream each
68/// call's input as the model writes it. Without it, Anthropic holds a long
69/// argument value back and sends it in one burst. Unary requests ask too, so
70/// a tool definition, which a prompt cache keys on, is the same whether a
71/// turn streams.
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum ToolInputStreaming {
75    /// `eager_input_streaming: true` on each Rig tool definition.
76    #[default]
77    Eager,
78    /// The `fine-grained-tool-streaming-2025-05-14` beta flag, for a
79    /// provider that rejects the per-tool field.
80    BetaHeader,
81    /// Neither: the provider streams tool input as it chooses.
82    Off,
83}
84
85impl Quirks {
86    /// Anthropic's own contract.
87    pub const fn anthropic() -> Self {
88        Self {
89            max_tokens: MaxTokens::ByModel,
90            strict_tool_schemas: true,
91            unsigned_thinking: false,
92            tool_input_streaming: ToolInputStreaming::Eager,
93        }
94    }
95
96    /// Default to 4096 output tokens without constrained tool schemas, with
97    /// eager tool input, which pi sends every Messages-format provider it
98    /// does not know to reject it.
99    pub const fn gateway() -> Self {
100        Self {
101            max_tokens: MaxTokens::Fixed(4096),
102            strict_tool_schemas: false,
103            unsigned_thinking: false,
104            tool_input_streaming: ToolInputStreaming::Eager,
105        }
106    }
107}
108
109/// Where a dialect's `max_tokens` default comes from.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum MaxTokens {
112    /// Anthropic's published per-model output limits.
113    ByModel,
114    /// One ceiling for every model, which is all a gateway documents.
115    Fixed(u64),
116}
117
118/// The API name the Messages wire reports for every dialect.
119pub(crate) const MESSAGES_API: &str = "anthropic.messages";
120
121/// Anthropic itself.
122pub const ANTHROPIC: Dialect = Dialect {
123    name: super::PROVIDER_NAME,
124    base_url: "https://api.anthropic.com",
125    api_key_env: "ANTHROPIC_API_KEY",
126    base_url_env: Some("ANTHROPIC_BASE_URL"),
127    request_id_header: Some("request-id"),
128    quirks: Quirks::anthropic(),
129};
130
131/// Registered Messages-format dialects in declaration order.
132const ALL: &[&Dialect] = &[&ANTHROPIC, &ZAI, &MINIMAX, &MOONSHOT, &XIAOMIMIMO];
133
134/// Every Messages-format dialect this build knows, in declaration order.
135pub fn all() -> impl Iterator<Item = &'static Dialect> {
136    ALL.iter().copied()
137}
138
139impl Dialect {
140    /// The dialect this crate ships under `name`.
141    pub fn by_name(name: &str) -> Option<Self> {
142        all().copied().find(|dialect| dialect.name == name)
143    }
144}
145
146impl Serialize for Dialect {
147    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
148        let registered = Self::by_name(self.name).as_ref() == Some(self);
149        named_dialect::serialize(serializer, "Anthropic", self.name, registered)
150    }
151}
152
153impl<'de> Deserialize<'de> for Dialect {
154    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
155        named_dialect::deserialize(deserializer, "Anthropic", Self::by_name)
156    }
157}
158
159/// Build a Messages-format dialect with `request-id` response headers and
160/// [`Quirks::gateway`] defaults. Only registered dialects support serialization.
161pub const fn compatible(
162    name: &'static str,
163    base_url: &'static str,
164    api_key_env: &'static str,
165    base_url_env: Option<&'static str>,
166) -> Dialect {
167    Dialect {
168        name,
169        base_url,
170        api_key_env,
171        base_url_env,
172        request_id_header: Some("request-id"),
173        quirks: Quirks::gateway(),
174    }
175}
176
177impl Dialect {
178    /// The `max_tokens` this dialect defaults `model` to.
179    pub fn default_max_tokens(&self, model: &str) -> Option<u64> {
180        match self.quirks.max_tokens {
181            MaxTokens::ByModel => default_max_tokens_for_model(model),
182            MaxTokens::Fixed(tokens) => Some(tokens),
183        }
184    }
185}
186
187/// Z.AI's Anthropic-format endpoint.
188pub const ZAI: Dialect = compatible(
189    crate::providers::zai::PROVIDER_NAME,
190    "https://api.z.ai/api/anthropic",
191    "ZAI_API_KEY",
192    Some("ZAI_ANTHROPIC_API_BASE"),
193);
194
195/// MiniMax's Anthropic-format endpoint.
196pub const MINIMAX: Dialect = compatible(
197    crate::providers::minimax::PROVIDER_NAME,
198    "https://api.minimax.io/anthropic",
199    "MINIMAX_API_KEY",
200    Some("MINIMAX_ANTHROPIC_API_BASE"),
201);
202
203/// Moonshot's Anthropic-format endpoint, which sends and takes back
204/// thinking without a signature.
205pub const MOONSHOT: Dialect = Dialect {
206    quirks: Quirks {
207        unsigned_thinking: true,
208        ..Quirks::gateway()
209    },
210    ..compatible(
211        crate::providers::moonshot::PROVIDER_NAME,
212        "https://api.moonshot.ai/anthropic",
213        "MOONSHOT_API_KEY",
214        Some("MOONSHOT_ANTHROPIC_API_BASE"),
215    )
216};
217
218/// Xiaomi MiMo's Anthropic-format endpoint.
219pub const XIAOMIMIMO: Dialect = compatible(
220    crate::providers::xiaomimimo::PROVIDER_NAME,
221    "https://api.xiaomimimo.com/anthropic",
222    "XIAOMI_MIMO_API_KEY",
223    Some("XIAOMI_MIMO_ANTHROPIC_API_BASE"),
224);
225
226/// The settings of a Messages-format provider: serializable, and the
227/// credential is never serialized. [`connect`](Self::connect) puts it on a
228/// transport as an [`Anthropic`](super::Anthropic) client.
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231pub struct AnthropicConfig {
232    /// The API key, sent as `x-api-key`.
233    pub api_key: Secret,
234    /// The API root, without a trailing `/v1`.
235    pub base_url: String,
236    /// The `anthropic-version` header.
237    pub version: String,
238    /// The `anthropic-beta` flags, joined with commas when non-empty.
239    pub betas: Vec<String>,
240    /// Which Messages-format provider this is.
241    pub dialect: Dialect,
242}
243
244impl AnthropicConfig {
245    /// Anthropic itself, with default settings.
246    pub fn new(api_key: impl Into<Secret>) -> Self {
247        Self::with_key(&ANTHROPIC, api_key)
248    }
249
250    /// `dialect` with `api_key`, at the dialect's default base URL and
251    /// with default settings. The base URL is normalized as
252    /// [`with_base_url`](Self::with_base_url) does.
253    pub fn with_key(dialect: &Dialect, api_key: impl Into<Secret>) -> Self {
254        Self {
255            api_key: api_key.into(),
256            base_url: normalize_base_url(dialect.base_url),
257            version: super::completion::ANTHROPIC_VERSION_LATEST.to_owned(),
258            betas: Vec::new(),
259            dialect: *dialect,
260        }
261    }
262
263    /// Anthropic from `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`.
264    pub fn from_env() -> Result<Self, EnvError> {
265        Self::from_env_with(&ANTHROPIC)
266    }
267
268    /// A Messages-format provider from the variables its dialect names.
269    pub fn from_env_with(dialect: &Dialect) -> Result<Self, EnvError> {
270        let mut provider = Self::with_key(dialect, env::required(dialect.api_key_env)?);
271        if let Some(name) = dialect.base_url_env
272            && let Some(base_url) = env::optional(name)?
273        {
274            provider.base_url = normalize_base_url(&base_url);
275        }
276        Ok(provider)
277    }
278
279    /// Pin the `anthropic-version` header.
280    pub fn with_version(mut self, version: impl Into<String>) -> Self {
281        self.version = version.into();
282        self
283    }
284
285    /// Request an `anthropic-beta` flag.
286    pub fn with_beta(mut self, beta: impl Into<String>) -> Self {
287        self.betas.push(beta.into());
288        self
289    }
290
291    /// Point the wire at another API root.
292    pub fn with_base_url(mut self, base_url: impl AsRef<str>) -> Self {
293        self.base_url = normalize_base_url(base_url.as_ref());
294        self
295    }
296
297    /// The Messages wire for `model`.
298    pub(crate) fn completion(&self, model: impl Into<String>) -> Messages {
299        let model = model.into();
300        Messages {
301            default_max_tokens: self.dialect.default_max_tokens(&model),
302            provider: self.clone(),
303            model,
304            prompt_caching: false,
305            static_prefix_cache_ttl: None,
306            strict_tools: false,
307            tool_input_streaming: self.dialect.quirks.tool_input_streaming,
308        }
309    }
310
311    /// The request headers every Messages-format endpoint takes.
312    pub(super) fn headers(&self, builder: http::request::Builder) -> http::request::Builder {
313        self.headers_with(builder, &[])
314    }
315
316    /// [`Self::headers`], with `extra` among the `anthropic-beta` flags.
317    pub(super) fn headers_with(
318        &self,
319        builder: http::request::Builder,
320        extra: &[&str],
321    ) -> http::request::Builder {
322        let builder = builder
323            .header("x-api-key", self.api_key.expose())
324            .header("anthropic-version", &self.version);
325        let mut betas: Vec<&str> = self.betas.iter().map(String::as_str).collect();
326        for extra in extra {
327            if !betas.contains(extra) {
328                betas.push(extra);
329            }
330        }
331        if betas.is_empty() {
332            builder
333        } else {
334            builder.header("anthropic-beta", betas.join(","))
335        }
336    }
337}
338
339/// Trim the suffixes a user may paste from the API docs, so a base URL that
340/// already names the endpoint does not produce `/v1/messages/v1/messages`.
341pub fn normalize_base_url(base_url: &str) -> String {
342    let trimmed = base_url.trim_end_matches('/');
343    for suffix in ["/v1/messages", "/messages", "/v1"] {
344        if let Some(stripped) = trimmed.strip_suffix(suffix) {
345            return stripped.to_owned();
346        }
347    }
348    trimmed.to_owned()
349}
350
351/// The Messages wire: `POST /v1/messages`, SSE when streamed.
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
353pub struct Messages {
354    /// The provider this wire speaks to.
355    pub provider: AnthropicConfig,
356    /// The model to address.
357    pub model: String,
358    /// What `max_tokens` defaults to when the caller sets none. Anthropic
359    /// requires the field; `None` means a request without one is rejected
360    /// here rather than by the API.
361    pub default_max_tokens: Option<u64>,
362    /// Manual prompt caching: `cache_control` breakpoints on the system
363    /// prompt, the last tool definition, and the last content block of the
364    /// last message. The request's
365    /// [`cache`](crate::completion::GenerationOptions::cache) sets the
366    /// top-level marker and its TTL.
367    pub prompt_caching: bool,
368    /// TTL for the static prefix (tools + system), independent of the
369    /// conversation tail. `None` inherits the top-level TTL.
370    pub static_prefix_cache_ttl: Option<CacheTtl>,
371    /// Whether Rig-generated tools request the provider's strict validation.
372    pub strict_tools: bool,
373    /// How a request with tools asks for tool input as it is written; the
374    /// dialect's by default.
375    #[serde(default)]
376    pub tool_input_streaming: ToolInputStreaming,
377}
378
379impl Messages {
380    /// Set the `max_tokens` a request without one defaults to.
381    pub fn with_default_max_tokens(mut self, tokens: u64) -> Self {
382        self.default_max_tokens = Some(tokens);
383        self
384    }
385
386    /// Enable cache breakpoints on the system prompt, final tool, and final message block.
387    /// With a request [`cache`](crate::completion::GenerationOptions::cache), the
388    /// provider owns the moving message breakpoint and the markers take its TTL.
389    /// Existing tool markers are preserved and count toward the four-breakpoint budget.
390    pub fn with_prompt_caching(mut self) -> Self {
391        self.prompt_caching = true;
392        self
393    }
394
395    /// Cache the static prefix (tool definitions and system prompt) at its
396    /// own TTL, independent of the conversation tail's.
397    ///
398    /// Mark the final tool definition and system prompt at `ttl` without
399    /// changing the conversation tail's TTL.
400    ///
401    /// ```no_run
402    /// use rig_core::providers::anthropic::completion::{CLAUDE_SONNET_4_6, CacheTtl};
403    /// use rig_core::providers::anthropic::Anthropic;
404    ///
405    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
406    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
407    /// messages.wire = messages.wire.with_static_prefix_cache_ttl(CacheTtl::OneHour);
408    /// # Ok(())
409    /// # }
410    /// ```
411    ///
412    /// One-hour markers must precede five-minute markers. A five-minute
413    /// prefix under a request with
414    /// [`CacheRetention::Long`](crate::completion::CacheRetention::Long) fails
415    /// during encoding.
416    /// Each marker must meet the model's minimum cacheable prompt length.
417    pub fn with_static_prefix_cache_ttl(mut self, ttl: CacheTtl) -> Self {
418        self.static_prefix_cache_ttl = Some(ttl);
419        self
420    }
421
422    /// Request Anthropic's strict tool use for every Rig-generated tool.
423    ///
424    /// Anthropic constrains tool inputs to the supported JSON Schema subset
425    /// when `strict: true` is present on a tool definition. Rig sanitizes
426    /// each generated schema for that subset and leaves provider-specific
427    /// tools supplied through `additional_params` unchanged. Unsupported
428    /// validation keywords are retained only as model guidance in schema
429    /// descriptions; neither Anthropic nor Rig enforces them, so validate
430    /// tool inputs before execution when those constraints matter.
431    ///
432    /// Anthropic caches compiled schemas for up to 24 hours: do not include
433    /// PHI in schema property names, enum or const values, or regex
434    /// patterns. A dialect that does not implement constrained tool schemas
435    /// ignores this.
436    pub fn with_strict_tools(mut self) -> Self {
437        self.strict_tools = true;
438        self
439    }
440
441    /// Choose how a request with tools asks for tool input as the model
442    /// writes it, for a gateway whose support differs from its dialect's:
443    /// [`ToolInputStreaming::BetaHeader`] for one that rejects the per-tool
444    /// field, [`ToolInputStreaming::Off`] for one that rejects both.
445    ///
446    /// ```no_run
447    /// use rig_core::providers::anthropic::{Anthropic, completion::CLAUDE_SONNET_4_6};
448    /// use rig_core::providers::anthropic::ToolInputStreaming;
449    ///
450    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
451    /// let mut messages = Anthropic::from_env()?.completion(CLAUDE_SONNET_4_6);
452    /// messages.wire = messages.wire.with_tool_input_streaming(ToolInputStreaming::BetaHeader);
453    /// # Ok(())
454    /// # }
455    /// ```
456    pub fn with_tool_input_streaming(mut self, streaming: ToolInputStreaming) -> Self {
457        self.tool_input_streaming = streaming;
458        self
459    }
460}
461
462impl Wire for Messages {
463    type Op = Completion;
464    type Payload = crate::wire::Encoded;
465    type Frame = crate::wire::WireFrame;
466    type Decoder<'id> = MessagesDecoder;
467    type Reassembler = super::streaming::document::Message;
468
469    /// Constrained output decoding does not suppress strict tool calls.
470    fn describe(&self) -> Descriptor<'_> {
471        Descriptor::new(self.provider.dialect.name)
472            .model(self.model.as_str())
473            .capabilities(Capabilities::completion(
474                ProviderCapabilities::default()
475                    .with_native_output_tool_composition(true)
476                    .with_forced_tool_choice_rejected(rejects_forced_tool_choice(&self.model)),
477            ))
478            .replay(self)
479    }
480
481    fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
482        let model = request.model.clone().unwrap_or_else(|| self.model.clone());
483        let fine_grained = !request.tools.is_empty()
484            && self.tool_input_streaming == ToolInputStreaming::BetaHeader;
485        let body = super::completion::body(self, &request, mode)?;
486        let betas: Vec<&str> = [
487            super::completion::drops_unbound_thinking(self, &model, body.get("thinking"))
488                .then_some(super::completion::THINKING_BINDING_BETA),
489            fine_grained.then_some(super::completion::FINE_GRAINED_TOOL_STREAMING_BETA),
490        ]
491        .into_iter()
492        .flatten()
493        .collect();
494        crate::providers::internal::trace_json(
495            crate::providers::internal::LogTarget::Completions,
496            "Anthropic completion request",
497            &body,
498        );
499        let request = self
500            .provider
501            .headers_with(
502                http::Request::post(format!("{}/v1/messages", self.provider.base_url)),
503                &betas,
504            )
505            .header(http::header::CONTENT_TYPE, "application/json")
506            .body(body.into_body())?;
507        Ok(Encoded::new(
508            request,
509            match mode {
510                Mode::Unary => Framing::Whole,
511                Mode::Streaming => Framing::Sse,
512            },
513        )
514        .with_request_id_header(self.provider.dialect.request_id_header)
515        .with_projection(MessagesDecoder::project))
516    }
517
518    fn decoder<'id>(&self) -> Self::Decoder<'id> {
519        MessagesDecoder::new(self.provider.dialect.quirks.unsigned_thinking)
520    }
521}
522
523impl crate::completion::ReplayTarget for Messages {
524    /// Section 6.1 of the typed-options design, by dialect and model class.
525    fn map_options(
526        &self,
527        request: &CompletionRequest,
528        fields: crate::completion::options::OptionFields<'_>,
529    ) -> crate::completion::options::OptionMap {
530        super::options::map_options(self, request, fields)
531    }
532
533    fn api(&self) -> crate::message::Api {
534        crate::message::Api::from_static(MESSAGES_API)
535    }
536
537    fn provider(&self) -> &str {
538        self.provider.dialect.name
539    }
540
541    fn model(&self) -> &str {
542        &self.model
543    }
544
545    /// Messages takes images in user turns and tool results, never in
546    /// assistant turns, on models that read images: every Claude model, and
547    /// on another dialect each model its catalog entry lists as reading
548    /// them, or for a model the catalog does not list each dialect's
549    /// documented vision naming (pi's model data agrees).
550    fn accepts(&self, model: &str) -> crate::completion::Accepts {
551        let rule: Option<fn(&str) -> bool> = match self.provider.dialect.name {
552            name if name == ANTHROPIC.name => None,
553            name if name == ZAI.name => Some(crate::providers::zai::reads_images),
554            name if name == MOONSHOT.name => Some(crate::providers::moonshot::reads_images),
555            name if name == MINIMAX.name => Some(crate::providers::minimax::reads_images),
556            name if name == XIAOMIMIMO.name => Some(crate::providers::xiaomimimo::reads_images),
557            _ => Some(|_| true),
558        };
559        let images = rule.is_none_or(|rule| {
560            crate::catalog::reads_images_or(self.provider.dialect.name, model, rule)
561        });
562        crate::completion::Accepts {
563            user_images: images,
564            assistant_images: false,
565            tool_result_images: images,
566            tools: true,
567        }
568    }
569
570    /// The encoder carries images by typed base64, URL or file id in user
571    /// turns and tool results, and documents by file id, PDF data or URL,
572    /// or the text they hold. Assistant images, audio and video it never
573    /// carries.
574    fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
575        use crate::completion::{Media, Place};
576        match media {
577            Media::Image(image, Place::User | Place::ToolResult) => image_source(image).is_some(),
578            Media::Document(document) => document_source(document).is_some(),
579            Media::Image(_, Place::Assistant) | Media::Audio(_) | Media::Video(_) => false,
580        }
581    }
582
583    /// Anthropic takes call ids of `[a-zA-Z0-9_-]`, at most 64 long (pi's
584    /// rule).
585    fn normalize_tool_call_id(
586        &self,
587        id: &str,
588        _model: &str,
589        _source: Option<&crate::message::Origin>,
590    ) -> String {
591        crate::providers::internal::wire_ids::legal_call_id(id, 64)
592    }
593
594    /// An edited call keeps the `caller` that ties it to the code
595    /// execution that made it.
596    fn identity(&self, item: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
597        item.get("caller")
598            .filter(|_| item.get("type").and_then(serde_json::Value::as_str) == Some("tool_use"))
599            .map(|caller| serde_json::Map::from_iter([("caller".to_owned(), caller.clone())]))
600            .unwrap_or_default()
601    }
602
603    /// A model that takes no system message inside `messages` gets every
604    /// one folded into the leading system prompt.
605    fn later_system(&self, model: &str) -> crate::completion::LaterSystem {
606        if super::completion::takes_mid_conversation_system(model) {
607            crate::completion::LaterSystem::InPlace
608        } else {
609            crate::completion::LaterSystem::Leading
610        }
611    }
612
613    /// A container item is request state, never content, and unsigned
614    /// thinking that is redacted or blank has nothing to send.
615    /// Claude models whose thinking binds to the tools and system prompt it
616    /// was made under.
617    fn binds_context(&self, model: &str) -> bool {
618        self.provider.dialect.name == ANTHROPIC.name && super::completion::binds_context(model)
619    }
620
621    /// A request in adaptive thinking asks Anthropic to drop a block bound
622    /// to another context (`drop_block`), so its turns replay verbatim.
623    /// The thinking read is what the body sends: the mapped `reasoning`,
624    /// with `additional_params` over it.
625    fn drops_unbound_items(&self, request: &CompletionRequest) -> bool {
626        let model = request.model.as_deref().unwrap_or(&self.model);
627        let thinking = crate::completion::options::param(self, request, "thinking");
628        super::completion::drops_unbound_thinking(self, model, thinking.as_ref())
629    }
630
631    /// What the encoder sends for the block, so the two never disagree.
632    fn sends_alone(&self, block: &crate::message::AssistantContent) -> bool {
633        let ids = crate::providers::internal::wire_ids::WireIds::default();
634        super::completion::assistant_part(block, self, &ids).is_some()
635    }
636
637    fn call_id_slot(&self) -> Option<&'static str> {
638        Some("/id")
639    }
640
641    /// A server tool's use (`server_tool_use`, `mcp_tool_use`) and the
642    /// `*_tool_result` that answers it by `tool_use_id`.
643    fn hosted_pair(
644        &self,
645        item: &serde_json::Value,
646    ) -> Option<(crate::completion::Pairing, String)> {
647        use crate::completion::Pairing;
648        let kind = item.get("type")?.as_str()?;
649        let (side, key) = if kind.ends_with("_tool_use") {
650            (Pairing::Use, "id")
651        } else if kind.ends_with("_tool_result") {
652            (Pairing::Result, "tool_use_id")
653        } else {
654            return None;
655        };
656        Some((side, item.get(key)?.as_str()?.to_owned()))
657    }
658}
659
660#[cfg(test)]
661mod tests;