fix(lib): preserve custom tool calls in parse_chat_completion#3505
fix(lib): preserve custom tool calls in parse_chat_completion#3505PranavMishra28 wants to merge 1 commit into
Conversation
`parse_chat_completion` (behind `client.chat.completions.parse()` and the streaming `get_final_completion()`) logged a warning and then *dropped* every `custom`-type tool call, so a supported GPT-5 tool call the model made vanished from the parsed result (`tool_calls` could even come back `None`). The handling was also inconsistent: the trailing `else` branch already preserves any non-function tool call by appending it unchanged; only `custom` was special-cased to discard. Append the custom call the same way (it has no schema to parse `parsed_arguments` against, so it's surfaced as-is) instead of dropping it, and remove the now-inaccurate "Ignoring tool call" warning that fired on every custom call in normal use. Adds tests/lib/chat/test_parse_custom_tool_calls.py covering a custom-only and a mixed function+custom message; both fail before this change. Note: `ParsedChatCompletionMessage.tool_calls` is a generated type narrowed to `list[ParsedFunctionToolCall]`, so a custom call round-trips through attribute access and its own `model_dump()` but its `custom` payload is still dropped when the whole completion is serialized — same limitation the existing `else` branch already has for non-function calls. Fully fixing serialization needs the generated type widened, which is out of scope for a lib-only change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| # includes it and callers rely on `tool_calls` reflecting every call | ||
| # the model made. This mirrors the `else` branch below, which already | ||
| # preserves any non-function tool call unchanged. | ||
| tool_calls.append(tool_call) |
There was a problem hiding this comment.
leaving a note since dropping custom calls looks intentional at a glance — but the else branch just below already appends any non-function tool call as-is, so custom was actually the only tool-call type being discarded here. this just makes it consistent. custom calls have no schema to parse args against, so they pass through unchanged (no parsed_arguments), same as the else path.
|
@seratch would appreciate your eyes on this when you get a chance. small one: parse_chat_completion() drops GPT-5 custom tool calls on a custom-only turn (the raw completion keeps them, only .parse() loses them). the fix just appends the custom call instead of discarding it, mirroring how the else branch already preserves non-function calls. tests included, targeted at next. happy to adjust if you'd rather handle it a different way. |
What
parse_chat_completion()— the helper behindclient.chat.completions.parse()and streamingget_final_completion()— logs a warning and then drops everycustom-type tool call:So a supported GPT-5 custom tool call that the model emits disappears from the parsed result —
message.tool_callsomits it, and for a custom-only turn it comes backNone. The raw (unparsed)ChatCompletioncarries the call correctly; only.parse()loses it.The handling is also internally inconsistent: the trailing
elsebranch already preserves any non-function tool call by appending it unchanged — onlycustomis special-cased to discard.Fix
Append the custom tool call instead of dropping it, mirroring the
elsebranch. Custom calls legitimately don't getparsed_arguments(there's no schema to parse their free-form input against), so they're surfaced as-is. The now-inaccurate"Ignoring tool call"warning — which fired on every custom call in normal use — is removed.Tests
tests/lib/chat/test_parse_custom_tool_calls.py(pure unit tests, no mock server / live API): a custom-only message and a mixed function+custom message. Both fail onnextbefore this change (the custom call is dropped /tool_callsisNone) and pass after. The existingtests/lib/chat/test_completions.pysuite still passes;ruffandpyright --strictare clean on the changed files.Scope / honest limitation
ParsedChatCompletionMessage.tool_callsis a generated type narrowed tolist[ParsedFunctionToolCall]. With this fix a custom call round-trips through attribute access and its ownmodel_dump(), but itscustompayload is still dropped when the entire completion is serialized (pydantic serializes the list by its declared element type) — the same limitation the existingelsebranch already has for non-function calls. Fully fixing serialization would require widening the generatedtool_callstype, which is out of scope for alib/-only change; happy to coordinate if you'd prefer that path.Targeted at
nextto match where external fixes merge.