On this page
- The basic request shape
- Treat content as typed data
- System instructions are not ordinary messages
- Multi-turn conversations are client-managed by default
- Tool use is represented in the assistant content
- Tool results belong to the next request
- Do not flatten tool rounds into fake prose
- Streaming is a sequence of semantic events
- Tool arguments can stream incrementally
- Thinking/reasoning should be modeled separately
- Round-trip provider-native blocks when required
- Stop reasons are control state
- max_tokens is a request budget, not a context window
- Usage accounting should be stored as provider data
- Prompt caching affects request construction
- Images and documents are content, not separate chat messages
- Server tools are not the same as client tools
- Beta features need version-aware handling
- Errors should be normalized, not erased
- A clean Anthropic adapter
- Test with non-text blocks
- Where BYOKchat fits
- Further reading
Anthropic’s Messages API is a structured conversation API built around messages containing typed content blocks.
That sounds similar to other chat APIs, but several details matter when you are building a reusable client:
- the system instruction is separate from the
messagesarray; - message content can be text or typed blocks;
- tool use is represented as blocks in the assistant response;
- tool results are sent back as user content blocks;
- streaming is event-oriented rather than “append every string you see”;
- thinking/reasoning features can introduce additional content or continuation requirements;
- server tools and beta features can add more block types over time.
The right architecture is not to flatten all of that into one string.
The basic request shape
A simplified request looks like:
{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"system": "You are a concise technical assistant.",
"messages": [
{
"role": "user",
"content": "Explain event loops."
}
]
}
The response contains a message object whose content is a list of blocks.
A simple text-only response may look conceptually like:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "An event loop..."
}
]
}
The array matters because a response can contain more than one kind of output.
Treat content as typed data
A weak client assumes:
assistant response = one text string
A stronger client assumes:
assistant response = ordered content blocks
Possible block categories can include things such as:
- text;
- tool use;
- thinking/reasoning-related blocks;
- citations or source-bearing content;
- server-tool use/results;
- other provider-defined blocks.
Your app does not need to expose every block directly to the user, but the adapter should understand their semantics.
System instructions are not ordinary messages
Anthropic’s Messages API uses a top-level system field rather than putting a system role into the normal message sequence.
That matters for a provider-neutral model.
Your application can represent instructions semantically:
interface ConversationContext {
systemInstructions?: string;
messages: PortableMessage[];
}
Then the Anthropic adapter maps:
systemInstructions → request.system
portable user/assistant turns → request.messages
Do not leak one provider’s role model into the app’s entire database schema.
Multi-turn conversations are client-managed by default
The usual pattern is to send the relevant conversation history again on each request:
{
"messages": [
{ "role": "user", "content": "What is a mutex?" },
{ "role": "assistant", "content": "A mutex is..." },
{ "role": "user", "content": "How is it different from a semaphore?" }
]
}
Your client therefore still needs:
- durable conversation storage;
- context budgeting;
- summarization/truncation policy;
- edit/regenerate semantics;
- crash recovery.
The API request is not your product database.
Tool use is represented in the assistant content
You provide tool definitions in the request. When Claude decides to use one, the assistant output contains a tool-use block rather than the final answer alone.
Conceptually:
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {
"city": "Tokyo"
}
}
The application then:
- validates the requested tool and arguments;
- applies permission policy;
- executes the tool if allowed;
- sends the result back in a subsequent request;
- lets the model continue.
The model never gets direct authority to execute application code merely because it produced a tool block.
Tool results belong to the next request
A tool result is represented as a content block associated with the tool-use ID.
Conceptually:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "18°C and clear"
}
]
}
The important invariant is the correlation ID.
Your persistence layer should preserve enough provider metadata to reconnect the tool result with the exact call.
Do not flatten tool rounds into fake prose
This is a poor durable representation:
Assistant: TOOL get_weather({city: Tokyo})
User: TOOL RESULT 18°C
It loses typed semantics and makes future migration difficult.
Prefer a neutral conversation item model with explicit tool call/result records, then map to Anthropic blocks at the adapter boundary.
See How AI Tool Calling Works.
Streaming is a sequence of semantic events
Anthropic streaming uses Server-Sent Events and emits events that represent message and content-block lifecycle.
A typical stream can include concepts such as:
message_start
content_block_start
content_block_delta
content_block_stop
message_delta
message_stop
The exact delta payload depends on the block type.
This is why a renderer should not simply concatenate every string field found in the stream.
Normalize provider events into application events such as:
assistantTextDelta
reasoningDelta
toolInputDelta
toolStarted
usageUpdated
responseCompleted
Tool arguments can stream incrementally
During tool use, JSON input may arrive in pieces.
A partial fragment can look like:
{"city":"San
Do not execute the tool yet.
Wait until the block/response boundary indicates the input is complete, then parse and validate it.
See How Streaming Tool Calls Work.
Thinking/reasoning should be modeled separately
Anthropic supports thinking/reasoning features on compatible models and modes. The exact controls and block types can evolve, and not every model exposes identical behavior.
Your client should therefore represent reasoning capability separately from visible answer text.
Useful application states include:
reasoning unsupported
reasoning enabled but hidden
reasoning visible as provider summary/content
provider continuation state required
Do not assume every reasoning block should be stored or rendered as normal assistant prose.
Round-trip provider-native blocks when required
Some provider features require previous native blocks to be passed back exactly or semantically preserved across turns.
This is especially relevant for:
- tool use;
- server tools;
- thinking/reasoning state;
- beta features with special result blocks.
The adapter can preserve opaque/native metadata alongside the portable conversation.
A good rule is:
portable semantic record for product durability
+ provider-native continuation metadata when required
Stop reasons are control state
A response can finish for different reasons.
Your app should distinguish concepts such as:
normal end of turn
maximum token limit
tool use requested
provider pause/resume condition
refusal/safety outcome
failure/cancellation
Do not map every non-error HTTP response to “assistant finished successfully.”
The stop reason may tell the client that another tool or continuation round is required.
max_tokens is a request budget, not a context window
The Messages request includes an output-token budget.
That does not mean the total model context is the same value.
The client must account for:
system instructions
+ conversation history
+ tool definitions
+ attachments
+ provider-specific blocks
+ requested output budget
≤ model constraints
See Context Window vs Output Limit vs Reasoning Tokens.
Usage accounting should be stored as provider data
Usage can include input and output token counts and, depending on features, cache-related or other accounting fields.
Persist usage per request instead of trying to reconstruct it later from text length.
A provider-neutral analytics record might store:
{
inputTokens,
outputTokens,
cachedInputTokens,
providerRawUsage
}
Use optional fields rather than forcing every provider into one identical accounting shape.
Prompt caching affects request construction
Anthropic supports prompt caching features that can reward stable repeated prefixes.
Architecturally, this reinforces a useful ordering principle:
stable instructions/tool definitions first
changing conversation tail later
But do not rewrite prompts solely for cache behavior if it changes semantics.
Cache policy belongs in the provider adapter/context builder, not the UI.
Images and documents are content, not separate chat messages
Multimodal inputs fit naturally into the content-block model.
Your portable attachment representation should preserve:
- media type;
- source type;
- local file identity;
- provider upload/reference metadata where applicable;
- lifecycle/deletion state.
Then the Anthropic adapter converts the attachment into the API’s accepted source form.
Server tools are not the same as client tools
Anthropic can expose provider-hosted tools/features in addition to tools executed by your application.
These differ in an important way:
client tool → your app receives request and performs operation
server tool → provider executes operation inside provider workflow
Privacy, observability, billing, and persistence boundaries differ.
Do not show them as identical just because both appear as tool-related content blocks.
Beta features need version-aware handling
Provider beta features can require special request headers and introduce new content-block variants.
A robust client should keep beta enablement explicit:
interface AnthropicFeatureFlags {
betas: string[];
}
Do not globally send every known beta header.
Tie beta flags to the feature that requires them and test their removal path.
Errors should be normalized, not erased
Keep provider request IDs and raw error metadata for diagnostics, but convert errors into application categories:
authentication
permission
rate_limit
invalid_request
model_unavailable
context_too_large
provider_unavailable
network
unknown
The UI can then present consistent recovery actions without losing provider detail.
A clean Anthropic adapter
The adapter owns:
- top-level system mapping;
- content-block encoding;
- tool block correlation;
- streaming event decoding;
- reasoning/native continuation details;
- provider error mapping.
Test with non-text blocks
A text-only test suite misses most of the difficult parts.
Include fixtures for:
multiple text blocks
tool use
streamed partial tool JSON
tool result round trip
reasoning block
unknown additive block type
max-token stop
rate limit
stream disconnect
server-tool pause/resume
usage metadata changes
Unknown block types should not crash the whole conversation parser.
Where BYOKchat fits
A multi-provider client can support Anthropic deeply without making the rest of the app Anthropic-shaped. The portable conversation owns user-visible history, while the Anthropic adapter owns Messages API content blocks, tool correlation, streaming semantics, and provider-native continuation details.
That preserves both feature depth and provider portability.