feat: collect provider cache tokens and reported model in transport
This commit is contained in:
@@ -36,7 +36,7 @@ def _source(**overrides):
|
||||
return SourceConfig(**base)
|
||||
|
||||
|
||||
def _chunk(content=None, reasoning=None, usage=None):
|
||||
def _chunk(content=None, reasoning=None, usage=None, model=None):
|
||||
delta = {}
|
||||
if content is not None:
|
||||
delta["content"] = content
|
||||
@@ -45,6 +45,8 @@ def _chunk(content=None, reasoning=None, usage=None):
|
||||
body = {"choices": [{"delta": delta}]} if (delta or usage is None) else {"choices": []}
|
||||
if usage is not None:
|
||||
body["usage"] = usage
|
||||
if model is not None:
|
||||
body["model"] = model
|
||||
return f"data: {json.dumps(body)}\n\n"
|
||||
|
||||
|
||||
@@ -261,6 +263,124 @@ class TestEmptyCompletion:
|
||||
await _complete(_transport_for(handler), _source())
|
||||
|
||||
|
||||
class TestObservabilityFields:
|
||||
"""issue #3: 供应商 prompt cache 命中数与 API 实际返回的模型版本串。
|
||||
|
||||
网关报文一律不可信: 形态异常只归 None,绝不因一个可观测字段打断调用。
|
||||
"""
|
||||
|
||||
def _cached_usage(self, cached):
|
||||
return {**_USAGE, "prompt_tokens_details": {"cached_tokens": cached}}
|
||||
|
||||
async def test_stream_reads_cached_tokens(self):
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._cached_usage(128)))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens == 128
|
||||
|
||||
async def test_non_stream_reads_cached_tokens(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{"message": {"content": "42"}}],
|
||||
"usage": self._cached_usage(128),
|
||||
},
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.cached_prompt_tokens == 128
|
||||
|
||||
async def test_zero_cached_tokens_is_a_real_zero(self):
|
||||
"""0(真实零命中)与 None(该源未上报)必须可区分——issue #3 的核心诉求。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._cached_usage(0)))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens == 0
|
||||
|
||||
async def test_usage_without_details_is_none(self):
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens is None
|
||||
|
||||
async def test_missing_usage_frame_is_none(self):
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens is None
|
||||
|
||||
@pytest.mark.parametrize("bad", ["abc", -1, True, 1.5, None, [], {"x": 1}])
|
||||
async def test_malformed_cached_tokens_degrade_to_none(self, bad):
|
||||
"""`True` 必须排除: Python 里 isinstance(True, int) 为真。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._cached_usage(bad)))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens is None
|
||||
|
||||
async def test_details_not_a_dict_is_none(self):
|
||||
def handler(request):
|
||||
usage = {**_USAGE, "prompt_tokens_details": "oops"}
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=usage))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.cached_prompt_tokens is None
|
||||
|
||||
async def test_non_stream_reads_reported_model(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{"message": {"content": "42"}}],
|
||||
"usage": _USAGE,
|
||||
"model": "MiniMax-Text-01-250321",
|
||||
},
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.model_reported == "MiniMax-Text-01-250321"
|
||||
|
||||
async def test_stream_keeps_the_first_reported_model(self):
|
||||
"""末帧异常值不得覆盖首帧: 首次写入即固定。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(
|
||||
_chunk(content="a", model="MiniMax-Text-01-250321"),
|
||||
_chunk(content="b", model="something-else"),
|
||||
_chunk(usage=_USAGE),
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.model_reported == "MiniMax-Text-01-250321"
|
||||
|
||||
@pytest.mark.parametrize("bad", [None, "", " ", 123, {}])
|
||||
async def test_missing_or_malformed_model_is_none(self, bad):
|
||||
def handler(request):
|
||||
body = {"choices": [{"message": {"content": "42"}}], "usage": _USAGE}
|
||||
if bad is not None:
|
||||
body["model"] = bad
|
||||
return httpx.Response(200, json=body)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.model_reported is None
|
||||
|
||||
async def test_raw_payload_is_unchanged(self):
|
||||
"""新字段是独立格子,不改动 raw 的既有内容。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._cached_usage(5)))
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert set(result.raw) == {"usage"}
|
||||
|
||||
|
||||
class TestNonStreamFastPath:
|
||||
async def test_non_stream_parses_message(self):
|
||||
def handler(request):
|
||||
|
||||
Reference in New Issue
Block a user