support Anthropic server-tool passthrough (web_search/web_fetch) and tool_choice dispatch#661
Conversation
pi-ai's Anthropic parser silently drops server_tool_use / web_search_tool_result / web_fetch_tool_result content blocks, since it only recognizes text/thinking/redacted_thinking/tool_use. This lost web_search and web_fetch built-in tool results on the client-facing response. - Add builtin-tools.ts as the shared allowlist of Anthropic built-in tool-type strings (all web_search_* and web_fetch_* versions) and their response block types, used by both the inbound parser and the raw-response tap. - anthropic-to-context.ts: split built-in server-side tool declarations out of context.tools (pi-ai's Tool type can't represent them) and carry them out-of-band as builtinTools for re-injection into the outgoing payload. - fetch-tap.ts: reconstruct server-tool content blocks directly from the raw upstream bytes (both non-streaming JSON and streaming SSE), keyed by requestId, for consumption exactly once by the response serializer. - context-to-anthropic.ts: splice captured blocks back into both the non-streaming response and the streaming SSE frame sequence. - index.ts: wire requestId/builtinTools through the beta messages route.
All inbound parsers (anthropic/openai/responses-to-context.ts)
normalize tool_choice to OpenAI's shape (string, or
{type:"function", function:{name}}), since pi-ai's own OpenAI
provider expects exactly that shape verbatim. But pi-ai's Anthropic
provider does not convert it back — it only wraps bare strings and
otherwise forwards objects as-is, assuming they're already
Anthropic-shaped ({type:"tool", name}).
An Anthropic-originated request with tool_choice:{type:"tool",name}
therefore got normalized to OpenAI shape on the way in, then
round-tripped straight back out to Anthropic's own API unconverted,
which rejected it with a 400 invalid_request_error on tool_choice's
type field.
Add toAnthropicToolChoice() in pi-ai-executor.ts and apply it at the
single callOptions construction site, once the resolved dispatch
provider is known, mirroring the existing provider-conditional
patterns already there (builtin tools, headers).
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR review follow-up: a malformed inbound tool_choice
({type:"function", function:{}}) previously converted to
{type:"tool", name: undefined}. JSON.stringify silently drops the
undefined key, so Anthropic would receive {"type":"tool"} with no
name and reject it with a different, less useful error than before.
Forward the original (still-invalid) shape unchanged when name is
missing instead of manufacturing an incomplete Anthropic shape.
User description
Summary
server_tool_use,web_search_tool_result,web_fetch_tool_result) that pi-ai's parser silently drops, covering all currentweb_search_*/web_fetch_*tool-type versions.tool_choicebeing sent to Anthropic in OpenAI's shape ({type:"function",function:{name}}) instead of Anthropic's own shape ({type:"tool",name}), which caused a 400invalid_request_errorand provider cooldown on the "cc" (Claude Code OAuth) provider.Details
Server-tool block passthrough (
918a7e04)pi-ai's Anthropic response parser only recognizes
text/thinking/redacted_thinking/tool_useblocks, silently dropping Anthropic's built-in server-side tool blocks. This lostweb_search/web_fetchresults on the client-facing response.builtin-tools.ts: shared allowlist of Anthropic built-in tool-type strings (allweb_search_*/web_fetch_*versions) and their response block types.anthropic-to-context.ts: splits built-in tool declarations out ofcontext.tools(pi-ai'sTooltype can't represent them), carried out-of-band for re-injection into the outgoing payload.fetch-tap.ts: reconstructs server-tool content blocks from raw upstream bytes (streaming + non-streaming), keyed by request ID.context-to-anthropic.ts: splices captured blocks back into both the non-streaming response and streaming SSE frames.tool_choice shape fix (
dba8b0d4)All inbound parsers normalize
tool_choiceto OpenAI's shape (pi-ai's OpenAI provider expects that verbatim), but pi-ai's Anthropic provider doesn't convert it back — it assumes objects are already Anthropic-shaped. An Anthropic-originatedtool_choice:{type:"tool",name}request got normalized to OpenAI shape inbound, then round-tripped unconverted back out to Anthropic's API, which rejected it. AddedtoAnthropicToolChoice()inpi-ai-executor.ts, applied once the resolved dispatch provider is known.Both fixes are scoped to
inference-v2only (v1 shares no code with v2).Testing
bun run typecheck— cleanbun run test— 2322 tests pass (full suite via pre-commit hook), including new regression coverage for both fixesDescription
Restore Anthropic server-tool blocks (
server_tool_use,web_search_tool_result,web_fetch_tool_result) dropped by pi-ai's parserFix
tool_choicebeing sent to Anthropic in OpenAI's shape instead of Anthropic's{type:"tool",name}shapeAdd
builtin-tools.tsallowlist andfetch-tap.tsraw-byte reconstruction for server-tool blocksAdd comprehensive tests for server-tool block splicing and
tool_choiceconversion