fix: judge stream completion by finish reason and trailing usage
Both transport paths now share one verdict: a stream is complete on [DONE] or on finish_reason followed by measured usage and a clean EOF (both modes; missing_done only governs real truncation). Outcomes follow the design's first-match table: upstream_error finishes stay transient, fidelity mode delivers reasoning-only/length results, text mode raises ResultInvalid for content-less length/filter endings, and non-stream fidelity judges before building the snapshot so truncated tools reach tool_truncated. Projections honour the reasoning alias, dialect warnings are bounded, and responses carry finish_reason and stream_end. Red: 39 of 46 tests in tests/unit/test_completion_transport.py failed before this change (e.g. eof_after_finish raised 保真响应缺 [DONE]; reasoning-only length raised empty_completion; reasoning-only stream hit the TTFT watchdog, re-confirmed by disabling alias liveness).
This commit is contained in:
@@ -146,10 +146,10 @@ async def test_invalid_mode_combinations_fail_before_http(kwargs):
|
||||
|
||||
|
||||
async def test_openai_never_salvages():
|
||||
"""已收完整工具也必须有 DONE。"""
|
||||
"""无 finish 后 usage 的截断流即使收全工具也不打捞(1.3.10: 判据由 DONE 改为完成判定)。"""
|
||||
transport = _transport_for(lambda _: _sse_stream(*tool_frames(), done=False))
|
||||
try:
|
||||
with pytest.raises(TransientError, match="DONE"):
|
||||
with pytest.raises(TransientError, match="missing_done"):
|
||||
await transport.complete(
|
||||
messages=[],
|
||||
source=_source(missing_done="salvage"),
|
||||
@@ -165,6 +165,28 @@ async def test_openai_never_salvages():
|
||||
await transport.aclose()
|
||||
|
||||
|
||||
async def test_openai_finish_with_trailing_usage_is_complete_without_done():
|
||||
"""对照: 同一工具流补上 finish 之后的 usage 帧,即使无 DONE 也完整交回(#31)。"""
|
||||
usage = 'data: {"choices": [], "usage": {"prompt_tokens": 3, "completion_tokens": 4}}\n\n'
|
||||
transport = _transport_for(lambda _: _sse_stream(*tool_frames(), usage, done=False))
|
||||
try:
|
||||
result = await transport.complete(
|
||||
messages=[],
|
||||
source=_source(),
|
||||
stream=True,
|
||||
overlay={},
|
||||
call_id="c",
|
||||
reasoning_effort=None,
|
||||
first_token_event=None,
|
||||
content_event=None,
|
||||
response_mode="openai",
|
||||
)
|
||||
finally:
|
||||
await transport.aclose()
|
||||
assert result.stream_end == "eof_after_finish"
|
||||
assert [t["id"] for t in result.completion.message["tool_calls"]] == ["a", "b"]
|
||||
|
||||
|
||||
async def test_tool_progress_sets_events():
|
||||
"""工具首进展驱动两个事件与时间观测。"""
|
||||
import asyncio
|
||||
|
||||
Reference in New Issue
Block a user