[Frontend] Omit absent fields from /inference/v1/generate stream chunks (#57264)

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
aoshen02
2026-09-17 05:58:56 +00:00
committed by GitHub
co-authored by Claude Fable 5.1
parent e6b1a5e5e3
commit 8538017f4b
3 changed files with 67 additions and 7 deletions
@@ -679,6 +679,36 @@ mod tests {
assert!(response.mm_placeholders.is_none());
}
#[test]
fn stream_chunk_omits_absent_fields() {
// Contract shared with the Python frontend: absent optional fields
// are omitted from stream chunks, not serialized as null.
let chunk = GenerateStreamResponse {
request_id: "raw-1".to_string(),
choices: vec![GenerateResponseStreamChoice {
index: 0,
logprobs: None,
finish_reason: None,
token_ids: vec![1],
}],
usage: None,
prompt_token_ids: None,
mm_placeholders: None,
};
let json = serde_json::to_value(&chunk).expect("serialize chunk");
for key in ["usage", "prompt_token_ids", "mm_placeholders"] {
assert!(json.get(key).is_none(), "{key} should be omitted");
}
for key in ["logprobs", "finish_reason"] {
assert!(
json["choices"][0].get(key).is_none(),
"{key} should be omitted"
);
}
}
#[test]
fn extract_mm_placeholders_returns_none_without_features() {
assert!(extract_mm_placeholders(None).is_none());
@@ -569,6 +569,40 @@ async def test_stream_omits_prompt_metadata_by_default():
assert "mm_placeholders" not in chunk
@pytest.mark.asyncio
async def test_stream_chunks_omit_absent_fields():
"""Absent optional fields are omitted from stream chunks, like Rust."""
engine = _mock_engine()
async def mock_generate(*args, **kwargs):
yield _make_request_output("req-1", token_ids=[10])
yield _make_request_output(
"req-1", token_ids=[20], finish_reason="stop", finished=True
)
engine.generate = MagicMock(side_effect=mock_generate)
serving = _build_serving_tokens(engine)
request = GenerateRequest(
token_ids=[1, 2, 3],
sampling_params=SamplingParams(max_tokens=2),
model=MODEL_NAME,
stream=True,
)
response = await serving.serve_tokens(request)
parsed = _parse_sse_chunks([chunk async for chunk in response])
data_chunks = [c for c in parsed if isinstance(c, dict) and c.get("choices")]
assert len(data_chunks) == 2
for chunk in data_chunks:
assert "usage" not in chunk
assert "prompt_token_ids" not in chunk
assert "mm_placeholders" not in chunk
assert "logprobs" not in chunk["choices"][0]
assert "finish_reason" not in data_chunks[0]["choices"][0]
assert data_chunks[1]["choices"][0]["finish_reason"] == "stop"
@pytest.mark.asyncio
async def test_stream_text_only_prompt_omits_mm_placeholders_key():
engine = _mock_engine()
@@ -486,13 +486,9 @@ class ServingTokens(GenerateBaseServing):
total_tokens=(num_prompt_tokens + num_generated_tokens[i]),
)
# Omit absent prompt metadata (Rust skips None fields).
exclude = {
name
for name in ("prompt_token_ids", "mm_placeholders")
if getattr(chunk, name) is None
}
yield f"data: {chunk.model_dump_json(exclude=exclude)}\n\n"
# Omit absent fields, like the Rust frontend and the
# final usage chunk below.
yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n"
total_completion_tokens = sum(num_generated_tokens)
final_usage_info = UsageInfo(