Skip to content

feat(ai-proxy): translate tool use for the Anthropic provider - #133

Merged
ndreno merged 1 commit into
mainfrom
feat/ai-proxy-tool-use
Jul 21, 2026
Merged

feat(ai-proxy): translate tool use for the Anthropic provider#133
ndreno merged 1 commit into
mainfrom
feat/ai-proxy-tool-use

Conversation

@ndreno

@ndreno ndreno commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The bug

On the Anthropic translation path (Chat Completions and Responses), tool use was dropped in both directions, making Codex/Claude tool calling non-functional. OpenAI/Ollama passthrough was unaffected.

  • Request: tools / tool_choice were never forwarded to Anthropic — the model never saw the tools, so it could never call them.
  • Response: tool_use blocks were ignored (only the first text block was kept), yet finish_reason was set to "tool_calls" with no tool_calls array — a malformed turn.
  • Codex tools like apply_patch (freeform custom tools) were silently discarded.

What this does

New shared protocols::tools module, wired into both Chat Completions and Responses.

Request mapping

  • tools: OpenAI function.parameters (Chat Completions) / flat parameters (Responses) → Anthropic input_schema.
  • tool_choice: "auto"/"none" unchanged, "required"any, {function:{name}} / {name}{type:"tool", name}; parallel_tool_calls: falsedisable_parallel_tool_use: true.
  • History: assistant tool_callstool_use blocks; role:"tool" messages → tool_result blocks (consecutive results merged into one user turn).

Response mapping

  • Anthropic tool_use → OpenAI tool_calls (Chat Completions) / function_call items (Responses); content: null on a tool-only turn; finish_reason: "tool_calls".

Unsupported tools (fail visible)

  • Codex freeform custom (apply_patch), local_shell, and hosted server tools have no Anthropic representation → Responses path returns 400 custom_tools_not_supported_for_provider instead of dropping them. Route those to OpenAI.

Full custom/freeform mapping and true SSE tool-call streaming remain deferred (ADR-0024 / ADR-0030 §2) — this PR closes the non-streaming function-tool gap, which is the bulk of the value.

Testing

  • 16 new unit tests: tool schema + tool_choice mapping (both protocols), assistant tool_calls / role:"tool" history translation, consecutive tool-result merging, tool_calls emission with stringified arguments, plain-text passthrough, custom/local_shell rejection. Full suite 134 passing, clippy clean.
  • End-to-end against a WireMock Anthropic /v1/messages upstream (patched wasm, compiled .bca, running gateway):
    • Chat Completions with a get_weather function tool → outbound request to Anthropic carries tools with the correct input_schema and tool_choice: {type:"auto"}; the tool_use response comes back to the client as OpenAI tool_calls (arguments as a JSON string), finish_reason: "tool_calls".
    • Responses with a custom apply_patch tool → 400 custom_tools_not_supported_for_provider.

The Anthropic translation path dropped tools entirely in both directions,
so Codex/Claude tool calling was non-functional (OpenAI/Ollama passthrough
was unaffected):
- Request: `tools`/`tool_choice` were never forwarded, so the model never
  saw the tools and could not call them.
- Response: `tool_use` blocks were ignored and only the first text block
  kept, yet `finish_reason` was set to `tool_calls` with no `tool_calls`
  array — a malformed tool-calling turn.

Add a shared `protocols::tools` module and wire it into both Chat
Completions and Responses:
- map `tools` (`function.parameters` / flat `parameters` -> `input_schema`)
  and `tool_choice` (`required` -> `any`, named-tool forms -> `{type:tool}`,
  `parallel_tool_calls:false` -> `disable_parallel_tool_use`);
- translate assistant `tool_calls` and `role:"tool"` history to `tool_use`
  / `tool_result` blocks (consecutive results merged into one user turn);
- translate Anthropic `tool_use` responses back to OpenAI `tool_calls`
  (Chat Completions) / `function_call` (Responses), with `content: null`
  on a tool-only turn.

Codex freeform `custom` tools (apply_patch), `local_shell`, and hosted
server tools have no Anthropic representation. On the Responses path they
now return 400 `custom_tools_not_supported_for_provider` instead of being
dropped silently (fail visible, mirroring the dropped-reasoning handling).
Full custom/freeform mapping and true SSE tool-call streaming remain
deferred (ADR-0024 / ADR-0030 §2).

Tests: 16 new unit tests (tool schema + tool_choice mapping, history
translation, tool_calls emission, custom-tool rejection). Verified
end-to-end against a WireMock Anthropic /v1/messages upstream: outbound
request carries `tools`/`input_schema`, response comes back as `tool_calls`,
and a Responses custom tool returns 400.

- protocols/tools.rs: shared OpenAI <-> Anthropic tool mapping
- protocols/chat_completion.rs: message + tool translation both ways
- protocols/responses.rs: forward tools, reject custom tools with 400
- docs (dispatchers guide) + CHANGELOG
@ndreno
ndreno force-pushed the feat/ai-proxy-tool-use branch from e11d415 to 594bb02 Compare July 21, 2026 06:50
@ndreno
ndreno changed the base branch from feat/ai-proxy-auth-strategy to main July 21, 2026 06:50
@ndreno
ndreno merged commit a99a1d7 into main Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant