AxioRankDocs
Integrations

Anthropic Messages API

Guard the tool_use loop you run yourself with the Anthropic SDK, so every dispatch is scored before it executes.

Unlike a framework that executes tools for you, the Anthropic SDK does not run your tools. Your loop does: the model returns tool_use content blocks, you execute them, and you send back tool_result blocks. So this adapter guards your dispatch step. Hand it each tool_use block plus your handlers, and it scores the call through AxioRank first, returning a ready-to-send tool_result.

Install

npm install @axiorank/sdk
pip install axiorank

The adapter only reads a block's id / name / input, so it never imports the Anthropic client itself. Bring your own (pip install "axiorank[anthropic]" installs it for convenience in Python).

Guard your dispatch

guardToolHandlers / guard_tool_handlers builds one dispatcher over a map of tool handlers: pass any tool_use block and it scores the call, runs the matching handler on allow, and hands back the tool_result block.

import Anthropic from "@anthropic-ai/sdk";
import { AxioRank } from "@axiorank/sdk";
import { guardToolHandlers } from "@axiorank/sdk/anthropic";

const anthropic = new Anthropic();
const axio = new AxioRank({ apiKey: process.env.AXIORANK_API_KEY! });

const dispatch = guardToolHandlers(
  { deployToProd: async ({ service }) => deploy(service) },
  axio.trace(), // one shared trace id for the whole run
  { onDeny: "return" },
);

const msg = await anthropic.messages.create({ model: "claude-opus-4-8", tools, messages });

const toolResults = [];
for (const block of msg.content) {
  if (block.type === "tool_use") toolResults.push(await dispatch(block));
}
messages.push({ role: "assistant", content: msg.content });
messages.push({ role: "user", content: toolResults });
import anthropic
from axiorank import AxioRank
from axiorank.integrations.anthropic import guard_tool_handlers

client = anthropic.Anthropic()
axio = AxioRank(api_key="axr_live_...")

dispatch = guard_tool_handlers(
    {"deploy": lambda i: deploy(i["service"])},
    axio,
    on_deny="return",
)

msg = client.messages.create(model="claude-opus-4-8", tools=tools, messages=messages)

results = [dispatch(b) for b in msg.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": msg.content})
messages.append({"role": "user", "content": results})

Driving the AsyncAnthropic client? Use guard_tool_handlers_async with an AsyncAxioRank; the returned dispatcher is awaitable and accepts sync or async handlers. Blocks can be SDK objects or plain dicts.

Guard a single block

When you'd rather keep your own routing, guard one block at a time with your executor. guardToolUse(block, axio, executor, opts) in TypeScript, or guard_tool_use(block, axio, executor) in Python (async variant: guard_tool_use_async). The executor receives (name, input) and returns the tool's output; the call resolves to the tool_result block to send back.

import { guardToolUse } from "@axiorank/sdk/anthropic";

const result = await guardToolUse(block, axio, (name, input) => runTool(name, input));

When a call is denied

This adapter defaults to onDeny: "return" / on_deny="return" (the other adapters default to raising): a denied call comes back as a tool_result with is_error: true and a short refusal as its content, so the model reads why it was blocked and can re-plan. Pass onDeny: "throw" (TypeScript) or on_deny="raise" (Python) to raise AxioRankDeniedError and fail the loop instead.

  • Unknown tool: a block naming a tool with no registered handler yields an is_error tool_result (it is not scored, since nothing runs), so the loop keeps going.
  • Handler exceptions propagate: a handler that throws is never masked as a tool_result. Only AxioRank denials are shaped into one.
  • Output inspection: with inspectResults: true / inspect_results=True, an untrusted-source tool's output is also inspected before the model ingests it; a denied output is shaped exactly like a denied call.

Assembled blocks, correlated runs

Operate on the assembled tool_use block after the message completes, not on streaming input_json_delta events. Pass axio.trace() instead of the bare client (in both TypeScript and Python, from axiorank 0.7.2) so every dispatch in one run shares a trace id, letting the gateway stitch a multi-step kill chain into a single trace.

Next steps

On this page