Skip to content

Python: normalize single Anthropic tools#6903

Open
VectorPeak wants to merge 3 commits into
microsoft:mainfrom
VectorPeak:codex-anthropic-normalize-single-tool
Open

Python: normalize single Anthropic tools#6903
VectorPeak wants to merge 3 commits into
microsoft:mainfrom
VectorPeak:codex-anthropic-normalize-single-tool

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

What Problem This Solves

The Python Anthropic provider accepted options["tools"] at the provider boundary, but then treated that value as though it were always an iterable sequence during request preparation. That is narrower than the framework-level ChatOptions.tools contract, where a caller can naturally pass either one tool-like value or a sequence of tool-like values.

This is a real triggerable path, not just a typing edge case. A caller can pass a single decorated callable, FunctionTool, or dict tool specification directly through ChatOptions(tools=tool). That shape should be normalized before Anthropic-specific request preparation begins, so the provider maps the same logical single tool the same way whether it arrives as tools=tool or tools=[tool].

Direct iteration created two different single-tool failure modes:

  • A single FunctionTool or decorated callable could fail before the Anthropic payload was built, because the provider attempted to iterate the tool object itself.
  • A single dict tool could be silently misread, because Python dict iteration yields keys. The provider could therefore see "type", "name", and "description" as separate tool items instead of preserving the dictionary as one Anthropic tool specification.

The bug therefore sits at the framework-to-provider boundary. Core normalization should decide whether the caller supplied one tool or many; the Anthropic provider should then perform Anthropic-specific conversion on already-normalized tool entries.

Changes

This PR normalizes options["tools"] with the shared core normalize_tools() helper before Anthropic-specific tool handling runs.

That keeps the responsibility split clean:

  • Core tool normalization decides whether the caller supplied one tool or many tools.
  • Anthropic-specific preparation continues to translate already-normalized tool objects and tool specifications into Anthropic request payloads.
  • Existing Anthropic branches for custom tools, dict tools, shell tool alias handling, MCP server routing, tool name aliases, and tool choice behavior remain downstream of the same provider mapping function.

The production change is intentionally small: _prepare_tools_for_anthropic(...) now iterates over normalize_tools(tools) instead of iterating over tools directly. In practical terms, tools=<single tool> now reaches the same Anthropic preparation logic as tools=[<single tool>].

Evidence

Focused Anthropic regression coverage now exercises the two single-tool shapes that exposed the bug:

ChatOptions(tools=<decorated function tool>)
ChatOptions(tools={"type": "custom", "name": "custom_tool", ...})

The tests verify that these inputs prepare one Anthropic tool entry with the expected name/type fields. That proves the provider no longer treats a single tool object as the iterable container, and no longer splits a single dict tool into mapping keys.

The reviewer follow-up also removed unnecessary type: ignore[arg-type] comments from the new single-tool tests, so the tests now exercise the typed ChatOptions.tools surface directly instead of hiding the accepted input shape behind ignores.

This evidence is intentionally scoped to the PR diff, focused Anthropic regression tests, reviewer feedback, and visible PR checks. It does not claim a full repository CI or repository-wide typecheck run.

Possible call chain / impact

Before this change, a representative failing path was:

caller builds ChatOptions(tools=<single FunctionTool/callable/dict>)
  -> AnthropicChatClient._prepare_tools_for_anthropic(...)
  -> direct iteration of options["tools"]
  -> TypeError for a non-iterable tool object, or key-by-key iteration for a dict
  -> request preparation fails or produces the wrong tool payload before reaching Anthropic

After this change, the path is:

caller builds ChatOptions(tools=<single FunctionTool/callable/dict or sequence>)
  -> AnthropicChatClient._prepare_tools_for_anthropic(...)
  -> normalize_tools(tools)
  -> existing Anthropic-specific FunctionTool / dict / MCP / shell-tool conversion
  -> Anthropic request payload tools

The compatibility impact should be narrow and positive. Existing callers that already pass a list or other supported sequence continue through the same Anthropic conversion logic after normalization. The newly fixed callers are the ones using the broader framework API shape with a single tool, which now behaves like the equivalent one-item sequence for the covered request-preparation fields.

This is a localized Anthropic provider compatibility fix, not a broad tool-calling rewrite or a change to Anthropic request semantics.

Description & Review Guide

  • What are the major changes?

    • Reuse core normalize_tools() before Anthropic-specific tool conversion.
    • Add regression coverage for a single decorated function tool and a single dict tool.
  • What is the impact of these changes?

    • Single-tool inputs now prepare the same Anthropic request payload as the equivalent one-item list.
    • Existing list/sequence inputs, shell tool aliasing, MCP routing, and tool choice behavior stay on the same conversion path.
  • What do you want reviewers to focus on?

    • Whether using the shared core tool normalization helper is the right provider-level boundary before Anthropic-specific mapping.

Related Issue

Fixes #6901

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) - a workflow keeps the label and title prefix in sync automatically.

Copilot AI review requested due to automatic review settings July 3, 2026 12:13
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the Anthropic Python provider’s handling of ChatOptions["tools"] when callers pass a single tool (either a FunctionTool/decorated function or a single dict spec), aligning it with the framework-wide tools surface and other providers.

Changes:

  • Normalize tools via shared normalize_tools() before applying Anthropic-specific tool mapping.
  • Add regression tests for a single FunctionTool input and a single dict tool specification.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
python/packages/anthropic/agent_framework_anthropic/_chat_client.py Uses shared normalize_tools() so single-tool inputs don’t break or get misinterpreted during Anthropic tool preparation.
python/packages/anthropic/tests/test_anthropic_client.py Adds regression coverage to ensure single-tool inputs produce the same Anthropic request payload shape as one-item lists.

Comment thread python/packages/anthropic/tests/test_anthropic_client.py Outdated
) -> None:
"""Test passing through a single dict tool."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools={"type": "custom", "name": "custom_tool", "description": "A custom tool"}) # type: ignore[arg-type]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d9e11e6 by removing the remaining unnecessary # type: ignore[arg-type] from the single mapping tool regression test.

Validation run locally:

  • uv run pytest packages/anthropic/tests/test_anthropic_client.py -q
  • uv run ruff check packages/anthropic/tests/test_anthropic_client.py
  • git diff --check -- python/packages/anthropic/tests/test_anthropic_client.py

VectorPeak and others added 2 commits July 3, 2026 20:19
Thanks

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@VectorPeak

Copy link
Copy Markdown
Contributor Author

@copilot-pull-request-reviewer I addressed the review feedback in d9e11e6 by removing the unnecessary type: ignore[arg-type] from the single mapping tool regression test.

Local validation completed:

  • uv run pytest packages/anthropic/tests/test_anthropic_client.py -q
  • uv run ruff check packages/anthropic/tests/test_anthropic_client.py
  • git diff --check -- python/packages/anthropic/tests/test_anthropic_client.py

Could you please re-review when you get a chance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: Anthropic provider does not normalize single tools

3 participants