Repository navigation
Description
Required prerequisites
- I have read the documentation https://camel-ai.github.io/camel/camel.html.
- I have searched the Issue Tracker and Discussions that this hasn't already been reported. (+1 or comment there if it has.)
- Consider asking first in a Discussion.
What version of camel are you using?
0.2.91a7 (running from source, master @ 8c791b7b)
System information
- camel installed from source (editable, master @
8c791b7b) - Python 3.13.15, macOS (darwin, arm64)
- anthropic SDK 0.89.0 (matches the pin
anthropic>=0.89.0,<0.90.0inpyproject.toml)
Problem description
AnthropicModel builds its request params with
"max_tokens": self.model_config_dict.get("max_tokens", None),(camel/models/anthropic_model.py:1156 in _run, camel/models/anthropic_model.py:1301 in _arun).
With the default AnthropicConfig — where max_tokens is None and therefore absent from as_dict() — every call sends max_tokens=None to messages.create, in both streaming and non-streaming mode. The anthropic SDK's MessageCreateParamsBase declares max_tokens: Required[int], so this is not a valid request. Two failure modes, both reproduced offline:
- With the request actually sent, the serialized body contains
"max_tokens": nulland the API rejects it with400 ... max_tokens: Field required. I verified this against an offline mock that enforces the provider contract (mock captured request body:{"max_tokens": null, "messages": [{"role": "user", "content": "hi"}], "model": "claude-sonnet-4-20250514"}). - On a client with the SDK default timeout,
messages.createnever even sends the request:BaseClient._calculate_nonstreaming_timeoutcomputesmaximum_time * max_tokens / 128_000withmax_tokens=Noneand raisesTypeError: unsupported operand type(s) for *: 'int' and 'NoneType'first.
In other words, the default configuration of AnthropicModel is unusable; every user who does not explicitly set max_tokens gets a hard failure. The AnthropicConfig docstring documents max_tokens defaulting to None as a supported configuration (camel/configs/anthropic_config.py:28-32).
This is a regression introduced by #3874 (commit 746cb758, 2026-02-25), which changed the previous .get("max_tokens", 4096) fallback to .get("max_tokens", None) in both _run and _arun. That PR was aimed at the opposite problem (an oversized default max_tokens for OpenAI-compatible endpoints, cf. #3621), but the new None default breaks the direct Anthropic path because max_tokens is a required parameter there.
camel's own tests never catch this because they mock messages.create, so no request body is ever validated against the provider contract.
Reproducible example code
Fully offline (no API key, no network) — an httpx.MockTransport stands in for the API and enforces the Required[int] contract on max_tokens:
import json
import httpx
from anthropic import Anthropic, AsyncAnthropic
from camel.configs import AnthropicConfig
from camel.messages import BaseMessage
from camel.models import ModelFactory
from camel.types import ModelPlatformType, ModelType, OpenAIBackendRole
captured = []
def handler(request: httpx.Request) -> httpx.Response:
body = json.loads(request.content)
captured.append(body)
if not isinstance(body.get("max_tokens"), int): # Required[int] contract
return httpx.Response(400, json={"type": "error", "error": {
"type": "invalid_request_error", "message": "max_tokens: Field required"}})
return httpx.Response(200, json={
"id": "msg_mock", "type": "message", "role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [{"type": "text", "text": "ok"}], "stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1}})
def make_clients():
hc = httpx.Client(transport=httpx.MockTransport(handler),
base_url="https://api.anthropic.com")
ahc = httpx.AsyncClient(transport=httpx.MockTransport(handler),
base_url="https://api.anthropic.com")
return (Anthropic(api_key="dummy", timeout=180.0, http_client=hc),
AsyncAnthropic(api_key="dummy", timeout=180.0, http_client=ahc))
def run_case(label, config):
captured.clear()
client, async_client = make_clients()
model = ModelFactory.create(
ModelPlatformType.ANTHROPIC, ModelType.CLAUDE_SONNET_4,
api_key="dummy", model_config_dict=config.as_dict(),
client=client, async_client=async_client)
msg = BaseMessage.make_user_message(role_name="user", content="hi")
try:
model.run([msg.to_openai_message(OpenAIBackendRole.USER)])
print(f"[{label}] run() succeeded")
except Exception as e:
print(f"[{label}] run() raised {type(e).__name__}: {str(e).splitlines()[0]}")
print(f"[{label}] request body: {json.dumps(captured[-1])}")
run_case("default config", AnthropicConfig()) # no max_tokens set
run_case("max_tokens=777", AnthropicConfig(max_tokens=777)) # control
run_case("default config + stream", AnthropicConfig(stream=True))Traceback
Actual output (camel 0.2.91a7 @ 8c791b7b, anthropic 0.89.0, Python 3.13.15, macOS):
[default config] run() raised BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'max_tokens: Field required'}}
[default config] request body: {"max_tokens": null, "messages": [{"role": "user", "content": "hi"}], "model": "claude-sonnet-4-20250514"}
[max_tokens=777] run() succeeded
[max_tokens=777] request body: {"max_tokens": 777, "messages": [{"role": "user", "content": "hi"}], "model": "claude-sonnet-4-20250514"}
[default config + stream] run() raised BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'max_tokens: Field required'}}
[default config + stream] request body: {"max_tokens": null, "messages": [{"role": "user", "content": "hi"}], "model": "claude-sonnet-4-20250514", "stream": true}
The same None also trips the SDK's default-timeout path before any request is sent (observed with a default-timeout client):
>>> Anthropic(api_key="dummy").messages.create(
... model="claude-sonnet-4-20250514", max_tokens=None,
... messages=[{"role": "user", "content": "hi"}])
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
(raised from anthropic._base_client.BaseClient._calculate_nonstreaming_timeout, which computes maximum_time * max_tokens / 128_000.)
Expected behavior
With the default AnthropicConfig (no max_tokens set), AnthropicModel should send a valid max_tokens value so that a plain ModelFactory.create(ModelPlatformType.ANTHROPIC, ...) call works out of the box — as it did before #3874, when the fallback was 4096.
Concrete basis:
- anthropic SDK (the version camel pins,
>=0.89.0,<0.90.0) typesmax_tokensasRequired[int]inMessageCreateParamsBase. AnthropicConfig's own docstring documentsmax_tokensas optional with defaultNone, so "no explicit max_tokens" is an intended, supported configuration.git log -S 'get("max_tokens", 4096)'shows commit746cb758(fix: anthropic max_tokens limitation #3874) replacing the4096fallback withNonein both_runand_arun.
Additional context
Root cause: camel/models/anthropic_model.py:1156 and camel/models/anthropic_model.py:1301 use self.model_config_dict.get("max_tokens", None); when the config omits max_tokens, None is passed for a parameter the provider requires. Restoring a non-None fallback (e.g. 4096, the pre-#3874 behavior) in both places fixes it — verified locally by running the repro above (default config then succeeds and sends "max_tokens": 4096), plus pytest test/models/test_anthropic_model.py (all 44 pre-existing tests pass; 56 with regression tests added on top).
Related: #3874 (introduced the regression), #3621 (the oversized-max_tokens problem #3874 was addressing, on the OpenAI-compatible side).
Happy to be assigned — I can open a PR with the fix once this issue is accepted.
Activity
BlueX888 commentedon Sep 24, 2026
Reproduced this. Root cause: camel/models/anthropic_model.py:1156,1301 .get('max_tokens', None) sends None for Required[int] param
Minimal reproduction:
Observed output:
Happy to open a PR with this fix, or to be assigned if you prefer to take it.