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:
@@ -297,6 +297,8 @@ class RetryMW:
|
||||
# 逐次尝试原样重传: 换源不改变调用方要的档位(源级默认由 transport
|
||||
# 自己按选中的源解析,两者在 effective_effort 里汇合)
|
||||
reasoning_effort=request.reasoning_effort,
|
||||
# T3 对冲编排接线前恒为 None: 调用方不观测首 token(计划 §3.1)
|
||||
first_token_event=None,
|
||||
)
|
||||
if result.usage_source == "unavailable":
|
||||
# 用量不可得时按入场预扣量结算(delta==0),否则押金会被整笔退回,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
时间量纲一律**秒**(CHS Redis 实现内部的毫秒换算是后端私事,不进契约)。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
@@ -46,6 +47,10 @@ class Transport(Protocol):
|
||||
该参数**不设默认值**,与 `TelemetryRecorder.record_llm_call` 同一既有约定:
|
||||
库外无第三方实现者,写全签名的成本为零,而默认值会把"某一层漏传"变成静默的
|
||||
"调用方没表态"——一次本该报错的漏配就此变成一次悄悄涨价的调用。
|
||||
|
||||
`first_token_event` 同一约定(1.3.7 对冲 H2): `None` = 调用方不观测首 token
|
||||
(未启用对冲);非流式实现**永不置位**(物理上无中途信号,事件自然退化为纯时间
|
||||
阈值),流式实现在首个增量(内容或思考)到达时置位。
|
||||
"""
|
||||
|
||||
async def complete(
|
||||
@@ -57,6 +62,7 @@ class Transport(Protocol):
|
||||
overlay: dict[str, Any],
|
||||
call_id: str,
|
||||
reasoning_effort: Effort | None,
|
||||
first_token_event: asyncio.Event | None,
|
||||
) -> TransportResult: ...
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from polygateway.types import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Mapping
|
||||
|
||||
_THINK_PATTERN = re.compile(r"<think>(.*?)</think>", re.DOTALL)
|
||||
@@ -432,11 +433,14 @@ class OpenAICompatTransport:
|
||||
overlay: dict[str, Any],
|
||||
call_id: str,
|
||||
reasoning_effort: Effort | None,
|
||||
first_token_event: asyncio.Event | None,
|
||||
) -> TransportResult:
|
||||
"""一次原始调用;HTTP/线路/流式异常按 ARCH §6.2 翻译为领域错误。
|
||||
|
||||
`reasoning_effort` 是**请求级**档位(`None` = 不表态);它与源级配置的优先级
|
||||
在 `_build_payload` 里由 `effective_effort` 裁定,本层只负责把它送到。
|
||||
`first_token_event` 为对冲处置位: `None` = 不观测首 token;仅流式路径
|
||||
(`_complete_stream`)在首个增量到达时置位,非流式路径收它但永不置位。
|
||||
"""
|
||||
profile = get_provider(source.provider, registry=self._registry)
|
||||
try:
|
||||
@@ -461,9 +465,13 @@ class OpenAICompatTransport:
|
||||
ctx: dict[str, Any] = {"source_name": source.name, "operation": "chat"}
|
||||
try:
|
||||
if stream:
|
||||
result = await self._complete_stream(client, url, payload, source, profile)
|
||||
result = await self._complete_stream(
|
||||
client, url, payload, source, profile, first_token_event
|
||||
)
|
||||
else:
|
||||
result = await self._complete_once(client, url, payload, source, profile)
|
||||
result = await self._complete_once(
|
||||
client, url, payload, source, profile, first_token_event
|
||||
)
|
||||
except StreamLivenessTimeout as exc:
|
||||
raise TransientError(f"{source.name} 流活性超时({exc.kind})", **ctx) from exc
|
||||
except httpx.TimeoutException as exc:
|
||||
@@ -549,6 +557,7 @@ class OpenAICompatTransport:
|
||||
payload: dict[str, Any],
|
||||
source: SourceConfig,
|
||||
profile: ProviderProfile,
|
||||
first_token_event: asyncio.Event | None,
|
||||
) -> TransportResult:
|
||||
started = time.monotonic()
|
||||
async with client.stream("POST", url, json=payload) as resp:
|
||||
@@ -573,6 +582,10 @@ class OpenAICompatTransport:
|
||||
now = time.monotonic()
|
||||
if ttft_ms is None:
|
||||
ttft_ms = (now - started) * 1000
|
||||
# 首个增量即对冲语义上的"首 token"(思考增量同样是存活证据,
|
||||
# 与看门狗活性口径一致);None = 调用方未启用对冲,零分支成本
|
||||
if first_token_event is not None:
|
||||
first_token_event.set()
|
||||
else:
|
||||
max_gap = max(max_gap, (now - last) * 1000)
|
||||
last = now
|
||||
@@ -650,8 +663,13 @@ class OpenAICompatTransport:
|
||||
payload: dict[str, Any],
|
||||
source: SourceConfig,
|
||||
profile: ProviderProfile,
|
||||
first_token_event: asyncio.Event | None,
|
||||
) -> TransportResult:
|
||||
"""非流式快路径(三项目均无,库新增): 单 JSON 响应,仅 total 超时。"""
|
||||
"""非流式快路径(三项目均无,库新增): 单 JSON 响应,仅 total 超时。
|
||||
|
||||
接收 `first_token_event` 但**永不置位**: 非流式无中途信号,事件自然退化
|
||||
为纯时间阈值(对冲只能靠 `hedge_after_s` 触发)。
|
||||
"""
|
||||
resp = await client.post(url, json=payload)
|
||||
if resp.status_code != 200:
|
||||
raise _status_to_error(
|
||||
|
||||
Reference in New Issue
Block a user