test(llm): 锁定 llm_provider_failed 失败日志 request_id 条件透传(CR-H4 收尾)
This commit is contained in:
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import structlog
|
||||||
from fakes_resilience import (
|
from fakes_resilience import (
|
||||||
AuthError,
|
AuthError,
|
||||||
FakeLedger,
|
FakeLedger,
|
||||||
@@ -18,9 +21,22 @@ from fakes_resilience import (
|
|||||||
transient,
|
transient,
|
||||||
)
|
)
|
||||||
from ww_llm_gateway.gateway import CircuitBreaker, Gateway
|
from ww_llm_gateway.gateway import CircuitBreaker, Gateway
|
||||||
from ww_llm_gateway.types import LlmRequest
|
from ww_llm_gateway.types import LlmRequest, Scope
|
||||||
from ww_shared import AppError, ErrorCode
|
from ww_shared import AppError, ErrorCode
|
||||||
|
|
||||||
|
_SCOPE = Scope(user_id=uuid.UUID(int=1))
|
||||||
|
|
||||||
|
|
||||||
|
def _failing_gw() -> Gateway:
|
||||||
|
"""单 provider 链、每次 transient 失败:耗尽重试后记 `llm_provider_failed` 再抛。"""
|
||||||
|
primary = ScriptedAdapter("deepseek", failures=[transient() for _ in range(10)])
|
||||||
|
return Gateway(
|
||||||
|
{"deepseek": primary},
|
||||||
|
FakeLedger(),
|
||||||
|
chain_resolver=chain_resolver(chain(("deepseek", "deepseek-chat"))),
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_primary_success_no_fallback(req: LlmRequest) -> None:
|
async def test_primary_success_no_fallback(req: LlmRequest) -> None:
|
||||||
primary = ScriptedAdapter("deepseek", text="主模型")
|
primary = ScriptedAdapter("deepseek", text="主模型")
|
||||||
@@ -238,3 +254,41 @@ async def test_open_circuit_skips_provider(req: LlmRequest) -> None:
|
|||||||
assert resp.served_by.provider == "openai"
|
assert resp.served_by.provider == "openai"
|
||||||
assert resp.served_by.fell_back is True
|
assert resp.served_by.fell_back is True
|
||||||
assert primary.complete_calls == 0
|
assert primary.complete_calls == 0
|
||||||
|
|
||||||
|
|
||||||
|
# CR-H4 收尾:`llm_provider_failed`(失败/回退日志)也须条件透传 request_id——
|
||||||
|
# 设了则带上、未设则不发键(否则 None 覆盖 merge_contextvars 供的 id,回退 sync/SSE 追踪)。
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_failure_log_carries_request_id_conditionally() -> None:
|
||||||
|
with structlog.testing.capture_logs() as logs:
|
||||||
|
with pytest.raises(AppError):
|
||||||
|
await _failing_gw().run(
|
||||||
|
LlmRequest(tier="writer", input="x", scope=_SCOPE, request_id="rid-run")
|
||||||
|
)
|
||||||
|
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
|
||||||
|
assert failed["request_id"] == "rid-run"
|
||||||
|
|
||||||
|
with structlog.testing.capture_logs() as logs:
|
||||||
|
with pytest.raises(AppError):
|
||||||
|
await _failing_gw().run(LlmRequest(tier="writer", input="x", scope=_SCOPE))
|
||||||
|
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
|
||||||
|
assert "request_id" not in failed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_stream_failure_log_carries_request_id_conditionally() -> None:
|
||||||
|
with structlog.testing.capture_logs() as logs:
|
||||||
|
with pytest.raises(AppError):
|
||||||
|
async for _ in _failing_gw().stream(
|
||||||
|
LlmRequest(tier="writer", input="x", scope=_SCOPE, request_id="rid-stream")
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
|
||||||
|
assert failed["request_id"] == "rid-stream"
|
||||||
|
|
||||||
|
with structlog.testing.capture_logs() as logs:
|
||||||
|
with pytest.raises(AppError):
|
||||||
|
async for _ in _failing_gw().stream(LlmRequest(tier="writer", input="x", scope=_SCOPE)):
|
||||||
|
pass
|
||||||
|
failed = next(e for e in logs if e["event"] == "llm_provider_failed")
|
||||||
|
assert "request_id" not in failed
|
||||||
|
|||||||
Reference in New Issue
Block a user