Python: Fix agent option merge to support dict-defined tools#4314
Python: Fix agent option merge to support dict-defined tools#4314moonbox3 merged 2 commits intomicrosoft:mainfrom
Conversation
_merge_options used getattr(tool, 'name', None) to de-duplicate tools, which returns None for dict-style tool definitions. This caused all override dict tools to be treated as duplicates of each other and of any base dict tools, silently dropping them. Add _get_tool_name() helper that extracts the name from both object-style tools (via .name attribute) and dict-style tools (via tool['function']['name']). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 92%
✓ Correctness
The diff introduces a
_get_tool_namehelper to support both object-style tools (with a.nameattribute) and dict-style tools (OpenAI{"function": {"name": ...}}format) for deduplication in_merge_options. The implementation is correct and the tests cover the key scenarios: dict-only merging, deduplication, and mixed object/dict tool lists. No bugs, race conditions, or incorrect API usage detected.
✓ Security Reliability
The diff introduces a helper
_get_tool_nameto support both dict-based and object-based tool definitions during merge deduplication. The implementation is safe: it usesisinstancechecks, safe.get()accessors, andgetattrwith a default. No injection risks, secrets, resource leaks, or unsafe deserialization. One minor pre-existing reliability concern carries forward: tools whose name resolves toNone(malformed dicts, objects without.name) all collide in the deduplication set, meaning only one nameless tool from the override list will survive if a nameless tool already exists in the base. This is not introduced by the PR but is worth noting.
✗ Test Coverage
The new tests cover dict-tool merging, dict-tool deduplication, and mixed object+dict merging, which is good. However, the most important scenario — deduplication across object and dict tools with the same name — is not tested, even though enabling that cross-format dedup is the whole point of the
_get_tool_namerefactor. Additionally,_get_tool_nameis imported in the test file but has no direct unit tests for its edge cases (malformed dicts, missing keys, non-dictfunctionvalues). These are not blocking but represent meaningful coverage gaps.
Blocking Issues
- No test verifies that a dict tool and an object tool sharing the same name are deduplicated during merge. This is the core behaviour the refactor enables and must be covered.
Suggestions
- Pre-existing minor concern (not introduced by this PR): if
_get_tool_namereturnsNonefor a tool without a name, thatNoneentersexisting_names, causing all subsequent unnamed tools to be treated as duplicates. Consider filteringNonefromexisting_namesor documenting this as intentional. - Consider discarding
Nonefromexisting_names(e.g.,existing_names = {_get_tool_name(t) for t in result["tools"]} - {None}) so that nameless/malformed tools are never silently deduplicated against each other. This is a pre-existing issue but easy to fix here. - Add direct unit tests for
_get_tool_nameedge cases: dict with no 'function' key, dict where 'function' is not a dict, dict where 'function' has no 'name', object with no 'name' attribute, and a non-dict/non-object input. - Consider testing the behaviour when multiple tools have
Nonenames (i.e.,_get_tool_namereturnsNonefor both) — currently they would be falsely deduplicated becauseNonematches in the set.
Automated review by moonbox3's agents
…soft#4303) - Exclude None from existing_names set so nameless/malformed tools are not silently deduplicated against each other - Add test for cross-type dedup (dict tool + object tool with same name) - Add test verifying nameless tools are preserved (not falsely deduped) - Add unit tests for _get_tool_name edge cases: missing function key, non-dict function value, missing name, no name attribute, non-dict inputs, and valid dict/object tools Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 92%
✓ Correctness
The change correctly prevents false deduplication of tools whose names cannot be extracted (i.e.,
_get_tool_namereturnsNone). By removingNonefrom theexisting_namesset, nameless tools in the override list are no longer incorrectly matched against nameless tools in the base list. The accompanying tests thoroughly cover edge cases for_get_tool_nameand the deduplication logic. No correctness issues found.
✓ Security Reliability
The diff is a small, well-scoped reliability fix that prevents tools with no extractable name (where
_get_tool_namereturnsNone) from being falsely deduplicated during option merging. The fix correctly removesNonefrom the dedup set so that nameless tools from the override are always preserved. Accompanying tests thoroughly cover edge cases of_get_tool_nameand the deduplication behavior. No security or reliability concerns found.
✓ Test Coverage
The diff adds a small but important fix to exclude None from the deduplication set in _merge_options, and provides thorough test coverage for both the fix and the _get_tool_name helper. The core bug scenario (nameless tools falsely deduplicated) is directly tested, and edge cases for _get_tool_name are well covered. One minor gap: there is no test combining nameless and named tools in the same merge to verify both behaviors work simultaneously.
Suggestions
- Consider adding a combined scenario test where base contains both a nameless tool and a named tool, and override contains both a nameless tool and a duplicate-named tool. This would verify that nameless tools are preserved while named duplicates are still correctly deduplicated in a single merge call.
Automated review by moonbox3's agents
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
Motivation and Context
When tools are defined as raw dictionaries (e.g.
{"type": "function", "function": {"name": "..."}}), the option merge logic usedgetattr(t, "name", None)which always returnedNonefor dicts, causing all dict-defined tools to be silently dropped as false duplicates.Fixes #4303
Description
The root cause was that
_merge_optionsassumed tools were always objects with a.nameattribute. Dict-defined tools store their name nested undertool["function"]["name"], sogetattrnever found it and every tool resolved toNone, collapsing them during deduplication. The fix introduces a_get_tool_namehelper that handles both object-style and dict-style tool definitions, and uses it in the deduplication logic. Tests cover dict-only, mixed, and deduplication scenarios.Contribution Checklist