Fix: Anthropic SDK toolRunner Drops Headers
The Error
Using client.messages.toolRunner with a proxy or gateway (like Cloudflare AI Gateway), the first tool call works, but follow-up calls fail with:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "x-api-key header is required"
}
}
The same setup works perfectly with client.messages.create for single calls.
Quick Fix
Replace toolRunner with a manual tool-use loop:
async function runWithTools(
client: Anthropic,
params: Anthropic.MessageCreateParams,
toolHandlers: Record<string, (input: any) => Promise<string>>
): Promise<Anthropic.Message> {
let messages = [...params.messages];
while (true) {
const response = await client.messages.create({
...params,
messages,
});
if (response.stop_reason !== "tool_use") {
return response;
}
const toolBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: await Promise.all(
toolBlocks.map(async (block) => ({
type: "tool_result" as const,
tool_use_id: block.id,
content: String(await toolHandlers[block.name](block.input)),
}))
),
});
}
}
What’s Happening
client.messages.toolRunner is a convenience helper that automatically handles the tool-use loop: it sends the initial message, executes tools locally when the model requests them, and sends tool results back in follow-up API calls.
The bug: the follow-up API calls made internally by toolRunner do not include defaultHeaders that were set on the client instance. Only the first call (initial prompt) includes all headers correctly.
This breaks any setup where custom headers are required for authentication:
const client = new Anthropic({
baseURL: "https://gateway.ai.cloudflare.com/v1/{account}/anthropic",
apiKey: "not-used", // Proxy handles the real key
defaultHeaders: {
"cf-aig-authorization": "Bearer <CF_API_TOKEN>", // Dropped on follow-up calls
},
});
Call sequence:
Call 1 (initial prompt):
Headers: { "cf-aig-authorization": "Bearer ...", "x-api-key": "not-used" }
Result: Model requests tool_use
Call 2 (tool results — INTERNAL to toolRunner):
Headers: { "x-api-key": "not-used" } // cf-aig-authorization MISSING
Result: 401 authentication_error
Affected setups:
- Cloudflare AI Gateway (unified billing via
cf-aig-authorization) - Cloudflare AI Gateway (BYOK via
cf-aig-authorization) - Any custom proxy using custom headers for authentication
- Any middleware that injects provider keys based on request headers
The bug affects both client.messages.toolRunner and client.beta.messages.toolRunner.
Step-by-Step Solution
Option 1: Manual Tool Loop (Recommended)
Replace toolRunner with an explicit loop. Each client.messages.create call independently sends all headers:
import Anthropic from "@anthropic-ai/sdk";
import { zodTool } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic({
baseURL: "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic",
apiKey: "not-used",
defaultHeaders: {
"cf-aig-authorization": "Bearer <CF_API_TOKEN>",
},
});
const tools = [
{
name: "get_weather",
description: "Get weather for a location",
input_schema: {
type: "object" as const,
properties: { location: { type: "string" } },
required: ["location"],
},
},
];
const toolHandlers: Record<string, (input: any) => string> = {
get_weather: (input) => `Sunny, 22C in ${input.location}`,
};
async function chat(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
for (let i = 0; i < 10; i++) { // Bounded loop — max 10 tool iterations
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools,
messages,
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.type === "text" ? textBlock.text : "";
}
if (response.stop_reason !== "tool_use") {
return `Unexpected stop_reason: ${response.stop_reason}`;
}
const toolUseBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
);
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = toolUseBlocks.map(
(block) => ({
type: "tool_result" as const,
tool_use_id: block.id,
content: toolHandlers[block.name]?.(block.input) ?? "Unknown tool",
})
);
messages.push({ role: "user", content: toolResults });
}
return "Max tool iterations reached";
}
Option 2: Per-Request Headers
If you still want to use toolRunner, pass headers on each request via request options (if supported by your SDK version):
// Check if your SDK version supports per-request options in toolRunner
const result = await client.messages.toolRunner({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "What's the weather in London?" }],
tools: [weatherTool],
}, {
headers: {
"cf-aig-authorization": "Bearer <CF_API_TOKEN>",
},
});
Option 3: Use Vercel AI SDK
The Vercel AI SDK (@ai-sdk/anthropic) makes independent doGenerate calls per step with headers intact:
import { anthropic } from "@ai-sdk/anthropic";
import { generateText, tool } from "ai";
import { z } from "zod";
const result = await generateText({
model: anthropic("claude-sonnet-4-20250514", {
// Headers are sent on every call
}),
tools: {
getWeather: tool({
description: "Get weather",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => `Sunny in ${location}`,
}),
},
prompt: "What's the weather in London?",
maxSteps: 5,
});
Prevention
- Test multi-turn tool calls through your proxy before going to production — the bug only appears on follow-up calls
- Prefer manual tool loops over
toolRunnerwhen using proxies or gateways - Log all outgoing headers to detect when they get dropped
- Pin your SDK version and test header propagation after each upgrade
Related Issues
- Fix: Anthropic SDK Streaming Hangs Indefinitely
- Fix: Claude API Error 401 Authentication
- Advanced Claude Skills with Tool Use and Function Calling
Tools That Help
When debugging proxy and gateway authentication issues, a dev tool extension can help inspect outgoing request headers to verify they are being sent correctly on each API call.