diff --git a/src/polygateway/middleware/retry.py b/src/polygateway/middleware/retry.py index bb9e648..acb7da7 100644 --- a/src/polygateway/middleware/retry.py +++ b/src/polygateway/middleware/retry.py @@ -436,6 +436,8 @@ class RetryMW: source_name=source.name, cost=None, usage_source=result.usage_source, + cached_prompt_tokens=result.cached_prompt_tokens, + model_reported=result.model_reported, ) async def _settle_and_release(self, permit: Permit, actual: int) -> None: diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index db64ae0..59d7c8a 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -150,6 +150,49 @@ class TestCacheFlow: assert terminal.calls == 2 +class TestObservabilityFieldsOnHit: + """issue #3 决策 B1: 命中行原样回放,与 model/prompt_tokens 同一口径。""" + + async def test_fields_replayed_on_hit(self): + backend = InMemoryCache() + mw = _mw(backend) + terminal = _Terminal( + _resp(cached_prompt_tokens=64, model_reported="MiniMax-Text-01-250321") + ) + await mw(ChatRequest(messages=_MSGS), terminal) + hit = await mw(ChatRequest(messages=_MSGS), terminal) + assert hit.cache_hit is True + assert hit.cached_prompt_tokens == 64 + assert hit.model_reported == "MiniMax-Text-01-250321" + + async def test_legacy_cache_entry_without_new_keys_rehydrates(self): + """旧格式条目(无这两个键)必须照常重建为 None,不得抛异常回源。""" + backend = InMemoryCache() + mw = _mw(backend) + key = build_cache_key("m", _MSGS, "proj", None) + legacy = { + "content": "legacy", + "thinking": "", + "model": "m", + "provider": "p", + "prompt_tokens": 1, + "completion_tokens": 2, + "latency_ms": 30, + "ttft_ms": 5.0, + "max_inter_token_ms": 2.0, + "cache_hit": False, + "call_id": "orig", + "source_name": "s1", + "cost": None, + "usage_source": "measured", + } + await backend.set(key, json.dumps(legacy), ttl_s=100) + terminal = _Terminal(_resp()) + hit = await mw(ChatRequest(messages=_MSGS), terminal) + assert hit.content == "legacy" and terminal.calls == 0 # 真的走了缓存 + assert hit.cached_prompt_tokens is None and hit.model_reported is None + + class _BrokenBackend: async def get(self, key): raise ConnectionError("redis down") diff --git a/tests/unit/test_retry.py b/tests/unit/test_retry.py index 01b1278..bb54ae9 100644 --- a/tests/unit/test_retry.py +++ b/tests/unit/test_retry.py @@ -194,6 +194,35 @@ class TestSuccessPath: assert (await limiter.source_stats("a")).tpm_used == 16 +class TestObservabilityPassthrough: + """issue #3: transport 采到的两个可观测字段必须原样上浮到 LLMResponse。""" + + async def test_fields_reach_the_response(self): + result = TransportResult( + content="ok", + thinking="", + prompt_tokens=10, + completion_tokens=5, + usage_source="measured", + ttft_ms=12.0, + max_inter_token_ms=3.0, + raw={}, + cached_prompt_tokens=64, + model_reported="MiniMax-Text-01-250321", + ) + mw, *_ = _harness([_src("a")], [result]) + resp = await mw(_REQ) + assert resp.cached_prompt_tokens == 64 + assert resp.model_reported == "MiniMax-Text-01-250321" + # model 仍是配置别名: 真实版本是旁证,不顶替溯源主字段 + assert resp.model == "m" + + async def test_absent_fields_stay_none(self): + mw, *_ = _harness([_src("a")], [_ok()]) + resp = await mw(_REQ) + assert resp.cached_prompt_tokens is None and resp.model_reported is None + + class TestRetryAndFailover: async def test_transient_switches_source_then_succeeds(self): mw, _, _, transport, sleep, _ = _harness(