Anthropic

Anthropic API reference

The /anthropic surface: one POST /v1/messages carrying content blocks, tool use and thinking, plus token counting, batches, files and models. The one provider here that shares nothing structural with OpenAI.

Anthropic · Endpoints

Everything under the /anthropic prefix. This is the one provider here that shares nothing structural with OpenAI: everything generative goes through a single POST /v1/messages, replies are lists of typed content blocks, and the system prompt is a top-level field rather than a message.

MethodPathNotes
POST/anthropic/v1/messagesContent blocks, system, tool use, thinking, stop_reason, usage, SSE with stream: true
POST/anthropic/v1/messages/count_tokensWhat a prompt costs before you send it
POST/anthropic/v1/messages/batchesAsynchronous batch of message requests
GET/anthropic/v1/messages/batches/{id}Status and per-request counts
GET/anthropic/v1/messages/batches/{id}/resultsJSONL, one result per line, keyed by custom_id
POST/anthropic/v1/messages/batches/{id}/cancelMoves the batch to canceling
DELETE/anthropic/v1/messages/batches/{id}Returns message_batch_deleted
GET/anthropic/v1/modelsSimulated catalog, cursor-paginated by after_id/before_id
GET/anthropic/v1/models/{id}max_input_tokens is the context window; capabilities is a nested tree of flags
POST/anthropic/v1/filesMultipart upload, beta-gated — see Files
GET/anthropic/v1/filesSimulated catalog, filterable by scope_id
GET/anthropic/v1/files/{id}filename, mime_type, size_bytes, downloadable
GET/anthropic/v1/files/{id}/contentOnly for files the API produced itself
DELETE/anthropic/v1/files/{id}Returns file_deleted
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  apiKey: "sk-mock-key-01",
  baseURL: "https://api.llm-mock.dev/anthropic",
});
Managed Agents is out of scope. The /v1/agents, /v1/sessions, /v1/environments, /v1/vaults and /v1/memory_stores surface is not mocked. That is a deliberate decision, not an oversight: it is larger than everything else on this provider combined.

Anthropic · Messages

One endpoint carries the whole generative surface. Four shapes differ from every other provider here, and all four are the API's doing, not the mock's.

const message = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  system: "You are terse.",
  messages: [{ role: "user", content: "Hello!" }],
});

message.content; // [{ type: "text", text: "Echo: Hello!", citations: null }]
ShapeWhat differs
max_tokensRequired. Omitting it is a 400, where every other provider defaults it.
systemA top-level field, not a message with role: "system". It counts towards input_tokens.
stop_reasonWhere OpenAI has finish_reason. One of end_turn, max_tokens, stop_sequence, tool_use, refusal.
thinkingBlocks are emitted whenever thinking is on, but display decides whether they carry text.

The default display: "omitted" leaves thinking an empty string; "summarized" fills in a summary. That empty-but-present block is what a streaming UI sees as a long pause, so it is worth being able to test.

Consecutive same-role messages are accepted — the real API combines them into one turn — but the conversation must open with user. The x-llm-mock-response and x-llm-mock-tool-calls headers work exactly as on the other providers.

Token counting

const { input_tokens } = await client.messages.countTokens({
  model: "claude-opus-5",
  messages: [{ role: "user", content: "Hello!" }],
});

The count adds up the messages, the system prompt and the serialized tool declarations exactly the way /v1/messages builds its usage.input_tokens — so counting a prompt and then sending it gives the same number twice. It is an approximation, like every token count on this server: good for asserting that a prompt got bigger or smaller, not that it costs 412 tokens.

Anthropic · Tool calls

Tool calls are content blocks here, not a parallel tool_calls array. A tool_use block carries a decoded input object — not OpenAI's JSON string — and an id prefixed toolu_.

const message = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [{ name: "get_weather", input_schema: { /* JSON Schema */ } }],
  tool_choice: { type: "any" },
  messages: [{ role: "user", content: "Weather in Valencia?" }],
});

message.content; // [{ type: "tool_use", id: "toolu_…", name: "get_weather", input: { city: "…" } }]
tool_choiceEffect
{ type: "any" }Forces a call
{ type: "tool", name }Names the one to call
{ type: "none" }Forbids them
{ type: "auto" }The default — left to a model this mock does not have, so no call is made

The result comes back as a tool_result block inside a user message, and its presence terminates the loop: the next reply is prose, not another call. Arguments are synthesized from your JSON Schema, or pinned exactly with x-llm-mock-tool-calls.

Anthropic · Streaming (SSE)

The sharpest divergence on the whole server. Every frame carries an event: line as well as data:, and there is no sentinel string — the stream ends on a named message_stop.

event: message_start
data: {"type":"message_start","message":{…,"content":[],"stop_reason":null}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Echo: "}}

event: message_stop
data: {"type":"message_stop"}
Both halves are load-bearing, verified against the real client: drop the event: lines and the SDK reads zero chunks; drop message_stop and it errors with "stream ended without producing a Message".

That makes three SSE conventions across the five providers here:

ProviderFrameTerminator
OpenAI, Azuredata: onlydata: [DONE]
Gemini, Gemini Enterprisedata: onlyEnd of stream (a [DONE] is rejected)
Anthropicevent: and data:event: message_stop

A tool call streams its arguments as input_json_delta pieces of a JSON string, and a thinking block streams thinking_delta pieces followed by its signature under a separate signature_delta.

Anthropic · Batches

A batch is created by one request and read back by several others, which normally needs a store — and this server never keeps one.

const batch = await client.messages.batches.create({
  requests: [
    { custom_id: "a", params: { model: "claude-opus-5", max_tokens: 64, messages: [/* … */] } },
  ],
});

for await (const result of await client.messages.batches.results(batch.id)) {
  result.custom_id;      // "a"
  result.result.message; // a full Message, echoing that request's prompt
}

The batch rides inside its own id, the same trick the Files endpoints use: msgbatch_ followed by the base64url of the custom_id, model and prompt of every request. Retrieve, results, cancel and delete all just decode it, so they work across restarts and across processes.

That has one visible consequence, and it is deliberate that you see it. An id travels in a URL, so it cannot grow forever: past roughly 1600 characters — a few dozen short requests — creating the batch fails with a 400 that says exactly that, instead of quietly dropping requests. The real API takes 100,000 of them; if you need that shape, you are testing the API, not your code.

Batches complete instantly. With no clock to advance and no state to advance it in, a new batch is already ended and its results are already there. When you need to exercise the polling branch — the one that reads processing_status and waits — pin the status with a header:

HeaderValues
x-llm-mock-batch-statusin_progress, canceling, ended (the default)

A batch that has not ended has a null results_url and its results endpoint answers 400, the same as the real one. Listing always returns an empty page: a stateless mock keeps no list of batches.

Anthropic · Files

Uploaded files carry their metadata inside the id they are minted, so an upload round-trips to a retrieve with nothing stored. Same bytes and same name give the same id, so a test can assert on it.

const file = await client.beta.files.upload({
  file: await toFile(Buffer.from("hello"), "notes.txt", { type: "text/plain" }),
});

await client.beta.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  betas: ["files-api-2025-04-14"],
  messages: [{ role: "user", content: [{ type: "document", source: { type: "file", file_id: file.id } }] }],
});
The beta flag is part of the contract. Every call has to carry anthropic-beta: files-api-2025-04-14, and a call without it is a 400 that says so. The header may list several betas comma-separated, or be repeated — both are accepted. The SDK also appends ?beta=true to the path, which is its own routing artefact; the mock does not require it.

A file you uploaded cannot be downloaded, on the real API and here: GET /content on one answers 403. Only files the API produced itself — code execution output — can be read back, so the simulated catalog carries two of those to make the download path testable at all. Their bytes are a deterministic placeholder, and x-llm-mock-response pins them.

file_missing… is reserved and always 404s; a foreign-looking id resolves to a plausible synthetic file rather than an error, so ids copied out of production logs still behave. A message whose only content is a file attachment echoes the attachment — Echo: [file_mock_report_pdf] — because without that, a prose-less turn would fall through to the generic greeting and there would be no way to assert the reference arrived.

Anthropic · Errors & headers

Three things differ from every other provider on this server, and all three are the API's own doing.

WhatDetail
Credentialx-api-key, not a bearer token. Authorization: Bearer is accepted too, as OAuth callers use it.
anthropic-versionRequired on every request. Any value is accepted; a missing header is a 400.
Error envelopeWraps twice and echoes the request id. The only one here that uses HTTP 529.
{
  "type": "error",
  "error": { "type": "authentication_error", "message": "invalid x-api-key" },
  "request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}

The request_id matches the x-request-id response header, as it does on the real API. Error types are invalid_request_error, authentication_error, permission_error, not_found_error, request_too_large, rate_limit_error, api_error and overloaded_error (529).

Validating anthropic-version against a list of known versions would mean maintaining that list and guessing at versions that do not exist yet, so any value passes. A missing header still fails — that is the classic mistake when calling this API by hand, and worth reproducing.

Other providers

Same server, same key set, one page each.

ProviderPrefixReference
OpenAI/openai/v1Read →
Gemini (AI Studio)/geminiRead →
Gemini Enterprise/gemini-enterpriseRead →
Azure OpenAI/azure/openaiRead →