Skip to content

fix(bedrock): keep mid-conversation system messages in place for Claude Invoke - #32578

Merged
mateo-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_bedrock_invoke_keep_mid_conversation_system
Jul 10, 2026
Merged

fix(bedrock): keep mid-conversation system messages in place for Claude Invoke#32578
mateo-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_bedrock_invoke_keep_mid_conversation_system

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

All runs are live end to end against real Bedrock (us.anthropic.claude-opus-4-8, us-west-2) with Claude Code as the client, costing real $. The proxy config maps claude-opus-4-8 to bedrock/us.anthropic.claude-opus-4-8 with aws_region_name: us-west-2, i.e. the plain bedrock/ prefix so /v1/messages traffic goes down the Invoke path

python litellm/proxy/proxy_cli.py --config repro_config.yaml --port 4000 --detailed_debug > proxy.log 2>&1

The client is a real Claude Code session that grows context from ~35k to ~250k input tokens across 13 API calls by reading three ~45k-token text files plus two small ones, all seconds apart (far under the 5m cache TTL):

ANTHROPIC_BASE_URL=http://127.0.0.1:4000 ANTHROPIC_AUTH_TOKEN=sk-repro-1234 \
claude -p "Read big1.txt fully, then big2.txt fully, then big3.txt fully, then small1.txt, then small2.txt. Use exactly one Read tool call at a time, waiting for each result before issuing the next. Do not summarize anything until all five files are read; after the fifth read, reply with exactly the word: done" \
  --allowedTools "Read" --model claude-opus-4-8

Per-call cache usage extracted from the message_start events in the debug log:

grep -o '"cache_read_input_tokens": *[0-9]*, "cache_creation": {"ephemeral_5m_input_tokens": *[0-9]*' proxy.log

Before, at 07aeaa1 (current litellm_internal_staging HEAD):

call cache_read cache_creation
1 0 37,312
2 33,436 35,328
3 68,764 283
4 69,047 25,975
5 95,022 12,062
6 33,436 105,078
7 138,514 25,986
8 164,500 12,053
9 33,436 174,727
10 208,163 25,961
11 234,124 12,056
12 246,180 2,719
13 248,899 2,736

Each bolded collapse coincides with Claude Code appending a new mid-conversation role: "system" message (a Read-truncation notice on call 6, a task-tool reminder on call 9), which the current code hoists into the top-level system field; the mutated system prefix invalidates the whole cached message history, so reads pin at the 33,436-token tools + system prefix and everything after re-writes at cache-write pricing

After, at b36ba99 (this branch), identical session:

call cache_read cache_creation
1 34,534 0
2 34,534 34,126
3 68,660 467
4 69,127 19,098
5 88,225 18,940
6 107,165 31,104
7 138,269 19,094
8 157,363 19,084
9 176,447 31,280
10 207,727 19,096
11 226,823 19,088
12 245,911 2,719
13 248,630 2,732

Reads grow monotonically with zero collapses and the outbound payloads carry the mid-conversation system entries in place, which Bedrock accepts with 200s (call 1 reads 34,534 because an identical warm-cache run had finished minutes earlier)

Version bisect with the same session against PyPI wheels: v1.89.1 (no hoist, passes role: "system" through in place) grows monotonically 33,435 -> 248,685 with no collapses; v1.91.0 (first release with the hoist) collapses from 202,378 back to 33,436 with a 212,403-token re-write on the turn a new system message arrives

Type

🐛 Bug Fix

Changes

#31364 fixed a real 400 (messages.0: use the top-level 'system' parameter for the initial system prompt) by hoisting every role: "system" entry from messages into the top-level system field on the Bedrock Invoke /v1/messages path. Bedrock's actual validation, verified live with direct invoke-model calls against us.anthropic.claude-opus-4-8, rejects a system entry only at messages.0 or when it neither immediately precedes an assistant message nor ends the array ("messages.N: role 'system' must precede an 'assistant' message or end the array"); mid-conversation system entries as Claude Code emits them via the mid-conversation-system-2026-04-07 beta (task reminders and tool-output truncation notices, inserted before an assistant turn or trailing) satisfy that rule and are accepted in place, which pre-hoist LiteLLM through v1.90.x relied on in production. Hoisting them is destructive for prompt caching: system precedes messages in the cache prefix, so every newly appended mid-conversation system message mutates system and invalidates the cache for the entire message history, re-writing 100k+ tokens at cache-write pricing on long agentic sessions

This PR hoists only the leading run of system entries (the case Invoke actually rejects) and forwards mid-conversation ones untouched. Billing-header stripping from top-level system previously only ran as a side effect of the hoist; it now applies regardless, so x-anthropic-billing-header blocks still never reach Bedrock

Two regression tests are added: one asserts a mid-conversation system message stays in place while the top-level system field stays byte-identical (this is the cache-collapse regression) and still strips billing-header blocks, the other asserts a mixed conversation hoists exactly the leading run and preserves the position of a later system entry. Both fail on the pre-fix code

Note that this is only a fix for Invoke because Converse (when empirically tested), rejects role: "system" in messages at any position. We have always hoisted in Converse since December 2024 (61b35c12bb)

…de Invoke

Hoisting every role system entry into the top-level system field mutates
the cache prefix whenever a client such as Claude Code appends a new
mid-conversation system message, invalidating the prompt cache for the
entire message history on Bedrock Invoke. Bedrock only rejects a system
entry at messages.0, so hoist just the leading run and forward the rest
in place
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR narrows the Bedrock Invoke system-message normalization from hoisting every role: "system" entry in messages to hoisting only the leading run — the only case Bedrock actually rejects. Mid-conversation system entries (e.g. Claude Code's mid-conversation-system-2026-04-07 reminders) are now forwarded in place, preventing the prompt-cache invalidation that caused full re-writes on long agentic sessions.

  • anthropic_claude3_transformation.py: replaces the "filter all system messages" list-comprehension with a next()-based leading-count probe and a conditional slice; billing-header stripping is promoted from a side-effect of the hoist to an unconditional step so it applies even when no leading system messages are present.
  • test_anthropic_claude3_transformation.py: two new pure-unit regression tests cover the mid-conversation-in-place case (with billing-header filtering) and the mixed leading-run-plus-mid-conversation case; both are designed to fail on the pre-fix code.

Confidence Score: 5/5

The change is minimal, tightly scoped to one private method, backed by two targeted regression tests, and supported by live end-to-end billing evidence in the PR description.

The logic is straightforward: a next() scan replaces a full list comprehension, billing-header filtering is now unconditional (a strict improvement), and both new tests are mock-only unit tests that directly encode the failure modes.

No files require special attention.

Important Files Changed

Filename Overview
litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py Fixes _normalize_system_role_messages_for_bedrock to hoist only the leading run of role: "system" messages instead of all of them; billing-header stripping now always applies regardless of whether anything was hoisted.
tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py Adds two new regression tests: one verifies mid-conversation system messages stay in place while billing-header blocks are still stripped, the other verifies only the leading run is hoisted in a mixed conversation. No network calls.

Reviews (1): Last reviewed commit: "fix(bedrock): keep mid-conversation syst..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing litellm_bedrock_invoke_keep_mid_conversation_system (b36ba99) with litellm_internal_staging (142d5aa)

Open in CodSpeed

@mateo-berri
mateo-berri requested a review from tin-berri July 10, 2026 21:29
@mateo-berri
mateo-berri merged commit cc36d54 into litellm_internal_staging Jul 10, 2026
129 of 131 checks passed
@mateo-berri
mateo-berri deleted the litellm_bedrock_invoke_keep_mid_conversation_system branch July 10, 2026 21:30
mateo-berri added a commit that referenced this pull request Jul 11, 2026
fix(bedrock): backport #32578 and #32831 to stable/1.91.x for v1.91.2
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jul 11, 2026
….2) (#1525)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.91.1` → `v1.91.2` |

---

### Release Notes

<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>

### [`v1.91.2`](https://github.com/BerriAI/litellm/releases/tag/v1.91.2)

[Compare Source](BerriAI/litellm@v1.91.2...v1.91.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.91.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- fix(bedrock): backport [#&#8203;32578](BerriAI/litellm#32578) and [#&#8203;32831](BerriAI/litellm#32831) to stable/1.91.x for v1.91.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;32872](BerriAI/litellm#32872)

**Full Changelog**: <BerriAI/litellm@v1.91.1...v1.91.2>

### [`v1.91.2`](https://github.com/BerriAI/litellm/releases/tag/v1.91.2)

[Compare Source](BerriAI/litellm@v1.91.1...v1.91.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.91.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- fix(bedrock): backport [#&#8203;32578](BerriAI/litellm#32578) and [#&#8203;32831](BerriAI/litellm#32831) to stable/1.91.x for v1.91.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;32872](BerriAI/litellm#32872)

**Full Changelog**: <BerriAI/litellm@v1.91.1...v1.91.2>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/1525
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Jul 11, 2026
….2) (#14)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.91.1` → `v1.91.2` |

---

### Release Notes

<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>

### [`v1.91.2`](https://github.com/BerriAI/litellm/releases/tag/v1.91.2)

[Compare Source](BerriAI/litellm@v1.91.2...v1.91.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.91.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- fix(bedrock): backport [#&#8203;32578](BerriAI/litellm#32578) and [#&#8203;32831](BerriAI/litellm#32831) to stable/1.91.x for v1.91.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;32872](BerriAI/litellm#32872)

**Full Changelog**: <BerriAI/litellm@v1.91.1...v1.91.2>

### [`v1.91.2`](https://github.com/BerriAI/litellm/releases/tag/v1.91.2)

[Compare Source](BerriAI/litellm@v1.91.1...v1.91.2)

##### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.91.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.91.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

##### What's Changed

- fix(bedrock): backport [#&#8203;32578](BerriAI/litellm#32578) and [#&#8203;32831](BerriAI/litellm#32831) to stable/1.91.x for v1.91.2 by [@&#8203;mateo-berri](https://github.com/mateo-berri) in [#&#8203;32872](BerriAI/litellm#32872)

**Full Changelog**: <BerriAI/litellm@v1.91.1...v1.91.2>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI1Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/14
yuneng-berri added a commit that referenced this pull request Jul 11, 2026
…1.92.0 stable cut (#32959)

* fix(utils): resolve bedrock regional inference profiles to regional pricing in get_model_info (LIT-4056) (#32389)

* fix(utils): resolve bedrock regional inference profiles to regional pricing in get_model_info (LIT-4056)

* test(register_model): use a triple provider prefix as the unresolvable-key fixture

get_model_info now resolves bedrock/bedrock/... like a routing prefix, so the
double-prefix fixture stopped exercising the register_model fallback path.
Lock the new double-prefix resolution in as a model-info regression test

(cherry picked from commit 734fd29)

* fix(guardrails): walk Responses-API text taxonomy in shared content helpers (#32542)

* fix(guardrails): walk Responses-API text taxonomy in shared content helpers

Every guardrail sharing litellm/proxy/guardrails/_content_utils.py silently
drops all text on the /v1/responses path. AIM turns it into a loud 422 (
{"error":"No messages in the request"}); every other guardrail (Lakera v2,
Cato, Lasso, Repello, IBM, Azure Content Safety, enterprise secret
detection) scans an empty payload and lets the request through unscanned.

Three defects, all in _content_utils.py:

1. _iter_text_parts_in_content recognised only part.type == "text", but the
   Responses API uses input_text (request) and output_text (assistant).
2. _coerce_input_to_messages gated on "every item has a role key"; any
   Responses input list containing a function_call or function_call_output
   item failed the check and was wrapped as one opaque blob.
3. build_inspection_messages forwarded any role through, including a bare
   tool role missing tool_call_id, which validators like AIM's /fw/v1/analyze
   reject with a schema error.

Fix walks the actual Responses item taxonomy (message, function_call,
function_call_output, bare content parts and strings), recognises
{text, input_text, output_text} everywhere, and coerces any role outside
{system, user, assistant} to user in the outbound inspection payload.

* style: ruff-format changed guardrail files

* test(guardrails): cover function_call_output string form; drop em-dash in new docstring

* fix(guardrails): map function_call_output straight to user role

Avoids ever materialising a schema-invalid bare tool message. The
downstream role-safety coercion in build_inspection_messages still
guards genuinely caller-supplied non-standard roles (developer,
function, custom values); add a regression test covering that path
so the coercion has real coverage after this simplification.

* test(guardrails): pin chat-completions tool-role coercion in build_inspection_messages

* docs(test): soften AIM-specific claims in LIT-4294 test docstrings

Ryan's review flagged that several test docstrings assert AIM's
/fw/v1/analyze validates + rejects specific schema violations. That
behavior is customer-reported in the LIT-4294 writeup, not directly
verified by us. Rephrase to attribute the AIM 422 to the customer's
writeup and describe the underlying constraint as the OpenAI chat
schema; any downstream API that validates against that schema rejects
the same shape.

* refactor(guardrails): move unsupported-role coercion into AIM only

The generic coercion in build_inspection_messages collapsed any role
outside {system, user, assistant} to user for every caller of the
helper. Combined with the pre-existing apply_redacted_messages_back
write-back behavior in Lakera/AIM/Cato, that turned a loud OpenAI 400
on chat-completions tool-message masking into a silent semantic
corruption of the outbound request (role tool with tool_call_id got
rewritten to bare role user, dropping the assistant + tool_calls
sibling).

AIM specifically requires the coercion because its /fw/v1/analyze
validates the payload against the OpenAI chat schema; other guardrails
either do not validate roles or do their own reconstruction. Move the
coercion to AimGuardrail._build_aim_inspection_messages so the shared
helper keeps caller roles intact and no new cross-guardrail role
corruption is introduced. The pre-existing apply_redacted_messages_back
structural flatten remains as separate follow-up work.

function_call_output items still synthesise role user in the shared
helper because they have no natural role field, which is a different
concern from coercing a caller-supplied role.

* refactor(guardrails): preserve role fidelity in shared _content_utils

Shared inspection helpers should extract text and preserve semantic
role signals; role coercion for third-party schema safety stays inside
the guardrail that needs it (AIM).

Three shared-helper changes:
- Bare content-part dicts (input_text/output_text) with an explicit role
  keep it; only role-less parts default to user.
- Responses message items already had their role preserved; the
  behavior is now covered by an explicit test.
- function_call_output items default to role tool (semantic equivalent
  of the chat-completions tool message shape) instead of role user, so
  Responses and chat completions produce symmetric inspection payloads.
  A caller-supplied role on the item is still preserved.

AIM's schema-safe coercion in _build_aim_inspection_messages already
handles the resulting role tool: it collapses to user before the POST
to /fw/v1/analyze so AIM's OpenAI-schema validator does not reject the
bare tool message (no tool_call_id can survive the flatten). Added a
regression test in test_aim.py covering that path.

(cherry picked from commit e84a19a)

* feat: add Meta Model API provider and muse-spark-1.1 (day-0) (#32701)

(cherry picked from commit d82645d)

* fix(bedrock): keep mid-conversation system messages in place for Claude Invoke (#32578)

Hoisting every role system entry into the top-level system field mutates
the cache prefix whenever a client such as Claude Code appends a new
mid-conversation system message, invalidating the prompt cache for the
entire message history on Bedrock Invoke. Bedrock only rejects a system
entry at messages.0, so hoist just the leading run and forward the rest
in place

(cherry picked from commit cc36d54)

* feat(otel): emit the gen_ai.client.operation.exception event on failed LLM calls (#32655)

* feat(otel): emit the gen_ai.client.operation.exception event on failed LLM calls

The GenAI semantic conventions record failures of a GenAI client operation as
a log-based event named gen_ai.client.operation.exception, carrying the
exception.type / exception.message / exception.stacktrace trio at severity
WARN and correlated to the failed span. OTel v2 never emitted it: a failed LLM
call produced only the deprecated error.* span attributes, a generic exception
span event without a stacktrace, and the stacktrace under the vendor key
litellm.provider.error.stack_trace.

Build the logs pipeline (LoggerProvider + console/OTLP log exporters mirroring
the metrics plumbing) and record the event behind the enable_events flag, which
until now was defined but consumed nowhere. An operator-configured LoggerProvider
global is reused so the events ride their existing logs pipeline; an explicit
NoOpLoggerProvider global is honored as an opt-out and builds no recorder at all.

The existing span-side error surface (error.type, error.message, the exception
span event, and the litellm.provider.error.* detail keys) is untouched for
backwards compatibility.

* fix(otel): always ride the semconv-required exception pair on the GenAI event

Filtering the event attributes on truthiness conflated "absent" with "empty",
so an empty exception.type or exception.message would have been dropped, leaving
an event with neither semconv-required field. Build the attributes so the pair is
unconditional and only the recommended stacktrace is omitted when the payload
carries none.

* docs(otel): document the events plumbing module in the package README

* test(otel): cover the log exporter selection and logs endpoint normalization

The new logs plumbing had no coverage for exporter-kind selection, the
console fallback for an unrecognized kind, the /v1/logs signal-path rewriting
that lets one OTEL_ENDPOINT serve every signal, or the simple-vs-batch
processor split.

(cherry picked from commit 99b4c5e)

* fix(bedrock): gate in-place system role messages on model support for Claude Invoke (#32831)

* fix(bedrock): gate in-place system role messages on model support for Claude Invoke

* feat(bedrock): default unmapped Claude 4.8+ to in-place system role handling via fallback rule

(cherry picked from commit 5e23a5a)

* fix(anthropic): translate adaptive thinking/effort to pre-4.6 model support (#32867)

* fix(anthropic): translate adaptive thinking/effort to pre-4.6 model support

AnthropicMessagesConfig now reshapes the 4.6+ adaptive-thinking interface
(thinking:{type:adaptive} + output_config:{effort:...}) to whatever the routed
model supports. Thinking-capable non-adaptive models (e.g. Haiku 4.5, Sonnet 4.5)
get the effort translated to a legacy thinking budget_tokens. Models with no
reasoning support have thinking/effort dropped under drop_params. And because
adaptive thinking carries no budget while the legacy form must satisfy Anthropic's
max_tokens > budget_tokens rule, the translated budget is capped below max_tokens,
dropping thinking when max_tokens can't fit the minimum budget. 4.6+ models pass
through untouched.

This matters because clients like Claude Code speak native Anthropic /v1/messages
and send the adaptive interface unconditionally, regardless of the routed model.
The native passthrough previously only capability-gated the OpenAI-style
reasoning_effort alias and forwarded native output_config/adaptive thinking raw, so
a pre-4.6 model rejected it with "This model does not support the effort parameter"
and the request failed. Claude Code already gets drop_params auto-set, so its
requests now succeed.

* test(anthropic): gate undersized-max_tokens thinking drop on drop_params; add edge tests

Addresses review feedback on the max_tokens-too-small branch. Previously a
thinking-capable model whose max_tokens could not fit the minimum thinking budget
had thinking silently dropped regardless of drop_params, while a residual
output_config field in the same call still raised when drop_params was off. Gate
both consistently on drop_params: raise a clear error (naming max_tokens for the
undersized case) when drop_params is off, drop otherwise. Claude Code gets
drop_params auto-set, so it still succeeds.

Adds tests for the undersized-max_tokens raise, the residual output_config raise,
and the no-adaptive-interface passthrough on a non-adaptive model.

* fix(anthropic): make adaptive-effort translation silent to avoid breaking provider strip contracts

The previous raise-when-not-drop_params behavior broke existing bedrock and vertex
messages tests: those providers already silently strip unsupported output_config
for pre-4.6 models (issue #22797) with no drop_params required, and the shared
parent transform raising pre-empted that. It also conflicted with the goal of
keeping requests working rather than failing them.

Make the reshape silent: translate effort to legacy thinking for thinking-capable
models, drop thinking for non-reasoning models, and remove only the consumed effort
key from output_config, leaving any residual (e.g. format) for provider subclasses
(bedrock/vertex) to handle. No raise, no drop_params gating. This also resolves the
review note about inconsistent drop_params handling by making every path uniform.

Updates the tests to assert the silent behavior and residual output_config
preservation.

* fix(anthropic): handle output_config-capable but non-adaptive models (Opus 4.5)

Greptile caught a real bug: the early-return guard treated supports_output_config
as equivalent to supporting adaptive thinking. Claude Opus 4.5 advertises
supports_output_config (it accepts output_config.effort) but is not adaptive, so it
rejects thinking:{type:adaptive} with "adaptive thinking is not supported on this
model". The guard early-returned for Opus 4.5 and forwarded the adaptive thinking
block raw, reproducing the exact failure the fix is meant to prevent.

thinking:{type:adaptive} and output_config.effort are independent capabilities.
Only early-return for adaptive-thinking models. For a model that supports
output_config.effort but is not adaptive, keep the native effort and drop only the
unsupported adaptive thinking block. Verified live against Opus 4.5: the Claude Code
payload now returns 200 instead of 400.

Adds regression tests for Opus 4.5 with and without adaptive thinking.

* fix(anthropic): translate adaptive thinking for effort-capable pre-4.6 models

Claude Opus 4.5 advertises supports_output_config but not adaptive thinking,
so the early-return guard forwarded thinking.type=adaptive raw and Anthropic
rejected it. The guard now only skips true adaptive models; effort-only
requests on effort-capable models still pass through untouched. The
_map_reasoning_effort call is wrapped to surface unrecognized effort values
as a clean 400, matching _translate_reasoning_effort_to_anthropic

* fix(anthropic): fall back to legacy thinking when effort level unsupported

Opus 4.5 accepts output_config.effort but only low/medium/high; Claude Code
defaults to xhigh on newer models, so preserving that level raw gets rejected
by Anthropic. Gate the native-effort passthrough on _validate_effort_for_model
and fall through to the budget translation for unsupported levels

* fix(anthropic): keep effort-only requests untouched for provider normalization

The xhigh fall-through consumed effort-only requests on effort-capable
models, breaking bedrock invoke's own normalization which clamps xhigh to
the model's ceiling after the base transform runs
(test_bedrock_messages_normalizes_output_config_effort_for_opus). Restrict
the fall-through to requests that carry adaptive thinking; effort-only
requests pass through so provider subclasses keep owning level clamping

---------

Co-authored-by: Abhimanyu Kapur <38531241+akapur99@users.noreply.github.com>
(cherry picked from commit 3a62e54)

* fix(bedrock): flag mapped Claude 4.8+ entries with supports_mid_conversation_system (#32882)

Exact cost-map hits resolve before fallback-generalization rules, so the
mapped Sonnet 5, Fable 5 and jp Opus 4.8 Bedrock entries bypassed the
bedrock-anthropic-claude-mid-conversation-system rule and hoisted
mid-conversation system messages, invalidating the prompt cache.

(cherry picked from commit c15891f)

* Merge pull request #32873 from BerriAI/litellm_fallback_rules_routing_split

refactor(fallback-generalizations): split rules into routing and provider-neutral capability kinds

(cherry picked from commit 45d3644)

* Merge pull request #32874 from BerriAI/litellm_thread_provider_capability_probes

fix(anthropic): thread real provider through capability probes instead of pinning anthropic

(cherry picked from commit ead7ad3)

* test: add /v1/messages to supported_endpoints schema enum (#32739)

(cherry picked from commit bf02a4a)

---------

Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: yucheng-berri <yucheng@berri.ai>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Abhimanyu Kapur <38531241+akapur99@users.noreply.github.com>
Co-authored-by: tin-berri <tin@berri.ai>
mateo-berri added a commit that referenced this pull request Jul 20, 2026
…/messages

Azure AI Foundry and Vertex AI serve Claude on the first-party Anthropic
Messages contract, which was verified live to be byte-identical to
api.anthropic.com: a leading role:"system" entry in messages is rejected on
every model ("messages.0: use the top-level 'system' parameter"), and a
mid-conversation role:"system" reminder is accepted in place on Claude 4.8+/5
but 400s on Claude 4.7 and older ("role 'system' is not supported on this
model"). This is the same contract Bedrock Invoke already handles model-aware
(PRs #32578/#32831/#32882); Vertex and Azure did no hoisting at all, so a Claude
Code session on an older Vertex/Azure Claude model hard-400s on its reminder
turns, and the only thing sparing 4.8+/5 was that nothing was hoisted

Extract Bedrock's model-gated normalization into the shared
AnthropicMessagesConfig base as _normalize_system_role_messages and call it from
the Vertex and Azure messages configs. Flagged models (4.8+/5) hoist only the
leading run of system entries and keep mid-conversation reminders in place so
the top-level system prefix stays byte-identical and the prompt cache is
preserved; unflagged models hoist every system entry so the request returns a
completion instead of a 400

Add supports_mid_conversation_system to the azure_ai and vertex_ai Claude 4.8+/5
cost-map entries. Exact cost-map hits win over the claude-mid-conversation-system
fallback rule, so without the explicit flag those models would be treated as
unsupported and hoist every reminder, collapsing the prompt cache (the exact
customer regression). A per-provider test guards this so future 4.8+/5 entries
cannot silently miss the flag

Closes the Vertex/Azure gap from the customer RCA
pholex pushed a commit to pholex/litellm that referenced this pull request Jul 21, 2026
…de Invoke (BerriAI#32578)

Backport of BerriAI#32578 to stable/1.91.x.
Cherry-picked from cc36d54 (litellm_internal_staging).
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.

2 participants