Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions pydantic_ai_slim/pydantic_ai/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Comment on lines +918 to +921
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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',
f"Tool {f.name!r} has `dict`-typed parameters that OpenAI's API will silently ignore. "
f'Use a Pydantic `BaseModel`, `dataclass`, or `TypedDict` with explicit fields instead, '
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': {
Expand Down Expand Up @@ -1527,6 +1537,16 @@ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -
return tools

def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we'll also need to do the check in _map_json_schema

if _has_dict_typed_params(f.parameters_json_schema):
warnings.warn(
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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
50 changes: 50 additions & 0 deletions tests/models/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}}}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 dict argument, or a BaseModel argument that itself has a dict field, instead of testing the schemas directly.

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
Loading