Skip to content

feat(anthropic): per-deployment opt-out for legacy thinking rewrite - #77

Merged
songkuan-zheng merged 1 commit into
ship/v1.83.10from
fix/anthropic-passthrough-thinking
Jun 10, 2026
Merged

feat(anthropic): per-deployment opt-out for legacy thinking rewrite#77
songkuan-zheng merged 1 commit into
ship/v1.83.10from
fix/anthropic-passthrough-thinking

Conversation

@songkuan-zheng

Copy link
Copy Markdown
Collaborator

Tier classification

  • A — Company-specific logic (litellm_extras/ only)
  • B — Internal infra / branding (CI, Dockerfile, e2e, internal navbar version)
  • C — Universal bug fix in litellm/ core
  • D — Universal mechanism + company opinion in litellm/ core

If Tier C or D, did you try upstream first?

  • Yes — upstream PR/issue: <link>
  • No — justification: To be filed upstream as a follow-up. Default value (False) preserves existing behavior, so this carries zero risk on the ship branch; upstreaming the mechanism in parallel lets us release internally without blocking on BerriAI review.

Relevant issues

None tracked externally. Internal motivation: callers hitting /v1/messages with claude-sonnet-4-6 need byte-exact passthrough of the deprecated legacy thinking={type:"enabled",budget_tokens:N} shape (e.g. for precise cache pricing, A/B parity with non-LiteLLM clients). Today LiteLLM rewrites it unconditionally into the adaptive form ({type:"adaptive"} + output_config.effort), discretizing budget_tokens into a 4-bucket effort band with no opt-out.

Pre-Submission checklist

  • Tests added in tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_legacy_thinking_passthrough.py (5 cases — default rewrite locked, flag-via-dict opt-out, flag-via-pydantic opt-out, no-op on non-adaptive models, default-False on pydantic instance)
  • Targeted unit tests pass — python3 -m pytest tests/test_litellm/llms/anthropic/experimental_pass_through/messages/ -q → 41 passed
  • Scope is isolated — single mechanism: gate _translate_legacy_thinking_for_adaptive_model on a new litellm_params flag
  • @greptileai review with Confidence ≥ 4/5 — to be requested after open

Type

🆕 New Feature

Changes

What

Add disable_adaptive_thinking_rewrite: Optional[bool] = False to GenericLiteLLMParams (and mirror it in LiteLLMParamsTypedDict). When the flag is True on a model_list deployment, the /v1/messages passthrough preserves the caller's legacy thinking shape verbatim instead of rewriting it.

Where

File Change
litellm/types/router.py Declare disable_adaptive_thinking_rewrite on the pydantic model + TypedDict, alongside anthropic_beta_overrides (same plumbing pattern)
litellm/llms/anthropic/experimental_pass_through/messages/transformation.py Add litellm_params arg to _translate_legacy_thinking_for_adaptive_model, duck-typed read of the flag from either dict or pydantic model, short-circuit before the existing _is_adaptive_thinking_model gate. Thread litellm_params through the single call site in transform_anthropic_messages_request.
tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_legacy_thinking_passthrough.py New file, 5 unit tests

Blast radius

Limited to the /v1/messages passthrough. The same translation lives in litellm/llms/anthropic/chat/transformation.py (the /v1/chat/completions path) and in the bedrock/databricks adaptive-thinking handlers — those are intentionally out of scope for this PR. If callers need the same opt-out on chat completions, the pattern is identical: gate the _translate_* calls on the same flag. Will be a follow-up.

Limitations

  • On Opus 4.7+ models, upstream Anthropic rejects the legacy thinking shape with a 400 at the provider. This flag does NOT paper over that — it only short-circuits LiteLLM's local rewrite. Suitable for Sonnet/Opus 4.6 (where legacy is deprecated but still functional upstream).

Usage

model_list:
  - model_name: claude-sonnet-4-6-raw
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY
      disable_adaptive_thinking_rewrite: true

Request body sent to upstream Anthropic preserves thinking={"type":"enabled","budget_tokens":2048} byte-for-byte instead of being rewritten to thinking={"type":"adaptive"} + output_config={"effort":"low"}.

Test plan

  • Unit: python3 -m pytest tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_legacy_thinking_passthrough.py -v — 5/5 pass
  • Regression: python3 -m pytest tests/test_litellm/llms/anthropic/experimental_pass_through/messages/ -q — 41 passed, 0 new failures
  • Manual smoke (post-merge): boot proxy with disable_adaptive_thinking_rewrite: true, curl /v1/messages with the legacy thinking shape, confirm the upstream request body matches via httpx debug logging

…e rewrite

Add `disable_adaptive_thinking_rewrite` to `GenericLiteLLMParams`. When
set on a model_list deployment, the /v1/messages passthrough preserves
the caller's legacy `thinking={"type":"enabled","budget_tokens":N}`
verbatim instead of rewriting it to the adaptive form
(`{"type":"adaptive"}` + `output_config.effort`).

Motivation: on Claude Sonnet/Opus 4.6, the legacy thinking shape is
deprecated upstream but still functional. LiteLLM rewrites it
proactively, which discretizes `budget_tokens` into a 4-bucket effort
band — callers that need byte-exact passthrough (precise cache pricing,
A/B parity with non-LiteLLM clients) have no way to opt out today.

The flag is duck-typed: the gate reads from either a dict-shaped
`litellm_params` or a `GenericLiteLLMParams` pydantic instance,
mirroring the `additional_drop_params` pattern. It only short-circuits
the rewrite path — it does NOT bypass upstream Anthropic's 400 rejection
of the legacy shape on Opus 4.7+ (those models reject it at the
provider, not in LiteLLM).

Usage:
  model_list:
    - model_name: claude-sonnet-4-6-raw
      litellm_params:
        model: anthropic/claude-sonnet-4-6
        api_key: os.environ/ANTHROPIC_API_KEY
        disable_adaptive_thinking_rewrite: true

Tests cover: default rewrite preserved, flag-via-dict opt-out, flag-via-
pydantic-model opt-out, no-op on non-adaptive models, and default-False
on a fresh pydantic instance.

Tier: D (universal mechanism + opinionated default of False preserves
existing behavior). To be filed upstream as a passthrough hook.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant