POST /v1/infer/v1/messages is the Anthropic Messages API on the node's model. Point an Anthropic SDK at https://beta-api.b3os.org/v1/infer with your B3OS key as the auth token, and set B3OS_MODEL to a chat model from GET /v1/infer/models (setup). Anthropic compatibility lists every field the node supports, ignores or refuses.

Streaming

ts
import Anthropic from "@anthropic-ai/sdk";const client = new Anthropic({ authToken: process.env.B3OS_API_KEY, baseURL: "https://beta-api.b3os.org/v1/infer" });const stream = client.messages.stream({ model: process.env.B3OS_MODEL!, max_tokens: 256, messages: [{ role: "user", content: "Count from 1 to 5." }],});stream.on("text", (text) => process.stdout.write(text));const message = await stream.finalMessage();console.log("\n", message.usage);

The events are Anthropic's: message_start, content_block_start, content_block_delta, content_block_stop, message_delta and message_stop. A stream that fails after it started ends with an error event; one that fails before the first token is a plain HTTP error. Server-Timing carries ttft, the time to the first generated token on the node.

Tool use

Define custom tools with input_schema. When the model wants one it stops with stop_reason: "tool_use"; run the tool and send its output back as a tool_result block in the next user turn.

python
tools = [{ "name": "get_weather", "description": "Current weather for a city.", "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},}]messages = [{"role": "user", "content": "What is the weather in Eugene, Oregon? Use the tool."}]first = client.messages.create(model=os.environ["B3OS_MODEL"], max_tokens=512, tools=tools, messages=messages)call = next(b for b in first.content if b.type == "tool_use")messages += [ {"role": "assistant", "content": first.content}, {"role": "user", "content": [{"type": "tool_result", "tool_use_id": call.id, "content": "52F and raining"}]},]answer = client.messages.create(model=os.environ["B3OS_MODEL"], max_tokens=512, tools=tools, messages=messages)print(answer.content[-1].text)

tool_choice takes auto, any, none or {"type": "tool", "name": ...}, with disable_parallel_tool_use. Tools Anthropic runs on its side (web_search, bash, code_execution and the like) are refused: the node runs custom tools only.

Extended thinking

thinking with type: "enabled" turns the model's reasoning on; budget_tokens (at least 1,024, and below max_tokens) caps it. The reasoning comes back in thinking blocks before the answer, and goes back with its turn when you continue the conversation.

python
message = client.messages.create( model=os.environ["B3OS_MODEL"], max_tokens=4096, thinking={"type": "enabled", "budget_tokens": 1024}, messages=[{"role": "user", "content": "Is 391 prime?"}],)for block in message.content: print(block.type, block.thinking if block.type == "thinking" else block.text)

A budget that does not fit below max_tokens is refused before the model runs:

bash
curl -s -X POST 'https://beta-api.b3os.org/v1/infer/v1/messages' \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H 'content-type: application/json' \ -H 'anthropic-version: 2023-06-01' \ -d '{"model":"b3os/qwen3.5-9b","max_tokens":1024,"thinking":{"type":"enabled","budget_tokens":1024},"messages":[{"role":"user","content":"Is 391 prime?"}]}'

Captured 2026-10-31 from commit 6bd0d80a , label dev (recorded on a dev node before this reached production). Case infer/messages-thinking-budget .

adaptive turns the reasoning on with no budget (only max_tokens bounds it), and disabled, the default, answers without reasoning. output_config.effort is accepted and has no effect.

Long sessions

The node clears nothing from a conversation. context_management (context editing) is accepted and ignored, so every turn you send stays in the prompt, and a long agent session grows until the prompt no longer fits the model's 32,768-token context. That request gets 400 context_length_exceeded. Summarize or drop old turns in your client before then; POST /v1/infer/v1/messages/count_tokens tells you a request's size for free.

Instructions go in the top-level system parameter. A system message inside messages, which some agents send in the middle of a session, is refused with 400 invalid_request rather than moved, because moving it would change what the model reads at that turn. Fold such instructions into the next user turn, or into system when the session starts.

Limits

Messages share chat completions' admission and limits: the estimated prompt plus max_tokens counts against your organization's tokens per minute for the model, and requests per minute and requests in flight apply. Going over answers 429 rate_limited with Retry-After. See Infer limits. Input and output tokens are metered as infer.input_tokens and infer.output_tokens.

json
{ "model": "b3os/qwen3.5-9b", "max_tokens": 512, "messages": [{ "role": "user", "content": "What is the weather in Eugene, Oregon? Use the tool." }], "tools": [ { "name": "get_weather", "description": "Current weather for a city.", "input_schema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ]}

Try it in the playground

Opens the dashboard playground with this request filled in ( POST /v1/infer/v1/messages ). The link carries no key.

Ask a question... ⌘I