Skip to content

[BUG] AnthropicModel sends max_tokens=None whenever the user config omits it, violating the SDK Required[int] contract, so every default-config call fails #4370

Open

Description

@BlueX888

Required prerequisites

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.0 in pyproject.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:

  1. With the request actually sent, the serialized body contains "max_tokens": null and the API rejects it with 400 ... 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"}).
  2. On a client with the SDK default timeout, messages.create never even sends the request: BaseClient._calculate_nonstreaming_timeout computes maximum_time * max_tokens / 128_000 with max_tokens=None and raises TypeError: 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) types max_tokens as Required[int] in MessageCreateParamsBase.
  • AnthropicConfig's own docstring documents max_tokens as optional with default None, so "no explicit max_tokens" is an intended, supported configuration.
  • git log -S 'get("max_tokens", 4096)' shows commit 746cb758 (fix: anthropic max_tokens limitation #3874) replacing the 4096 fallback with None in both _run and _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

  1. BlueX888 commented on Sep 24, 2026

    @BlueX888
    Author

    Reproduced this. Root cause: camel/models/anthropic_model.py:1156,1301 .get('max_tokens', None) sends None for Required[int] param

    Minimal reproduction:

    PYTHONPATH=$PWD ../camel.tree/.venv/bin/python ../repro_anthropic_max_tokens.py
    

    Observed output:

    BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'max_tokens: Field required'}}
    

    Happy to open a PR with this fix, or to be assigned if you prefer to take it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions