mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-31 07:30:43 +00:00
serialize_lc_object() passed floats through unchanged, so a non-finite
value (NaN/inf/-inf) survived serialization and broke the two display
paths that share it with opposite JSON policies: the SSE wire
(format_sse / Redis bridge use the lenient default and emit bare
NaN/Infinity tokens the langgraph-sdk/useStream decoder rejects,
breaking the live stream) and the REST surface (Starlette JSONResponse
uses allow_nan=False and raises, turning a thread read into HTTP 500).
Non-finite floats reach the serializer from ordinary runtime data:
json.loads("1e400") yields inf for a tool parsing external JSON, a
provider logprob of -inf for a zero-probability token, or a divide-by-
zero ratio. They round-trip inside Python and checkpoint persistence and
only fail at the JSON boundary.
Normalize non-finite floats to None and recurse into model_dump()/dict()
output so nested values (e.g. a message's response_metadata) are
normalized too. This is a display/wire normalization only; persisted
checkpoint state is never routed through this serializer. Finite floats
are unchanged.
Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
72 lines
2.4 KiB
Python
72 lines
2.4 KiB
Python
"""Tests for SSE frame formatting utilities."""
|
|
|
|
import json
|
|
|
|
|
|
def _format_sse(event: str, data, *, event_id: str | None = None) -> str:
|
|
from app.gateway.services import format_sse
|
|
|
|
return format_sse(event, data, event_id=event_id)
|
|
|
|
|
|
def test_sse_end_event_data_null():
|
|
"""End event should have data: null."""
|
|
frame = _format_sse("end", None)
|
|
assert "data: null" in frame
|
|
|
|
|
|
def test_sse_metadata_event():
|
|
"""Metadata event should include run_id and attempt."""
|
|
frame = _format_sse("metadata", {"run_id": "abc", "attempt": 1}, event_id="123-0")
|
|
assert "event: metadata" in frame
|
|
assert "id: 123-0" in frame
|
|
|
|
|
|
def test_sse_error_format():
|
|
"""Error event should use message/name format."""
|
|
frame = _format_sse("error", {"message": "boom", "name": "ValueError"})
|
|
parsed = json.loads(frame.split("data: ")[1].split("\n")[0])
|
|
assert parsed["message"] == "boom"
|
|
assert parsed["name"] == "ValueError"
|
|
|
|
|
|
def test_sse_frame_rejects_non_finite_floats():
|
|
"""A non-finite float must not reach the wire as a bare NaN/Infinity token.
|
|
|
|
The canonical serializer normalizes non-finite floats before they reach
|
|
``format_sse``; this guards the wire boundary directly so a regression in
|
|
either layer is caught. A strict ``parse_constant`` mirrors the JS
|
|
``useStream`` / ``langgraph-sdk`` SSE decoder, which rejects such tokens.
|
|
"""
|
|
from deerflow.runtime.serialization import serialize
|
|
|
|
payload = serialize({"messages": [], "metric": float("inf")}, mode="values")
|
|
frame = _format_sse("values", payload)
|
|
data_line = frame.split("data: ")[1].split("\n")[0]
|
|
|
|
def _reject_constant(token):
|
|
raise ValueError(f"non-finite JSON token: {token}")
|
|
|
|
json.loads(data_line, parse_constant=_reject_constant)
|
|
|
|
|
|
def test_sse_messages_frame_rejects_non_finite_floats_in_metadata():
|
|
"""A non-finite float inside messages-mode metadata must not reach the
|
|
wire as a bare NaN/Infinity token either (#7669)."""
|
|
from deerflow.runtime.serialization import serialize
|
|
|
|
payload = serialize(
|
|
(
|
|
{"type": "AIMessageChunk", "content": "hi"},
|
|
{"langgraph_node": "model", "score": float("inf")},
|
|
),
|
|
mode="messages",
|
|
)
|
|
frame = _format_sse("messages", payload)
|
|
data_line = frame.split("data: ")[1].split("\n")[0]
|
|
|
|
def _reject_constant(token):
|
|
raise ValueError(f"non-finite JSON token: {token}")
|
|
|
|
json.loads(data_line, parse_constant=_reject_constant)
|