Litellm internal staging 04 11 2026 - #25562
Conversation
…ming (#25498) * fix(responses): map refusal stop_reason to incomplete status in streaming Fixes streaming responses API translation where Anthropic's stop_reason="refusal" was incorrectly translated to status="completed" instead of "incomplete". Root cause: build_base_response was unconditionally overwriting finish_reason with None from later chunks, losing the terminal content_filter value. Changes: - streaming_chunk_builder_utils: skip None finish_reason values in build_base_response - streaming_iterator: snapshot chunks before returning pending events (sync path) - streaming_handler: treat usage-only chunks as meaningful content - transformation: map finish_reason=refusal to status=incomplete - tests: add regression tests for refusal handling Made-with: Cursor * Fix test
…nputs (#25481) * feat(guardrails): optional skip system message in unified guardrail inputs Made-with: Cursor * feat(dashboard): skip_system_message_in_guardrail in guardrail UI Add a tri-state control (inherit / yes / no) when creating or editing guardrails so admins can set litellm_params.skip_system_message_in_guardrail without YAML. Table edit merges existing litellm_params before PUT to avoid wiping content-filter and other provider fields. Document the dashboard flow in the guardrails quick start with a screenshot. Made-with: Cursor * fix(guardrails): type structured_messages as AllMessageValues for mypy Use AllMessageValues in openai_messages_without_system and cast adapter request messages so GenericGuardrailAPIInputs matches TypedDict. Made-with: Cursor
…25419) When modify_params is true, Bedrock Converse setup no longer prepends or appends the default user message if the boundary assistant turn has prefix: true, so OpenAI-style assistant prefill reaches the API unchanged. Made-with: Cursor
…se parsing (#25287) * feat(containers): Azure container routing, managed IDs, and delete response wire format - Add AzureContainerConfig and safe URL joining for paths with api-version query - Encode/decode managed container IDs in responses, streaming, and proxy handlers - Accept OpenAI delete response object literal container.file.deleted - Tests for Azure URL regression and DeleteContainerFileResponse parsing Made-with: Cursor * fix(responses): gate response id update on parsed_chunk having response Delta stream events do not include a response body; Mock-based tests (and any truthy synthetic .response on transforms) must not trigger _update_responses_api_response_id_with_model_id. Fixes test_stop_async_iteration_not_logged_as_failure (TypeError: Mock not iterable). Made-with: Cursor * feat(containers): encode container IDs in SDK responses for routing - Add ContainerRequestUtils.encode_container_id_in_response utility - Encode container_id in create/retrieve/delete responses (SDK path) - Fix streaming iterator: gate response ID update on parsed_chunk key - Follows responses API pattern (encode after handler, not in handler) Made-with: Cursor * fix(containers): module-level imports and managed cntr_ ID encoding - Move ResponsesAPIRequestUtils imports to module scope (utils, main, handler_factory). - Serialize absent model_id as empty segment instead of literal None; decode empty and legacy "None" segments as missing for router affinity. - Add unit tests for build/decode round-trip and legacy IDs. Made-with: Cursor * fix(containers): decode managed IDs in endpoint_factory SDK path - Add decode_managed_container_id_for_request in containers/utils and reuse from main. - Strip LiteLLM cntr_ wrappers before generic_container_handler (64-char API limit). - Resolve provider for logging/errors; add unit test for decode helper. - Use resolved_custom_llm_provider after decode for mypy-safe provider typing. Made-with: Cursor * Fix p1 concern * Fix p1 concern
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…se traces (#25448) * fix(logging): preserve proxy key-auth metadata on /v1/messages Langfuse traces update_from_kwargs() overwrites proxy metadata (user_api_key_hash, etc.) with Anthropic's native metadata when both exist. Merge instead of replace. * fix(test): update stale assertion for new metadata merge semantics * test: add explicit conflict-resolution test for metadata merge
* feat(prometheus): reduce default latency bucket cardinality and make configurable * test(prometheus): add coverage for PrometheusServicesLogger latency buckets * Revert "test(prometheus): add coverage for PrometheusServicesLogger latency buckets" This reverts commit 1bfd004. * test(prometheus): add coverage for PrometheusServicesLogger latency buckets
…errors (#25530) * fix(s3): add retry with exponential backoff for transient S3 503/500 errors S3 occasionally returns 503 "Slow Down" during PUT operations when request rates spike above partition limits. The current code makes a single upload attempt via httpx — unlike boto3, httpx has no built-in retry for transient S3 errors. Failed uploads permanently lose the request's audit/logging data. Add exponential backoff retry (3 attempts, 1s/2s delays) for S3 500/503 responses in both async_upload_data_to_s3 and upload_data_to_s3. Logs a warning on each retry with the S3 object key for observability. In production we observed ~18 permanent S3 upload failures per day (124 over 7 days) — all transient 503s that would have succeeded on a single retry. * test(s3): add unit tests for S3 upload retry logic Tests cover: - Async retry on 503 (succeeds on second attempt) - Async retry on 500 - Exhausted retries on persistent 503 (calls handle_callback_failure) - No retry on 4xx errors (403) - Sync retry on 503 * style(s3): move time import to module level Address review feedback: move `import time` from inside upload_data_to_s3 to the top-level imports per project style guide.
Greptile SummaryThis staging PR bundles five features: (1) per-guardrail
Confidence Score: 3/5Mostly safe, but the silent LATENCY_BUCKETS default change will break existing Prometheus dashboards and alerts for any user who upgrades without setting prometheus_latency_buckets. One P1 finding: the default Prometheus histogram buckets changed in a backwards-incompatible way. This won't crash the proxy but it breaks the monitoring contract for existing deployments — a hard-to-debug production observability regression. All other changes are well-implemented and well-tested. litellm/types/integrations/prometheus.py — default LATENCY_BUCKETS change; ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx — fullLitellmParams spread may carry over stale provider-specific keys on provider switch.
|
| Filename | Overview |
|---|---|
| litellm/types/integrations/prometheus.py | Default LATENCY_BUCKETS significantly changed (22 buckets removed, 3 added); breaks existing Prometheus dashboards/alerts keyed on removed le labels. |
| litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py | Adds _extract_blocked_assessments to surface structured policy-violation details; enriches HTTPException with guardrailIdentifier, guardrailVersion, and assessments. |
| litellm/proxy/utils.py | Adds guardrail context enrichment (name, mode) to HTTPExceptions raised during all hook phases; wraps all guardrail tasks and streaming iterators with enrichment helpers. |
| litellm/proxy/common_request_processing.py | Fixes dict-detail HTTPException serialization for both streaming and non-streaming proxy surfaces; preserves full guardrail payload as structured fields. |
| litellm/responses/utils.py | Adds container ID encoding/decoding with provider and model metadata; wires into _update_container_ids_in_response for non-streaming responses. |
| litellm/integrations/s3_v2.py | Adds 3-attempt exponential-backoff retry (1s, 2s) for transient S3 500/503 errors in both sync and async upload paths. |
| litellm/llms/base_llm/guardrail_translation/utils.py | New helpers effective_skip_system_message_for_guardrail and openai_messages_without_system extract per-guardrail vs global skip logic cleanly. |
| ui/litellm-dashboard/src/components/guardrails/edit_guardrail_form.tsx | Preserves full stored litellm_params on edit (avoids silent loss of content-filter fields); adds skip_system_message_choice. Stale provider-specific params can survive a provider switch. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Guardrail Request] --> B{during_call_hook}
B --> C[_run_guardrail_task_with_enrichment]
C --> D{Unified guardrail?}
D -- Yes --> E[async_moderation_hook]
D -- No --> F[callback.async_moderation_hook]
E --> G{skip_system_message?}
F --> G
G -- Yes --> H[openai_messages_without_system]
G -- No --> I[Full messages]
H --> J[Apply guardrail]
I --> J
J -- Blocked --> K[HTTPException with dict detail]
K --> L[_enrich_http_exception_with_guardrail_context]
L --> M[ProxyException via _serialize_http_exception_detail]
M --> N[Structured error response]
J -- Allowed --> O[Request proceeds]
Reviews (2): Last reviewed commit: "fix linting" | Re-trigger Greptile
| langfuse_default_tags: Optional[List[str]] = None | ||
| langsmith_batch_size: Optional[int] = None | ||
| prometheus_initialize_budget_metrics: Optional[bool] = False | ||
| prometheus_latency_buckets: Optional[List[float]] = None |
| # Merge metadata carefully — don't overwrite the merged metadata | ||
| # from kwargs/litellm_metadata with the caller's litellm_params metadata. | ||
| # e.g. anthropic_messages passes Anthropic's native metadata ({user_id: ...}) | ||
| # in litellm_params, which would overwrite proxy key-auth fields. | ||
| lp_metadata = litellm_params.pop("metadata", None) | ||
| base_litellm_params.update(litellm_params) | ||
| if lp_metadata and isinstance(lp_metadata, dict): | ||
| base_litellm_params.setdefault("metadata", {}) | ||
| for k, v in lp_metadata.items(): | ||
| if k not in base_litellm_params["metadata"]: | ||
| base_litellm_params["metadata"][k] = v |
There was a problem hiding this comment.
pop() mutates the caller's litellm_params dict
litellm_params.pop("metadata", None) removes the metadata key from the dict that was passed in by the caller. If any upstream caller keeps a reference to the same dict (e.g. for retry logic, follow-up logging, or span attribution), it will silently lose its metadata after this call. CLAUDE.md's "use dict spread for immutable copies" guideline recommends building a new dict instead:
lp_metadata = litellm_params.get("metadata", None)
base_litellm_params.update({k: v for k, v in litellm_params.items() if k != "metadata"})Context Used: CLAUDE.md (source)
| import litellm | ||
|
|
||
| return bool(getattr(litellm, "skip_system_message_in_guardrail", False)) |
There was a problem hiding this comment.
Inline
import litellm inside function body
CLAUDE.md explicitly says: "Avoid imports within methods — place all imports at the top of the file … The only exception is avoiding circular imports where absolutely necessary." The only reason import litellm is deferred here is to avoid a circular import (litellm/__init__.py imports from litellm.llms.*). If that is the intent, a comment explaining the circular-import rationale would prevent future reviewers from "cleaning it up" back to a module-level import.
def effective_skip_system_message_for_guardrail(guardrail_to_apply: Any) -> bool:
per = getattr(guardrail_to_apply, "skip_system_message_in_guardrail", None)
if per is not None:
return bool(per)
# Deferred to avoid circular import (litellm.__init__ → litellm.llms.*)
import litellm # noqa: PLC0415
return bool(getattr(litellm, "skip_system_message_in_guardrail", False))Context Used: CLAUDE.md (source)
| """ | ||
| try: | ||
| # If it doesn't start with cntr_, it's not a managed ID | ||
| if not container_id.startswith("cntr_"): | ||
| return DecodedResponseId( | ||
| custom_llm_provider=None, |
There was a problem hiding this comment.
replace("cntr_", "") strips all occurrences, not just the prefix
container_id.replace("cntr_", "") would remove every occurrence of the literal string in container_id, not only the leading one. Standard base64 (b64encode) cannot produce cntr_ (underscores don't appear in the standard alphabet), so this is safe today, but the intent is clearly prefix removal. Use removeprefix (Python 3.9+) or an explicit slice to make the intent unambiguous:
| """ | |
| try: | |
| # If it doesn't start with cntr_, it's not a managed ID | |
| if not container_id.startswith("cntr_"): | |
| return DecodedResponseId( | |
| custom_llm_provider=None, | |
| cleaned_id = container_id.removeprefix("cntr_") |
…g_04_11_2026 Litellm internal staging 04 11 2026
Relevant issues
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes