Messages
The Anthropic Messages format, with streaming, tools, extended thinking (on or off), token counting and prompt caching.
POST https://api.interodin.com/v1/messages takes the Anthropic Messages format. Claude Code and the Anthropic SDKs use it unchanged.
Base URL: Anthropic SDKs append /v1/messages themselves, so give them the origin, https://api.interodin.com, without /v1.
Authentication: the SDKs send your key as x-api-key. Authorization: Bearer works too.
Headers and query strings: the anthropic-version and anthropic-beta headers and the ?beta=true query string that the SDKs send are accepted. Other query parameters are accepted and ignored. Model ids with a [1m] suffix, such as odin-v1[1m], are accepted. The suffix is ignored.
Basic request
curl https://api.interodin.com/v1/messages \
-H "x-api-key: $INTERODIN_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "odin-v1",
"max_tokens": 1024,
"system": "You are a concise assistant.",
"messages": [{"role": "user", "content": "Name three Norse gods."}]
}'max_tokens is required, as in the Anthropic API. Values above the model's maximum output are clamped. A value of zero or less fails with 400 invalid_request.
Every response carries an x-request-id header. Quote it when you contact support.
To tag a request with your own end-user id, set metadata.user_id to a string of up to 1024 characters. A value of any other type fails with 400 invalid_request. The API forwards a hash that is specific to your organization, never the value you sent. Use an opaque id, such as a database id, rather than an email address. Other metadata keys are dropped, and a metadata that is not an object is ignored.
Streaming
With stream: true you get the standard event sequence: message_start, then for each content block content_block_start, content_block_delta (repeated) and content_block_stop, then message_delta and message_stop. ping events may appear at any time.
Input token usage arrives in message_start.message.usage. The final output token count arrives in message_delta.usage.
If the model goes silent for 300 seconds in the middle of a stream, the stream ends with an error event of type api_error. This is the upstream_unavailable failure. The status line was already sent, so there is no x-interodin-error-code header and the event has no code: treat any error event as a failed request and retry.
import os
import anthropic
client = anthropic.Anthropic(base_url="https://api.interodin.com", api_key=os.environ["INTERODIN_API_KEY"])
with client.messages.stream(
model="odin-v1",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about Odin."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print("\n", stream.get_final_message().usage)Tools
Define tools with name, description and input_schema. When the model calls one, the reply has stop_reason: "tool_use" and a tool_use content block. Send the result back as a tool_result block in the next user message.
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({ baseURL: 'https://api.interodin.com', apiKey: process.env.INTERODIN_API_KEY })
const tools: Anthropic.Tool[] = [
{
name: 'get_weather',
description: 'Get the current weather for a city',
input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
},
]
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: 'What is the weather in Cluj?' }]
const first = await client.messages.create({ model: 'odin-v1', max_tokens: 1024, tools, messages })
messages.push({ role: 'assistant', content: first.content })
const results: Anthropic.ToolResultBlockParam[] = []
for (const block of first.content) {
if (block.type !== 'tool_use') continue
const { city } = block.input as { city: string }
results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify({ city, celsius: 14 }) })
}
messages.push({ role: 'user', content: results })
const second = await client.messages.create({ model: 'odin-v1', max_tokens: 1024, tools, messages })
for (const block of second.content) if (block.type === 'text') console.log(block.text)tool_choice accepts {"type": "auto"}, {"type": "any"}, {"type": "none"} or {"type": "tool", "name": "..."}.
Only custom tools (name, description, input_schema) and the web search tool are kept. Other tools with a type, such as code execution, bash, text editor and computer use, and mcp_servers, are removed from the request without an error, so the model never sees them. A tool_choice that names a removed tool is removed too.
Web search
Add the web search tool and the model can search the web and cite its sources:
{
"model": "odin-v1",
"max_tokens": 2000,
"tools": [{ "type": "web_search_20250305", "name": "web_search", "max_uses": 3 }],
"messages": [{ "role": "user", "content": "What is the latest stable Node.js release?" }]
}The response has server_tool_use and web_search_tool_result blocks, then the answer. usage.server_tool_use.web_search_requests tells you how many searches ran.
- Price. Each search is billed on top of the tokens it uses. The per-search price is on the pricing page on the InterOdin website.
- Limit. A request runs at most 5 searches.
max_usesis capped at 5, and set to 5 when you leave it out. - Balance. Your balance must cover the tokens and the searches the request could run. A request that could cost more than your balance fails with
402 insufficient_credits. - Where. Web search works on this API and on Responses, not on Chat Completions.
JSON output
To get JSON in a guaranteed shape, define a single tool whose input_schema is the shape you want, and force it with tool_choice. The tool's input is your JSON object.
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({ baseURL: 'https://api.interodin.com', apiKey: process.env.INTERODIN_API_KEY })
const message = await client.messages.create({
model: 'odin-v1',
max_tokens: 1024,
tools: [
{
name: 'record_person',
description: 'Record a person extracted from the text',
input_schema: {
type: 'object',
properties: { name: { type: 'string' }, age: { type: 'integer' }, city: { type: 'string' } },
required: ['name', 'age', 'city'],
},
},
],
tool_choice: { type: 'tool', name: 'record_person' },
messages: [{ role: 'user', content: 'Extract the person: Ana is 31 and lives in Iasi.' }],
})
const call = message.content.find((block) => block.type === 'tool_use')
if (call?.type === 'tool_use') console.log(call.input)Extended thinking
Both models think before they answer, and thinking tokens are billed as output tokens. Thinking is either on or off:
- Off. Send
thinking: {"type": "disabled"}. This works on both models. Answers are much faster and cheaper, but weaker on arithmetic and logic. - On. Leave
thinkingout, or send{"type": "enabled", "budget_tokens": N}. Thinking comes back asthinkingcontent blocks. Add"display": "omitted"to keep the text of the blocks out of the reply. It is still billed. - No levels.
budget_tokensdoes not cap the thinking, andoutput_config.effort(lowtomax) is accepted but has no effect.max_tokensmust still be larger than anybudget_tokensyou send.
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({ baseURL: 'https://api.interodin.com', apiKey: process.env.INTERODIN_API_KEY })
const message = await client.messages.create({
model: 'odin-v1',
max_tokens: 8192,
thinking: { type: 'enabled', budget_tokens: 4096 },
messages: [{ role: 'user', content: 'Is 1,000,003 prime?' }],
})
for (const block of message.content) {
if (block.type === 'thinking') console.log('[thinking]', block.thinking)
if (block.type === 'text') console.log(block.text)
}When you continue a conversation that used tools with thinking, send the assistant's content blocks back unchanged, including any thinking blocks.
Counting tokens
POST /v1/messages/count_tokens takes the same body as /v1/messages (without max_tokens) and returns {"input_tokens": N}. It is free: counting is never billed, and it works even when your balance is zero. It does count toward your rate limits.
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({ baseURL: 'https://api.interodin.com', apiKey: process.env.INTERODIN_API_KEY })
const count = await client.messages.countTokens({
model: 'odin-v1',
system: 'You are a concise assistant.',
messages: [{ role: 'user', content: 'How many tokens is this?' }],
})
console.log(count.input_tokens)Prompt caching
Caching is automatic: you don't need cache_control breakpoints. If you send them, for example from code written for Anthropic, they are accepted and ignored. Cached input is reported in usage.cache_read_input_tokens and billed at the cached-input rate.
To keep a conversation on the same cache, send the x-interodin-session-id header with a stable id. It is scoped to your organization, so it never collides with other customers.
import os
import anthropic
client = anthropic.Anthropic(
base_url="https://api.interodin.com",
api_key=os.environ["INTERODIN_API_KEY"],
default_headers={"x-interodin-session-id": "conv_7d2a9f"},
)
message = client.messages.create(
model="odin-v1",
max_tokens=512,
messages=[{"role": "user", "content": "Summarize our plan so far."}],
)
print(message.usage.cache_read_input_tokens)See Prompt caching for details.
Vision
Image input depends on the model. Odin v1 is text-only. An image content block you put in the newest message fails with 400 image_input_not_supported. Each model page says whether the model accepts images.
document blocks (PDFs and other files) are not supported by any model and fail with 400 unsupported_content when they are in the newest message.
An image or file that came from a tool result or from an earlier turn of the conversation (for example a file a coding agent read) is replaced by a short text note, so one image in the history cannot break every later message. The model sees the note instead and can say it cannot view the image.