Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/dreamy-lovelace-4a90c8

# Conflicts:
#	ui/litellm-dashboard/vitest.config.ts
This commit is contained in:
Yuneng Jiang 2026-07-03 14:19:48 -07:00
commit fc17ea3409
No known key found for this signature in database
6 changed files with 237 additions and 35 deletions

View file

@ -2629,7 +2629,7 @@ jobs:
cd ui/litellm-dashboard
CI=true npm run test -- --run \
--pool forks --poolOptions.forks.maxForks=8
--pool forks --poolOptions.forks.maxForks=6
e2e_ui_testing:
docker:

View file

@ -4,7 +4,7 @@
## Linear ticket
<!-- if you are an internal contributor (e.g., your username is postfixed with -berri or -berriai), add "Resolves " followed by the Linear ticket e.g., "Resolves LIT-1234" to magically link the Linear ticket to the GitHub PR -->
<!-- if you are an internal contributor, add "Resolves " followed by the Linear ticket e.g., "Resolves LIT-1234" to link the Linear ticket to the GitHub PR. If you don't have one, leave the section blank rather than guessing -->
## Pre-Submission checklist

View file

@ -21,7 +21,9 @@ End-to-end tests belong in `tests/e2e/` and must follow the harness conventions
When creating PRs, don't set base to `main`. `litellm_internal_staging` serves that purpose
Always use @.github/pull_request_template.md as a guide for your PR body
When writing a PR body, treat the comments and imperative instructions inside @.github/pull_request_template.md as rules to follow, not just layout
If you're resolving a linear ticket, in the "## Linear ticket" section of the PR, say "Resolves LIT-1234", replacing "LIT-1234" with the actual ticket id that you're resolving. If you don't have the ticket id, don't make one up or search for it. Just leave the section blank
Never use `pytest` commands or the like as "Screenshots / Proof of Fix". We prefer curl'ing a live proxy instance running on localhost:4000 (I like to run it with `python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log`) and showing both the command run and the output. Also, it should hit real LLM provider APIs, not mocks, and cost real $$$ because that is the most realistic test. The proof of fix should be exactly what the end user / customer would see / do. The run logs in PR #27703 is a prime example of how to do it (not a huge fan of using a python test script that future me and the team will have no visibility into; I prefer just curl commands or a short list of bash commands (e.g., using `for`)). If it's a UI thing, just tell me which URLs to go to (e.g., http://localhost:4000/ui/?page=logs), where to click, what fields to fill out, etc. along with the other commands to run in an ordered list, and I'll do it myself and post the screenshots after you make the PR

View file

@ -78,7 +78,7 @@ async def _prepare_context_managed_request(
system: Optional[Any],
context_management_spec: Any,
litellm_metadata: Optional[Dict],
drop_params: Optional[bool],
additional_drop_params: Optional[list[str]],
llm_router: Any,
user_api_key_auth: Any = None,
) -> Optional[PolyfillResult]:
@ -95,7 +95,7 @@ async def _prepare_context_managed_request(
# silently drop intermediate turns.
polyfill_will_run = _polyfill_will_run(
context_management_spec=context_management_spec,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
)
if polyfill_will_run:
@ -117,7 +117,7 @@ async def _prepare_context_managed_request(
system=working_system,
context_management_spec=context_management_spec,
litellm_metadata=litellm_metadata,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
llm_router=llm_router,
user_api_key_auth=user_api_key_auth,
)
@ -143,18 +143,19 @@ async def _prepare_context_managed_request(
def _polyfill_will_run(
*,
context_management_spec: Any,
drop_params: Optional[bool],
additional_drop_params: Optional[list[str]],
) -> bool:
"""Return True when ``compact_20260112`` will run via the polyfill dispatcher.
Mirrors the gating in ``_run_polyfill_if_enabled``: an empty spec or
effective ``drop_params`` short-circuits the polyfill. The pre-processing
skip only applies when the dispatcher will actually invoke
``apply_compact_20260112`` (which has its own compaction-block slicing).
Mirrors the gating in ``_run_polyfill_if_enabled``: an empty spec or an
explicit ``context_management`` entry in ``additional_drop_params``
short-circuits the polyfill. The pre-processing skip only applies when the
dispatcher will actually invoke ``apply_compact_20260112`` (which has its
own compaction-block slicing).
"""
edits = _normalize_spec_edits(
context_management_spec=context_management_spec,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
)
if edits is None:
return False
@ -169,7 +170,7 @@ def _polyfill_will_run(
def _spec_has_non_compact_edits(
*,
context_management_spec: Any,
drop_params: Optional[bool],
additional_drop_params: Optional[list[str]],
) -> bool:
"""Return True when the spec includes edits other than ``compact_20260112``.
@ -180,7 +181,7 @@ def _spec_has_non_compact_edits(
"""
edits = _normalize_spec_edits(
context_management_spec=context_management_spec,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
)
if edits is None:
return False
@ -195,10 +196,22 @@ def _spec_has_non_compact_edits(
)
def _context_management_explicitly_dropped(additional_drop_params: Optional[list[str]]) -> bool:
"""True when the caller opted out of context_management via ``additional_drop_params``.
``drop_params`` deliberately does NOT gate the polyfill: ``context_management``
is a LiteLLM-supported param (native on Anthropic, polyfilled elsewhere), and
``drop_params`` only exists to drop genuinely unsupported params.
"""
if not isinstance(additional_drop_params, list):
return False
return "context_management" in additional_drop_params
def _normalize_spec_edits(
*,
context_management_spec: Any,
drop_params: Optional[bool],
additional_drop_params: Optional[list[str]],
) -> Optional[List[Dict[str, Any]]]:
"""Return the normalized ``edits`` list, or ``None`` if the polyfill won't run.
@ -208,8 +221,7 @@ def _normalize_spec_edits(
if not context_management_spec:
return None
effective_drop_params = drop_params if drop_params is not None else litellm.drop_params
if effective_drop_params:
if _context_management_explicitly_dropped(additional_drop_params):
return None
from litellm.llms.anthropic.experimental_pass_through.context_management.dispatcher import (
@ -230,22 +242,23 @@ async def _run_polyfill_if_enabled(
system: Optional[Any],
context_management_spec: Any,
litellm_metadata: Optional[Dict],
drop_params: Optional[bool],
additional_drop_params: Optional[list[str]],
llm_router: Any,
user_api_key_auth: Any = None,
) -> Optional[PolyfillResult]:
"""Run the async context_management polyfill if a spec is present.
Returns ``None`` when the spec is empty or drop_params is on. Raises
``AnthropicContextManagementError`` so the /v1/messages endpoint can
emit an Anthropic-format 400. All other exceptions are best-effort
swallowed (matches v0 behavior).
Returns ``None`` when the spec is empty or ``context_management`` is
listed in ``additional_drop_params`` (the explicit opt-out; ``drop_params``
does not disable the polyfill because context_management is a supported
param). Raises ``AnthropicContextManagementError`` so the /v1/messages
endpoint can emit an Anthropic-format 400. All other exceptions are
best-effort swallowed (matches v0 behavior).
"""
if not context_management_spec:
return None
effective_drop_params = drop_params if drop_params is not None else litellm.drop_params
if effective_drop_params:
if _context_management_explicitly_dropped(additional_drop_params):
return None
try:
@ -274,7 +287,7 @@ async def _run_polyfill_if_enabled(
# emits an Anthropic-format error.
if _spec_has_non_compact_edits(
context_management_spec=context_management_spec,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
):
raise AnthropicContextManagementError(
status_code=500,
@ -533,7 +546,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
) -> Union[AnthropicMessagesResponse, AsyncIterator[Any], Iterator[bytes]]:
"""Handle non-Anthropic models asynchronously using the adapter"""
context_management = kwargs.pop("context_management", None)
drop_params: Optional[bool] = kwargs.get("drop_params", None)
additional_drop_params: Optional[list[str]] = kwargs.get("additional_drop_params", None)
litellm_router = kwargs.pop("litellm_router", None)
if litellm_router is None:
try:
@ -555,7 +568,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
system=system,
context_management_spec=context_management,
litellm_metadata=proxy_litellm_metadata,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
llm_router=litellm_router,
user_api_key_auth=user_api_key_auth,
)
@ -661,7 +674,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
# ``compact_20260112`` editor can ``await`` the summarization model);
# bridge to it via ``run_async_function``.
context_management = kwargs.pop("context_management", None)
drop_params: Optional[bool] = kwargs.get("drop_params", None)
additional_drop_params: Optional[list[str]] = kwargs.get("additional_drop_params", None)
# Deliberately do NOT auto-attach the proxy ``llm_router`` here:
# ``run_async_function`` spawns a new event loop in a worker thread
# to bridge to the async dispatcher, but the proxy router's httpx
@ -696,7 +709,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
system=system,
context_management_spec=context_management,
litellm_metadata=proxy_litellm_metadata,
drop_params=drop_params,
additional_drop_params=additional_drop_params,
llm_router=litellm_router,
user_api_key_auth=user_api_key_auth,
)

View file

@ -12,11 +12,13 @@ Coverage:
- custom instructions default prompt is not used even when tools present
"""
import json
from typing import Any, Dict, List
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import litellm
from litellm.llms.anthropic.experimental_pass_through.context_management import (
AnthropicContextManagementError,
apply_context_management,
@ -2042,12 +2044,12 @@ async def test_dispatcher_trigger_below_minimum_raises_through():
# ---------------------------------------------------------------------------
# _run_polyfill_if_enabled: drop_params gate
# _run_polyfill_if_enabled: additional_drop_params gate (drop_params must NOT gate)
# ---------------------------------------------------------------------------
async def test_run_polyfill_skipped_when_drop_params_true():
"""When drop_params=True the polyfill must be skipped (returns None)."""
async def test_run_polyfill_skipped_when_context_management_in_additional_drop_params():
"""additional_drop_params=["context_management"] is the explicit opt-out."""
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
_run_polyfill_if_enabled,
)
@ -2059,12 +2061,39 @@ async def test_run_polyfill_skipped_when_drop_params_true():
system=None,
context_management_spec={"edits": [{"type": "compact_20260112"}]},
litellm_metadata={},
drop_params=True,
additional_drop_params=["context_management"],
llm_router=None,
)
assert result is None
async def test_run_polyfill_runs_when_litellm_drop_params_true(monkeypatch):
"""drop_params must not disable the polyfill: context_management is a
LiteLLM-supported param (polyfilled where not native), and drop_params only
exists to strip genuinely unsupported params."""
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
_run_polyfill_if_enabled,
)
monkeypatch.setattr(litellm, "drop_params", True)
with patch(
"litellm.llms.anthropic.experimental_pass_through.context_management.editors.compact._read_summary_model_setting",
return_value=None,
):
result = await _run_polyfill_if_enabled(
model=MODEL,
messages=_simple_messages(),
tools=None,
system=None,
context_management_spec={"edits": [{"type": "compact_20260112"}]},
litellm_metadata={},
additional_drop_params=None,
llm_router=None,
)
assert result is not None
assert result.applied_edits[0]["type"] == "compact_20260112"
async def test_run_polyfill_skipped_when_spec_empty():
"""Empty context_management_spec must also return None (no polyfill work)."""
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
@ -2078,12 +2107,169 @@ async def test_run_polyfill_skipped_when_spec_empty():
system=None,
context_management_spec=None,
litellm_metadata={},
drop_params=False,
additional_drop_params=None,
llm_router=None,
)
assert result is None
# ---------------------------------------------------------------------------
# Adapter handler entry points: polyfill vs drop_params / additional_drop_params
# ---------------------------------------------------------------------------
_CLEAR_TOOL_USES_SPEC: Dict[str, Any] = {
"edits": [
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "tool_uses", "value": 1},
"keep": {"type": "tool_uses", "value": 0},
}
]
}
_CLEARED_PLACEHOLDER = "[Cleared by context management]"
def _tool_use_messages() -> List[Dict[str, Any]]:
return [
{"role": "user", "content": "check the weather in two cities"},
{
"role": "assistant",
"content": [{"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"city": "SF"}}],
},
{
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": "toolu_01", "content": "sunny in SF"}],
},
{
"role": "assistant",
"content": [{"type": "tool_use", "id": "toolu_02", "name": "get_weather", "input": {"city": "NY"}}],
},
{
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": "toolu_02", "content": "rainy in NY"}],
},
{"role": "user", "content": "now summarize both"},
]
def _openai_chat_response():
from litellm.types.utils import ModelResponse
return ModelResponse(
id="chatcmpl-test",
model="gpt-4o",
choices=[{"finish_reason": "stop", "index": 0, "message": {"role": "assistant", "content": "done"}}],
usage={"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
)
async def _call_async_adapter_handler(**handler_kwargs: Any):
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
LiteLLMMessagesToCompletionTransformationHandler,
)
captured: Dict[str, Any] = {}
async def _capture_acompletion(**kwargs):
captured.update(kwargs)
return _openai_chat_response()
with patch("litellm.acompletion", side_effect=_capture_acompletion):
response = await LiteLLMMessagesToCompletionTransformationHandler.async_anthropic_messages_handler(
max_tokens=128,
messages=_tool_use_messages(),
model=MODEL,
context_management=_CLEAR_TOOL_USES_SPEC,
litellm_router=MagicMock(),
**handler_kwargs,
)
return response, captured
def _assert_polyfill_applied(response: Any, captured: Dict[str, Any]) -> None:
applied_edits = (response.get("context_management") or {}).get("applied_edits")
assert applied_edits, "polyfill must run and report applied_edits"
assert applied_edits[0]["type"] == "clear_tool_uses_20250919"
forwarded = json.dumps(captured["messages"], default=str)
assert _CLEARED_PLACEHOLDER in forwarded
assert "sunny in SF" not in forwarded
assert "rainy in NY" in forwarded
async def test_async_handler_runs_polyfill_when_request_drop_params_true():
"""Regression (LIT-3768): per-request drop_params=True silently skipped the
polyfill, so Claude Code requests (where the proxy defaults drop_params on)
lost context editing on non-Anthropic models."""
response, captured = await _call_async_adapter_handler(drop_params=True)
_assert_polyfill_applied(response, captured)
async def test_async_handler_runs_polyfill_when_litellm_drop_params_true(monkeypatch):
"""Regression (LIT-3768): proxy-wide litellm.drop_params=True silently
skipped the polyfill too."""
monkeypatch.setattr(litellm, "drop_params", True)
response, captured = await _call_async_adapter_handler()
_assert_polyfill_applied(response, captured)
async def test_async_handler_additional_drop_params_strips_context_management():
"""additional_drop_params=["context_management"] stays the escape hatch:
the polyfill must not run and the request is forwarded untouched."""
response, captured = await _call_async_adapter_handler(additional_drop_params=["context_management"])
assert response.get("context_management") is None
forwarded = json.dumps(captured["messages"], default=str)
assert _CLEARED_PLACEHOLDER not in forwarded
assert "sunny in SF" in forwarded
def _call_sync_adapter_handler(**handler_kwargs: Any):
from litellm.llms.anthropic.experimental_pass_through.adapters.handler import (
LiteLLMMessagesToCompletionTransformationHandler,
)
captured: Dict[str, Any] = {}
def _capture_completion(**kwargs):
captured.update(kwargs)
return _openai_chat_response()
with patch("litellm.completion", side_effect=_capture_completion):
response = LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(
max_tokens=128,
messages=_tool_use_messages(),
model=MODEL,
context_management=_CLEAR_TOOL_USES_SPEC,
litellm_router=None,
**handler_kwargs,
)
return response, captured
def test_sync_handler_runs_polyfill_when_request_drop_params_true():
"""The sync entry point reads its own kwargs; cover its gate separately."""
response, captured = _call_sync_adapter_handler(drop_params=True)
_assert_polyfill_applied(response, captured)
def test_sync_handler_runs_polyfill_when_litellm_drop_params_true(monkeypatch):
"""Proxy-wide litellm.drop_params=True must not skip the polyfill on the
sync entry point either."""
monkeypatch.setattr(litellm, "drop_params", True)
response, captured = _call_sync_adapter_handler()
_assert_polyfill_applied(response, captured)
def test_sync_handler_additional_drop_params_strips_context_management():
"""The additional_drop_params=["context_management"] escape hatch is honored
on the sync entry point too: no polyfill, request forwarded untouched."""
response, captured = _call_sync_adapter_handler(additional_drop_params=["context_management"])
assert response.get("context_management") is None
forwarded = json.dumps(captured["messages"], default=str)
assert _CLEARED_PLACEHOLDER not in forwarded
assert "sunny in SF" in forwarded
async def test_prepare_context_managed_request_forwards_proxy_litellm_metadata():
"""The handler must hand the polyfill the proxy ``litellm_metadata`` (which
carries ``user_api_key`` / ``user_api_key_team_id`` / ...), not the
@ -2120,7 +2306,7 @@ async def test_prepare_context_managed_request_forwards_proxy_litellm_metadata()
"user_api_key_user_id": "user-xyz",
"litellm_call_id": "call-1",
},
drop_params=False,
additional_drop_params=None,
llm_router=_RouterStub(),
)

View file

@ -9,6 +9,7 @@ export default defineConfig({
css: true, // lets you import CSS/modules without extra mocks
testTimeout: 30000,
silent: process.env.CI ? "passed-only" : false,
teardownTimeout: 60000,
coverage: {
provider: "v8",
reporter: ["text", "lcov"],