diff --git a/pydantic_ai_slim/pydantic_ai/profiles/openai.py b/pydantic_ai_slim/pydantic_ai/profiles/openai.py index 258a86305f..f9a2d748f8 100644 --- a/pydantic_ai_slim/pydantic_ai/profiles/openai.py +++ b/pydantic_ai_slim/pydantic_ai/profiles/openai.py @@ -47,11 +47,6 @@ def openai_model_profile(model_name: str) -> ModelProfile: _STRICT_INCOMPATIBLE_KEYS = [ 'minLength', 'maxLength', - 'pattern', - 'format', - 'minimum', - 'maximum', - 'multipleOf', 'patternProperties', 'unevaluatedProperties', 'propertyNames', @@ -61,11 +56,21 @@ def openai_model_profile(model_name: str) -> ModelProfile: 'contains', 'minContains', 'maxContains', - 'minItems', - 'maxItems', 'uniqueItems', ] +_STRICT_COMPATIBLE_STRING_FORMATS = [ + 'date-time', + 'time', + 'date', + 'duration', + 'email', + 'hostname', + 'ipv4', + 'ipv6', + 'uuid', +] + _sentinel = object() @@ -127,6 +132,9 @@ def transform(self, schema: JsonSchema) -> JsonSchema: # noqa C901 value = schema.get(key, _sentinel) if value is not _sentinel: incompatible_values[key] = value + if format := schema.get('format'): + if format not in _STRICT_COMPATIBLE_STRING_FORMATS: + incompatible_values['format'] = format description = schema.get('description') if incompatible_values: if self.strict is True: diff --git a/tests/models/test_openai.py b/tests/models/test_openai.py index 880c70eff3..78aecead66 100644 --- a/tests/models/test_openai.py +++ b/tests/models/test_openai.py @@ -12,7 +12,7 @@ import pytest from dirty_equals import IsListOrTuple from inline_snapshot import snapshot -from pydantic import BaseModel, Discriminator, Field, Tag +from pydantic import AnyUrl, BaseModel, Discriminator, Field, Tag from typing_extensions import TypedDict from pydantic_ai import Agent, ModelHTTPError, ModelRetry, UnexpectedModelBehavior @@ -1094,6 +1094,14 @@ def tool_with_default(x: int = 1) -> str: return f'{x}' # pragma: no cover +def tool_with_datetime(x: datetime) -> str: + return f'{x}' # pragma: no cover + + +def tool_with_url(x: AnyUrl) -> str: + return f'{x}' # pragma: no cover + + def tool_with_recursion(x: MyRecursiveDc, y: MyDefaultRecursiveDc): return f'{x} {y}' # pragma: no cover @@ -1156,6 +1164,45 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str: ), snapshot(True), ), + ( + tool_with_datetime, + None, + snapshot( + { + 'additionalProperties': False, + 'properties': {'x': {'format': 'date-time', 'type': 'string'}}, + 'required': ['x'], + 'type': 'object', + } + ), + snapshot(True), + ), + ( + tool_with_url, + None, + snapshot( + { + 'additionalProperties': False, + 'properties': {'x': {'format': 'uri', 'minLength': 1, 'type': 'string'}}, + 'required': ['x'], + 'type': 'object', + } + ), + snapshot(None), + ), + ( + tool_with_url, + True, + snapshot( + { + 'additionalProperties': False, + 'properties': {'x': {'type': 'string', 'description': 'minLength=1, format=uri'}}, + 'required': ['x'], + 'type': 'object', + } + ), + snapshot(True), + ), ( tool_with_recursion, None, @@ -1432,16 +1479,8 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str: { 'additionalProperties': False, 'properties': { - 'x': { - 'prefixItems': [{'type': 'integer'}], - 'type': 'array', - 'description': 'minItems=1, maxItems=1', - }, - 'y': { - 'prefixItems': [{'type': 'string'}], - 'type': 'array', - 'description': 'minItems=1, maxItems=1', - }, + 'x': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'}, + 'y': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'string'}], 'type': 'array'}, }, 'required': ['x', 'y'], 'type': 'object', @@ -1537,9 +1576,10 @@ class MyModel(BaseModel): }, 'my_recursive': {'anyOf': [{'$ref': '#'}, {'type': 'null'}]}, 'my_tuple': { + 'maxItems': 1, + 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array', - 'description': 'minItems=1, maxItems=1', }, }, 'required': ['my_recursive', 'my_patterns', 'my_tuple', 'my_list', 'my_discriminated_union'], @@ -1555,11 +1595,7 @@ class MyModel(BaseModel): 'properties': {}, 'required': [], }, - 'my_tuple': { - 'prefixItems': [{'type': 'integer'}], - 'type': 'array', - 'description': 'minItems=1, maxItems=1', - }, + 'my_tuple': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'}, 'my_list': {'items': {'type': 'number'}, 'type': 'array'}, 'my_discriminated_union': {'anyOf': [{'$ref': '#/$defs/Apple'}, {'$ref': '#/$defs/Banana'}]}, },