-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add warning about OpenAI models + dict typed tools #3712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -913,6 +913,16 @@ def _map_json_schema(self, o: OutputObjectDefinition) -> chat.completion_create_ | |
| return response_format_param | ||
|
|
||
| def _map_tool_definition(self, f: ToolDefinition) -> chat.ChatCompletionToolParam: | ||
| if _has_dict_typed_params(f.parameters_json_schema): | ||
| warnings.warn( | ||
| f"Tool '{f.name}' has dict-typed parameters that OpenAI's API will silently ignore. " | ||
| f'Use a Pydantic BaseModel with explicit fields instead of dict types, ' | ||
| f'or switch to a different provider which supports dict types. ' | ||
| f'See: https://github.com/pydantic/pydantic-ai/issues/3654', | ||
| UserWarning, | ||
| stacklevel=4, | ||
| ) | ||
|
|
||
| tool_param: chat.ChatCompletionToolParam = { | ||
| 'type': 'function', | ||
| 'function': { | ||
|
|
@@ -1527,6 +1537,16 @@ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) - | |
| return tools | ||
|
|
||
| def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we'll also need to do the check in |
||
| if _has_dict_typed_params(f.parameters_json_schema): | ||
| warnings.warn( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move the warning into the helper method so that we don't repeat the text |
||
| f"Tool '{f.name}' has dict-typed parameters that OpenAI's API will silently ignore. " | ||
| f'Use a Pydantic BaseModel with explicit fields instead of dict types, ' | ||
| f'or switch to a different provider which supports dict types. ' | ||
| f'See: https://github.com/pydantic/pydantic-ai/issues/3654', | ||
| UserWarning, | ||
| stacklevel=4, | ||
| ) | ||
|
|
||
| return { | ||
| 'name': f.name, | ||
| 'parameters': f.parameters_json_schema, | ||
|
|
@@ -2677,3 +2697,38 @@ def _map_mcp_call( | |
| provider_name=provider_name, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _has_dict_typed_params(json_schema: dict[str, Any]) -> bool: | ||
| """Detect if a JSON schema contains dict-typed parameters. | ||
|
|
||
| Dict types manifest as objects with additionalProperties that is: | ||
| - True (allows any additional properties) | ||
| - A schema object (e.g., {'type': 'string'}) | ||
|
|
||
| These are incompatible with OpenAI's API which silently drops them. | ||
|
|
||
| c.f. https://github.com/pydantic/pydantic-ai/issues/3654 | ||
| """ | ||
| properties: dict[str, Any] = json_schema.get('properties', {}) | ||
| for prop_schema in properties.values(): | ||
| if isinstance(prop_schema, dict): | ||
| # Check for object type with additionalProperties | ||
| if prop_schema.get('type') == 'object': # type: ignore[reportUnknownMemberType] | ||
| additional_props: Any = prop_schema.get('additionalProperties') # type: ignore[reportUnknownMemberType] | ||
| # If additionalProperties is True or a schema object (not False/absent) | ||
| if additional_props not in (False, None): | ||
| return True | ||
|
|
||
| # Check arrays of objects with additionalProperties | ||
| if prop_schema.get('type') == 'array': # type: ignore[reportUnknownMemberType] | ||
| items: Any = prop_schema.get('items', {}) # type: ignore[reportUnknownMemberType] | ||
| if isinstance(items, dict) and items.get('type') == 'object': # type: ignore[reportUnknownMemberType] | ||
| if items.get('additionalProperties') not in (False, None): # type: ignore[reportUnknownMemberType] | ||
| return True | ||
|
|
||
| # Recursively check nested objects | ||
| if 'properties' in prop_schema and _has_dict_typed_params(prop_schema): # type: ignore[reportUnknownArgumentType] | ||
| return True | ||
|
|
||
| return False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3325,3 +3325,53 @@ async def test_openai_reasoning_in_thinking_tags(allow_model_requests: None): | |
| """, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def test_has_dict_typed_params_simple_dict(): | ||
| """Test detection of simple dict[str, str] type.""" | ||
| from pydantic_ai.models.openai import _has_dict_typed_params # pyright: ignore[reportPrivateUsage] | ||
|
|
||
| schema = {'properties': {'my_dict': {'type': 'object', 'additionalProperties': {'type': 'string'}}}} | ||
| assert _has_dict_typed_params(schema) is True | ||
|
|
||
|
|
||
| def test_has_dict_typed_params_nested_dict(): | ||
| """Test detection of nested dict types.""" | ||
| from pydantic_ai.models.openai import _has_dict_typed_params # pyright: ignore[reportPrivateUsage] | ||
|
|
||
| schema = { | ||
| 'properties': { | ||
| 'nested': {'type': 'object', 'properties': {'inner_dict': {'type': 'object', 'additionalProperties': True}}} | ||
| } | ||
| } | ||
| assert _has_dict_typed_params(schema) is True | ||
|
|
||
|
|
||
| def test_has_dict_typed_params_array_of_dicts(): | ||
| """Test detection of list[dict[str, int]] type.""" | ||
| from pydantic_ai.models.openai import _has_dict_typed_params # pyright: ignore[reportPrivateUsage] | ||
|
|
||
| schema = { | ||
| 'properties': { | ||
| 'dict_list': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': {'type': 'integer'}}} | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to test this with a tool function that actually has a So maybe can we build an agent with a tool like that, then run it, and test that a warning was emitted? |
||
| assert _has_dict_typed_params(schema) is True | ||
|
|
||
|
|
||
| def test_has_dict_typed_params_basemodel_no_warning(): | ||
| """Test that BaseModel with explicit fields doesn't trigger warning.""" | ||
| from pydantic_ai.models.openai import _has_dict_typed_params # pyright: ignore[reportPrivateUsage] | ||
|
|
||
| schema = { | ||
| 'properties': { | ||
| 'name': {'type': 'string'}, | ||
| 'age': {'type': 'integer'}, | ||
| 'nested_object': { | ||
| 'type': 'object', | ||
| 'properties': {'field1': {'type': 'string'}}, | ||
| 'additionalProperties': False, | ||
| }, | ||
| } | ||
| } | ||
| assert _has_dict_typed_params(schema) is False | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.