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.
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.
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:
{"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:
{"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.
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.
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"}]}'
{"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:
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
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
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:
message_start— message metadata (id, model, role, usage)content_block_start— text or tool_use blockcontent_block_delta— text_delta or input_json_deltacontent_block_stopmessage_delta— stop_reason, usagemessage_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.