Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9fb64a6

Browse files
authoredNov 8, 2024
feat(client): add ._request_id property to object responses (#743)
1 parent 472b7d3 commit 9fb64a6

File tree

6 files changed

+131
-6
lines changed

6 files changed

+131
-6
lines changed
 

‎README.md

+24
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,30 @@ Error codes are as followed:
427427
| >=500 | `InternalServerError` |
428428
| N/A | `APIConnectionError` |
429429

430+
## Request IDs
431+
432+
> For more information on debugging requests, see [these docs](https://docs.anthropic.com/en/api/errors#request-id)
433+
434+
All object responses in the SDK provide a `_request_id` property which is added from the `request-id` response header so that you can quickly log failing requests and report them back to Anthropic.
435+
436+
```python
437+
message = client.messages.create(
438+
max_tokens=1024,
439+
messages=[
440+
{
441+
"role": "user",
442+
"content": "Hello, Claude",
443+
}
444+
],
445+
model="claude-3-opus-20240229",
446+
)
447+
print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG
448+
```
449+
450+
Note that unlike other properties that use an `_` prefix, the `_request_id` property
451+
*is* public. Unless documented otherwise, *all* other `_` prefix properties,
452+
methods and modules are *private*.
453+
430454
### Retries
431455

432456
Certain errors are automatically retried 2 times by default, with a short exponential backoff.

‎src/anthropic/_legacy_response.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from ._types import NoneType
2727
from ._utils import is_given, extract_type_arg, is_annotated_type
28-
from ._models import BaseModel, is_basemodel
28+
from ._models import BaseModel, is_basemodel, add_request_id
2929
from ._constants import RAW_RESPONSE_HEADER
3030
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
3131
from ._exceptions import APIResponseValidationError
@@ -140,8 +140,11 @@ class MyModel(BaseModel):
140140
if is_given(self._options.post_parser):
141141
parsed = self._options.post_parser(parsed)
142142

143+
if isinstance(parsed, BaseModel):
144+
add_request_id(parsed, self.request_id)
145+
143146
self._parsed_by_type[cache_key] = parsed
144-
return parsed
147+
return cast(R, parsed)
145148

146149
@property
147150
def headers(self) -> httpx.Headers:

‎src/anthropic/_models.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44
import inspect
5-
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
5+
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
66
from datetime import date, datetime
77
from typing_extensions import (
88
Unpack,
@@ -95,6 +95,22 @@ def model_fields_set(self) -> set[str]:
9595
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
9696
extra: Any = pydantic.Extra.allow # type: ignore
9797

98+
if TYPE_CHECKING:
99+
_request_id: Optional[str] = None
100+
"""The ID of the request, returned via the `request-id` header. Useful for debugging requests and reporting issues to Anthropic.
101+
This will **only** be set for the top-level response object, it will not be defined for nested objects. For example:
102+
103+
```py
104+
message = await client.messages.create(...)
105+
message._request_id # req_xxx
106+
message.usage._request_id # raises `AttributeError`
107+
```
108+
109+
Note: unlike other properties that use an `_` prefix, this property
110+
*is* public. Unless documented otherwise, all other `_` prefix properties,
111+
methods and modules are *private*.
112+
"""
113+
98114
def to_dict(
99115
self,
100116
*,
@@ -665,6 +681,21 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
665681
setattr(typ, "__pydantic_config__", config) # noqa: B010
666682

667683

684+
def add_request_id(obj: BaseModel, request_id: str | None) -> None:
685+
obj._request_id = request_id
686+
687+
# in Pydantic v1, using setattr like we do above causes the attribute
688+
# to be included when serializing the model which we don't want in this
689+
# case so we need to explicitly exclude it
690+
if not PYDANTIC_V2:
691+
try:
692+
exclude_fields = obj.__exclude_fields__ # type: ignore
693+
except AttributeError:
694+
cast(Any, obj).__exclude_fields__ = {"_request_id", "__exclude_fields__"}
695+
else:
696+
cast(Any, obj).__exclude_fields__ = {*(exclude_fields or {}), "_request_id", "__exclude_fields__"}
697+
698+
668699
# our use of subclasssing here causes weirdness for type checkers,
669700
# so we just pretend that we don't subclass
670701
if TYPE_CHECKING:

‎src/anthropic/_response.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from ._types import NoneType
2828
from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base
29-
from ._models import BaseModel, is_basemodel
29+
from ._models import BaseModel, is_basemodel, add_request_id
3030
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
3131
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
3232
from ._exceptions import AnthropicError, APIResponseValidationError
@@ -339,8 +339,11 @@ class MyModel(BaseModel):
339339
if is_given(self._options.post_parser):
340340
parsed = self._options.post_parser(parsed)
341341

342+
if isinstance(parsed, BaseModel):
343+
add_request_id(parsed, self.request_id)
344+
342345
self._parsed_by_type[cache_key] = parsed
343-
return parsed
346+
return cast(R, parsed)
344347

345348
def read(self) -> bytes:
346349
"""Read and return the binary response content."""
@@ -443,8 +446,11 @@ class MyModel(BaseModel):
443446
if is_given(self._options.post_parser):
444447
parsed = self._options.post_parser(parsed)
445448

449+
if isinstance(parsed, BaseModel):
450+
add_request_id(parsed, self.request_id)
451+
446452
self._parsed_by_type[cache_key] = parsed
447-
return parsed
453+
return cast(R, parsed)
448454

449455
async def read(self) -> bytes:
450456
"""Read and return the binary response content."""

‎tests/test_legacy_response.py

+20
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ def test_response_parse_custom_model(client: Anthropic) -> None:
9191
assert obj.bar == 2
9292

9393

94+
def test_response_basemodel_request_id(client: Anthropic) -> None:
95+
response = LegacyAPIResponse(
96+
raw=httpx.Response(
97+
200,
98+
headers={"request-id": "my-req-id"},
99+
content=json.dumps({"foo": "hello!", "bar": 2}),
100+
),
101+
client=client,
102+
stream=False,
103+
stream_cls=None,
104+
cast_to=str,
105+
options=FinalRequestOptions.construct(method="get", url="/foo"),
106+
)
107+
obj = response.parse(to=CustomModel)
108+
assert obj._request_id == "my-req-id"
109+
assert obj.foo == "hello!"
110+
assert obj.bar == 2
111+
assert obj.to_dict() == {"foo": "hello!", "bar": 2}
112+
113+
94114
def test_response_parse_annotated_type(client: Anthropic) -> None:
95115
response = LegacyAPIResponse(
96116
raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})),

‎tests/test_response.py

+41
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,47 @@ async def test_async_response_parse_custom_model(async_client: AsyncAnthropic) -
156156
assert obj.bar == 2
157157

158158

159+
def test_response_basemodel_request_id(client: Anthropic) -> None:
160+
response = APIResponse(
161+
raw=httpx.Response(
162+
200,
163+
headers={"request-id": "my-req-id"},
164+
content=json.dumps({"foo": "hello!", "bar": 2}),
165+
),
166+
client=client,
167+
stream=False,
168+
stream_cls=None,
169+
cast_to=str,
170+
options=FinalRequestOptions.construct(method="get", url="/foo"),
171+
)
172+
obj = response.parse(to=CustomModel)
173+
assert obj._request_id == "my-req-id"
174+
assert obj.foo == "hello!"
175+
assert obj.bar == 2
176+
assert obj.to_dict() == {"foo": "hello!", "bar": 2}
177+
178+
179+
@pytest.mark.asyncio
180+
async def test_async_response_basemodel_request_id(client: Anthropic) -> None:
181+
response = AsyncAPIResponse(
182+
raw=httpx.Response(
183+
200,
184+
headers={"request-id": "my-req-id"},
185+
content=json.dumps({"foo": "hello!", "bar": 2}),
186+
),
187+
client=client,
188+
stream=False,
189+
stream_cls=None,
190+
cast_to=str,
191+
options=FinalRequestOptions.construct(method="get", url="/foo"),
192+
)
193+
obj = await response.parse(to=CustomModel)
194+
assert obj._request_id == "my-req-id"
195+
assert obj.foo == "hello!"
196+
assert obj.bar == 2
197+
assert obj.to_dict() == {"foo": "hello!", "bar": 2}
198+
199+
159200
def test_response_parse_annotated_type(client: Anthropic) -> None:
160201
response = APIResponse(
161202
raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})),

0 commit comments

Comments
 (0)
Please sign in to comment.