# Anthropic simulator

A stateful Claude API simulator — Messages and token counting, models, files, message batches, skills and the Managed Agents beta, with no model called and nothing billed.

`anthropic-twin` is a local, stateful simulator of the Claude API on `api.anthropic.com` — Messages and token counting, models, files, message batches with their JSONL results, skills with their versions and the Managed Agents beta: agents, environments, sessions with their events and resources, vaults with their credentials and memory stores with their memories and versions. It answers the API's shapes without calling a model: a Messages reply is an assistant message with a stop reason and usage, and its text is the twin's own, so test the code around the model, not the model's answers. Nothing reaches Anthropic and no tokens are billed.

## Quickstart

You need an API key from [Account → API keys](https://www.rystic.ai/account/api-keys) ([how](https://www.rystic.ai/docs/licensing.md#get-an-api-key)).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                                                          # your API key
rystic run anthropic-twin -d --port 0 --scenario anthropic_skill_library    # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version
```

Expect `"service_hosts":["api.anthropic.com"]`. The examples below use `localhost:8080`; use the port `rystic run` printed. On Enterprise, `rystic login ./rystic-license.json` takes the place of the key ([license files](https://www.rystic.ai/docs/licensing.md#enterprise-license-files)).

Send a message, then create a skill and read it back:

```bash
A=(-H 'x-api-key: sk-ant-placeholder' -H 'anthropic-version: 2023-06-01')
curl -s "${A[@]}" -H 'content-type: application/json' localhost:8080/v1/messages \
  -d '{"model":"claude-haiku-4-5-20251001","max_tokens":64,"messages":[{"role":"user","content":"Say hello"}]}'
# -> {…,"role":"assistant","stop_reason":"end_turn",…,"type":"message","usage":{…}}

mkdir -p release-notes && printf -- '---\nname: release-notes\ndescription: Drafts release notes.\n---\nWrite notes.\n' > release-notes/SKILL.md
curl -s "${A[@]}" localhost:8080/v1/skills -F 'display_title=Release notes' \
  -F 'files[]=@release-notes/SKILL.md;filename=release-notes/SKILL.md'
# -> {…,"display_name":"Release notes","id":"skill_01J00000000000000000000001",…}
curl -s "${A[@]}" localhost:8080/v1/skills/skill_01J00000000000000000000001
# -> {…,"display_name":"Release notes",…,"type":"skill",…}
```

## Point your client at it

Anthropic's SDKs build every URL on `https://api.anthropic.com`: set the base URL to the twin, and nothing else changes. **A request left unpointed goes to the real Claude API, and is billed.** The twin reads the key from `x-api-key` or `Authorization: Bearer`, whichever your SDK release sends.

Python (`anthropic`):

```python
import anthropic

client = anthropic.Anthropic(base_url="http://localhost:8080", api_key="sk-ant-placeholder")
client.messages.create(model="claude-haiku-4-5-20251001", max_tokens=64,
                       messages=[{"role": "user", "content": "Say hello"}])
```

TypeScript (`@anthropic-ai/sdk`):

```ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ baseURL: "http://localhost:8080", apiKey: "sk-ant-placeholder" });
await client.skills.list();
```

The SDKs' beta methods (`client.beta.agents`, `client.beta.sessions` and the rest) reach the twin too, with the `?beta=true` they send.

## Plans

`anthropic-twin` is on Pro and Enterprise, not Free ([plans](https://www.rystic.ai/docs/licensing.md#plans)); on a Free key, `rystic pull anthropic-twin` and a start are refused with `product_not_in_plan`. Both plans serve all 73 Claude API routes, every control route and all five scenarios. Without `--scenario` the twin boots an **empty** account — the API's 14 models and no skill, file, batch or agent — and `POST /_rystic/reset` wipes it back to that.

## Auth

The twin takes any API key, in `x-api-key` or as `Authorization: Bearer`; no Anthropic account or key is involved, and the same key in both headers reads as one. A request with neither is refused 401 `authentication_error` before any record is looked up; a file upload without one answers 404 `not_found_error` instead. The Managed Agents routes take `anthropic-beta: managed-agents-2026-04-01`, and a memory-store request without it answers 404 `not_found_error`, as on the API. Every request also carries `anthropic-version`, as the SDKs send it: the twin accepts `2023-01-01`, `2023-06-01` and `2026-10-21` and answers them alike, and a missing, empty or unknown version answers 400 `invalid_request_error`.

## Coverage

The twin serves 73 operations. Messages: create, answered from a model the account's model table lists (any other model answers 404 `not_found_error` "model: <id>") and stopping at `max_tokens` with `stop_reason` `max_tokens`, and `count_tokens`; an alias such as `claude-sonnet-4-5` names its dated model on both. A body that is not JSON, a missing `model`, `messages` or `max_tokens` and an empty `messages` list answer the API's 400 `invalid_request_error`, and Claude Opus 4.7 and every newer model refuse a `temperature` other than 1.0, any `top_k` and a `top_p` below 0.99, as on the API. Models: list (newest first) and read by id or alias, each with its `capabilities` in the API's shape. Files: multipart upload with an optional `expires_in_seconds`, list, read, delete and download — a file you upload is not downloadable, and its `/content` answers 400 `file_not_downloadable`, as on the API. Message batches: create (`in_progress`, expiring 24 hours on), list, read, cancel (the batch ends at once, every request canceled), results of an ended batch as `application/x-jsonl` and delete of an ended batch. Skills: create from `files[]` parts with a `SKILL.md` at their root, list, read, delete and versions created, listed, read and deleted, each new version becoming `latest_version_id`. Managed Agents: agents created, read, updated into new versions and archived; environments, vaults and their credentials and memory stores, each created, read, updated, archived and deleted; a memory store's memories written, read and deleted, each change kept as a memory version; sessions created from an agent and an environment, their events sent and listed, their resources managed. Every error but the streaming refusal is the API's envelope, `{"type":"error","error":{"type","message"}}`.

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs anthropic; do not edit) -->

<!-- coverage-claim -->
73 routes, counted from the simulator's literal registrations.

| Resource | Operations |
|---|---|
| Agent (5) | `GET, POST /v1/agents`; `GET, POST /v1/agents/{agent_id}`; `POST /v1/agents/{agent_id}/archive` |
| Agent Version (1) | `GET /v1/agents/{agent_id}/versions` |
| Environment (6) | `GET, POST /v1/environments`; `GET, POST, DELETE /v1/environments/{environment_id}`; `POST /v1/environments/{environment_id}/archive` |
| File (5) | `GET, POST /v1/files`; `GET, DELETE /v1/files/{file_id}`; `GET /v1/files/{file_id}/content` |
| Memory (3) | `POST /v1/memory_stores/{memory_store_id}/memories`; `POST, DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}` |
| Memory Store (8) | `GET, POST /v1/memory_stores`; `GET, POST, DELETE /v1/memory_stores/{memory_store_id}`; `POST /v1/memory_stores/{memory_store_id}/archive`; `GET /v1/memory_stores/{memory_store_id}/memories`; `GET /v1/memory_stores/{memory_store_id}/memories/{memory_id}` |
| Memory Version (2) | `GET /v1/memory_stores/{memory_store_id}/memory_versions`; `GET /v1/memory_stores/{memory_store_id}/memory_versions/{memory_version_id}` |
| Message Batch (6) | `GET, POST /v1/messages/batches`; `GET, DELETE /v1/messages/batches/{message_batch_id}`; `POST /v1/messages/batches/{message_batch_id}/cancel`; `GET /v1/messages/batches/{message_batch_id}/results` |
| Model (4) | `POST /v1/messages`; `POST /v1/messages/count_tokens`; `GET /v1/models`; `GET /v1/models/{model_id}` |
| Session (6) | `GET, POST /v1/sessions`; `GET, POST, DELETE /v1/sessions/{session_id}`; `POST /v1/sessions/{session_id}/archive` |
| Session Event (2) | `GET, POST /v1/sessions/{session_id}/events` |
| Session Resource (5) | `GET, POST /v1/sessions/{session_id}/resources`; `GET, POST, DELETE /v1/sessions/{session_id}/resources/{resource_id}` |
| Skill (4) | `GET, POST /v1/skills`; `GET, DELETE /v1/skills/{skill_id}` |
| Skill Version (4) | `GET, POST /v1/skills/{skill_id}/versions`; `GET, DELETE /v1/skills/{skill_id}/versions/{version}` |
| Vault (6) | `GET, POST /v1/vaults`; `GET, POST, DELETE /v1/vaults/{vault_id}`; `POST /v1/vaults/{vault_id}/archive` |
| Vault Credential (6) | `GET, POST /v1/vaults/{vault_id}/credentials`; `GET, POST, DELETE /v1/vaults/{vault_id}/credentials/{credential_id}`; `POST /v1/vaults/{vault_id}/credentials/{credential_id}/archive` |

<!-- END generated: served-operations -->

Measured against the live Claude API: the newest full-suite round, on 2026-10-31 (stamp `20261031T052431Z`), matched 458 of the suite's 476 tests (96.2%), none inconclusive, against a target of 93%.

## Control plane

Every route under `/_rystic/` answers on both plans ([Plans](#plans)); [routes by plan](https://www.rystic.ai/docs/control-plane.md#routes-by-plan) lists them.

## Scenarios

Every scenario pins the clock at `2026-02-10T00:00:00Z` and lists the API's 14 models and `claude-opus-4-1-20250805`, so a Messages call works in any of them.

- `anthropic_skill_library`: three skills with six versions between them — Invoice Parser and Brand Style Guide (custom) and PDF Report Builder (from Anthropic) — so skill and version reads return real records from the first call.
- `anthropic_batch_queue`: three message batches, one in progress, one canceling and one ended, with eleven per-request rows; the ended batch's results read back as JSONL.
- `anthropic_batch_archive`: three ended batches over five weeks, one archived, with succeeded, errored and expired results and nothing still running.
- `anthropic_file_vault`: four files — a PDF, a CSV and a PNG (each downloadable) and a text upload that expired on 2026-02-01 — so listing, downloading and the expired file's 404 all have targets.
- `anthropic_fresh_workspace`: the models and nothing else, so every collection starts empty.

## Seeding state

`POST /_rystic/seed` takes a register map — `state_skill`, `state_skill_version`, `state_file`, `state_message_batch`, `state_batch_request`, `state_model_info`, `state_agent`, `state_environment`, `state_session`, `state_vault`, `state_memory_store` and the rest `GET /_rystic/registers` lists, each keyed by the record's id — and merges it into the running account; [Scenarios](https://www.rystic.ai/docs/concepts/worlds.md) covers seed files, `GET /_rystic/registers` and the seed gate.

```bash
curl -X POST localhost:8080/_rystic/seed -H 'Content-Type: application/json' -d '{
  "state_skill": {"skill_01SeededSkill000001": {"id": "skill_01SeededSkill000001", "type": "skill",
    "display_name": "Seeded", "source": {"type": "custom"}, "latest_version_id": "skillver_01Seeded",
    "created_at": "2026-02-01T00:00:00Z", "updated_at": "2026-02-01T00:00:00Z"}}
}'
# -> 204; GET /v1/skills/skill_01SeededSkill000001 then reads it back
```

## Determinism & reset

Ids are sequences the reset rewinds, so the first skill created after a reset has the same id every time, and the clock starts at a pinned instant — `2026-01-01T00:00:00Z` empty, `2026-02-10T00:00:00Z` in every scenario — and moves one millisecond for each record written, never on a read and never by wall time. Two identical runs from the same reset answer the same ids and timestamps. [Reset and determinism](https://www.rystic.ai/docs/concepts/reset-and-determinism.md) covers reset, the clock and the RNG.

## Egress

None. The twin calls no model and makes no outbound call to Anthropic; `GET /_rystic/egress` is always empty. The metered
edition, which an API-key start runs, also reaches Rystic's lease server to
start and to renew its lease while it runs.

## Limitations

- The model is not called: a reply's text does not follow the prompt, and its `usage` counts are the twin's own, so `count_tokens` need not match a reply's `input_tokens`.
- No streaming: `stream: true` is refused with 501 and a body of the twin's own, not answered with server-sent events, so the SDKs' `messages.stream()` raises an error against the twin.
- Nothing runs on its own: a batch you create stays `in_progress` until you cancel it, and a session records the events you send and answers none.
- No rate limits and no `request-id` or `anthropic-ratelimit-*` response headers. Every id is the twin's own; treat ids and page cursors as opaque.

Left out on purpose: the Admin API (organizations, users, invites, workspaces, API keys, usage and cost reports), Text Completions, sessions' event stream and the other routes the list below names.

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs anthropic; do not edit) -->

**Known issues.**

- Most error answers carry `"request_id": null`, where the API always sends a `req_…` string; read an error's `type` and `message`, not its `request_id`.
- A session never reads `running`: after a `user.message` event the twin lets you delete it at once, where the API refuses `DELETE` with 400 until the session is interrupted or finishes; send a `user.interrupt` event before deleting, as you would against the API.
- A message batch never ends at its `expires_at` when you advance the clock past it, and an archived batch still serves its results; don't rely on batch expiry in a test that moves the clock a day or more.
- A request carrying two different API keys, one in `x-api-key` and another as `Authorization: Bearer`, is answered, where the API refuses it with 401 `authentication_error`; send one key, or the same key in both headers.
- A request without an API key is refused 401 `authentication_error` before its path is checked, where on some routes the API checks the path first: a message batch id not in the API's format answers 400 `invalid_request_error`, and a memory path with such ids 404 `not_found_error`. One that also lacks an `anthropic-version` value answers 400 here, where the API answers 401.

**Not served.** 171 of the 244 operations in the published spec are not served.

- environments, 6 of 14 served: `GET /v1/environments/{environment_id}/work/poll?beta=true`, `GET /v1/environments/{environment_id}/work/stats?beta=true`, `POST /v1/environments/{environment_id}/work/{work_id}/ack?beta=true`, `POST /v1/environments/{environment_id}/work/{work_id}/heartbeat?beta=true`, `POST /v1/environments/{environment_id}/work/{work_id}/stop?beta=true`, `GET /v1/environments/{environment_id}/work/{work_id}?beta=true`, `POST /v1/environments/{environment_id}/work/{work_id}?beta=true`, `GET /v1/environments/{environment_id}/work?beta=true`
- files, 5 of 10 served: `GET /v1/files/{file_id}/content?beta=true`, `GET /v1/files/{file_id}?beta=true`, `DELETE /v1/files/{file_id}?beta=true`, `GET /v1/files?beta=true`, `POST /v1/files?beta=true`
- memory_stores, 13 of 14 served: `POST /v1/memory_stores/{memory_store_id}/memory_versions/{memory_version_id}/redact?beta=true`
- messages, 8 of 16 served: `POST /v1/messages/batches/{message_batch_id}/cancel?beta=true`, `GET /v1/messages/batches/{message_batch_id}/results?beta=true`, `GET /v1/messages/batches/{message_batch_id}?beta=true`, `DELETE /v1/messages/batches/{message_batch_id}?beta=true`, `GET /v1/messages/batches?beta=true`, `POST /v1/messages/batches?beta=true`, `POST /v1/messages/count_tokens?beta=true`, `POST /v1/messages?beta=true`
- models, 2 of 4 served: `GET /v1/models/{model_id}?beta=true`, `GET /v1/models?beta=true`
- sessions, 13 of 19 served: `GET /v1/sessions/{session_id}/events/stream?beta=true`, `POST /v1/sessions/{session_id}/threads/{thread_id}/archive?beta=true`, `GET /v1/sessions/{session_id}/threads/{thread_id}/events?beta=true`, `GET /v1/sessions/{session_id}/threads/{thread_id}/stream?beta=true`, `GET /v1/sessions/{session_id}/threads/{thread_id}?beta=true`, `GET /v1/sessions/{session_id}/threads?beta=true`
- skills, 8 of 17 served: `GET /v1/skills/{skill_id}/versions/{version}/content?beta=true`, `GET /v1/skills/{skill_id}/versions/{version}?beta=true`, `DELETE /v1/skills/{skill_id}/versions/{version}?beta=true`, `GET /v1/skills/{skill_id}/versions?beta=true`, `POST /v1/skills/{skill_id}/versions?beta=true`, `GET /v1/skills/{skill_id}?beta=true`, `DELETE /v1/skills/{skill_id}?beta=true`, `GET /v1/skills?beta=true`, … and 1 more
- vaults, 12 of 13 served: `POST /v1/vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate?beta=true`
- Not served at all: complete (1), deployment_runs (2), deployments (8), dreams (5), organizations (100), tunnels (10), user_profiles (5)

**Required fields the twin may leave out.** The spec requires these in a success response, and the twin's response may not carry them:

- `GET /v1/vaults/{vault_id}/credentials` (200): `data/*/auth/mcp_server_url`
- `POST /v1/vaults/{vault_id}/credentials/{credential_id}/archive` (200): `auth/mcp_server_url`

**Not measured.** The fidelity suite runs 476 tests, and 176 documented branches have none. 76 need a credential the measuring account does not hold (401 ×3, 403 ×73). The other 100:

| Operation | Status | Why |
|---|---:|---|
| `DELETE /v1/environments/{environment_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/files/{file_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/memory_stores/{memory_store_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}` | 412 | no known request reaches it |
| `DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/messages/batches/{message_batch_id}` | 200 | no known request reaches it |
| `DELETE /v1/messages/batches/{message_batch_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/sessions/{session_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/sessions/{session_id}/resources/{resource_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/skills/{skill_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/skills/{skill_id}/versions/{version}` | 429 | no request a client sends provokes it |
| `DELETE /v1/vaults/{vault_id}` | 429 | no request a client sends provokes it |
| `DELETE /v1/vaults/{vault_id}/credentials/{credential_id}` | 429 | no request a client sends provokes it |
| `GET /v1/agents` | 429 | no request a client sends provokes it |
| `GET /v1/agents/{agent_id}` | 429 | no request a client sends provokes it |
| `GET /v1/agents/{agent_id}/versions` | 429 | no request a client sends provokes it |
| `GET /v1/agents/{agent_id}/versions` | 500 | no request a client sends provokes it |
| `GET /v1/agents/{agent_id}/versions` | 503 | no request a client sends provokes it |
| `GET /v1/environments` | 429 | no request a client sends provokes it |
| `GET /v1/environments/{environment_id}` | 429 | no request a client sends provokes it |
| … and 80 more | | |

**Not modeled.** What the twin's operations leave out:

- Documented statuses not reproduced: 404 (3 operations), 408 (9 operations), 409 (9 operations), 412 (9 operations), 413 (9 operations), 431 (9 operations), 499 (9 operations), 500 (7 operations), 501 (9 operations), 503 (9 operations), 504 (9 operations), 529 (2 operations).
- Generic transport/infrastructure statuses documented with the same ErrorResponse envelope and no caller-provokable trigger: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504, 529. (`DELETE /v1/environments/{environment_id}`)
- The spec marks x-api-key as required:false while documenting a 401 for missing credentials; whether an anonymous request reaches routing or is refused before the handler is unmeasured. (`DELETE /v1/sessions/{session_id}`)
- Documented transport failures with no stated trigger condition: 408 deadline exceeded, 409 aborted, 412 failed precondition, 413 out of range, 431 header fields too large, 499 cancelled, 500 internal, 501 unimplemented, 503 unavailable, 504 upstream deadline exceeded. (`DELETE /v1/sessions/{session_id}/resources/{resource_id}`)
- Whether the backing file, github checkout or memory store referenced by the resource survives the detach. (`DELETE /v1/sessions/{session_id}/resources/{resource_id}`)
- Documented transport/status contracts with no stated trigger on this operation: 408 deadline exceeded, 409 aborted, 412 failed precondition, 413 out of range, 431 request header fields too large, 499 cancelled, 500 internal, 501 unimplemented, 503 unavailable, 504 upstream deadline exceeded, 529 overloaded. (`DELETE /v1/skills/{skill_id}`)
- Whether the anthropic-workspace-id header mismatching the credential's Workspace answers 403 or 404; the parameter description says it must match, without naming the status. (`DELETE /v1/skills/{skill_id}`)
- Documented transport-level outcomes with no stated trigger: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504, 529 — declared in the spec's generic response set, with no condition tying them to this operation. (`DELETE /v1/skills/{skill_id}/versions/{version}`)
- The skills-2025-10-02 beta header itself is not declared as a parameter on this operation, so the epoch addressing form has no modelled request shape. (`DELETE /v1/skills/{skill_id}/versions/{version}`)
- Whether the literal 'latest' is accepted in the version path on DELETE (documented for GET /v1/skills/{skill_id}/versions/{version}); the delete operation's parameter description names only the version ID and the beta epoch form. (`DELETE /v1/skills/{skill_id}/versions/{version}`)
- Whether anthropic-workspace-id mismatching the credential's Workspace answers 403 or 400. (`DELETE /v1/vaults/{vault_id}`)
- Documented transport/server statuses 408, 409, 412, 413, 431, 499, 500, 501, 503 and 504 share the generic BetaErrorResponse envelope with no stated trigger for this operation and are not modelled as branches. (`DELETE /v1/vaults/{vault_id}/credentials/{credential_id}`)
- Whether an unrecognised page cursor draws a 400 invalid_request_error or an empty page is not documented. (`GET /v1/agents`)
- Generic transport/infrastructure statuses documented for this operation but not modeled as branches: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504. (`GET /v1/agents/{agent_id}`)
- The documented 408, 409, 412, 413, 431, 499, 501 and 504 outcomes carry no stated trigger for this operation and are left unmodeled. (`GET /v1/agents/{agent_id}/versions`)
- Ordering of the returned `data` array (creation order, name order, or unspecified) is not documented. (`GET /v1/environments`)
- Whether the anthropic-beta header is required for this route (and which beta label) is not stated on the operation; the example sends managed-agents-2026-04-01 because the resource's scope/config variants declare x-anthropic-beta-required for it. (`GET /v1/environments`)
- Whether supplying ids[] with entries exceeding 100 after de-duplication yields the same 400 invalid_request_error message as the mutual-exclusivity violation is undocumented; both are folded into one 400 branch. (`GET /v1/files`)
- Documented transport/infrastructure statuses 408, 409, 412, 413, 431, 499, 500, 501, 503, 504 and 529 all carry the same ErrorResponse envelope but have no stated caller-reachable trigger on this operation; they are left unmodeled rather than given invented conditions. (`GET /v1/files/{file_id}/content`)
- A File with downloadable=false (any file uploaded via POST /v1/files) answers 400 invalid_request_error with error.details.error_code 'file_not_downloadable', not 403. (`GET /v1/files/{file_id}/content`)
- The ordering/stability contract of the page_... cursor across concurrent creates and archives is not documented. (`GET /v1/memory_stores`)
- … and 72 more

<!-- END generated: known-gaps -->

Release notes: [Changelog](https://www.rystic.ai/docs/changelog.md#anthropic)

---

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