Skip to content

Commit 688da81

Browse files
stainless-app[bot]meorphis
authored andcommitted
feat(api): add support for structured outputs beta
https://docs.claude.com/en/docs/build-with-claude/structured-outputs
1 parent 411220a commit 688da81

File tree

51 files changed

+1354
-334
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1354
-334
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# ignore all snapshots which are not referred in the source
2+
*-new.*
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01C63EzoQa1oa1eNn778a6ep\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":656,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":3,\"service_tier\":\"standard\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I'll get\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" the weather for San Francisco for you.\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_011V184uLZKFsxYfCJSjiQ6q\",\"name\":\"get_weather\",\"input\":{}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"loc\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ati\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"on\\\": \\\"San Fr\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"anc\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"isc\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"o, CA\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"units\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\": \\\"f\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":656,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":85} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
3+
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01Vm8Ddgc8qm4iuUSKbf6jku\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":781,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":6,\"service_tier\":\"standard\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The weather in San Francisco,\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" CA is currently **Sunny** with a temperature of **\"}}\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"68°F**.\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":781,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":25} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
4+
]

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 34
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic%2Fanthropic-5e665f72d2774cd751988ccc94f623f264d9358aa073289779de5815d36e89a3.yml
3-
openapi_spec_hash: c5f969a677c73796d192cf09dbb047f9
4-
config_hash: bbb07992b537724667f4589012714eb7
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic%2Fanthropic-2f35b2ff9174d526a6d35796d2703490bfa5692312af67cbdfa4500283dabe31.yml
3+
openapi_spec_hash: dc52b25c487e97d355ef645644aa13e7
4+
config_hash: 239752fc0713a82e121ea45f7e2ebbf6

api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ from anthropic.types.beta import (
263263
BetaInputJSONDelta,
264264
BetaInputTokensClearAtLeast,
265265
BetaInputTokensTrigger,
266+
BetaJSONOutputFormat,
266267
BetaMCPToolResultBlock,
267268
BetaMCPToolUseBlock,
268269
BetaMCPToolUseBlockParam,

examples/structured_outputs.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# /// script
2+
# requires-python = ">=3.9"
3+
# dependencies = [
4+
# "anthropic",
5+
# ]
6+
#
7+
# [tool.uv.sources]
8+
# anthropic = { path = "../", editable = true }
9+
# ///
10+
11+
12+
import pydantic
13+
14+
import anthropic
15+
16+
17+
class Order(pydantic.BaseModel):
18+
product_name: str
19+
price: float
20+
quantity: int
21+
22+
23+
client = anthropic.Anthropic()
24+
25+
prompt = """
26+
Extract the product name, price, and quantity from this customer message:
27+
"Hi, I’d like to order 2 packs of Green Tea for 5.50 dollars each."
28+
"""
29+
30+
parsed_message = client.beta.messages.parse(
31+
model="claude-sonnet-4-5-20250929-structured-outputs",
32+
messages=[{"role": "user", "content": prompt}],
33+
max_tokens=1024,
34+
output_format=Order,
35+
)
36+
37+
print(parsed_message.parsed_output) # Order(product_name='Green Tea', price=5.5, quantity=2)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# /// script
2+
# requires-python = ">=3.9"
3+
# dependencies = [
4+
# "anthropic",
5+
# ]
6+
#
7+
# [tool.uv.sources]
8+
# anthropic = { path = "../", editable = true }
9+
# ///
10+
11+
import pydantic
12+
13+
import anthropic
14+
15+
16+
class Order(pydantic.BaseModel):
17+
product_name: str
18+
price: float
19+
quantity: int
20+
21+
22+
client = anthropic.Anthropic()
23+
24+
prompt = """
25+
Extract the product name, price, and quantity from this customer message:
26+
"Hi, I’d like to order 2 packs of Green Tea for 5.50 dollars each."
27+
"""
28+
29+
with client.beta.messages.stream(
30+
model="claude-sonnet-4-5-20250929-structured-outputs",
31+
messages=[{"role": "user", "content": prompt}],
32+
betas=["structured-outputs-2025-09-17"],
33+
max_tokens=1024,
34+
output_format=Order,
35+
) as stream:
36+
for event in stream:
37+
if event.type == "text":
38+
print(event.parsed_snapshot())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ show_error_codes = true
160160
#
161161
# We also exclude our `tests` as mypy doesn't always infer
162162
# types correctly and Pyright will still catch any type errors.
163-
exclude = ['src/anthropic/_files.py', '_dev/.*.py', 'tests/.*', 'examples/mcp_server_weather.py', 'examples/tools_with_mcp.py', 'examples/memory/basic.py']
163+
exclude = ['src/anthropic/_files.py', '_dev/.*.py', 'tests/.*', 'examples/mcp_server_weather.py', 'examples/tools_with_mcp.py', 'examples/memory/basic.py', 'src/anthropic/lib/_parse/_transform.py']
164164

165165
strict_equality = true
166166
implicit_reexport = true

src/anthropic/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
)
4545
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
4646
from ._utils._logs import setup_logging as _setup_logging
47+
from .lib._parse._transform import transform_schema
4748

4849
__all__ = [
4950
"types",
@@ -91,6 +92,7 @@
9192
"AI_PROMPT",
9293
"beta_tool",
9394
"beta_async_tool",
95+
"transform_schema",
9496
]
9597

9698
if not _t.TYPE_CHECKING:

src/anthropic/_compat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
131131
return model.model_dump_json(indent=indent)
132132

133133

134+
def model_parse_json(model: type[_ModelT], data: str | bytes) -> _ModelT:
135+
if PYDANTIC_V1:
136+
return model.parse_raw(data) # pyright: ignore[reportDeprecated]
137+
return model.model_validate_json(data)
138+
139+
134140
def model_dump(
135141
model: pydantic.BaseModel,
136142
*,

src/anthropic/_models.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ class GenericModel(BaseGenericModel, BaseModel):
774774

775775

776776
if not PYDANTIC_V1:
777-
from pydantic import TypeAdapter as _TypeAdapter
777+
from pydantic import TypeAdapter as _TypeAdapter, computed_field as computed_field
778778

779779
_CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
780780

@@ -811,6 +811,18 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
811811
def TypeAdapter(*_args: Any, **_kwargs: Any) -> Any:
812812
raise RuntimeError("attempted to use TypeAdapter in pydantic v1")
813813

814+
def computed_field(func: Any | None = None, /, **__: Any) -> Any:
815+
def _exc_func(*_: Any, **__: Any) -> Any:
816+
raise RuntimeError("attempted to use computed_field in pydantic v1")
817+
818+
def _dec(*_: Any, **__: Any) -> Any:
819+
return _exc_func
820+
821+
if func is not None:
822+
return _dec(func)
823+
else:
824+
return _dec
825+
814826

815827
class FinalRequestOptionsInput(TypedDict, total=False):
816828
method: Required[str]

0 commit comments

Comments
 (0)