Anthropic Messages API

An Anthropic-compatible /v1/messages endpoint so the Anthropic SDKs and Claude Code work out of the box.

POST /v1/messages

Pendra translates Anthropic's Messages format to OpenAI Chat Completions under the hood, so you can point any Anthropic SDK — or Claude Code — at Pendra and get Anthropic-shaped responses back. The Python and JavaScript examples use the official Anthropic SDKs; only the base URL and key change.

Authentication

Send your Pendra API key on the x-api-key header (Anthropic convention). The Authorization: Bearer header also works for OpenAI-style clients.

Body application/json
model string Required
Model ID. Must be a model your workers serve — browse available models. An id Pendra doesn't serve returns 404.
max_tokens integer Required
Maximum number of tokens to generate.
messages array Required
The conversation — an array of { role, content } objects. A user message's content can include text, image and document blocks; a document carries a .txt or .csv of up to 1 MB (see Text and CSV files).
system string | array
System prompt — a string, or an array of content blocks.
stream boolean default: false
Set true for Anthropic-format SSE (see Streaming below).
tools array
Anthropic-shaped tool definitions. Pair with tool_choice.

Other optional fields

  • temperature, top_p, top_k, stop_sequences — standard sampling controls. Any you leave out use the settings the model's authors recommend, where they publish them, and Pendra's standard defaults otherwise. Set them explicitly when you need output that doesn't change as models are added or updated.
  • thinking — pass {"type":"enabled"} to request a chain-of-thought on reasoning-capable models, or {"type":"disabled"} to skip thinking on hybrid models that support toggling it. Add budget_tokens, as in {"type":"enabled","budget_tokens":4096}, to limit the thinking: when the budget is used up the thinking ends and the model goes on to its answer. On Pendra the budget is counted on top of max_tokens, which is kept for the answer, so budget_tokens does not have to be smaller than max_tokens. On a few models whose thinking can't be ended part-way (gpt-oss and Gemma 4 among them) the budget is not enforced as a limit and only sets the thinking effort, so you still get an answer. With thinking on and no budget_tokens, a default budget of 8,192 tokens applies in the same way, and it is not applied on those few models either. See Limit how long it thinks.
  • output_config — set format to {"type":"json_schema","schema":{...}} to get a reply that matches your JSON Schema. See Structured output below.
  • cache_control — accepted and ignored. You don't need to place cache breakpoints on Pendra; see Prompt caching below.

Response

Pendra returns the Anthropic message envelope (see example). content is an array of content blocks (text, tool_use, etc.). stop_reason is "end_turn" on a natural finish or "max_tokens" when capped. usage uses Anthropic naming (input_tokens / output_tokens).

When Pendra has something to tell you about a reply, such as the thinking having reached its budget_tokens, the response also carries a pendra object with a notice: a stable code and a readable message. When streaming it is on the final message_delta event. Anthropic SDKs ignore the extra field. The codes are listed under Notices.

{
  "type": "message",
  "stop_reason": "end_turn",
  "content": [ ... ],
  "pendra": {
    "notice": {
      "code": "reasoning_budget_reached",
      "message": "Thinking was capped at 4096 tokens and the model moved on to its answer. ..."
    }
  }
}

When the model thinks, its reasoning comes back as a thinking block with a signature, as it does on Anthropic. You can pass the block back unchanged in the next turn's history. Pendra's signatures only work on Pendra, and Pendra ignores any signature you send it.

A non-streaming reply also says which of your workers ran it and how long it took: the X-Worker-Id, X-Worker-Name and Server-Timing headers, and a pendra field in the body that the Anthropic SDKs and Claude Code ignore:

"pendra": {"worker": {"id": "wrk-1a2b3c4d", "name": "london-gpu-01", "version": "3.115.1"},
           "request_id": "3f1c8a2e-5b7d-4e19-9c0a-6d2f8b1e4a73",
           "timing": {"ttft_ms": 115, "tokens_per_second": 58.34, "queue_wait_ms": 16600}}

timing is the request's latency and throughput, the same figures as the Server-Timing header. Read them from the body: a reply slow enough that Pendra keeps the connection alive is sent without that header. Streamed replies don't carry the pendra field. They name the worker the request was sent to in the X-Worker-Id and X-Worker-Name headers when one is free straight away; a stream that had to wait for a busy worker starts without them. See Which worker served the request and Timing for what each field means.

Structured output

Set output_config.format to a JSON Schema and the reply's text is JSON that matches it. The Anthropic Python SDK's messages.parse() does this for you from a Pydantic model:

from anthropic import Anthropic
from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int

client = Anthropic(base_url="https://api.pendra.ai", api_key="pdr_sk_...")
message = client.messages.parse(
    model="qwen3.6:27b",
    max_tokens=512,
    messages=[{"role": "user", "content": "Extract the name and age from: Sara is 34."}],
    output_format=Person,
)
print(message.parsed_output)  # name='Sara' age=34

Only "type": "json_schema" is supported. Any other format type returns a 400 invalid_request_error, so a request never comes back unconstrained without you knowing.

Prompt caching

Pendra caches prompts automatically. When you send follow-up turns of the same conversation, it reuses the part of the prompt it already processed instead of reprocessing it from scratch, which makes the reply start sooner. You don't have to do anything to switch this on.

usage.cache_read_input_tokens tells you how many of that request's input_tokens came from the cache, so a cache hit rate is cache_read_input_tokens / input_tokens. It's always present — 0 on a first, cold request. A higher rate means a faster first token on that request.

cache_creation_input_tokens is always 0. Other providers charge for writing a prompt into the cache and report it here; Pendra's caching is automatic and free, so there is nothing to report.

Because it's automatic, Pendra doesn't use cache breakpoints. If your client sends cache_control blocks — Claude Code and the Anthropic SDKs do — Pendra accepts the request and ignores the field. You can leave your breakpoints in place; they simply have no effect, and removing them changes nothing. Tuning them won't move your hit rate either: cache_read_input_tokens is the number to watch, and it reflects what Pendra reused on its own.

On a streamed response, the cache numbers arrive on the final message_delta event rather than message_start — they aren't known until the model has finished reading the prompt.

Streaming

Set "stream": true to receive Anthropic-format SSE events (message_start, content_block_delta, message_delta, message_stop).

event: message_start
data: {"type":"message_start","message":{"id":"msg_01N3Xc8","type":"message","role":"assistant","content":[],"model":"qwen3.6:27b","stop_reason":null,"usage":{"input_tokens":9,"output_tokens":0}}}

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

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":9,"output_tokens":2,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}

event: message_stop
data: {"type":"message_stop"}

While the model is still reading a long prompt, Pendra sends an SSE comment line (: keep-alive) every 15 seconds so the connection isn't dropped as idle before message_start arrives. It carries no event: and no data:, so the Anthropic SDKs and Claude Code discard it; only hand-rolled parsers need to skip lines starting with :.

If you pin a worker with X-Pendra-Worker-Id and it can't serve the request — it doesn't exist, doesn't have the model, or is running a version too old for this endpoint — you get a normal HTTP error response instead of a stream, so check the status code before you start reading events. The same goes, without a pin, when none of your workers is online, when none of your connected workers is new enough for this endpoint, or when an encrypted request is sealed to a key none of your workers holds any more — each a 503 with a Retry-After header, the last with X-Pendra-Error-Code: no_private_inference_worker (fetch your organisation's key again and retry). A worker that's merely busy still reports in-band as an error event once the stream has started. See Errors & rate limits.

Pin a Pendra model

Claude Code defaults to Anthropic's own model names, like claude-sonnet-4-5. Pendra serves open models under their own ids and won't substitute one for a name it doesn't recognise, so those requests come back as a 404 telling you which variable to set. Pick an id from /models and set it before you start.

Use with Claude Code

Point Claude Code at Pendra by setting four environment variables — see the full recipe in Integrations → Claude Code.