From 7b66c970e9706a5b96c1987bc445ff941e51fb66 Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Tue, 17 Mar 2026 03:11:58 +0000 Subject: [PATCH 01/25] fix: auto-recover shared aiohttp session when closed (#23806) When the shared aiohttp session closes (due to network interruption, idle timeout, or Redis failover side effects), the proxy permanently falls back to creating a new HTTPS connection per request, losing the benefit of connection pooling for the entire pod lifetime. Fix: make add_shared_session_to_data() async and recreate the session when it is found closed, restoring connection pooling automatically. Fixes #23806 --- litellm/proxy/route_llm_request.py | 29 ++++-- .../proxy/test_aiohttp_session_recovery.py | 99 +++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 tests/test_litellm/proxy/test_aiohttp_session_recovery.py diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index e5fc9fe76a4..265a311d393 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -123,23 +123,40 @@ def get_team_id_from_data(data: dict) -> Optional[str]: return None -def add_shared_session_to_data(data: dict) -> None: +async def add_shared_session_to_data(data: dict) -> None: """ Add shared aiohttp session for connection reuse (prevents cold starts). + If the session was closed (e.g. due to network interruption or idle timeout), + automatically recreates it so connection pooling is restored. Silently continues without session reuse if import fails or session is unavailable. Args: data: Dictionary to add the shared session to """ try: + import litellm.proxy.proxy_server as proxy_server from litellm._logging import verbose_proxy_logger - from litellm.proxy.proxy_server import shared_aiohttp_session - if shared_aiohttp_session is not None and not shared_aiohttp_session.closed: - data["shared_session"] = shared_aiohttp_session + session = proxy_server.shared_aiohttp_session + + if session is not None and not session.closed: + data["shared_session"] = session verbose_proxy_logger.info( - f"SESSION REUSE: Attached shared aiohttp session to request (ID: {id(shared_aiohttp_session)})" + f"SESSION REUSE: Attached shared aiohttp session to request (ID: {id(session)})" ) + elif session is not None and session.closed: + # Session was created at startup but has since closed — recreate it + verbose_proxy_logger.warning( + f"SESSION REUSE: Shared aiohttp session is closed (ID: {id(session)}), recreating..." + ) + new_session = await proxy_server._initialize_shared_aiohttp_session() + if new_session is not None: + proxy_server.shared_aiohttp_session = new_session + data["shared_session"] = new_session + else: + verbose_proxy_logger.info( + "SESSION REUSE: Failed to recreate shared session, continuing without session reuse" + ) else: verbose_proxy_logger.info( "SESSION REUSE: No shared session available for this request" @@ -248,7 +265,7 @@ async def route_request( # noqa: PLR0915 - Complex routing function, refactorin """ Common helper to route the request """ - add_shared_session_to_data(data) + await add_shared_session_to_data(data) team_id = get_team_id_from_data(data) router_model_names = llm_router.model_names if llm_router is not None else [] diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py new file mode 100644 index 00000000000..e87ca5a1632 --- /dev/null +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -0,0 +1,99 @@ +""" +Tests for shared aiohttp session auto-recovery. + +When the shared session closes (e.g. network interruption, idle timeout), +add_shared_session_to_data should recreate it instead of permanently +falling back to per-request connections. + +Fixes: https://github.com/BerriAI/litellm/issues/23806 +""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.mark.asyncio +async def test_add_shared_session_attaches_open_session(): + """When the shared session is open, it should be attached to data.""" + from litellm.proxy.route_llm_request import add_shared_session_to_data + + mock_session = MagicMock() + mock_session.closed = False + + with patch( + "litellm.proxy.proxy_server.shared_aiohttp_session", mock_session + ): + data = {} + await add_shared_session_to_data(data) + assert data["shared_session"] is mock_session + + +@pytest.mark.asyncio +async def test_add_shared_session_recreates_closed_session(): + """When the shared session is closed, it should be recreated.""" + from litellm.proxy import proxy_server as proxy_server_module + from litellm.proxy.route_llm_request import add_shared_session_to_data + + closed_session = MagicMock() + closed_session.closed = True + + new_session = MagicMock() + new_session.closed = False + + with patch.object( + proxy_server_module, + "shared_aiohttp_session", + closed_session, + ): + with patch.object( + proxy_server_module, + "_initialize_shared_aiohttp_session", + new_callable=AsyncMock, + return_value=new_session, + ) as mock_init: + data = {} + await add_shared_session_to_data(data) + + mock_init.assert_called_once() + assert data["shared_session"] is new_session + assert proxy_server_module.shared_aiohttp_session is new_session + + +@pytest.mark.asyncio +async def test_add_shared_session_handles_recreation_failure(): + """When recreation fails, data should not contain shared_session.""" + from litellm.proxy import proxy_server as proxy_server_module + from litellm.proxy.route_llm_request import add_shared_session_to_data + + closed_session = MagicMock() + closed_session.closed = True + + with patch.object( + proxy_server_module, + "shared_aiohttp_session", + closed_session, + ): + with patch.object( + proxy_server_module, + "_initialize_shared_aiohttp_session", + new_callable=AsyncMock, + return_value=None, + ): + data = {} + await add_shared_session_to_data(data) + assert "shared_session" not in data + + +@pytest.mark.asyncio +async def test_add_shared_session_no_session_available(): + """When no session was ever created, data should not contain shared_session.""" + from litellm.proxy.route_llm_request import add_shared_session_to_data + + with patch( + "litellm.proxy.proxy_server.shared_aiohttp_session", None + ): + data = {} + await add_shared_session_to_data(data) + assert "shared_session" not in data From ab4fda2eebbc6e1dbc772e93d4be8c18b58deb8c Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Tue, 17 Mar 2026 08:08:44 +0000 Subject: [PATCH 02/25] fix: add asyncio.Lock to prevent session/connector leak on concurrent recreation When multiple requests detect a closed shared session simultaneously, they would each create a new aiohttp.ClientSession, leaking intermediate sessions and their TCP connectors. Added double-checked locking pattern with asyncio.Lock to ensure only one coroutine recreates the session. Added concurrent recreation test case. --- litellm/proxy/route_llm_request.py | 45 ++++++++++++---- .../proxy/test_aiohttp_session_recovery.py | 51 +++++++++++++++++++ 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 265a311d393..376bf07f2b5 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -1,3 +1,4 @@ +import asyncio from typing import TYPE_CHECKING, Any, Literal, Optional from fastapi import HTTPException, status @@ -123,11 +124,24 @@ def get_team_id_from_data(data: dict) -> Optional[str]: return None +_shared_session_lock: Optional[asyncio.Lock] = None + + +def _get_shared_session_lock() -> asyncio.Lock: + """Lazily create the shared session lock (must be called within a running event loop).""" + global _shared_session_lock + if _shared_session_lock is None: + _shared_session_lock = asyncio.Lock() + return _shared_session_lock + + async def add_shared_session_to_data(data: dict) -> None: """ Add shared aiohttp session for connection reuse (prevents cold starts). If the session was closed (e.g. due to network interruption or idle timeout), automatically recreates it so connection pooling is restored. + Uses an asyncio.Lock to prevent race conditions where multiple concurrent + requests could each create a new session, leaking intermediate ones. Silently continues without session reuse if import fails or session is unavailable. Args: @@ -146,23 +160,32 @@ async def add_shared_session_to_data(data: dict) -> None: ) elif session is not None and session.closed: # Session was created at startup but has since closed — recreate it - verbose_proxy_logger.warning( - f"SESSION REUSE: Shared aiohttp session is closed (ID: {id(session)}), recreating..." - ) - new_session = await proxy_server._initialize_shared_aiohttp_session() - if new_session is not None: - proxy_server.shared_aiohttp_session = new_session - data["shared_session"] = new_session - else: - verbose_proxy_logger.info( - "SESSION REUSE: Failed to recreate shared session, continuing without session reuse" + # Use lock to prevent concurrent recreation (avoids session/connector leak) + lock = _get_shared_session_lock() + async with lock: + # Double-check under lock — another coroutine may have already recreated it + session = proxy_server.shared_aiohttp_session + if session is not None and not session.closed: + data["shared_session"] = session + return + + verbose_proxy_logger.warning( + f"SESSION REUSE: Shared aiohttp session is closed (ID: {id(session)}), recreating..." ) + new_session = await proxy_server._initialize_shared_aiohttp_session() + if new_session is not None: + proxy_server.shared_aiohttp_session = new_session + data["shared_session"] = new_session + else: + verbose_proxy_logger.info( + "SESSION REUSE: Failed to recreate shared session, continuing without session reuse" + ) else: verbose_proxy_logger.info( "SESSION REUSE: No shared session available for this request" ) except Exception: - # Silently continue without session reuse if import fails or session unavailable + # Silently continue without session reuse if import fails or session is unavailable pass diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index e87ca5a1632..b71a61d2c29 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -97,3 +97,54 @@ async def test_add_shared_session_no_session_available(): data = {} await add_shared_session_to_data(data) assert "shared_session" not in data + + +@pytest.mark.asyncio +async def test_add_shared_session_concurrent_recreation_uses_lock(): + """When multiple coroutines detect a closed session concurrently, + only one should recreate it (double-checked locking via asyncio.Lock).""" + import litellm.proxy.route_llm_request as route_module + from litellm.proxy import proxy_server as proxy_server_module + from litellm.proxy.route_llm_request import add_shared_session_to_data + + # Reset the module-level lock so each test is isolated + route_module._shared_session_lock = None + + closed_session = MagicMock() + closed_session.closed = True + + new_session = MagicMock() + new_session.closed = False + + call_count = 0 + + async def mock_init(): + nonlocal call_count + call_count += 1 + # Simulate some async work + await asyncio.sleep(0.01) + proxy_server_module.shared_aiohttp_session = new_session + return new_session + + with patch.object( + proxy_server_module, + "shared_aiohttp_session", + closed_session, + ): + with patch.object( + proxy_server_module, + "_initialize_shared_aiohttp_session", + side_effect=mock_init, + ): + # Launch 5 concurrent calls + results = [{} for _ in range(5)] + await asyncio.gather( + *(add_shared_session_to_data(d) for d in results) + ) + + # Only 1 coroutine should have called _initialize (the rest see the + # re-checked session as open under the lock) + assert call_count == 1, f"Expected 1 init call, got {call_count}" + # All should have the new session + for d in results: + assert d.get("shared_session") is new_session From 9e09bbc1dfbccc551a8898ab3268a1014c7c81d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= <258577966+voidborne-d@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:54:01 +0000 Subject: [PATCH 03/25] fix: reset _shared_session_lock in all tests for event loop isolation Address Greptile P1 review: tests that exercise the closed-session code path need to reset the module-level lock to avoid RuntimeError on Python < 3.10 when asyncio.Lock is reused across different event loops. --- tests/test_litellm/proxy/test_aiohttp_session_recovery.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index b71a61d2c29..763744609e3 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -33,9 +33,13 @@ async def test_add_shared_session_attaches_open_session(): @pytest.mark.asyncio async def test_add_shared_session_recreates_closed_session(): """When the shared session is closed, it should be recreated.""" + import litellm.proxy.route_llm_request as route_module from litellm.proxy import proxy_server as proxy_server_module from litellm.proxy.route_llm_request import add_shared_session_to_data + # Reset the module-level lock so each test uses the current event loop + route_module._shared_session_lock = None + closed_session = MagicMock() closed_session.closed = True @@ -64,9 +68,13 @@ async def test_add_shared_session_recreates_closed_session(): @pytest.mark.asyncio async def test_add_shared_session_handles_recreation_failure(): """When recreation fails, data should not contain shared_session.""" + import litellm.proxy.route_llm_request as route_module from litellm.proxy import proxy_server as proxy_server_module from litellm.proxy.route_llm_request import add_shared_session_to_data + # Reset the module-level lock so each test uses the current event loop + route_module._shared_session_lock = None + closed_session = MagicMock() closed_session.closed = True From 32ecd241168c57fcc1de28c75b3cb72524af8ea2 Mon Sep 17 00:00:00 2001 From: d Date: Tue, 17 Mar 2026 13:09:26 +0000 Subject: [PATCH 04/25] fix: address P2 review feedback - exception handling and warning accuracy - Add try/except around _initialize_shared_aiohttp_session call to catch and log exceptions (instead of letting them bubble to outer handler) - Fix warning message when re-checked session is None (was incorrectly logging closed session ID on a None session) - Add debug logging to outer except handler instead of bare pass - Add test for _initialize_shared_aiohttp_session raising exception --- litellm/proxy/route_llm_request.py | 35 +++++++++++++++---- .../proxy/test_aiohttp_session_recovery.py | 30 ++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 376bf07f2b5..96e6f705cca 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -169,10 +169,23 @@ async def add_shared_session_to_data(data: dict) -> None: data["shared_session"] = session return - verbose_proxy_logger.warning( - f"SESSION REUSE: Shared aiohttp session is closed (ID: {id(session)}), recreating..." - ) - new_session = await proxy_server._initialize_shared_aiohttp_session() + # session could be None here (if another coroutine set it to None) + # or closed — either way we need to recreate + if session is not None: + verbose_proxy_logger.warning( + f"SESSION REUSE: Shared aiohttp session is closed (ID: {id(session)}), recreating..." + ) + else: + verbose_proxy_logger.warning( + "SESSION REUSE: Shared aiohttp session is None after re-check, recreating..." + ) + try: + new_session = await proxy_server._initialize_shared_aiohttp_session() + except Exception: + verbose_proxy_logger.exception( + "SESSION REUSE: Exception during shared session recreation" + ) + new_session = None if new_session is not None: proxy_server.shared_aiohttp_session = new_session data["shared_session"] = new_session @@ -185,8 +198,18 @@ async def add_shared_session_to_data(data: dict) -> None: "SESSION REUSE: No shared session available for this request" ) except Exception: - # Silently continue without session reuse if import fails or session is unavailable - pass + # Continue without session reuse — this outer handler covers import failures + # and other unexpected errors to avoid breaking the request path. + # Inner recovery logic has its own specific exception handling. + try: + from litellm._logging import verbose_proxy_logger + + verbose_proxy_logger.debug( + "SESSION REUSE: Unexpected error in session setup, continuing without reuse", + exc_info=True, + ) + except Exception: + pass async def route_request( # noqa: PLR0915 - Complex routing function, refactoring tracked separately diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index 763744609e3..224a4aa1392 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -94,6 +94,36 @@ async def test_add_shared_session_handles_recreation_failure(): assert "shared_session" not in data +@pytest.mark.asyncio +async def test_add_shared_session_handles_recreation_exception(): + """When _initialize_shared_aiohttp_session raises, data should not contain shared_session.""" + import litellm.proxy.route_llm_request as route_module + from litellm.proxy import proxy_server as proxy_server_module + from litellm.proxy.route_llm_request import add_shared_session_to_data + + # Reset the module-level lock so each test uses the current event loop + route_module._shared_session_lock = None + + closed_session = MagicMock() + closed_session.closed = True + + with patch.object( + proxy_server_module, + "shared_aiohttp_session", + closed_session, + ): + with patch.object( + proxy_server_module, + "_initialize_shared_aiohttp_session", + new_callable=AsyncMock, + side_effect=RuntimeError("connection pool exhausted"), + ): + data = {} + await add_shared_session_to_data(data) + # Should gracefully handle exception — no shared_session attached + assert "shared_session" not in data + + @pytest.mark.asyncio async def test_add_shared_session_no_session_available(): """When no session was ever created, data should not contain shared_session.""" From ef22144854f0248163ff9250060ed49afff23bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= <258577966+voidborne-d@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:07:15 +0000 Subject: [PATCH 05/25] address P2 feedback: add lock docstring warning, remove redundant mock write - Add WARNING docstring to _get_shared_session_lock() about not resetting the lock to None while coroutines may be in the recovery path - Remove redundant proxy_server_module.shared_aiohttp_session assignment in mock_init (add_shared_session_to_data overwrites it synchronously) --- litellm/proxy/route_llm_request.py | 7 ++++++- tests/test_litellm/proxy/test_aiohttp_session_recovery.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 96e6f705cca..79e8f41972f 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -128,7 +128,12 @@ _shared_session_lock: Optional[asyncio.Lock] = None def _get_shared_session_lock() -> asyncio.Lock: - """Lazily create the shared session lock (must be called within a running event loop).""" + """Lazily create the shared session lock (must be called within a running event loop). + + WARNING: Do not reset _shared_session_lock to None while any coroutine may be + executing the session-recovery path; doing so breaks the double-checked locking + guarantee and can cause duplicate session creation. + """ global _shared_session_lock if _shared_session_lock is None: _shared_session_lock = asyncio.Lock() diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index 224a4aa1392..a2b09527962 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -161,7 +161,6 @@ async def test_add_shared_session_concurrent_recreation_uses_lock(): call_count += 1 # Simulate some async work await asyncio.sleep(0.01) - proxy_server_module.shared_aiohttp_session = new_session return new_session with patch.object( From 3ff4ac3de3fc2fb013ed2ecbe4b5b82304d43fe1 Mon Sep 17 00:00:00 2001 From: Xianzong Xie Date: Tue, 17 Mar 2026 11:21:26 -0700 Subject: [PATCH 06/25] Capture incomplete terminal error in background streaming Committed-By-Agent: codex Co-authored-by: codex --- litellm/proxy/response_polling/background_streaming.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/response_polling/background_streaming.py b/litellm/proxy/response_polling/background_streaming.py index 5ec0aac8e29..17ee101549e 100644 --- a/litellm/proxy/response_polling/background_streaming.py +++ b/litellm/proxy/response_polling/background_streaming.py @@ -258,8 +258,8 @@ async def background_streaming_task( # noqa: PLR0915 ), ) - # Extract error for failed responses - if event_type == "response.failed": + # Extract error for failed and incomplete responses + if event_type == "response.failed" or event_type == "response.incomplete": terminal_error = response_data.get("error") # Core response fields @@ -337,7 +337,7 @@ async def background_streaming_task( # noqa: PLR0915 ) verbose_proxy_logger.info( - f"Finished background streaming for {polling_id}, status={final_status}, output_items={len(output_items)}" + f"Finished background streaming for {polling_id}, status={final_status}, error={terminal_error}, output_items={len(output_items)}" ) except Exception as e: From bd5c39c4d1da36f6641ce5ea57df9067b93241ee Mon Sep 17 00:00:00 2001 From: Xianzong Xie Date: Tue, 17 Mar 2026 11:35:50 -0700 Subject: [PATCH 07/25] Log incomplete details in background streaming Committed-By-Agent: codex Co-authored-by: codex --- litellm/proxy/response_polling/background_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/response_polling/background_streaming.py b/litellm/proxy/response_polling/background_streaming.py index 17ee101549e..b4d51814e5a 100644 --- a/litellm/proxy/response_polling/background_streaming.py +++ b/litellm/proxy/response_polling/background_streaming.py @@ -337,7 +337,7 @@ async def background_streaming_task( # noqa: PLR0915 ) verbose_proxy_logger.info( - f"Finished background streaming for {polling_id}, status={final_status}, error={terminal_error}, output_items={len(output_items)}" + f"Finished background streaming for {polling_id}, status={final_status}, error={terminal_error}, incomplete_details={incomplete_details_data}, output_items={len(output_items)}" ) except Exception as e: From cb888364867f7d4226c3162b411cea0299d60d0a Mon Sep 17 00:00:00 2001 From: Xianzong Xie Date: Tue, 17 Mar 2026 11:39:12 -0700 Subject: [PATCH 08/25] Add incomplete response error propagation test Committed-By-Agent: codex Co-authored-by: codex --- tests/proxy_unit_tests/test_response_polling_handler.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/proxy_unit_tests/test_response_polling_handler.py b/tests/proxy_unit_tests/test_response_polling_handler.py index 6235dde8475..c5f3d7c6f45 100644 --- a/tests/proxy_unit_tests/test_response_polling_handler.py +++ b/tests/proxy_unit_tests/test_response_polling_handler.py @@ -1414,6 +1414,11 @@ class TestBackgroundStreamingTerminalEvents: background_streaming_task, ) + error_payload = { + "type": "incomplete_response", + "message": "The model stopped before producing a complete response", + "code": "max_output_tokens", + } events = [ {"type": "response.in_progress"}, { @@ -1421,6 +1426,7 @@ class TestBackgroundStreamingTerminalEvents: "response": { "id": "resp_123", "status": "incomplete", + "error": error_payload, "incomplete_details": {"reason": "max_output_tokens"}, "usage": {"input_tokens": 10, "output_tokens": 4096}, "model": "gpt-4o", @@ -1442,6 +1448,7 @@ class TestBackgroundStreamingTerminalEvents: final_call = handler.update_state.call_args_list[-1] assert final_call.kwargs["status"] == "incomplete" + assert final_call.kwargs["error"] == error_payload assert final_call.kwargs["incomplete_details"] == {"reason": "max_output_tokens"} assert final_call.kwargs["usage"] == {"input_tokens": 10, "output_tokens": 4096} From ca8f5cffa0051dfb9f607b4f4f7fe925312c9a18 Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Tue, 17 Mar 2026 18:52:57 +0000 Subject: [PATCH 09/25] style: apply black formatting to fix CI lint check --- litellm/proxy/route_llm_request.py | 4 +++- .../proxy/test_aiohttp_session_recovery.py | 12 +++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 79e8f41972f..f1590b16c24 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -185,7 +185,9 @@ async def add_shared_session_to_data(data: dict) -> None: "SESSION REUSE: Shared aiohttp session is None after re-check, recreating..." ) try: - new_session = await proxy_server._initialize_shared_aiohttp_session() + new_session = ( + await proxy_server._initialize_shared_aiohttp_session() + ) except Exception: verbose_proxy_logger.exception( "SESSION REUSE: Exception during shared session recreation" diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index a2b09527962..f089d10425c 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -22,9 +22,7 @@ async def test_add_shared_session_attaches_open_session(): mock_session = MagicMock() mock_session.closed = False - with patch( - "litellm.proxy.proxy_server.shared_aiohttp_session", mock_session - ): + with patch("litellm.proxy.proxy_server.shared_aiohttp_session", mock_session): data = {} await add_shared_session_to_data(data) assert data["shared_session"] is mock_session @@ -129,9 +127,7 @@ async def test_add_shared_session_no_session_available(): """When no session was ever created, data should not contain shared_session.""" from litellm.proxy.route_llm_request import add_shared_session_to_data - with patch( - "litellm.proxy.proxy_server.shared_aiohttp_session", None - ): + with patch("litellm.proxy.proxy_server.shared_aiohttp_session", None): data = {} await add_shared_session_to_data(data) assert "shared_session" not in data @@ -175,9 +171,7 @@ async def test_add_shared_session_concurrent_recreation_uses_lock(): ): # Launch 5 concurrent calls results = [{} for _ in range(5)] - await asyncio.gather( - *(add_shared_session_to_data(d) for d in results) - ) + await asyncio.gather(*(add_shared_session_to_data(d) for d in results)) # Only 1 coroutine should have called _initialize (the rest see the # re-checked session as open under the lock) From d0c5f494a8bf99a078561a939cb0e557559c614f Mon Sep 17 00:00:00 2001 From: Kelvin Tran Date: Tue, 17 Mar 2026 14:30:12 -0700 Subject: [PATCH 10/25] fix: cache_control directive dropped anthropic document/file blocks --- .../prompt_templates/factory.py | 16 +++- ...llm_core_utils_prompt_templates_factory.py | 89 ++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 53d2ca2f23f..2b4dbc4a3ac 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2142,13 +2142,25 @@ def anthropic_messages_pt( # noqa: PLR0915 user_content.append(_content_element) elif m.get("type", "") == "document": - user_content.append(cast(AnthropicMessagesDocumentParam, m)) + _document_content_element = cast( + AnthropicMessagesDocumentParam, + add_cache_control_to_content( + anthropic_content_element=cast(AnthropicMessagesDocumentParam, m), + original_content_element=dict(m), + ), + ) + user_content.append(_document_content_element) elif m.get("type", "") == "file": - user_content.append( + _file_content_element = ( anthropic_process_openai_file_message( cast(ChatCompletionFileObject, m) ) ) + _file_content_element = add_cache_control_to_content( + anthropic_content_element=cast(AnthropicMessagesDocumentParam, _file_content_element), + original_content_element=dict(m), + ) + user_content.append(cast(AnthropicMessagesDocumentParam,_file_content_element)) elif isinstance(user_message_types_block["content"], str): _anthropic_content_text_element: AnthropicMessagesTextParam = { "type": "text", diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index e87233a52a3..fb101162988 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -1,4 +1,4 @@ -import json +import base64 from unittest.mock import MagicMock, patch import pytest @@ -8,6 +8,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( BAD_MESSAGE_ERROR_STR, BedrockConverseMessagesProcessor, BedrockImageProcessor, + anthropic_messages_pt, ollama_pt, ) @@ -1590,3 +1591,89 @@ def test_bedrock_tools_unpack_defs_no_oom_with_nested_refs(): # Verify $defs have been removed (Bedrock doesn't support them) tool_schema = result[0]["toolSpec"].get("inputSchema", {}).get("json", {}) assert "$defs" not in tool_schema, "$defs should be removed after expansion" + + +def test_anthropic_messages_pt_file_block_preserves_cache_control(): + """ + Test that cache_control on file-type content blocks is preserved + when translating to Anthropic message format. + Regression test for https://github.com/BerriAI/litellm/issues/23873 + """ + + pdf_b64 = base64.b64encode(b"%PDF-1.4 fake pdf content").decode() + messages = [ + { + "role": "user", + "content": [ + { + "type": "file", + "file": { + "filename": "document.pdf", + "file_data": f"data:application/pdf;base64,{pdf_b64}", + }, + "cache_control": {"type": "ephemeral"}, + }, + { + "type": "text", + "text": "Summarize this document.", + "cache_control": {"type": "ephemeral"}, + }, + ], + } + ] + + result = anthropic_messages_pt( + messages=messages, + model="claude-sonnet-4-20250514", + llm_provider="anthropic", + ) + + assert len(result) == 1 + content_blocks = result[0]["content"] + assert len(content_blocks) == 2 + + file_block = content_blocks[0] + assert file_block["type"] == "document" + assert "cache_control" in file_block, ( + "cache_control should be preserved on file/document content blocks" + ) + assert file_block["cache_control"]["type"] == "ephemeral" + + text_block = content_blocks[1] + assert text_block["type"] == "text" + assert "cache_control" in text_block + assert text_block["cache_control"]["type"] == "ephemeral" + + +def test_anthropic_messages_pt_file_block_without_cache_control(): + """ + Test that file blocks without cache_control still work correctly. + """ + import base64 + + pdf_b64 = base64.b64encode(b"%PDF-1.4 fake").decode() + messages = [ + { + "role": "user", + "content": [ + { + "type": "file", + "file": { + "filename": "doc.pdf", + "file_data": f"data:application/pdf;base64,{pdf_b64}", + }, + }, + ], + } + ] + + result = anthropic_messages_pt( + messages=messages, + model="claude-sonnet-4-20250514", + llm_provider="anthropic", + ) + + assert len(result) == 1 + file_block = result[0]["content"][0] + assert file_block["type"] == "document" + assert "cache_control" not in file_block From 9fa1809c30c0bc18f70bbe60626966285a172cfa Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 17:37:04 -0700 Subject: [PATCH 11/25] =?UTF-8?q?bump:=20version=200.4.56=20=E2=86=92=200.?= =?UTF-8?q?4.57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- litellm-proxy-extras/pyproject.toml | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index b65dbe45233..006aad9480b 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.4.56" +version = "0.4.57" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.4.56" +version = "0.4.57" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/pyproject.toml b/pyproject.toml index 37223a3e251..0e2fb40d935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ boto3 = { version = "^1.40.76", optional = true } redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = ">=1.25.0,<2.0.0", optional = true, python = ">=3.10"} a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "^0.4.56", optional = true} +litellm-proxy-extras = {version = "^0.4.57", optional = true} rich = {version = "^13.7.1", optional = true} litellm-enterprise = {version = "^0.1.33", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 2bdafda612a..827986487fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,7 +57,7 @@ grpcio>=1.75.0; python_version >= "3.14" sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.4.56 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.4.57 # for proxy extras - e.g. prisma migrations llm-sandbox==0.3.31 # for skill execution in sandbox ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env From cc37bf59344f5a5ba364d6fbc406f17f30e550a4 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 17:37:25 -0700 Subject: [PATCH 12/25] adding build --- ...litellm_proxy_extras-0.4.57-py3-none-any.whl | Bin 0 -> 76172 bytes .../dist/litellm_proxy_extras-0.4.57.tar.gz | Bin 0 -> 31935 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57.tar.gz diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..eeed18e312aecb6afdcdfe13d71fbd7fce68ea44 GIT binary patch literal 76172 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 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.57.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..293c44e593d5147d8cb69b76abd3406c9e55c107 GIT binary patch literal 31935 zcmXt z9q#}8@L~7_dyd)rI?uI!YYjsf9v%kz2MgF*xtQ77+Ce`$Ie2(N%{*M3Oq_Z6d4+g| zMR{FJoG^A9cbsNaV)!Lsy6PG&C2)S>^e1EuD%IDg=Ut?GvYh(VxxZ&R`_c|%Arl+?Zf3MrrdcfzPH`@w^ z@5?pmK<9GbOPOzQ@YNNi=ncH^Y9t`N%{~D|`RO09V|bLh>3%-DNlQnE`do=B4^Hrd!y1wM-& z9`VZyRbVeHA}+-<4+tj*N~=prcUqiS$LHE9;*XbhMU04TH7m0uY0kHeEnqMhuECEA z3Q0+}`Q2gnjteF$Vz=k@WBXAx)d!NRm$nugJ?D9^NSc3&%{1=KxJR1q8XVqflNLC( zt3$Rlu0`%ilX|r~F?9ExV{b0beU4v|*iY2MNSa!CMUloE@bvI*1o<+hccAzu_-BUa zN&`4e4bL&%+q{L`ww@@=B=0s8uhegnBX)x>N^_Q#FiU4!Ol8`IpOvr;j4lZpyI-xf z5EDw=UnKaL>TSABl==WQk((}@vY_^3@zZ_T0ELBq1rMr4g}Tq)mvnrX&oHBk=&1bV zZJ!iHRJY^|epFCsQpA)&_#CLM@i_*h4$r!qyhu5x_9``V=~%#ge26b%(G{6~*An8w z{=WJ7pKlsgB-rC;@_L*GSXjpKvT^Wx`#fS0caa7D;O6*4ahX^5UGPgslB1Gbp2g-c zG$SEr-TiCzGca1{MfVg@ZCw-cdddoX7m9FLJm{cDbWZK5ConkRSN@S{`g8O7UDM-E zN_bTIAn&US6#y$Pz7`c-J@(qzZ5u{KDNLRml=f;NE*uasp-i>f^Q-#Ok*WqE~h*XlA3_ClVOW-9AcYcCygnTp6BDvHux+ z7Jw(z0*B0>u3&asflJzsx|V^

q$qREtayi-m&CL1b=*Ac@53@t}s-VDBL~sLrfi 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

_)(gW}{?KFs8NxqarOl zgP(1}@-Rd@lGIE1N=Sp>sYQ&;QzV0u9x%hx57NVcd%f^6(3Q^arNu1lZodxCFBQGN z*Bq|oLBhHA)D^0V4TS(cOm9zQNOU)t8b$!ozo--gJgp?GH}V*Cs7wSb?iz|72VT4= z2bb+-u$Oj}Z&wM`QX-iY!sv4ePHu4!1$?;PXscqn4Jy5ofx|tJcZI4A;SfDW$h1W_ zpUbsI?!&Vl(m7%cpvALB;suuyJ*%kY)*{UkP&P~j*R_|hdXXFWh=7rM-H!*)LNn-h zsFp8Z>} zHjIUjZEJ6c_->P+!fu~~P?ds>z-R}rB*2q#(S?5mwXlE%N-)mLk?A~-+kn9|3SNr@tJ}fb2!V-> zIv72?hBbm*<(+t>!Qc(8x@9t-`8vX6+x5LM9@e$uGP6|kP6s2s(J=i}5BS)W1B23B zp{IvKaHQbdo~}6Jo}hqVD|6Y%P|tFCZKlW6d{CpP=AYk5Nu!FgN57zk&l#$2Cip8s zr*k(fH9L6?5|6h%U(G7}9QWdUF;?7Zw>xSaTM^->L1#xe&5J0(*fl#(H{x;zAE>5L z6}*T853whN_Cc-R`2R!?J!VuOaeMbX5hnMY0*}Dn!^2L5Gvcr26!E}5=M?%*fu?YRo+;rAs2o{1Ne56fYIMz48q~8(5Ep$ z&Fd~wW0;IH9YD3&s1?-DWxI`oiW~%}yM%>;&KyYfyN!iDP!XWiuxbYPSA5;>3wsTFa7cZIV6VY&!&dr*x_zkJydm@xV?~)bM)I8}eU1vpk?B_x zzF7AU*S{fUjaHZIe(Ohz2jsvfS!y6>Q=^v^lT#YDnrFXhR_E4S!S0RXQgQSSO7gNV zEcO$}`&D8`Ps~M`1MIrF)7qnl^u7%eiiA?)P2%D*docBy44C6RlFLuH;Cjw|CqVZl zuTGFw@UH2SmI)_O0^w)F2fWlTQa`JH{w?&*De2$W!)`ODw_-f}XS$etcA!KROq{{f zG(braO1I=J-*r?(pN;}gmdDrfIIlLDbzYAKS@Q;Q{M3GK-g430Y!E zI&=)YR)JzgOXs;P{`8j1Vdz*X2dmq#iBdAyxq{QQ1DgUcBUsmrH$6vhhO<(;cHj3k z4wfNs??u2kCRJD9GgDVkCsN$>KnShOAgtgM%@(IrPQj*F@NJe=;HBII5GT>{E{$i_i(FOZXdC=Ge`vrT8EnGI0Qf5$s^*$@bO*nU-tvm z6A1e@%nkjl5&(yZIB--tW|htNv6EJ>1*5dPA?1Fjn)wSU3BC&#o?p0$*FTAi0@CcBy; zivB05&0h!Sa;wIigX{vbGS?WfFpKW1j$`xxe@A0+Fjute=e);0#qr=|N;g;Qp}26- zm!wpQe2Y-OMxRZT!KmUk+r1SsoU&a;7EJgg5$6I(Z;bR({yW=V$Ml=nc3hdHZQXv7 zk&*ChA@K6Mqnq{Lef_ZWj5wLT-*hV3RZX5 zcaNEiT`9AAeK|ETMLBoO$)%0vU^|}jjQ=h(Pd8tzyomPJ&HV7%yc(yaoL-K4)b8u0 z-o!mp3>|%cjDtFz<`I6Kq*MkTy0gqRuu%`BM~{JXH<+tC2K3e5N;MPqbh$+S6#dsr zcdBVZ@Lsd6FJKRjy=c+pyH&M)s2MAZV?aF@5f7?v+zrnc{ysKd4YGJY8r);1Hj|K0 z!w1X$Ox?tSP~U)kl0lSIBL^ArD!&mUQ#f%lvi)leQC!&QnN(xX^%&4?Ujx@}7{&jP#Ju`vyy4=?)Zv=teG9Yg@>H*$! z+yL?q3Ddd+-D5=3wK)xtfKT7+nCr}Zjylu%D!DTduwszFjDqffoh7iB{RjcWZN88& z$FclLXg-cn**tVI$x?5QGU)o>-jP&JEkx^eB+L-WZ~#jz!EdblBCYn%h*gswL$I=g z^$IUZk6}Ibrt8N3ZfW}b=&I$1a_@xH&laJi*ojhPvP*Az2T0lQ`8{>~@+-KLpJu)` z*3{@0^GtT>e@6L6z4~*%UUXv4(v-AGzvV`!?5MQO5K)e76kr3;19Tpms$7x|Y{_<0kcg`LIGFLU7-5 z`4|*~xZ6AGJt^6<4jaE(n6Lm<$fFk=KXB(=tZUnjM~ADgGs>S*y2{yG4U=ZCPhPB7 zcw|};z2E1{gK+;~Wph{Dc^OwheF47*ZcWloE!Z3H08r|^`RJB=ay@1~Lrf|?_i}Oq z_Le}7F&`ZCAHp~R`b<(I(-E+n)U*W}FCSIUl=80$O<3a>@VEiUJlIB-*!%-KJ>clx zir7r*D}od?i|LW;nZ?`N-T@*Xq821TCtAK`At1%NLv+EyIZlPC0Db zw8pq&a=*9#c*;b-=^s$~0!l zR1O+>`Qt&-V8#WB#2a;5zaO*d|2X=MKRT!vVN>vu9AdajR3$|?CLAysMXiKswYCvv z<+?=2wa$$sJYuA$g>7~wV7M?w;qyLMxSS7oh{cj(NfhFPJN0!f)!=OUGarR?RM&(h zdf7wMqQQhVSgSh9Kg%E4Y7_}+5)}+glIDLpOQ0?e$$d?F6lXsj2X$*|)8H{&{!4ikZ?z@fMO({gF9N zu>g==a|C=GOqkwE6WE-Lkdb?&zMb={5iO$rKcCl64f;TD3;OnXf4wYujDBCLfN!M& zR^w~2O31F_(5PN*@GERUVeqt(WNxbaKO}vmXdBFc&SrR>{wauhO;hJ|R%$f;T4}%6 zy5`l9syWsC`N<-=ge33oQlKzm`*_|~3F(eD$s1^Ik$eoN&gi5Lk1|ER6lkmtwDL;)N;hf;W3cxK^K;t6>*csn3N)T?= zeC?<4fNY2r@T{ZFCNuzyTr%_nxB!aJ#BUuW1DhLQ@DJSoX20Qjq5T6d-27+aU(7Oz zO$W*ULe8+2Fxo+Qe6%ZYRGXqtBuUyzx8O}UPyWk_cOy+ z)1A;6-Ie*0@5VN^`g3`jtCLXa@y~p>gkVw%q8~J-RC`qdt^D&BVgnzEKHts0!=v?{ zU+Kr}a=`Ym>)RHd8c^wi%Co2dmD@33nl9U>VL8+fYvvgD;w90j)1pW(Y|WT27U<8l zg(D{bGUr-ep5#fS7=s;gGf$d>4VhSEpI4ig4=;HAp)DAN@82_oXQkv;SU%A1l zFZqa<_ChV06y4axE2)>m`(+k1jj&c-ii3>YCVkWg!~$C4T)aDm=J5iD2K;AQuOr2= z%6<-Oj=K;2lgkRo`-<9qCRXQ|6@cY zHY=bRN59BE#1IE@KIEp*`E{iE(C1K}Vp8Omr5surt3o{K3u-1mlrd2oNAVxCv3&+>`)2uZuPU+`am?X zQ~1DViS{Fkd8G-5=e^{uG!yU^)n`$>Wq)a?p4Xv@bRHzL&7IBSk6^rxkrn11^|W*f z(R(0Urir}kuSgmYbjQ(2Yh@xz4$}X8;xm7eVsiJ3{FN8`dFBstU0Q{Tzr2(SWg^YW zo!;p>AQ}f}H{fIoC=2g^;GcLAwB7=vB4BR<5HuSZfxNr}AliZ&d`35cdP#+RrlGuA zBpLVjS)5<&bG*|V>y@YEbLndRp@Och^1_Yrzgxd*`4YL<3+@B*bC?2}%pHPP^&+SR z;RuPwfd^(dO*bXAbgwmJPWv*~Uea4RiCI4%S#B0N0%)8z58NZl5oq-UyFo(xY1JQm ztr@=zrfwm~U;5h&wtE(RDN)Js_i$KJhcUC_IQh$S@b&($>|HFVcWDLPu+(8NHw%nG7 z=eyGL7N)7VS>l;z?YZd&wFLe9n1@u!5t1n(1a3bQZVG9Sm{SuzhRK$GbS&w?IcaGo z4jA?+m(6>SktF>tqwU08S|k0d8M7yWf`@Z%i)&lv6?4em`j1goNr$syM%M;Py_d=S zqxHP6WGVF2pE+$C;ttvxd6I`$k2M6-a@KT}ALFfAM{kiwf6em58GiZQ%ZzYlh1P;^ ziO>6i=#-XVn6xHxOCK1>fgL)ln}exSNcO9nlDU2QBt}%dNd`IGK^bt}Maxcx6?Si6 zV|nACa0dTy&tmE(rB!AEk4sH}!JdXsD0N!aL^3P$SH_t%Lxdb>%P4Sav~Q2~)D2kD zU_~()2B3!lJ>p>W!u+MaSYuPeiy$1uS%&6#Rc6A0fv6tbt*Fp{S0dgAmu(p=Qufg?b^khJAxx zC#7p+x@X4Mj%4Tx#&QGCJ^@lGsD6@WqpZPA&J!dJemkt5Q8gTOB)32PH6%H zxDu0JKqSDzEp0`?eBCb}EWYbMmAPQV8>6!CWxH|rgcjb2 zH9EZSHB$_>t`{~&fWGiHZ0;3r-BP8`yA{GC-jW;CRQuVZL-@VM08`N@FdE;~RPI`~&wF z`+Xv+vs^985hQ2%EPOq66Zr_Z4A|KKB}W3K-YV2Bv!P+%ZB`E4PHs=|5!lzLMmZR4 zz-HcDA9PmG5HI_*ImFo9(W@w&aBv}AZwZJn8|5|XFM64#b^YBcy`1NmLv^5K#N#{u z3gu-ieqPyK2LY3T&0U=&(dUjDBeS3BO^-)~FC*~xKoR}hlF;(&)2u-k8~m+myRo@-`h2lZS%!2D(H}qM z2`qce&g=lDFOB{{+X1D>2tO4Yc~|eQkcRIuGGV;E469pK^9_&e+rziE#^^!>&GmC7 zH)?M$i)(T9{=rus4p;OfIjvcr97~3#&p~B^? z{ycBzE`#4pzMFnGyL>G3aD{4CQT@}9_ zWgJrwtt4ICi+IU+uMLAd%}#ihr?tsd)~_cLz3(m`y+>$ zh|}ie>}d$$UNInk-2YjuR&%m)WZ7Df)y}m)730^xQ!J?+eL2sw)Lnt?{QB%595byk z|CY&*hFmINUgVJD30YQ=n+~UaeNr3N^;Jxbw|%rR;vQAL{!6>WWJ8Tl%%RA&!emuu z$`>q^3ix^YmMU&M+Un)jYG-MiyVLLeJu|gg-n-51{QfwgP4FVs-44Lcui!lBU%c0F z4PDfNTfLb}eWMXq9iF-7pXwur+8a3SZq8YHIf6l78chSW_JC;mkM1YhfTP)>Rs@wv z*2g-YO)at1{>mz&rPiMObWk@fj0fZE zU|4e%Hhu<~cnn;(e*@31m};s$dQ}{94W;l&0!yk*WAlKkh)TV%AcMz18w*XGEb`*| z!SD$BPgp=)2S|H2Hxy`)R%lH>@BPWt@9PKGi+lOfi1SHI4F`YvzMuKn{xzX!7OFnj z31+H}0R8JdqY!ee4Bg2~KIc25Sk33hx8tn?ws{q4&K6U5c528s}nNYFKpHV8L) z!bhMD?YuLGcj&5TG?+4yHvUkF)3bc03K|F`li8k(HAr1u&?87mUEu294BYx| zcp{H>m&b!apl0@G^0Bsz&yoIX#hEl7#G9E8m>J9O?dcnCWiCv)c+)Y05Eeb#D#ctw zL(zwfPfz<&_+%%NCJBU`Il_-X1#$}rwkk39`5@GPNBX#Pq`>6z%T8LZ2CqOQ1- zK%x{zS;M+qya&5NIdC0-2!8U!;mlyhM|Il-e~qO+Jr3G}R_-~Z?84BW#TnQ#N5ZBJ z?ZC|UYY5Y@sy{;{p+}s8i?D(}^Ilti#kN+{6mmgq*9-xo|6>gu8g|I^T0c#W>9l~4 zcJC<&zW;(-2E+gUN8yp+@K?e*2tnsvc7{mkXv@-|`v9e6vu5+-!F2uoX4Y3EeM``U z#6WNF!*1FE3$oRrY77^FVNbPbP6+#Kf01 z?ZQfifVlKN3P64w(u+ypAEaWe2j1C>hV{!a`rR=J5DAdDq3~!iz(e^&dc9I>$&zZi9;S6%}{BH z_$bUvE~%eLIw{qx!>CK@f;GBb0?zMJDCJ1-m-{o@&se5GQQ5G5o`;p{AKWjlB{nye zHo;y*1o*^@S}6JkXhQVxn%uh{`>nmpke}Age}ZN8<*yqLoligV_9UbCU@l6%-WMH7 z2XBHYpAs8>>{T}Z(Q&`MirXMy&;lHcp>ub-><=!UOn;JaIptS(R0aJ<*ciK09(Se@ z3J)$&prFL(D zFI__f;*@fp;Ic3pN!UrHEQ=8qwcL!lDNu(LULuY?9uhXZL9yk}>)?nGgchdt)LXCE z$wxgr{r-ztp`6{{gpKYb?mmi{48HE~s7`+5N#4$0^h3oG#uW_l_N@_z{Y zo<#Y?x`!gF+ksm< z4+@ge8SL28X*`G#p40jaHs=ilZ~TrCN#HbY6=NyUe{|{?=6=5d!7yX@zx7=*NZImd zoYYqc5>BVvry+X0hwG8<5c8{zRrB>M=S5q>ZwGCTp(5iR{`~G3UZ0rzX>q*-aYd}b zk@<|Oh`RG+C#$XqouNT>!r-!8x%Ww^Y=P#y28uEAf?JD~p$&{ze#Rwy_<3YvbC zXW>#Ga#s-Ni}t8OWU8Xw+0-i(^zQjT0MvArmAF7x4 zp0hzgsldcI0+xnO(RW5bq<)EH<2Vx+&Jl)yD3{tnas^_pojNgsAgIIahnz0<)T3KR zFMx_{k+DKWA|OIw0S^V2y@?D^C?R*~2hM}=R-m;3-rh&IddE5e5%=9;4UPbf*MFk! z%2wRiEdeuQz6n5ngf6cvpsN*%yI`6S9r!IHp)(*o5};`!Y|d-UdrXA=F-fXSttzG0 z;pSoF-hY$(8|XR)ZpxjYBnL z{*sX;Xt)sezdU{iW`P9Z5!JxqD=Ix!FPrgj&%(pd-}Yr-YS}mYQD0pB9e+W$y-Atsj}UH) zw~aGyPl8le;w32G-5b;)su_5h+%jz2;JQvCM4s4KCcf-~5={j~ya@P*O0PL$GMqf_ zZ_(ec)OIipIR4jxQa#|e0g|KV@UwZKop4o7-DA4Mr#_!j&j`n|zjRwfD+j}OeuVAiOjxG=q~kh&!a{UB=%u2u%ji+u7X5_AKWo|?J5pk{f}?+2zt2S3izWJ+$Qbz^(G7m10Z{)I0o?%e z4^ez^t6aYby5t7c%9F3rCua&ZF?ql9PqHpe*gkd0bCQ)!ET|&-}r!RUVrCRc5 z^m_>$mzhf#x-+*AGuT4%HG_J6uz-#U+(Y85Ni7!B^EX_jXq!%aW~l2SGm(;c39I!7 zZu930>4rmLFFZ@v)j$KxNtN(;|LPb!>DyCZXAHc}KTL>D^_v|Te_TtECy*%0{J7-& zwZv@o^OZ0|r)#bB{uf~}+BciKYSvZw{}=z?foo~okMh>;!vhcM#I^`TV-qU3v}XnC z^!uonj{|)>Mw`>3|qTi@f(8-JC6KTz!NY4Ho;3H)XVAbDlpHnGdVXy%v zc~R+${^xgwk?@xQ(zgzTDsLyBL#53sC9agEh-uth1g)YR|i=axz z#?Ihqz(`pP?8Rd{Cpk_7>8=JCzhr}$fE3RqU#h%#%F7Eh3=8tNg6PGH2UiH+Hvc`= zV(_zVj${P6@MJ5fJe)shB;=h52^qo=dJfhp{Qu^m{)P-Hmj%Et-?T|)ttjsGOi)>s$tPSrJB#}hfs73BN) z`a{<7A7T3Ib1Gr>?YqC$ZW#}YF<&p3v#TjbLMOmGN&q=)q#miUGD^^!(Rsug@$}-U zcFKxS*P9~qo@;r9qeXlXy@#~btsV)eNvBkp)~acsS8FW!-KMGz+lHY>eA2;oY6wPo zyK2R#EC+>-(pna!b(W}KWv`t}A{0&4pS3VQv(+&9ijI%bRRgewBNy0m z-_o7`!fu%I4L5j z+rpw7yHN@@zRHXx<(8jyXZTzo3|iEsGIeiC#$qSzjXo3ZKauGW5p@1QYkfV4u8z&2 z%a_x3a>Ml%tu4}S&D=ob+uaE3z@u4h3A(=cu3Mi|0bC5`Y*8>eQ1!ps7{?z)ij1Mh zzXf!;ndoAVG7i4cy?^|@qigQ5cx?@C`uwsUn-K}A4bCxR_#ODX0j6M1Mo6<%5QkEl zI`*O6>*pWzdk+@>7aFSO_=5LMkcD$<6ET|;{xV54@Pjy9vP8lt)e$Dmiu z`$IoGR+__^eND8#${5jmV?R374HW`> z=U@}PG;Yf&sS*g-hBi775*(COeBn`a?!W~2?xIV$2fr@i#h~ITGTjS#w;&Hd`TMx@ zx-3aQoa6j9{QxsJnG4HJ`*YTS4{*}#n1k4AL!WG_dM#z39f|d-=E=Be>w%b)gU`DC z=HM{)`sc||HoERco39lS7|4$Ohb;pI;w+EBk%iC`R`;fw0Bl1(9SF?0`FMSN<4qoo~5FFGp4cU1xX2q@mNgSSa zof+R{ySQS*J5QabFPH(Bc==v&LGu^cd%A^uS&aE_SM!BZi+QtPO-?t1uUCQ0zpDkW zQ~1>}jP@&~zZB0Sfh>#?OhF;V9Y2FC5EpRU`WUzZG<0<6!p~K@{tNaxKk{h5=R

mhAdnTtjv@ z#aGt)mp#Kp8(K;%{_yq0&g+|%=aAV1~7T>m#r(z(0UvW+6pj87M~-hHE$(%`6W!@0a*BV1Wm~Y z>948MF}{?r2bo#qNzFl^dm9?#G4}`mtArAa4Cck}guf01(khMA;$)H}^aWD!wv-7H z+-h8}chf6fGJpQPNpk9^dqKsnMlbBa@S9CblIeGJ_S6@jXraGj|4Fmw#|t}GtB~xt zMm)TK;w@D;f9#H-)RU?=kZ{|Z3k5n*V}x)xQjPR8bevr#Qg$kfflc|xZ7tJ#bTiF% zSavU_tb=7t`6v+NWrJo^DT5|RRH$3~Da=_^ox zj>9#2Eg6zpd_+!$eDT{(_PP78DJOi#=^6i5o$^q3Z->1J^< zo$}@9cQFl)+nu+!)-|_DOtjI_%OaVGDWPl~Xd~c`-}}MH_!U4q zNW-&qiK1Ua1oaqZ#X}}B(KEx^(3+_K6}vizP55q6_Mqz#ut~|SCHd!W`~upU+TXT$ zkh}4UzKor0{KYY=g1k3QreM+`r$&9g56$*Fnh42bBu6hP72*EgZ`bC3M7UB%N>taB zUus2{+Zp(}j{b5CzNW1q)q!$=;HaF9I+!CelYvHP^;9C^C)573$87LB>e;*f3MLbi zr0zZId@8@65bH8csF#!IPsd0s6*qk~<>p8w>k}voAPKY2nytE$TDIimAl~`@dNq5Z ztKC`gJI`;!nDhbjv1h`}ZVxRAq7NXZLKNq`nt|R#OuZD-&{^s6YEPb zs?5OJuNR8fZ*${sxARdQ569#bq`i+5xwaL)ZG=*TgHvKbB)k9trY|9u#~uz#gv`yu zF;frckH|7fzcb$7{jN$V8T1f(^JHXw?a{yiUs0KyOMY2NEMVZcM3*vaS#RLiPml|p z1%9`YD5DN;6>aGm5{iHB1R@E6a%{#rK(~(=2673H<>&IWJ5Frq`AXk%o*agDXcwU$ zKWilP8YYGAc@2UU5~3Dqb3FQ6Y>k_HVSL>rSb0X&JpLbwoCcE0Xlus{2HRl|4KWX= zUrOvvpk3ii>OjFh*M9GZyOH~{xhE_eaPObL!2JH!2^dQS^~wjaR{gZG@pmiO1fBQQ zv)+lY%n4A#j`utiKZyUo0F63w#es>#l@)z28X`&IOOsd%%Tk#qtB&IYvx)V{vma*% zTCUa{faYUh`_OF&x{70AxrLHEOw^kz?`1&YocnqZ&tu*lc5~a*RDEr!5ppvoWw)wg zx}@sR_^*H5{%k*fux2yI9hU;)_s=1wOLL|L8L^W#57uOE=VjHV$v9Q7^3rI*DPdt1 zE2hHws!H`q;He$>CsgviG4i|4bF-8k4_FxpU%Iu9|L4 zaZ)+aGGqFbO+2O`h0U1Q?TMsqN!LpmedNs-0JhF>j7^`9(&J)%6y$DU_4;NpZz%IGxvLunFrZ zuDBWYzjpTiPse|+$^O@_&HndVxCp{zAaGWE4@d!zpVY>`?bQEE!Cn^s)yl?y8yy}0 zy?*`w4fO?j`wMX12#Dmf=YJYayw7Ch*ps2;2qYT@Dj5$7y?IiIlg{HC2Z9%R6vd(7 zs1XnnYa3Og$FYkPxK66ZyD!t#biELj1-{VbBvW~h)8~&0p|F&cuq;BQnb~@skMA4# z*$n^hp7#HGF8R+ms#*RsVnHDA=KjD&8?YArwqE`#0DEcv-)`3btF7;UpG*Er;KyXX ze36fgJfFOG!k^}MFNfYBAQQ*^w7MLaEFii^*5`C{&%a?9uUfsfvGo0KtJgEK^zlQJh1ef||h%z6)L`(`ge@kf#pr?c_9+RH>3yOjpZoB#@r`Hln^*q17E` z4cXb=^WQdrfg7)vKmQs3PdNXE=Kq`G|8aTuyFCc6Y=7!dPv=rMkf}GcCqbh$8cVbF zulxMRznT+o@aZ7*rjuO#zv%qKd6&sx;j%)dkx0XM) z!vGrn+HfuE@v?_bYE@^0c;;g9ev@J37@1=l+;1dpKtdoG<=IlDq1uJDk; z7erS;k15RM%mCWZDIfztihJw9rim~EbVFj>#ZVmO?dcuNb??lQ z;o$j)_%R!lbqa_#A|L76K0D#P;}VNtYm$PhYE%)x)khb55l%8bPz47wP*T>IA`yZM zVnQbb*XdGPA+)8QZc*#G>i{{B8&FTXfS&8rEbho-$DM4;!L73SSNM!JN%o#m!EzE! zn~6IP4`Ue!Nb)q^XFlRqZZYmL3w_SpV<9k_?k`LFQ@X#bnHr-N+x%nPWnlTk6aZ5B zYS<40MRqqDfBZ>5SI%g|f7nuh3Zy#y`dhMQYcQ~bz#RMDd=`}S@Tj%KoWCDo7AfIiW3`M=?za&esWgHSnJZ{A%ejp;jC$G_+l%As{hQoZlFl=|h{m zV6&rq6i8B0BxSM1Ndlc9&C$1I@*~F~DhHLzNXch3B#ifxR(Z%!M&U@!G)@o`Xs&sx z%+amfBfo>X57KDBaGTJRW;1mUaN;0!wo`HhAT^LO`4C(4&~seC$tf@jQI3d)W=x5p z7k?(vu&-Q|!hOwR=#Q}=0dN^pDPqVFGV};zqkFL7A*OP79-QGEWMz`t{CF@*P^qfm z3=>M0PfKZ2iH812vP3pTp-M@ZHJmzbAd}JP^r#-1s6m*%H{m#-B+nHnxWaCbV>wue ztSgv8q2(dp_KaVvy~@eAeDKj(o~~r2&X5s zeG@hlJWhpH82%G;hehSH^k!!ywbBWyi)f0u;D2y# zuK95d1iK5xK1NB1LYz~da*nd;gB?BI-C0CaT3%102R;2$toUT9oX}-0h8RY|j^{(b z=rH!B1x&XAe4L8y14*%I+5lmwGumOaSW27`%_E9IUvO@aJ8YhAoAl$;L=PmXAURBkno`HR#|G7t z(4Q8Mbdc&_m(>r zJ|#U_wjG=79fF>}+~+U%VGwp3gc>1tQ5hzH`P<)UrNO2o(Fl)|eNMko>uvIQ6w%Xf z64Z)igj?k2jG%*Pfj%mfcJ6WV$L_0B%s#dSQ`;HOf+O3`Vhb{7#gb_zV{PMHCGetD zJaPNv4sS&R+sq1r&z?V&r(}{)At|COLXMpD{Ez?0v71#gkkJ9{DG;0V2IQK~su}V& z$GS!-E8y9{8r<7RQbX9lTFpImn!M74IiagHnEDnNu2A`kXt^(_SIgUq(m!?Cj&hu2 z%5Q*}w%|TEA^SR$Z)@D~EPi+HxS!-JBbL!%`1X-QKpcl}Vx@6DvbO4RkXZ?8T;Vv- z_H4L87gnh|W!1b?VN>fovC749CC9%7V{>WTRiF~_l->+9pNr)EP^}NmeJP5mY3B@N zY|w^_equ`$89$s{5x&#|@{Ve{Z^$1)4ma3LA-|@9j-JV%?Z$6^!a5#cr#z6YnV`9V zzem0pT-isYbdQwER&gJ$$k4s5^gYog(`wy8tOzmR)<=}RULt#aNly;ywam_)0Aoks zze?+ha3w8zKq}j&Y`6%Y#5BVass-hS8-Y3nnJE-5oIBAqdEO_;*XD_zq^hm#GZ~{x zY&c6b7yP7RqxDZ7O`-$hZGz+Ry#seLOkG0DA=iIU7aM*KBzsHY;pbhb_mv>hRIybj zk7MU=xpkcOJ7v-KEjO5XeyCby%;bZ>rgYevhxaspq%74FE{!LijX}Aimn1KwxclNJ zTjBdf!J%y2MJ%;~As>h*by}$6(3Yjnf>aubc#K@gWFkdbax)d@nIrO2B$HeNhU|MM zt|qmXlxLr`1({LtK-ty=Fb~97)RJhVFdtGAp_Gb#k0}+KesiGHYow!BGtGR#HVFOq?iflR`u~DQdwY z+p}}Ur8m*jhvhoxd#wz3({4S#G-`fatd+jxu}yZcEgrZ z=$5=a+d(@Fvt~4Z<=?fJd`N7NdCvlzY;wzF7To45g`0Fng5g&#^c-e)H*(tJJX*3l z;Nyr@O9q+bCIFOp5G{b36*)e@I9%dJT({A|+ZjcMBx_>(AiTM@O;f(y*#w7#KXB9q z%qRAjnZqS++31jGJ&9c9NP+o;TYU@Lq>bshw)Bia?8hRjSM1&{KNfrhq{o6PlWX`W zwgm~-M!Daba*tZxh?Fsu#pcg-nwO#u6of&cd1(Pl)B-mk^&73yKHER8ZX&JO4wi8o z{qYISV@hvoHX#+gSa#3!gg~?YagT02sF>nq!Dpa>*dEXDke_;HJ6S%oEnM&~E14DP-8WQZgbN7@&M zj+aa2BFwOwh^JIP{0!?0@smBiB+|4G#s_<((P`04_ODx!<%(`xJfL_Ai*`<24KfyS zfWi^@SGgd|H&5Vya{2M%w)xjxmd8Iz>)&8HLF1qxCH_HF(1iA^(t{TUXjU6NE`aj``OYB6 z7pGM_a`B)+&Td%IjmvAYV9)>SKgoA(AeEQz-re+X$#)&HOO9Exs77i74-O6%-hpK{ zAX-XWzKD$F-{kIdZ3~RIt7w6>uB%xQ1-NLt1Pa7G0RxO(igwjHrUWg(Aw;1BgXCRF zwcd+_an&t@bb@o;!O9)MMB zdXQ95V$(8JYFvq7WsU6xdaBCT6JKOJJ?TS!0u$X?Mu@#MsM9_VCC&E|xWp>>sXg=U z0lhY)7AeqC5dLG|8rZ0TIo^=$Go>ORpcYds035<(_5`FvXM&~`SUqfb#!oUDI!}Mk zoRJE?l4*=pAS_=dS6uLOd6gOIh7a1G%zYp8s<1(+_l#2jO(u0lUzu=&1oFkv5FAp<=cz#9iU+8&GZC12pGBLTcfd!>JU6mm`>4ltg6Bu*P zja;Zy!=;UD%GDLv*p!22Y;I}qZ_)Cmwy2S`VNJzkvc|G3hGdLj$|IgJbSYbXjsDn? z%=ittW2*$nd}nOV!c!ocUywFX~<3+JoG_D8^O$>_!={=J?4O0eMvml8n zLTgW)j4eiYhUsZESE0f>qD}>&W9e{UrDxfa-U|$@)Y+9KZR7gR!J{>uq%#yUO^8Oi zV_G!@Fxhivd}GE93=b zGcRBpC-bRWW@1sP!}>1jGR0j7X8?)+eK+r5OyJI|I#v?hGU{N)jyLN-p=>*9ic*d1! zU*#0#^xK$$hUW^SDhKMVOwR(j;EejggRF@=^8&~?l^EqL=KqJ>prbs~#rDup&Vs$8 zEMzu`W3?PtjKv$PZl+uikYSu zo=s$+z^cfwm%=I0-dC;CL#6Dtdt?>Y!Sx&<{9pDQsn(;2bjUW#96LFtIy!Fioa08L zvu#=wHUMc7l+n9Ko^27y+06E4toiRAr)DhMJiSw|8h8>PCF&)DOtCO&Xw*dZmW7Y_ zj8`DIR1|fn`w5KGq{tY|DX$yNCzD6wxp$uB4+HYxShPLk#`I=e@}Q;qpfj1UksVeX z7u}hdjYDBRidHd%&*DibX)C%lw6w0mdc=Bp(F?Jy%BU30fpNzMXc z!esjwoHL!}Amo1p`Bq!aJ;ZSoVv0L)cIZ(tCU#_W1Q0HGU&JCBK_g zR~Q)70tyKr`4q|*s0$z5_K!hw#Oj*n#qo#!o}$UeoBnl4ldJ2?|M|uIaCzE47h5K` z4@#XfPRWJQ59sSf+mbt*iZ_TX_V9q*A{ob2xOaARxkw^WjQ&DF=R|rhp|p(M5XXL; z=|EMOmc8OL`=C__2*xYeROw}nLX4KDC?VZr3@rypGb7eiZkpQg-2JzIGnPNk-rZJkve9bf_h23kWHOkqajpiVdP+_LYKG*hlmmxje z>1Z{o6J3g^9W*u7(NyOvqdFqd5s-V$1Sd1|vk@D54Z93^q3O+5BQC{Pz?!&d;_?*} zmk7DnOj+b{IGU(vqVkpNNgTV^OjM*!vr+jO(K{LR3t0z3q))gFTo0?%f5%pSvy~{QPGg@i z-yV2A4chRASu+j0OXIy9ntXHQ02PROTTGF)%QxmWnxX(AoB|3&#kZIfqdvf}H>(ka%EsezzM(SKaVhOEADCP`TqXTpJ5W z+ere!d4ezOg#%429@!TyNle?EPBZ@wT9b(bP>CI4>6)D33rxm=a2r|Z$WGv6{73w< z#B+9t^`N9rq1Vdj?hhMj-ocqf(kuiR-mq6BdPR9wlqS#m_uRKl-W#(HjRyh#hcB7C z#Imu4Ou*3=^T;6Hf_%I_M^y3IeK=cPz(WzLg@Gw!zDQosm#9lB@+C?aWHQRA9JV+v zGFWqL#1BP)++ji`KnsM8j4~_p$D|%4iEkRiU;@;dJEKXfnArT&1Ns%-j zs({i8i)F54*F5}2O>(jK2Pp~`h+>4trdZ2_)zK8_v+!X#NkTFtIzAw&ZwpkUdKp>_ zNDp5(aRvyd0*ZShklAdf^nj6#QbcDg>lSYjv_h5g-o@K^gpPIZScjB@}k|cH&@?s|yFbGRc zQVvoJRDwv60g6?aM-n!)f_v&4PMW9{qQfFur;;xC*U)<0Rhg(_)ce54&ETFwshfCX znz+sxeov;>BXPZuy!#mij$~3=^ORwIp(Toq_zV2(fB3L}dYV|h0@Fr}V2WC7h)Bh$ z(|9^L9Z*iwsEn1^Hm51@xbe7JQ|n=D1_NYbid=L}!Ok{xMK z^T`F04~9Z9!o6gD8;okiX*+V#dBik}jh~6{jB!T6L(^y`Wj8||hni>5qz3vDFia}7 zS}{I5GkXHMR}>V@Vw96@O!h8W&>l&htDF5V1--~aRf!q?j5 z9=+RX6RNji;>IX*#Bk1n7Su4n`hLpoPr^WBqrP-br(FAw16rXOm3j{14&SrDtP!=` zp$(mFz)No)CVvq-9304SD276gF;*}VD>nCvUNR^oJ9hg}EY`8k3uuh|N<-X5ETDw) z5>k%1hq0S0)kQ;yB3&&4oMf1f7rGZMFom(vdzI?KDyLBcLb5kfp|jGB62k7|or?9> zOsJX8NN$5m#IRVbgR;PaC0eS$IAKO1{OkRV=@hom!^ciy}UNVc* zcb@VSXz@E|5GB(F4d3^jp&ikkeIjZftP#y-qRc>d8TY35p-JmZWjy{a$w!x0<+V)S zE}##eoQaZt@-^>I+1VojZJ4GT^r1jD@$Ym_hI|K;sOW0wgcCcr60i8eaf8rjqG2G) z&)k$v=a(A2USRi?5yIJLOkFsVopNq%MFs>MH2nW9r= zL78-3WDAW`k~cZTu&_W%bf%*uXss{}2!&E}q-oYijF?XPU{yAcV6Ir87n~D)Pj4Ot zDK{A)mUB&))^ekAk^D%?kw{X)del_#Y)}{;;ZZ#m9axyitHlsV>eNkBJC;!8hFsLy(%LG-s+%ie=t zOE3oxOmF)%Eefq$Q*oG|?Z=!^G8xheMxm+d=zVQ^6fyH<=_gs^xNCIqer+%XegNbJ zU2l+D?f}iqt=6eQQsx9-P=#Q8T59@e>lr}(#7adkmhywhLEe^r= zEN4$Zt(VkZ#tmf&{?p8*sXTI#*^MRE=2~{uNQcCz#UC>Gje>8Ex7GCUQ9M1Mm;q3tuG!Y zRZA*oiF44C_R(dVVl``@U2=5k&!P|Whr>qutPvlCEiXz#NmneKOL4u-#WYB(em~0J z9Xpru@uwaget=WrCTyO7FS2U!l%+!2#dD&J=16!s>~Rw}jV1bd3GmsCSY)i>AFwJh zw62;P?!_-)uRwJjzyTNwq?7k%4!iKtlvy6YWFNUx)iKK-`W_<}R9biBe8uyO(g=sN z(#^)8LkMDnTsEmv7^>JMokTXajupznQJOQZODCx$(f!r_g4!iW)t2vDk1S|~8b#u% z6AN%qRv0Sk0o0pkM@F}=()LkUV9EF~$wIezh9>2e*5e*XA;b>(lN3qLH44tl%FTaL zgX6@S1Ad>N*nc&dBU}7n`Je2N;=9`RNV{MU)e*u$Kp=uvuN7e8iLuDZ&wO~`K9Rd{Yo8Ei5#NJNq57cGPZO>+Df z{S*BN1Xj z9zCIfPgFy~Q_J$lvp3P->@BE`_Y>zXdU*wZvKPP{k>2s=kx^!cy;Py)fjRN0f)n+@ zKW%?!X9p+=$P~UAdgdLFtR}recoUELE4%|mcK<&KzJ0?gx{i42K@cS`JlAGVSs7i7 zIUw)Q8-GW5K${<6kt@Hsi+&qT)H$f?Yv4a2{_{xxvGJCMEz88ry))klrj}{D4^6s^ zp)&~CgpEeyJ*blK#_>I>Lclz}gH()plw8rSd!QA#!wCzi8Bt3WHB`Y@M0ZT~@Bjwm z6-T4#$PWPN13vlzh-Z(@-RSeh4TDCoPF!E(>iX>F!?Ah!?%ny>MW3diZ0znP+w;Hh z>H$NjV02h&fWqPfuA8ltDa~jqn9iu%C>ENXc-An8M@W|UYvbYyX!zyDyR-M^)ph?} z|2mqu@f)n2ZE#t~ljD=0So`ENc0@*;Z45wqY0YLY59oE^OLsO+X4w^io=mBL=fIl+ z!yPi1!gfj^MKyzO32(=1uI-V#M|6b4&AECE?|~VTw49j+$!7w66a!tl5ocjo@} z!m6@+1D|1D9pC=cNV>&yB|Q;7&=-F{fQf=IXr`J7yGpAgQsQ%fSLV%8<0Azp{{X(X zFfqo6{*fBzPhlYfrlneCH;C0|WVNp%V2WRqT74Q~RhEyS9Hy6_9w=x1>1N^woQ}pwN<{By2(Xtv ze@%3-=CmszT)QG|*kNDvE?P6ITWDuS`b@KeV<~z()NXvHCF;_MPs7e&0wjS|7wJ=O zclwt3fpDtwH#E&VgQ*SE40$b3nHmipKShZ+HHv?a$Pz_(zV4|r=8i(Zm3-=Q@|b`Q zD^(;6yf9DCu7UK@3LzZ(RU@KDAjl9i%C<2AYr^ghhIgzK zq&3|4Fy49;94J@L4@WXIeyx1k)k{lIb@pkj>`%`O{DsO`(`0d$rixU^o#oR+Q9Ly| zJBND&xo&jz9Z0>hhgLaN>f~~oAdX7uHf$oAx8gk~P6+!v%JikKo|uvw5i>ZzXfZ^c z?EWa`rA6WWfk15O>d}Q%F~A<3XIDAO5R_EaD9Rlu8c5hgC%_Mh)|az<`UzCFrSeed z+jiV#FJ2awi17oHg2nVc{cy|G-t3PJVq&G4+KOr9A=N`s+)AK2^z6WW5i<1Ji>e2p z-3h25#WfJe{70-_GTtfsa23qdI9R~4WK{`9fP7y_8A7pYOVJBUi0dOavbGUDB zkjSMZdQLM~943&XXoTckQq;Fxwsn;JSfPc@L{gJbQ$S4>JwC_~T}9srw^7|`CXWTW z;yNLpB;D&JX(apAz&~3iHkXAa_L)h;m#mRM#nV$AOh3_sPTc_liqd-|h2e?7$G3*Y za!Q2u^!WDp?eR_D{CIsX%T&mX#ePbOgL?JKqtK3l9jXP3L=j_Ojy!2TlT;>zYrbBj z&>!H7GKyL9?Ve#QeCAgMZfq%4A_vCLxO?GK2tXNIYQXqV-*nsiWcZL_qO$b_5^Eq&`2%5 zlze9we?sq|uh?I-ekNj=z1UZb8muSF2d2c^H{=id&pr6(0FE;-q4;@5YG&4h*%m^0hwX(~*n8Awro1$tim zdu%>N0!e#EHM23Q2_62?D>dO}$&9S^LBevcQm+C~n7#RANG>jKk^fE6GhirydoN0- z7w>kWKsiII*RWJ$Sn%PCJ(y!DKB3Z}F!!-P4rHd6+KJoD=TWx6t)UztlJTx7l}!yv zcae${Y{Q&@6@5* z!wU*82*?|Lt90bKAHPf9I!Q>G4ELB-(Q9%SBV)jAJ{!Op{#3xwO+s zJh3cMwyu(>6U8RJj34{fkI;U(ev)Dr011E~2vTy+*8?6BSt3CE+r{p0cd^lU9N|?o zc)=&Fi?_99M4iM$YkEMae=tTBeukVs*L&^<)k@P>gKUN6HIS_`t11<+xep)u6%US5 z8g)hU3{so>KPE9o$rK*uSu!o!B@!0dhXc5^*rF3L*>Av&@%O^d#{)oAN6UDT9TTL@ z%V;=&ASD3RM9|fW(lwKO-zc5SLf{{rOY5zoK9MUh9bW}uJ4hVZ2BVHX?pLy-vR<|I zOrjV1#|WCy8z?iJjGq@7f=1sNQuzXL?kkSP4HR@2_14LFoSTvMLjNq~hI~$NWC9k` zn8t=cyb5XbhF^sxj5}|IV=bVkW$>}6@d(U8H`aRtrwRx!nI?un%R(pWWhVGv2B?*v zh^EOMfI{@_BRckS41cyBZ2xV0wmsVZe*4w-+XwC29OOqt&@1ZS1tgYt+6K#3Fo#q8 zkBRG1ff$1~stgbS9h1(tA`RtLArj0~U63of07HD3r*oc0J3}f)mmzAGT0^GRLx`GP zKP@NJvpPo&$oJHdQ{2lzp^xnnexijxq!mbho^lIEwvCFQvuA~$3sg&3?&=eq#3#?_ z&2C8e<-4H_Jr|LbvQ`;8DPowPCS+WQ^-4pfP%rGAad9_P;hyE#XHERm0t2mr*hQGV z97MMR(sMYTPHGpbptrQ4S>XsVwpt3&m*BBgn5;?hRiOT{{#Vd)3yOTlKv$cAJIBFu zOcwxjeO06)SnIw3YzFPkV8sG7jAX=KEkVUVJOY^yMY%ukA;d~CkM6<&C(;;qgJ{?4 z7qkvdu{Y?CJw2eQw8WFfqcf4%v0HhVvP?0$%yj!qp3rL_W(IafQx8}-O9h<*{~}@? zxQHEo1!K(UvdoUcdTy}tO{QhP!jUV@e%oNh)2Q=SeCE8|W~1W`CcaV$j$z}`=dzzH zxJujLyI0&Ul!+0RK(|6>fD2 zzslDNT9Jtq=rX;^wg#a<`<>B{4{@yz zfHa03(WEjGz7N}7G=+x&5;yslb0xj&pN)1%HN>3#vE3vvKy~C2Gt{S+pfF;)%mONT2&Cvg&XQ*+P)ocNB$`EleQ4~)5XZ2 zE{*)*@d+l}RXFYa7TWF*$jJP&X&2~I=(Hu(7AC`Q!PrUlzg|Z;9vZfx4oh^oW#g)Y z;^Jb^O9Zm>)egQD2Wsn~=s)s<9ZByEBqq;&-oY7rFu#gg`-%u2v&*+6zw6x;A1q zp!1HgrK;1!_Ef2Tx>%kvHBX!JQ^zPsD-dzHFm2UrNvkYHB5JFK%>-IzGMG~?&dth)J8Bfg}lQcCEsmZGa{2}ZexIi8F9T0wq3l>Nw>tud-O zZ^}n5ElW9(7HuvY5=bf)JgD23eAuGC=*CvwYecvo@9sVgs->5F|KU0hY19x+St*Do z)M1Oerb$LlyLJDF8RQ|CDdRb9$<;J|h1N~#c87IkyCE`G(=zg!PB~1{=B|!7#!7zB zM8m1ndSV_(IWufEIDUn076Ln~-}qvM*>iG#4`71>U+{0F_U16y@EUDm8I@ZlHRvp@!EdpoUza@*v6GxCI&{T5oo)tjh(WDG}w(T}EZ> zKvKqx&x7Mikh{iF7<0m$8ZlVXs;LTXq1KG&p52=6bWE{Wq5nd<^#Q_Hlbd7}%{u;q zW@v{Cb|F+-=k|1=b&;A<_sYzYLfMH^TTd}B2^$sYxJ>z939EV zUoMZD&@vmqDB~Ew6MEm)0%!zB`?6RLFufkAVoo`SJK=2KF6qRfhoH~QL)7z`gYxG3`prgF*{`m3|T=s|NA`tG!HDs z?paC$blZ8BqQHOtC5i%yJyiv;r0An;t0uN!!#H?D&n}833&v4u6s6Hjq`6ZS#@)#L zFm*$gYEW*7K2KKEt+kr?o8qAN1#y#?_torA`AT8B^NJ40qkg;-S+ zH?!qZ;B){7E)Q7o8QUVQk7uftA%|%&n7k7tvWWH)hS$u&6t*W!l*>xBPb${qPaiW1>(OufL{J zG}ArsUXsB$THSbeF?HiWe19~@RA`t->oQ#t=@Rfa44;D=s3+qaYdvC+i28{JWwZxQ z8X+Q9>X>K0cw3kNQubA+u*qZ&V0Z%Cj?#d)j)At+VHd*bAopx?%`S5q0*PWwZFd?c ziOGN0#>iM@RMbyt8GKzY21{kW>@f&|iPzaz9n&e_Gs-Z$VlNSMhAx{2k`}PrJSSZw}=APrZXD zy`Ibe^fk(R$krGxVxZY`k0B3%F3>9Qc*Z&M0No*hCnlRCUmuNV2JXR>rj^TX@L)OF zsEQC0jyFn&5xpKSnf#KaWd_lfM|ttUE~4QL($zHpbcg=?;2C6{JbDqFd!QN8?w*|< zoxC~?XCug#A&w|am(%3jyN+kk0`Au$&vI*iZpaV3?<;;^MvFL^pud3d#1%RI)60;Y z(mSkEyi(wu)vhs$hbg!+a8L1|#O)yPPOP7l=VBDWEk;Dhn?z(zXgVIK{@vl@@Y}H0 z`Qv`L+wuQE{(2fdfy$TZWIAGg3^3h4r{wIWt3aFV4&!A+R)RhVN;!D}G|AowKZmnH z63@vWgGJsWkZj0C`6Zg4>vk1SIZpQwzke){`B9%e#oW4rFx+8X#B96a>hr3v+sB88 zLJg%!ZGhpS|0VC^2XcCz|4#V-n?L`l`{MQUAJ%-%@Y`@NTwVKh&o?sL*)-+%ey`1JML zQ||~5guc(XC}FphH+5;MrM{0m4Vc+p_#}Ktx!NxO(BmKW_=m^*!#@9Tz(0J$Kah63 zW2y&FBJyrBoU7%4@Ui)o|B}eBDI6gEKF{_joxnkD!uwu_Uc!uKleP250I%R%%avgq@=Z}{?nIF;lv*ic0TzAxO3XsFDYM=QOb-NI0&OjV~jVm_elulP%am5u^Tyez}S6p$$6<1tw#T8dvam5u^ lTyez}S6p$$6<1tw#T8dvam5u^TybUn@;}IZmInZ!1psZEODg~X literal 0 HcmV?d00001 From 3e2845181c5f33980ffbdb96714f6733439bd93a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 17:38:09 -0700 Subject: [PATCH 13/25] bumping next version --- ui/litellm-dashboard/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 2a9bb3e5e20..fce2e09b54c 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -35,7 +35,7 @@ "jwt-decode": "^4.0.0", "lucide-react": "^0.513.0", "moment": "^2.30.1", - "next": "^16.1.6", + "next": "^16.1.7", "openai": "^4.93.0", "papaparse": "^5.5.2", "react": "^18.3.1", From 62835ff03dbbcd4d02cc49163a99a1e726ff2dd9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 17:44:01 -0700 Subject: [PATCH 14/25] adding package-lock --- ui/litellm-dashboard/package-lock.json | 164 ++++++++++++++++++------- 1 file changed, 118 insertions(+), 46 deletions(-) diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index c062356ebbd..2b62c1c16bb 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -23,7 +23,7 @@ "jwt-decode": "^4.0.0", "lucide-react": "^0.513.0", "moment": "^2.30.1", - "next": "^16.1.6", + "next": "^16.1.7", "openai": "^4.93.0", "papaparse": "^5.5.2", "react": "^18.3.1", @@ -92,6 +92,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1773,6 +1774,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1783,6 +1785,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1792,12 +1795,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1828,9 +1833,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", + "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1844,9 +1849,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", + "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", "cpu": [ "arm64" ], @@ -1860,9 +1865,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", + "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", "cpu": [ "x64" ], @@ -1876,9 +1881,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", + "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", "cpu": [ "arm64" ], @@ -1892,9 +1897,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", + "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", "cpu": [ "arm64" ], @@ -1908,9 +1913,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", + "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", "cpu": [ "x64" ], @@ -1924,9 +1929,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", + "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", "cpu": [ "x64" ], @@ -1940,9 +1945,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", + "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", "cpu": [ "arm64" ], @@ -1956,9 +1961,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", + "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", "cpu": [ "x64" ], @@ -1975,6 +1980,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1988,6 +1994,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1997,6 +2004,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2320,7 +2328,7 @@ "version": "1.58.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz", "integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.58.1" @@ -3425,12 +3433,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.2.48", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3472,6 +3482,7 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", + "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -4332,12 +4343,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4351,6 +4364,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4363,6 +4377,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -4734,6 +4749,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4759,6 +4775,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4874,6 +4891,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -4997,6 +5015,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -5021,6 +5040,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5096,6 +5116,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5156,6 +5177,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -5569,12 +5591,14 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -6488,6 +6512,7 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6520,6 +6545,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6557,6 +6583,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -6717,6 +6744,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6867,6 +6895,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7364,6 +7393,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7416,6 +7446,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7476,6 +7507,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7521,6 +7553,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7569,6 +7602,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7845,6 +7879,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -8130,6 +8165,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -8142,6 +8178,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -8595,6 +8632,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -9167,6 +9205,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9180,6 +9219,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9294,6 +9334,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -9343,14 +9384,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", + "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.1.7", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -9362,14 +9403,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", + "@next/swc-darwin-arm64": "16.1.7", + "@next/swc-darwin-x64": "16.1.7", + "@next/swc-linux-arm64-gnu": "16.1.7", + "@next/swc-linux-arm64-musl": "16.1.7", + "@next/swc-linux-x64-gnu": "16.1.7", + "@next/swc-linux-x64-musl": "16.1.7", + "@next/swc-win32-arm64-msvc": "16.1.7", + "@next/swc-win32-x64-msvc": "16.1.7", "sharp": "^0.34.4" }, "peerDependencies": { @@ -9505,6 +9546,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9523,6 +9565,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9867,6 +9910,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -9913,6 +9957,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9925,6 +9970,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9934,6 +9980,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9943,7 +9990,7 @@ "version": "1.58.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.58.1" @@ -9962,7 +10009,7 @@ "version": "1.58.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9985,6 +10032,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10013,6 +10061,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -10030,6 +10079,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10055,6 +10105,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10097,6 +10148,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10122,6 +10174,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -10135,6 +10188,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -10248,6 +10302,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -11036,6 +11091,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -11045,6 +11101,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -11057,6 +11114,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -11354,6 +11412,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -11394,6 +11453,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -11449,6 +11509,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -12089,6 +12150,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -12124,6 +12186,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12159,6 +12222,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -12196,6 +12260,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -12212,6 +12277,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12239,6 +12305,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -12248,6 +12315,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -12289,6 +12357,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -12355,6 +12424,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -12442,6 +12512,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { @@ -12558,7 +12629,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12760,6 +12831,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -13213,7 +13285,7 @@ "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" From bae2eddd73998a767cfa4858310a2c47bc5ec62a Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 17 Mar 2026 17:50:58 -0700 Subject: [PATCH 15/25] docs fix sidebar --- docs/my-website/sidebars.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 1362745a91f..79a0279bad5 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -631,6 +631,7 @@ const sidebars = { "mcp_openapi", "mcp_oauth", "mcp_aws_sigv4", + "mcp_zero_trust", "mcp_public_internet", "mcp_semantic_filter", "mcp_control", From 88f59e1465ae55097abc3520e1f6d271b5f39bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= <258577966+voidborne-d@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:54:23 +0000 Subject: [PATCH 16/25] fix: use AsyncMock for concurrent test consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from greptile — use new_callable=AsyncMock on the concurrent test's patch.object to ensure the mock is properly typed as async, even though side_effect already handles the coroutine. --- tests/test_litellm/proxy/test_aiohttp_session_recovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py index f089d10425c..29bd9a491b7 100644 --- a/tests/test_litellm/proxy/test_aiohttp_session_recovery.py +++ b/tests/test_litellm/proxy/test_aiohttp_session_recovery.py @@ -167,6 +167,7 @@ async def test_add_shared_session_concurrent_recreation_uses_lock(): with patch.object( proxy_server_module, "_initialize_shared_aiohttp_session", + new_callable=AsyncMock, side_effect=mock_init, ): # Launch 5 concurrent calls From 92b89353ae6017bcc2cec821c4a9c6a71c6e0da5 Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Mon, 16 Mar 2026 23:22:00 +0100 Subject: [PATCH 17/25] fix: surface Anthropic code execution results as code_interpreter_call in Responses API PR #18945 added support for capturing Anthropic server-side tool results (bash_code_execution_tool_result, etc.) in provider_specific_fields, but the data never reached the Responses API output because: 1. Non-streaming: provider_specific_fields wasn't copied into _hidden_params 2. Streaming: chunk delta's provider_specific_fields wasn't accumulated 3. Tool results weren't mapped to standard output items This fix: - Copies provider_specific_fields to _hidden_params in transform_response() - Accumulates provider_specific_fields from streaming chunk deltas - Maps bash_code_execution_tool_result to code_interpreter_call output items with code and outputs (matching OpenAI's native shape) - Removes redundant function_call items for server-side tools - Adds OutputCodeInterpreterCall type to the output union --- litellm/llms/anthropic/chat/handler.py | 86 +- litellm/llms/anthropic/chat/transformation.py | 79 +- .../streaming_iterator.py | 52 +- .../transformation.py | 57 +- litellm/types/llms/openai.py | 55 +- litellm/types/responses/main.py | 18 + .../chat/test_anthropic_chat_handler.py | 247 ++++- .../test_anthropic_chat_transformation.py | 889 +++++++++--------- 8 files changed, 971 insertions(+), 512 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 5eebebc2e23..51b9c9835a7 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -48,6 +48,10 @@ from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ) +from litellm.types.responses.main import ( + OutputCodeInterpreterCall, + OutputCodeInterpreterCallLog, +) from litellm.types.utils import ( Delta, GenericStreamingChunk, @@ -538,6 +542,11 @@ class ModelResponseIterator: # Accumulate compaction blocks for multi-turn reconstruction self.compaction_blocks: List[Dict[str, Any]] = [] + # Track server tool use inputs and results for code_interpreter_results + self._server_tool_inputs: Dict[str, Any] = {} + self.tool_results: List[Dict[str, Any]] = [] + self._last_code_interpreter_results_count: int = 0 + def check_empty_tool_call_args(self) -> bool: """ Check if the tool call block so far has been an empty string @@ -568,9 +577,7 @@ class ModelResponseIterator: speed=self.speed, ) - def _content_block_delta_helper( - self, chunk: dict - ) -> Tuple[ + def _content_block_delta_helper(self, chunk: dict) -> Tuple[ str, Optional[ChatCompletionToolCallChunk], List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]], @@ -682,6 +689,44 @@ class ModelResponseIterator: return content_block_start + def _build_code_interpreter_results(self) -> list: + """Convert accumulated tool_results to OutputCodeInterpreterCall objects. + + Called during streaming to produce provider-neutral code_interpreter_results + alongside the raw tool_results, so the Responses API layer doesn't need + Anthropic-specific knowledge. + """ + # Only convert tool_results added since the last call to avoid + # duplicates when _merge_provider_specific_fields extends the list. + new_results = self.tool_results[self._last_code_interpreter_results_count :] + self._last_code_interpreter_results_count = len(self.tool_results) + results = [] + for tr in new_results: + call_id = tr.get("tool_use_id", "") + content = tr.get("content", {}) + if isinstance(content, dict): + parts = [] + if content.get("stdout"): + parts.append(content["stdout"]) + if content.get("stderr"): + parts.append(f"STDERR: {content['stderr']}") + logs = "".join(parts) if parts else str(content) + else: + logs = str(content) + tool_input = self._server_tool_inputs.get(call_id, {}) + code = tool_input.get("command", "") if isinstance(tool_input, dict) else "" + results.append( + OutputCodeInterpreterCall( + type="code_interpreter_call", + id=call_id, + code=code, + container_id=None, + status="completed", + outputs=[OutputCodeInterpreterCallLog(type="logs", logs=logs)], + ) + ) + return results + def chunk_parser(self, chunk: dict) -> ModelResponseStream: # noqa: PLR0915 try: type_chunk = chunk.get("type", "") or "" @@ -748,6 +793,17 @@ class ModelResponseIterator: ), index=self.tool_index, ) + # Track server tool use inputs for code_interpreter_results + if ( + content_block_start["content_block"]["type"] + == "server_tool_use" + ): + tool_input = content_block_start["content_block"].get( + "input", {} + ) + self._server_tool_inputs[ + content_block_start["content_block"]["id"] + ] = tool_input # Include caller information if present (for programmatic tool calling) if "caller" in content_block_start["content_block"]: caller_data = content_block_start["content_block"]["caller"] @@ -768,9 +824,9 @@ class ModelResponseIterator: # Handle compaction blocks # The full content comes in content_block_start self.compaction_blocks.append(content_block_start["content_block"]) - provider_specific_fields[ - "compaction_blocks" - ] = self.compaction_blocks + provider_specific_fields["compaction_blocks"] = ( + self.compaction_blocks + ) provider_specific_fields["compaction_start"] = { "type": "compaction", "content": content_block_start["content_block"].get( @@ -792,9 +848,9 @@ class ModelResponseIterator: self.web_search_results.append( content_block_start["content_block"] ) - provider_specific_fields[ - "web_search_results" - ] = self.web_search_results + provider_specific_fields["web_search_results"] = ( + self.web_search_results + ) elif content_type == "web_fetch_tool_result": # Capture web_fetch_tool_result for multi-turn reconstruction # The full content comes in content_block_start, not in deltas @@ -802,16 +858,18 @@ class ModelResponseIterator: self.web_search_results.append( content_block_start["content_block"] ) - provider_specific_fields[ - "web_search_results" - ] = self.web_search_results + provider_specific_fields["web_search_results"] = ( + self.web_search_results + ) elif content_type != "tool_search_tool_result": # Handle other tool results (code execution, etc.) # Skip tool_search_tool_result as it's internal metadata - if not hasattr(self, "tool_results"): - self.tool_results = [] self.tool_results.append(content_block_start["content_block"]) provider_specific_fields["tool_results"] = self.tool_results + # Convert to provider-neutral code_interpreter_results + provider_specific_fields["code_interpreter_results"] = ( + self._build_code_interpreter_results() + ) elif type_chunk == "content_block_stop": ContentBlockStop(**chunk) # type: ignore diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 47cdd8287e0..033afea2ffa 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -59,6 +59,10 @@ from litellm.types.utils import ( PromptTokensDetailsWrapper, ServerToolUse, ) +from litellm.types.responses.main import ( + OutputCodeInterpreterCall, + OutputCodeInterpreterCallLog, +) from litellm.utils import ( ModelResponse, Usage, @@ -960,11 +964,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if mcp_servers: optional_params["mcp_servers"] = mcp_servers elif param == "tool_choice" or param == "parallel_tool_calls": - _tool_choice: Optional[ - AnthropicMessagesToolChoice - ] = self._map_tool_choice( - tool_choice=non_default_params.get("tool_choice"), - parallel_tool_use=non_default_params.get("parallel_tool_calls"), + _tool_choice: Optional[AnthropicMessagesToolChoice] = ( + self._map_tool_choice( + tool_choice=non_default_params.get("tool_choice"), + parallel_tool_use=non_default_params.get("parallel_tool_calls"), + ) ) if _tool_choice is not None: @@ -1062,9 +1066,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): self.map_openai_context_management_to_anthropic(value) ) if anthropic_context_management is not None: - optional_params[ - "context_management" - ] = anthropic_context_management + optional_params["context_management"] = ( + anthropic_context_management + ) elif param == "speed" and isinstance(value, str): # Pass through Anthropic-specific speed parameter for fast mode optional_params["speed"] = value @@ -1138,9 +1142,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): text=system_message_block["content"], ) if "cache_control" in system_message_block: - anthropic_system_message_content[ - "cache_control" - ] = system_message_block["cache_control"] + anthropic_system_message_content["cache_control"] = ( + system_message_block["cache_control"] + ) anthropic_system_message_list.append( anthropic_system_message_content ) @@ -1164,9 +1168,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) ) if "cache_control" in _content: - anthropic_system_message_content[ - "cache_control" - ] = _content["cache_control"] + anthropic_system_message_content["cache_control"] = ( + _content["cache_control"] + ) anthropic_system_message_list.append( anthropic_system_message_content @@ -1463,9 +1467,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) return _message - def extract_response_content( - self, completion_response: dict - ) -> Tuple[ + def extract_response_content(self, completion_response: dict) -> Tuple[ str, Optional[List[Any]], Optional[ @@ -1749,6 +1751,48 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): provider_specific_fields["web_search_results"] = web_search_results if tool_results is not None: provider_specific_fields["tool_results"] = tool_results + # Convert to provider-neutral OutputCodeInterpreterCall objects + # so the Responses API layer can use them without Anthropic-specific knowledge. + container_id = ( + completion_response.get("container", {}).get("id") + if isinstance(completion_response.get("container"), dict) + else None + ) + code_by_id: Dict[str, str] = {} + for tc in tool_calls: + try: + args = json.loads(tc.get("function", {}).get("arguments", "{}")) + code_by_id[tc.get("id", "")] = args.get("command", "") + except Exception: + pass + code_interpreter_results = [] + for tr in tool_results: + call_id = tr.get("tool_use_id", "") + content = tr.get("content", {}) + if isinstance(content, dict): + parts = [] + if content.get("stdout"): + parts.append(content["stdout"]) + if content.get("stderr"): + parts.append(f"STDERR: {content['stderr']}") + logs = "".join(parts) if parts else str(content) + else: + logs = str(content) + code_interpreter_results.append( + OutputCodeInterpreterCall( + type="code_interpreter_call", + id=call_id, + code=code_by_id.get(call_id, ""), + container_id=container_id, + status="completed", + outputs=[ + OutputCodeInterpreterCallLog(type="logs", logs=logs) + ], + ) + ) + provider_specific_fields["code_interpreter_results"] = ( + code_interpreter_results + ) if container is not None: provider_specific_fields["container"] = container if compaction_blocks is not None: @@ -1794,6 +1838,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): model_response.created = int(time.time()) model_response.model = completion_response["model"] + _hidden_params["provider_specific_fields"] = provider_specific_fields model_response._hidden_params = _hidden_params return model_response diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index ce037850b86..0b7d6e8a7a4 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -107,6 +107,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): self._reasoning_done_emitted = False self._reasoning_item_id: Optional[str] = None self._accumulated_reasoning_content_parts: List[str] = [] + self._accumulated_provider_specific_fields: Dict[str, Any] = {} def _get_or_assign_tool_output_index(self, call_id: str) -> int: existing = self._tool_output_index_by_call_id.get(call_id) @@ -479,16 +480,37 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): event.__dict__["sequence_number"] = self._sequence_number return event - def create_litellm_model_response( - self, - ) -> Optional[ModelResponse]: - return cast( + def _merge_provider_specific_fields(self, src: dict) -> None: + """Merge provider_specific_fields, extending list values instead of replacing.""" + for key, val in src.items(): + existing = self._accumulated_provider_specific_fields.get(key) + if ( + existing is not None + and isinstance(val, list) + and isinstance(existing, list) + ): + existing.extend(val) + else: + self._accumulated_provider_specific_fields[key] = val + + def create_litellm_model_response(self) -> Optional[ModelResponse]: + response = cast( Optional[ModelResponse], stream_chunk_builder( chunks=self.collected_chat_completion_chunks, logging_obj=self.litellm_logging_obj, ), ) + if response is not None and self._accumulated_provider_specific_fields: + if ( + not hasattr(response, "_hidden_params") + or response._hidden_params is None + ): + response._hidden_params = {} + response._hidden_params.setdefault("provider_specific_fields", {}).update( + self._accumulated_provider_specific_fields + ) + return response @staticmethod def _snapshot_chunk_for_stream_chunk_builder( @@ -853,6 +875,17 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): if chunk is not None: chunk = cast(ModelResponseStream, chunk) self._ensure_output_item_for_chunk(chunk) + # Accumulate provider_specific_fields from chunk and delta + for src in ( + getattr(chunk, "provider_specific_fields", None), + getattr( + chunk.choices[0].delta if chunk.choices else None, + "provider_specific_fields", + None, + ), + ): + if src and isinstance(src, dict): + self._merge_provider_specific_fields(src) # Proceed to transformation self.collected_chat_completion_chunks.append( self._snapshot_chunk_for_stream_chunk_builder(chunk) @@ -964,6 +997,17 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): try: chunk = self.litellm_custom_stream_wrapper.__next__() self._ensure_output_item_for_chunk(chunk) + # Accumulate provider_specific_fields from chunk and delta + for src in ( + getattr(chunk, "provider_specific_fields", None), + getattr( + chunk.choices[0].delta if chunk.choices else None, + "provider_specific_fields", + None, + ), + ): + if src and isinstance(src, dict): + self._merge_provider_specific_fields(src) # Emit any just-queued output_item event if self._pending_response_events: return self._pending_response_events.pop(0) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 71fa88fb751..b54d5930efc 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -42,6 +42,7 @@ from litellm.types.llms.openai import ( from litellm.types.responses.main import ( GenericResponseOutputItem, GenericResponseOutputItemContentAnnotation, + OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, OutputText, @@ -1696,6 +1697,7 @@ class LiteLLMCompletionResponsesConfig: ) -> List[ Union[ GenericResponseOutputItem, + OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, ResponseFunctionToolCall, @@ -1704,6 +1706,7 @@ class LiteLLMCompletionResponsesConfig: responses_output: List[ Union[ GenericResponseOutputItem, + OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, ResponseFunctionToolCall, @@ -1725,8 +1728,56 @@ class LiteLLMCompletionResponsesConfig: chat_completion_response=chat_completion_response ) ) + + # Convert server-side tool results (e.g. Anthropic code execution) + # into code_interpreter_call output items, replacing the corresponding + # function_call items so the output matches OpenAI's native shape. + tool_result_items = ( + LiteLLMCompletionResponsesConfig._extract_tool_result_output_items( + chat_completion_response + ) + ) + if tool_result_items: + result_by_id = {item.id: item for item in tool_result_items} + replaced_ids = set(result_by_id.keys()) + responses_output = [ + ( + result_by_id[getattr(item, "call_id", None)] + if ( + getattr(item, "type", None) == "function_call" + and getattr(item, "call_id", None) in replaced_ids + ) + else item + ) + for item in responses_output + ] + return responses_output + @staticmethod + def _extract_tool_result_output_items( + chat_completion_response: ModelResponse, + ) -> list: + """Extract pre-built code_interpreter_call output items from provider_specific_fields. + + Provider transformers (e.g. Anthropic) convert their native tool results + into OutputCodeInterpreterCall objects and store them in + provider_specific_fields["code_interpreter_results"]. This method + simply retrieves them — no provider-specific parsing here. + """ + output_items: list = [] + for choice in chat_completion_response.choices or []: + message = getattr(choice, "message", None) + if not message: + continue + psf = getattr(message, "provider_specific_fields", None) + if not psf or not isinstance(psf, dict): + continue + results = psf.get("code_interpreter_results") + if results and isinstance(results, list): + output_items.extend(results) + return output_items + @staticmethod def _extract_reasoning_output_items( chat_completion_response: ModelResponse, @@ -2055,9 +2106,9 @@ class LiteLLMCompletionResponsesConfig: hasattr(completion_details, "reasoning_tokens") and completion_details.reasoning_tokens is not None ): - output_details_dict[ - "reasoning_tokens" - ] = completion_details.reasoning_tokens + output_details_dict["reasoning_tokens"] = ( + completion_details.reasoning_tokens + ) else: output_details_dict["reasoning_tokens"] = 0 diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index a2df3f2e0d6..a265198e6b8 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -84,6 +84,7 @@ from typing_extensions import Annotated, Dict, Required, TypedDict, override from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject from litellm.types.responses.main import ( GenericResponseOutputItem, + OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, ) @@ -969,12 +970,12 @@ class OpenAIChatCompletionChunk(ChatCompletionChunk): class Hyperparameters(BaseModel): batch_size: Optional[Union[str, int]] = None # "Number of examples in each batch." - learning_rate_multiplier: Optional[ - Union[str, float] - ] = None # Scaling factor for the learning rate - n_epochs: Optional[ - Union[str, int] - ] = None # "The number of epochs to train the model for" + learning_rate_multiplier: Optional[Union[str, float]] = ( + None # Scaling factor for the learning rate + ) + n_epochs: Optional[Union[str, int]] = ( + None # "The number of epochs to train the model for" + ) model_config = {"extra": "allow"} @@ -1003,18 +1004,18 @@ class FineTuningJobCreate(BaseModel): model: str # "The name of the model to fine-tune." training_file: str # "The ID of an uploaded file that contains training data." - hyperparameters: Optional[ - Hyperparameters - ] = None # "The hyperparameters used for the fine-tuning job." - suffix: Optional[ - str - ] = None # "A string of up to 18 characters that will be added to your fine-tuned model name." - validation_file: Optional[ - str - ] = None # "The ID of an uploaded file that contains validation data." - integrations: Optional[ - List[str] - ] = None # "A list of integrations to enable for your fine-tuning job." + hyperparameters: Optional[Hyperparameters] = ( + None # "The hyperparameters used for the fine-tuning job." + ) + suffix: Optional[str] = ( + None # "A string of up to 18 characters that will be added to your fine-tuned model name." + ) + validation_file: Optional[str] = ( + None # "The ID of an uploaded file that contains validation data." + ) + integrations: Optional[List[str]] = ( + None # "A list of integrations to enable for your fine-tuning job." + ) seed: Optional[int] = None # "The seed controls the reproducibility of the job." @@ -1242,6 +1243,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): List[ Union[ GenericResponseOutputItem, + OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, ResponseFunctionToolCall, @@ -1308,13 +1310,16 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): if not isinstance(serialized, list): return serialized return [ - { - k: v - for k, v in item.items() - if v is not None or k not in ("status", "content", "encrypted_content") - } - if isinstance(item, dict) and item.get("type") == "reasoning" - else item + ( + { + k: v + for k, v in item.items() + if v is not None + or k not in ("status", "content", "encrypted_content") + } + if isinstance(item, dict) and item.get("type") == "reasoning" + else item + ) for item in serialized ] diff --git a/litellm/types/responses/main.py b/litellm/types/responses/main.py index 7a666d5e65f..e46857565c7 100644 --- a/litellm/types/responses/main.py +++ b/litellm/types/responses/main.py @@ -49,6 +49,24 @@ class OutputImageGenerationCall(BaseLiteLLMOpenAIResponseObject): result: Optional[str] # Base64 encoded image data (without data:image prefix) +class OutputCodeInterpreterCallLog(BaseLiteLLMOpenAIResponseObject): + """Log output from a code interpreter call""" + + type: Literal["logs"] + logs: str + + +class OutputCodeInterpreterCall(BaseLiteLLMOpenAIResponseObject): + """A code interpreter / code execution call output""" + + type: Literal["code_interpreter_call"] + id: str + code: Optional[str] + container_id: Optional[str] + status: Literal["in_progress", "completed", "incomplete", "failed"] + outputs: Optional[List[OutputCodeInterpreterCallLog]] + + class GenericResponseOutputItem(BaseLiteLLMOpenAIResponseObject): """ Generic response API output item diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index d9f513d8d1d..35c7a62027b 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -6,6 +6,7 @@ from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ) +from litellm.types.responses.main import OutputCodeInterpreterCall def test_redacted_thinking_content_block_delta(): @@ -479,14 +480,22 @@ def test_partial_json_chunk_accumulation(): # First partial chunk should return None (still accumulating) result1 = iterator._parse_sse_data(f"data:{partial_chunk_1}") assert result1 is None, "First partial chunk should return None while accumulating" - assert iterator.chunk_type == "accumulated_json", "Should switch to accumulated_json mode" - assert iterator.accumulated_json == partial_chunk_1, "Should have accumulated first part" + assert ( + iterator.chunk_type == "accumulated_json" + ), "Should switch to accumulated_json mode" + assert ( + iterator.accumulated_json == partial_chunk_1 + ), "Should have accumulated first part" # Second partial chunk should complete the JSON and return a parsed result result2 = iterator._parse_sse_data(f"data:{partial_chunk_2}") assert result2 is not None, "Second chunk should return parsed result" - assert iterator.accumulated_json == "", "Buffer should be cleared after successful parse" - assert result2.choices[0].delta.content == "Hello", f"Expected 'Hello', got '{result2.choices[0].delta.content}'" + assert ( + iterator.accumulated_json == "" + ), "Buffer should be cleared after successful parse" + assert ( + result2.choices[0].delta.content == "Hello" + ), f"Expected 'Hello', got '{result2.choices[0].delta.content}'" def test_complete_json_chunk_no_accumulation(): @@ -503,7 +512,9 @@ def test_complete_json_chunk_no_accumulation(): assert result is not None, "Complete chunk should return parsed result immediately" assert iterator.chunk_type == "valid_json", "Should remain in valid_json mode" assert iterator.accumulated_json == "", "Buffer should remain empty" - assert result.choices[0].delta.content == "Hello", f"Expected 'Hello', got '{result.choices[0].delta.content}'" + assert ( + result.choices[0].delta.content == "Hello" + ), f"Expected 'Hello', got '{result.choices[0].delta.content}'" def test_multiple_partial_chunks_accumulation(): @@ -620,7 +631,9 @@ def test_web_search_tool_result_no_extra_tool_calls(): # Should have exactly 2 tool calls: # 1. From content_block_start (server_tool_use) with id and name # 2. From content_block_delta with the actual query - assert len(tool_calls_emitted) == 2, f"Expected 2 tool calls, got {len(tool_calls_emitted)}" + assert ( + len(tool_calls_emitted) == 2 + ), f"Expected 2 tool calls, got {len(tool_calls_emitted)}" # First tool call should have the id and name assert tool_calls_emitted[0]["id"] == "srvtoolu_01ABC123" @@ -722,7 +735,10 @@ def test_web_search_tool_result_captured_in_provider_specific_fields(): { "type": "content_block_delta", "index": 0, - "delta": {"type": "input_json_delta", "partial_json": '{"query": "otter facts"}'}, + "delta": { + "type": "input_json_delta", + "partial_json": '{"query": "otter facts"}', + }, }, # 4. content_block_stop for server_tool_use {"type": "content_block_stop", "index": 0}, @@ -822,7 +838,10 @@ def test_web_fetch_tool_result_captured_in_provider_specific_fields(): { "type": "content_block_delta", "index": 0, - "delta": {"type": "input_json_delta", "partial_json": '{"url": "https://example.com"}'}, + "delta": { + "type": "input_json_delta", + "partial_json": '{"url": "https://example.com"}', + }, }, # 4. content_block_stop for server_tool_use {"type": "content_block_stop", "index": 0}, @@ -946,7 +965,7 @@ def test_web_fetch_tool_result_no_extra_tool_calls(): def test_container_in_provider_specific_fields_streaming(): """ Test that container is captured in provider_specific_fields for streaming responses. - + When container with skills is used, the container field should be present in the provider_specific_fields of the message_delta chunk. """ @@ -1025,7 +1044,9 @@ def test_container_in_provider_specific_fields_streaming(): ] # Verify container was captured - assert container_field is not None, "container should be captured in provider_specific_fields" + assert ( + container_field is not None + ), "container should be captured in provider_specific_fields" assert ( container_field["id"] == "container_011CW9hA9zpZ8xD3bjjShy4p" ), "container id should match" @@ -1033,18 +1054,14 @@ def test_container_in_provider_specific_fields_streaming(): container_field["expires_at"] == "2025-12-16T04:57:16.913181Z" ), "expires_at should match" assert len(container_field["skills"]) == 1, "Should have 1 skill" - assert ( - container_field["skills"][0]["skill_id"] == "pptx" - ), "skill_id should be pptx" - assert ( - container_field["skills"][0]["version"] == "20251013" - ), "version should match" + assert container_field["skills"][0]["skill_id"] == "pptx", "skill_id should be pptx" + assert container_field["skills"][0]["version"] == "20251013", "version should match" def test_container_in_provider_specific_fields_non_streaming(): """ Test that container is captured in provider_specific_fields for non-streaming responses. - + When container with skills is used in non-streaming, the container field should be present in the provider_specific_fields of the response. """ @@ -1106,7 +1123,7 @@ def test_container_in_provider_specific_fields_non_streaming(): def test_container_absent_when_not_provided(): """ Test that container is not added to provider_specific_fields when not provided. - + This ensures we don't add empty or None container fields. """ iterator = ModelResponseIterator( @@ -1133,3 +1150,197 @@ def test_container_absent_when_not_provided(): assert ( "container" not in model_response.choices[0].delta.provider_specific_fields ), "container should not be present when not provided in delta" + + +def test_streaming_code_execution_produces_code_interpreter_results(): + """ + Test that bash_code_execution_tool_result content blocks in streaming + produce code_interpreter_results in provider_specific_fields, so the + Responses API layer can use them without Anthropic-specific knowledge. + """ + + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "", + }, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "Running code..."}, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01ABC", + "name": "bash_code_execution", + "input": {"command": "echo hello"}, + }, + }, + {"type": "content_block_stop", "index": 1}, + { + "type": "content_block_start", + "index": 2, + "content_block": { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01ABC", + "content": { + "type": "bash_code_execution_result", + "stdout": "hello\n", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 2}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + + found_code_interpreter_results = False + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + psf = None + if parsed.choices and parsed.choices[0].delta: + psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) + if psf and "code_interpreter_results" in psf: + found_code_interpreter_results = True + results = psf["code_interpreter_results"] + assert len(results) == 1 + assert isinstance(results[0], OutputCodeInterpreterCall) + assert results[0].type == "code_interpreter_call" + assert results[0].id == "srvtoolu_01ABC" + assert results[0].code == "echo hello" + assert results[0].outputs is not None + assert len(results[0].outputs) == 1 + assert results[0].outputs[0].logs == "hello\n" + + assert found_code_interpreter_results, ( + "code_interpreter_results should appear in provider_specific_fields " + "when bash_code_execution_tool_result is streamed" + ) + + +def test_streaming_multiple_code_executions_no_duplicates(): + """ + Test that multiple code executions in a single streaming response produce + exactly one code_interpreter_result per execution — no duplicates from + _build_code_interpreter_results rebuilding the full list. + """ + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + # First code execution + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01AAA", + "name": "bash_code_execution", + "input": {"command": "echo first"}, + }, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01AAA", + "content": { + "type": "bash_code_execution_result", + "stdout": "first\n", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 1}, + # Second code execution + { + "type": "content_block_start", + "index": 2, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01BBB", + "name": "bash_code_execution", + "input": {"command": "echo second"}, + }, + }, + {"type": "content_block_stop", "index": 2}, + { + "type": "content_block_start", + "index": 3, + "content_block": { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01BBB", + "content": { + "type": "bash_code_execution_result", + "stdout": "second\n", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 3}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + + # Collect ALL code_interpreter_results emitted across all chunks + all_results = [] + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + psf = None + if parsed.choices and parsed.choices[0].delta: + psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) + if psf and "code_interpreter_results" in psf: + all_results.extend(psf["code_interpreter_results"]) + + # Should have exactly 2 results, one per execution — no duplicates + assert len(all_results) == 2, ( + f"Expected 2 code_interpreter_results, got {len(all_results)}. " + f"IDs: {[r.id for r in all_results]}" + ) + assert all_results[0].id == "srvtoolu_01AAA" + assert all_results[0].code == "echo first" + assert all_results[0].outputs[0].logs == "first\n" + assert all_results[1].id == "srvtoolu_01BBB" + assert all_results[1].code == "echo second" + assert all_results[1].outputs[0].logs == "second\n" diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index a95b9413b9d..a3469964862 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -183,7 +183,9 @@ def test_extract_response_content_with_citations(): }, } - _, citations, _, _, _, _, _, _ = config.extract_response_content(completion_response) + _, citations, _, _, _, _, _, _ = config.extract_response_content( + completion_response + ) assert citations == [ [ { @@ -305,7 +307,7 @@ def test_web_search_tool_result_extraction(): "type": "server_tool_use", "id": "srvtoolu_01ABC123", "name": "web_search", - "input": {"query": "average weight african elephant kg"} + "input": {"query": "average weight african elephant kg"}, }, { "type": "web_search_tool_result", @@ -317,32 +319,39 @@ def test_web_search_tool_result_extraction(): "title": "African Elephant Facts", "encrypted_content": "encrypted_data_here", "page_age": "2024-01-15", - "snippet": "Adult African elephants weigh between 4,000-6,000 kg..." + "snippet": "Adult African elephants weigh between 4,000-6,000 kg...", } - ] + ], }, { "type": "text", - "text": "Based on my search, African elephants weigh around 5,000 kg." + "text": "Based on my search, African elephants weigh around 5,000 kg.", }, { "type": "tool_use", "id": "toolu_01XYZ789", "name": "add_numbers", - "input": {"a": 5000, "b": 100} - } + "input": {"a": 5000, "b": 100}, + }, ], "stop_reason": "tool_use", "usage": { "input_tokens": 100, "output_tokens": 50, - "server_tool_use": {"web_search_requests": 1} - } + "server_tool_use": {"web_search_requests": 1}, + }, } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify text extraction assert "Based on my search" in text @@ -388,7 +397,7 @@ def test_web_search_tool_result_in_provider_specific_fields(): "type": "server_tool_use", "id": "srvtoolu_provider_test", "name": "web_search", - "input": {"query": "test query"} + "input": {"query": "test query"}, }, { "type": "web_search_tool_result", @@ -398,21 +407,18 @@ def test_web_search_tool_result_in_provider_specific_fields(): "type": "web_search_result", "url": "https://example.com/test", "title": "Test Result", - "snippet": "Test snippet content" + "snippet": "Test snippet content", } - ] + ], }, - { - "type": "text", - "text": "Here is the result." - } + {"type": "text", "text": "Here is the result."}, ], "stop_reason": "end_turn", "usage": { "input_tokens": 50, "output_tokens": 25, - "server_tool_use": {"web_search_requests": 1} - } + "server_tool_use": {"web_search_requests": 1}, + }, } raw_response = httpx.Response(status_code=200, headers={}) @@ -432,7 +438,10 @@ def test_web_search_tool_result_in_provider_specific_fields(): assert "web_search_results" in provider_fields assert len(provider_fields["web_search_results"]) == 1 assert provider_fields["web_search_results"][0]["type"] == "web_search_tool_result" - assert provider_fields["web_search_results"][0]["tool_use_id"] == "srvtoolu_provider_test" + assert ( + provider_fields["web_search_results"][0]["tool_use_id"] + == "srvtoolu_provider_test" + ) def test_multiple_web_search_tool_results(): @@ -447,34 +456,52 @@ def test_multiple_web_search_tool_results(): "type": "server_tool_use", "id": "srvtoolu_search1", "name": "web_search", - "input": {"query": "african elephant weight"} + "input": {"query": "african elephant weight"}, }, { "type": "web_search_tool_result", "tool_use_id": "srvtoolu_search1", - "content": [{"type": "web_search_result", "url": "https://example1.com", "title": "Result 1", "snippet": "First result"}] + "content": [ + { + "type": "web_search_result", + "url": "https://example1.com", + "title": "Result 1", + "snippet": "First result", + } + ], }, { "type": "server_tool_use", "id": "srvtoolu_search2", "name": "web_search", - "input": {"query": "asian elephant weight"} + "input": {"query": "asian elephant weight"}, }, { "type": "web_search_tool_result", "tool_use_id": "srvtoolu_search2", - "content": [{"type": "web_search_result", "url": "https://example2.com", "title": "Result 2", "snippet": "Second result"}] + "content": [ + { + "type": "web_search_result", + "url": "https://example2.com", + "title": "Result 2", + "snippet": "Second result", + } + ], }, - { - "type": "text", - "text": "Found information about both elephants." - } + {"type": "text", "text": "Found information about both elephants."}, ] } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify both web_search_tool_results are extracted assert web_search_results is not None @@ -751,7 +778,7 @@ def test_anthropic_beta_header_merging_with_output_format(): optional_params = { "output_format": { "type": "json_schema", - "schema": {"type": "object", "properties": {}} + "schema": {"type": "object", "properties": {}}, } } @@ -761,10 +788,12 @@ def test_anthropic_beta_header_merging_with_output_format(): # Both beta headers should be present beta_value = result_headers["anthropic-beta"] - assert "context-1m-2025-08-07" in beta_value, \ - f"User's context-1m beta header missing from: {beta_value}" - assert "structured-outputs-2025-11-13" in beta_value, \ - f"Structured output beta header missing from: {beta_value}" + assert ( + "context-1m-2025-08-07" in beta_value + ), f"User's context-1m beta header missing from: {beta_value}" + assert ( + "structured-outputs-2025-11-13" in beta_value + ), f"Structured output beta header missing from: {beta_value}" def test_anthropic_beta_header_merging_with_multiple_features(): @@ -780,10 +809,10 @@ def test_anthropic_beta_header_merging_with_multiple_features(): optional_params = { "output_format": { "type": "json_schema", - "schema": {"type": "object", "properties": {}} + "schema": {"type": "object", "properties": {}}, }, "context_management": _sample_context_management_payload(), - "tools": [{"type": "web_fetch_20250910", "name": "web_fetch"}] + "tools": [{"type": "web_fetch_20250910", "name": "web_fetch"}], } result_headers = config.update_headers_with_optional_anthropic_beta( @@ -950,20 +979,12 @@ def test_tool_search_regex_detection(): # Test with tool search regex tool tools = [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - } + {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"} ] assert config.is_tool_search_used(tools) is True # Test without tool search - tools = [ - { - "type": "function", - "function": {"name": "get_weather"} - } - ] + tools = [{"type": "function", "function": {"name": "get_weather"}}] assert config.is_tool_search_used(tools) is False @@ -975,10 +996,7 @@ def test_tool_search_bm25_detection(): # Test with tool search BM25 tool tools = [ - { - "type": "tool_search_tool_bm25_20251119", - "name": "tool_search_tool_bm25" - } + {"type": "tool_search_tool_bm25_20251119", "name": "tool_search_tool_bm25"} ] assert config.is_tool_search_used(tools) is True @@ -1002,10 +1020,7 @@ def test_tool_search_regex_mapping(): """Test that tool search regex tools are properly mapped""" config = AnthropicConfig() - tool = { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - } + tool = {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"} mapped_tool, mcp_server = config._map_tool_helper(tool) @@ -1019,10 +1034,7 @@ def test_tool_search_bm25_mapping(): """Test that tool search BM25 tools are properly mapped""" config = AnthropicConfig() - tool = { - "type": "tool_search_tool_bm25_20251119", - "name": "tool_search_tool_bm25" - } + tool = {"type": "tool_search_tool_bm25_20251119", "name": "tool_search_tool_bm25"} mapped_tool, mcp_server = config._map_tool_helper(tool) @@ -1037,20 +1049,17 @@ def test_deferred_tools_separation(): config = AnthropicConfig() tools = [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex" - }, + {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"}, { "type": "function", "function": {"name": "get_weather"}, - "defer_loading": True + "defer_loading": True, }, { "type": "function", "function": {"name": "search_files"}, - "defer_loading": False - } + "defer_loading": False, + }, ] non_deferred, deferred = config._separate_deferred_tools(tools) @@ -1069,14 +1078,21 @@ def test_server_tool_use_in_response(): "type": "server_tool_use", "id": "srvtoolu_01ABC123", "name": "tool_search_tool_regex", - "input": {"query": "weather"} + "input": {"query": "weather"}, } ] } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) assert len(tool_calls) == 1 assert tool_calls[0]["id"] == "srvtoolu_01ABC123" @@ -1091,9 +1107,7 @@ def test_tool_search_usage_tracking(): usage_object = { "input_tokens": 100, "output_tokens": 50, - "server_tool_use": { - "tool_search_requests": 2 - } + "server_tool_use": {"tool_search_requests": 2}, } usage = config.calculate_usage(usage_object=usage_object, reasoning_content=None) @@ -1109,16 +1123,13 @@ def test_tool_reference_expansion(): deferred_tools = [ { "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather" - } + "function": {"name": "get_weather", "description": "Get weather"}, } ] content = [ {"type": "text", "text": "I'll search for tools"}, - {"type": "tool_reference", "tool_name": "get_weather"} + {"type": "tool_reference", "tool_name": "get_weather"}, ] expanded = config._expand_tool_references(content, deferred_tools) @@ -1140,13 +1151,11 @@ def test_defer_loading_preserved_in_transformation(): "description": "Get weather information", "parameters": { "type": "object", - "properties": { - "location": {"type": "string"} - }, - "required": ["location"] - } + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, }, - "defer_loading": True + "defer_loading": True, } mapped_tool, mcp_server = config._map_tool_helper(tool) @@ -1166,45 +1175,51 @@ def test_tool_search_complete_response_parsing(): "content": [ { "type": "text", - "text": "I'll search for weather-related tools that can help you." + "text": "I'll search for weather-related tools that can help you.", }, { "type": "server_tool_use", "id": "srvtoolu_015i6aVA2niwzv4RG4DtnxDJ", "name": "tool_search_tool_regex", "input": {"pattern": "weather", "limit": 5}, - "caller": {"type": "direct"} + "caller": {"type": "direct"}, }, { "type": "tool_search_tool_result", "tool_use_id": "srvtoolu_015i6aVA2niwzv4RG4DtnxDJ", "content": { "type": "tool_search_tool_search_result", - "tool_references": [{"type": "tool_reference", "tool_name": "get_weather"}] - } - }, - { - "type": "text", - "text": "Great! I found a weather tool." + "tool_references": [ + {"type": "tool_reference", "tool_name": "get_weather"} + ], + }, }, + {"type": "text", "text": "Great! I found a weather tool."}, { "type": "tool_use", "id": "toolu_01CrCNx4ntSaeeV9iArT4JfQ", "name": "get_weather", - "input": {"location": "San Francisco"} - } + "input": {"location": "San Francisco"}, + }, ], "usage": { "input_tokens": 1639, "output_tokens": 170, - "server_tool_use": {"web_search_requests": 0} - } + "server_tool_use": {"web_search_requests": 0}, + }, } # Extract content - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify text extraction (should concatenate both text blocks) assert "I'll search for weather-related tools" in text @@ -1222,12 +1237,14 @@ def test_tool_search_complete_response_parsing(): usage = config.calculate_usage( usage_object=completion_response["usage"], reasoning_content=None, - completion_response=completion_response + completion_response=completion_response, ) assert usage.server_tool_use is not None assert usage.server_tool_use.web_search_requests == 0 - assert usage.server_tool_use.tool_search_requests == 1 # Counted from server_tool_use blocks + assert ( + usage.server_tool_use.tool_search_requests == 1 + ) # Counted from server_tool_use blocks def test_allowed_callers_field_preservation(): @@ -1242,13 +1259,11 @@ def test_allowed_callers_field_preservation(): "description": "Execute a SQL query", "parameters": { "type": "object", - "properties": { - "sql": {"type": "string"} - }, - "required": ["sql"] - } + "properties": {"sql": {"type": "string"}}, + "required": ["sql"], + }, }, - "allowed_callers": ["code_execution_20250825"] + "allowed_callers": ["code_execution_20250825"], } transformed_tool, _ = config._map_tool_helper(tool_with_allowed_callers) @@ -1265,19 +1280,16 @@ def test_programmatic_tool_calling_beta_header(): # Test detection with allowed_callers tools = [ - { - "type": "code_execution_20250825", - "name": "code_execution" - }, + {"type": "code_execution_20250825", "name": "code_execution"}, { "type": "function", "function": { "name": "query_database", "description": "Execute a SQL query", - "parameters": {"type": "object", "properties": {}} + "parameters": {"type": "object", "properties": {}}, }, - "allowed_callers": ["code_execution_20250825"] - } + "allowed_callers": ["code_execution_20250825"], + }, ] is_programmatic = model_info.is_programmatic_tool_calling_used(tools) @@ -1285,8 +1297,7 @@ def test_programmatic_tool_calling_beta_header(): # Test header generation headers = model_info.get_anthropic_headers( - api_key="test-key", - programmatic_tool_calling_used=True + api_key="test-key", programmatic_tool_calling_used=True ) assert "anthropic-beta" in headers @@ -1303,10 +1314,7 @@ def test_caller_field_in_response(): "type": "message", "role": "assistant", "content": [ - { - "type": "text", - "text": "I'll query the database." - }, + {"type": "text", "text": "I'll query the database."}, { "type": "tool_use", "id": "toolu_123", @@ -1314,15 +1322,24 @@ def test_caller_field_in_response(): "input": {"sql": "SELECT * FROM users"}, "caller": { "type": "code_execution_20250825", - "tool_id": "srvtoolu_abc" - } - } + "tool_id": "srvtoolu_abc", + }, + }, ], "stop_reason": "tool_use", - "usage": {"input_tokens": 100, "output_tokens": 50} + "usage": {"input_tokens": 100, "output_tokens": 50}, } - text, citations, thinking, reasoning, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content(completion_response) + ( + text, + citations, + thinking, + reasoning, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) assert len(tool_calls) == 1 assert tool_calls[0]["id"] == "toolu_123" @@ -1337,10 +1354,7 @@ def test_code_execution_20250825_tool_type(): """Test that code_execution_20250825 tool type is handled correctly.""" config = AnthropicConfig() - tool = { - "type": "code_execution_20250825", - "name": "code_execution" - } + tool = {"type": "code_execution_20250825", "name": "code_execution"} transformed_tool, _ = config._map_tool_helper(tool) assert transformed_tool is not None @@ -1360,13 +1374,11 @@ def test_allowed_callers_in_function_field(): "description": "Execute a SQL query", "parameters": { "type": "object", - "properties": { - "sql": {"type": "string"} - }, - "required": ["sql"] + "properties": {"sql": {"type": "string"}}, + "required": ["sql"], }, - "allowed_callers": ["code_execution_20250825"] - } + "allowed_callers": ["code_execution_20250825"], + }, } transformed_tool, _ = config._map_tool_helper(tool) @@ -1389,15 +1401,15 @@ def test_input_examples_field_preservation(): "type": "object", "properties": { "location": {"type": "string"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, - "required": ["location"] - } + "required": ["location"], + }, }, "input_examples": [ {"location": "San Francisco, CA", "unit": "fahrenheit"}, - {"location": "Tokyo, Japan", "unit": "celsius"} - ] + {"location": "Tokyo, Japan", "unit": "celsius"}, + ], } transformed_tool, _ = config._map_tool_helper(tool_with_examples) @@ -1420,11 +1432,9 @@ def test_input_examples_beta_header(): "function": { "name": "get_weather", "description": "Get weather information", - "parameters": {"type": "object", "properties": {}} + "parameters": {"type": "object", "properties": {}}, }, - "input_examples": [ - {"location": "San Francisco, CA"} - ] + "input_examples": [{"location": "San Francisco, CA"}], } ] @@ -1433,8 +1443,7 @@ def test_input_examples_beta_header(): # Test header generation headers = model_info.get_anthropic_headers( - api_key="test-key", - input_examples_used=True + api_key="test-key", input_examples_used=True ) assert "anthropic-beta" in headers @@ -1453,16 +1462,14 @@ def test_input_examples_in_function_field(): "description": "Get weather information", "parameters": { "type": "object", - "properties": { - "location": {"type": "string"} - }, - "required": ["location"] + "properties": {"location": {"type": "string"}}, + "required": ["location"], }, "input_examples": [ {"location": "Paris, France"}, - {"location": "London, UK"} - ] - } + {"location": "London, UK"}, + ], + }, } transformed_tool, _ = config._map_tool_helper(tool) @@ -1483,17 +1490,13 @@ def test_input_examples_with_other_features(): "description": "Execute a SQL query", "parameters": { "type": "object", - "properties": { - "sql": {"type": "string"} - }, - "required": ["sql"] - } + "properties": {"sql": {"type": "string"}}, + "required": ["sql"], + }, }, - "input_examples": [ - {"sql": "SELECT * FROM users WHERE id = 1"} - ], + "input_examples": [{"sql": "SELECT * FROM users WHERE id = 1"}], "defer_loading": True, - "allowed_callers": ["code_execution_20250825"] + "allowed_callers": ["code_execution_20250825"], } transformed_tool, _ = config._map_tool_helper(tool) @@ -1517,19 +1520,20 @@ def test_input_examples_empty_list_not_added(): "description": "Get weather information", "parameters": { "type": "object", - "properties": { - "location": {"type": "string"} - }, - "required": ["location"] - } + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, }, - "input_examples": [] + "input_examples": [], } transformed_tool, _ = config._map_tool_helper(tool) assert transformed_tool is not None # Empty list should not be added - assert "input_examples" not in transformed_tool or len(transformed_tool.get("input_examples", [])) == 0 + assert ( + "input_examples" not in transformed_tool + or len(transformed_tool.get("input_examples", [])) == 0 + ) # ============ Effort Parameter Tests ============ @@ -1540,18 +1544,14 @@ def test_effort_output_config_preservation(): config = AnthropicConfig() messages = [{"role": "user", "content": "Analyze this code"}] - optional_params = { - "output_config": { - "effort": "medium" - } - } + optional_params = {"output_config": {"effort": "medium"}} result = config.transform_request( model="claude-opus-4-5-20251101", messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) assert "output_config" in result @@ -1565,18 +1565,13 @@ def test_effort_beta_header_injection(): model_info = AnthropicModelInfo() # Test with effort parameter - optional_params = { - "output_config": { - "effort": "low" - } - } + optional_params = {"output_config": {"effort": "low"}} effort_used = model_info.is_effort_used(optional_params=optional_params) assert effort_used is True headers = model_info.get_anthropic_headers( - api_key="test-key", - effort_used=effort_used + api_key="test-key", effort_used=effort_used ) assert "anthropic-beta" in headers @@ -1597,7 +1592,7 @@ def test_effort_validation(): messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) assert result["output_config"]["effort"] == effort @@ -1609,7 +1604,7 @@ def test_effort_validation(): messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) @@ -1618,18 +1613,14 @@ def test_effort_with_claude_opus_45(): config = AnthropicConfig() messages = [{"role": "user", "content": "Complex analysis task"}] - optional_params = { - "output_config": { - "effort": "high" - } - } + optional_params = {"output_config": {"effort": "high"}} result = config.transform_request( model="claude-opus-4-5-20251101", messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) assert "output_config" in result @@ -1650,7 +1641,7 @@ def test_effort_validation_with_opus_46(): messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) assert result["output_config"]["effort"] == effort @@ -1661,14 +1652,16 @@ def test_max_effort_rejected_for_opus_45(): messages = [{"role": "user", "content": "Test"}] - with pytest.raises(ValueError, match="effort='max' is only supported by Claude Opus 4.6"): + with pytest.raises( + ValueError, match="effort='max' is only supported by Claude Opus 4.6" + ): optional_params = {"output_config": {"effort": "max"}} config.transform_request( model="claude-opus-4-5-20251101", messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) @@ -1685,23 +1678,16 @@ def test_effort_with_other_features(): "description": "Get data", "parameters": { "type": "object", - "properties": { - "query": {"type": "string"} - }, - "required": ["query"] - } - } + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, } ] optional_params = { - "output_config": { - "effort": "low" - }, + "output_config": {"effort": "low"}, "tools": tools, - "thinking": { - "type": "enabled", - "budget_tokens": 1000 - } + "thinking": {"type": "enabled", "budget_tokens": 1000}, } result = config.transform_request( @@ -1709,7 +1695,7 @@ def test_effort_with_other_features(): messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) # Verify all features are present @@ -1752,11 +1738,14 @@ def test_translate_system_message_skips_empty_list_content(): # Test list content with empty text block messages = [ - {"role": "system", "content": [ - {"type": "text", "text": ""}, - {"type": "text", "text": "Valid content"}, - {"type": "text", "text": ""}, - ]}, + { + "role": "system", + "content": [ + {"type": "text", "text": ""}, + {"type": "text", "text": "Valid content"}, + {"type": "text", "text": ""}, + ], + }, {"role": "user", "content": "Hello"}, ] @@ -1794,9 +1783,16 @@ def test_translate_system_message_preserves_cache_control(): # Test list content with cache_control messages = [ - {"role": "system", "content": [ - {"type": "text", "text": "Cached content", "cache_control": {"type": "ephemeral"}}, - ]}, + { + "role": "system", + "content": [ + { + "type": "text", + "text": "Cached content", + "cache_control": {"type": "ephemeral"}, + }, + ], + }, {"role": "user", "content": "Hello"}, ] @@ -1938,7 +1934,7 @@ def test_transform_request_uses_dynamic_max_tokens(): messages=messages, optional_params={}, # No max_tokens provided litellm_params={}, - headers={} + headers={}, ) assert result["max_tokens"] == 64000 @@ -1959,7 +1955,7 @@ def test_transform_request_respects_user_max_tokens(): messages=messages, optional_params={"max_tokens": 1000}, litellm_params={}, - headers={} + headers={}, ) assert result["max_tokens"] == 1000 @@ -2006,11 +2002,12 @@ def test_calculate_usage_completion_tokens_details_with_reasoning(): "output_tokens": 500, } # Simulating reasoning content that would count as ~50 tokens - reasoning_content = "Let me think about this step by step. " * 10 # Roughly 50 tokens + reasoning_content = ( + "Let me think about this step by step. " * 10 + ) # Roughly 50 tokens usage = config.calculate_usage( - usage_object=usage_object, - reasoning_content=reasoning_content + usage_object=usage_object, reasoning_content=reasoning_content ) # completion_tokens_details should be populated with both reasoning and text tokens @@ -2051,7 +2048,7 @@ def test_reasoning_effort_maps_to_adaptive_thinking_for_claude_4_6_models(): non_default_params=non_default_params, optional_params=optional_params, model=model, - drop_params=False + drop_params=False, ) # Should map to adaptive thinking type @@ -2062,7 +2059,9 @@ def test_reasoning_effort_maps_to_adaptive_thinking_for_claude_4_6_models(): # reasoning_effort should not be in the result (it's transformed to thinking) assert "reasoning_effort" not in result # Should set output_config with the mapped effort value - assert "output_config" in result, f"output_config missing for {model} with effort={effort}" + assert ( + "output_config" in result + ), f"output_config missing for {model} with effort={effort}" assert result["output_config"]["effort"] == effort_map[effort] @@ -2123,10 +2122,10 @@ def test_reasoning_effort_maps_to_budget_thinking_for_non_opus_4_6(): # Test with Claude Sonnet 4.5 (non-Opus 4.6 model) test_cases = [ - ("low", 1024), # DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET - ("medium", 2048), # DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET - ("high", 4096), # DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET - ("minimal", 128), # DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET + ("low", 1024), # DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET + ("medium", 2048), # DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET + ("high", 4096), # DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET + ("minimal", 128), # DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET ] for effort, expected_budget in test_cases: @@ -2137,7 +2136,7 @@ def test_reasoning_effort_maps_to_budget_thinking_for_non_opus_4_6(): non_default_params=non_default_params, optional_params=optional_params, model="claude-sonnet-4-5-20250929", - drop_params=False + drop_params=False, ) # Should map to enabled thinking type with budget_tokens @@ -2166,9 +2165,9 @@ def test_reasoning_effort_sets_output_config_for_46_models(): drop_params=False, ) - assert "output_config" in result, ( - f"output_config missing for {model} with effort={effort}" - ) + assert ( + "output_config" in result + ), f"output_config missing for {model} with effort={effort}" assert result["output_config"]["effort"] == effort @@ -2207,9 +2206,9 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): drop_params=False, ) - assert "output_config" not in result, ( - f"output_config should not be set for {model}" - ) + assert ( + "output_config" not in result + ), f"output_config should not be set for {model}" def test_max_effort_rejected_for_sonnet_46(): @@ -2217,7 +2216,9 @@ def test_max_effort_rejected_for_sonnet_46(): config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] - with pytest.raises(ValueError, match="effort='max' is only supported by Claude Opus 4.6"): + with pytest.raises( + ValueError, match="effort='max' is only supported by Claude Opus 4.6" + ): config.transform_request( model="claude-sonnet-4-6-20260219", messages=messages, @@ -2260,9 +2261,7 @@ def test_effort_beta_header_not_injected_for_46_models(): optional_params={"output_config": {"effort": "high"}}, model=model, ) - assert result is False, ( - f"is_effort_used should return False for {model}" - ) + assert result is False, f"is_effort_used should return False for {model}" def test_effort_beta_header_still_injected_for_older_models(): @@ -2302,17 +2301,12 @@ def test_code_execution_tool_results_extraction(): "role": "assistant", "model": "claude-sonnet-4-5-20250929", "content": [ - { - "type": "text", - "text": "I'll calculate that for you." - }, + {"type": "text", "text": "I'll calculate that for you."}, { "type": "server_tool_use", "id": "srvtoolu_01ABC", "name": "bash_code_execution", - "input": { - "command": "python3 << 'EOF'\nprint(2 + 2)\nEOF\n" - } + "input": {"command": "python3 << 'EOF'\nprint(2 + 2)\nEOF\n"}, }, { "type": "bash_code_execution_tool_result", @@ -2321,8 +2315,8 @@ def test_code_execution_tool_results_extraction(): "type": "bash_code_execution_result", "stdout": "4\n", "stderr": "", - "return_code": 0 - } + "return_code": 0, + }, }, { "type": "server_tool_use", @@ -2331,28 +2325,22 @@ def test_code_execution_tool_results_extraction(): "input": { "command": "create", "path": "test.txt", - "file_text": "Hello" - } + "file_text": "Hello", + }, }, { "type": "text_editor_code_execution_tool_result", "tool_use_id": "srvtoolu_01DEF", "content": { "type": "text_editor_code_execution_result", - "is_file_update": False - } + "is_file_update": False, + }, }, - { - "type": "text", - "text": "Done!" - } + {"type": "text", "text": "Done!"}, ], "stop_reason": "stop", "stop_sequence": None, - "usage": { - "input_tokens": 100, - "output_tokens": 50 - } + "usage": {"input_tokens": 100, "output_tokens": 50}, } # Create mock HTTP response @@ -2377,11 +2365,17 @@ def test_code_execution_tool_results_extraction(): # Verify first tool call assert transformed_response.choices[0].message.tool_calls[0].id == "srvtoolu_01ABC" - assert transformed_response.choices[0].message.tool_calls[0].function.name == "bash_code_execution" + assert ( + transformed_response.choices[0].message.tool_calls[0].function.name + == "bash_code_execution" + ) # Verify second tool call assert transformed_response.choices[0].message.tool_calls[1].id == "srvtoolu_01DEF" - assert transformed_response.choices[0].message.tool_calls[1].function.name == "text_editor_code_execution" + assert ( + transformed_response.choices[0].message.tool_calls[1].function.name + == "text_editor_code_execution" + ) # Verify tool results are in provider_specific_fields provider_fields = transformed_response.choices[0].message.provider_specific_fields @@ -2404,10 +2398,83 @@ def test_code_execution_tool_results_extraction(): assert editor_result["content"]["is_file_update"] is False # Verify text content is properly concatenated - assert "I'll calculate that for you." in transformed_response.choices[0].message.content + assert ( + "I'll calculate that for you." + in transformed_response.choices[0].message.content + ) assert "Done!" in transformed_response.choices[0].message.content +def test_code_execution_tool_results_in_hidden_params(): + """ + Test that tool_results reaches _hidden_params so the Responses API adapter + can surface them via provider_specific_fields. + + The Responses API adapter reads _hidden_params.get("provider_specific_fields") + to set provider_specific_fields on the response. Without this, server-side + code execution results (stdout/stderr) are lost when using responses.create(). + """ + import httpx + + from litellm.types.utils import ModelResponse + + config = AnthropicConfig() + + mock_anthropic_response = { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5-20250929", + "content": [ + {"type": "text", "text": "Here's the result."}, + { + "type": "server_tool_use", + "id": "srvtoolu_01ABC", + "name": "bash_code_execution", + "input": {"command": "echo hello"}, + }, + { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01ABC", + "content": { + "type": "bash_code_execution_result", + "stdout": "hello\n", + "stderr": "", + "return_code": 0, + }, + }, + ], + "stop_reason": "stop", + "stop_sequence": None, + "usage": {"input_tokens": 100, "output_tokens": 50}, + } + + mock_raw_response = MagicMock(spec=httpx.Response) + mock_raw_response.json.return_value = mock_anthropic_response + mock_raw_response.status_code = 200 + mock_raw_response.headers = {} + + model_response = ModelResponse() + + transformed_response = config.transform_parsed_response( + completion_response=mock_anthropic_response, + raw_response=mock_raw_response, + model_response=model_response, + json_mode=False, + prefix_prompt=None, + ) + + # Verify tool_results is in _hidden_params for the Responses API adapter + hidden = transformed_response._hidden_params + assert "provider_specific_fields" in hidden + assert "tool_results" in hidden["provider_specific_fields"] + assert len(hidden["provider_specific_fields"]["tool_results"]) == 1 + assert ( + hidden["provider_specific_fields"]["tool_results"][0]["content"]["stdout"] + == "hello\n" + ) + + def test_tool_search_tool_result_not_in_tool_results(): """ Test that tool_search_tool_result is NOT included in tool_results @@ -2425,21 +2492,12 @@ def test_tool_search_tool_result_not_in_tool_results(): "role": "assistant", "model": "claude-sonnet-4-5-20250929", "content": [ - { - "type": "text", - "text": "Found tools." - }, - { - "type": "tool_search_tool_result", - "tool_references": ["tool1", "tool2"] - } + {"type": "text", "text": "Found tools."}, + {"type": "tool_search_tool_result", "tool_references": ["tool1", "tool2"]}, ], "stop_reason": "stop", "stop_sequence": None, - "usage": { - "input_tokens": 100, - "output_tokens": 50 - } + "usage": {"input_tokens": 100, "output_tokens": 50}, } mock_raw_response = MagicMock(spec=httpx.Response) @@ -2479,22 +2537,16 @@ def test_web_search_tool_result_backwards_compatibility(): "role": "assistant", "model": "claude-sonnet-4-5-20250929", "content": [ - { - "type": "text", - "text": "Here are the results." - }, + {"type": "text", "text": "Here are the results."}, { "type": "web_search_tool_result", "search_query": "test query", - "results": [{"title": "Result 1", "url": "https://example.com"}] - } + "results": [{"title": "Result 1", "url": "https://example.com"}], + }, ], "stop_reason": "stop", "stop_sequence": None, - "usage": { - "input_tokens": 100, - "output_tokens": 50 - } + "usage": {"input_tokens": 100, "output_tokens": 50}, } mock_raw_response = MagicMock(spec=httpx.Response) @@ -2540,24 +2592,28 @@ def test_compaction_block_extraction(): "content": [ { "type": "compaction", - "content": "Summary of the conversation: The user requested help building a web scraper..." + "content": "Summary of the conversation: The user requested help building a web scraper...", }, { "type": "text", - "text": "I don't have access to real-time data, so I can't provide the current weather in San Francisco." - } + "text": "I don't have access to real-time data, so I can't provide the current weather in San Francisco.", + }, ], "stop_reason": "max_tokens", "stop_sequence": None, - "usage": { - "input_tokens": 86, - "output_tokens": 100 - } + "usage": {"input_tokens": 86, "output_tokens": 100}, } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify compaction blocks are extracted assert compaction_blocks is not None @@ -2587,18 +2643,12 @@ def test_compaction_block_in_provider_specific_fields(): "content": [ { "type": "compaction", - "content": "Summary of the conversation: The user requested help building a web scraper..." + "content": "Summary of the conversation: The user requested help building a web scraper...", }, - { - "type": "text", - "text": "Here is the response." - } + {"type": "text", "text": "Here is the response."}, ], "stop_reason": "end_turn", - "usage": { - "input_tokens": 50, - "output_tokens": 25 - } + "usage": {"input_tokens": 50, "output_tokens": 25}, } raw_response = httpx.Response(status_code=200, headers={}) @@ -2618,7 +2668,10 @@ def test_compaction_block_in_provider_specific_fields(): assert "compaction_blocks" in provider_fields assert len(provider_fields["compaction_blocks"]) == 1 assert provider_fields["compaction_blocks"][0]["type"] == "compaction" - assert "Summary of the conversation" in provider_fields["compaction_blocks"][0]["content"] + assert ( + "Summary of the conversation" + in provider_fields["compaction_blocks"][0]["content"] + ) def test_multiple_compaction_blocks(): @@ -2629,24 +2682,22 @@ def test_multiple_compaction_blocks(): completion_response = { "content": [ - { - "type": "compaction", - "content": "First summary..." - }, - { - "type": "text", - "text": "Some text." - }, - { - "type": "compaction", - "content": "Second summary..." - } + {"type": "compaction", "content": "First summary..."}, + {"type": "text", "text": "Some text."}, + {"type": "compaction", "content": "Second summary..."}, ] } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify both compaction blocks are extracted assert compaction_blocks is not None @@ -2665,37 +2716,26 @@ def test_compaction_block_request_transformation(): ) messages = [ - { - "role": "user", - "content": "What is the weather in San Francisco?" - }, + {"role": "user", "content": "What is the weather in San Francisco?"}, { "role": "assistant", "content": [ - { - "type": "text", - "text": "I don't have access to real-time data." - } + {"type": "text", "text": "I don't have access to real-time data."} ], "provider_specific_fields": { "compaction_blocks": [ { "type": "compaction", - "content": "Summary of the conversation: The user requested help building a web scraper..." + "content": "Summary of the conversation: The user requested help building a web scraper...", } ] - } + }, }, - { - "role": "user", - "content": "What about New York?" - } + {"role": "user", "content": "What about New York?"}, ] result = anthropic_messages_pt( - messages=messages, - model="claude-opus-4-6", - llm_provider="anthropic" + messages=messages, model="claude-opus-4-6", llm_provider="anthropic" ) # Find the assistant message @@ -2727,14 +2767,8 @@ def test_compaction_with_context_management(): messages = [{"role": "user", "content": "Hello"}] optional_params = { - "context_management": { - "edits": [ - { - "type": "compact_20260112" - } - ] - }, - "max_tokens": 100 + "context_management": {"edits": [{"type": "compact_20260112"}]}, + "max_tokens": 100, } result = config.transform_request( @@ -2742,7 +2776,7 @@ def test_compaction_with_context_management(): messages=messages, optional_params=optional_params, litellm_params={}, - headers={} + headers={}, ) # Verify context_management is included @@ -2758,30 +2792,28 @@ def test_compaction_block_with_other_content_types(): completion_response = { "content": [ - { - "type": "compaction", - "content": "Summary of previous conversation..." - }, - { - "type": "thinking", - "thinking": "Let me think about this..." - }, - { - "type": "text", - "text": "Based on my analysis..." - }, + {"type": "compaction", "content": "Summary of previous conversation..."}, + {"type": "thinking", "thinking": "Let me think about this..."}, + {"type": "text", "text": "Based on my analysis..."}, { "type": "tool_use", "id": "toolu_123", "name": "get_weather", - "input": {"location": "San Francisco"} - } + "input": {"location": "San Francisco"}, + }, ] } - text, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks = config.extract_response_content( - completion_response - ) + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) # Verify all content types are extracted assert compaction_blocks is not None @@ -2798,11 +2830,11 @@ def test_map_openai_context_management_to_anthropic(): Test mapping OpenAI Responses API context_management format to Anthropic format. """ config = AnthropicConfig() - + # Test OpenAI list format with compaction openai_format = [{"type": "compaction", "compact_threshold": 200000}] result = config.map_openai_context_management_to_anthropic(openai_format) - + assert result is not None assert "edits" in result assert len(result["edits"]) == 1 @@ -2811,26 +2843,32 @@ def test_map_openai_context_management_to_anthropic(): assert result["edits"][0]["trigger"]["value"] == 200000 # Test OpenAI format with instructions - openai_format_with_instructions = [{ - "type": "compaction", - "compact_threshold": 150000, - "instructions": "Focus on preserving code snippets" - }] - result = config.map_openai_context_management_to_anthropic(openai_format_with_instructions) - + openai_format_with_instructions = [ + { + "type": "compaction", + "compact_threshold": 150000, + "instructions": "Focus on preserving code snippets", + } + ] + result = config.map_openai_context_management_to_anthropic( + openai_format_with_instructions + ) + assert result is not None assert result["edits"][0]["trigger"]["value"] == 150000 assert result["edits"][0]["instructions"] == "Focus on preserving code snippets" - + # Test Anthropic format (should pass through) anthropic_format = { - "edits": [{ - "type": "compact_20260112", - "trigger": {"type": "input_tokens", "value": 150000} - }] + "edits": [ + { + "type": "compact_20260112", + "trigger": {"type": "input_tokens", "value": 150000}, + } + ] } result = config.map_openai_context_management_to_anthropic(anthropic_format) - + assert result == anthropic_format @@ -2839,46 +2877,51 @@ def test_map_openai_params_with_context_management(): Test that map_openai_params correctly transforms context_management from OpenAI to Anthropic format. """ config = AnthropicConfig() - + # Test with OpenAI list format non_default_params = { "context_management": [{"type": "compaction", "compact_threshold": 200000}] } optional_params = {} - + result = config.map_openai_params( non_default_params=non_default_params, optional_params=optional_params, model="claude-opus-4-6", - drop_params=False + drop_params=False, ) - + assert "context_management" in result assert "edits" in result["context_management"] assert result["context_management"]["edits"][0]["type"] == "compact_20260112" assert result["context_management"]["edits"][0]["trigger"]["value"] == 200000 - + # Test with Anthropic dict format (should pass through) non_default_params_anthropic = { "context_management": { - "edits": [{ - "type": "compact_20260112", - "trigger": {"type": "input_tokens", "value": 150000}, - "instructions": "Focus on preserving code" - }] + "edits": [ + { + "type": "compact_20260112", + "trigger": {"type": "input_tokens", "value": 150000}, + "instructions": "Focus on preserving code", + } + ] } } optional_params = {} - + result = config.map_openai_params( non_default_params=non_default_params_anthropic, optional_params=optional_params, model="claude-opus-4-6", - drop_params=False + drop_params=False, ) - + assert "context_management" in result - assert result["context_management"] == non_default_params_anthropic["context_management"] + assert ( + result["context_management"] + == non_default_params_anthropic["context_management"] + ) def test_cache_control_in_supported_params(): @@ -2897,9 +2940,7 @@ def test_map_openai_params_with_cache_control(): """ config = AnthropicConfig() - non_default_params = { - "cache_control": {"type": "ephemeral"} - } + non_default_params = {"cache_control": {"type": "ephemeral"}} optional_params = {} result = config.map_openai_params( @@ -2919,9 +2960,7 @@ def test_map_openai_params_cache_control_ignored_when_not_dict(): """ config = AnthropicConfig() - non_default_params = { - "cache_control": "ephemeral" - } + non_default_params = {"cache_control": "ephemeral"} optional_params = {} result = config.map_openai_params( @@ -2974,17 +3013,9 @@ def test_compaction_block_empty_list_not_added(): "type": "message", "role": "assistant", "model": "claude-opus-4-6", - "content": [ - { - "type": "text", - "text": "Just a regular response." - } - ], + "content": [{"type": "text", "text": "Just a regular response."}], "stop_reason": "end_turn", - "usage": { - "input_tokens": 10, - "output_tokens": 5 - } + "usage": {"input_tokens": 10, "output_tokens": 5}, } raw_response = httpx.Response(status_code=200, headers={}) @@ -3001,7 +3032,10 @@ def test_compaction_block_empty_list_not_added(): # Verify compaction_blocks is not in provider_specific_fields when there are none provider_fields = result.choices[0].message.provider_specific_fields if provider_fields: - assert "compaction_blocks" not in provider_fields or provider_fields.get("compaction_blocks") is None + assert ( + "compaction_blocks" not in provider_fields + or provider_fields.get("compaction_blocks") is None + ) def test_fast_mode_beta_header(): @@ -3014,8 +3048,7 @@ def test_fast_mode_beta_header(): optional_params = {"speed": "fast"} result_headers = config.update_headers_with_optional_anthropic_beta( - headers=headers, - optional_params=optional_params + headers=headers, optional_params=optional_params ) assert "anthropic-beta" in result_headers @@ -3029,14 +3062,10 @@ def test_fast_mode_with_other_beta_headers(): config = AnthropicConfig() headers = {} - optional_params = { - "speed": "fast", - "output_format": {"type": "json_object"} - } + optional_params = {"speed": "fast", "output_format": {"type": "json_object"}} result_headers = config.update_headers_with_optional_anthropic_beta( - headers=headers, - optional_params=optional_params + headers=headers, optional_params=optional_params ) assert "anthropic-beta" in result_headers @@ -3056,9 +3085,7 @@ def test_fast_mode_usage_calculation(): } usage = config.calculate_usage( - usage_object=usage_object, - reasoning_content=None, - speed="fast" + usage_object=usage_object, reasoning_content=None, speed="fast" ) assert usage.prompt_tokens == 1000 @@ -3171,7 +3198,7 @@ def test_fast_mode_parameter_mapping(): non_default_params=non_default_params, optional_params=optional_params, model="claude-opus-4-6", - drop_params=False + drop_params=False, ) assert "speed" in result @@ -3236,9 +3263,9 @@ def test_map_tool_helper_enforces_object_type_when_missing(): assert "properties" in result["input_schema"] assert "query" in result["input_schema"]["properties"] # Original parameters dict must not be modified in place - assert tool["function"]["parameters"] == original_params, ( - "parameters dict was mutated; _map_tool_helper should not modify caller data" - ) + assert ( + tool["function"]["parameters"] == original_params + ), "parameters dict was mutated; _map_tool_helper should not modify caller data" def test_map_tool_helper_enforces_object_type_when_wrong_type(): @@ -3264,13 +3291,13 @@ def test_map_tool_helper_enforces_object_type_when_wrong_type(): result, _ = config._map_tool_helper(tool) assert result is not None assert result["input_schema"]["type"] == "object" - assert result["input_schema"].get("properties") == {}, ( - "properties should be injected as {} when schema has non-object type and no properties key" - ) + assert ( + result["input_schema"].get("properties") == {} + ), "properties should be injected as {} when schema has non-object type and no properties key" # Original parameters dict must not be modified in place - assert tool["function"]["parameters"] == original_params, ( - "parameters dict was mutated; _map_tool_helper should not modify caller data" - ) + assert ( + tool["function"]["parameters"] == original_params + ), "parameters dict was mutated; _map_tool_helper should not modify caller data" def test_map_tool_helper_preserves_valid_object_schema(): From 2bf8751f6b0cfe30b51e4bb298c7e36ebc4ea46b Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Tue, 17 Mar 2026 17:57:25 +0100 Subject: [PATCH 18/25] fix: streaming code_interpreter_results dropped for multiple code executions stream_chunk_builder uses "last value wins" for list-valued provider_specific_fields keys. _build_code_interpreter_results was emitting only new items (incremental), so earlier results were silently dropped when multiple sequential code executions occurred. - Emit cumulative list from _build_code_interpreter_results, matching web_search_results pattern - Assemble server_tool_use input from input_json_delta deltas at content_block_stop (Anthropic streams input: {} in start block) - Handle dict items in _extract_tool_result_output_items after model_dump() serialization in stream_chunk_builder - Simplify _merge_provider_specific_fields to last-value-wins for lists, matching stream_chunk_builder semantics --- litellm/llms/anthropic/chat/handler.py | 45 ++++-- .../streaming_iterator.py | 19 ++- .../transformation.py | 5 +- .../chat/test_anthropic_chat_handler.py | 133 +++++++++++++++--- 4 files changed, 165 insertions(+), 37 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 51b9c9835a7..7d5fa2a5591 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -545,7 +545,7 @@ class ModelResponseIterator: # Track server tool use inputs and results for code_interpreter_results self._server_tool_inputs: Dict[str, Any] = {} self.tool_results: List[Dict[str, Any]] = [] - self._last_code_interpreter_results_count: int = 0 + self._current_server_tool_id: Optional[str] = None def check_empty_tool_call_args(self) -> bool: """ @@ -695,13 +695,14 @@ class ModelResponseIterator: Called during streaming to produce provider-neutral code_interpreter_results alongside the raw tool_results, so the Responses API layer doesn't need Anthropic-specific knowledge. + + Returns the full cumulative list each time (not incremental), matching + how web_search_results works. stream_chunk_builder uses "last value + wins" for list-valued provider_specific_fields keys, so the last + emission must contain every result. """ - # Only convert tool_results added since the last call to avoid - # duplicates when _merge_provider_specific_fields extends the list. - new_results = self.tool_results[self._last_code_interpreter_results_count :] - self._last_code_interpreter_results_count = len(self.tool_results) results = [] - for tr in new_results: + for tr in self.tool_results: call_id = tr.get("tool_use_id", "") content = tr.get("content", {}) if isinstance(content, dict): @@ -793,17 +794,23 @@ class ModelResponseIterator: ), index=self.tool_index, ) - # Track server tool use inputs for code_interpreter_results + # Track server tool use inputs for code_interpreter_results. + # The initial input in content_block_start is typically {} + # for streaming; the full input arrives via input_json_delta + # and is assembled at content_block_stop. if ( content_block_start["content_block"]["type"] == "server_tool_use" ): + self._current_server_tool_id = content_block_start[ + "content_block" + ]["id"] tool_input = content_block_start["content_block"].get( "input", {} ) - self._server_tool_inputs[ - content_block_start["content_block"]["id"] - ] = tool_input + self._server_tool_inputs[self._current_server_tool_id] = ( + tool_input + ) # Include caller information if present (for programmatic tool calling) if "caller" in content_block_start["content_block"]: caller_data = content_block_start["content_block"]["caller"] @@ -886,6 +893,24 @@ class ModelResponseIterator: ), index=self.tool_index, ) + # Update server_tool_inputs with fully assembled input + # from input_json_delta chunks (content_block_start has {}) + if ( + self.current_content_block_type == "server_tool_use" + and self._current_server_tool_id + ): + args = "" + for block in self.content_blocks: + if block["delta"]["type"] == "input_json_delta": + args += block["delta"].get("partial_json", "") + if args: + try: + self._server_tool_inputs[ + self._current_server_tool_id + ] = json.loads(args) + except (json.JSONDecodeError, TypeError): + pass + self._current_server_tool_id = None # Reset response_format tool tracking when block stops self.is_response_format_tool = False # Reset current content block type diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index 0b7d6e8a7a4..0672b03bcd7 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -481,17 +481,16 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): return event def _merge_provider_specific_fields(self, src: dict) -> None: - """Merge provider_specific_fields, extending list values instead of replacing.""" + """Merge provider_specific_fields using last-value-wins for lists. + + List-valued keys (web_search_results, tool_results, + code_interpreter_results, etc.) are emitted cumulatively — each + emission contains the full list so far. Using "last value wins" + matches stream_chunk_builder's semantics and avoids quadratic + growth from repeated extend calls. + """ for key, val in src.items(): - existing = self._accumulated_provider_specific_fields.get(key) - if ( - existing is not None - and isinstance(val, list) - and isinstance(existing, list) - ): - existing.extend(val) - else: - self._accumulated_provider_specific_fields[key] = val + self._accumulated_provider_specific_fields[key] = val def create_litellm_model_response(self) -> Optional[ModelResponse]: response = cast( diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index b54d5930efc..b7f7e9adda2 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -1738,7 +1738,10 @@ class LiteLLMCompletionResponsesConfig: ) ) if tool_result_items: - result_by_id = {item.id: item for item in tool_result_items} + result_by_id = { + (item.get("id") if isinstance(item, dict) else item.id): item + for item in tool_result_items + } replaced_ids = set(result_by_id.keys()) responses_output = [ ( diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index 35c7a62027b..d7a04a054ab 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -1245,9 +1245,9 @@ def test_streaming_code_execution_produces_code_interpreter_results(): def test_streaming_multiple_code_executions_no_duplicates(): """ - Test that multiple code executions in a single streaming response produce - exactly one code_interpreter_result per execution — no duplicates from - _build_code_interpreter_results rebuilding the full list. + Test that multiple code executions in a single streaming response emit + cumulative code_interpreter_results on each chunk (matching stream_chunk_builder's + "last value wins" contract). The final emission must contain ALL results. """ chunks = [ { @@ -1323,24 +1323,125 @@ def test_streaming_multiple_code_executions_no_duplicates(): iterator = ModelResponseIterator(None, sync_stream=True) - # Collect ALL code_interpreter_results emitted across all chunks - all_results = [] + # Collect each emission of code_interpreter_results + emissions = [] for chunk in chunks: parsed = iterator.chunk_parser(chunk) psf = None if parsed.choices and parsed.choices[0].delta: psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) if psf and "code_interpreter_results" in psf: - all_results.extend(psf["code_interpreter_results"]) + emissions.append(psf["code_interpreter_results"]) - # Should have exactly 2 results, one per execution — no duplicates - assert len(all_results) == 2, ( - f"Expected 2 code_interpreter_results, got {len(all_results)}. " - f"IDs: {[r.id for r in all_results]}" + # Should have 2 emissions (one per tool_result block) + assert len(emissions) == 2, f"Expected 2 emissions, got {len(emissions)}" + + # First emission: cumulative list with 1 result + assert len(emissions[0]) == 1 + assert emissions[0][0].id == "srvtoolu_01AAA" + assert emissions[0][0].code == "echo first" + assert emissions[0][0].outputs[0].logs == "first\n" + + # Second (final) emission: cumulative list with BOTH results + # This is what stream_chunk_builder will pick as "last value wins" + assert len(emissions[1]) == 2, ( + f"Expected final emission to have 2 results, got {len(emissions[1])}. " + f"IDs: {[r.id for r in emissions[1]]}" ) - assert all_results[0].id == "srvtoolu_01AAA" - assert all_results[0].code == "echo first" - assert all_results[0].outputs[0].logs == "first\n" - assert all_results[1].id == "srvtoolu_01BBB" - assert all_results[1].code == "echo second" - assert all_results[1].outputs[0].logs == "second\n" + assert emissions[1][0].id == "srvtoolu_01AAA" + assert emissions[1][0].code == "echo first" + assert emissions[1][0].outputs[0].logs == "first\n" + assert emissions[1][1].id == "srvtoolu_01BBB" + assert emissions[1][1].code == "echo second" + assert emissions[1][1].outputs[0].logs == "second\n" + + +def test_streaming_code_execution_input_assembled_from_deltas(): + """ + In real Anthropic streaming, content_block_start for server_tool_use has + input: {}. The actual input arrives via input_json_delta deltas and must + be assembled at content_block_stop so the code field is populated. + + This test uses realistic chunk shapes (empty input in start, partial JSON + in deltas) to exercise the input assembly path. + """ + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + # server_tool_use with empty input (real streaming behaviour) + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01AAA", + "name": "code_execution", + "input": {}, + }, + }, + # Input arrives via deltas, split across two chunks + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "input_json_delta", + "partial_json": '{"comma', + }, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "input_json_delta", + "partial_json": 'nd": "echo hello"}', + }, + }, + {"type": "content_block_stop", "index": 0}, + # Tool result + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "code_execution_tool_result", + "tool_use_id": "srvtoolu_01AAA", + "content": { + "type": "code_execution_result", + "stdout": "hello\n", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 1}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + + code_results = None + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + psf = None + if parsed.choices and parsed.choices[0].delta: + psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) + if psf and "code_interpreter_results" in psf: + code_results = psf["code_interpreter_results"] + + # The code field must contain the assembled input, not be empty + assert code_results is not None, "No code_interpreter_results emitted" + assert len(code_results) == 1 + assert code_results[0].id == "srvtoolu_01AAA" + assert code_results[0].code == "echo hello" + assert code_results[0].outputs[0].logs == "hello\n" From 4be1d76fd7da5c4c0b04f1ead59e98bcf54d1dfb Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Tue, 17 Mar 2026 18:37:32 +0100 Subject: [PATCH 19/25] fix: empty stdout/stderr produces str(content) instead of empty logs When both stdout and stderr are empty strings, the `if parts else str(content)` fallback produced the raw dict representation as logs. Drop the fallback so logs is correctly empty. --- litellm/llms/anthropic/chat/handler.py | 2 +- litellm/llms/anthropic/chat/transformation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 7d5fa2a5591..88d9ee65969 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -711,7 +711,7 @@ class ModelResponseIterator: parts.append(content["stdout"]) if content.get("stderr"): parts.append(f"STDERR: {content['stderr']}") - logs = "".join(parts) if parts else str(content) + logs = "".join(parts) else: logs = str(content) tool_input = self._server_tool_inputs.get(call_id, {}) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 033afea2ffa..21ca8db825c 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1775,7 +1775,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): parts.append(content["stdout"]) if content.get("stderr"): parts.append(f"STDERR: {content['stderr']}") - logs = "".join(parts) if parts else str(content) + logs = "".join(parts) else: logs = str(content) code_interpreter_results.append( From 5b3e84f383627c951c4d7bf5f150e4173c39fdc4 Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Tue, 17 Mar 2026 18:59:47 +0100 Subject: [PATCH 20/25] fix: address remaining review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Empty stdout/stderr now produces outputs=None (matching OpenAI parity) instead of outputs=[{logs:""}], in both streaming and non-streaming paths - Fix test fixture to use real Anthropic type "bash_code_execution_tool_result" instead of "code_execution_tool_result" - Add test for empty-output → outputs=None behavior - Add unit tests for _extract_tool_result_output_items: Pydantic objects, plain dicts (post-model_dump), empty/missing provider_specific_fields, and in-place substitution preserving output ordering --- litellm/llms/anthropic/chat/handler.py | 5 +- litellm/llms/anthropic/chat/transformation.py | 9 +- .../chat/test_anthropic_chat_handler.py | 72 +++++++- ...est_code_interpreter_results_extraction.py | 163 ++++++++++++++++++ 4 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 88d9ee65969..0b84830a8b0 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -716,6 +716,9 @@ class ModelResponseIterator: logs = str(content) tool_input = self._server_tool_inputs.get(call_id, {}) code = tool_input.get("command", "") if isinstance(tool_input, dict) else "" + log_outputs = ( + [OutputCodeInterpreterCallLog(type="logs", logs=logs)] if logs else None + ) results.append( OutputCodeInterpreterCall( type="code_interpreter_call", @@ -723,7 +726,7 @@ class ModelResponseIterator: code=code, container_id=None, status="completed", - outputs=[OutputCodeInterpreterCallLog(type="logs", logs=logs)], + outputs=log_outputs, ) ) return results diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 21ca8db825c..99b02e7ab46 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1778,6 +1778,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): logs = "".join(parts) else: logs = str(content) + log_outputs = ( + [OutputCodeInterpreterCallLog(type="logs", logs=logs)] + if logs + else None + ) code_interpreter_results.append( OutputCodeInterpreterCall( type="code_interpreter_call", @@ -1785,9 +1790,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): code=code_by_id.get(call_id, ""), container_id=container_id, status="completed", - outputs=[ - OutputCodeInterpreterCallLog(type="logs", logs=logs) - ], + outputs=log_outputs, ) ) provider_specific_fields["code_interpreter_results"] = ( diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index d7a04a054ab..ab298f6809b 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -1410,10 +1410,10 @@ def test_streaming_code_execution_input_assembled_from_deltas(): "type": "content_block_start", "index": 1, "content_block": { - "type": "code_execution_tool_result", + "type": "bash_code_execution_tool_result", "tool_use_id": "srvtoolu_01AAA", "content": { - "type": "code_execution_result", + "type": "bash_code_execution_result", "stdout": "hello\n", "stderr": "", "return_code": 0, @@ -1445,3 +1445,71 @@ def test_streaming_code_execution_input_assembled_from_deltas(): assert code_results[0].id == "srvtoolu_01AAA" assert code_results[0].code == "echo hello" assert code_results[0].outputs[0].logs == "hello\n" + + +def test_empty_output_produces_null_outputs(): + """ + When both stdout and stderr are empty, outputs should be None + (matching OpenAI's native behavior) rather than [{logs: ""}]. + """ + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01AAA", + "name": "bash_code_execution", + "input": {"command": "true"}, + }, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01AAA", + "content": { + "type": "bash_code_execution_result", + "stdout": "", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 1}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + + code_results = None + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + psf = None + if parsed.choices and parsed.choices[0].delta: + psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) + if psf and "code_interpreter_results" in psf: + code_results = psf["code_interpreter_results"] + + assert code_results is not None, "No code_interpreter_results emitted" + assert len(code_results) == 1 + assert code_results[0].id == "srvtoolu_01AAA" + assert ( + code_results[0].outputs is None + ), f"Expected outputs=None for empty execution, got {code_results[0].outputs}" diff --git a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py new file mode 100644 index 00000000000..c6ff1c7af8f --- /dev/null +++ b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py @@ -0,0 +1,163 @@ +""" +Tests for the Responses API _extract_tool_result_output_items path +and the non-streaming _hidden_params propagation of code_interpreter_results. +""" + +from unittest.mock import MagicMock + +from litellm.responses.litellm_completion_transformation.transformation import ( + LiteLLMCompletionResponsesConfig, +) +from litellm.types.responses.main import ( + OutputCodeInterpreterCall, + OutputCodeInterpreterCallLog, +) +from litellm.types.utils import Choices, Message, ModelResponse + + +def _make_model_response(code_interpreter_results=None, provider_specific_fields=None): + """Helper to build a ModelResponse with provider_specific_fields on the message.""" + psf = provider_specific_fields or {} + if code_interpreter_results is not None: + psf["code_interpreter_results"] = code_interpreter_results + msg = Message(content="test", provider_specific_fields=psf if psf else None) + choice = Choices(index=0, message=msg, finish_reason="stop") + resp = ModelResponse() + resp.choices = [choice] + return resp + + +def test_extract_tool_result_output_items_from_pydantic_objects(): + """Non-streaming path: code_interpreter_results are Pydantic OutputCodeInterpreterCall objects.""" + items = [ + OutputCodeInterpreterCall( + type="code_interpreter_call", + id="srvtoolu_01AAA", + code="echo hello", + container_id=None, + status="completed", + outputs=[OutputCodeInterpreterCallLog(type="logs", logs="hello\n")], + ), + OutputCodeInterpreterCall( + type="code_interpreter_call", + id="srvtoolu_01BBB", + code="echo world", + container_id=None, + status="completed", + outputs=[OutputCodeInterpreterCallLog(type="logs", logs="world\n")], + ), + ] + resp = _make_model_response(code_interpreter_results=items) + result = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) + assert len(result) == 2 + assert result[0].id == "srvtoolu_01AAA" + assert result[1].id == "srvtoolu_01BBB" + + +def test_extract_tool_result_output_items_from_dicts(): + """Streaming path: after model_dump(), code_interpreter_results are plain dicts.""" + items = [ + { + "type": "code_interpreter_call", + "id": "srvtoolu_01AAA", + "code": "echo hello", + "container_id": None, + "status": "completed", + "outputs": [{"type": "logs", "logs": "hello\n"}], + }, + ] + resp = _make_model_response(code_interpreter_results=items) + result = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) + assert len(result) == 1 + assert result[0]["id"] == "srvtoolu_01AAA" + + +def test_extract_tool_result_output_items_empty(): + """No code_interpreter_results → empty list.""" + resp = _make_model_response() + result = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) + assert result == [] + + +def test_extract_tool_result_output_items_no_provider_specific_fields(): + """Message with no provider_specific_fields → empty list.""" + msg = Message(content="test") + choice = Choices(index=0, message=msg, finish_reason="stop") + resp = ModelResponse() + resp.choices = [choice] + result = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) + assert result == [] + + +def test_in_place_substitution_preserves_ordering(): + """ + function_call items matching code_interpreter_results should be replaced + in-place, preserving the original output ordering. + + Simulates: [message, function_call(exec1), function_call(regular), function_call(exec2)] + Expected: [message, code_interpreter_call(exec1), function_call(regular), code_interpreter_call(exec2)] + """ + code_results = [ + OutputCodeInterpreterCall( + type="code_interpreter_call", + id="srvtoolu_01AAA", + code="echo first", + container_id=None, + status="completed", + outputs=[OutputCodeInterpreterCallLog(type="logs", logs="first\n")], + ), + OutputCodeInterpreterCall( + type="code_interpreter_call", + id="srvtoolu_01CCC", + code="echo third", + container_id=None, + status="completed", + outputs=[OutputCodeInterpreterCallLog(type="logs", logs="third\n")], + ), + ] + resp = _make_model_response(code_interpreter_results=code_results) + + # Build a mock responses_output list with interleaved items + class MockItem: + def __init__(self, type, call_id=None): + self.type = type + self.call_id = call_id + + msg_item = MockItem(type="message") + fc_exec1 = MockItem(type="function_call", call_id="srvtoolu_01AAA") + fc_regular = MockItem(type="function_call", call_id="srvtoolu_01BBB") + fc_exec2 = MockItem(type="function_call", call_id="srvtoolu_01CCC") + + responses_output = [msg_item, fc_exec1, fc_regular, fc_exec2] + + # Apply the same logic as _transform_chat_completion_choices_to_responses_output + tool_result_items = ( + LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) + ) + if tool_result_items: + result_by_id = { + (item.get("id") if isinstance(item, dict) else item.id): item + for item in tool_result_items + } + replaced_ids = set(result_by_id.keys()) + responses_output = [ + ( + result_by_id[getattr(item, "call_id", None)] + if ( + getattr(item, "type", None) == "function_call" + and getattr(item, "call_id", None) in replaced_ids + ) + else item + ) + for item in responses_output + ] + + # Verify ordering: message, code_interpreter(AAA), function_call(BBB), code_interpreter(CCC) + assert len(responses_output) == 4 + assert responses_output[0].type == "message" + assert responses_output[1].type == "code_interpreter_call" + assert responses_output[1].id == "srvtoolu_01AAA" + assert responses_output[2].type == "function_call" + assert responses_output[2].call_id == "srvtoolu_01BBB" + assert responses_output[3].type == "code_interpreter_call" + assert responses_output[3].id == "srvtoolu_01CCC" From 3962fbc33ac12c8a6e232dea488779f5570cb5f2 Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Tue, 17 Mar 2026 19:26:03 +0100 Subject: [PATCH 21/25] fix: non-dict tool result content falls back to outputs=None Replace str(content) fallback with empty string so non-dict content (e.g. list-shaped text_editor results) produces outputs=None instead of raw Python object representations in logs. --- litellm/llms/anthropic/chat/handler.py | 2 +- litellm/llms/anthropic/chat/transformation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 0b84830a8b0..387901588f0 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -713,7 +713,7 @@ class ModelResponseIterator: parts.append(f"STDERR: {content['stderr']}") logs = "".join(parts) else: - logs = str(content) + logs = "" tool_input = self._server_tool_inputs.get(call_id, {}) code = tool_input.get("command", "") if isinstance(tool_input, dict) else "" log_outputs = ( diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 99b02e7ab46..238d72eda44 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1777,7 +1777,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): parts.append(f"STDERR: {content['stderr']}") logs = "".join(parts) else: - logs = str(content) + logs = "" log_outputs = ( [OutputCodeInterpreterCallLog(type="logs", logs=logs)] if logs From 8f60117228821ccde41d4845382afee507dfb70c Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Wed, 18 Mar 2026 00:42:03 +0100 Subject: [PATCH 22/25] fix: guard code_interpreter conversion to bash_code_execution results only Skip non-bash tool result types (e.g. text_editor_code_execution_tool_result) to avoid producing empty code_interpreter_call items in Responses API output. --- litellm/llms/anthropic/chat/handler.py | 2 ++ litellm/llms/anthropic/chat/transformation.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 387901588f0..91fd3034066 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -703,6 +703,8 @@ class ModelResponseIterator: """ results = [] for tr in self.tool_results: + if tr.get("type") != "bash_code_execution_tool_result": + continue call_id = tr.get("tool_use_id", "") content = tr.get("content", {}) if isinstance(content, dict): diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 238d72eda44..ca101df0e95 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1767,6 +1767,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): pass code_interpreter_results = [] for tr in tool_results: + if tr.get("type") != "bash_code_execution_tool_result": + continue call_id = tr.get("tool_use_id", "") content = tr.get("content", {}) if isinstance(content, dict): From d10007cef49ae278e97c05e40ce367481e983975 Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Wed, 18 Mar 2026 11:15:15 +0100 Subject: [PATCH 23/25] test: add non-bash skip test and mock end-to-end streaming integration test - test_non_bash_tool_result_skipped: verifies text_editor results produce zero code_interpreter_call items - test_end_to_end_streaming_chunks_to_code_interpreter_output: exercises full path from Anthropic SSE chunks through ModelResponseIterator, stream_chunk_builder, and _extract_tool_result_output_items without a live server --- .../chat/test_anthropic_chat_handler.py | 68 +++++++++++ ...est_code_interpreter_results_extraction.py | 106 +++++++++++++++++- 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index ab298f6809b..20427e8cc94 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -1513,3 +1513,71 @@ def test_empty_output_produces_null_outputs(): assert ( code_results[0].outputs is None ), f"Expected outputs=None for empty execution, got {code_results[0].outputs}" + + +def test_non_bash_tool_result_skipped(): + """ + Tool result types other than bash_code_execution_tool_result (e.g. + text_editor_code_execution_tool_result) should be skipped and NOT + produce code_interpreter_call items. + """ + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01AAA", + "name": "text_editor", + "input": {"command": "view", "path": "/tmp/test.py"}, + }, + }, + {"type": "content_block_stop", "index": 0}, + # text_editor result — should NOT become a code_interpreter_call + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "text_editor_code_execution_tool_result", + "tool_use_id": "srvtoolu_01AAA", + "content": [ + {"type": "text", "text": "file contents here"}, + ], + }, + }, + {"type": "content_block_stop", "index": 1}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + + code_results = None + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + psf = None + if parsed.choices and parsed.choices[0].delta: + psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None) + if psf and "code_interpreter_results" in psf: + code_results = psf["code_interpreter_results"] + + # code_interpreter_results should be emitted but empty (no bash results) + assert ( + code_results is not None + ), "Expected code_interpreter_results key to be emitted" + assert ( + len(code_results) == 0 + ), f"Expected 0 code_interpreter_results for text_editor result, got {len(code_results)}" diff --git a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py index c6ff1c7af8f..eea9be38faa 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py +++ b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py @@ -1,10 +1,13 @@ """ -Tests for the Responses API _extract_tool_result_output_items path -and the non-streaming _hidden_params propagation of code_interpreter_results. +Tests for the Responses API _extract_tool_result_output_items path, +the non-streaming _hidden_params propagation of code_interpreter_results, +and mock end-to-end streaming integration. """ from unittest.mock import MagicMock +from litellm.llms.anthropic.chat.handler import ModelResponseIterator +from litellm.main import stream_chunk_builder from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) @@ -161,3 +164,102 @@ def test_in_place_substitution_preserves_ordering(): assert responses_output[2].call_id == "srvtoolu_01BBB" assert responses_output[3].type == "code_interpreter_call" assert responses_output[3].id == "srvtoolu_01CCC" + + +def test_end_to_end_streaming_chunks_to_code_interpreter_output(): + """ + Mock end-to-end test: Anthropic SSE chunks → ModelResponseIterator → + stream_chunk_builder → _extract_tool_result_output_items → final output + with code_interpreter_call items replacing function_call items. + + This exercises the full streaming data flow without a live server. + """ + # Realistic Anthropic streaming chunks for a single code execution + raw_chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_01XYZ", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 100, "output_tokens": 1}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "server_tool_use", + "id": "srvtoolu_01AAA", + "name": "bash_code_execution", + "input": {}, + }, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "input_json_delta", + "partial_json": '{"command": "echo e2e_test"}', + }, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "content_block_start", + "index": 1, + "content_block": { + "type": "bash_code_execution_tool_result", + "tool_use_id": "srvtoolu_01AAA", + "content": { + "type": "bash_code_execution_result", + "stdout": "e2e_test\n", + "stderr": "", + "return_code": 0, + }, + }, + }, + {"type": "content_block_stop", "index": 1}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 50}, + }, + ] + + # Step 1: Parse chunks through ModelResponseIterator (Anthropic handler) + iterator = ModelResponseIterator(None, sync_stream=True) + parsed_chunks = [] + for chunk in raw_chunks: + parsed = iterator.chunk_parser(chunk) + d = parsed.model_dump() + # In production, CustomStreamWrapper sets the model on each chunk; + # stream_chunk_builder requires it. + d["model"] = "claude-sonnet-4-20250514" + parsed_chunks.append(d) + + # Step 2: Assemble via stream_chunk_builder (simulates end-of-stream) + assembled = stream_chunk_builder(chunks=parsed_chunks) + assert assembled is not None + + # Verify stream_chunk_builder picked up code_interpreter_results via last-value-wins + psf = assembled.choices[0].message.provider_specific_fields + assert psf is not None + assert "code_interpreter_results" in psf + code_results = psf["code_interpreter_results"] + assert len(code_results) == 1 + # After model_dump + stream_chunk_builder, results are plain dicts + assert code_results[0]["id"] == "srvtoolu_01AAA" + assert code_results[0]["code"] == "echo e2e_test" + + # Step 3: Extract via _extract_tool_result_output_items (Responses API layer) + tool_result_items = ( + LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(assembled) + ) + assert len(tool_result_items) == 1 + item = tool_result_items[0] + # Items are dicts after the model_dump path + assert item["type"] == "code_interpreter_call" + assert item["id"] == "srvtoolu_01AAA" + assert item["code"] == "echo e2e_test" + assert item["outputs"][0]["logs"] == "e2e_test\n" From cf8d1ac521648fea10bc121cd51da166e96493a4 Mon Sep 17 00:00:00 2001 From: Andrzej Pomirski Date: Wed, 18 Mar 2026 12:05:25 +0100 Subject: [PATCH 24/25] fix: streaming container_id and consistent Pydantic types in output - Populate container_id on streaming code_interpreter_results by re-emitting at message_delta when container info arrives - Reconstruct Pydantic OutputCodeInterpreterCall objects from plain dicts in _extract_tool_result_output_items so responses_output has uniform types across streaming and non-streaming paths --- litellm/llms/anthropic/chat/handler.py | 14 +++++++++++++- .../transformation.py | 14 +++++++++----- .../test_code_interpreter_results_extraction.py | 17 ++++++++++------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 91fd3034066..70ecf91725d 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -546,6 +546,7 @@ class ModelResponseIterator: self._server_tool_inputs: Dict[str, Any] = {} self.tool_results: List[Dict[str, Any]] = [] self._current_server_tool_id: Optional[str] = None + self._container_id: Optional[str] = None def check_empty_tool_call_args(self) -> bool: """ @@ -726,7 +727,7 @@ class ModelResponseIterator: type="code_interpreter_call", id=call_id, code=code, - container_id=None, + container_id=self._container_id, status="completed", outputs=log_outputs, ) @@ -928,6 +929,17 @@ class ModelResponseIterator: finish_reason, usage, container = self._handle_message_delta(chunk) if container: provider_specific_fields["container"] = container + # Store container_id and re-emit code_interpreter_results + # so stream_chunk_builder's last-value-wins picks up the + # version with container_id populated. + container_id = ( + container.get("id") if isinstance(container, dict) else None + ) + if container_id and self.tool_results: + self._container_id = container_id + provider_specific_fields["code_interpreter_results"] = ( + self._build_code_interpreter_results() + ) elif type_chunk == "message_start": """ Anthropic diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index b7f7e9adda2..cf18511bfa3 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -1738,10 +1738,7 @@ class LiteLLMCompletionResponsesConfig: ) ) if tool_result_items: - result_by_id = { - (item.get("id") if isinstance(item, dict) else item.id): item - for item in tool_result_items - } + result_by_id = {item.id: item for item in tool_result_items} replaced_ids = set(result_by_id.keys()) responses_output = [ ( @@ -1778,7 +1775,14 @@ class LiteLLMCompletionResponsesConfig: continue results = psf.get("code_interpreter_results") if results and isinstance(results, list): - output_items.extend(results) + for item in results: + # In the streaming path, items are plain dicts after + # model_dump() in stream_chunk_builder. Reconstruct + # Pydantic objects so responses_output has a uniform type. + if isinstance(item, dict): + output_items.append(OutputCodeInterpreterCall(**item)) + else: + output_items.append(item) return output_items @staticmethod diff --git a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py index eea9be38faa..60e45c9b8ce 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py +++ b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py @@ -58,7 +58,8 @@ def test_extract_tool_result_output_items_from_pydantic_objects(): def test_extract_tool_result_output_items_from_dicts(): - """Streaming path: after model_dump(), code_interpreter_results are plain dicts.""" + """Streaming path: after model_dump(), code_interpreter_results are plain dicts. + _extract_tool_result_output_items reconstructs them as Pydantic objects.""" items = [ { "type": "code_interpreter_call", @@ -72,7 +73,8 @@ def test_extract_tool_result_output_items_from_dicts(): resp = _make_model_response(code_interpreter_results=items) result = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) assert len(result) == 1 - assert result[0]["id"] == "srvtoolu_01AAA" + assert isinstance(result[0], OutputCodeInterpreterCall) + assert result[0].id == "srvtoolu_01AAA" def test_extract_tool_result_output_items_empty(): @@ -258,8 +260,9 @@ def test_end_to_end_streaming_chunks_to_code_interpreter_output(): ) assert len(tool_result_items) == 1 item = tool_result_items[0] - # Items are dicts after the model_dump path - assert item["type"] == "code_interpreter_call" - assert item["id"] == "srvtoolu_01AAA" - assert item["code"] == "echo e2e_test" - assert item["outputs"][0]["logs"] == "e2e_test\n" + # Items are reconstructed as Pydantic OutputCodeInterpreterCall objects + assert isinstance(item, OutputCodeInterpreterCall) + assert item.type == "code_interpreter_call" + assert item.id == "srvtoolu_01AAA" + assert item.code == "echo e2e_test" + assert item.outputs[0].logs == "e2e_test\n" From 4770b657e15a25815eccf211f87843db356625e8 Mon Sep 17 00:00:00 2001 From: Chesars Date: Wed, 18 Mar 2026 22:05:27 -0300 Subject: [PATCH 25/25] =?UTF-8?q?refactor:=20extract=20duplicated=20stdout?= =?UTF-8?q?/stderr=20=E2=86=92=20logs=20logic=20to=20shared=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- litellm/llms/anthropic/chat/handler.py | 15 ++------------- litellm/llms/anthropic/chat/transformation.py | 17 ++--------------- litellm/types/responses/main.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 70ecf91725d..7dce72f1e82 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -50,7 +50,7 @@ from litellm.types.llms.openai import ( ) from litellm.types.responses.main import ( OutputCodeInterpreterCall, - OutputCodeInterpreterCallLog, + build_code_interpreter_log_outputs, ) from litellm.types.utils import ( Delta, @@ -708,20 +708,9 @@ class ModelResponseIterator: continue call_id = tr.get("tool_use_id", "") content = tr.get("content", {}) - if isinstance(content, dict): - parts = [] - if content.get("stdout"): - parts.append(content["stdout"]) - if content.get("stderr"): - parts.append(f"STDERR: {content['stderr']}") - logs = "".join(parts) - else: - logs = "" + log_outputs = build_code_interpreter_log_outputs(content) tool_input = self._server_tool_inputs.get(call_id, {}) code = tool_input.get("command", "") if isinstance(tool_input, dict) else "" - log_outputs = ( - [OutputCodeInterpreterCallLog(type="logs", logs=logs)] if logs else None - ) results.append( OutputCodeInterpreterCall( type="code_interpreter_call", diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index ca101df0e95..bbc73fcfd40 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -61,7 +61,7 @@ from litellm.types.utils import ( ) from litellm.types.responses.main import ( OutputCodeInterpreterCall, - OutputCodeInterpreterCallLog, + build_code_interpreter_log_outputs, ) from litellm.utils import ( ModelResponse, @@ -1771,20 +1771,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): continue call_id = tr.get("tool_use_id", "") content = tr.get("content", {}) - if isinstance(content, dict): - parts = [] - if content.get("stdout"): - parts.append(content["stdout"]) - if content.get("stderr"): - parts.append(f"STDERR: {content['stderr']}") - logs = "".join(parts) - else: - logs = "" - log_outputs = ( - [OutputCodeInterpreterCallLog(type="logs", logs=logs)] - if logs - else None - ) + log_outputs = build_code_interpreter_log_outputs(content) code_interpreter_results.append( OutputCodeInterpreterCall( type="code_interpreter_call", diff --git a/litellm/types/responses/main.py b/litellm/types/responses/main.py index e46857565c7..ebd2ad5b5a8 100644 --- a/litellm/types/responses/main.py +++ b/litellm/types/responses/main.py @@ -67,6 +67,24 @@ class OutputCodeInterpreterCall(BaseLiteLLMOpenAIResponseObject): outputs: Optional[List[OutputCodeInterpreterCallLog]] +def build_code_interpreter_log_outputs( + content: Any, +) -> Optional[List[OutputCodeInterpreterCallLog]]: + """Convert Anthropic bash_code_execution stdout/stderr to log outputs. + + Shared by streaming (handler.py) and non-streaming (transformation.py) paths. + """ + if not isinstance(content, dict): + return None + parts = [] + if content.get("stdout"): + parts.append(content["stdout"]) + if content.get("stderr"): + parts.append(f"STDERR: {content['stderr']}") + logs = "".join(parts) + return [OutputCodeInterpreterCallLog(type="logs", logs=logs)] if logs else None + + class GenericResponseOutputItem(BaseLiteLLMOpenAIResponseObject): """ Generic response API output item