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
6 changes: 3 additions & 3 deletions src/openai/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Optional, cast
from typing import TYPE_CHECKING, Any, Optional, cast, Union
from typing_extensions import Literal

import httpx
Expand Down Expand Up @@ -60,7 +60,7 @@ class APIError(OpenAIError):
If there was no response associated with this error then it will be `None`.
"""

code: Optional[str] = None
code: Optional[Union[str, int]] = None
param: Optional[str] = None
type: Optional[str]

Expand All @@ -71,7 +71,7 @@ def __init__(self, message: str, request: httpx.Request, *, body: object | None)
self.body = body

if is_dict(body):
self.code = cast(Any, construct_type(type_=Optional[str], value=body.get("code")))
self.code = cast(Any, construct_type(type_=Optional[Union[str, int]], value=body.get("code")))

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 Preserve integer error codes under pydantic v1

Because this package still allows pydantic>=1.9.0,<3, environments pinned to pydantic 1.x will run this through v1 Union validation, which tries the str branch first and coerces a JSON integer like 400 to '400' instead of preserving it as an int. In those supported installs, integer API error codes still surface as strings, so callers comparing exc.code == 400 and the new test scenario fail; handle primitive ints explicitly or avoid the str-first Union validation for this field.

Useful? React with 👍 / 👎.

self.param = cast(Any, construct_type(type_=Optional[str], value=body.get("param")))
self.type = cast(Any, construct_type(type_=str, value=body.get("type")))
else:
Expand Down
4 changes: 2 additions & 2 deletions src/openai/types/shared/error_object.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

from typing import Optional
from typing import Optional, Union

from ..._models import BaseModel

__all__ = ["ErrorObject"]


class ErrorObject(BaseModel):
code: Optional[str] = None
code: Optional[Union[str, int]] = None

message: str

Expand Down
33 changes: 33 additions & 0 deletions tests/test_exceptions_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import httpx
import pytest
import openai
from openai import OpenAI, BadRequestError
from respx import MockRouter

base_url = "https://api.openai.com/v1"

@pytest.mark.respx(base_url=base_url)
def test_error_code_integer_parsing(respx_mock: MockRouter) -> None:
respx_mock.post("/chat/completions").mock(
return_value=httpx.Response(
400,
json={
"error": {
"message": "The request is invalid.",
"type": "invalid_request_error",
"code": 400,
"param": "model"
}
}
)
)

client = OpenAI(base_url=base_url, api_key="test-api-key")

with pytest.raises(BadRequestError) as exc_info:
client.chat.completions.create(messages=[{"role": "user", "content": "hello"}], model="gpt-4")

assert exc_info.value.status_code == 400
assert exc_info.value.code == 400
assert "The request is invalid." in exc_info.value.message
assert exc_info.value.param == "model"