feat: let chat() take a tenant and caller-defined dimensions

Validation runs before the request enters the onion: every failure inside it
is downgraded to a warning by the telemetry layer, so validating in there
would not validate anything.

The dimensions stay out of the cache key — cache_namespace already carries
tenant isolation, and folding meta in would cold-start every existing entry.
This commit is contained in:
2026-08-17 09:43:51 -04:00
parent dba706b59c
commit 4be2b4f287
2 changed files with 76 additions and 1 deletions
+56
View File
@@ -184,6 +184,62 @@ class TestSamplingOverlay:
assert [c["seed"] for c in captured] == [1, 2]
class _MemoryRecorder:
"""收下遥测行原样存起来;断言"哪些行被写了"必须能看到零行的情形。"""
def __init__(self):
self.rows = []
async def record_llm_call(self, **fields):
self.rows.append(fields)
class TestCallerDimensions:
"""调用方自定义维度进遥测(issue #11 Task 4)。"""
_MSG = [{"role": "user", "content": "hi"}]
async def test_dimensions_reach_telemetry_row(self):
recorder = _MemoryRecorder()
async with _client(telemetry=recorder) as client:
await client.chat(self._MSG, tenant_id="t1", meta={"batch": "b-42"})
row = recorder.rows[-1]
assert row["tenant_id"] == "t1"
assert json.loads(row["meta"]) == {"batch": "b-42"}
async def test_default_path_writes_sentinels(self):
"""不传两参数时落哨兵值而非 NULL(§4.4: NULL 在 RLS 下是永久不可见的黑洞)。"""
recorder = _MemoryRecorder()
async with _client(telemetry=recorder) as client:
await client.chat(self._MSG)
row = recorder.rows[-1]
assert row["tenant_id"] == "" and row["meta"] == "{}"
async def test_invalid_meta_key_rejected_before_any_telemetry(self):
"""校验早于遥测(§4.2 核心承诺): 放进洋葱就会被降级成 warning 而调用照常发出。"""
recorder = _MemoryRecorder()
async with _client(telemetry=recorder) as client:
with pytest.raises(ValueError, match="meta"):
await client.chat(self._MSG, meta={"BAD-KEY": 1})
assert recorder.rows == []
async def test_non_finite_float_rejected_before_any_telemetry(self):
"""nan 产出的是 PG 拒收的非法 JSON;放行等于把调用方 bug 变成静默丢遥测(§6)。"""
recorder = _MemoryRecorder()
async with _client(telemetry=recorder) as client:
with pytest.raises(ValueError, match="nan"):
await client.chat(self._MSG, meta={"k": float("nan")})
assert recorder.rows == []
async def test_meta_does_not_enter_cache_key(self):
"""仅 meta 不同必须仍命中缓存(F1): 进 key 会让存量缓存全量冷启动且不报错。"""
client = _client(cache=InMemoryCache(), cache_namespace="proj", cache_ttl_s=3600)
async with client:
first = await client.chat(self._MSG, meta={"batch": "b-1"})
second = await client.chat(self._MSG, meta={"batch": "b-2"})
assert first.cache_hit is False and second.cache_hit is True
class TestModelFingerprint:
"""配置级采样参数须进缓存身份,否则改 temperature 后仍读旧缓存(决策 C)。"""