feat: carry caller dimensions through the OCR chain
OcrClient is the third telemetry path that skips the chat onion: _emit builds its own ChatRequest purely to reuse the shared TelemetryEmitter, so wiring chat() and embed() alone left every OCR row without a tenant while those rows land in the same llm_calls table. Take the dimensions at both public entries, validate them there (anything failing further down is degraded to a warning), and thread them through _call -> _attempt -> _emit so success, rejection, cancellation and retryable failure rows all carry the same pair.
This commit is contained in:
@@ -480,6 +480,59 @@ class TestTelemetry:
|
||||
assert (await limiter.source_stats("m1")).tpm_used == 0 # settle(0) 全额退回预扣
|
||||
|
||||
|
||||
class TestOcrCallerDimensions:
|
||||
"""issue #11: 调用方自定义维度必须沿 OCR 链四层透传到每一行遥测。
|
||||
|
||||
OCR 行与 chat 行落在同一张 `llm_calls` 表: 不覆盖这条链会让同一张表里
|
||||
一部分行有租户归属、一部分永远空白,而"先启用后加列则归属无法还原"。
|
||||
"""
|
||||
|
||||
async def test_recognize_text_row_carries_dimensions(self):
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client([_src()], ["text"], telemetry=recorder)
|
||||
await client.recognize_text(b"jpg", tenant_id="t1", meta={"batch": "b-42"})
|
||||
assert recorder.rows[0]["tenant_id"] == "t1"
|
||||
assert recorder.rows[0]["meta"] == '{"batch": "b-42"}'
|
||||
|
||||
async def test_parse_layout_row_carries_dimensions(self):
|
||||
"""两个公共方法都是入口: 只测一个会漏掉另一个的透传缺口。"""
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client([_src()], ["layout"], telemetry=recorder)
|
||||
await client.parse_layout(b"jpg", tenant_id="t2", meta={"batch": "b-43"})
|
||||
assert recorder.rows[0]["tenant_id"] == "t2"
|
||||
assert recorder.rows[0]["meta"] == '{"batch": "b-43"}'
|
||||
|
||||
async def test_failed_attempt_row_also_carries_dimensions(self):
|
||||
"""失败行同样需要归属: 某租户的请求没被服务,正是审计最需要的一行。"""
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client(
|
||||
[_src()],
|
||||
[TransientError("boom", status_code=500), "text"],
|
||||
telemetry=recorder,
|
||||
)
|
||||
await client.recognize_text(b"jpg", tenant_id="t1", meta={"batch": "b-42"})
|
||||
assert len(recorder.rows) == 2 # 失败尝试 + 成功尝试
|
||||
assert [r["tenant_id"] for r in recorder.rows] == ["t1", "t1"]
|
||||
assert [r["meta"] for r in recorder.rows] == ['{"batch": "b-42"}'] * 2
|
||||
|
||||
@pytest.mark.parametrize("method", ["recognize_text", "parse_layout"])
|
||||
async def test_invalid_meta_rejected_before_any_telemetry(self, method):
|
||||
"""校验必须早于遥测: 链路内的失败都被降级成 warning,放下游等于没有校验。"""
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client([_src()], ["text"], telemetry=recorder)
|
||||
with pytest.raises(ValueError, match="meta"):
|
||||
await getattr(client, method)(b"jpg", meta={"Bad Key": 1})
|
||||
assert recorder.rows == []
|
||||
assert client._transport.calls == [] # 连调用都没发出
|
||||
|
||||
async def test_defaults_land_as_sentinels(self):
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client([_src()], ["text"], telemetry=recorder)
|
||||
await client.recognize_text(b"jpg")
|
||||
assert recorder.rows[0]["tenant_id"] == "" # 空串哨兵,不是 None
|
||||
assert recorder.rows[0]["meta"] == "{}"
|
||||
|
||||
|
||||
class TestAssembly:
|
||||
_ENV = {
|
||||
"OCR__MONKEY__1__BASE_URL": "http://10.77.0.20:7866",
|
||||
|
||||
Reference in New Issue
Block a user