feat: cap telemetry bodies at a configurable length

Chat rows stored full message and response text with no upper bound, so
downstream contracts and tenders lived in llm_calls indefinitely. Add
_cap_text/_cap_messages in the single telemetry exit (_record), applied
after digest_messages and before json.dumps, plus to response/thinking.

Capping is per text, not over the serialized JSON: cutting the whole
string would emit invalid JSON into an unvalidated TEXT column. The cap
builds new dicts and never mutates in place — digest_messages passes
non-list content straight through as the same object, so an in-place cut
would silently poison the caller's messages and the cache key.

text_cap is required on TelemetryEmitter (internal class, three known
construction sites) and defaults to None on the three public clients, so
the default behaviour stays byte-for-byte identical. Settings wiring
lands separately.
This commit is contained in:
2026-08-19 13:39:17 -04:00
parent e0a33ecf93
commit 33ed7ecdfc
9 changed files with 352 additions and 51 deletions
+5 -5
View File
@@ -169,7 +169,7 @@ def _source(model="qwen-max"):
class TestEmitterCost:
async def test_success_row_costed(self):
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec, pricing=_TABLE)
emitter = TelemetryEmitter(rec, pricing=_TABLE, text_cap=None)
await emitter.emit_attempt(
request=_REQ,
source=_source(),
@@ -182,13 +182,13 @@ class TestEmitterCost:
async def test_cache_hit_row_costs_zero(self):
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec, pricing=_TABLE)
emitter = TelemetryEmitter(rec, pricing=_TABLE, text_cap=None)
await emitter.emit_cache_hit(request=_REQ, response=_resp(cache_hit=True))
assert rec.rows[0]["cost"] == 0.0
async def test_failure_row_cost_none(self):
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec, pricing=_TABLE)
emitter = TelemetryEmitter(rec, pricing=_TABLE, text_cap=None)
await emitter.emit_attempt(
request=_REQ,
source=_source(),
@@ -201,7 +201,7 @@ class TestEmitterCost:
async def test_unknown_model_none_without_blocking(self):
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec, pricing=_TABLE)
emitter = TelemetryEmitter(rec, pricing=_TABLE, text_cap=None)
await emitter.emit_attempt(
request=_REQ,
source=_source(model="mystery"),
@@ -215,7 +215,7 @@ class TestEmitterCost:
async def test_no_pricing_keeps_none(self):
"""未注入价格表 = M1 现状: cost 恒 None(回归)。"""
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec)
emitter = TelemetryEmitter(rec, text_cap=None)
await emitter.emit_attempt(
request=_REQ,
source=_source(),