fix: handle invalid_tool_calls args normalization for DeepSeek

Extended ToolCallArgsParsingWrapper to handle both tool_calls and
invalid_tool_calls args formatting inconsistencies from DeepSeek:

- tool_calls.args: string -> dict (for successful calls)
- invalid_tool_calls.args: dict -> string (for failed calls)

The wrapper now normalizes both types before AIMessage construction,
preventing Pydantic validation errors in both success and error cases.

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 21:03:48 -05:00
parent 3e50868a4d
commit 27a824f4a6
2 changed files with 34 additions and 14 deletions

View File

@@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.1] - 2025-11-05
### Fixed
- Fixed Pydantic validation error for tool_calls when using DeepSeek and other AI providers that return `args` as JSON strings instead of dictionaries. Added `ToolCallArgsParsingWrapper` that monkey-patches ChatOpenAI's `_create_chat_result` method to parse string arguments before AIMessage construction.
- Fixed Pydantic validation errors for both `tool_calls` and `invalid_tool_calls` when using DeepSeek and other AI providers:
- `tool_calls.args`: Converts JSON strings to dictionaries (for successful tool calls)
- `invalid_tool_calls.args`: Converts dictionaries to JSON strings (for failed tool calls)
- Added `ToolCallArgsParsingWrapper` that monkey-patches ChatOpenAI's `_create_chat_result` method to normalize arguments before AIMessage construction.
## [0.4.0] - 2025-11-05

View File

@@ -50,19 +50,36 @@ class ToolCallArgsParsingWrapper:
if 'choices' in response_dict:
for choice in response_dict['choices']:
if 'message' in choice and 'tool_calls' in choice['message']:
tool_calls = choice['message']['tool_calls']
if tool_calls:
for tool_call in tool_calls:
if 'function' in tool_call and 'arguments' in tool_call['function']:
args = tool_call['function']['arguments']
# Parse string arguments to dict
if isinstance(args, str):
try:
tool_call['function']['arguments'] = json.loads(args)
except json.JSONDecodeError:
# Keep as string if parsing fails
pass
if 'message' not in choice:
continue
message = choice['message']
# Fix regular tool_calls: string args -> dict
if 'tool_calls' in message and message['tool_calls']:
for tool_call in message['tool_calls']:
if 'function' in tool_call and 'arguments' in tool_call['function']:
args = tool_call['function']['arguments']
# Parse string arguments to dict
if isinstance(args, str):
try:
tool_call['function']['arguments'] = json.loads(args)
except json.JSONDecodeError:
# Keep as string if parsing fails
pass
# Fix invalid_tool_calls: dict args -> string
if 'invalid_tool_calls' in message and message['invalid_tool_calls']:
for invalid_call in message['invalid_tool_calls']:
if 'args' in invalid_call:
args = invalid_call['args']
# Convert dict arguments to JSON string
if isinstance(args, dict):
try:
invalid_call['args'] = json.dumps(args)
except (TypeError, ValueError):
# Keep as-is if serialization fails
pass
# Call original method with fixed response
return original_create_chat_result(response_dict, generation_info)