Skip to main content

cratefield_adapter_anthropic/
lib.rs

1//! `cratefield-adapter-anthropic`: the minimal [`TextModel`] port over the
2//! Anthropic Messages API (issue #430). Uses the runtime's [`HttpClient`]
3//! port — no `reqwest`, no vendor SDK — so the same adapter runs on
4//! Workers and natively, and inherits that port's destination vetting,
5//! deadline and response-size caps.
6//!
7//! **Degraded mode.** When the API key is absent the adapter answers
8//! [`TextModelError::NotConfigured`] without any network call, so a
9//! pipeline stage can degrade instead of breaking.
10//!
11//! **Tools** (issue #665): [`Prompt::tools`] travel as the Messages API's
12//! `tools` array, [`Prompt::tool_choice`] as its `tool_choice`, and the
13//! answer's `tool_use` blocks come back as [`Completion::tool_calls`]. A
14//! turn built with [`Turn::assistant_tool_calls`] or
15//! [`Turn::tool_results`] is sent as the content-block array the API
16//! requires; every other turn keeps the bare-string content it always sent.
17//! [`Anthropic::supports`] reports [`Capability::Tools`]. A prompt that
18//! carries both [`Prompt::json_schema`] and its own tools is refused with
19//! [`TextModelError::Rejected`] before any request.
20//!
21//! **Streaming** (issue #859): [`Anthropic::stream`] sends the same
22//! request with `"stream": true` and answers the port's [`TextDelta`]s
23//! straight off the Messages event stream — text and reasoning as they
24//! are produced, each tool call from its start through its argument
25//! chunks to the finished call, running usage, and one `Finish` naming
26//! the wire's `stop_reason`. A finished call is the same [`ToolCall`]
27//! `complete` builds — one shared conversion — and the stream owns the
28//! response body, so dropping it cancels the upstream exchange. A prompt
29//! carrying [`Prompt::json_schema`] still completes and decomposes:
30//! [`Completion::json`] is a buffered feature.
31//!
32//! **Images** (issue #628): a turn built with [`Turn::user_parts`] is sent
33//! as the content-block array the Messages API requires — each [`Part`] as
34//! a `text` block or an inline base64 `image` block, in order — and
35//! [`Anthropic::supports`] reports [`Capability::Images`], because every
36//! current Claude model accepts image input. An over-limit prompt is
37//! refused by [`Prompt::check_images`] before any network call.
38
39#![doc = include_str!("../README.md")]
40#![forbid(unsafe_code)]
41
42use async_trait::async_trait;
43use bytes::Bytes;
44use cratefield_core::sse::{SseEvent, sse_events};
45use cratefield_core::{
46    BoxStream, ByteStream, Capability, Clock, Completion, FinishReason, HttpClient, HttpError,
47    HttpPolicy, MAX_RESPONSE_BYTES, MAX_RESPONSE_TIMEOUT, ModelTier, Part, Prompt, Role, TextDelta,
48    TextModel, TextModelError, ToolCall, ToolChoice, Turn, completion_deltas, encode_image,
49    retry_after,
50};
51use futures_util::StreamExt;
52use http::header::CONTENT_TYPE;
53use http::{Request, StatusCode};
54use std::sync::Arc;
55use std::time::Duration;
56
57const MESSAGES_ENDPOINT: &str = "https://api.anthropic.com/v1/messages";
58
59/// The `anthropic-version` header value the Messages API requires on every
60/// call. `2023-06-01` is the GA version date the API documents as the
61/// required value; features arrive behind request fields, not versions.
62const ANTHROPIC_VERSION: &str = "2023-06-01";
63
64/// The model [`Anthropic::from_env`] uses when `ANTHROPIC_MODEL` is unset:
65/// the current most-capable Claude 5 model id. The Messages API takes
66/// exact ids — no date suffixes. Overriding it has a catch: the JSON path
67/// forces `tool_choice: {"type":"tool"}`, and a model that rejects a
68/// forced tool choice makes every `json_schema` call come back a 400
69/// [`TextModelError::Rejected`].
70pub const DEFAULT_MODEL: &str = "claude-opus-5";
71
72/// The single tool the JSON path declares; its input schema is the
73/// caller's [`Prompt::json_schema`], and forcing it is how a completion
74/// comes back as JSON without the adapter parsing it out of prose.
75const JSON_TOOL_NAME: &str = "respond";
76const JSON_TOOL_DESCRIPTION: &str =
77    "Return the completion as JSON matching the provided schema exactly.";
78
79/// The Anthropic `TextModel` over `POST https://api.anthropic.com/v1/messages`.
80pub struct Anthropic {
81    http: Arc<dyn HttpClient>,
82    /// Needed only to read the HTTP-date form of `Retry-After` (issue #278).
83    /// A constructor argument rather than a builder default so a deployment
84    /// that forgets it fails to compile instead of silently retrying a
85    /// date-form 429 immediately.
86    clock: Arc<dyn Clock>,
87    api_key: Option<String>,
88    model: String,
89}
90
91impl Anthropic {
92    /// `api_key: None` => every call is [`TextModelError::NotConfigured`]
93    /// (no network).
94    pub fn new(
95        http: Arc<dyn HttpClient>,
96        clock: Arc<dyn Clock>,
97        api_key: Option<String>,
98        model: impl Into<String>,
99    ) -> Self {
100        Self {
101            http,
102            clock,
103            api_key,
104            model: model.into(),
105        }
106    }
107
108    /// Reads `ANTHROPIC_API_KEY` and `ANTHROPIC_MODEL` (default
109    /// [`DEFAULT_MODEL`]) from the process environment. On Workers the
110    /// venture should read the secrets from its `Env` and use
111    /// [`Anthropic::new`] instead (`std::env` has no Workers vars).
112    pub fn from_env(http: Arc<dyn HttpClient>, clock: Arc<dyn Clock>) -> Self {
113        Self::new(
114            http,
115            clock,
116            std::env::var("ANTHROPIC_API_KEY").ok(),
117            std::env::var("ANTHROPIC_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_owned()),
118        )
119    }
120
121    /// Status → port error, the one place the provider's taxonomy meets
122    /// ours. The provider's error envelope
123    /// (`{"type":"error","error":{"type":…,"message":…}}`) is parsed once
124    /// for its message; the raw body is the fallback, so a proxy's HTML
125    /// error page still leaves the caller something to log.
126    fn map_status(status: StatusCode, body: &str, retry_after: Option<Duration>) -> TextModelError {
127        let detail = provider_message(body);
128        match status {
129            // "Later", in both shapes the provider says it: an explicit
130            // rate limit (its `Retry-After` honoured in both RFC 9110
131            // forms, through core's parser) and a server on its side.
132            StatusCode::TOO_MANY_REQUESTS => TextModelError::Transient { retry_after },
133            status if status.is_server_error() => TextModelError::Transient { retry_after: None },
134            // The provider refused the request itself: 400 a malformed or
135            // over-limit prompt, 422 content it will not process. Neither
136            // is worth a retry unchanged.
137            StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY => {
138                TextModelError::Rejected(detail)
139            }
140            // A bad, revoked or unauthorised key is a configuration
141            // mistake, not a condition time fixes: retrying with the same
142            // key burns the rate budget and fails the same way.
143            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => TextModelError::Rejected(detail),
144            // Any other 4xx is still the request's fault (404: the model
145            // is not available to this org; 413: the prompt is too large).
146            status if status.is_client_error() => TextModelError::Rejected(detail),
147            // Redirects and anything unmodelled: the call did not complete.
148            _ => TextModelError::Transport(format!("unexpected status {status}: {detail}")),
149        }
150    }
151
152    /// Opens the streaming exchange (issue #859): the request `complete`
153    /// sends with `stream: true` added, the head under the same
154    /// [`HttpPolicy`] deadline, the body under the port's
155    /// [`StreamPolicy`](cratefield_core::StreamPolicy) defaults. Every
156    /// refusal `complete` raises before any body arrives is raised here
157    /// too — an unwired key, an over-limit prompt, a non-2xx head, mapped
158    /// by [`Anthropic::map_status`] off a bounded read of the error body
159    /// — and success is the [`TextDelta`] stream the events fold into.
160    ///
161    /// # Errors
162    ///
163    /// Everything `complete` refuses before a body would arrive:
164    /// [`TextModelError::NotConfigured`], a local
165    /// [`TextModelError::Rejected`] (`wire_request`'s refusal, an
166    /// over-limit image prompt) and the non-2xx mapping.
167    async fn open_stream(
168        &self,
169        prompt: &Prompt,
170    ) -> Result<BoxStream<'static, Result<TextDelta, TextModelError>>, TextModelError> {
171        // An over-limit image prompt is refused before anything is built
172        // or sent, exactly as `complete` refuses it: the limits are about
173        // the request a provider would be handed (issue #628).
174        prompt.check_images()?;
175
176        let Some(api_key) = &self.api_key else {
177            tracing::info!(
178                provider = "anthropic",
179                outcome = "not_configured",
180                model = %self.model,
181                "text model outcome"
182            );
183            return Err(TextModelError::NotConfigured);
184        };
185
186        let mut payload = wire_request(&self.model, prompt)?;
187        payload.stream = true;
188        let request = messages_request(api_key, &payload)?;
189
190        let response = self
191            .http
192            .send_streaming(request)
193            .await
194            .map_err(|err: HttpError| TextModelError::Transport(err.to_string()))?;
195
196        let status = response.status();
197        if !status.is_success() {
198            // A non-2xx head is an ordinary error answer, buffered or not:
199            // map it exactly as `complete` maps one.
200            let retry_after = retry_after(response.headers(), self.clock.as_ref());
201            let body = collect_bounded(response.into_body()).await;
202            let error = Self::map_status(status, &body, retry_after);
203            tracing::warn!(
204                provider = "anthropic",
205                code = status.as_u16(),
206                outcome = "failed",
207                model = %self.model,
208                error = %error,
209                "text model outcome"
210            );
211            return Err(error);
212        }
213
214        Ok(fold_stream(
215            sse_events(response.into_body()),
216            StreamParser::new(&self.model),
217        ))
218    }
219}
220
221const fn wire_role(role: Role) -> &'static str {
222    match role {
223        Role::User => "user",
224        Role::Assistant => "assistant",
225    }
226}
227
228#[derive(serde::Serialize)]
229struct MessagesRequest<'a> {
230    model: &'a str,
231    max_tokens: u32,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    system: Option<&'a str>,
234    messages: Vec<WireTurn<'a>>,
235    #[serde(skip_serializing_if = "Vec::is_empty")]
236    tools: Vec<WireTool<'a>>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    tool_choice: Option<WireToolChoice<'a>>,
239    /// The streaming switch (issue #859): `true` on the stream path only,
240    /// so `complete`'s request body stays byte-for-byte what it always
241    /// was.
242    #[serde(skip_serializing_if = "is_false")]
243    stream: bool,
244}
245
246/// One message on the wire. `content` is the bare string the adapter has
247/// always sent for a turn that carries neither tool calls nor tool results,
248/// and the content-block array the Messages API needs for the tool turns
249/// (issue #665).
250#[derive(serde::Serialize)]
251struct WireTurn<'a> {
252    role: &'static str,
253    content: WireContent<'a>,
254}
255
256/// A turn's content: a bare string, or the block array a tool turn needs.
257/// `untagged` so the plain form stays exactly the string it was before
258/// tools existed — no existing request shape changes.
259#[derive(serde::Serialize)]
260#[serde(untagged)]
261enum WireContent<'a> {
262    Text(&'a str),
263    Blocks(Vec<WireBlock<'a>>),
264}
265
266/// One content block of a tool or image turn.
267#[derive(serde::Serialize)]
268#[serde(tag = "type", rename_all = "snake_case")]
269enum WireBlock<'a> {
270    /// The model's own words alongside (or answering) tool calls, or one
271    /// text [`Part`] of a parts-bearing turn (issue #628).
272    Text { text: &'a str },
273    /// An inline image [`Part`] (issue #628): the base64 source the
274    /// Messages API expects.
275    Image { source: WireImageSource },
276    /// A call the assistant asked for, read back as a previous turn.
277    ToolUse {
278        id: &'a str,
279        name: &'a str,
280        input: &'a serde_json::Value,
281    },
282    /// The result a user turn carries back for one call.
283    ToolResult {
284        tool_use_id: &'a str,
285        content: &'a str,
286        // A successful result omits `is_error`, matching the documented
287        // shape; only a tool-level failure carries it.
288        #[serde(skip_serializing_if = "is_false")]
289        is_error: bool,
290    },
291}
292
293/// The `source` of an inline image block (issue #628): the base64 payload
294/// and media type the Messages API documents for an inline image. `data` is
295/// owned because the encoding is built here from the part's raw bytes.
296#[derive(serde::Serialize)]
297struct WireImageSource {
298    #[serde(rename = "type")]
299    kind: &'static str,
300    media_type: &'static str,
301    data: String,
302}
303
304/// `skip_serializing_if` for the `is_error` flag. Serde passes the field by
305/// reference, so the signature is fixed even though a `bool` is `Copy`.
306#[allow(clippy::trivially_copy_pass_by_ref)]
307fn is_false(value: &bool) -> bool {
308    !*value
309}
310
311#[derive(serde::Serialize)]
312struct WireTool<'a> {
313    name: &'a str,
314    description: &'a str,
315    input_schema: &'a serde_json::Value,
316}
317
318/// The `tool_choice` wire shape: a `type` always, a `name` only for the
319/// `tool` form that pins one tool.
320#[derive(serde::Serialize)]
321struct WireToolChoice<'a> {
322    #[serde(rename = "type")]
323    kind: &'static str,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    name: Option<&'a str>,
326}
327
328/// Maps the port's [`ToolChoice`] onto the Messages wire. The port's enum
329/// is `#[non_exhaustive]`; a choice this adapter does not know falls back
330/// to the provider's own default, `auto`.
331fn wire_tool_choice(choice: &ToolChoice) -> WireToolChoice<'_> {
332    let (kind, name) = match choice {
333        ToolChoice::None => ("none", None),
334        ToolChoice::Required => ("any", None),
335        ToolChoice::Tool(name) => ("tool", Some(name.as_str())),
336        // `Auto`, and any future variant this adapter does not know: the
337        // provider's own default.
338        _ => ("auto", None),
339    };
340    WireToolChoice { kind, name }
341}
342
343/// Serialises one turn. A turn carrying no parts, no tool calls and no tool
344/// results keeps its old shape exactly — `content` as a bare string. Every
345/// other turn becomes the content-block array the Messages API requires:
346/// the turn's [`Part`]s in order (issue #628), then, for a tool turn, the
347/// text block (only when non-empty) and the `tool_use`/`tool_result` blocks.
348///
349/// # Errors
350///
351/// [`TextModelError::Rejected`] when a part is a variant this adapter was
352/// never taught to send: [`Part`] is `#[non_exhaustive]`, and a prompt is
353/// refused rather than sent with a part silently dropped.
354fn wire_turn(turn: &Turn) -> Result<WireTurn<'_>, TextModelError> {
355    let role = wire_role(turn.role);
356    if turn.parts.is_empty() && turn.tool_calls.is_empty() && turn.tool_results.is_empty() {
357        return Ok(WireTurn {
358            role,
359            content: WireContent::Text(turn.content.as_str()),
360        });
361    }
362
363    let mut blocks =
364        Vec::with_capacity(turn.parts.len() + turn.tool_calls.len() + turn.tool_results.len() + 1);
365    // The parts a caller built with `Turn::user_parts` travel in order —
366    // text and image interleaved exactly as given.
367    for part in &turn.parts {
368        match part {
369            Part::Text(text) => blocks.push(WireBlock::Text {
370                text: text.as_str(),
371            }),
372            Part::Image { media_type, bytes } => blocks.push(WireBlock::Image {
373                source: WireImageSource {
374                    kind: "base64",
375                    media_type: media_type.as_str(),
376                    data: encode_image(bytes),
377                },
378            }),
379            // `Part` is `#[non_exhaustive]`: a variant this adapter has not
380            // been taught to serialise is refused rather than dropped, the
381            // same "never silently drop" rule. Not `Unsupported`: no
382            // capability names an unknown content kind.
383            _ => {
384                return Err(TextModelError::Rejected(
385                    "the prompt carries a content part this adapter does not know how to send"
386                        .to_owned(),
387                ));
388            }
389        }
390    }
391    if !turn.tool_calls.is_empty() && !turn.content.is_empty() {
392        blocks.push(WireBlock::Text {
393            text: turn.content.as_str(),
394        });
395    }
396    for call in &turn.tool_calls {
397        blocks.push(WireBlock::ToolUse {
398            id: call.id.as_str(),
399            name: call.name.as_str(),
400            input: &call.arguments,
401        });
402    }
403    for result in &turn.tool_results {
404        blocks.push(WireBlock::ToolResult {
405            tool_use_id: result.tool_call_id.as_str(),
406            content: result.content.as_str(),
407            is_error: result.is_error,
408        });
409    }
410    if !turn.tool_results.is_empty() && !turn.content.is_empty() {
411        blocks.push(WireBlock::Text {
412            text: turn.content.as_str(),
413        });
414    }
415
416    Ok(WireTurn {
417        role,
418        content: WireContent::Blocks(blocks),
419    })
420}
421
422/// Builds the wire body: a single forced tool — input schema = the caller's
423/// — when a schema was asked for; the caller's own `tools` and `tool_choice`
424/// when tools were offered; plain text otherwise. Hand-rolling this JSON
425/// would be a second escaping bug waiting to happen; the prompt is user
426/// content.
427///
428/// A prompt that carries both a [`Prompt::json_schema`] and its own
429/// [`Prompt::tools`] is [`TextModelError::Rejected`]: the schema path
430/// already declares a synthetic forced tool of its own, so the two would
431/// collide on the same request (issue #665). A turn carrying a [`Part`]
432/// this adapter cannot serialise is refused the same way, by
433/// [`wire_turn`].
434fn wire_request<'a>(
435    model: &'a str,
436    prompt: &'a Prompt,
437) -> Result<MessagesRequest<'a>, TextModelError> {
438    if prompt.json_schema.is_some() && !prompt.tools.is_empty() {
439        return Err(TextModelError::Rejected(
440            "a prompt cannot carry both json_schema and tools: the json_schema path already \
441             declares a forced tool of its own, so the two would collide"
442                .to_owned(),
443        ));
444    }
445
446    let (tools, tool_choice): (Vec<WireTool<'a>>, Option<WireToolChoice<'a>>) =
447        match prompt.json_schema.as_ref() {
448            Some(schema) => (
449                vec![WireTool {
450                    name: JSON_TOOL_NAME,
451                    description: JSON_TOOL_DESCRIPTION,
452                    input_schema: schema,
453                }],
454                Some(WireToolChoice {
455                    kind: "tool",
456                    name: Some(JSON_TOOL_NAME),
457                }),
458            ),
459            None => (
460                prompt
461                    .tools
462                    .iter()
463                    .map(|tool| WireTool {
464                        name: tool.name.as_str(),
465                        description: tool.description.as_str(),
466                        input_schema: &tool.parameters,
467                    })
468                    .collect(),
469                prompt.tool_choice.as_ref().map(wire_tool_choice),
470            ),
471        };
472
473    Ok(MessagesRequest {
474        model,
475        max_tokens: prompt.max_tokens,
476        system: prompt.system.as_deref(),
477        messages: prompt
478            .messages
479            .iter()
480            .map(wire_turn)
481            .collect::<Result<Vec<_>, _>>()?,
482        tools,
483        tool_choice,
484        stream: false,
485    })
486}
487
488/// The Messages POST both paths send — `complete` buffered, `stream` with
489/// `"stream": true` added by [`Anthropic::open_stream`]: the same headers,
490/// the same serialisation of [`MessagesRequest`], the same [`HttpPolicy`]
491/// bounds on the head. The caps are the design (issue #430). The port's
492/// default 10 s deadline is a model call's normal case, not its tail, so
493/// the adapter asks for the port ceiling — 30 s, the most a Worker
494/// request can responsibly spend on one upstream — and keeps the port's
495/// body cap. `HttpPolicy::clamped` can only tighten, so this is also the
496/// widest request any caller can get through the adapter. The
497/// consequences are deliberate: keep `max_tokens` modest (a body over the
498/// cap is refused, not truncated), one model call per pipeline stage, and
499/// a timeout surfaces as `Transport` for the caller to judge — no retry
500/// happens in here. A streamed body is bounded by its own
501/// [`StreamPolicy`](cratefield_core::StreamPolicy) instead, and the
502/// defaults are the right ones for a model call (between-token pauses are
503/// quiet time, not dead time), so none is attached. Resend attaches no
504/// policy; this one does, on purpose.
505fn messages_request(
506    api_key: &str,
507    payload: &MessagesRequest<'_>,
508) -> Result<Request<Bytes>, TextModelError> {
509    // Hand-rolling this JSON would be a second escaping bug waiting to
510    // happen; the prompt is user content.
511    let body =
512        serde_json::to_vec(payload).map_err(|err| TextModelError::Transport(err.to_string()))?;
513    let mut request = Request::builder()
514        .method(http::Method::POST)
515        .uri(MESSAGES_ENDPOINT)
516        .header(CONTENT_TYPE, "application/json")
517        .header("x-api-key", api_key)
518        .header("anthropic-version", ANTHROPIC_VERSION)
519        .body(Bytes::from(body))
520        .map_err(|err| TextModelError::Transport(err.to_string()))?;
521    request.extensions_mut().insert(HttpPolicy {
522        timeout: MAX_RESPONSE_TIMEOUT,
523        max_response_bytes: MAX_RESPONSE_BYTES,
524    });
525    Ok(request)
526}
527
528#[derive(serde::Deserialize)]
529struct MessagesResponse {
530    #[serde(default)]
531    model: String,
532    #[serde(default)]
533    content: Vec<ContentBlock>,
534    #[serde(default)]
535    stop_reason: Option<String>,
536    #[serde(default)]
537    usage: Option<WireUsage>,
538}
539
540/// The content blocks the port has a use for. `thinking`,
541/// `redacted_thinking`, server-tool results and whatever ships next parse
542/// as [`ContentBlock::Other`] rather than refusing the whole response —
543/// the adapter was never asked to produce them.
544#[derive(serde::Deserialize)]
545#[serde(tag = "type", rename_all = "snake_case")]
546enum ContentBlock {
547    Text {
548        text: String,
549    },
550    ToolUse {
551        id: String,
552        name: String,
553        input: serde_json::Value,
554    },
555    #[serde(other)]
556    Other,
557}
558
559// The field names are the provider's wire names, kept verbatim so the
560// struct reads against the Messages API reference; the shared `_tokens`
561// postfix is theirs, not ours.
562#[allow(clippy::struct_field_names)]
563#[derive(serde::Deserialize, Default, Clone, Copy)]
564struct WireUsage {
565    #[serde(default)]
566    input_tokens: u32,
567    #[serde(default)]
568    output_tokens: u32,
569    /// Prompt tokens served from the provider's prompt cache (issue #560):
570    /// absent unless the call actually read one. Reported separately from
571    /// `input_tokens` — the wire's `input_tokens` counts only the
572    /// uncached remainder.
573    #[serde(default)]
574    cache_read_input_tokens: Option<u32>,
575    /// Prompt tokens the call wrote into the provider's cache, billed as
576    /// input: absent unless the call cached something new.
577    #[serde(default)]
578    cache_creation_input_tokens: Option<u32>,
579}
580
581#[derive(serde::Deserialize)]
582struct ErrorEnvelope {
583    #[serde(default)]
584    error: Option<ApiErrorBody>,
585}
586
587#[derive(serde::Deserialize)]
588struct ApiErrorBody {
589    #[serde(default)]
590    message: String,
591}
592
593/// The provider's own words, from its error envelope, with the raw body as
594/// the fallback when the envelope does not parse.
595fn provider_message(body: &str) -> String {
596    match serde_json::from_str::<ErrorEnvelope>(body) {
597        Ok(parsed) => match parsed.error {
598            Some(error) if !error.message.is_empty() => error.message,
599            _ => body.to_string(),
600        },
601        Err(_) => body.to_string(),
602    }
603}
604
605/// The wire's split prompt — the uncached remainder, cache writes (billed
606/// as input) and cache reads — summed into the port's one total plus the
607/// cached subset (issue #560). The port wants every completion's
608/// `input_tokens` to be the total the provider processed; only the read
609/// half is `cached_input_tokens`, because only a read was *served* from
610/// the cache.
611fn reported_usage(usage: WireUsage) -> (u64, u64, Option<u64>) {
612    let input_tokens = usage
613        .input_tokens
614        .saturating_add(usage.cache_creation_input_tokens.unwrap_or_default())
615        .saturating_add(usage.cache_read_input_tokens.unwrap_or_default());
616    (
617        u64::from(input_tokens),
618        u64::from(usage.output_tokens),
619        usage.cache_read_input_tokens.map(u64::from),
620    )
621}
622
623/// Reads a parsed answer into the port's three content shapes: the joined
624/// text, the forced tool's input on the schema path, and the calls the model
625/// asked for on every other path (issue #665). The schema path's truncation
626/// and missing-block guards live here because they depend on what the blocks
627/// turned out to be.
628///
629/// The text may be empty when the model only calls tools, and that is not an
630/// error.
631fn interpret_content(
632    prompt: &Prompt,
633    parsed: &MessagesResponse,
634) -> Result<(String, Option<serde_json::Value>, Vec<ToolCall>), TextModelError> {
635    let mut text = String::new();
636    let mut json = None;
637    let mut tool_calls = Vec::new();
638    for block in &parsed.content {
639        match block {
640            ContentBlock::Text { text: block_text } => text.push_str(block_text),
641            // On the schema path the forced tool's input is the answer; on
642            // every other path a `tool_use` block is a call the model asked
643            // for, collected in the order the model named them. A response
644            // the adapter never asked for tools on can still carry one, and
645            // it is a call either way.
646            ContentBlock::ToolUse { id, name, input } => {
647                if prompt.json_schema.is_some() {
648                    json = Some(input.clone());
649                } else {
650                    tool_calls.push(tool_use_call(id, name, input.clone()));
651                }
652            }
653            ContentBlock::Other => {}
654        }
655    }
656
657    if prompt.json_schema.is_some() {
658        // `max_tokens` can cut the forced tool call off mid-JSON, leaving
659        // `input` empty or partial (a JSON `null` parses to `Value::Null`
660        // here) — returning that as a schema-shaped success would be a lie.
661        // `Rejected`, not `Transient`: retrying the identical request
662        // truncates identically; only a larger `max_tokens` changes the
663        // outcome.
664        if parsed.stop_reason.as_deref() == Some("max_tokens") {
665            return Err(TextModelError::Rejected(
666                "the response was truncated at max_tokens before the schema-shaped \
667                 result was complete; a larger max_tokens is needed"
668                    .to_owned(),
669            ));
670        }
671        // Forced tool use was the whole point of the call. A response without
672        // the tool block is not a refusal (that would have been a 4xx) and
673        // not an empty answer — the JSON path broke between provider and
674        // here, which is a transport failure.
675        if json.is_none() {
676            return Err(TextModelError::Transport(
677                "forced tool call came back without a tool_use block".to_owned(),
678            ));
679        }
680    } else {
681        // No schema was requested; `json` stays `None` even if the provider
682        // somehow answered with a tool block — on this path a tool block is a
683        // call, not a structured answer. A text completion cut off by
684        // `max_tokens` is still a useful answer, so it is returned as-is. A
685        // *tool* call cut off at `max_tokens` is not: its `input` may be
686        // partial, and handing that to an executor is exactly what the schema
687        // check downstream exists to prevent. `Rejected`, not `Transient`:
688        // retrying the identical request truncates identically.
689        json = None;
690        if parsed.stop_reason.as_deref() == Some("max_tokens") && !tool_calls.is_empty() {
691            return Err(truncated_tool_call());
692        }
693    }
694
695    Ok((text, json, tool_calls))
696}
697
698/// One `tool_use` block as the port's [`ToolCall`] — the single conversion
699/// the buffered answer and the stream share, so a call reassembled from
700/// `input_json_delta` chunks is exactly the call `complete` builds from
701/// the same block.
702fn tool_use_call(id: &str, name: &str, arguments: serde_json::Value) -> ToolCall {
703    ToolCall::new(id.to_owned(), name.to_owned(), arguments)
704}
705
706/// The truncation refusal `complete` raises when a tool call was cut off
707/// at `max_tokens`, shared with the stream so both paths refuse
708/// identically.
709fn truncated_tool_call() -> TextModelError {
710    TextModelError::Rejected(
711        "the response was truncated at max_tokens before the tool call's arguments \
712         were complete; a larger max_tokens is needed"
713            .to_owned(),
714    )
715}
716
717/// The wire's `stop_reason` → the port's [`FinishReason`]. `refusal` is
718/// the provider withholding content under its own policy: the port's
719/// content-filter bucket.
720fn finish_reason(stop_reason: &str) -> FinishReason {
721    match stop_reason {
722        "end_turn" | "stop_sequence" => FinishReason::Stop,
723        "max_tokens" => FinishReason::Length,
724        "tool_use" => FinishReason::ToolUse,
725        "refusal" => FinishReason::ContentFilter,
726        other => FinishReason::Other(other.to_owned()),
727    }
728}
729
730/// A streamed call's arguments, from the `input_json_delta` text
731/// accumulated by the time `content_block_stop` closes the block. An
732/// empty accumulation is the arguments-less `{}` the buffered wire
733/// carries in the block's `input` — the same value `complete` reads out.
734///
735/// # Errors
736///
737/// [`TextModelError::Transport`] when the accumulation never added up to
738/// JSON: the streaming twin of `complete`'s unparseable-answer failure,
739/// never a half-formed call handed onward.
740fn tool_use_arguments(json: &str) -> Result<serde_json::Value, TextModelError> {
741    if json.is_empty() {
742        return Ok(serde_json::Value::Object(serde_json::Map::new()));
743    }
744    serde_json::from_str(json).map_err(|err| {
745        TextModelError::Transport(format!("the tool call's arguments did not parse: {err}"))
746    })
747}
748
749/// What one streamed event folds into: zero or more deltas, or the one
750/// failure that ends the stream.
751type StreamDeltas = Vec<Result<TextDelta, TextModelError>>;
752
753/// The Messages stream's events, in the wire's shapes. `ping` and any
754/// event type this adapter never learned parse as
755/// [`StreamEvent::Other`](Self::Other) rather than failing the stream —
756/// the same tolerance `complete` gives a content block it does not know.
757#[derive(serde::Deserialize)]
758#[serde(tag = "type", rename_all = "snake_case")]
759enum StreamEvent {
760    /// The answer's head: the model answering it and the prompt-side
761    /// usage.
762    MessageStart { message: StreamMessageHead },
763    /// A content block opened: text, a `tool_use` call — id and name now,
764    /// arguments by delta — or, as [`StreamBlock::Other`], a thinking or
765    /// otherwise unreadable block this adapter follows through its deltas.
766    ContentBlockStart {
767        index: u32,
768        #[serde(rename = "content_block")]
769        block: StreamBlock,
770    },
771    /// One run of content for the block at `index`.
772    ContentBlockDelta { index: u32, delta: StreamBlockDelta },
773    /// The block at `index` is complete.
774    ContentBlockStop { index: u32 },
775    /// The message is wrapping up: its stop reason, and the cumulative
776    /// output count.
777    MessageDelta {
778        delta: StreamStopReason,
779        #[serde(default)]
780        usage: Option<StreamOutputUsage>,
781    },
782    /// The message is over — the stream's one clean end.
783    MessageStop,
784    /// A failure mid-stream, usually `overloaded_error` under load.
785    Error { error: StreamErrorBody },
786    /// `ping`, and whatever ships next.
787    #[serde(other)]
788    Other,
789}
790
791/// The `message` object of a `message_start` event.
792#[derive(serde::Deserialize)]
793struct StreamMessageHead {
794    #[serde(default)]
795    model: String,
796    #[serde(default)]
797    usage: Option<WireUsage>,
798}
799
800/// A content block's opening frame. A text block says nothing of its own
801/// here — its content arrives as deltas.
802#[derive(serde::Deserialize)]
803#[serde(tag = "type", rename_all = "snake_case")]
804enum StreamBlock {
805    Text,
806    /// A call the model asked for: id and name now, arguments by delta.
807    ToolUse {
808        id: String,
809        name: String,
810    },
811    #[serde(other)]
812    Other,
813}
814
815/// One `content_block_delta` frame: a run of answer text, a run of a
816/// `tool_use` block's arguments JSON, or a run of the model's reasoning.
817#[derive(serde::Deserialize)]
818#[serde(tag = "type", rename_all = "snake_case")]
819enum StreamBlockDelta {
820    TextDelta {
821        text: String,
822    },
823    InputJsonDelta {
824        partial_json: String,
825    },
826    ThinkingDelta {
827        thinking: String,
828    },
829    /// `signature_delta`, and whatever ships next: content the port has
830    /// no delta for.
831    #[serde(other)]
832    Other,
833}
834
835/// The `delta` object of a `message_delta` event.
836#[derive(serde::Deserialize)]
837struct StreamStopReason {
838    #[serde(default)]
839    stop_reason: Option<String>,
840}
841
842/// The `usage` object of a `message_delta` event: the output count so
843/// far, cumulative across the whole message.
844#[derive(serde::Deserialize)]
845struct StreamOutputUsage {
846    #[serde(default)]
847    output_tokens: u32,
848}
849
850/// The body of a mid-stream `error` event — the envelope the HTTP errors
851/// use, minus the status line.
852#[derive(serde::Deserialize)]
853struct StreamErrorBody {
854    #[serde(rename = "type", default)]
855    kind: String,
856    #[serde(default)]
857    message: String,
858}
859
860/// One `tool_use` block opened and not yet closed: the ordinal it holds
861/// among the completion's tool calls, the id and name
862/// `content_block_start` named, and the arguments JSON the
863/// `input_json_delta` chunks have accumulated so far.
864struct OpenToolCall {
865    ordinal: usize,
866    id: String,
867    name: String,
868    json: String,
869}
870
871/// Folds the Messages stream's events into the port's [`TextDelta`]s —
872/// the state between two events, owned by the stream
873/// [`Anthropic::stream`] returns alongside the body it reads, so dropping
874/// the stream drops both.
875struct StreamParser {
876    /// The model id this adapter was built with: what `Finish` reports
877    /// when the stream never named one — the same fallback `complete`
878    /// applies to an empty `model` field.
879    fallback_model: String,
880    /// The model `message_start` named.
881    model: Option<String>,
882    /// The prompt side of the usage, summed once at `message_start` by
883    /// [`reported_usage`]: every [`TextDelta::Usage`] this stream emits
884    /// reports it unchanged; only `output_tokens` grows.
885    input_tokens: u64,
886    cached_input_tokens: Option<u64>,
887    /// Completion tokens so far: `message_start`'s first token, then the
888    /// cumulative count each `message_delta` carries.
889    output_tokens: u64,
890    /// The `tool_use` blocks opened and not yet closed, by content-block
891    /// index.
892    open: std::collections::BTreeMap<u32, OpenToolCall>,
893    /// How many `tool_use` blocks have started — the next call's ordinal.
894    /// The port's tool-call `index` is the ordinal among the completion's
895    /// calls (0, 1, …), **not** the stream's content-block index, which
896    /// counts text and thinking blocks too.
897    calls: usize,
898    /// The stop reason `message_delta` named, if it arrived.
899    stop_reason: Option<String>,
900    /// Fused by `Finish` or a failure: every later event is ignored, and
901    /// a body that ends now ends the stream cleanly.
902    done: bool,
903}
904
905impl StreamParser {
906    /// A parser for one streamed answer, falling back to `fallback_model`
907    /// for the `Finish` when the stream names no model.
908    fn new(fallback_model: &str) -> Self {
909        Self {
910            fallback_model: fallback_model.to_owned(),
911            model: None,
912            input_tokens: 0,
913            cached_input_tokens: None,
914            output_tokens: 0,
915            open: std::collections::BTreeMap::new(),
916            calls: 0,
917            stop_reason: None,
918            done: false,
919        }
920    }
921
922    /// Folds one event in, in stream order. Empty for the events the port
923    /// has no delta for: a ping, a text block's start, a signature delta.
924    fn push(&mut self, event: &SseEvent) -> StreamDeltas {
925        if self.done {
926            return Vec::new();
927        }
928        let parsed: StreamEvent = match serde_json::from_str(&event.data) {
929            Ok(parsed) => parsed,
930            Err(err) => {
931                // A 2xx stream whose events do not parse is the streaming
932                // twin of `complete`'s unparseable-body transport failure:
933                // nothing about the prompt was refused.
934                return self.halt(Err(TextModelError::Transport(format!(
935                    "model stream event did not parse: {err}"
936                ))));
937            }
938        };
939        match parsed {
940            StreamEvent::MessageStart { message } => self.message_start(message),
941            StreamEvent::ContentBlockStart { index, block } => match block {
942                StreamBlock::ToolUse { id, name } => {
943                    let ordinal = self.calls;
944                    self.calls += 1;
945                    self.open.insert(
946                        index,
947                        OpenToolCall {
948                            ordinal,
949                            id: id.clone(),
950                            name: name.clone(),
951                            json: String::new(),
952                        },
953                    );
954                    vec![Ok(TextDelta::ToolCallStarted {
955                        index: ordinal,
956                        id,
957                        name,
958                    })]
959                }
960                StreamBlock::Text | StreamBlock::Other => Vec::new(),
961            },
962            StreamEvent::ContentBlockDelta { index, delta } => match delta {
963                StreamBlockDelta::TextDelta { text } => vec![Ok(TextDelta::Text(text))],
964                StreamBlockDelta::ThinkingDelta { thinking } => {
965                    vec![Ok(TextDelta::Reasoning(thinking))]
966                }
967                StreamBlockDelta::InputJsonDelta { partial_json } => {
968                    match self.open.get_mut(&index) {
969                        Some(call) => {
970                            call.json.push_str(&partial_json);
971                            vec![Ok(TextDelta::ToolCallArguments {
972                                index: call.ordinal,
973                                chunk: partial_json,
974                            })]
975                        }
976                        // Arguments for a block that never started have
977                        // nowhere to accumulate; a well-formed stream
978                        // cannot send one, and inventing a block would
979                        // fabricate a call. Ignored.
980                        None => Vec::new(),
981                    }
982                }
983                StreamBlockDelta::Other => Vec::new(),
984            },
985            StreamEvent::ContentBlockStop { index } => match self.open.remove(&index) {
986                Some(call) => vec![finished_tool_call(&call)],
987                None => Vec::new(),
988            },
989            StreamEvent::MessageDelta { delta, usage } => {
990                self.message_delta(delta.stop_reason, usage)
991            }
992            StreamEvent::MessageStop => self.message_stop(),
993            StreamEvent::Error { error } => self.error_event(error),
994            StreamEvent::Other => Vec::new(),
995        }
996    }
997
998    /// The answer's head: fix the prompt-side counts every later
999    /// [`TextDelta::Usage`] reports, and name them once now.
1000    fn message_start(&mut self, message: StreamMessageHead) -> StreamDeltas {
1001        let (input_tokens, output_tokens, cached_input_tokens) =
1002            reported_usage(message.usage.unwrap_or_default());
1003        self.input_tokens = input_tokens;
1004        self.output_tokens = output_tokens;
1005        self.cached_input_tokens = cached_input_tokens;
1006        self.model = (!message.model.is_empty()).then_some(message.model);
1007        vec![Ok(TextDelta::Usage {
1008            input_tokens,
1009            output_tokens,
1010            cached_input_tokens,
1011        })]
1012    }
1013
1014    /// The wrap-up: the stop reason, and the cumulative output count. A
1015    /// `message_delta` without its `usage` block still wraps up — the
1016    /// last known counts stand, and the omission is a warn line, not a
1017    /// silent free completion.
1018    fn message_delta(
1019        &mut self,
1020        stop_reason: Option<String>,
1021        usage: Option<StreamOutputUsage>,
1022    ) -> StreamDeltas {
1023        self.stop_reason = stop_reason;
1024        // A tool call cut off at `max_tokens` is the one place the stream
1025        // refuses where a text answer would only end: the port promises a
1026        // finished call is the whole call `complete` returns, and
1027        // `complete` refuses exactly this truncation rather than hand a
1028        // partial `input` to an executor — so the stream does too, before
1029        // any caller has gone further on the promise. A *text* answer cut
1030        // off at `max_tokens` is `Finish{Length}`, the answer `complete`
1031        // returns as-is.
1032        if self.stop_reason.as_deref() == Some("max_tokens") && self.calls > 0 {
1033            return self.halt(Err(truncated_tool_call()));
1034        }
1035        if let Some(usage) = usage {
1036            self.output_tokens = u64::from(usage.output_tokens);
1037            vec![Ok(TextDelta::Usage {
1038                input_tokens: self.input_tokens,
1039                output_tokens: self.output_tokens,
1040                cached_input_tokens: self.cached_input_tokens,
1041            })]
1042        } else {
1043            // A missing report must not silently read as a free
1044            // completion: said out loud, the same warning the
1045            // openai-compatible adapter gives a server that omits the
1046            // usage block — except the counts here do not default to
1047            // zero, they keep the last known values (message_start's,
1048            // plus any earlier cumulative count).
1049            let model = self
1050                .model
1051                .as_deref()
1052                .unwrap_or(self.fallback_model.as_str());
1053            tracing::warn!(
1054                provider = "anthropic",
1055                model = %model,
1056                "text model completed without a usage block; the last known token counts stand"
1057            );
1058            Vec::new()
1059        }
1060    }
1061
1062    /// The clean end: one `Finish`, naming the wire's stop reason and the
1063    /// model the stream named — the same outcome line `complete` logs.
1064    fn message_stop(&mut self) -> StreamDeltas {
1065        let reason = match self.stop_reason.as_deref() {
1066            // No `message_delta` named a reason: infer it from the shape,
1067            // the way `completion_deltas` would have from a buffered
1068            // answer.
1069            None if self.calls > 0 => FinishReason::ToolUse,
1070            None => FinishReason::Stop,
1071            Some(stop_reason) => finish_reason(stop_reason),
1072        };
1073        let model = self
1074            .model
1075            .take()
1076            .unwrap_or_else(|| self.fallback_model.clone());
1077        tracing::info!(
1078            provider = "anthropic",
1079            outcome = "completed",
1080            model = %model,
1081            input_tokens = self.input_tokens,
1082            output_tokens = self.output_tokens,
1083            "text model outcome"
1084        );
1085        self.halt(Ok(TextDelta::Finish { reason, model }))
1086    }
1087
1088    /// A failure mid-stream. No `Retry-After` rides an event; the
1089    /// transient kinds are the provider's own rate-limit and server
1090    /// vocabulary — the statuses `map_status` transients — and everything
1091    /// else refused the request.
1092    fn error_event(&mut self, error: StreamErrorBody) -> StreamDeltas {
1093        let detail = if error.message.is_empty() {
1094            error.kind.clone()
1095        } else {
1096            error.message
1097        };
1098        let error = match error.kind.as_str() {
1099            "rate_limit_error" | "api_error" | "overloaded_error" => {
1100                TextModelError::Transient { retry_after: None }
1101            }
1102            _ => TextModelError::Rejected(detail),
1103        };
1104        self.halt(Err(error))
1105    }
1106
1107    /// The stream's tail, once the body has ended: the transport failure
1108    /// when `message_stop` never arrived — a stream that stops short is
1109    /// not a success — and nothing when the stream already ended cleanly.
1110    fn finish(&mut self) -> StreamDeltas {
1111        if self.done {
1112            return Vec::new();
1113        }
1114        self.halt(Err(TextModelError::Transport(
1115            "the model's stream ended without a message_stop event".to_owned(),
1116        )))
1117    }
1118
1119    /// Fuses the parser — no further event is folded, and a body that
1120    /// ends now ends the stream cleanly — and yields the one item that
1121    /// ends the stream.
1122    fn halt(&mut self, item: Result<TextDelta, TextModelError>) -> StreamDeltas {
1123        self.done = true;
1124        vec![item]
1125    }
1126}
1127
1128/// A closed `tool_use` block becomes the port's finished call, through
1129/// the one conversion `complete` uses.
1130fn finished_tool_call(call: &OpenToolCall) -> Result<TextDelta, TextModelError> {
1131    tool_use_arguments(&call.json).map(|arguments| TextDelta::ToolCallFinished {
1132        index: call.ordinal,
1133        call: tool_use_call(&call.id, &call.name, arguments),
1134    })
1135}
1136
1137/// Reads a non-2xx streaming body out, capped at the buffered path's
1138/// response ceiling — the provider's error words live there, and a
1139/// hostile head must not cost more than `complete` would pay to read
1140/// them. A body that fails mid-read just ends the collection: whatever
1141/// arrived is still the best available detail.
1142async fn collect_bounded(mut body: ByteStream) -> String {
1143    let mut collected = Vec::new();
1144    while let Some(item) = body.next().await {
1145        let Ok(chunk) = item else {
1146            break;
1147        };
1148        collected.extend_from_slice(&chunk);
1149        if collected.len() >= MAX_RESPONSE_BYTES {
1150            break;
1151        }
1152    }
1153    String::from_utf8_lossy(&collected).into_owned()
1154}
1155
1156/// Merges the SSE event stream into `parser` and yields the port's
1157/// deltas — the same `unfold` shape core's `sse_events` uses. The state
1158/// owns the event stream, which owns the response body: dropping the
1159/// returned stream drops the upstream exchange, the cancellation contract
1160/// the port's streaming half carries. No detached task — the body is
1161/// only ever polled by the stream itself.
1162fn fold_stream(
1163    events: BoxStream<'static, Result<SseEvent, HttpError>>,
1164    parser: StreamParser,
1165) -> BoxStream<'static, Result<TextDelta, TextModelError>> {
1166    Box::pin(futures_util::stream::unfold(
1167        Fold {
1168            events: Some(events),
1169            parser,
1170            pending: Vec::new().into_iter(),
1171        },
1172        |mut fold| async move {
1173            loop {
1174                if let Some(delta) = fold.pending.next() {
1175                    if fold.parser.done {
1176                        // Finish or failure emitted: drop the body instead
1177                        // of reading the provider's closing frames.
1178                        fold.events = None;
1179                    }
1180                    return Some((delta, fold));
1181                }
1182                let mut events = fold.events.take()?;
1183                match events.next().await {
1184                    Some(Ok(event)) => {
1185                        fold.events = Some(events);
1186                        fold.pending = fold.parser.push(&event).into_iter();
1187                    }
1188                    Some(Err(err)) => {
1189                        // The body failed mid-stream: the port's transport
1190                        // error, and the body drops here.
1191                        fold.pending =
1192                            vec![Err(TextModelError::Transport(err.to_string()))].into_iter();
1193                    }
1194                    None => {
1195                        // The body ended; the parser says whether that was
1196                        // a clean end.
1197                        fold.pending = fold.parser.finish().into_iter();
1198                    }
1199                }
1200            }
1201        },
1202    ))
1203}
1204
1205/// The `unfold` state behind [`fold_stream`]: the event stream being
1206/// drained, the parser folding it, and the deltas a folded event
1207/// produced that have not been yielded yet.
1208struct Fold {
1209    events: Option<BoxStream<'static, Result<SseEvent, HttpError>>>,
1210    parser: StreamParser,
1211    pending: std::vec::IntoIter<Result<TextDelta, TextModelError>>,
1212}
1213
1214#[async_trait]
1215impl TextModel for Anthropic {
1216    async fn complete(&self, prompt: &Prompt) -> Result<Completion, TextModelError> {
1217        // An over-limit image prompt is refused before anything is built or
1218        // sent: the limits are about the request a provider would be handed
1219        // (issue #628).
1220        prompt.check_images()?;
1221
1222        // Outcome logging in the resend adapter's shape: provider, status,
1223        // outcome, model, token counts. The API key is never logged, and
1224        // neither is the prompt nor the completion — a prompt is user
1225        // content, and the operator needs none of it to see what happened.
1226        let Some(api_key) = &self.api_key else {
1227            tracing::info!(
1228                provider = "anthropic",
1229                outcome = "not_configured",
1230                model = %self.model,
1231                "text model outcome"
1232            );
1233            return Err(TextModelError::NotConfigured);
1234        };
1235
1236        let payload = wire_request(&self.model, prompt)?;
1237        let request = messages_request(api_key, &payload)?;
1238
1239        let response = self
1240            .http
1241            .send(request)
1242            .await
1243            .map_err(|err: HttpError| TextModelError::Transport(err.to_string()))?;
1244
1245        let status = response.status();
1246        // One parser for both `Retry-After` forms (issue #214/#278); the
1247        // date form needs the clock this adapter is constructed with.
1248        let retry_after = retry_after(response.headers(), self.clock.as_ref());
1249        let text = String::from_utf8_lossy(response.body()).to_string();
1250
1251        if !status.is_success() {
1252            let error = Self::map_status(status, &text, retry_after);
1253            tracing::warn!(
1254                provider = "anthropic",
1255                code = status.as_u16(),
1256                outcome = "failed",
1257                model = %self.model,
1258                error = %error,
1259                "text model outcome"
1260            );
1261            return Err(error);
1262        }
1263
1264        // A 2xx body that does not parse is not a "rejected": nothing about
1265        // the prompt was refused, the answer just never arrived in usable
1266        // form — the same bucket as a cut-off connection.
1267        let parsed: MessagesResponse = serde_json::from_str(&text).map_err(|err| {
1268            TextModelError::Transport(format!("model response did not parse: {err}"))
1269        })?;
1270
1271        let (completion_text, json, tool_calls) = interpret_content(prompt, &parsed)?;
1272
1273        // `Completion` is non-exhaustive and carries the counts flat, so
1274        // the wire's usage block is unwrapped straight onto the builder
1275        // rather than rebuilt as a struct of its own.
1276        let (input_tokens, output_tokens, cached_input_tokens) =
1277            reported_usage(parsed.usage.unwrap_or_default());
1278        let model = if parsed.model.is_empty() {
1279            self.model.clone()
1280        } else {
1281            parsed.model
1282        };
1283        tracing::info!(
1284            provider = "anthropic",
1285            code = status.as_u16(),
1286            outcome = "completed",
1287            model = %model,
1288            input_tokens,
1289            output_tokens,
1290            "text model outcome"
1291        );
1292        let mut completion =
1293            Completion::new(completion_text, model).usage(input_tokens, output_tokens);
1294        if let Some(cached_input_tokens) = cached_input_tokens {
1295            completion = completion.cached_input_tokens(cached_input_tokens);
1296        }
1297        if let Some(json) = json {
1298            completion = completion.json(json);
1299        }
1300        if !tool_calls.is_empty() {
1301            completion = completion.tool_calls(tool_calls);
1302        }
1303        Ok(completion)
1304    }
1305
1306    /// Streams `prompt` off the Messages API's event stream (issue #859):
1307    /// the request `complete` sends with `stream: true` added, the head
1308    /// under the same [`HttpPolicy`], answered as [`TextDelta`]s as the
1309    /// events arrive — text and reasoning as they are produced, each tool
1310    /// call from its start through its argument chunks to the finished
1311    /// call, running usage, and one `Finish` naming the wire's
1312    /// `stop_reason` (`end_turn`/`stop_sequence` →
1313    /// [`FinishReason::Stop`], `max_tokens` → `Length`, `tool_use` →
1314    /// [`FinishReason::ToolUse`], `refusal` →
1315    /// [`FinishReason::ContentFilter`]).
1316    ///
1317    /// The deltas promise what the buffered answer delivers: a finished
1318    /// call is exactly the [`ToolCall`] `complete` builds — both go
1319    /// through the one shared conversion — and a
1320    /// [`CompletionBuilder`](cratefield_core::CompletionBuilder) over the
1321    /// stream equals `complete`'s completion, field for field. Two
1322    /// deliberate divergences:
1323    ///
1324    /// - a prompt carrying [`Prompt::json_schema`] **completes and
1325    ///   decomposes** instead of streaming: `Completion::json` is what
1326    ///   validating the whole answer produces, and only a buffered call
1327    ///   can fill it — the port default's shape, written inline because
1328    ///   an override cannot call the default body;
1329    /// - a **tool call cut off at `max_tokens`** ends the stream with the
1330    ///   truncation refusal `complete` raises, not
1331    ///   [`FinishReason::Length`]: a stream that reported success at a
1332    ///   truncated call would tell a caller who already relayed the
1333    ///   call's start and argument chunks that they add up to a runnable
1334    ///   call. A *text* answer cut off at `max_tokens` is
1335    ///   [`FinishReason::Length`] — the answer `complete` returns as-is.
1336    ///
1337    /// # Cancellation
1338    ///
1339    /// The returned stream owns the response body, so dropping it drops
1340    /// the upstream exchange — the contract
1341    /// [`HttpClient::send_streaming`] carries, and with it the rest of
1342    /// the spend. No detached task: the body is only ever polled by the
1343    /// stream itself.
1344    fn stream<'a>(
1345        &'a self,
1346        prompt: &'a Prompt,
1347    ) -> BoxStream<'a, Result<TextDelta, TextModelError>> {
1348        if prompt.json_schema.is_some() {
1349            return Box::pin(futures_util::stream::once(self.complete(prompt)).flat_map(
1350                |outcome| {
1351                    futures_util::stream::iter(match outcome {
1352                        Ok(completion) => completion_deltas(&completion)
1353                            .into_iter()
1354                            .map(Ok)
1355                            .collect::<Vec<_>>(),
1356                        Err(err) => vec![Err(err)],
1357                    })
1358                },
1359            ));
1360        }
1361        Box::pin(
1362            futures_util::stream::once(self.open_stream(prompt)).flat_map(|opened| match opened {
1363                Ok(deltas) => deltas,
1364                Err(err) => Box::pin(futures_util::stream::iter([Err(err)]))
1365                    as BoxStream<'static, Result<TextDelta, TextModelError>>,
1366            }),
1367        )
1368    }
1369
1370    fn supports(&self, _tier: ModelTier, capability: Capability) -> bool {
1371        // The Messages API carries tools and inline images, so this adapter
1372        // does both — and nothing else. Every current Claude model accepts
1373        // image input, so `Images` is reported for whatever model id this
1374        // adapter was constructed with. The tier is the router's routing key
1375        // and is ignored; `Capability` is `#[non_exhaustive]`, so an
1376        // unclaimed capability stays "no".
1377        matches!(capability, Capability::Tools | Capability::Images)
1378    }
1379}