# Messages and streaming

Messages, token counting and models: replies from the account's model table, request and tool checks as the API makes them and streaming as server-sent events.

Messages is the call your code makes around a model. It returns an assistant message with a stop reason and usage, without calling a model. Send a message, count tokens, trigger an error, stream a reply and force a tool call:

```bash
A=(-H 'x-api-key: sk-ant-placeholder' -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json')
curl -s "${A[@]}" "$RYSTIC_URL/v1/messages" -d '{"model":"claude-haiku-4-5-20251001","max_tokens":64,"messages":[{"role":"user","content":"Say hello"}]}' | jq -c '{role, stop_reason, text: .content[0].text}'
# -> {"role":"assistant","stop_reason":"end_turn","text":"Hello! How can I help you today?"}
curl -s "${A[@]}" "$RYSTIC_URL/v1/messages" -d '{"model":"claude-sonnet-4-5","max_tokens":64,"messages":[{"role":"user","content":"Say hello"}]}' | jq -c '{model}'
# -> {"model":"claude-sonnet-4-5-20250929"}
curl -s "${A[@]}" "$RYSTIC_URL/v1/messages/count_tokens" -d '{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"Say hello"}]}'
# -> {"input_tokens":12}
curl -s "${A[@]}" "$RYSTIC_URL/v1/messages" -d '{"model":"no-such-model","max_tokens":64,"messages":[{"role":"user","content":"Say hello"}]}' | jq -c '{type: .error.type, message: .error.message}'
# -> {"type":"not_found_error","message":"model: no-such-model"}
curl -sN "${A[@]}" "$RYSTIC_URL/v1/messages" -d '{"model":"claude-haiku-4-5-20251001","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"Say hello"}]}' | grep '^event:'
# -> event: message_start
# -> event: content_block_start
# -> event: content_block_delta
# -> event: content_block_stop
# -> event: message_delta
# -> event: message_stop
curl -s "${A[@]}" "$RYSTIC_URL/v1/messages" -d '{"model":"claude-haiku-4-5-20251001","max_tokens":64,"tool_choice":{"type":"any"},"tools":[{"name":"get_weather","description":"Current weather for a city.","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"messages":[{"role":"user","content":"Weather in Paris?"}]}' | jq -c '{stop_reason, name: .content[0].name, input: .content[0].input}'
# -> {"stop_reason":"tool_use","name":"get_weather","input":{"city":"string"}}
```

### What a reply holds

A reply is an assistant message in the API's shape, from a model the account's model table lists. Any other model returns 404 `not_found_error` with the message "model: `<id>`". An alias such as `claude-sonnet-4-5` names its dated model, which the reply's `model` carries. `POST /v1/messages/count_tokens` resolves aliases the same way. A reply longer than `max_tokens` is cut short with `stop_reason` `max_tokens`.

**Differs from the Claude API:** a reply's text is the simulator's own and the same whatever you ask, unless a test scripts it ([Agent loops](https://www.rystic.ai/docs/simulators/anthropic/features/agent-loop.md)). No reply follows the prompt, carries thinking blocks or calls a server tool such as web search or code execution.

**Differs from the Claude API:** a reply's `usage` counts are the simulator's own, so `count_tokens` need not match the reply's `input_tokens`.

### Request checks

Each of these returns 400 `invalid_request_error` with the API's message:

- a body that is not JSON
- a missing `model`, `messages` or `max_tokens`
- an empty `messages` list
- a `max_tokens` that is not a non-negative integer

Sampling parameters are checked by model:

| Model | Returns 400 for |
|---|---|
| Claude Opus 4.7 and every newer model | a `temperature` other than 1.0, any `top_k` or a `top_p` below 0.99 |
| older models | `temperature` sent with `top_p`; each sampling parameter alone is accepted |

### Tool checks

Tool definitions, `tool_choice` and tool turns are checked as the API checks them, before the model is looked up. Each of these returns 400 `invalid_request_error` with the API's message:

- a malformed `input_schema` or tool name
- two tools sharing a name
- a `tool_choice` naming no declared tool, or `any` with no tools
- a `tool_use` whose `tool_result` is not at the head of the next turn
- a `tool_result` with no matching `tool_use`
- a forced call or `stream: true` at `max_tokens` 0

Unless a test scripts the reply, `tool_choice` alone decides whether it calls a tool:

| `tool_choice` | The reply |
|---|---|
| `tool` or `any` (a forced call) | a `tool_use` block, with `stop_reason` `tool_use` |
| `auto`, `none` or no `tool_choice` | text |

**Differs from the Claude API:** with `auto` or no `tool_choice`, the simulator never decides by itself to call a tool, so the reply is text.

The `tool_use` block carries an `id`, the tool's `name`, a `caller` and an `input` that the tool's schema accepts.

### Streaming

`stream: true` returns the same message as server-sent events, `message_start` through `message_stop`, which the SDKs' `messages.stream()` reads.

**Differs from the Claude API:** a stream arrives whole, one delta per content block, all at once. It carries no `ping` and no mid-stream `error` event.

### Models

`GET /v1/models` lists the model table, newest first, paged as [Pagination](https://www.rystic.ai/docs/simulators/anthropic.md#pagination) describes. `GET /v1/models/{model_id}` reads one model by its id or an alias. It returns the model's `capabilities` in the API's shape, with `max_input_tokens` and `max_tokens`. An empty account lists the API's 14 models, and every scenario adds `claude-opus-4-1-20250805`.

---

Source: https://www.rystic.ai/docs/simulators/anthropic/features/messages · Markdown: https://www.rystic.ai/docs/simulators/anthropic/features/messages.md
