feat(api): warn on unrecognized stage codes (B4 gray-release monitoring)

LLM/output stage codes outside STATUS_CODE_MAP were passed through
silently with an empty nextStage - undetectable drift between the
prompt output code set and the endpoint mapping table.

- endpoints: _stage_label / _warn_unknown_stage_code helpers; both
  streaming and non-streaming paths emit a warning with sessionId,
  clientMode, raw and normalized codes when the label lookup misses
  (transparency behavior unchanged: code passes through, label empty)
- known codes and covered internal normalizations (e.g. 2020->0002)
  stay silent; the warning fires on the normalized value

Verified: 229 unit tests (4 new monitoring tests)
This commit is contained in:
Eric Wang
2026-10-22 23:19:11 +08:00
parent b8008da959
commit 11131885a6
2 changed files with 145 additions and 2 deletions
+107
View File
@@ -0,0 +1,107 @@
"""B4 监测:未识别 stage 码告警(灰度期确认提示词输出码集收敛)。
LLM/后端输出映射表(STATUS_CODE_MAP)外的阶段码时,当前行为是
原样透传 nextStageCode、nextStage 为空串——静默异常。
本测试固定:两条路径(非流式/流式)都会打 warning,已知码不打。
"""
import pytest
import src.api.endpoints as endpoints
from src.api.endpoints import chat
from src.backends.chat import ChatInput, ChatResult, TextDelta
from src.schemas.models import ProcessRequest_chat
class FakeBackend:
"""固定返回指定 state 前缀的 backend。"""
def __init__(self, content: str):
self._content = content
async def complete(self, chat_input: ChatInput):
return ChatResult(content=self._content, status_code=None, form_update={})
async def stream(self, chat_input: ChatInput):
yield TextDelta(self._content)
def make_request():
return ProcessRequest_chat(
sessionId="session-b4",
timeStamp="20261022120000",
text="测试输入",
)
class _WarningRecorder:
def __init__(self):
self.messages: list[str] = []
def warning(self, message: str) -> None:
self.messages.append(message)
def __getattr__(self, _name):
# info/debug/error 等:no-op(本测试只关心 warning)
return lambda *a, **k: None
@pytest.mark.asyncio
async def test_unknown_stage_code_warns_non_stream(monkeypatch):
recorder = _WarningRecorder()
monkeypatch.setattr(endpoints, "logger", recorder)
backend = FakeBackend("<state>9999</state>收到了")
response = await chat(make_request(), stream=False, backend=backend)
assert response.nextStageCode == "9999"
assert response.nextStage == ""
assert len(recorder.messages) == 1
assert "rawStageCode=9999" in recorder.messages[0]
assert "sessionId=session-b4" in recorder.messages[0]
@pytest.mark.asyncio
async def test_unknown_stage_code_warns_stream(monkeypatch):
recorder = _WarningRecorder()
monkeypatch.setattr(endpoints, "logger", recorder)
backend = FakeBackend("<state>9999</state>收到了")
response = await chat(make_request(), stream=True, backend=backend)
chunks = []
async for chunk in response.body_iterator:
chunks.append(chunk.decode() if isinstance(chunk, bytes) else chunk)
assert '"nextStageCode": "9999"' in "".join(chunks)
assert len(recorder.messages) == 1
assert "rawStageCode=9999" in recorder.messages[0]
@pytest.mark.asyncio
async def test_normalized_internal_code_warns_with_normalized_value(monkeypatch):
"""归一化映射表内的内部码(如 2020->0002 已是已知码)不告警;
映射表未覆盖的内部码按归一化后的值告警。"""
recorder = _WarningRecorder()
monkeypatch.setattr(endpoints, "logger", recorder)
known = FakeBackend("<state>2020</state>识别不了")
response = await chat(make_request(), stream=False, backend=known)
assert response.nextStageCode == "0002" # 归一化生效
assert recorder.messages == [] # 已知码不告警
unknown = FakeBackend("<state>3009</state>x")
response = await chat(make_request(), stream=False, backend=unknown)
assert response.nextStageCode == "3009"
assert len(recorder.messages) == 1
assert "normalizedStageCode=3009" in recorder.messages[0]
@pytest.mark.asyncio
async def test_known_stage_code_no_warning(monkeypatch):
recorder = _WarningRecorder()
monkeypatch.setattr(endpoints, "logger", recorder)
backend = FakeBackend("<state>1002</state>继续")
response = await chat(make_request(), stream=False, backend=backend)
assert response.nextStage == "通话中"
assert recorder.messages == []