fix(serialization): strip base64 image data from streamed values events (#3631)

The SSE stream's `values` snapshots serialize the full state, including the
hide_from_ui human messages that ViewImageMiddleware fills with base64 image
payloads. #3535 stripped those from the REST wait/history/state endpoints via
serialize_channel_values_for_api, but the streaming path (worker publishes
serialize(chunk, mode="values")) still went through serialize_channel_values,
so the same base64 leaked to the frontend over SSE. Route values-mode
serialization through serialize_channel_values_for_api as well. Non-hidden
messages and https image URLs are left untouched.
This commit is contained in:
Yufeng He 2026-06-19 11:23:07 +08:00 committed by GitHub
parent b5a4d3414b
commit 3e5c76eb0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 2 deletions

View File

@ -133,11 +133,14 @@ def serialize(obj: Any, *, mode: str = "") -> Any:
"""Serialize LangChain objects with mode-specific handling.
* ``messages`` obj is ``(message_chunk, metadata_dict)``
* ``values`` obj is the full state dict; ``__pregel_*`` keys stripped
* ``values`` obj is the full state dict; ``__pregel_*`` keys stripped and
base64 ``data:`` image blocks dropped from hide_from_ui messages
* everything else recursive ``model_dump()`` / ``dict()`` fallback
"""
if mode == "messages":
return serialize_messages_tuple(obj)
if mode == "values":
return serialize_channel_values(obj) if isinstance(obj, dict) else serialize_lc_object(obj)
# ``values`` snapshots stream the full state to the frontend, so they
# must drop base64 image payloads the same way the REST endpoints do.
return serialize_channel_values_for_api(obj) if isinstance(obj, dict) else serialize_lc_object(obj)
return serialize_lc_object(obj)

View File

@ -328,3 +328,32 @@ def test_serialize_channel_values_for_api_no_messages():
result = serialize_channel_values_for_api({"title": "empty"})
assert result == {"title": "empty"}
def test_serialize_values_mode_strips_base64_from_hidden_messages():
"""The SSE stream emits ``values`` snapshots of the full state, so it must
strip base64 image data from hide_from_ui messages just like the REST
endpoints do otherwise the same payload leaks over the stream."""
import json
from deerflow.runtime.serialization import serialize
state = {
"messages": [
_make_msg(
[
{"type": "text", "text": "context"},
{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,iVBOR..."},
},
],
hide_from_ui=True,
),
],
}
result = serialize(state, mode="values")
# the hidden message survives (count/order preserved) but the data: block is gone
assert len(result["messages"]) == 1
assert "data:image/png;base64" not in json.dumps(result)
assert result["messages"][0]["content"] == [{"type": "text", "text": "context"}]