Simulators
Messages and streaming
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:
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). 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,messagesormax_tokens - an empty
messageslist - a
max_tokensthat 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_schemaor tool name - two tools sharing a name
- a
tool_choicenaming no declared tool, oranywith no tools - a
tool_usewhosetool_resultis not at the head of the next turn - a
tool_resultwith no matchingtool_use - a forced call or
stream: trueatmax_tokens0
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, with first_id, last_id and has_more. 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.