Melious

Anthropic-shaped chat endpoint. Claude Code, the Anthropic SDK, and anything else that speaks this protocol works unmodified against Melious.

Endpoint:

POST /v1/messages

Auth: Bearer token or x-api-key. Requires scope inference.chat. Header: anthropic-version is optional. Send it if your client already does — Python anthropic 2.x and TypeScript 1.x send 2026-10-21 and the key as Authorization: Bearer; older releases send 2023-06-01 and x-api-key. We accept any version value and both auth headers, and never reject a request for them.

Example

from anthropic import Anthropic

client = Anthropic(
    api_key="sk-mel-<YOUR_API_KEY>",
    base_url="https://api.melious.ai",
)

response = client.messages.create(
    model="claude-sonnet-4",
    max_tokens=256,
    messages=[{"role": "user", "content": "Name three Hanseatic cities."}],
)
text = "".join(block.text for block in response.content if block.type == "text")
print(text)
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  apiKey: "sk-mel-<YOUR_API_KEY>",
  baseURL: "https://api.melious.ai",
});

const response = await client.messages.create({
  model: "claude-sonnet-4",
  max_tokens: 256,
  messages: [{ role: "user", content: "Name three Hanseatic cities." }],
});
const text = response.content
  .filter((block) => block.type === "text")
  .map((block) => block.text)
  .join("");
console.log(text);
curl https://api.melious.ai/v1/messages \
  -H "x-api-key: sk-mel-<YOUR_API_KEY>" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 256,
    "messages": [{"role": "user", "content": "Name three Hanseatic cities."}]
  }'

Request

ParameterTypeDefaultDescription
modelstring—Anthropic model name or an open-weight model ID. See Model mapping below.
messagesarray—Conversation history. Each message has role ("user" or "assistant") and content (string or content blocks).
max_tokensinteger—Required. Maximum tokens to generate.
systemstring | arraynoneSystem prompt — a string, or an array of {"type": "text", "text": "..."} blocks.
toolsarraynoneAnthropic-shape tool definitions with name, description, and input_schema.
tool_choiceobjectauto{"type": "auto"}, {"type": "any"}, {"type": "tool", "name": "..."}, or {"type": "none"}. A boolean disable_parallel_tool_use on it is forwarded to the provider as the inverse of its parallel-tool-calls flag; non-boolean values are ignored. Forwarded, not enforced.
stop_sequencesarraynoneCustom stop sequences.
temperaturenumber1Sampling temperature, [0, 1].
top_pnumber1Nucleus sampling, [0, 1].
top_kinteger—Forwarded to the provider. Whether the model honors it is up to the provider.
thinkingobjectmodel default{"type": "enabled", "budget_tokens": N}, {"type": "adaptive"}, or {"type": "disabled"}. Enabled/adaptive turn reasoning on. Without output_config.effort, the budget picks the effort (≥10000 high, ≥5000 medium, ≥2000 low, below that minimal); with neither, the model default applies. Non-reasoning models ignore it.
output_configobjectnoneformat for structured output. effort — exactly low, medium, high, xhigh, or max, lowercase — sets the reasoning depth and wins over thinking.budget_tokens; thinking.type: "disabled" wins over it. Any other value is ignored, not rejected.
streambooleanfalseEnable SSE streaming.

messages[].content blocks

  • {"type": "text", "text": "..."}
  • {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "..."}} or {"type": "image", "source": {"type": "url", "url": "..."}}
  • {"type": "tool_use", "id": "...", "name": "...", "input": {...}} (assistant only)
  • {"type": "tool_result", "tool_use_id": "...", "content": "..."} (user only)

Model mapping

Anthropic-proprietary names are mapped to open-weight models before inference runs. You can also send open-weight IDs directly.

You send (contains, case-insensitive)We run (default)
opusthe main model
sonnetthe secondary model
haikuthe lightweight model
anything elsethe string as-is

Which open-weight model fills each slot is shown on melious.ai/hub/models.

The response echoes the original model name back. If you sent claude-sonnet-4, the response model field is claude-sonnet-4 — so clients that hard-check the model name (Claude Code, some middleware) don't break.

We map Anthropic names because Claude Code hardcodes them. For direct control, send an open-weight ID: "model": "<MODEL_ID>" routes to exactly that model with no mapping.

Response

{
  "id": "msg_...",
  "type": "message",
  "role": "assistant",
  "content": [
    {"type": "text", "text": "Hamburg, Lübeck, Bremen."}
  ],
  "model": "claude-sonnet-4",
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 7
  }
}

stop_reason

  • end_turn — model finished naturally.
  • max_tokens — hit the cap.
  • stop_sequence — matched a custom stop sequence.
  • tool_use — model wants to call a tool.

content blocks

Same shapes as input: text, tool_use, plus thinking on reasoning models ({"type": "thinking", "thinking": "...", "signature": ""}), always before the text. tool_use blocks include id, name, and parsed input. A reasoning model that uses its whole max_tokens on thinking returns a thinking block and no text block.

No environment_impact or billing_cost

Messages responses carry neither Melious extension, streamed or not. Requests are still metered and billed; among conversational endpoints, Chat completions returns both fields. See Environmental impact.

Streaming

stream: true switches the response to Server-Sent Events with Anthropic event types:

EventMeaning
message_startBeginning of the message — includes initial usage.
content_block_startNew content block (thinking, text, or tool_use) begins.
content_block_deltaToken(s) appended to the current block: thinking_delta, text_delta, or input_json_delta for tool arguments.
content_block_stopCurrent block ended.
message_deltaFinal metadata (stop_reason, usage deltas).
message_stopStream ended.
errorMid-stream failure. Always the last event; nothing from the success tail comes before it.

Streams don't send environment_impact or billing_cost either — message_delta carries stop_reason and token counts, nothing more — and a stream that ends early can still be charged for what was generated.

See Streaming for consumer code.

What's supported vs not

Supported: system prompts, multi-turn conversation, tool use with tool_use/tool_result blocks, streaming, vision (if the underlying model supports it), stop sequences, temperature/top_p/top_k, max_tokens.

Extended thinking works on reasoning models. Send thinking: {"type": "enabled", "budget_tokens": 8000} to turn it on, {"type": "disabled"} to turn it off, or leave it out for the model's default. Thinking comes back as a thinking content block before the text (signature is always an empty string — don't send it back expecting Anthropic to verify it). Read the first text block, not content[0].

To pick the depth directly, send output_config: {"effort": "high"} — low, medium, high, xhigh, or max, lowercase. Effort wins over the budget, disabled wins over both, and enabled or adaptive with neither follows the model default. xhigh and max reach the model as sent only on glm-5.3 and glm-5.3-flash; everywhere else they fold to high.

Not supported today:

  • Anthropic prompt caching (cache_control blocks) — silently dropped; there's no explicit cache handle to set. Transparent prefix caching still happens on the provider side and shows up as usage.cache_read_input_tokens in the response, billed at the cheaper cache-read rate.
  • Fine-grained tool-streaming signals beyond content_block_delta / input_json_delta.

If your client depends on one of these, open an issue — we'll scope the work.

Errors

Errors use the Anthropic shape:

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

request_id is the same id as the request-id and x-request-id response headers — every Messages response carries both — and it's what the Anthropic SDKs expose as _request_id and APIStatusError.request_id. Quote it to support.

Use error.type to classify the error and check error.code when present. These are endpoint-level errors. Account-level throttling uses a flat 429 with code RATE_LIMIT_EXCEEDED; see Errors. Common statuses:

StatustypeMeaning
400invalid_request_errorMalformed request, bad content block, capability or context mismatch.
401authentication_errorMissing or invalid API key.
403permission_errorBilling refusal, or the key can't call this endpoint.
404not_found_errorModel not found.
429rate_limit_errorThrottled — back off per Retry-After.
503api_errorTransient — retry with backoff.

Mid-stream failures keep the HTTP 200 and arrive as an error SSE event — the last event on the stream: {"type": "error", "error": {"type": "api_error", "message": "..."}}. A code is included when one applies; the event carries no request_id, so read the request-id header instead.

Nothing from the success tail comes before it: no content_block_stop for the open block, no tool_use block, no message_delta, no message_stop. A failure before any output skips message_start too — only ping events may come first. Treat a stream that ends with neither message_stop nor error as failed — it was cut off abnormally. See Streaming for retry boundaries and billing.

Full tables and retry guidance: Errors.

Count tokens for preflight sizing • From Anthropic for setup • Tool calling for the loop.

On this page