Anthropic Messages API
An Anthropic-compatible /v1/messages endpoint so the Anthropic SDKs and Claude Code work out of the box.
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.
model
string
Required
404.
max_tokens
integer
Required
messages
array
Required
{ 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
stream
boolean
default: false
true for Anthropic-format SSE (see Streaming below).
tools
array
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. Addbudget_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 ofmax_tokens, which is kept for the answer, sobudget_tokensdoes not have to be smaller thanmax_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 nobudget_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— setformatto{"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.