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
36 changes: 33 additions & 3 deletions src/openai/lib/_parsing/_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,33 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp
raise TypeError(f"Unable to automatically parse response format type {response_format}")


def _is_date_format(json_schema: object) -> bool:
if not isinstance(json_schema, dict) or not json_schema:
return False
return json_schema.get("format") in {"date", "date-time"} or any(
map(
_is_date_format,
(json_schema.get("properties") or {}).values(),
)
Comment on lines +259 to +263

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Traverse union branches when detecting date formats

For a response model such as class R(BaseModel): when: date | None, Pydantic places the date schema below properties.when.anyOf, but this helper only recurses through properties, $defs, and items, so it returns false and skips the new warning. Dates under Pydantic v1's definitions, dictionary additionalProperties, and tuple prefixItems are missed for the same reason; recurse through all dictionary/list child schemas or explicitly handle these containers.

Useful? React with 👍 / 👎.

) or any(
map(
_is_date_format,
(json_schema.get("$defs") or {}).values(),
)
) or _is_date_format(json_schema.get("items") or {})

SCHEMA_DATE_WARNING = ("response_format contains a `date`/`datetime` field. Models are known to "
"occasionally mis-transcribe ambiguous date strings (e.g. splicing digits "
"from the day/month into the year); consider validating parsed date values.")

def _warn_if_schema_has_date_field(json_schema: dict[str, Any]) -> None:
if not _is_date_format(json_schema):
return
log.warning(
SCHEMA_DATE_WARNING
)


def type_to_response_format_param(
response_format: type | completion_create_params.ResponseFormat | Omit,
) -> ResponseFormatParam | Omit:
Expand All @@ -263,11 +290,11 @@ def type_to_response_format_param(
return response_format

# type checkers don't narrow the negation of a `TypeGuard` as it isn't
# a safe default behaviour but we know that at this point the `response_format`
# a safe default behavior but we know that at this point the `response_format`
# can only be a `type`
response_format = cast(type, response_format)

json_schema_type: type[pydantic.BaseModel] | pydantic.TypeAdapter[Any] | None = None
json_schema_type: type[pydantic.BaseModel] | pydantic.TypeAdapter[Any] | None

if is_basemodel_type(response_format):
name = response_format.__name__
Expand All @@ -278,10 +305,13 @@ def type_to_response_format_param(
else:
raise TypeError(f"Unsupported response_format type - {response_format}")

json_schema = to_strict_json_schema(json_schema_type)
_warn_if_schema_has_date_field(json_schema)

return {
"type": "json_schema",
"json_schema": {
"schema": to_strict_json_schema(json_schema_type),
"schema": json_schema,
"name": name,
"strict": True,
},
Expand Down
73 changes: 71 additions & 2 deletions tests/test_debug_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import json
import logging
import importlib
from typing import Any, AsyncIterator
from datetime import datetime, date
from typing import Any, AsyncIterator, Annotated
from unittest.mock import Mock, AsyncMock, patch

import httpx2
import pytest
from pydantic import BaseModel, Field

from openai import OpenAI, AsyncOpenAI, APIStatusError, APITimeoutError, APIConnectionError
from openai import OpenAI, AsyncOpenAI, APIStatusError, APITimeoutError, \
APIConnectionError, APIResponse
from openai._models import FinalRequestOptions
from openai.lib._parsing._completions import SCHEMA_DATE_WARNING

FAKE_SECRET = "fake-private-value-for-logging-tests"
BASE_URL = "https://example.test/v1"
Expand Down Expand Up @@ -398,3 +402,68 @@ async def test_sdk_log_switch_does_not_enable_transport_payloads(
assert client.request(object, request_options("json")) == {"result": FAKE_SECRET}
assert logging.getLogger("httpx2").level == logging.NOTSET
assert not any(FAKE_SECRET in record.getMessage() or FAKE_SECRET in repr(record.args) for record in caplog.records)

class PlainDate(BaseModel):
date_field: date

class PlainDateTime(BaseModel):
date_field: datetime

class AnnotatedDate(BaseModel):
date_field: Annotated[date, Field(alias="date")]

class AnnotatedDateTime(BaseModel):
date_field: Annotated[datetime, Field(alias="date")]

class DateWithField(BaseModel):
date_field: date = Field(alias="date")

class DatetimeWithField(BaseModel):
date_field: datetime = Field(alias="date")

class NestedDate(BaseModel):
date_field: PlainDate

class NestedDateTime(BaseModel):
date_field: PlainDateTime

class ListDate(BaseModel):
date_field: list[date]

class ListDateTime(BaseModel):
date_field: list[datetime]

class ListPlainDate(BaseModel):
date_field: list[PlainDate]

class ListPlainDateTime(BaseModel):
date_field: list[PlainDateTime]


@pytest.mark.parametrize(
"pydantic_model",
[
PlainDate,
PlainDateTime,
AnnotatedDate,
AnnotatedDateTime,
DateWithField,
DatetimeWithField,
ListDate,
ListDateTime,
ListPlainDate,
ListPlainDateTime,
],
)
def test_date_warning(pydantic_model: BaseModel, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger="openai"), OpenAI(
api_key="fake-api-key",
base_url=BASE_URL,
http_client=httpx2.Client(
transport=httpx2.MockTransport(lambda _: httpx2.Response(
200,
))
),
) as client, patch.object(APIResponse, APIResponse.parse.__name__):
client.chat.completions.parse(model="fake-model", messages=[{"role": "user", "content": "Hello"}], response_format=pydantic_model)
assert SCHEMA_DATE_WARNING in caplog.text
Loading