feat: add a required first-token event to the transport port

Transport.complete gains the keyword-only first_token_event (no default,
per the port convention): streaming sets it on the first delta, the
non-streaming path accepts it but never sets it, None means the caller
does not observe the first token. All fake/wrapping transports and the
three direct call sites follow the signature; the e2e wrapper forwards.

Red-green evidence: tests/outputs/137/t1/ (batch A TypeError red, then
147 file tests + 1550 unit tests green).
This commit is contained in:
2026-09-10 13:16:38 -04:00
parent 166b2865d0
commit 0a6d6225db
12 changed files with 114 additions and 9 deletions
+64 -1
View File
@@ -3,6 +3,7 @@
SSE 帧样本按三项目真实网关响应形态二次构造(OpenAI 兼容 chunk 结构)。
"""
import asyncio
import json
import httpx
@@ -79,7 +80,9 @@ def _transport_for(handler, *, registry=None):
)
async def _complete(transport, source, *, stream=True, overlay=None, reasoning_effort=None):
async def _complete(
transport, source, *, stream=True, overlay=None, reasoning_effort=None, first_token_event=None
):
return await transport.complete(
messages=[{"role": "user", "content": "hi"}],
source=source,
@@ -87,6 +90,7 @@ async def _complete(transport, source, *, stream=True, overlay=None, reasoning_e
overlay=overlay or {},
call_id="cid-1",
reasoning_effort=reasoning_effort,
first_token_event=first_token_event,
)
@@ -214,6 +218,65 @@ class TestStreamHappyPath:
assert await _recorded_cost(result, source) is None
class TestFirstTokenEvent:
"""首 token 处置位(1.3.7 对冲 H2): 流式置位、非流式永不置位、None 不观测。"""
async def test_stream_sets_first_token_event(self):
"""流式首 token(内容或思考增量)到达即置位——对冲触发窗的取消信号。"""
def handler(request):
return _sse_stream(
_chunk(reasoning="ponder"), _chunk(content="hi"), _chunk(usage=_USAGE)
)
event = asyncio.Event()
result = await _complete(_transport_for(handler), _source(), first_token_event=event)
assert result.content == "hi"
assert event.is_set()
async def test_non_stream_never_sets_first_token_event(self):
"""非流式物理上无中途信号: 即使调用方给了事件,本路径也永不置位。"""
def handler(request):
return httpx.Response(
200, json={"choices": [{"message": {"content": "42"}}], "usage": _USAGE}
)
event = asyncio.Event()
result = await _complete(
_transport_for(handler), _source(), stream=False, first_token_event=event
)
assert result.content == "42"
assert not event.is_set()
async def test_none_first_token_event_keeps_behavior(self):
"""`None` = 调用方不观测首 token(未启用对冲): 行为与旧版逐字相同。"""
def handler(request):
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
result = await _complete(_transport_for(handler), _source(), first_token_event=None)
assert result.content == "ok"
assert result.ttft_ms is not None
async def test_first_token_event_is_required_keyword(self):
"""端口必填约定: 漏传必须 TypeError——默认值会把"漏传"伪装成"不观测""""
def handler(request):
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
transport = _transport_for(handler)
with pytest.raises(TypeError):
await transport.complete(
messages=[{"role": "user", "content": "hi"}],
source=_source(),
stream=True,
overlay={},
call_id="cid-1",
reasoning_effort=None,
)
class TestMissingDoneSemantics:
def _no_done_handler(self, request):
return _sse_stream(_chunk(content="partial"), _chunk(usage=_USAGE), done=False)