From 18191dac8e1437a0f708525d474b7ecfe459d966 Mon Sep 17 00:00:00 2001 From: Robert Craigie Date: Wed, 7 Aug 2024 10:13:08 +0100 Subject: [PATCH 1/5] fix(client): raise helpful error message for response_format misuse --- src/openai/resources/chat/completions.py | 11 ++++++ tests/api_resources/chat/test_completions.py | 35 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/openai/resources/chat/completions.py b/src/openai/resources/chat/completions.py index 3dcd3774d7..fb4c71ba99 100644 --- a/src/openai/resources/chat/completions.py +++ b/src/openai/resources/chat/completions.py @@ -2,10 +2,12 @@ from __future__ import annotations +import inspect from typing import Dict, List, Union, Iterable, Optional, overload from typing_extensions import Literal import httpx +import pydantic from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven @@ -647,6 +649,7 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ChatCompletion | Stream[ChatCompletionChunk]: + validate_response_format(response_format) return self._post( "/chat/completions", body=maybe_transform( @@ -1302,6 +1305,7 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: + validate_response_format(response_format) return await self._post( "/chat/completions", body=await async_maybe_transform( @@ -1375,3 +1379,10 @@ def __init__(self, completions: AsyncCompletions) -> None: self.create = async_to_streamed_response_wrapper( completions.create, ) + + +def validate_response_format(response_format: object) -> None: + if inspect.isclass(response_format) and issubclass(response_format, pydantic.BaseModel): + raise TypeError( + "You tried to pass a `BaseModel` class to `chat.completions.create()`; You must use `beta.chat.completions.parse()` instead" + ) diff --git a/tests/api_resources/chat/test_completions.py b/tests/api_resources/chat/test_completions.py index d744dfe6ea..9fa3cc8284 100644 --- a/tests/api_resources/chat/test_completions.py +++ b/tests/api_resources/chat/test_completions.py @@ -6,6 +6,7 @@ from typing import Any, cast import pytest +import pydantic from openai import OpenAI, AsyncOpenAI from tests.utils import assert_matches_type @@ -257,6 +258,23 @@ def test_streaming_response_create_overload_2(self, client: OpenAI) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_create_disallows_pydantic(self, client: OpenAI) -> None: + class MyModel(pydantic.BaseModel): + a: str + + with pytest.raises(TypeError, match=r"You tried to pass a `BaseModel` class"): + client.chat.completions.create( + messages=[ + { + "content": "string", + "role": "system", + } + ], + model="gpt-4o", + response_format=cast(Any, MyModel), + ) + class TestAsyncCompletions: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -498,3 +516,20 @@ async def test_streaming_response_create_overload_2(self, async_client: AsyncOpe await stream.close() assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_create_disallows_pydantic(self, async_client: AsyncOpenAI) -> None: + class MyModel(pydantic.BaseModel): + a: str + + with pytest.raises(TypeError, match=r"You tried to pass a `BaseModel` class"): + await async_client.chat.completions.create( + messages=[ + { + "content": "string", + "role": "system", + } + ], + model="gpt-4o", + response_format=cast(Any, MyModel), + ) From d34a081c30f869646145919b2256ded115241eb5 Mon Sep 17 00:00:00 2001 From: Robert Craigie Date: Wed, 7 Aug 2024 10:13:24 +0100 Subject: [PATCH 2/5] chore(internal): format some docstrings --- src/openai/resources/beta/chat/completions.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/openai/resources/beta/chat/completions.py b/src/openai/resources/beta/chat/completions.py index 88ea2c0572..aee88c3c0f 100644 --- a/src/openai/resources/beta/chat/completions.py +++ b/src/openai/resources/beta/chat/completions.py @@ -78,14 +78,17 @@ def parse( from pydantic import BaseModel from openai import OpenAI + class Step(BaseModel): explanation: str output: str + class MathResponse(BaseModel): steps: List[Step] final_answer: str + client = OpenAI() completion = client.beta.chat.completions.parse( model="gpt-4o-2024-08-06", @@ -184,12 +187,12 @@ def stream( ```py with client.beta.chat.completions.stream( - model='gpt-4o-2024-08-06', + model="gpt-4o-2024-08-06", messages=[...], ) as stream: for event in stream: - if event.type == 'content.delta': - print(event.content, flush=True, end='') + if event.type == "content.delta": + print(event.content, flush=True, end="") ``` When the context manager is entered, a `ChatCompletionStream` instance is returned which, like `.create(stream=True)` is an iterator. The full list of events that are yielded by the iterator are outlined in [these docs](https://github.com/openai/openai-python/blob/main/helpers.md#chat-completions-events). @@ -287,14 +290,17 @@ async def parse( from pydantic import BaseModel from openai import AsyncOpenAI + class Step(BaseModel): explanation: str output: str + class MathResponse(BaseModel): steps: List[Step] final_answer: str + client = AsyncOpenAI() completion = await client.beta.chat.completions.parse( model="gpt-4o-2024-08-06", @@ -393,12 +399,12 @@ def stream( ```py async with client.beta.chat.completions.stream( - model='gpt-4o-2024-08-06', + model="gpt-4o-2024-08-06", messages=[...], ) as stream: async for event in stream: - if event.type == 'content.delta': - print(event.content, flush=True, end='') + if event.type == "content.delta": + print(event.content, flush=True, end="") ``` When the context manager is entered, an `AsyncChatCompletionStream` instance is returned which, like `.create(stream=True)` is an async iterator. The full list of events that are yielded by the iterator are outlined in [these docs](https://github.com/openai/openai-python/blob/main/helpers.md#chat-completions-events). From 43e10c0f251a42f1e6497f360c6c23d3058b3da3 Mon Sep 17 00:00:00 2001 From: Alex Protsyk Date: Thu, 8 Aug 2024 15:33:53 +0200 Subject: [PATCH 3/5] fix(json schema): support recursive BaseModels in Pydantic v1 (#1623) --- src/openai/lib/_pydantic.py | 5 +++++ tests/lib/test_pydantic.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/openai/lib/_pydantic.py b/src/openai/lib/_pydantic.py index 967ad5de57..a90effdf1a 100644 --- a/src/openai/lib/_pydantic.py +++ b/src/openai/lib/_pydantic.py @@ -62,6 +62,11 @@ def _ensure_strict_json_schema( for def_name, def_schema in defs.items(): _ensure_strict_json_schema(def_schema, path=(*path, "$defs", def_name)) + definitions = json_schema.get("definitions") + if is_dict(definitions): + for definition_name, definition_schema in definitions.items(): + _ensure_strict_json_schema(definition_schema, path=(*path, "definitions", definition_name)) + return json_schema diff --git a/tests/lib/test_pydantic.py b/tests/lib/test_pydantic.py index dc09596da2..a8fe8f4570 100644 --- a/tests/lib/test_pydantic.py +++ b/tests/lib/test_pydantic.py @@ -130,6 +130,7 @@ def test_most_types() -> None: "type": "object", "properties": {"column_name": {"title": "Column Name", "type": "string"}}, "required": ["column_name"], + "additionalProperties": False, }, "Condition": { "title": "Condition", @@ -147,6 +148,7 @@ def test_most_types() -> None: }, }, "required": ["column", "operator", "value"], + "additionalProperties": False, }, "OrderBy": { "title": "OrderBy", From 598e7a23768e7addbe1319ada2e87caee3cf0d14 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:21:24 +0000 Subject: [PATCH 4/5] chore(internal): updates (#1624) --- .stats.yml | 2 +- pyproject.toml | 3 +- src/openai/resources/chat/completions.py | 30 +++++++++++++++++++ .../types/chat/completion_create_params.py | 5 ++++ src/openai/types/chat_model.py | 2 +- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index ac652c9271..cad2c64cd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 68 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-4097c2f86beb3f3bb021775cd1dfa240e960caf842aeefc2e08da4dc0851ea79.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-97797a9363b9960b5f2fbdc84426a2b91e75533ecd409fe99e37c231180a4339.yml diff --git a/pyproject.toml b/pyproject.toml index af661cbad7..037b63242a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -202,7 +202,6 @@ unfixable = [ "T201", "T203", ] -ignore-init-module-imports = true [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" @@ -214,7 +213,7 @@ combine-as-imports = true extra-standard-library = ["typing_extensions"] known-first-party = ["openai", "tests"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "bin/**.py" = ["T201", "T203"] "scripts/**.py" = ["T201", "T203"] "tests/**.py" = ["T201", "T203"] diff --git a/src/openai/resources/chat/completions.py b/src/openai/resources/chat/completions.py index fb4c71ba99..dc577d6251 100644 --- a/src/openai/resources/chat/completions.py +++ b/src/openai/resources/chat/completions.py @@ -149,6 +149,11 @@ def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. @@ -347,6 +352,11 @@ def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. @@ -538,6 +548,11 @@ def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. @@ -805,6 +820,11 @@ async def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. @@ -1003,6 +1023,11 @@ async def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. @@ -1194,6 +1219,11 @@ async def create( [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. diff --git a/src/openai/types/chat/completion_create_params.py b/src/openai/types/chat/completion_create_params.py index bf648a3858..61126b37ac 100644 --- a/src/openai/types/chat/completion_create_params.py +++ b/src/openai/types/chat/completion_create_params.py @@ -126,6 +126,11 @@ class CompletionCreateParamsBase(TypedDict, total=False): [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + Outputs which guarantees the model will match your supplied JSON schema. Learn + more in the + [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. diff --git a/src/openai/types/chat_model.py b/src/openai/types/chat_model.py index 686f26b783..09bc081f7a 100644 --- a/src/openai/types/chat_model.py +++ b/src/openai/types/chat_model.py @@ -6,8 +6,8 @@ ChatModel: TypeAlias = Literal[ "gpt-4o", - "gpt-4o-2024-08-06", "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", "gpt-4o-mini", "gpt-4o-mini-2024-07-18", "gpt-4-turbo", From b63193a31a1c349cfc91377f29cc4196da3822ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:21:56 +0000 Subject: [PATCH 5/5] release: 1.40.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- src/openai/_version.py | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4b272115e4..2b233c7b0d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.40.1" + ".": "1.40.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b51e8a03..a6c65f0eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 1.40.2 (2024-08-08) + +Full Changelog: [v1.40.1...v1.40.2](https://github.com/openai/openai-python/compare/v1.40.1...v1.40.2) + +### Bug Fixes + +* **client:** raise helpful error message for response_format misuse ([18191da](https://github.com/openai/openai-python/commit/18191dac8e1437a0f708525d474b7ecfe459d966)) +* **json schema:** support recursive BaseModels in Pydantic v1 ([#1623](https://github.com/openai/openai-python/issues/1623)) ([43e10c0](https://github.com/openai/openai-python/commit/43e10c0f251a42f1e6497f360c6c23d3058b3da3)) + + +### Chores + +* **internal:** format some docstrings ([d34a081](https://github.com/openai/openai-python/commit/d34a081c30f869646145919b2256ded115241eb5)) +* **internal:** updates ([#1624](https://github.com/openai/openai-python/issues/1624)) ([598e7a2](https://github.com/openai/openai-python/commit/598e7a23768e7addbe1319ada2e87caee3cf0d14)) + ## 1.40.1 (2024-08-07) Full Changelog: [v1.40.0...v1.40.1](https://github.com/openai/openai-python/compare/v1.40.0...v1.40.1) diff --git a/pyproject.toml b/pyproject.toml index 037b63242a..9600130ca3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai" -version = "1.40.1" +version = "1.40.2" description = "The official Python library for the openai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/openai/_version.py b/src/openai/_version.py index f88b8dead1..7e6783b6c6 100644 --- a/src/openai/_version.py +++ b/src/openai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "openai" -__version__ = "1.40.1" # x-release-please-version +__version__ = "1.40.2" # x-release-please-version