From 6fb5e04fcd68de404e1714a4fcdb7f3d17914c8c Mon Sep 17 00:00:00 2001 From: George Weale Date: Wed, 5 Aug 2026 16:32:31 -0700 Subject: [PATCH] fix: stop logging the full live LlmRequest in base_llm_flow Co-authored-by: George Weale PiperOrigin-RevId: 959935395 --- .../adk/flows/llm_flows/base_llm_flow.py | 9 +++- .../flows/llm_flows/test_base_llm_flow.py | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/google/adk/flows/llm_flows/base_llm_flow.py b/src/google/adk/flows/llm_flows/base_llm_flow.py index 13fce952..171fa8c6 100644 --- a/src/google/adk/flows/llm_flows/base_llm_flow.py +++ b/src/google/adk/flows/llm_flows/base_llm_flow.py @@ -591,10 +591,15 @@ class BaseLlmFlow(ABC): llm_request.model = agent.canonical_live_model.model llm = self.__get_llm(invocation_context) + # Only log non-sensitive request metadata. The full request carries the + # user conversation and http_options.headers, which may hold credentials. logger.debug( - 'Establishing live connection for agent: %s with llm request: %s', + 'Establishing live connection for agent: %s, model: %s, contents: %s,' + ' response modalities: %s', agent.name, - llm_request, + llm_request.model, + len(llm_request.contents), + llm_request.live_connect_config.response_modalities, ) attempt = 1 diff --git a/tests/unittests/flows/llm_flows/test_base_llm_flow.py b/tests/unittests/flows/llm_flows/test_base_llm_flow.py index 67b6a408..f370da66 100644 --- a/tests/unittests/flows/llm_flows/test_base_llm_flow.py +++ b/tests/unittests/flows/llm_flows/test_base_llm_flow.py @@ -15,6 +15,7 @@ """Unit tests for BaseLlmFlow toolset integration.""" import asyncio +import logging from unittest import mock from unittest.mock import AsyncMock @@ -930,6 +931,59 @@ async def test_run_live_skips_send_history_on_resumption(): mock_connection.send_history.assert_not_called() +@pytest.mark.asyncio +async def test_run_live_does_not_log_http_options_headers(caplog): + """run_live must not log http_options headers, which can carry secrets.""" + + sentinel = 'do-not-log-this-live-credential' + agent = Agent(name='test_agent', model=Gemini()) + invocation_context = await testing_utils.create_invocation_context( + agent=agent, + run_config=RunConfig( + http_options=types.HttpOptions( + headers={'Authorization': f'Bearer {sentinel}'} + ) + ), + ) + invocation_context.live_request_queue = LiveRequestQueue() + + flow = BaseLlmFlowForTesting() + + # We need a way to break the infinite loop in run_live for testing. + class StopError(Exception): + pass + + async def mock_receive(): + if False: # Makes this function an async generator. + yield + raise StopError('stop') + + mock_connection = mock.AsyncMock() + mock_connection.receive = mock.Mock(side_effect=mock_receive) + + with caplog.at_level(logging.DEBUG, logger='google_adk'): + with mock.patch.object(flow, '_send_to_model', new_callable=AsyncMock): + with mock.patch( + 'google.adk.models.google_llm.Gemini.connect' + ) as mock_connect: + mock_connect.return_value.__aenter__.return_value = mock_connection + + try: + async for _ in flow.run_live(invocation_context): + pass + except StopError: + pass + + # The request headers reached the flow, so the log line had access to them. + assert ( + invocation_context.run_config.http_options.headers['Authorization'] + == f'Bearer {sentinel}' + ) + assert sentinel not in caplog.text + # The log line is still there and still useful. + assert 'Establishing live connection for agent: test_agent' in caplog.text + + @pytest.mark.asyncio async def test_live_session_resumption_go_away(): """Test that go_away triggers reconnection."""