Skip to content

🔄 Upstream Sync: LiteLLM v1.92.0 - #121

Merged
mateo-di merged 2279 commits into
carto/mainfrom
upstream-sync/v1.92.0
Aug 4, 2026
Merged

🔄 Upstream Sync: LiteLLM v1.92.0#121
mateo-di merged 2279 commits into
carto/mainfrom
upstream-sync/v1.92.0

Conversation

@Cartofante

@Cartofante Cartofante commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

🔄 Upstream Sync: LiteLLM v1.92.0

Syncs CARTO's LiteLLM fork with upstream stable release v1.92.0.

Metric Value
Version 1.83.14v1.92.0
Commits 2268
Files Changed 5714
Upstream Release v1.92.0

Caution

⚠️ DO NOT SQUASH MERGE THIS PR

Use "Create a merge commit" only. Squashing destroys upstream history and breaks future syncs.


🧪 Pre-Merge Checklist

  • CI checks pass (lint, tests, Docker build)
  • CARTO customizations preserved
  • pyproject.toml version matches upstream

📊 Release Information (click to expand)
🔀 Branch Flow (click to expand)
  1. BerriAI/litellm:main merged into CartoDB/litellm:main
  2. ✅ Created dedicated sync branch: upstream-sync/v1.92.0
  3. 📝 This PR: upstream-sync/v1.92.0carto/main

[!NOTE]
Why a dedicated branch? Allows pushing conflict resolution commits directly to this PR.

📝 CARTO-Specific File Guidelines (click to expand)

When reviewing or resolving conflicts:

✅ Keep CARTO Versions (Ours)

  • .github/workflows/carto_*.yaml - CARTO workflows
  • .github/workflows/carto-*.yml - CARTO workflows
  • CARTO_*.md, docs/CARTO_*.md - CARTO documentation

🔄 Accept Upstream (Theirs)

  • pyproject.toml - Version field
  • litellm/ - Core library code
  • tests/ - Upstream tests
  • requirements.txt - Dependencies

⚠️ Manual Review Required

  • Dockerfile, docker/Dockerfile.non_root - CARTO customizations
  • Makefile - Check # CARTO: sections
🔧 Conflict Resolution (click to expand)

If this PR has conflicts:

Option 1: Automated (Recommended)

The carto-upstream-sync-resolver workflow triggers automatically.

What it does:

  1. 🤖 Detects conflicts → 🔀 Merges carto/main → ✏️ Resolves conflicts → 🧪 Runs tests → 📌 Pushes to this PR

You just need to: Wait for resolution commits, verify CARTO customizations, merge.

[!TIP]
Single PR workflow! No separate resolution PR needed.

Option 2: Manual Resolution

git fetch origin
git checkout upstream-sync/v1.92.0
git merge origin/carto/main  # Creates conflicts
# ... resolve conflicts ...
make lint && make test-unit
git push origin upstream-sync/v1.92.0
📚 Documentation Links (click to expand)

🤖 This PR was automatically created by the carto-upstream-sync workflow.

🔧 CARTO Feature Fixes Applied

Status: ✅ Fixed
Features Restored: 1

PR #118: fix(docker): fetch arm64 prisma schema-engine for non_root image

  • Files: docker/Dockerfile.non_root
  • Decision: Direct restoration was appropriate because this is an independent env var that doesn't conflict with any upstream changes. The variable tells prisma-client-py to fetch both amd64 and arm64 schema-engine binaries during build, enabling arm64 hosts to run prisma migrate deploy
  • Evidence: Dockerfile syntax check passed (docker build --check), grep confirms env var is present at line 58

Fixed: 2026-07-14 02:47:30 UTC
Workflow Run: #31

CARTO Customizations Analysis

Overall Assessment: ✅ PASS

Decision Count Description
Upstream Substitutes 2 Upstream provides equivalent functionality
Customized Upstream 2 Upstream enhanced with CARTO-specific behavior
Preserved CARTO 8 Full CARTO implementation kept
Incorrectly Dropped 0 CARTO feature lost (needs fixing!)
Total 12

CARTO Feature Preservation Analysis

Summary

Decision Count
Upstream Substitutes 2
Customized Upstream 2
Preserved CARTO 8
Incorrectly Dropped 0

Overall Assessment: PASS

All 12 CARTO features from the manifest have been correctly handled during the upstream sync resolution. No features were incorrectly dropped. The verification patterns for all features exist in the resolved codebase.

Feature Details

Upstream Substitutes (2)

These features are now provided by upstream LiteLLM, so CARTO-specific code was correctly not duplicated:

  1. OCI Gemini Tool Call UUIDs (PR fix(oci): Add tool calling support for OCI Gemini streaming #68) - Upstream refactored OCI to use setdefault for tool call ID generation, achieving the same functionality
  2. OCI Inline PEM Key Normalization (PR feat(oci): Enable Oracle Cloud Infrastructure configuration via UI BerriAI/litellm#17159) - This feature was successfully upstreamed; identical code exists in v1.92.0

Customized Upstream (2)

These features use upstream as the base while adding CARTO-specific enhancements:

  1. Snowflake Streaming + Tool Calling (PRs fix: Snowflake PAT auth and Claude streaming support #38, fix: Enable Snowflake tool calling via Responses API #58) - Upstream's v1.92.0 refactored Snowflake to use Anthropic Messages API. The resolved version builds on this structure while adding CARTO's Cortex compatibility fixes (+77 lines)
  2. Azure URL Suffix Stripping (PR fix(azure): Strip operation suffixes from deployment URLs to prevent 404 errors #70) - Upstream's select_azure_base_url_or_endpoint was enhanced with CARTO's re.sub logic to strip operation suffixes that the SDK would append again (+22 lines)

Preserved CARTO (8)

These are CARTO-only features not present in upstream, all correctly preserved:

  1. OCI Parallel Tool Result Reordering (PR 🔄 Upstream Sync: LiteLLM v1.92.0 #121) - Reorders TOOL results to match toolCalls order for OCI's positional validation
  2. Snowflake Full URL Passthrough - Prevents URL duplication when api_base already contains the full Cortex endpoint
  3. JSON Repair for Streaming Tool Calls (PR fix: repair malformed JSON in streaming tool call arguments #54) - _validate_and_repair_tool_arguments repairs malformed JSON in streaming
  4. Redis Session Storage (PR Update poetry.lock #16) - _store_session_in_redis and _patch_store_session_in_redis for session persistence
  5. Snowflake Cortex Claude Function-Calling Follow-up Turns (PR fix(snowflake): unblock Claude function-calling follow-up turns on Cortex #111) - _strip_openai_annotations removes OpenAI-only fields Cortex rejects
  6. Snowflake Cortex Array Content Flattening (PR fix(snowflake): flatten array-form message content for Cortex #112) - _content_to_text_string flattens array-form content to plain strings
  7. Databricks Empty Tool Call Arguments Normalization (PRs fix: stop Databricks streaming from rewriting tool_call arguments '{}' to '' #109, fix: strip OpenAI 'annotations' field from outbound Databricks chat messages #110) - Coerces empty arguments to '{}'
  8. Databricks Strip OpenAI Annotations (PR fix: strip OpenAI 'annotations' field from outbound Databricks chat messages #110) - Removes 'annotations' field from Databricks requests

Issues Found

None. All CARTO features are present and verified.


Feature-by-Feature Breakdown

PR #68: OCI Gemini Tool Call UUIDs

  • Decision: ✅ Upstream Substitutes (high confidence)
  • Reason: Upstream refactored OCI to use modular helpers. Tool call ID generation now uses setdefault in generic.py (lines 400-402, 459); separate UUID generation not required. The manifest correctly notes that upstream's approach achieves the same goal.
  • Files: litellm/llms/oci/chat/generic.py
  • Recommendation: Correct decision - upstream provides equivalent functionality via setdefault pattern

PR #121: OCI Parallel Tool Result Reordering

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: This is a CARTO-only feature not present in upstream. The _reorder_tool_results_to_match_tool_calls function (lines 198-236) sorts TOOL results to match the assistant's toolCalls order, fixing OCI's positional validation. Added +43 lines to upstream's generic.py (438 -> 481 lines).
  • Files: litellm/llms/oci/chat/generic.py
  • Recommendation: Correct decision - this is unique CARTO functionality that must be preserved. Candidate for upstreaming.

PR BerriAI#17159: OCI Inline PEM Key Normalization

  • Decision: ✅ Upstream Substitutes (high confidence)
  • Reason: This feature was upstreamed to LiteLLM. The upstream v1.92.0 contains identical code: oci_key validation, escaped newline normalization, and load_private_key_from_str call. No CARTO additions needed.
  • Files: litellm/llms/oci/common_utils.py
  • Recommendation: Correct decision - CARTO's feature was successfully upstreamed

PR #[38,58]: Snowflake Streaming + Tool Calling

  • Decision: 🔧 Customized Upstream (high confidence)
  • Reason: Upstream v1.92.0 refactored Snowflake to use Anthropic Messages API with SnowflakeStreamingHandler and _extract_system_and_messages. The resolved version builds on upstream's refactored structure (+77 lines: 639->716) while adding CARTO enhancements for tool calling compatibility.
  • Files: litellm/llms/snowflake/chat/transformation.py
  • Recommendation: Correct decision - upstream base enhanced with CARTO-specific Cortex compatibility fixes

PR #[]: Snowflake Full URL Passthrough

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature at line 158: 'CARTO: skip path construction if api_base already contains the full endpoint'. Not present in upstream. Prevents URL duplication when api_base already has the full Cortex path.
  • Files: litellm/llms/snowflake/chat/transformation.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

PR #70: Azure URL Suffix Stripping

  • Decision: 🔧 Customized Upstream (high confidence)
  • Reason: Upstream has select_azure_base_url_or_endpoint that handles /openai/deployments detection. CARTO enhanced this by adding re.sub to strip operation suffixes (/chat/completions, /completions, /embeddings, etc.) that the SDK would append again. Resolved version adds docstring and suffix stripping logic (+22 lines: 789->811).
  • Files: litellm/llms/azure/common_utils.py
  • Recommendation: Correct decision - upstream function enhanced with CARTO's suffix stripping logic

PR #54: JSON Repair for Streaming Tool Calls

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. The _validate_and_repair_tool_arguments function (line 43) and its calls (lines 351, 394) are not present in upstream v1.92.0. This repairs malformed JSON in streaming tool call arguments.
  • Files: litellm/litellm_core_utils/streaming_chunk_builder_utils.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

PR #16: Redis Session Storage

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. _store_session_in_redis (line 1106) and _patch_store_session_in_redis (line 2135 in transformation.py) are not present in upstream v1.92.0. The resolved version adds +42 lines to streaming_iterator.py (1099->1141) for Redis session persistence.
  • Files: litellm/responses/litellm_completion_transformation/streaming_iterator.py, litellm/responses/litellm_completion_transformation/transformation.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

PR #111: Snowflake Cortex Claude Function-Calling Follow-up Turns

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. _strip_openai_annotations is called at lines 273 and 298 within _extract_system_and_messages. Not present in upstream v1.92.0. Strips OpenAI-only 'annotations' field that Cortex's Anthropic-compatible endpoint rejects.
  • Files: litellm/llms/snowflake/chat/transformation.py
  • Recommendation: Correct decision - CARTO's fix properly anchored to call site after upstream rewrite

PR #112: Snowflake Cortex Array Content Flattening

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. _content_to_text_string (line 91) flattens OpenAI array-form content to plain strings. Not present in upstream v1.92.0. Fixes Cortex error 390142.
  • Files: litellm/llms/snowflake/chat/transformation.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

PR #[109,110]: Databricks Empty Tool Call Arguments Normalization

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. _normalize_empty_tool_call_arguments (line 150) and the streaming check at line 741 are not in upstream v1.92.0. Coerces empty arguments to '{}' to prevent INVALID_PARAMETER_VALUE errors.
  • Files: litellm/llms/databricks/chat/transformation.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

PR #110: Databricks Strip OpenAI Annotations

  • Decision: 🔒 Preserved CARTO (high confidence)
  • Reason: CARTO-only feature. _strip_openai_annotations (line 181) removes OpenAI-only 'annotations' field from Databricks requests. Not in upstream v1.92.0.
  • Files: litellm/llms/databricks/chat/transformation.py
  • Recommendation: Correct decision - unique CARTO functionality preserved

Analyzed: 2026-08-03 17:15:24 UTC
Workflow Run: #40
Analysis Artifacts: Download JSON/MD
Method: Claude Code (Opus 4.5) post-resolution semantic analysis

shivamrawat1 and others added 30 commits June 27, 2026 20:13
chore(ci): promote internal staging to main
…lose guardrail bypass (BerriAI#31519)

* fix(realtime): stop sending a second Gemini Live setup on follow-up session.update

Gemini Live (BidiGenerateContent) accepts setup as the first-and-only client
message; a second setup closes the socket with 1007 Request contains an invalid
argument. The AI Studio Gemini path forwarded every client session.update after
the first as a follow-up setup, and GA clients (pipecat) send several while
configuring the session, so the second one tore the session down before the
first turn. Callers saw silence after the first response, exponential per-turn
latency from reconnect/retry churn, and intermittent 1011 errors.

Drop subsequent session.updates instead of resending setup, matching what the
Vertex subclass already does. Tools and instructions must ride on the first
session.update before any conversation content.

Adds regression tests covering the plain follow-up, a follow-up that adds tools
(the case the previous identical-only dedup still forwarded), and the guardrail
create_response=False warning path.

* fix(realtime): retry the backend open handshake instead of failing with 1011

The upstream Live API open handshake (e.g. Gemini Live) intermittently hangs;
waiting longer never recovers a hung attempt, but a fresh attempt almost always
connects in ~1s. The proxy opened the backend websocket once with the default
open_timeout and no retry, so a single slow handshake surfaced to the caller as
a fatal 1011 internal error and dropped the call.

Bound each open attempt with a short open_timeout and retry; a bounded attempt
that already timed out spaces out the next try, so no backoff is needed.
Deterministic handshake-status rejections (auth/4xx) are not retried, and the
retry only ever wraps the open, never a live session.

Adds tests for retry-then-succeed, raise-after-max-attempts, and
no-retry-on-auth-failure.

* fix(realtime): close guardrail bypass + surface handshake status; drop obsolete tests

Three review fixes on the Gemini Live realtime path.

Transcription-guardrail bypass: Gemini Live rejects a second setup (1007), so once
the initial setup is sent the guardrail's automaticActivityDetection.disabled=true
can no longer be delivered as a follow-up session.update. With that follow-up now
dropped, the model's auto-response stayed enabled and a realtime_input_transcription
guardrail was bypassed (the model answered before the proxy could gate the turn).
Fold the disable into the one-and-only setup instead: the handler injects it into
the auto-sent setup (gemini_live_defer_setup false) and _send_to_backend injects it
into the deferred first setup. OpenAI sessions accept follow-up updates and are left
untouched.

Backend handshake status: the open-retry treated only InvalidStatusCode as
deterministic; websockets>=15 raises InvalidStatus for a rejected client handshake,
so a 401/403 fell into the broad WebSocketException branch and was retried before
the caller closed the client with 1011 instead of the upstream status. Treat both as
non-retryable.

Obsolete tests: the four tests asserting a follow-up session.update is merged and
re-sent as a second setup asserted behavior that crashes Gemini Live with 1007
(verified directly against the API). Removed; the drop is covered by new regression
tests.

* style(realtime): reformat changed files to ruff line-length 120

Post-merge with litellm_internal_staging, which unified ruff format width to 120
(BerriAI#31518). The realtime change set was formatted at 88, so the changed lines
tripped the whole-file ruff format check. Reformat with ruff 0.15.3 at the repo's
120 width; no logic changes.
…I#29718)

* feat: declarative fallback generalizations for unknown models

Unknown or newly-released models previously degraded (missed cost lookups,
wrong supports_* flags, broken provider routing) and were patched with one-off
hardcoded regexes scattered across Python. This adds a single data-driven source
of truth: a fallback_generalizations block in model_prices_and_context_window.json
holding ordered, case-insensitive regex rules that map a model name to the
metadata to apply when it has no exact entry.

A new fallback_generalizations module owns the rules and a compiled-regex cache
that is built once and invalidated on reload, so the O(n) scan runs only on a
cache miss. get_llm_provider now routes an otherwise-unknown model via the first
matching rule's litellm_provider, replacing the hardcoded _CLAUDE_PATTERN and
_matches_claude_model_pattern. _get_model_info_helper falls back to a matching
rule's model_info after the exact lookups miss, so get_model_info and the
supports_* helpers resolve unknown models from the same rule. get_model_cost_map
extracts the block out of the returned map, and the integrity check now counts
real model entries (excluding reserved meta keys) so the new key cannot mask a
genuinely shrunk upstream file.

The top level of the file stays a flat map of models so existing litellm releases
that fetch the live file keep working and keep receiving updates; the block ships
in both the root file and the bundled backup. An anthropic-claude rule reproduces
the old future-claude routing and additionally supplies capability flags and a
context window

https://claude.ai/code/session_01G8Jro8dPLktwnaaSJwVDpo

* refactor(anthropic): derive adaptive-thinking from a version threshold; harden generalizations

Replace the per-minor-version _is_claude_4_6_model / _is_claude_4_7_model substring
matchers with a single _claude_version_at_least predicate that parses the Claude
family version from the model name and compares against 4.6. This covers 4.8/4.9/5.x
without a code change (the old matchers missed 4.8 entirely) while keeping an explicit
supports_adaptive_thinking flag authoritative when present, so there is one source of
truth. The two direct call sites in the chat transformation now route through
_is_adaptive_thinking_model instead of the deleted matchers.

Also address review feedback on the generalizations module: return a copy of the
matched model_info so a future caller cannot mutate the compiled-rule cache, document
that patterns are matched with re.search and must anchor with ^ and $, and reindent
the fallback_generalizations block to the file's 2-space style in both JSON files.

https://claude.ai/code/session_01G8Jro8dPLktwnaaSJwVDpo

* fix(anthropic): surface adaptive-thinking from the cost map; fix date misparse

supports_adaptive_thinking shipped in the model cost map but was never declared
on ModelInfo nor copied during construction, so get_model_info (and the supports_*
factory) silently dropped it for every provider-prefixed or generalized name; only
a bare base entry resolved. Wire it through ModelInfo like the other capability
flags and backfill the flag onto the genuine Claude 4.6/4.7/4.8 entries across
providers so the data, not code, declares the capability. The anthropic-claude
fallback rule also carries the flag (and now accepts a dotted minor, e.g. 4.6) so
an unmapped future Claude degrades to adaptive thinking without a code change.

Tighten the Claude version parser so an eight-digit date suffix
(claude-opus-4-20250514, the non-adaptive Opus 4.0) is no longer read as minor
4.20250514. The cost map stays authoritative; the version check is only a fallback
for provider-prefixed names (bedrock/invoke routes, -v1-less ids) that resolve to
no mapped entry and so cannot be reached by an exact lookup or the bare-name rule.

https://claude.ai/code/session_01G8Jro8dPLktwnaaSJwVDpo

* fix(anthropic): date-safe adaptive-thinking version fallback, conservative fallback pricing, ruff strict gate

Reconcile adaptive-thinking detection after merging litellm_internal_staging.
Keep the cost-map resolver (_supports_model_capability) as the source of truth and
add a date-safe opus/sonnet/haiku >= 4.6 name version as a fallback for
provider-prefixed ids the cost map cannot resolve (e.g.
bedrock/invoke/us.anthropic.claude-opus-4-6). A two-digit cap on the minor keeps an
eight-digit date suffix from being misread as a minor version, so the dated Claude
4.0 release stays non-adaptive

Price the shipped anthropic-claude fallback rule at the Opus tier so an unknown or
newly released Claude is over-costed rather than billed as free

Drop the module-level global state in fallback_generalizations (PLW0603) in favor of
a small registry object, and switch its annotations plus the new utils helper to
builtin generics (UP006), bringing the ruff strict-rule totals back under ceiling

* refactor(anthropic): drive adaptive-thinking version gate from a declarative rule

Replace the bespoke _claude_version_at_least heuristic with a version-gated fallback_generalizations rule. Unmapped Claude ids now resolve adaptive thinking purely from the cost map: an explicit entry, or the new self-contained anthropic-claude-adaptive-thinking rule that matches opus/sonnet/haiku >= 4.6 (covering 5.x, 6.x and beyond with no code change). New families ship via Price Data Reload instead of a code edit

The rule carries the same Opus-tier pricing as the broad anthropic-claude rule plus supports_adaptive_thinking, and is matched first; the broad rule stays version-neutral, so an unmapped >= 4.6 Claude resolves to full pricing and the adaptive flag from one rule, while a sub-4.6 alias such as claude-opus-4-0 is still priced yet stays non-adaptive. The regex caps the minor at two digits so a dated 4.0 id (...-4-20250514) is never read as a >= 4.6 minor

* refactor(anthropic): dedupe adaptive-thinking rule via declarative extends

The version-gated anthropic-claude-adaptive-thinking rule duplicated the
broad anthropic-claude rule's entire Opus-tier price block because rules do
not merge: first match wins and returns one rule's whole model_info, so the
adaptive rule had to be self-contained.

Add a declarative extends field to fallback_generalizations: a rule names a
parent and inherits its model_info, with its own keys overriding. Inheritance
is resolved once at install time against each rule's raw model_info, so the
adaptive rule now carries only its delta (supports_adaptive_thinking) and
inherits pricing from the broad rule. Runtime matching, provider routing and
gating are unchanged; the broad rule stays anchored and first-match-wins still
holds.

* docs(anthropic): add ignored description key documenting each generalization regex

* fix(anthropic): drop fabricated pricing from the anthropic-claude fallback rule

Per review feedback, the base rule no longer carries input/output/cache costs, and the
adaptive-thinking rule that extends it inherits that no-pricing model_info. Pricing an
unmapped model at a guessed tier reports a confidently-wrong cost without the caller
knowing; dropping it keeps the standard unpriced behavior (zero, not a fabricated
number) so a missing price stays visible. The rules still supply provider routing,
context window, and capability flags, so a brand-new Claude can still be called and its
capabilities (including adaptive thinking for >= 4.6) resolved. Description and tests
updated to match
The error string is already produced by the f-string interpolation; the
trailing .format() call on it was redundant. Add a regression test that
the message renders the model name verbatim.
get_model_list always returns a list, never None, so the is-None branch
could not execute. Collapse to the single reachable message.
…erriAI#30631)

Keep only video test files and CI workflow entries; drop unrelated
production code and non-video test changes from this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
…BerriAI#30529)

* test(batches): add 1:1 test file scaffold for batches component paths

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add harness test for create batch endpoint

* Add retrieve endpoint harness tests

* Add list  endpoint harness tests

* Add cancel endpoint harness tests

* Add cancel endpoint harness tests

* Add test for litellm/batches/main.py

* Add test for litellm/tests/test_litellm/batches/test_batch_utils.py

* Add handler and transformation tests for all providers

* Fix: run batches tests in cicd

* fix(tests): remove azure/__init__.py that shadowed azure namespace package

Adding __init__.py to tests/test_litellm/llms/azure/ caused pytest to
insert tests/test_litellm/llms/ into sys.path[0], making our empty
azure/ dir shadow the real azure-identity namespace package. Any test
that patched azure.identity.* would then fail with AttributeError.

* style(tests): apply ruff format to test_batch_utils.py

Base migrated the formatter from black to ruff format (BerriAI#31317); reformat the
batches scaffold test file to match.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
…erriAI#30950)

* feat(a2a): support a2a-sdk 1.x proxy routing for 0.3 and 1.0 agents

Bump a2a-sdk to 1.x and wire send/stream through compat conversions so the proxy accepts A2A 1.0 JSON-RPC while preserving 0.3 wire clients.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add user controlled protocol version in agents

* Fix exeception mapping

* Fix a2a base url

* Add e2e test for a2a

* Fix lint

* Fix lint

* fix(a2a): harden card version detection and header isolation coverage

Use protocolVersion when inferring agent card wire format, assert distinct httpx cache keys in the header-isolation test, and suppress targeted basedpyright errors for optional SDK imports.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): suppress reportArgumentType for SDK compat types and fix streaming trace ID

- Add pyright: ignore[reportArgumentType] to SendMessageSuccessResponse id= and
  result= args in _send_message, and SendStreamingMessageResponse root= in
  _stream_messages, where a2a-sdk compat types diverge from basedpyright's
  inferred signature, reducing the reportArgumentType count back within budget.
- Fix streaming trace ID in astream_a2a_message to use str(request.id) when
  available instead of always generating a new uuid4(), restoring JSON-RPC
  request-ID correlation for observability.

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

* style(a2a): expand SendStreamingMessageResponse for black formatting

Move pyright: ignore comment to the root= argument line so Black
accepts the expanded multi-line form.

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

* fix(a2a): fix 2 reportArgumentType errors without suppression

- main.py: narrow logging_obj from object|None to Optional[Logging] via
  isinstance check before A2AStreamingIterator call, fixing the
  "Logging | object" argument type mismatch at line 699.
- a2a_endpoints.py: extract response_dict with explicit isinstance(dict)
  guard before passing to normalize_jsonrpc_response, fixing the
  "LLMResponseTypes | dict[str, Any]" type mismatch at line 835.
- Remove spurious pyright: ignore comments added in previous commits that
  were not suppressing the actual errors.

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

* fix(a2a): rewrite upstream URL for 1.0 agent cards in getAuthenticatedExtendedCard

1.0 upstream agent cards store the endpoint URL in supportedInterfaces[0].url
rather than a top-level url field. The previous guard only rewrote url when
it existed at the top level, so after normalize_agent_card lowered a 1.0 card
to 0.3 the upstream internal address leaked into the url field of the 0.3
response.

Fix: rewrite both url and supportedInterfaces[0].url to the proxy address
before calling normalize_agent_card, ensuring the upstream address is never
visible to downstream clients regardless of the upstream card's wire format.

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

* fix: extend _served_version to all PascalCase methods; add direct httpx-client isolation proof

- _served_version now checks `_PASCAL_TO_WIRE` membership instead of two
  hardcoded names, so GetTask/CancelTask/etc. are promoted to 1.0 wire format
  alongside SendMessage — prevents mixed wire formats mid-session
- test_create_a2a_client_uses_fresh_httpx_client now asserts
  a2a_client_a._litellm_httpx_client is not a2a_client_b._litellm_httpx_client
  (direct proof that header bleed cannot occur), in addition to the cache-key
  inequality check

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

* fix: id:0 silently dropped in version_convert; explicit continue in stream retry

- version_convert.py: replace `request_id or ""` with
  `str(request_id) if request_id is not None else ""` in both
  _send_result_to and _stream_result_to; id=0 is valid JSON-RPC and
  must not be coerced to "" which breaks response correlation
- main.py: add explicit `continue` after the A2ALocalhostURLError retry
  in _execute_a2a_stream_with_retry so the control flow (retry → next
  iteration → stream_succeeded guard) is unambiguous

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

* fix: preserve a2a retry and discovery card urls

* Fix black

* Fix test

* fix(a2a): avoid KeyError in discovery log after 0.3→1.0 card normalization

When a 0.3-style agent card is normalized to 1.0, the top-level url key is
replaced by supportedInterfaces; log the already-computed proxy_url instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): preserve taskId when lowering push notification config set params

Flatten 1.x create envelope fields before parsing into TaskPushNotificationConfig so 1.0 clients forwarding to 0.3 upstream keep taskId and config.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): ignore unknown fields in message/send proto fallback

ParseDict in _build_message_send_params now matches other inbound paths so 1.0 clients with extra proto fields are not rejected with -32602.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): normalize tasks/list params and response across protocol versions

Convert list task entries on the response path and lower ListTasksRequest params including status filters when forwarding 1.0 clients to 0.3 upstream.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): avoid reportArgumentType in _lower_list_tasks_params; use local var instead of _parse return

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(a2a): drop private SDK symbol in tasks/list status lowering

_lower_list_tasks_params imported _CORE_TO_COMPAT_TASK_STATE, a private
a2a-sdk symbol that could disappear on a patch release and silently break
status-filter lowering. Derive the 0.3 wire string from the public
protobuf enum name instead (TASK_STATE_<NAME> maps to the 0.3 value once
the prefix is dropped and underscores become dashes) and validate the
result against the 0.3 TaskState enum's own values via a fully-typed pure
helper. Behavior is unchanged for every state; unspecified or unrecognized
states still drop the filter. Adds parametrized regression tests covering
dashed wire values (input-required, auth-required) and the unspecified drop.

* fix(a2a): drop redundant push-notification envelope key; unify MessageToDict import

_flatten_create_push_notification_params used `config or pushNotificationConfig`,
which short-circuits so a co-present pushNotificationConfig key was never popped and
leaked into the flattened params. Pop both keys unconditionally and prefer config
when present. Adds a regression test on the helper that fails on the old leak.

Also import MessageToDict from a2a.compat.v0_3.conversions in _lower_list_tasks_params
to match every other conversion helper in the module instead of pulling it straight
from google.protobuf.json_format.

* fix(a2a): reject invalid message/stream params early with -32602

_handle_stream_message built MessageSendParams lazily inside the
stream_response() generator, so malformed 1.0 params surfaced as a generic
-32603 after the 200 status line was already committed. The non-streaming
path validates up front and returns -32602 (Invalid params). Validate
eagerly before returning the StreamingResponse and emit -32602 on failure
so both paths reject malformed params identically. Adds a regression test
asserting the streamed error code is -32602.

* fix(a2a): raise clear error when non-streaming send ends on an update event

_send_message fed the SDK iterator's last event straight into
SendMessageSuccessResponse, whose result only accepts Message or Task. A
non-standard upstream whose final event is a TaskStatusUpdateEvent or
TaskArtifactUpdateEvent made the response construction raise an opaque
pydantic ValidationError. Guard the converted result and raise a clear
RuntimeError instead, consistent with the no-response guard above it.
Adds regression tests for the Message happy path and the update-event
rejection via an injected fake client.

* test(a2a): lock in clean merged agent-card URL without PROXY_BASE_URL

Regression coverage proving _build_merged_agent_card produces no double
slash in supportedInterfaces[0].url when PROXY_BASE_URL is unset and
request.base_url carries a trailing slash. get_custom_url routes through
join_paths, which rstrips the base, so the f-string join stays clean.

* style(a2a): modernize type annotations to satisfy strict ruff budget

After merging the black->ruff-format migration from base, the A2A files
owned by this PR still used Optional[X]/quoted annotations that pushed
UP037/UP045 over their lowered ceilings. Convert to X | None, drop the
now-unnecessary quoted local annotation in _send_message, and remove the
imports left unused by the rewrite. Type semantics are unchanged.

* style(a2a): type a2a_endpoints dict params as dict[str, Any]

The merge with the formatter-migration baseline tightened the
reportUnknownArgumentType ceiling; bare dict annotations made every value
Unknown and pushed the codebase total over cap. Annotate the JSON-RPC
params, body, metadata, and litellm_params dicts as dict[str, Any] so
their values are typed, dropping the unknown-argument count back under the
ceiling. No behavior change.

* fix(a2a): guard localhost retry against a missing agent card

handle_a2a_localhost_retry rewrote the card URL and called create_client
with whatever agent_card it received. The caller resolves the card from
the SDK client (Optional), so a None card reached set_agent_card_url and
create_client, surfacing an opaque SDK error instead of a clear one. Add
an early RuntimeError guard mirroring the httpx-client check, drop the now
always-true card None-check on the stash line, and cover it with a
regression test.

* style(a2a): disable reportUnknownArgumentType in a2a-sdk boundary modules

The lint env type-checks without the optional a2a-sdk/protobuf installed, so
every call into the protobuf-generated compat conversions counts as an
Unknown-typed argument and the new A2A code pushed the codebase
reportUnknownArgumentType total over its ceiling. These three modules are
the A2A SDK boundary; turn the rule off file-wide with a documented reason
instead of scattering dozens of per-line ignores across every SDK call.

* fix(a2a): tolerate unknown fields when lowering 1.0->0.3; align streaming trace id

Two issues greptile flagged:

version_convert: the 1.0->0.3 lowering paths (_send_result_to, _task_to,
_stream_result_to) called ParseDict without ignore_unknown_fields=True, so a
1.0 upstream response carrying vendor extensions raised and best-effort fell
back to passing the un-lowered 1.0 shape to a 0.3 client. Set the flag to match
the agent-card path and every inbound path; unknown fields are now dropped and
the result is correctly lowered.

main.py: asend_message_streaming derived X-LiteLLM-Trace-Id from the JSON-RPC
request id, unlike asend_message which uses the logging object's
litellm_trace_id. Prefer the logging trace id (then request id, then a uuid) so
streamed and non-streamed calls correlate under the same trace.

Adds regression tests for both, including the stream-event lowering path.

* style(a2a): apply ruff format to a2a protocol and proxy modules

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
…oned format and re-encryption migration (BerriAI#31215)

* feat(proxy): add AES-256-GCM at-rest credential encryption with versioned format and re-encryption migration

* test(proxy): add behavior scenarios for credential migration endpoints

* fix(proxy): scan covered tables in encryption check, fix CI lint and route types

* fix(proxy): migrate callback_settings credentials, clear CI lint/recursion gates, add encryption endpoint+CLI tests

* fix(proxy): correct dry-run/real-run migrated vs residual-legacy counters in config and SSO walkers

* fix(proxy): make callback-vars residual detection gate-independent in encryption check
…del_error_cleanup

chore(router): simplify unknown-model error message construction
chore(ci): re-issue 31566 for full CI run
…0) (BerriAI#31533)

* fix(ui): keep virtual-keys filters across delete and refresh (LIT-4080)

Filtering virtual keys by User ID and then deleting a key reset the filter to show all keys, and re-clicking Fetch did not re-apply it. The page ran two competing fetch paths: useKeys (React Query) fetched the page unfiltered while a separate useFilterLogic hook held its own filteredKeys list and, on any refresh, only re-applied Team and Organization client-side, silently dropping the User ID and Key Alias filters. Delete refreshed through the unfiltered useKeys path, so the filtered view collapsed back to everything

VirtualKeysTable now owns its filter state and feeds every filter (team, organization, key alias, user id, key hash) straight into the useKeys options, so the filters are part of the React Query key. Any refetch or invalidation re-runs the same filtered query, which makes the reset-on-delete bug structurally impossible. Free-text inputs are debounced with @tanstack/react-pacer, sorting and pagination are server-side, and changing a filter or sort resets to page 1

Delete now invalidates keyKeys.lists() from key_info_view, matching the create path, instead of prop-drilling a refetch; the window "storage" refetch effect is removed. The dual-path useFilterLogic hook (and its test) are deleted

Regression coverage: VirtualKeysTable threads an active User ID filter into the useKeys query and clears it on reset, useKeys encodes filter options in its query key so a filter change refetches, and key_info_view invalidates the keys list on delete

* refactor(ui): simplify virtual-keys table data flow

VirtualKeysTable now fetches its own teams and organizations via
useOrganizations and the existing all-teams query instead of taking them
as props, so the prop-drill through UserDashboard and the two page
callers (page.tsx, ApiKeysDashboard) is gone along with their redundant
organization state and fetch

Filter state collapses from a useState plus a useDebouncedState mirror
into a single source whose debounced copy is derived with
useDebouncedValue, and one typed toKeyListFilters adapter maps it to the
key/list query options. Behavior is unchanged; same 300ms debounce and
the same reset timing

The unused onSortChange/currentSort props and their sync effect are
removed since no caller passed them, leaving sorting fully internal

Adds a created_by_user alias-over-email regression test that fails if the
display precedence is swapped

* test(ui): add required last_active to useKeys mock fixtures

The KeyResponse type requires last_active, so the typed mockKeys
fixtures were missing it. Add it so the file type-checks cleanly.

* chore(ui): ratchet lint budgets after virtual-keys refactor

Deleting filter_logic.tsx and simplifying VirtualKeysTable lowered the
no-explicit-any (2026 to 2016) and complexity (128 to 127) counts, so the
eslint-metrics.json baseline was stale and failed the frontend-lint
budget gate. Regenerate it, and drop the now-dead filter_logic.tsx
suppression entry for the file this PR removed.

* fix(ui): show a loading state for data-backed filter dropdowns

The Team ID and Organization ID filters source their options from async
hooks (teams / organizations). While that data was still loading the
dropdowns rendered 'No results found', so they looked empty rather than
loading. Add an opt-in loading flag to FilterOption that the searchable
select surfaces as a spinner and a 'Loading...' empty state, and wire it
from the teams and organizations query loading states. While loading, the
filter no longer caches an empty initial-options list, so the real
options appear once the data arrives.
…rriAI#31638)

* perf(ui): load virtual-keys team filter from the fast v2 endpoint

The virtual-keys table sourced all teams through fetchAllTeams, which
hits the unpaginated /team/list. On a proxy with 125 teams that call
takes ~9.5s, so the Team ID filter and the team-alias/budget columns sat
empty for that whole window. The key list itself does not carry
team_alias or team_max_budget, so the table genuinely needs a team
lookup and cannot just drop the fetch.

Add useAllTeams, which pages the fast /v2/team/list to completion (~0.6s
per 100-team page, so ~1.2s for 125 vs ~9.5s), and point VirtualKeysTable
at it instead of fetchAllTeams. The allTeams shape, the filter searchFn,
the column lookups, and the loading indicator are all unchanged; only the
source endpoint changes. fetchAllTeams stays for its other callers.

* test(ui): tighten team-filter test readability and robustness

Address adversarial review of the added tests. Rename the single-page
useAllTeams test to match what it asserts (one request for a one-page
result) rather than implying it guards the early-return, and drop the
unread, misleading total: 125 from the mock page response. Scope the
created_by alias-over-email assertion to the key's table row so it checks
the visible cell value; the hover popover that also holds the email is
portaled out of the row, so the previous document-wide negative assertion
was relying on antd's lazy popover mounting.

* fix(ui): scope useAllTeams cache by access token

The previous /team/list query keyed on accessToken, so a user switch in
the same SPA session produced a distinct cache entry. useAllTeams dropped
that, so team IDs and aliases could be briefly reused across users until
the staleTime expired. Put accessToken back in the query key to restore
per-identity isolation, and add a regression test that a token switch
triggers a refetch rather than serving the cached list.
…ws tool_calls (BerriAI#31633)

* fix(databricks): split parallel tool calls so each tool message follows tool_calls

Databricks OpenAI-compatible serving (e.g. GPT models) 400s with "messages with
role 'tool' must be a response to a preceeding message with 'tool_calls'" when an
assistant turn makes parallel tool calls. LiteLLM faithfully sends one assistant
message holding all tool_calls followed by one 'tool' message per result, so every
result after the first is preceded by another 'tool' message rather than the
assistant tool_calls message, which Databricks rejects.

Re-emit each result immediately after an assistant message that carries only its
matching tool_call, turning assistant(tool_calls=[A, B]), tool(A), tool(B) into
assistant(tool_calls=[A]), tool(A), assistant(tool_calls=[B]), tool(B). The
rewrite is a no-op when the turn is already valid (single call), the group is
incomplete, or ids don't line up, so no tool call is ever dropped. Scoped to
non-Claude models, matching the existing OpenAI-shaped transformation path.

* style(databricks): use builtin list generics in parallel tool-call split

Switch the List[...] annotations introduced by _split_parallel_tool_calls
to lowercase list[...] so the UP006 strict-rule budget stays within its
ceiling.
…s (VERIA-392) (BerriAI#31469)

/key/generate validated the caller's delegation ceiling against
data.max_budget only. The per-window entries in data.budget_limits
bypassed the check, so a non-admin caller could mint a key whose
1-day window vastly exceeded their own max_budget. The data.permissions
dict also went unvalidated for non-admin callers, so they could
self-grant capabilities like allow_pii_controls (and on Enterprise,
get_spend_routes).

Both gates now live in _common_key_generation_helper, covering
/key/generate and /key/service-account/generate. The existing empty
{} default on permissions still passes for non-admin callers.
…riAI#31604)

increment_spend_counters was parallelized in BerriAI#31578, but the dominant
per-request cost under high concurrency is the pre-call budget enforcement
in common_checks, which still ran a Redis-first get_current_spend per scope
(team, team windows, key windows, org, tag, user, team member, end user)
one sequential await after another inside the auth span.

The per-scope reads target distinct counter keys with no cross-scope
ordering dependency, so they now run concurrently under asyncio.gather.
Key metadata.tags injection still runs before the gather so the tag budget
check sees it, and every scope settles before the first error in
scope-priority order propagates, preserving the previous rejection semantics.

Resolves LIT-4090
…erriAI#31630)

Enforce that every `budget_limits[*].max_budget` is a finite number;
applies to every caller including proxy admin and runs before the
role / ceiling checks. Six parametrized regression tests cover NaN /
+inf / -inf for both non-admin and admin callers.
…erriAI#31631)

Mirror the scalar `max_budget` guard in `_common_key_generation_helper`
for the per-window check: a CLI session token caller (carrying
`max_budget=None`) cannot set `budget_limits` on a personal key. Pass
`team_table` into the helper so it can detect the personal-key shape;
reject before the `delegation_ceiling is None` early return.

Four new regression tests cover the personal-key reject, the team-key
happy path, the team-key over-team-budget path, and the proxy-admin
exemption.
…reuse

Defines the TypedDict mirror of LiteLLM_ObjectPermissionBase under
litellm/types/object_permission.py so SDK-side modules can adopt the
type without violating the SDK-must-not-import-from-proxy layering
rule. litellm/proxy/_types.py re-exports it for existing callers.

Supports the validator-surface retypes in the preceding proxy refactor
commit.
… on large uploads (BerriAI#31653)

* fix(vertex_ai/files): upload batch files in a single media request to fix 499s on large uploads

PR BerriAI#31036 switched the vertex batch file upload from a single GCS media
upload to a chunked resumable session. The resumable path sends the body as
many sequential PUTs, each waiting a full round-trip to GCS before the next,
so a multi-GB upload accumulates hundreds of round-trips and overruns the
client/load-balancer request timeout, surfacing as 499s (client closed
connection) on files as small as 500MB. This was a regression from the
last-known-good commit, where the upload completed as one continuous request.

Revert the batch upload to a single uploadType=media request, but stage the
transformed payload to a temp file first so peak memory stays bounded (the
goal of the resumable rewrite) without the per-chunk round-trips. The temp
file is closed deterministically (TemporaryFile unlinks on close), not left
to the GC. The now-unused resumable chunked-upload plumbing is removed.

Also swap the per-row transform's stdlib json for orjson (parse + serialize),
which is ~4x faster on this hot path; the streaming body now emits compact
orjson bytes.

The request stays synchronous, so the returned file object is real and
POST /v1/batches keeps working immediately against the uploaded object.

Tests: single media request carries the whole payload with a real
Content-Length (no chunked transfer-encoding); failed upload raises; the
staged temp file is closed deterministically; byte-for-byte transform parity.

* test(vertex_ai/files): mock single media upload POST instead of removed resumable method

test_avertex_batch_prediction patched BaseLLMHTTPHandler._aresumable_chunked_upload, which was removed when the batch jsonl upload moved from a chunked resumable GCS session to a single uploadType=media request. Patch the raw httpx.AsyncClient.post that _astage_and_upload_media issues so the real staging, upload and response transform run while the GCS object response is mocked, and assert the media URL and Content-Type.

* fix(vertex_ai/files): forward request timeout to media upload, drop orjson, sort imports

Forward the per-request timeout through _stage_and_upload_media /
_astage_and_upload_media to the GCS POST. Every other upload branch forwards
it; the new media path was dropping it, so a caller-provided timeout was
silently ignored (the files path passes 600s by default, but a custom
request_timeout would not have reached this upload). Regression test asserts
the resolved timeout reaches the request (mutation-verified).

Revert the orjson swap in the batch transform: importing orjson at module load
in this core-path file broke `import litellm` on environments without orjson
(the Windows import test). Back to stdlib json; the upload leg dominates large
uploads anyway, so the transform-side win was marginal.

Fix import ordering in llm_http_handler.py (I001) introduced by the new imports.

* fix(vertex_ai/files): stream batch upload to GCS instead of staging to a temp file

Addresses a disk-exhaustion concern: staging the full transformed batch body to
a local temp file before the GCS request meant an authenticated user could fill
the proxy's temp volume with large concurrent uploads (on top of Starlette's
input spool).

GCS's simple/media upload accepts chunked transfer-encoding, so stream the
transform straight to the single media request instead. Each block is produced
on a worker thread (the transform never runs on the event loop) and sent
chunked, so the body is neither buffered in memory nor written to disk, and the
upload is still one continuous request (no per-chunk round-trips, no 499). Drops
the temp-file staging, the tempfile/IO imports, and Content-Length computation.

Regression test asserts the upload streams (chunked transfer-encoding, no
Content-Length) and creates no temp file; mutation-verified that reintroducing
staging fails it.
…I#31227)

* fix(proxy): count only active users toward license seat limit

SCIM-deactivated users (metadata.scim_active == false) are kept in LiteLLM_UserTable for audit and reactivation, but they were still counted toward the per-user license limit, so deactivating a user never freed a seat. Okta never sends a SCIM DELETE and Entra only hard-deletes well after deactivation, so deactivation has to be what frees the seat

Add UserRepository.count_billable_users(), which counts every row except those where metadata.scim_active is false (absent, null, and true all count), and route the user-create license gate, the free-SSO 5-user cap, and the enterprise /user/available_users display through it. A separate litellm_active_users Prometheus gauge reports the billable count while litellm_total_users keeps its original meaning so existing dashboards are unaffected

* fix(proxy): floor billable user count at zero

count_billable_users() runs two separate count queries (total, then deactivated). Under a burst of deactivations between them, the deactivated count can momentarily exceed the earlier total and produce a negative result, which would flow into is_over_limit as a negative and show a negative seat count in the display and gauge. Clamp the result to zero so a transient race can never yield a nonsensical negative; the value self-corrects on the next call

Addresses Greptile P1 on the PR

* refactor(proxy): count teams via TeamRepository in available_users

* style: ruff format changed files at line-length 120
…n endpoint (BerriAI#31657)

* fix(mcp): resolve per-user OAuth identity authoritatively at the token endpoint

The OAuth token endpoint stored a user's per-server token under the identity
returned by _extract_user_id_from_request, which read only the Authorization
header and did getattr(cached, "user_id") on a raw user_api_key_cache lookup
with no model_type rehydration and no DB fallback. That silently returned None
in two common cases on a multi-replica gateway: the LiteLLM key arrives on
x-litellm-api-key (what MCP clients such as Claude Desktop and Claude Code
send) rather than Authorization, and a cross-replica cache hit deserializes to
a plain dict rather than a UserAPIKeyAuth, so getattr finds no attribute. When
it returned None the token was not persisted.

This was survivable until the authorization_code v2 migration began stripping
the caller's Authorization for migrated per-user OAuth servers and routing the
preemptive 401 existence check through the stored token, so a persist miss now
hard-fails: the egress challenges with 401 on every reconnect (the client sees
"rejected them on reconnect" or a successful connect with zero tools).

Resolve identity through get_key_object, the canonical resolver that reads the
cache with model_type and falls back to the DB, and accept the key from
x-litellm-api-key as well as Authorization. The silent persist skip is now a
warning. The caller-Authorization stripping stays as is, since reinstating it
would reopen the cross-user credential override it was added to prevent.

* fix(mcp): reject blocked or expired keys when resolving the token-endpoint identity

The OAuth token endpoint is unauthenticated, and get_key_object resolves a key row without the
blocked/expiry checks the main user_api_key_auth pipeline runs (that pipeline is bypassed here). So
a holder of a revoked or expired LiteLLM key could POST a valid upstream authorization code with
that key in x-litellm-api-key/Authorization and write or overwrite the stored per-user OAuth token
for that key's user. The cache-only resolver this replaced incidentally dropped blocked keys
(blocking purges the cache entry), so moving to the authoritative cache-then-DB resolution removed
that accidental shield.

Validate the resolved key before trusting its identity: return None when blocked or expired, so the
upsert is skipped. Deleted keys are already rejected, since get_key_object raises on a missing row.
Regression tests cover the blocked and expired cases and fail without the guard.
…nal_key_metadata

fix(proxy): reject team-scoped object_permission on personal keys for non-admins
…es (BerriAI#31645)

* fix(passthrough): drop top-level additional_drop_params on /v1/messages

On the Anthropic Messages pass-through path, additional_drop_params only
stripped nested dotted paths, so plain top-level keys like `thinking` and
`context_management` were forwarded to the provider. Bedrock rejects these
with "Extra inputs are not permitted", returning a 400 to Claude App/CLI
even when the user configured `additional_drop_params: ["thinking"]`.

delete_nested_value already handles plain top-level fields, so route every
drop param through it and remove the nested-only filter. Fixes BerriAI#25931.

* fix(passthrough): drop thinking for bedrock inference-profile ARNs on /v1/messages

Opaque Bedrock Application Inference Profile ARNs contain neither "anthropic"
nor "claude", so is_anthropic_claude_model returned False and the thinking
param was rewritten to reasoning_effort before additional_drop_params ran.
That made additional_drop_params: ["thinking"] a no-op for the converse-ARN
form, and the Bedrock Converse transform re-expanded reasoning_effort back into
additionalModelRequestFields.thinking, so the request 400'd.

Extend the thinking-translation gates to also accept bedrock ARNs via the
existing is_bedrock_arn_model helper, mirroring the cache_control path, so
thinking is preserved as thinking and additional_drop_params can drop it.
…erriAI#31655)

The Model Armor guardrail only sent text extracted from user messages to
sanitizeUserPrompt, so harmful content inside attached PDFs, Office docs,
and CSVs reached the LLM unscanned. A file-only message had no extractable
text, so the pre-call and moderation hooks returned early and the document
was never submitted to Model Armor at all.

Wire inline document/file scanning into async_pre_call_hook and
async_moderation_hook. extract_file_attachments walks message content blocks
(OpenAI type:file file_data and Anthropic type:document source), decodes the
base64 bytes, maps the MIME type to a Model Armor byteDataType, and skips
remote URLs, bare file_id references, oversize files past the 4 MB limit, and
unsupported types. Each attachment is sent through the byte API and a
MATCH_FOUND blocks the request before it reaches the LLM.

Resolves LIT-4084
…31668)

* fix: skip health check for semantic auto_router deployments

auto_router/<name> deployments are semantic meta-routers that select among
real LLM deployments at request time. They have no LLM endpoint to probe.
The health check was passing model=auto_router/router_1 to get_llm_provider(),
which raised BadRequestError: "Unmapped LLM provider for this endpoint" because
auto_router is not a real LLM provider, causing these deployments to always
appear unhealthy and curl requests to hang.

Detect semantic auto_router deployments in _run_model_health_check and return
{} (healthy) without calling litellm.ahealth_check. Sub-strategies
(complexity_router, adaptive_router, quality_router) are excluded from this
fast path and continue to be health-checked normally.

* ci: trigger circleci
…request (LIT-3858) (BerriAI#31663)

* fix(bedrock): drop unmappable Responses tools instead of failing the request (LIT-3858)

When an OpenAI Responses request is routed to a Bedrock Converse Anthropic model,
litellm translates the tools array into Bedrock toolConfig. Responses built-in tool
types beyond function (web_search, image_generation, namespace, tool_search, custom)
have no Bedrock equivalent, and previously caused two failures.

A web_search tool is derived into a web_search_options param. Bedrock Anthropic
models do not list web_search_options in get_supported_openai_params, so the request
raised UnsupportedParamsError (HTTP 400) even though it never needed web search. The
derived param is now dropped on the Bedrock chat-completion bridge for models that
do not support it, scoped to Bedrock so other providers are untouched and without
requiring drop_params. Nova still keeps it since it maps to a nova_grounding systemTool.

The remaining non-function tools reached _bedrock_tools_pt and were emitted as junk
litellm_unnamed_tool_N toolSpecs with empty schemas, polluting toolConfig with tools
the model could hallucinate calls to. They are now dropped because they carry neither
an OpenAI function nor an Anthropic input_schema, while mappable function and
input_schema tools survive untouched.

* refactor(responses): drop derived web_search_options via provider config

Greptile flagged that the LIT-3858 fix put Bedrock-specific logic in the
generic Responses->Chat Completion bridge: it imported AmazonConverseConfig
and branched on custom_llm_provider.startswith("bedrock").

Read web_search_options support from each provider's own
get_supported_openai_params instead, so the bridge stays provider-agnostic
and Bedrock capability knowledge lives in the Bedrock config that already
owns it. Behavior is unchanged for the cases the PR targeted (Bedrock
Anthropic drops, Bedrock Nova and OpenAI keep) and now generalizes correctly
to any provider whose config does not support the derived param.

Add a Cohere regression test proving the drop is provider-agnostic; it fails
under the old bedrock-only check and passes now.

* fix(responses): drop derived web_search_options for bedrock_converse alias

Greptile/T-Rex caught that the provider-agnostic drop regressed the
bedrock_converse route: get_supported_openai_params did not map the
bedrock_converse alias (only "bedrock"), so it returned None (unmapped) and
the derived web_search_options was forwarded for
model="bedrock/converse/us.anthropic.claude-sonnet-4-6",
custom_llm_provider="bedrock_converse" instead of being dropped. The previous
startswith("bedrock") check happened to match the alias.

Map bedrock_converse through AmazonConverseConfig in get_supported_openai_params,
mirroring the existing ["bedrock", "bedrock_converse"] pairing in
_strip_model_name. Add regression tests at both levels: the alias now drops the
derived param for Anthropic Converse models, still keeps it for Nova, and the
helper resolves identically to "bedrock".
mateo-di added 2 commits July 16, 2026 23:21
…ession storage

The PR #121 conflict resolution took upstream's streaming_iterator.py
__init__ (which has no litellm_completion_request param) while git
auto-merged handler.py cleanly, keeping CARTO's call sites that pass
litellm_completion_request= (PR #16, Redis session storage). Result:
every streaming Responses API request raised

  TypeError: LiteLLMCompletionStreamingIterator.__init__() got an
  unexpected keyword argument 'litellm_completion_request'

(3,079 occurrences in the cloud-native integration run for PR BerriAI#26437;
36 AI integration tests failed). The method body _store_session_in_redis
also reads self.litellm_completion_request, which was never assigned.

Restore the param and the attribute assignment exactly as on carto/main
before the sync (7244577), with CARTO markers so future syncs see it.
tests/litellm/responses/test_streaming_iterator_tool_calls.py (from
PR #68, OCI Gemini tool-call UUID fallback) tested iterator internals
that upstream v1.92.0 removed: _handle_tool_call_delta and
_emit_function_call_done_events no longer exist, so all 8 tests fail
with AttributeError.

The behavior itself is preserved upstream with a better implementation:
litellm/llms/oci/chat/generic.py now synthesizes deterministic
content-derived tool-call ids (_synthesize_oci_tool_call_id) whenever
OCI omits them, and the iterator routes id-less deltas via
index-to-id mapping (_queue_tool_call_delta_events). Covered by
tests/test_litellm/llms/oci/ and
tests/test_litellm/responses/litellm_completion_transformation/
test_tool_call_streaming_transformation.py (375 tests passing).

The carto-features.yml manifest was already updated to the new
patterns during the PR #121 conflict resolution.
@mateo-di

Copy link
Copy Markdown
Collaborator

Integration-test failure root cause + fix (from cloud-native PR #26437 AI test failures):

The conflict resolution took upstream's streaming_iterator.py __init__ (no litellm_completion_request param) while git auto-merged handler.py cleanly, keeping CARTO's call sites that pass that kwarg (PR #16, Redis session storage). Every streaming request failed with TypeError: LiteLLMCompletionStreamingIterator.__init__() got an unexpected keyword argument 'litellm_completion_request' (3,079 occurrences in the integration run; 36 AI tests failed).

Pushed two commits:

  • 1c1f2a07ed restores the param + attribute wiring (matches pre-sync carto/main)
  • 63229aef97 removes tests/litellm/responses/test_streaming_iterator_tool_calls.py — stale CARTO tests targeting internals upstream removed in v1.92.0; the behavior (OCI tool-call ID fallback, PR fix(oci): Add tool calling support for OCI Gemini streaming #68) is preserved upstream via _synthesize_oci_tool_call_id in litellm/llms/oci/chat/generic.py with existing upstream test coverage

Audit of all 11 manifest features against the branch: all verification patterns pass, and the targeted unit suites (responses transformation, chunk builder, snowflake, databricks, oci, azure) pass locally: 971 passed, 0 failed. The Redis wiring break was the only production-code regression.

@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 0
Customized Upstream 2
Preserved CARTO 15
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

…andler

Second dropped wiring from the PR #121 conflict resolution, subtler than
the first: async_responses_api_session_handler lost the CARTO PATCH that
reads sessions from Redis before the DB-backed store (PR #16). Both Redis
helpers survived the merge, so manifest pattern greps passed, but
_patch_get_session_from_redis was left orphaned - defined with zero call
sites. Session writes (restored in 1c1f2a0) landed in Redis while
reads went to the batch-delayed SpendLogs store, so immediate follow-up
turns lost their conversation history.

Observed blast radius in cloud-native PR BerriAI#26437 integration rerun:
conversations answered with other conversations' content, ai-api agent
loops spun (4,333 /responses calls in 13 minutes, ~25/min is normal),
and the CI org burned through its 10M AI quota mid-run.

Restore the Redis-first read exactly as on carto/main pre-sync, and add
regression tests pinning the read-before-DB wiring so an orphaned helper
can't silently pass future syncs.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 0
Customized Upstream 2
Preserved CARTO 9
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

…e Redis store keys

Two changes closing the remaining session-context regression from the
v1.92.0 sync (integration rerun on cloud-native PR BerriAI#26437 still showed
multi-turn context loss: "assistant failed to recall the previous
instruction", agent loops, 3,259 /responses calls):

1. Replace the earlier condensed restoration of the Redis-first session
   lookup with carto/main's exact block, byte-identical including its
   verbose_logger.debug lines (verified with diff against 7244577).
   Verbatim over rewrite - the debug lines also make CI diagnosable.

2. Key Redis session stores by the DECODED response id. This bug cannot
   be fixed by copying carto/main code because it does not exist at
   v1.83.14: upstream v1.92.0 introduced b64-encoded response ids
   (response.completed carries the encoded id) while previous_response_id
   is decoded in responses/utils.py before reaching the session handler.
   carto/main's code keyed stores by response.id, which at v1.83.14 was
   the raw provider id (store == lookup); on v1.92.0 the same code keys
   by the encoded id and the decoded lookup misses 100% of the time.
   Verified empirically by driving the streaming iterator on both
   versions. Decode at the two store call sites; the CARTO Redis helpers
   themselves remain byte-identical to carto/main.

Adds a regression test driving the real streaming iterator and asserting
the stored key equals the decoded form of the client-visible response id.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 2
Customized Upstream 1
Preserved CARTO 8
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

…hunks

SnowflakeStreamingHandler._parse_anthropic_chunk set function["name"] to
the cached tool name on every input_json_delta continuation chunk, not
just the content_block_start chunk. OpenAI-compatible clients accumulate
tool_call name by index across chunks, so the repeated name concatenates
into e.g. "set_layer_styleset_layer_style..." and the tool is no longer
recognized by the caller.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 2
Customized Upstream 3
Preserved CARTO 6
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

handle_generic_stream_chunk never set an index on tool-call deltas, so
every concurrent tool call in one agent turn defaulted to index 0.
litellm's stream_chunk_builder accumulates streaming tool calls by
index, so a second tool call in the same turn silently overwrote the
first in the final reconstructed message instead of appearing
alongside it. Assign the index by first-seen tool-call id, persisted
across chunks on OCIStreamWrapper the same way the existing Cohere
dedup state is threaded through.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 0
Customized Upstream 7
Preserved CARTO 4
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

… turns

Cortex's Anthropic-compatible /messages endpoint rejects unknown fields
inside text content blocks:
  messages.N.content.0.text.annotations: Extra inputs are not permitted

The OpenAI Agents SDK attaches an `annotations` field to text blocks when
replaying a prior assistant turn, and that field rides straight through
every follow-up-turn request. This is the same class of gap the v1.92.0
rewrite introduced in the two prior fixes: a CARTO customization for this
exact problem (_strip_openai_annotations) still existed in the file but
was never wired into the new Anthropic-format message builder. Restored
it as a content-level, non-mutating helper and call it from both plain
message pass-through branches in _extract_system_and_messages.
…ll site

The prior pattern set (def _content_to_text_blocks, def _strip_openai_
annotations, content_blocks) only proved those symbols existed in the
file, not that they were reachable. The resolver weakened the original
text_prelude_blocks pattern to content_blocks -- a name that also
appears in unrelated new code -- specifically to make this check pass
after the v1.92.0 rewrite silently dropped the annotation-stripping
call site. That's why CI stayed green through the regression this PR
fixed. Verification now greps for the actual call site.

Also narrows the description to what this entry actually verifies
today (annotation stripping only) rather than the original PR's full
scope, since the consecutive-assistant-turn merge is not currently
implemented against the new message builder.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

⚠️ CARTO Feature Analysis - Attention Needed

Decision Count
Upstream Substitutes 2
Customized Upstream 4
Preserved CARTO 6
Incorrectly Dropped 0

Overall Assessment: PASS with one item requiring verification. All 12 CARTO features have either been preserved, adapted to the new architecture, or superseded by equivalent upstream functionality. The _content_to_text_string function needs verification to confirm the /messages endpoint accepts array content

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

…lls order

OCI GENERIC validates tool results positionally against the assistant
message's toolCalls, rejecting out-of-order results with "Invalid
parameter: 'toolCallId' of '<id>' not found in 'toolCalls' of previous
message" even though the id is present in that message. Models that
issue parallel tool calls (xAI Grok) get their results back in
completion order, which need not match the emission order, so every
multi-call turn 400s. Confirmed against a captured outbound request:
assistant toolCalls [id-0, id-1] followed by TOOL(id-1), TOOL(id-0)
is rejected on id-1.

Reorder each run of consecutive TOOL messages to the preceding
assistant's toolCalls order in the GENERIC adapter; results are keyed
by toolCallId everywhere else, so the reorder is semantically neutral.
Unknown ids keep arrival order after the known ones.
@Cartofante

Copy link
Copy Markdown
Collaborator Author

📊 CARTO Feature Analysis Started

Mode: Analysis + Auto-fix if issues found

Analyzing how each CARTO customization was handled during conflict resolution.
This will explain WHY each decision was made.

Step Status
Extract CARTO PRs ⏳ In progress
Compare code versions ⏳ Pending
Analyze decisions ⏳ Pending
Generate report ⏳ Pending

View workflow →

@Cartofante

Copy link
Copy Markdown
Collaborator Author

✅ CARTO Feature Analysis Complete

Decision Count
Upstream Substitutes 2
Customized Upstream 2
Preserved CARTO 8
Incorrectly Dropped 0

Overall Assessment: PASS

📋 Full details in PR description above.


View workflow run → | Download analysis artifacts →

@mateo-di mateo-di left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Result: success. Merging and creating new release.

@mateo-di
mateo-di merged commit 358540b into carto/main Aug 4, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated ignore-semantic-pull-request Exempt from Conventional PR Title check (e.g. auto-generated upstream-sync PRs) sync-ready upstream-sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.