feat: carry the reasoning verdict through to LLMResponse
Both assembly paths fill it, streaming and non-streaming alike. Filling only one is exactly the divergence this issue exposed: M3 returns reasoning prose over SSE and nothing at all over the plain endpoint, so a verdict computed on one path says nothing about the other. The field defaults to UNKNOWN on both TransportResult and LLMResponse. A transport that does not judge should not get to declare absence on the provider's behalf, and a default that stays silent is the only one that cannot lie.
This commit is contained in:
@@ -22,7 +22,7 @@ from polygateway.transports.openai_compat import (
|
||||
_iter_sse_deltas,
|
||||
_sse_data_payload,
|
||||
)
|
||||
from polygateway.types import ChatRequest, LLMResponse, SourceConfig
|
||||
from polygateway.types import ChatRequest, LLMResponse, SourceConfig, ThinkingObservation
|
||||
|
||||
|
||||
def _source(**overrides):
|
||||
@@ -471,6 +471,81 @@ class TestReasoningTokens:
|
||||
assert result.reasoning_tokens is None
|
||||
|
||||
|
||||
class TestThinkingObservationVerdict:
|
||||
"""issue #16/#17: 两条组装路径都必须裁定"推理到底发生没发生"。
|
||||
|
||||
流式与非流式各测一遍是刻意的——只填一条路径正是本 issue 的根因形态:
|
||||
库在其中一条路径上悄悄给出了不同的可观测性,下游无从分辨。
|
||||
"""
|
||||
|
||||
def _reasoning_usage(self, reasoning):
|
||||
return {**_USAGE, "completion_tokens_details": {"reasoning_tokens": reasoning}}
|
||||
|
||||
async def test_stream_reasoning_content_is_observed(self):
|
||||
def handler(request):
|
||||
return _sse_stream(
|
||||
_chunk(reasoning="想一下"), _chunk(content="ok"), _chunk(usage=_USAGE)
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.thinking_observation is ThinkingObservation.OBSERVED
|
||||
|
||||
async def test_stream_without_any_signal_is_unknown(self):
|
||||
"""无正文、无 details: 库不知道,就如实说不知道。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.thinking_observation is ThinkingObservation.UNKNOWN
|
||||
|
||||
async def test_stream_zero_reasoning_tokens_is_absent(self):
|
||||
"""上游明确上报 0 才算 ABSENT——这是唯一的"确实没推理"证据。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._reasoning_usage(0)))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.thinking_observation is ThinkingObservation.ABSENT
|
||||
|
||||
async def test_non_stream_reasoning_content_is_observed(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{"message": {"content": "42", "reasoning_content": "想一下"}}],
|
||||
"usage": _USAGE,
|
||||
},
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.thinking_observation is ThinkingObservation.OBSERVED
|
||||
|
||||
async def test_non_stream_without_any_signal_is_unknown(self):
|
||||
"""M3 非流式实测形态: 推理已计费却既不回传正文也不回传 details。"""
|
||||
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200, json={"choices": [{"message": {"content": "42"}}], "usage": _USAGE}
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.thinking_observation is ThinkingObservation.UNKNOWN
|
||||
|
||||
async def test_non_stream_zero_reasoning_tokens_is_absent(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{"message": {"content": "42"}}],
|
||||
"usage": self._reasoning_usage(0),
|
||||
},
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.thinking_observation is ThinkingObservation.ABSENT
|
||||
|
||||
|
||||
class TestNonStreamFastPath:
|
||||
async def test_non_stream_parses_message(self):
|
||||
def handler(request):
|
||||
|
||||
Reference in New Issue
Block a user