Skip to content

Commit f36aaa6

Browse files
authored
Allow string format, pattern and others in OpenAI strict JSON mode (#2420)
1 parent f8dbc31 commit f36aaa6

File tree

2 files changed

+68
-24
lines changed

2 files changed

+68
-24
lines changed

pydantic_ai_slim/pydantic_ai/profiles/openai.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ def openai_model_profile(model_name: str) -> ModelProfile:
4747
_STRICT_INCOMPATIBLE_KEYS = [
4848
'minLength',
4949
'maxLength',
50-
'pattern',
51-
'format',
52-
'minimum',
53-
'maximum',
54-
'multipleOf',
5550
'patternProperties',
5651
'unevaluatedProperties',
5752
'propertyNames',
@@ -61,11 +56,21 @@ def openai_model_profile(model_name: str) -> ModelProfile:
6156
'contains',
6257
'minContains',
6358
'maxContains',
64-
'minItems',
65-
'maxItems',
6659
'uniqueItems',
6760
]
6861

62+
_STRICT_COMPATIBLE_STRING_FORMATS = [
63+
'date-time',
64+
'time',
65+
'date',
66+
'duration',
67+
'email',
68+
'hostname',
69+
'ipv4',
70+
'ipv6',
71+
'uuid',
72+
]
73+
6974
_sentinel = object()
7075

7176

@@ -127,6 +132,9 @@ def transform(self, schema: JsonSchema) -> JsonSchema: # noqa C901
127132
value = schema.get(key, _sentinel)
128133
if value is not _sentinel:
129134
incompatible_values[key] = value
135+
if format := schema.get('format'):
136+
if format not in _STRICT_COMPATIBLE_STRING_FORMATS:
137+
incompatible_values['format'] = format
130138
description = schema.get('description')
131139
if incompatible_values:
132140
if self.strict is True:

tests/models/test_openai.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313
from dirty_equals import IsListOrTuple
1414
from inline_snapshot import snapshot
15-
from pydantic import BaseModel, Discriminator, Field, Tag
15+
from pydantic import AnyUrl, BaseModel, Discriminator, Field, Tag
1616
from typing_extensions import TypedDict
1717

1818
from pydantic_ai import Agent, ModelHTTPError, ModelRetry, UnexpectedModelBehavior
@@ -1094,6 +1094,14 @@ def tool_with_default(x: int = 1) -> str:
10941094
return f'{x}' # pragma: no cover
10951095

10961096

1097+
def tool_with_datetime(x: datetime) -> str:
1098+
return f'{x}' # pragma: no cover
1099+
1100+
1101+
def tool_with_url(x: AnyUrl) -> str:
1102+
return f'{x}' # pragma: no cover
1103+
1104+
10971105
def tool_with_recursion(x: MyRecursiveDc, y: MyDefaultRecursiveDc):
10981106
return f'{x} {y}' # pragma: no cover
10991107

@@ -1155,6 +1163,45 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str:
11551163
),
11561164
snapshot(None),
11571165
),
1166+
(
1167+
tool_with_datetime,
1168+
None,
1169+
snapshot(
1170+
{
1171+
'additionalProperties': False,
1172+
'properties': {'x': {'format': 'date-time', 'type': 'string'}},
1173+
'required': ['x'],
1174+
'type': 'object',
1175+
}
1176+
),
1177+
snapshot(True),
1178+
),
1179+
(
1180+
tool_with_url,
1181+
None,
1182+
snapshot(
1183+
{
1184+
'additionalProperties': False,
1185+
'properties': {'x': {'format': 'uri', 'minLength': 1, 'type': 'string'}},
1186+
'required': ['x'],
1187+
'type': 'object',
1188+
}
1189+
),
1190+
snapshot(None),
1191+
),
1192+
(
1193+
tool_with_url,
1194+
True,
1195+
snapshot(
1196+
{
1197+
'additionalProperties': False,
1198+
'properties': {'x': {'type': 'string', 'description': 'minLength=1, format=uri'}},
1199+
'required': ['x'],
1200+
'type': 'object',
1201+
}
1202+
),
1203+
snapshot(True),
1204+
),
11581205
(
11591206
tool_with_recursion,
11601207
None,
@@ -1432,16 +1479,8 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str:
14321479
{
14331480
'additionalProperties': False,
14341481
'properties': {
1435-
'x': {
1436-
'prefixItems': [{'type': 'integer'}],
1437-
'type': 'array',
1438-
'description': 'minItems=1, maxItems=1',
1439-
},
1440-
'y': {
1441-
'prefixItems': [{'type': 'string'}],
1442-
'type': 'array',
1443-
'description': 'minItems=1, maxItems=1',
1444-
},
1482+
'x': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'},
1483+
'y': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'string'}], 'type': 'array'},
14451484
},
14461485
'required': ['x', 'y'],
14471486
'type': 'object',
@@ -1537,9 +1576,10 @@ class MyModel(BaseModel):
15371576
},
15381577
'my_recursive': {'anyOf': [{'$ref': '#'}, {'type': 'null'}]},
15391578
'my_tuple': {
1579+
'maxItems': 1,
1580+
'minItems': 1,
15401581
'prefixItems': [{'type': 'integer'}],
15411582
'type': 'array',
1542-
'description': 'minItems=1, maxItems=1',
15431583
},
15441584
},
15451585
'required': ['my_recursive', 'my_patterns', 'my_tuple', 'my_list', 'my_discriminated_union'],
@@ -1555,11 +1595,7 @@ class MyModel(BaseModel):
15551595
'properties': {},
15561596
'required': [],
15571597
},
1558-
'my_tuple': {
1559-
'prefixItems': [{'type': 'integer'}],
1560-
'type': 'array',
1561-
'description': 'minItems=1, maxItems=1',
1562-
},
1598+
'my_tuple': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'},
15631599
'my_list': {'items': {'type': 'number'}, 'type': 'array'},
15641600
'my_discriminated_union': {'anyOf': [{'$ref': '#/$defs/Apple'}, {'$ref': '#/$defs/Banana'}]},
15651601
},

0 commit comments

Comments
 (0)