Messages (Anthropic)

Last updated:

On this page
text
POST https://api.staik.se/v1/messages

Anthropic 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).

python
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 nameMaps to
claude-sonnet-4-6qwen3.6:35b-a3b
claude-opus-4-6qwen3.6:35b-a3b
claude-haiku-4-5-20251001qwen3.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.

json
{
  "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_delta and message_stop.
  • Tool calling: tools, tool_use and tool_result are 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_choiceMeaning
{"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:

python
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.

python
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:

ValueMeaning
trueThe arguments are constrained to the whole schema
falseNot 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.

python
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.typeHow it is read
base64 (application/pdf)Text layer and page image per page
textThe text as is
contentThe blocks in content (text and base64 images)
url, file400. 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:

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

Limitations

  • Extended thinking: the thinking parameter is ignored and the answer comes without reasoning. -thinking aliases 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.