Anthropic Claude Messages API

The POST /v1/messages endpoint implements the Anthropic Messages API with streaming SSE using event: + data: format, including content blocks for text and tool use.

To send events or fields aimock does not model yet, use a raw passthrough fixture with wire: "anthropic".

Endpoint

Method Path Format
POST /v1/messages SSE (event: + data:) or JSON
GET /v1/models OpenAI list; native paged JSON with modelCatalog=native
GET /v1/models/:id Native model JSON with Anthropic headers
POST /v1/messages/count_tokens JSON: input_tokens

Model Catalog

By default, the shared /v1/models list path returns the OpenAI format for every request, as in aimock 1.45. The Anthropic list is opt-in. Set policy.modelCatalog to "native", pass --policy modelCatalog=native, or send the request header X-AIMock-Policy: modelCatalog=native. With the opt-in, requests that send anthropic-version, or x-api-key without Authorization, get the Anthropic format. Other callers still get the OpenAI format. The /v1/models/:id detail path uses the same header selection without the opt-in.

The Anthropic SDK sends anthropic-version on every request. If your tests call client.models.list(), start aimock with --policy modelCatalog=native (or policy: { modelCatalog: "native" }). With the opt-in, a non-Anthropic client that sends x-api-key without Authorization also gets the Anthropic catalog.

The native catalog contains Anthropic IDs and unrecognized IDs from fixtures, in first-seen order. Empty fixture sets use the established default models. Known OpenAI and Google IDs are excluded. Custom test IDs remain visible.

Lists return data, has_more, first_id, and last_id. The default limit is 20, with an accepted range of 1–1000. Use after_id or before_id for another page. A client that ignores has_more sees only the first page.

The lifecycle filter accepts active, deprecated, and retired. Repeated keys and lifecycle[] values form a union. All mock models are active, so a deprecated-only or retired-only filter returns an empty page.

Invalid limits, lifecycle values, and unknown after_id or before_id cursors return 400 invalid_request_error. Unknown cursors return Model with id <id> not found, matching the status, error type, and message observed in authenticated upstream probes. aimock validates cursors before applying lifecycle filters, including filters that return an empty page.

Each model has type: "model", its ID as display_name, created_at: "1970-01-01T00:00:00Z", and lifecycle: "active". Capabilities, line, token limits, and deprecation dates are null. This synthetic metadata does not describe upstream capacity.

Detail paths accept raw or percent-encoded slashes in IDs. Missing IDs, malformed escapes, and IDs outside the Anthropic catalog return a native 404. The examples assume aimock runs with --policy modelCatalog=native and fixtures with claude-test and additional Claude IDs.

Native catalog shell
curl 'http://localhost:4010/v1/models?limit=1' \
  -H 'anthropic-version: 2023-06-01' -H 'x-api-key: mock'

curl 'http://localhost:4010/v1/models?limit=1&after_id=claude-test' \
  -H 'anthropic-version: 2023-06-01' -H 'x-api-key: mock'

curl 'http://localhost:4010/v1/models/claude-test' \
  -H 'anthropic-version: 2023-06-01' -H 'x-api-key: mock'

Approximate Token Counts

POST /v1/messages/count_tokens accepts a Messages-style body and returns {"input_tokens": N}. It converts messages, system text, and tools through the existing request translator, then estimates about 1 token per 4 characters of messages (plus tool definitions), minimum 1. This deterministic estimate is not a tokenizer or an upstream count.

Count a prompt shell
curl http://localhost:4010/v1/messages/count_tokens \
  -H 'Content-Type: application/json' \
  -H 'anthropic-version: 2023-06-01' -H 'x-api-key: mock' \
  --data '{"model":"claude-test","messages":[{"role":"user","content":"hello"}]}'

Counts ignore fixture usage. Messages responses use fixture counts and otherwise default to zero. To make a response agree with an estimate, set its fixture usage explicitly. Missing or invalid messages return a native 400.

Catalog and count routes never proxy or record. Count POSTs create internal journal entries. Catalog GETs do not. See Record & Replay.

Authentication Errors

With inbound API-key authentication enabled, rejected keys on Messages and count paths return 401 with the same body as every other HTTP route, unchanged from 1.45:

Default inbound rejection json
{"error":{"message":"Invalid API key","type":"authentication_error","code":"invalid_api_key"}}

To get the provider-native body instead, set policy.authErrors to "native", pass --policy authErrors=native, or send the request header X-AIMock-Policy: authErrors=native. Messages and count paths then return this body and a generated request-id response header:

Native inbound rejection json
{"type":"error","error":{"type":"authentication_error","message":"Invalid API key"}}

The native body omits error.code. With the native policy, check HTTP 401 or error.type: "authentication_error".

Shared /v1/models paths always return the default body, even with Anthropic headers and the native policy. WebSocket authentication also always returns it.

A live unauthenticated count probe returned Anthropic 401 with "x-api-key header is required" and a body request_id. aimock native bodies retain "Invalid API key" and omit that body field. The response header does not make these bodies identical.

Missing anthropic-version remains accepted by default, including with strict mode. Use the independent policy below to enable header enforcement.

Required Header Policy

Set policy.requiredHeaders to "enforce" in createServer or LLMock options to require anthropic-version. The default is "off". The exported AimockPolicy type declares this option. Config and both CLIs expose the same option; see aimock CLI.

Opt in ts
import { createServer } from "@copilotkit/aimock";

const instance = await createServer([], {
    port: 4010,
    policy: { requiredHeaders: "enforce" },
});

Enforcement applies to POST /v1/messages, POST /v1/messages/count_tokens, Anthropic-flavored GET /v1/models/:id requests, and Anthropic-flavored GET /v1/models list requests with modelCatalog=native. Bedrock's body-level anthropic_version is separate.

Authentication runs first. A missing required header then returns 400 before thinking, fixture matching, or proxy work. The policy is independent of strict mode: X-AIMock-Strict: false does not disable enforcement.

Shared model paths use the catalog selection described above. By default the list path is OpenAI-format and the version requirement does not apply to it. On the detail path, and on the list path with modelCatalog=native, x-api-key without Authorization selects Anthropic and can trigger the version requirement.

Missing version shell
curl http://localhost:4010/v1/messages/count_tokens \
  -H 'content-type: application/json' \
  -H 'x-request-id: req_policy_example' \
  -d '{"model":"claude-test","messages":[{"role":"user","content":"hello"}]}'
HTTP 400 json
{"type":"error","error":{"type":"invalid_request_error","message":"anthropic-version: header is required"},"request_id":"req_policy_example"}

Policy rejections include a safe request_id that matches the x-request-id response header. aimock uses a valid caller ID after trimming, or generates an ID when it cannot use the supplied value. This metadata addition applies to policy rejection; the authentication body above retains its documented shape.

The request header has highest precedence. To disable enforcement for one request, send X-AIMock-Policy: requiredHeaders=off:

Request override shell
curl http://localhost:4010/v1/messages/count_tokens \
  -H 'content-type: application/json' \
  -H 'X-AIMock-Policy: requiredHeaders=off' \
  -d '{"model":"claude-test","messages":[{"role":"user","content":"hello"}]}'

Earlier authenticated Anthropic captures returned 400 with the same error type and diagnostic message. Local checks against those retained captures confirm the policy fields; request IDs differ by request. This is not a fresh upstream comparison or a claim of full vendor conformance.

Unit Test: Text Streaming

claude-text.test.ts ts
const textFixture = {
  match: { userMessage: "hello" },
  response: { content: "Hi there!" },
};

const instance = await createServer([textFixture]);

const res = await post(`${instance.url}/v1/messages`, {
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "hello" }],
  stream: true,
});

const events = parseClaudeSSEEvents(res.body);
const types = events.map(e => e.type);

expect(types).toContain("message_start");
expect(types).toContain("content_block_start");
expect(types).toContain("content_block_delta");
expect(types).toContain("message_stop");

Unit Test: Tool Use

claude-tools.test.ts ts
const toolFixture = {
  match: { userMessage: "weather" },
  response: {
    toolCalls: [{ name: "get_weather", arguments: { city: "NYC" } }]
  },
};

const instance = await createServer([toolFixture]);

const res = await post(`${instance.url}/v1/messages`, {
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "what is the weather?" }],
  stream: true,
});

const events = parseClaudeSSEEvents(res.body);
const blockStart = events.find(
  e => e.type === "content_block_start"
    && e.content_block?.type === "tool_use"
);
expect(blockStart.content_block.name).toBe("get_weather");

SSE Event Sequence

Claude Messages streaming produces these events:

  1. message_start — message metadata (id, model, role, usage)
  2. content_block_start — text or tool_use block
  3. content_block_delta — text_delta or input_json_delta
  4. content_block_stop
  5. message_delta — stop_reason, usage
  6. message_stop

If the client disconnects mid-stream, aimock stops sending events and skips any remaining per-chunk latency. The journal entry records response.interrupted: true and response.interruptReason: "client aborted". Fixture interruptions (truncateAfterChunks, disconnectAfterMs) keep their own reasons. See Error Injection.

Cache Usage

Set response.usage.cache_read_input_tokens and cache_creation_input_tokens in a fixture to emit cache counts on Messages and Bedrock Claude invoke responses. Both fields appear in final responses, message_start, and message_delta when supplied.

The optional cache_creation object accepts ephemeral_5m_input_tokens and ephemeral_1h_input_tokens. It appears in final responses and message_start; omitted members render as zero. Numeric cache_creation values remain accepted without being emitted. Unrecognized usage fields are omitted on these responses.

Messages deltas use the fixture output count. Bedrock invoke deltas retain their zero output count. Applied misbehavior uses estimated token counts and omits fixture cache fields. Record mode retains upstream token and cache counts. Numeric delta counts replace start counts; null values keep the earlier count. The resulting fixtures replay the captured usage.

Request Translation

aimock internally translates Anthropic requests to a unified format for fixture matching. The claudeToCompletionRequest() function handles mapping Anthropic message arrays (including content block arrays) to OpenAI-style messages so the same fixtures work across all providers.

Ordered blocks (tool-first)

A combined content + toolCalls fixture accepts an optional blocks array to control stream order — see Ordered blocks. Claude Messages has full support: typed text / tool_use content blocks stream at incrementing indices in array order, so tool-first and interleaved ordering are natively observable to clients.