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/sdkpip install axiorankThe 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_errortool_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
- Framework integrations: the shared model and
onDeny. - Content-inspection engine: what gets flagged.