Messages (Anthropic)
Last updated:
On this page
POST https://api.staik.se/v1/messagesAnthropic Messages API-compatible endpoint — Claude Code, the Anthropic SDK, and
other tools that speak the Anthropic protocol work directly. Authenticate with
x-api-key or Authorization: Bearer (same key).
import anthropic
client = anthropic.Anthropic(
base_url="https://api.staik.se",
api_key="sk-st-your-key",
)
message = client.messages.create(
model="qwen3.6:35b-a3b",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain how a transformer works."}],
)
print(message.content[0].text)Model mapping
Send either staik model names or Claude model names — Claude names are mapped automatically (mainly for seamless Claude Code integration):
| Sent model name | Maps to |
|---|---|
claude-sonnet-4-6 | qwen3.6:35b-a3b |
claude-opus-4-6 | qwen3.6:35b-a3b |
claude-haiku-4-5-20251001 | qwen3.5:9b |
Prompt caching
cache_control breakpoints are supported (max 4 per request, matching Anthropic).
Marked context is cached and reused — the cached part is billed as discounted
cache_read instead of full price every turn. TTL is 5 minutes, or 1 hour with
ttl: "1h". Cache hits show up in the response's usage fields.
{
"system": [
{
"type": "text",
"text": "...large system prompt or context...",
"cache_control": {"type": "ephemeral"}
}
]
}Supported features
- Streaming in Anthropic format. The events are
message_start,content_block_start,content_block_delta,content_block_stop,message_deltaandmessage_stop. - Tool calling:
tools,tool_useandtool_resultare translated automatically. - System prompt as a string or content blocks.
- Quotas and queues are the same as for the OpenAI endpoint.
Tool choice (tool_choice)
tool_choice is supported with all four types. disable_parallel_tool_use: true
allows at most one tool call per response.
tool_choice | Meaning |
|---|---|
{"type": "auto"} | The model decides (default) |
{"type": "any"} | The model must call one of the tools |
{"type": "tool", "name": "extract_invoice"} | The model must call that specific tool |
{"type": "none"} | The model must not call tools |
Forcing a tool is the reliable way to get structured data out of a model:
message = client.messages.create(
model="qwen3.6:35b-a3b",
max_tokens=1024,
tools=[{
"name": "extract_invoice",
"description": "Fields from an invoice",
"input_schema": {
"type": "object",
"properties": {"total": {"type": "number"}, "due_date": {"type": "string"}},
"required": ["total"],
},
}],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": "Invoice: total SEK 1,234.50, due 30 Nov."}],
)
print(message.content[0].input) # {"total": 1234.5, "due_date": "..."}tool_choice without tools is ignored. A name that is not among tools returns 400.
The models on staik's GPUs follow the choice. Your own GPU nodes running Ollama follow a
forced tool (tool and any) but not auto with strict.
Arguments follow input_schema
When the model is forced to call a tool ({"type": "tool"}, or {"type": "any"}),
generation is constrained to the tool's input_schema, with and without streaming. This
is the same mechanism as response_format with json_schema (strict) in
chat completions. The response is a tool_use block
with stop_reason: "tool_use". With any and several tools the model picks one, and the
arguments follow that tool's schema.
"strict": true on a tool constrains its arguments even when the model decides to call
it on its own (auto). This works on qwen3.6:35b-a3b and qwen3.5:9b, but not on
gemma4:31b.
For tools with a schema we recommend qwen3.6:35b-a3b. gemma4:31b is not recommended:
it can get stuck emitting whitespace until max_tokens even when the arguments are
constrained, and it does not constrain strict with auto.
tools=[{"name": "extract_invoice", "strict": True, "input_schema": {...}}]The response header x-staik-tool-schema-enforced is present when the request has a
forced tool or a strict tool:
| Value | Meaning |
|---|---|
true | The arguments are constrained to the whole schema |
false | Not all of the schema could be constrained: it has unsupported keywords, or the model cannot constrain strict with auto. Validate the arguments yourself |
A response cut off by max_tokens (stop_reason: "max_tokens") can have incomplete
arguments even when the header is true.
JSON Schema keywords that are constrained: type (including lists such as
["string", "null"]), properties, required, additionalProperties, items,
prefixItems, enum, const, anyOf, oneOf, $ref (local, #/$defs/...), $defs,
definitions, minLength, maxLength, pattern, format (date, date-time,
time, duration, email, hostname, ipv4, ipv6, uri, uri-reference,
uri-template, uuid, json-pointer, relative-json-pointer), minimum, maximum,
exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItems and
patternProperties. Annotations such as title, description, default and
examples have no effect.
Not supported: allOf, not, if/then/else, dependentRequired,
dependentSchemas, unevaluatedProperties, unevaluatedItems, propertyNames,
uniqueItems, contains, minContains, maxContains, minProperties,
maxProperties, external $ref and other format values. They are left out of the
constraint, the rest of the schema is constrained, and the header is false.
Fields that may be missing
A required field of type string must always be filled in. If the document lacks the
value, the model writes something, such as an empty string. Give the field the type
["string", "null"] if it may be missing, so the model can answer null.
PDFs and documents
document blocks are supported. Send a PDF as base64 and staik reads it the way Claude
does: for each page the model gets the page's text layer and an image of the page, in
page order, under the heading "Sida 3 av 7" (page 3 of 7). The block's title and
context are included.
import base64
pdf = base64.standard_b64encode(open("invoice.pdf", "rb").read()).decode()
message = client.messages.create(
model="qwen3.6:35b-a3b",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"title": "invoice.pdf",
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf},
},
{"type": "text", "text": "What is the total amount?"},
],
}],
)source.type | How it is read |
|---|---|
base64 (application/pdf) | Text layer and page image per page |
text | The text as is |
content | The blocks in content (text and base64 images) |
url, file | 400. staik fetches no URLs or files, send base64 |
Documents work in user messages, in tool_result and in system.
Limits:
- at most 20 pages per PDF and 40 pages per request
- the whole request can be at most 20 MB, so a PDF can be about 15 MB (base64 makes it a third larger)
- password-protected and broken PDFs return 400
Over a limit the API returns 400 instead of truncating the document.
Cost: each page image counts as an image in usage.input_tokens, plus the text layer.
An A4 page is roughly 2,100 image tokens on the Qwen models and roughly 420 on
gemma4:31b.
Scanned PDFs have no text layer and can only be read as images, so they need a model with vision. A model without vision gets only the text layer, and a page without text then returns 400.
Errors
Errors use Anthropic's format, so the Anthropic SDK shows the message directly:
{
"type": "error",
"error": {"type": "invalid_request_error", "message": "..."}
}Limitations
- Extended thinking: the
thinkingparameter is ignored and the answer comes without reasoning.-thinkingaliases are treated as an unknown model and are answered by the default model. Thinking is available via chat completions. - Images are sent as base64 image blocks (same models and rules as chat completions). Images by URL return 400.
Claude Code
Two environment variables are all it takes: ANTHROPIC_BASE_URL=https://api.staik.se
and ANTHROPIC_API_KEY=sk-st-.... Full guide under
Claude Code.