diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 6c6ba9e12b4..8bab29cae47 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -2319,7 +2319,15 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta= _fire_first_delta() agent._fire_reasoning_delta(thinking_text) - # Return the native Anthropic Message for downstream processing + # Return the native Anthropic Message for downstream processing. + # If the stream was interrupted (the event loop broke out above on + # agent._interrupt_requested), do NOT call get_final_message() — on + # a partially-consumed stream the SDK may hang draining remaining + # events or return a Message with incomplete tool_use blocks (partial + # JSON in `input`). The outer poll loop raises InterruptedError, so + # this return value is discarded anyway. + if agent._interrupt_requested: + return None return stream.get_final_message() def _call(): diff --git a/run_agent.py b/run_agent.py index 6a5fe32d29b..125f7dff119 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4429,9 +4429,19 @@ class AIAgent: return True return False + # 20 MB base64 ≈ 15 MB decoded image — generous but prevents OOM from an + # oversized data: URL (a 100 MB+ payload creates ~275 MB of memory pressure, + # and gateway users sharing the same process can trivially OOM it). + _MAX_DATA_URL_BASE64_BYTES = 20 * 1024 * 1024 + @staticmethod def _materialize_data_url_for_vision(image_url: str) -> tuple[str, Optional[Path]]: header, _, data = str(image_url or "").partition(",") + if len(data) > AIAgent._MAX_DATA_URL_BASE64_BYTES: + logger.warning( + "data-URL payload too large (%d bytes), skipping", len(data) + ) + return "", None mime = "image/jpeg" if header.startswith("data:"): mime_part = header[len("data:"):].split(";", 1)[0].strip()