Docs

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.

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_uses is 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 thinking out, or send {"type": "enabled", "budget_tokens": N}. Thinking comes back as thinking content blocks. Add "display": "omitted" to keep the text of the blocks out of the reply. It is still billed.
  • No levels. budget_tokens does not cap the thinking, and output_config.effort (low to max) is accepted but has no effect. max_tokens must still be larger than any budget_tokens you 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.

On this page