Skip to content

Commit 975a313

Browse files
committed
fix: simplify anthropic structured output flow
1 parent cc025de commit 975a313

5 files changed

Lines changed: 78 additions & 82 deletions

File tree

PR_DESCRIPTION_fix_anthropic.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# PR Description
2+
3+
## Summary
4+
- Simplify `AnthropicModel` structured output handling by removing the legacy beta structured-outputs path and keeping the stable `messages.create + output_config` flow.
5+
- Update the Anthropic structured output example to better match real third-party Anthropic-compatible endpoints by supporting configurable base URL/model, using a local fallback token counter, and documenting a successful combined `response_format + tools` run.
6+
7+
## What Changed
8+
- `camel/models/anthropic_model.py`
9+
- Removed the legacy `use_beta_for_structured_outputs` constructor argument and internal beta-header branch.
10+
- Removed the `ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS` constant.
11+
- Unified sync and async execution onto the standard `messages.create` path.
12+
- `test/models/test_anthropic_model.py`
13+
- Removed beta structured-output compatibility tests that no longer apply.
14+
- Kept the tests that validate the current `output_config` request shape and tool/schema handling.
15+
- `docs/mintlify/reference/camel.models.anthropic_model.mdx`
16+
- Removed the obsolete `use_beta_for_structured_outputs` parameter from the generated reference text.
17+
- `examples/models/anthropic_structured_output_example.py`
18+
- Simplified the example to a single combined scenario: Anthropic structured output plus tool use.
19+
- Added support for `ANTHROPIC_API_BASE_URL` and `ANTHROPIC_MODEL` so the example can run against Anthropic-compatible platforms.
20+
- Added a local fallback token counter (`OpenAITokenCounter(ModelType.GPT_4O_MINI)`) with an inline comment explaining why third-party compatible endpoints may need it.
21+
- Added a minimal explicit `max_tokens` config for compatibility with endpoints that require it.
22+
- Appended a real sample output block showing the raw JSON response, parsed object, parsed JSON, and tool calls.
23+
24+
## Testing
25+
- `python3 -m py_compile camel/models/anthropic_model.py test/models/test_anthropic_model.py`: passed
26+
- `python3 -m py_compile examples/models/anthropic_structured_output_example.py`: passed
27+
- `ANTHROPIC_API_KEY=... ANTHROPIC_API_BASE_URL='https://zenmux.ai/api/anthropic' ANTHROPIC_MODEL='claude-opus-4-6' uv run --extra model_platforms python examples/models/anthropic_structured_output_example.py`: passed
28+
- Verified `response_format + tools` together
29+
- Verified parsed Pydantic output was populated
30+
- Verified tool calls were executed and recorded
31+
32+
## Risks / Compatibility
33+
- This removes the explicit legacy beta structured-output fallback from `AnthropicModel`. Environments that still depend on the old beta header path will need to migrate to the standard `output_config` flow.
34+
- The example now documents a third-party compatible endpoint workaround via a local fallback token counter. That change is limited to the example and does not alter the shared token counting implementation.
35+
36+
## Out of Scope
37+
- No broader changes to token counting behavior across the Anthropic backend.
38+
- No changes to unrelated model backends or shared structured output APIs.

camel/models/anthropic_model.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
update_langfuse_trace,
3434
)
3535

36-
ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS = "structured-outputs-2025-11-13"
3736
ANTHROPIC_SUPPORTED_STRING_FORMATS = {
3837
"date",
3938
"date-time",
@@ -137,9 +136,6 @@ class AnthropicModel(BaseModelBackend):
137136
async_client (Optional[Any], optional): A custom asynchronous Anthropic
138137
client instance. If provided, this client will be used instead of
139138
creating a new one. (default: :obj:`None`)
140-
use_beta_for_structured_outputs (bool, optional): Whether to send the
141-
legacy beta header for structured outputs for backward-compatible
142-
endpoints. (default: :obj:`False`)
143139
**kwargs (Any): Additional arguments to pass to the client
144140
initialization.
145141
"""
@@ -161,7 +157,6 @@ def __init__(
161157
max_retries: int = 3,
162158
client: Optional[Any] = None,
163159
async_client: Optional[Any] = None,
164-
use_beta_for_structured_outputs: bool = False,
165160
**kwargs: Any,
166161
) -> None:
167162
if model_config_dict is None:
@@ -220,8 +215,6 @@ def __init__(
220215
"ttl": cache_control,
221216
}
222217

223-
self._use_beta_for_structured_outputs = use_beta_for_structured_outputs
224-
225218
@property
226219
def token_counter(self) -> BaseTokenCounter:
227220
r"""Initialize the token counter for the model backend.
@@ -890,12 +883,7 @@ def _run(
890883
if tool_choice is not None:
891884
request_params["tool_choice"] = tool_choice
892885

893-
# Add beta for structured outputs if configured
894-
if self._use_beta_for_structured_outputs:
895-
request_params["betas"] = [ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS]
896-
create_func = self._client.beta.messages.create
897-
else:
898-
create_func = self._client.messages.create
886+
create_func = self._client.messages.create
899887

900888
# Check if streaming
901889
is_streaming = self.model_config_dict.get("stream", False)
@@ -1034,12 +1022,7 @@ async def _arun(
10341022
if tool_choice is not None:
10351023
request_params["tool_choice"] = tool_choice
10361024

1037-
# Add beta for structured outputs if configured
1038-
if self._use_beta_for_structured_outputs:
1039-
request_params["betas"] = [ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS]
1040-
create_func = self._async_client.beta.messages.create
1041-
else:
1042-
create_func = self._async_client.messages.create
1025+
create_func = self._async_client.messages.create
10431026

10441027
# Check if streaming
10451028
is_streaming = self.model_config_dict.get("stream", False)

docs/mintlify/reference/camel.models.anthropic_model.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Anthropic API in a unified BaseModelBackend interface.
4242
- **max_retries** (int, optional): Maximum number of retries for API calls. (default: :obj:`3`)
4343
- **client** (Optional[Any], optional): A custom synchronous Anthropic client instance. If provided, this client will be used instead of creating a new one. (default: :obj:`None`)
4444
- **async_client** (Optional[Any], optional): A custom asynchronous Anthropic client instance. If provided, this client will be used instead of creating a new one. (default: :obj:`None`)
45-
- **use_beta_for_structured_outputs** (bool, optional): Whether to use the beta API for structured outputs. (default: :obj:`False`) **kwargs (Any): Additional arguments to pass to the client initialization.
45+
- **kwargs** (Any): Additional arguments to pass to the client initialization.
4646

4747
<a id="camel.models.anthropic_model.AnthropicModel.__init__"></a>
4848

@@ -60,7 +60,6 @@ def __init__(
6060
max_retries: int = 3,
6161
client: Optional[Any] = None,
6262
async_client: Optional[Any] = None,
63-
use_beta_for_structured_outputs: bool = False,
6463
**kwargs: Any
6564
):
6665
```

examples/models/anthropic_structured_output_example.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@
1515
"""
1616
Anthropic structured output + tool use example for CAMEL.
1717
18-
This single example focuses on the most important combined scenario:
19-
Anthropic tool calling and ``response_format`` in the same request.
20-
2118
Required environment variable:
2219
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
2320
2421
Optional environment variables:
2522
export ANTHROPIC_API_BASE_URL="<your-anthropic-compatible-base-url>"
26-
export ANTHROPIC_USE_BETA_STRUCTURED_OUTPUTS="true"
23+
export ANTHROPIC_MODEL="<your-anthropic-model>"
2724
2825
Run:
2926
python3 examples/models/anthropic_structured_output_example.py
@@ -35,8 +32,9 @@
3532
from pydantic import BaseModel, Field
3633

3734
from camel.agents import ChatAgent
38-
from camel.configs import AnthropicConfig
3935
from camel.models import AnthropicModel
36+
from camel.types import ModelType
37+
from camel.utils import OpenAITokenCounter
4038

4139

4240
class TripDecision(BaseModel):
@@ -124,22 +122,18 @@ def build_anthropic_model() -> AnthropicModel:
124122
)
125123

126124
base_url = os.environ.get("ANTHROPIC_API_BASE_URL")
127-
use_beta_for_structured_outputs = (
128-
os.environ.get("ANTHROPIC_USE_BETA_STRUCTURED_OUTPUTS", "")
129-
.strip()
130-
.lower()
131-
== "true"
125+
model_type = os.environ.get("ANTHROPIC_MODEL") or (
126+
"anthropic/claude-sonnet-4.5"
132127
)
133128

134129
return AnthropicModel(
135-
model_type="anthropic/claude-sonnet-4.5",
130+
model_type=model_type,
136131
api_key=api_key,
137132
url=base_url,
138-
use_beta_for_structured_outputs=use_beta_for_structured_outputs,
139-
model_config_dict=AnthropicConfig(
140-
temperature=0.0,
141-
max_tokens=800,
142-
).as_dict(),
133+
# Third-party Anthropic-compatible platforms may require a local
134+
# fallback token counter instead of Anthropic's count_tokens API.
135+
token_counter=OpenAITokenCounter(ModelType.GPT_4O_MINI),
136+
model_config_dict={"max_tokens": 800},
143137
)
144138

145139

@@ -187,3 +181,30 @@ def main() -> None:
187181

188182
if __name__ == "__main__":
189183
main()
184+
185+
"""
186+
===============================================================================
187+
=== Raw Content ===
188+
{"recommended_area":"Higashiyama","estimated_total_budget_rmb":1100,"must_visit_spot":"Kiyomizu-dera","transport_tip":"Take a bus or taxi early in the morning to avoid crowds and save time-consider a Kyoto Bus One-Day Pass for unlimited rides."}
189+
190+
=== Parsed Object ===
191+
recommended_area='Higashiyama' estimated_total_budget_rmb=1100
192+
must_visit_spot='Kiyomizu-dera' transport_tip='Take a bus or taxi early in
193+
the morning to avoid crowds and save time-consider a Kyoto Bus One-Day Pass
194+
for unlimited rides.'
195+
196+
=== Parsed JSON ===
197+
{
198+
"recommended_area": "Higashiyama",
199+
"estimated_total_budget_rmb": 1100,
200+
"must_visit_spot": "Kiyomizu-dera",
201+
"transport_tip": "Take a bus or taxi early in the morning to avoid crowds and save time-consider a Kyoto Bus One-Day Pass for unlimited rides."
202+
}
203+
204+
=== Tool Calls ===
205+
- lookup_kyoto_area args={'area': 'Higashiyama'} result={'must_visit_spot':
206+
'Kiyomizu-dera', 'transport_tip': 'Take a bus or taxi early in the morning.'}
207+
- estimate_kyoto_budget args={'days': 2, 'hotel_tier': 'budget'}
208+
result={'total_budget_rmb': 1100}
209+
===============================================================================
210+
"""

test/models/test_anthropic_model.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from camel.configs import AnthropicConfig
2121
from camel.models import AnthropicModel
2222
from camel.models.anthropic_model import (
23-
ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS,
2423
strip_trailing_whitespace_from_messages,
2524
)
2625
from camel.types import ModelType
@@ -610,17 +609,6 @@ def test_convert_stream_chunk_message_stop():
610609
assert result.choices[0].finish_reason == "stop"
611610

612611

613-
def test_use_beta_for_structured_outputs():
614-
"""Test that beta API is used when configured."""
615-
model = AnthropicModel(
616-
ModelType.CLAUDE_HAIKU_4_5,
617-
api_key="dummy_api_key",
618-
use_beta_for_structured_outputs=True,
619-
)
620-
621-
assert model._use_beta_for_structured_outputs is True
622-
623-
624612
def test_build_output_config_enforces_additional_properties_false():
625613
"""Test output_config generation for structured outputs."""
626614
model = AnthropicModel(
@@ -840,36 +828,3 @@ async def test_arun_passes_output_config_tool_choice_and_extra_fields():
840828
]["description"]
841829
== "Must be at least 3 characters long."
842830
)
843-
844-
845-
def test_run_uses_beta_endpoint_for_legacy_structured_outputs_flag():
846-
"""Test legacy beta header path still works when explicitly enabled."""
847-
mock_client = MagicMock()
848-
mock_async_client = MagicMock()
849-
850-
mock_response = MagicMock()
851-
mock_response.content = [{"type": "text", "text": '{"city":"Kyoto"}'}]
852-
mock_response.stop_reason = "end_turn"
853-
mock_response.id = "msg_structured_beta"
854-
mock_response.usage = MagicMock()
855-
mock_response.usage.input_tokens = 8
856-
mock_response.usage.output_tokens = 3
857-
mock_client.beta.messages.create.return_value = mock_response
858-
859-
model = AnthropicModel(
860-
ModelType.CLAUDE_HAIKU_4_5,
861-
model_config_dict=AnthropicConfig(max_tokens=64).as_dict(),
862-
api_key="dummy_api_key",
863-
client=mock_client,
864-
async_client=mock_async_client,
865-
use_beta_for_structured_outputs=True,
866-
)
867-
868-
model._run(
869-
messages=[{"role": "user", "content": "Plan a trip"}],
870-
response_format=TravelResponse,
871-
)
872-
873-
request_kwargs = mock_client.beta.messages.create.call_args.kwargs
874-
assert request_kwargs["betas"] == [ANTHROPIC_BETA_FOR_STRUCTURED_OUTPUTS]
875-
assert request_kwargs["output_config"]["format"]["type"] == "json_schema"

0 commit comments

Comments
 (0)