Expand description
cratefield-adapter-anthropic: the minimal TextModel port over the
Anthropic Messages API (issue #430). Uses the runtime’s HttpClient
port — no reqwest, no vendor SDK — so the same adapter runs on
Workers and natively, and inherits that port’s destination vetting,
deadline and response-size caps.
Degraded mode. When the API key is absent the adapter answers
TextModelError::NotConfigured without any network call, so a
pipeline stage can degrade instead of breaking.
Tools (issue #665): Prompt::tools travel as the Messages API’s
tools array, Prompt::tool_choice as its tool_choice, and the
answer’s tool_use blocks come back as Completion::tool_calls. A
turn built with Turn::assistant_tool_calls or
Turn::tool_results is sent as the content-block array the API
requires; every other turn keeps the bare-string content it always sent.
Anthropic::supports reports Capability::Tools. A prompt that
carries both Prompt::json_schema and its own tools is refused with
TextModelError::Rejected before any request.
Streaming (issue #859): Anthropic::stream sends the same
request with "stream": true and answers the port’s TextDeltas
straight off the Messages event stream — text and reasoning as they
are produced, each tool call from its start through its argument
chunks to the finished call, running usage, and one Finish naming
the wire’s stop_reason. A finished call is the same ToolCall
complete builds — one shared conversion — and the stream owns the
response body, so dropping it cancels the upstream exchange. A prompt
carrying Prompt::json_schema still completes and decomposes:
Completion::json is a buffered feature.
Images (issue #628): a turn built with Turn::user_parts is sent
as the content-block array the Messages API requires — each Part as
a text block or an inline base64 image block, in order — and
Anthropic::supports reports Capability::Images, because every
current Claude model accepts image input. An over-limit prompt is
refused by Prompt::check_images before any network call.
§cratefield-adapter-anthropic
TextModel port over the Anthropic Messages API
for the Cratefield harness — the first text-model adapter (issue #430).
Uses the runtime’s HttpClient port — no reqwest, no vendor SDK — so it
runs unchanged on Workers (worker::Fetch) and natively, and inherits
that port’s destination vetting, deadline and response-size caps.
§Usage
use std::sync::Arc;
use cratefield_adapter_anthropic::Anthropic;
use cratefield_core::{Prompt, TextModel};
use cratefield_runtime_cloudflare::{FetchClient, WorkersClock};
// With a key:
let model = Anthropic::new(
Arc::new(FetchClient),
Arc::new(WorkersClock),
Some(key),
"claude-opus-5",
);
// Without a key (degraded mode): complete() -> Err(NotConfigured), with
// no network call. No retries inside the adapter:
let completion = model
.complete(Prompt::user("Summarise this in one line", 256))
.await?;Anthropic::from_env(http, clock) reads ANTHROPIC_API_KEY and
ANTHROPIC_MODEL (default claude-opus-5) from the process environment
(native/self-hosted). On Workers, read the secret from the venture’s
Env and call Anthropic::new. The Clock is what lets the adapter
read the HTTP-date form of Retry-After (issue #278); without it a
date-form 429 would read as “retry now”.
With Prompt::json_schema set, the request declares a single tool whose
input schema is the caller’s schema and forces it with
tool_choice: {type: "tool"}; the tool input comes back as
Completion::json. Without it, no tools are sent and json stays
None.
With Prompt::tools set (issue #665), the request carries a tools array
— each spec’s parameters as the tool’s input_schema — and, when the
caller steered it, a tool_choice (Auto → {type: "auto"}, None →
{type: "none"}, Required → {type: "any"}, Tool(n) →
{type: "tool", name: n}; omitted when unset). The answer’s tool_use
blocks come back as Completion::tool_calls, and a turn built with
Turn::assistant_tool_calls or Turn::tool_results is sent as the
content-block array the Messages API expects — every other turn keeps the
bare-string content it always sent. The model’s text may be empty when it
only calls tools, and that is not an error. A tool call cut off by
max_tokens is refused as Rejected rather than handed on with partial
arguments. A prompt that sets both json_schema and tools is refused as
Rejected before any request: the schema path already declares its own
forced tool, so the two would collide.
§Streaming
stream (issue #859) sends the same request with "stream": true and
answers cratefield_core::TextDeltas straight off the Messages event
stream: text and reasoning as they are produced, each tool call from its
start through its argument chunks to the finished call, running usage, and
one Finish naming the wire’s stop_reason. A finished call is exactly
the cratefield_core::ToolCall complete returns for the same answer —
one shared conversion — and a CompletionBuilder over the stream equals
complete’s completion, field for field. The stream owns the response
body, so dropping it cancels the upstream exchange. Two divergences: a
json_schema prompt still completes and decomposes (structured output is
a buffered feature), and a tool call cut off at max_tokens ends the
stream with the same Rejected truncation refusal complete raises —
never a half-runnable call.
With a Turn::user_parts message (issue #628), the turn goes as the
Messages API content-block array: each Part::Text as a text block and
each Part::Image as an inline image block, in order. supports reports
Capability::Images for every current Claude model, and the port’s bounds
are enforced locally before any request, an over-limit prompt refused as
TextModelError::ImageLimit rather than paid for.
Every completion reports token usage. The Messages API splits the prompt
across input_tokens, cache_creation_input_tokens and
cache_read_input_tokens when prompt caching is in play; the adapter sums
them into the one input_tokens total the port carries, and reports the
read half alone as cached_input_tokens — None when the answer carried
no cache fields at all.
Error mapping (to cratefield_core::TextModelError): 429 →
Transient { retry_after } (from the Retry-After header, both RFC 9110
forms), any 5xx — including Anthropic’s 529 overloaded_error →
Transient { retry_after: None }, 400/422 → Rejected (malformed
request, filtered content), 401/403 → Rejected (a bad key is not worth
retrying), any other 4xx → Rejected. Everything that stops the call
from completing maps to Transport: timeout, blocked destination, a body
over the response cap, a success body that does not parse, a forced JSON
call that comes back without its tool block. No error Display ever
includes the API key, and neither the prompt nor the completion text is
ever logged — a prompt is user content.
§The caps are the design
The adapter attaches the port’s widest HttpPolicy to every request: a
30 s deadline — the HttpClient ceiling, up from that port’s 10 s
default, because a model call’s normal case sits past it — and the port’s
4 MiB response cap, which HttpPolicy::clamped lets a caller tighten but
never raise. What that implies for a caller: keep max_tokens modest
(an oversized completion is refused as Transport, not truncated), make
one model call per pipeline stage, and let a timeout surface as
Transport for the caller to judge — never a retry inside the adapter.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Structs§
- Anthropic
- The Anthropic
TextModeloverPOST https://api.anthropic.com/v1/messages.
Constants§
- DEFAULT_
MODEL - The model
Anthropic::from_envuses whenANTHROPIC_MODELis unset: the current most-capable Claude 5 model id. The Messages API takes exact ids — no date suffixes. Overriding it has a catch: the JSON path forcestool_choice: {"type":"tool"}, and a model that rejects a forced tool choice makes everyjson_schemacall come back a 400TextModelError::Rejected.