Anthropic API

Paddock implements the Anthropic Messages API on /v1/messages, so Anthropic SDK clients, Claude Code among them, can point at a local model.

Base URL and Authentication

The Anthropic SDKs append /v1/messages to their base URL themselves, so the base URL is the server root, without /v1:

http://localhost:11540

A caller on loopback never needs a key. When one is required, send it as Authorization: Bearer <key>; Paddock authenticates /v1 requests through that header. Errors use the Anthropic envelope:

{ "type": "error", "error": { "type": "invalid_request_error", "message": "..." } }

Endpoints

EndpointDescription
POST /v1/messagesThe Messages API: content blocks, system prompts, tool use, thinking, streaming, and the MCP connector.
POST /v1/messages/count_tokensRuns the same conversion and template-render pipeline without generating; returns input_tokens.

Request Surface

model, max_tokens (required, per the Anthropic API), and messages are the core fields. A system prompt sent as several text blocks keeps its boundaries: the blocks are joined with a blank line rather than run together, which used to fuse the end of one block into the start of the next and could pull an instruction into a heading. Claude Code and the Agent SDK both send it that way. Also supported: system (a string or text blocks), tools, tool_choice (auto, any, tool, none, with disable_parallel_tool_use), stop_sequences, temperature, top_p, thinking, stream, metadata, and mcp_servers. top_k, min_p, and seed are local extensions. Unknown fields are rejected with a 400 that names the field, matching the Anthropic API's own behavior.

Responses

Replies are content-block arrays: text, thinking, and tool_use blocks, with stop_reason set to end_turn, max_tokens, stop_sequence, or tool_use. Usage reports input_tokens and output_tokens.

Streaming

The streaming path follows the strict Anthropic event protocol that the SDK accumulator enforces:

message_start
ping
content_block_start / content_block_delta / content_block_stop   (per block)
message_delta
message_stop

A keep-alive ping event follows message_start, as on the live API, and another goes out after every 15 seconds of silence. That matters more than it sounds: a stream can legitimately produce nothing for minutes while the engine reads a long prompt for the first time, or while a tool call is generated (tool calls are emitted whole, on completion). Claude Code and Codex CLI both hang up on a stream idle for 300 seconds, so a cold prefill of a very long prompt used to lose the request and pay for a retry. The timer resets on every real event, so a stream that is producing never carries a ping beyond the first.

Compaction

A conversation that outgrows the context is compacted, and a span too large for the summarizer to read in one go is summarized in parts rather than failing with a 400. A streaming request compacts inside the stream and sends a ping every 15 seconds while it works, so a client that drops a silent stream stays connected. This is the same behaviour the Responses API has.

Thinking

Reasoning text is returned as thinking content blocks. thinking.type accepts enabled, disabled, and adaptive (treated as enabled), and thinking.display accepts summarized and omitted: with omitted, thinking blocks come back with an empty thinking field and no thinking deltas.

A reasoning model sometimes samples its end token in the middle of its own reasoning, which used to end the reply with no answer at all - under Claude Code a compaction would then report that summarization produced an empty response. Inside an open reasoning block a stop is now masked before the draw, and a model that keeps reaching for it, three times within 64 tokens, is moved on to its answer rather than left thinking to the token limit. A thought that instead loops - repeating a span, or collapsing onto two or three words - is moved on for the same reason. On Qwen, Laguna and Gemma 4; on NVIDIA GPUs the mask runs on the GPU, so thinking replies keep their speed.

Three behaviors differ from Anthropic's service: thinking.budget_tokens is accepted but not enforced (thinking length is model-driven), gpt-oss models reason unconditionally and return thinking blocks even without thinking: enabled, and thinking blocks carry an empty signature.

Tool Use

Tools are declared as name, description, and input_schema. The model's calls come back as tool_use blocks, and your tool_result blocks in the next user message feed results back in the standard round trip. Tool definitions are converted to the served model's own chat-template format, so tool use works across model families.

Vision and PDF Input

With a vision model served, image content blocks with base64 sources are accepted. URL image sources are rejected; the server does not fetch remote URLs. PDF document blocks (base64 source, application/pdf media type) are rendered into page images server-side, with a page cap and reported truncation. The renderer is built into the binary, so this needs no configuring.

Web Search

The Anthropic server web-search tool (web_search_20250305) is supported, including max_uses. Searches run server-side through a provider you bring your own key for, Exa or Tavily, configured in the Studio or with --web-search-provider and --web-search-api-key on the server itself (see Setting Up Web Search) and are reported as server_tool_use and web_search_tool_result content blocks, with usage.server_tool_use.web_search_requests counting the calls.

MCP Connector

The mcp_servers request field (the Anthropic MCP connector, current beta mcp-client-2025-11-20) makes Paddock connect to remote MCP servers and execute the model's tool calls server-side, emitting mcp_tool_use and mcp_tool_result blocks. Per-server tool configuration comes through mcp_toolset entries in tools (allowlists, denylists, and deferred loading through the search tools); the deprecated tool_configuration shape from mcp-client-2025-04-04 still works. See the MCP page for the full surface.

Claude Code and the Anthropic SDKs

Because /v1/messages follows the Messages spec, including the strict streaming protocol, Anthropic SDK clients and Claude Code work by changing the base URL:

export ANTHROPIC_BASE_URL=http://localhost:11540

Three things current Claude Code versions rely on, all of which are handled:

  • System messages in the middle of a conversation. Claude Code ends every request with one. They are accepted rather than rejected as an invalid message role, and on models whose templates allow a system turn there - Kolibri 1, Laguna, Gemma 4, Granite and Muse Glimmer - they stay where you put them instead of being folded into the next user turn.
  • Cached prefixes counted once. Token usage counts a cached prefix a single time, so Claude Code sees the real size of the conversation and stops compacting at half the window.
  • A prompt past the window returns Anthropic's own error. It comes back as the prompt is too long error the client expects, so Claude Code compacts and carries on rather than failing the turn.

Requests carrying tools keep speculating after the first turn, so an agent loop does not lose speculative decoding the moment it picks up a tool.

A request cut off because the server is shutting down, for a model switch or a restart, ends with a retryable error saying so rather than looking like a finished reply. Claude Code used to keep the truncated answer as complete.

Example

curl http://localhost:11540/v1/messages \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.5-9b",
    "max_tokens": 512,
    "system": "You are a concise assistant.",
    "messages": [{"role": "user", "content": "What does continuous batching mean?"}]
  }'

For throughput and latency measurements, see the benchmarks.