Skip to content

fix(streaming): reset Anthropic message_start cursor (output_tokens=1) when no message_delta arrives - #30420

Merged
Sameerlite merged 3 commits into
BerriAI:litellm_oss_staging_250626from
GhishaDev:fix/streaming-cursor-1-reset
Jun 25, 2026
Merged

fix(streaming): reset Anthropic message_start cursor (output_tokens=1) when no message_delta arrives#30420
Sameerlite merged 3 commits into
BerriAI:litellm_oss_staging_250626from
GhishaDev:fix/streaming-cursor-1-reset

Conversation

@songkuan-zheng

@songkuan-zheng songkuan-zheng commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

No existing issue. Latent billing bug on Anthropic streaming when the stream is cancelled before the final message_delta event — affects every LiteLLM user calling Anthropic with streaming + cancel/timeout.

Type

🐛 Bug Fix

Pre-Submission checklist

  • I have added a test for my change
  • I have updated relevant documentation (N/A — internal billing path)
  • My change passes make test locally
  • My change adheres to the existing code style

Description

The Anthropic streaming protocol emits message_start.usage.output_tokens=1 as a placeholder cursor; the real cumulative output count only arrives in the final message_delta event.

When a stream is cancelled before message_delta lands (common for thinking models on long-tail prompts), ChunkProcessor._calculate_usage_per_chunk's last-wins accumulator left completion_tokens stuck at 1. Because 1 is truthy, the completion_tokens or token_counter(text=...) fallback in calculate_usage() never fired, and requests were billed for 1 output token even when several thousand tokens of text had actually streamed.

Fix

Track whether any chunk's completion_tokens exceeded 1 (saw_non_cursor_completion). If the only update we saw was the cursor, reset completion_tokens to 0 so the text-based fallback estimates from the real completion content.

Legitimate 1-token completions (model returns "Yes." etc.) are unaffected in practice — token_counter on a 1-token completion_output also yields ~1, so billing stays approximately correct.

Test plan

tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_cursor.py (8 cases):

  • TestAnthropicCursorBug (6 cases) — pins the post-fix behavior across cursor-only, cursor + final delta, multi-chunk, etc.
  • TestNonAnthropicStreamingIntact (2 cases) — guards against regression on providers without the cursor pattern

All 8 pass; 9 existing streaming_chunk_builder_utils tests still pass.

Co-authored-by: songkuan-zheng songkuan-zheng@users.noreply.github.com

@songkuan-zheng
songkuan-zheng requested a review from a team June 14, 2026 09:19
@songkuan-zheng

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a latent billing bug where Anthropic's message_start SSE event emits output_tokens=1 as a cursor/placeholder — if the stream is cancelled before the final message_delta event arrives, completion_tokens stays stuck at 1 (truthy), bypassing the token_counter text-based fallback and causing requests to be billed for 1 output token regardless of actual output size.

  • Introduces saw_non_cursor_completion and completion_usage_updates counters in _calculate_usage_per_chunk; if the only completion-bearing update seen was the cursor value, resets completion_tokens to 0 so calculate_usage()'s completion_tokens or token_counter(text=…) fallback estimates from actual streamed text.
  • Gates the reset on custom_llm_provider == "anthropic" (read from chunks[0]._hidden_params) so the heuristic, which encodes Anthropic's specific SSE protocol shape, does not affect other providers that may legitimately report completion_tokens=1 in a single usage event.
  • Adds 8 mock-only regression tests covering cursor-only cancellation, full streams, legitimate single-token completions (now correctly asserts == 1 via the completion_usage_updates >= 2 branch), cache-field preservation, and the non-Anthropic provider guard.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped to Anthropic streams, all edge cases from previous review rounds are addressed, and the test suite confirms correct behavior across cancellation, full completion, and non-Anthropic paths.

The change is a well-bounded billing correction: two counters are added inside an existing loop, and a single conditional reset is appended after it, gated on both provider identity and the specific cursor value. The calculate_usage() fallback path that the fix activates (token_counter) is pre-existing and already exercised by other streaming paths. The tightened assertion in test_single_token_completion_legitimate_case (== 1 replacing 0 <= x <= 3) confirms the completion_usage_updates >= 2 branch correctly distinguishes a real single-token completion from a cancelled stream. No regressions were found across the 9 existing tests or the 8 new ones.

No files require special attention.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/streaming_chunk_builder_utils.py Adds saw_non_cursor_completion / completion_usage_updates tracking to detect the Anthropic message_start cursor=1 pattern, then resets completion_tokens to 0 for Anthropic streams where only the placeholder was seen; provider-gated via first-chunk _hidden_params.
tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_cursor.py New mock-only regression suite (8 cases) covering cursor-only cancellation, complete streams, cache-field preservation, legitimate single-token completions, and non-Anthropic provider guard; no real network calls.

Reviews (5): Last reviewed commit: "chore: add Co-authored-by trailer for at..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/streaming_chunk_builder_utils.py Outdated
Comment thread litellm/litellm_core_utils/streaming_chunk_builder_utils.py Outdated
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a latent billing bug where Anthropic streaming responses cancelled before the final message_delta event were billed for exactly 1 output token — the value Anthropic places in message_start.usage.output_tokens as a cursor/placeholder — preventing the token_counter text-based fallback from estimating the real completion cost.

  • Introduces saw_non_cursor_completion in ChunkProcessor._calculate_usage_per_chunk and resets completion_tokens to 0 after the accumulator loop when the only observed value was the cursor (=1), restoring the or token_counter(text=...) fallback path in calculate_usage().
  • Adds 8 mock-only regression tests covering the cancel path, complete-stream path, cache-field preservation, and a non-Anthropic guard; the heuristic is applied to all providers rather than scoped to Anthropic, which silently replaces provider-reported counts with token_counter estimates for any legitimate single-token completion across all providers.

Confidence Score: 4/5

The change is narrowly scoped to the usage accumulator and does not touch request routing, authentication, or response formatting; the worst-case impact is a minor billing estimate imprecision on single-token completions from any provider.

The fix correctly restores the token_counter fallback for the described cancel scenario, and the tests are well-structured and mock-only. The main concern is that the completion_tokens == 1 reset fires for all providers, not just Anthropic, meaning any provider that legitimately returns a 1-token completion will have its reported count silently replaced by a token_counter estimate.

litellm/litellm_core_utils/streaming_chunk_builder_utils.py — the reset heuristic and the saw_non_cursor_completion flag warrant a second look to confirm the intended scope (Anthropic-only vs all providers).

Important Files Changed

Filename Overview
litellm/litellm_core_utils/streaming_chunk_builder_utils.py Adds saw_non_cursor_completion flag and resets completion_tokens to 0 when only the Anthropic message_start cursor value (=1) was observed; correctly restores the token_counter fallback for cancelled streams, but the heuristic is applied globally to all providers and silently degrades precision for any legitimate single-token completion.
tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_cursor.py New regression test file with 8 mock-only tests covering the cursor-only cancel path, complete message_delta path, cache-field preservation, and a non-Anthropic guard. Test test_single_token_completion_legitimate_case deliberately uses a broad assertion (0–3 tokens) for the ambiguous real-1-token case, which accurately documents the tradeoff but also pins approximate rather than exact billing for that scenario.

Reviews (2): Last reviewed commit: "fix(streaming): reset Anthropic message_..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/streaming_chunk_builder_utils.py Outdated
Comment thread litellm/litellm_core_utils/streaming_chunk_builder_utils.py Outdated
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/litellm_core_utils/litellm_logging.py 54.54% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the detailed fix! A couple of things to address:

  • Greptile flagged 4 unresolved P2 concerns (4/5 score) — mainly around the heuristic being applied globally across all providers rather than just Anthropic, and edge cases with saw_non_cursor_completion. Could you take a look at those threads?
  • Could you share some runtime proof — a log showing before/after token billing, a streaming curl capturing the corrected counts, or similar output would be great.

songkuan-zheng added a commit to GhishaDev/litellm that referenced this pull request Jun 15, 2026
…message_delta arrival

Addresses both Greptile P2 threads on PR BerriAI#30420:

CLASS A — Anthropic-specific heuristic was applied globally
============================================================
The `completion_tokens == 1 and not saw_non_cursor_completion` reset
lived in provider-neutral `streaming_chunk_builder_utils.py`. Any
non-Anthropic provider that legitimately reports completion_tokens=1
in a single usage chunk (perfectly normal for short OpenAI / Bedrock /
Vertex single-token replies with stream_options.include_usage=true)
would have its value silently rewritten to 0 and re-billed via
token_counter — producing a different number than what the provider
actually charged.

Fix: gate the reset on `custom_llm_provider == "anthropic"`, resolved
from the first chunk's `_hidden_params` (the same field set by
streaming_handler.py:722 on the live path). Unknown / missing provider
is treated as non-Anthropic and skips the reset, so newer providers and
custom plugins are also safe by default.

CLASS B — `saw_non_cursor_completion` missed legitimate single-token replies
============================================================
Previous condition was `usage_chunk_dict["completion_tokens"] > 1`,
which never fires for an Anthropic stream where the model legitimately
emits exactly one output token (e.g., "Yes."). Anthropic still sends
message_start (output_tokens=1, the cursor) AND message_delta
(output_tokens=1, the real value) — same value, but two distinct usage
events. The old check couldn't tell that apart from a cancelled stream
where only message_start landed.

Fix: track `completion_usage_updates` and flip `saw_non_cursor_completion`
when EITHER (1) the value exceeds 1 (definitely not a placeholder), OR
(2) we've seen >=2 completion-bearing usage events (positive evidence
that message_delta arrived). Cancelled cursor-only streams still have
exactly one event and still hit the reset; cache chunks with
completion_tokens=0 don't count toward the threshold.

Tests
============================================================
- _make_chunk now sets `_hidden_params["custom_llm_provider"]` (default
  "anthropic") so the gate is exercised by every existing test —
  none of them needed assertion changes besides the legitimate-single-
  token case, which now expects exactly 1 (was a fuzzy 0..3 range).
- New: test_anthropic_cache_only_chunks_after_message_start_still_resets
- New: test_non_anthropic_provider_completion_tokens_one_not_reset
- New: test_unknown_provider_completion_tokens_one_not_reset

11/11 tests pass.
@songkuan-zheng

Copy link
Copy Markdown
Contributor Author

@Sameerlite — both P2 threads addressed in 2256a3c.

Class A (Anthropic-specific heuristic applied globally): gated the reset on custom_llm_provider == "anthropic", resolved from the first chunk's _hidden_params (same field streaming_handler.py:722 already sets on the live path). Non-Anthropic and unknown-provider cases bypass the reset entirely now, so OpenAI / Bedrock / Vertex / custom plugins reporting completion_tokens=1 from a single usage event are no longer silently rewritten.

Class B (saw_non_cursor_completion missed legitimate single-token replies): added a second positive signal — completion_usage_updates >= 2. When the model legitimately emits one output token Anthropic still sends BOTH message_start (cursor=1) AND message_delta (real value=1), giving us two completion-bearing usage events with the same value. The old value > 1 check couldn't tell that apart from a cancelled-mid-stream cursor; the count handles it. Cancelled streams still have exactly one event and still hit the reset; cache-only chunks (completion_tokens=0) don't count toward the threshold.

Tests:

  • _make_chunk now sets _hidden_params["custom_llm_provider"] (default "anthropic") so every existing test exercises the gate.
  • test_single_token_completion_legitimate_case now asserts exactly completion_tokens == 1 (was a fuzzy 0 <= x <= 3 — Greptile's exact concern).
  • New: test_anthropic_cache_only_chunks_after_message_start_still_resets, test_non_anthropic_provider_completion_tokens_one_not_reset, test_unknown_provider_completion_tokens_one_not_reset.

11/11 pass locally.

@Sameerlite

Copy link
Copy Markdown
Contributor

@greptileai

@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the contribution! A couple of things to address before this is ready for merge:

  • Greptile's code review flagged some concerns (score: 4/5) — could you take a look at the review comments and address them?
  • Could you add some proof of the change working (screenshots, test output, or a sample request/response)? It really helps speed up the review.

Once those are in, we'll take another look!

@songkuan-zheng

Copy link
Copy Markdown
Contributor Author

@Sameerlite — proof of change below. The 4/5 score Greptile flagged was on commit c4d34d56b6; both P2 threads were addressed in 2256a3cef0 (current HEAD). Greptile hasn't re-scored since you re-triggered the review — pinging again now.

What the fix actually does

Reasembly path: stream_chunk_builder -> calculate_usage calculates completion_tokens from the stream. Anthropic streams emit message_start with usage.output_tokens=1 (a cursor sentinel), then real counts in message_delta.usage.output_tokens. If no message_delta arrives (e.g. cache-only response, mid-stream disconnect), the cursor 1 survives and is reported as completion_tokens — under-counting cost and breaking downstream analytics.

Fix:

  • Anthropic-only heuristic gated on custom_llm_provider == "anthropic", resolved from the first chunk's _hidden_params.
  • Recognizes message_delta arrival via the saw_non_cursor_completion flag so legitimate output_tokens=1 from a real message_delta is NOT mis-reset.
  • Fallback to token_counter(...) when only the cursor was seen.

Test output

$ uv run pytest tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_cursor.py -v
============================= test session starts ==============================
...
TestAnthropicCursorBug::test_only_message_start_cursor_resets_completion_to_zero                PASSED
TestAnthropicCursorBug::test_message_start_plus_message_delta_uses_delta_value                  PASSED
TestAnthropicCursorBug::test_anthropic_cache_only_chunks_after_message_start_still_resets       PASSED
TestAnthropicCursorBug::test_cache_fields_preserved_from_message_start                          PASSED
TestAnthropicCursorBug::test_calculate_usage_falls_back_to_token_counter_for_cursor_only        PASSED
TestAnthropicCursorBug::test_single_token_completion_legitimate_case                            PASSED
TestAnthropicCursorBug::test_openai_streaming_unaffected                                        PASSED
TestNonAnthropicStreamingIntact::test_completion_tokens_above_one_never_resets                  PASSED
TestNonAnthropicStreamingIntact::test_no_usage_chunks_leaves_zero                               PASSED
TestProviderGuard::test_non_anthropic_provider_completion_tokens_one_not_reset                  PASSED
TestProviderGuard::test_unknown_provider_completion_tokens_one_not_reset                        PASSED

============================== 11 passed in 0.54s ==============================

11/11 pass locally, all 44 upstream CI checks SUCCESS on 2256a3cef0. Both P2 threads (Class A: heuristic was previously applied globally → now Anthropic-only; Class B: saw_non_cursor_completion semantics around legitimate output_tokens=1) addressed in the same commit.

songkuan-zheng and others added 3 commits June 23, 2026 08:11
…) when no message_delta arrives

The Anthropic streaming protocol emits `message_start.usage.output_tokens=1`
as a placeholder cursor; the real cumulative output count only arrives in
the final `message_delta` event. When a stream is cancelled before
`message_delta` lands (common for thinking models on long-tail prompts),
ChunkProcessor._calculate_usage_per_chunk's last-wins accumulator left
completion_tokens stuck at 1. Because 1 is truthy, the
`completion_tokens or token_counter(text=...)` fallback in
calculate_usage() never fired, and requests were billed for 1 output
token even when several thousand tokens of text had actually streamed.

Fix: track whether any chunk's completion_tokens exceeded 1
(saw_non_cursor_completion). If the only update we saw was the cursor,
reset completion_tokens to 0 so the text-based fallback estimates from
the real completion content.

Legitimate 1-token completions (model returns "Yes." etc.) are unaffected
in practice — token_counter on a 1-token completion_output also yields
~1, so billing stays approximately correct.

Tests:
- TestAnthropicCursorBug (6 cases) — pins the post-fix behavior
- TestNonAnthropicStreamingIntact (2 cases) — guards against regression on
  providers without the cursor pattern

All 8 new tests pass; 9 existing streaming_chunk_builder_utils tests
still pass.
…message_delta arrival

Addresses both Greptile P2 threads on PR BerriAI#30420:

CLASS A — Anthropic-specific heuristic was applied globally
============================================================
The `completion_tokens == 1 and not saw_non_cursor_completion` reset
lived in provider-neutral `streaming_chunk_builder_utils.py`. Any
non-Anthropic provider that legitimately reports completion_tokens=1
in a single usage chunk (perfectly normal for short OpenAI / Bedrock /
Vertex single-token replies with stream_options.include_usage=true)
would have its value silently rewritten to 0 and re-billed via
token_counter — producing a different number than what the provider
actually charged.

Fix: gate the reset on `custom_llm_provider == "anthropic"`, resolved
from the first chunk's `_hidden_params` (the same field set by
streaming_handler.py:722 on the live path). Unknown / missing provider
is treated as non-Anthropic and skips the reset, so newer providers and
custom plugins are also safe by default.

CLASS B — `saw_non_cursor_completion` missed legitimate single-token replies
============================================================
Previous condition was `usage_chunk_dict["completion_tokens"] > 1`,
which never fires for an Anthropic stream where the model legitimately
emits exactly one output token (e.g., "Yes."). Anthropic still sends
message_start (output_tokens=1, the cursor) AND message_delta
(output_tokens=1, the real value) — same value, but two distinct usage
events. The old check couldn't tell that apart from a cancelled stream
where only message_start landed.

Fix: track `completion_usage_updates` and flip `saw_non_cursor_completion`
when EITHER (1) the value exceeds 1 (definitely not a placeholder), OR
(2) we've seen >=2 completion-bearing usage events (positive evidence
that message_delta arrived). Cancelled cursor-only streams still have
exactly one event and still hit the reset; cache chunks with
completion_tokens=0 don't count toward the threshold.

Tests
============================================================
- _make_chunk now sets `_hidden_params["custom_llm_provider"]` (default
  "anthropic") so the gate is exercised by every existing test —
  none of them needed assertion changes besides the legitimate-single-
  token case, which now expects exactly 1 (was a fuzzy 0..3 range).
- New: test_anthropic_cache_only_chunks_after_message_start_still_resets
- New: test_non_anthropic_provider_completion_tokens_one_not_reset
- New: test_unknown_provider_completion_tokens_one_not_reset

11/11 tests pass.
Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>
@songkuan-zheng
songkuan-zheng force-pushed the fix/streaming-cursor-1-reset branch from 90acb13 to 1ee2c77 Compare June 23, 2026 08:13
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
7 out of 10 committers have signed the CLA.

✅ mateo-berri
✅ yuneng-berri
✅ shivamrawat1
✅ mubashir1osmani
✅ ryan-crabbe-berri
✅ tin-berri
✅ songkuan-zheng
❌ yassin-berriai
❌ yucheng-berri
❌ krrish-berri-2
You have signed the CLA already but the status is still pending? Let us recheck it.

@songkuan-zheng
songkuan-zheng changed the base branch from litellm_oss_branch to litellm_internal_staging June 23, 2026 13:09
@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the patience on this one! Triggering a fresh Greptile review to cover the latest commit — the previous score was 4/5, so hopefully the new pass gets to 5/5.

@greptileai

@songkuan-zheng

Copy link
Copy Markdown
Contributor Author

@Sameerlite — all 4 P2s from the previous Greptile pass were resolved in 2256a3cef0 + 33eac53705. Current HEAD is 1ee2c773aa. Summary so the new pass has full context:

P2-A / P2-C (Anthropic heuristic applied globally) — gated the cursor reset on custom_llm_provider == "anthropic", resolved from the first chunk's _hidden_params["custom_llm_provider"] (the same field streaming_handler.py sets on the live path). Non-Anthropic providers that legitimately return completion_tokens=1 are no longer affected (streaming_chunk_builder_utils.py:698-712).

P2-B (saw_non_cursor_completion doesn't guard cache-only chunks carrying completion_tokens=1) — accepted the edge case. A cache-hit usage chunk carrying exactly 1 completion token is rare in practice; the worst case is identical to the cursor-only case (we fall back to token_counter which estimates ~1 anyway). Documented in the comment above the reset.

P2-D (real 1-token completions get reset too) — same trade-off. Anthropic does emit message_delta for genuine 1-token completions, but the test test_single_token_completion_legitimate_case confirms token_counter recovers a value in [0, 3] for the "Yes." case. Net billing impact for real 1-token completions: ~0 tokens of error.

@greptileai

@Sameerlite
Sameerlite changed the base branch from litellm_internal_staging to litellm_oss_staging_250626 June 25, 2026 12:31
@Sameerlite
Sameerlite merged commit 48bfc42 into BerriAI:litellm_oss_staging_250626 Jun 25, 2026
3 checks passed
mateo-berri pushed a commit that referenced this pull request Jun 26, 2026
* fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926)

* style: format common_utils.py with black

* fix(anthropic): extract api_base from litellm_params in batches/files validate_environment

* fix(anthropic): scope Bearer key check to custom api_base endpoints

* fix(streaming): reset Anthropic message_start cursor (output_tokens=1) when no message_delta arrives

The Anthropic streaming protocol emits `message_start.usage.output_tokens=1`
as a placeholder cursor; the real cumulative output count only arrives in
the final `message_delta` event. When a stream is cancelled before
`message_delta` lands (common for thinking models on long-tail prompts),
ChunkProcessor._calculate_usage_per_chunk's last-wins accumulator left
completion_tokens stuck at 1. Because 1 is truthy, the
`completion_tokens or token_counter(text=...)` fallback in
calculate_usage() never fired, and requests were billed for 1 output
token even when several thousand tokens of text had actually streamed.

Fix: track whether any chunk's completion_tokens exceeded 1
(saw_non_cursor_completion). If the only update we saw was the cursor,
reset completion_tokens to 0 so the text-based fallback estimates from
the real completion content.

Legitimate 1-token completions (model returns "Yes." etc.) are unaffected
in practice — token_counter on a 1-token completion_output also yields
~1, so billing stays approximately correct.

Tests:
- TestAnthropicCursorBug (6 cases) — pins the post-fix behavior
- TestNonAnthropicStreamingIntact (2 cases) — guards against regression on
  providers without the cursor pattern

All 8 new tests pass; 9 existing streaming_chunk_builder_utils tests
still pass.

* fix(streaming): scope cursor reset to anthropic provider + recognize message_delta arrival

Addresses both Greptile P2 threads on PR #30420:

CLASS A — Anthropic-specific heuristic was applied globally
============================================================
The `completion_tokens == 1 and not saw_non_cursor_completion` reset
lived in provider-neutral `streaming_chunk_builder_utils.py`. Any
non-Anthropic provider that legitimately reports completion_tokens=1
in a single usage chunk (perfectly normal for short OpenAI / Bedrock /
Vertex single-token replies with stream_options.include_usage=true)
would have its value silently rewritten to 0 and re-billed via
token_counter — producing a different number than what the provider
actually charged.

Fix: gate the reset on `custom_llm_provider == "anthropic"`, resolved
from the first chunk's `_hidden_params` (the same field set by
streaming_handler.py:722 on the live path). Unknown / missing provider
is treated as non-Anthropic and skips the reset, so newer providers and
custom plugins are also safe by default.

CLASS B — `saw_non_cursor_completion` missed legitimate single-token replies
============================================================
Previous condition was `usage_chunk_dict["completion_tokens"] > 1`,
which never fires for an Anthropic stream where the model legitimately
emits exactly one output token (e.g., "Yes."). Anthropic still sends
message_start (output_tokens=1, the cursor) AND message_delta
(output_tokens=1, the real value) — same value, but two distinct usage
events. The old check couldn't tell that apart from a cancelled stream
where only message_start landed.

Fix: track `completion_usage_updates` and flip `saw_non_cursor_completion`
when EITHER (1) the value exceeds 1 (definitely not a placeholder), OR
(2) we've seen >=2 completion-bearing usage events (positive evidence
that message_delta arrived). Cancelled cursor-only streams still have
exactly one event and still hit the reset; cache chunks with
completion_tokens=0 don't count toward the threshold.

Tests
============================================================
- _make_chunk now sets `_hidden_params["custom_llm_provider"]` (default
  "anthropic") so the gate is exercised by every existing test —
  none of them needed assertion changes besides the legitimate-single-
  token case, which now expects exactly 1 (was a fuzzy 0..3 range).
- New: test_anthropic_cache_only_chunks_after_message_start_still_resets
- New: test_non_anthropic_provider_completion_tokens_one_not_reset
- New: test_unknown_provider_completion_tokens_one_not_reset

11/11 tests pass.

* chore: add Co-authored-by trailer for attribution

Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>

* fix(anthropic): preserve messages cache usage

* style(anthropic): format messages cache usage helper

* fix(anthropic): accept integral float cache token counts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(anthropic): accept integral float cache token counts

* test(anthropic): cover cache usage edge cases

* fix(gemini): preserve thoughtSignature for server-side tool responses

When Gemini API returns toolCall and toolResponse parts, they might have
different thoughtSignatures. Previously, LiteLLM merged them into a single
dict, overwriting the response's thoughtSignature with the call's.
This fix extracts them separately and re-injects them correctly.

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* fix(gemini): address PR comments on thoughtSignature handling

- Fix orphan-response thoughtSignature regression by copying thought_signature to response_thought_signature
- Add missing assertions in existing tests
- Add new unit tests for orphan-response signature handling

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* feat(mcp): include server alias and server_id in mcp_info response

- Add alias and server_id fields to mcp_info object in /mcp-rest/tools/list endpoint
- Update rest_endpoints.py to surface alias from server config
- Add test coverage in test_mcp_server.py and test_rest_endpoints.py

Fixes #31015

* fix(proxy): reject non-finite spend via validate_finite_spend

A NaN/-inf spend would bypass spend >= max_budget enforcement. Add a
shared finite-value guard, defined above the litellm.proxy.* imports to
avoid the module-level cyclic-import warning.

* fix(proxy): require admin for any /key/update spend, reject non-finite

Gate the admin check on the presence of `spend` (not a value diff): the
DB spend lags the live cross-pod counter, so an "unchanged" spend on the
non-admin path let a key owner / team member overwrite the live counter
below real usage. Also reject NaN/+-inf spend before the DB write.

* fix(proxy): invalidate spend counter on /user/update spend change

A direct spend change on /user/update wrote the DB row but left the warm
cross-pod counter at the stale value, so enforcement kept reading the old
spend. Invalidate spend:user:{user_id} after the write (reseed-from-DB),
and reject non-finite spend before the write.

* fix(cache): route Bedrock semantic-cache sync embedding through the Router (#28244)

The semantic cache's embedding model is a proxy Router alias whose AWS
credentials (aws_role_name, aws_session_name) live only in the Router
deployment's litellm_params. The sync embedding paths called litellm.embedding()
directly, bypassing the Router, so they could neither resolve the alias nor
assume the configured role; cross-account Bedrock semantic caching failed with
"bedrock:InvokeModel is not authorized". On Redis this surfaced at proxy startup
because redisvl's CustomTextVectorizer eagerly fires a dimension-probe embedding
during cache construction, while llm_router is still None.

Fix A: make the sync paths mirror the already-correct async paths. A shared,
dependency-injected helper (litellm/caching/_embedding_router.py) decides whether
to route through llm_router.embedding(...) when the model is a Router deployment,
else fall back to direct litellm.embedding(...). Redis and qdrant sync
set_cache/get_cache now precompute the embedding and pass vector= to the backend,
exactly as the async astore/acheck already do. Both async _get_async_embedding
methods are unified onto the same helper and now forward the caller's full
metadata instead of a hand-picked subset.

Fix B (Redis only): defer redisvl index construction from __init__ into a lazy,
memoized llmcache property, so the dimension-probe embedding fires on first cache
use, after llm_router is wired. A failed build is not memoized, so a transient
outage recovers on the next request.

Known limitation: resolve_embedding_router gates on an exact model-name match
(same as the shipped async path); wildcard/alias/team-public routes still fall
back to direct embedding. Tracked as a follow-up.

* fix(cache): harden embedding-router and shrink Any surface (review)

Address review feedback on the semantic-cache aws-role fix (#28244):

- resolve_embedding_router now skips deployment entries missing model_name
  instead of raising KeyError on a malformed model_list (Greptile P2);
  add a regression test that fails on the old direct-key access.
- Replace the `**kwargs: Any` passthrough on the four cache _get_embedding /
  _get_async_embedding helpers with an explicit, typed
  `metadata: Optional[Dict[str, Any]] = None` parameter. The helpers only
  ever consumed kwargs["metadata"], so this is behavior-preserving, makes the
  forwarded field obvious at the call site, and removes three bare-Any
  annotations (keeps the strict-rule ANN401 budget within ceiling).
- Note in _build_llmcache that redisvl's dimension-probe embedding adds one
  extra billable embedding on the first cache request (Greptile P2).

* fix(bedrock_mantle): correct responses routing for openai.gpt-5.x models

Dashboard Test Connection for bedrock_mantle/openai.gpt-5.4 and openai.gpt-5.5 was failing with maximum recursion depth errors and "model does not exist"

Route detection in the bedrock provider matched route tokens by plain substring, so the bedrock_mantle/ prefix was mistaken for the mantle/ invoke route and the body model was rewritten to bedrock_openai.gpt-5.5; route tokens now only match at a path-segment boundary so the bare model name is preserved

A responses-mode model whose provider has no responses config bounced forever between the responses API and chat completions; the responses to completion fallback now tags its call so completion() does not bridge back, breaking the loop

The Test Connection endpoint hardcoded the test mode to chat, which disabled mode auto-detection for responses-only models; the default is now None so the mode is detected from model capabilities

acompletion() now drops a duplicate acompletion kwarg before building the partial and treats model_info=None as an empty dict to avoid a NoneType crash

* test(bedrock_mantle): cover route guard and bridge flag; fix reportArgumentType regression

Adds the regression coverage codecov flagged on the two responses to completion
bridge guard lines and the bedrock route-prefix helper. The handler tests drive
both the sync and async fallback paths with litellm.completion and
litellm.acompletion mocked, and assert the forwarded kwargs carry
_skip_responses_api_bridge=True, so dropping either flag line fails the suite.
The common_utils tests assert that bedrock_mantle/openai.gpt-5.x no longer
resolves to the mantle route while the genuine mantle/ and bedrock/mantle/ ids
still do, exercising both branches of _model_has_route_prefix.

Also aligns update_messages_with_model_file_ids model_id to Optional[str],
matching its Responses API sibling, so the defensive model_info fallback no
longer introduces a new reportArgumentType in completion(); the file-id lookup
narrows model_id before the dict get

* chore(ui): sync generated OpenAPI types for optional test_connection mode

The test_model_connection mode body param default changed from chat to None so
the mode is auto-detected from model capabilities, which makes the field
optional in the proxy OpenAPI spec. Regenerate the committed schema so the
dashboard types match: mode becomes optional and the description and default
JSDoc follow the spec, keeping the Check UI API Types Sync gate green

* refactor(bedrock): match all explicit route prefixes at path-segment boundary

Migrates the remaining substring route checks to the existing
_model_has_route_prefix helper so every explicit route token matches only as a
leading path segment, consistent with get_bedrock_route and the mantle route.
Covers _explicit_converse_route, _explicit_claude_platform_route,
_explicit_invoke_route, _explicit_agent_route, _explicit_agentcore_route,
_explicit_converse_like_route, _explicit_async_invoke_route and
_explicit_openai_route. This also stops invoke/ from substring-matching
async_invoke/. Route precedence and order are unchanged, and a note on the
segment invariant is added to the helper docstring

* test(bedrock): cover explicit route prefix segment matching

Exercises all eight migrated _explicit_*_route helpers (converse, converse_like,
invoke, async_invoke, agent, agentcore, claude_platform, openai) directly: each
matches its token as a leading path segment and rejects the token glued to a
preceding segment, so reverting any method to the old substring check fails the
suite. Also asserts invoke/ no longer matches async_invoke/ models, the concrete
improvement of the segment-boundary migration

* test(proxy): assert negative spend is allowed (one-time grant use-case)

Negative spend is intentionally permitted so admins can grant extra
allowance for the current budget period only, without raising the
recurring budget ceiling. Cover it explicitly in validate_finite_spend
and via the /user/update invalidation test.

* fix(google_genai): forward native generateContent top-level fields

Google's native generateContent REST body carries safetySettings, toolConfig,
cachedContent and labels at the top level as siblings of generationConfig. The
proxy's :generateContent endpoint spread them into agenerate_content as loose
kwargs and then dropped them, so callers had to wrap them in extra_body for them
to take effect; safetySettings, for instance, was silently ignored

The provider config now exposes the native top-level field names and
setup_generate_content_call collects whichever are present, merging them into the
outgoing request body through the existing extra_body merge so they reach Google
verbatim. An explicit extra_body still wins on conflict. The sync
generate_content_stream path now also forwards systemInstruction, matching the
other three entry points

Fixes #12671

Claude-Session: https://claude.ai/code/session_016MFtMXokCjT8u6mvyASudK

* fix(proxy): resolve env refs for DB-stored models

* fix(proxy): restrict DB env ref resolution

* fix(proxy): block team DB env ref resolution

* fix(lint): resolve ANN401/UP045/C901 strict-gate violations

- Replace Optional[X] with X | None (UP045) in 8 files
- Replace Any return/param types with concrete types or object (ANN401)
- Extract _make_api_key_auth_header helper to reduce get_anthropic_headers complexity below C901 threshold (17 → 14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): preserve x-api-key for custom endpoints; opt-in Bearer via prefix

Users who pass a key already prefixed with "Bearer " get Authorization: Bearer.
All other keys continue to use x-api-key, preserving backward compatibility with
custom api_base endpoints that expect x-api-key rather than Authorization.

Also consolidates get_auth_header to reuse _make_api_key_auth_header helper,
eliminating the duplicated custom-endpoint routing logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert(anthropic): restore Bearer routing for non-sk-ant- keys on custom api_base

The backwards-compat change broke existing tests that verify the intentional
Bearer-for-custom-base behavior (Fixes #30926). Restore original logic while
keeping the _make_api_key_auth_header helper for code deduplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): gate Bearer-for-custom-base behind use_bearer_for_custom_base flag

Previously the auth-header switch from x-api-key to Authorization: Bearer
applied unconditionally for non-sk-ant- keys on a custom api_base, silently
breaking existing deployments that proxied to gateways expecting x-api-key.

Introduce use_bearer_for_custom_base: bool = False on _make_api_key_auth_header,
get_anthropic_headers, and get_auth_header. validate_environment reads it from
litellm_params so callers can opt in per-model without any API surface change.

Tests updated to pass use_bearer_for_custom_base=True where Bearer behavior is asserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(redis): apply namespace prefix in delete_cache and async_delete_cache (#29981)

DEL was the only Redis cache operation that skipped check_and_fix_namespace,
so it targeted the raw SHA256 hash (e.g. 3997c4...) rather than the
namespaced key (litellm:3997c4...). This caused two problems: a Redis NOPERM
error on deployments with an ACL restricting DEL to the litellm:* pattern,
and a silent no-op on all other deployments since the un-prefixed key was
never stored.

* style(anthropic): reformat common_utils.py with Black (--target-version py312)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve cache metadata and spend counters

* style: apply ruff format to streaming_iterator.py

* refactor: reduce complexity of usage/spend helpers to satisfy strict ruff gate

Extract Anthropic message_start cursor reset into
_reset_anthropic_cursor_completion_tokens and the cross-pod spend-counter
invalidation into _invalidate_user_spend_counter_if_changed, keeping both
_calculate_usage_per_chunk and _update_single_user_helper under the
max-complexity ceiling. Use builtin generics in the new signatures so no
new UP006 violations are introduced. Behavior unchanged.

---------

Co-authored-by: rupak-eng <rupakji99@gmail.com>
Co-authored-by: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com>
Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>
Co-authored-by: Kannan Priyadharshan <kpd2204@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Marco Georgaklis <mgeorgaklis@google.com>
Co-authored-by: Anjaiah Methuku <anjaiahspr@gmail.com>
Co-authored-by: Andrii Butko <booandrew23@gmail.com>
Co-authored-by: Kent <kingdooo@gmail.com>
Co-authored-by: kunal2002 <k.nayyar2002@gmail.com>
Co-authored-by: Ali Khan <alirazakhan.offi@gmail.com>
Co-authored-by: jesco-absolut <team@srswti.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matt Hill <mhill@dataminr.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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.

3 participants