feat: record which tier a call actually ran at

Twenty-five columns and not one of them answered "which tier was this?",
so the question the whole issue exists to settle - does a higher tier buy
anything - had no way to group its data.

The three emit entry points deliberately disagree, the way sampling
already does. A successful attempt records what the transport actually
sent: with EFFORT_FALLBACK=nearest a request for medium goes out as low,
and recomputing here would file the row under a tier that never left the
process. A failed attempt has no response to read, so it falls back to
the requested tier - which is exactly right for the tier errors that are
rejected before any HTTP happens, because the rejected tier is the
signal. Cache hits and terminal failures have no chosen source at all,
so a source-level tier is not a thing they could report.

emit_attempt now demands to be told whether the path reasons at all.
Embedding and OCR share the emitter but never send reasoning parameters;
without the flag a source that mistakenly carries ENABLE_THINKING would
hang a tier on a call that could not possibly have run at one.

The value lands as a plain str. StrEnum is a str subclass and asyncpg
promises nothing about encoding subclasses, and a telemetry write that
fails is only a warning - Postgres would just quietly lose the column.
NULL means nobody declared a tier, which is not the same statement as
'none', and the two must never be folded together.
This commit is contained in:
2026-09-05 05:57:29 -04:00
parent 9832dcee63
commit e06cd8e8b7
17 changed files with 431 additions and 42 deletions
+1
View File
@@ -584,6 +584,7 @@ class TestTelemetryCapDoesNotPoisonTheCacheKey:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
# 截断确实发生了(否则本用例恒真)
logged = json.loads(rec.rows[0]["messages"])
+1
View File
@@ -123,6 +123,7 @@ async def _recorded_cost(result, source):
latency_ms=1,
response=response,
error=None,
reasoning_applies=True,
)
return recorder.rows[0]["cost"]
+3 -1
View File
@@ -275,7 +275,9 @@ class TestTelemetryRecorderSignature:
params = inspect.signature(TelemetryRecorder.record_llm_call).parameters
assert {"tenant_id", "meta"} <= set(params)
@pytest.mark.parametrize("name", ["tenant_id", "meta", "thinking_observation"])
@pytest.mark.parametrize(
"name", ["tenant_id", "meta", "thinking_observation", "reasoning_effort"]
)
def test_caller_dimensions_have_no_default(self, name):
import inspect
+4
View File
@@ -177,6 +177,7 @@ class TestEmitterCost:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] == pytest.approx(7.2)
@@ -196,6 +197,7 @@ class TestEmitterCost:
latency_ms=1,
response=None,
error="TransientError: boom",
reasoning_applies=True,
)
assert rec.rows[0]["cost"] is None
@@ -209,6 +211,7 @@ class TestEmitterCost:
latency_ms=1,
response=_resp(model="mystery"),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] is None
@@ -223,5 +226,6 @@ class TestEmitterCost:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] is None
+244 -15
View File
@@ -25,6 +25,7 @@ from polygateway.types import (
BackpressurePolicy,
BreakerConfig,
ChatRequest,
Effort,
EmbeddingTransportResult,
GlobalLimits,
LLMResponse,
@@ -63,6 +64,7 @@ _EXPECTED_COLUMNS = [
"tenant_id",
"meta",
"thinking_observation",
"reasoning_effort",
]
@@ -132,6 +134,8 @@ async def _record_minimal(recorder, call_id="c1", **overrides):
"meta": "{}",
# 同样已由 emitter 归一化: 枚举取 .value 后才下沉,recorder 只见裸 str
"thinking_observation": "unknown",
# 同理: `Effort` 归一成裸 str,不表态则是 None(与 'low' 必须分得开)
"reasoning_effort": None,
}
fields.update(overrides)
await recorder.record_llm_call(**fields)
@@ -177,16 +181,17 @@ _FROZEN_SQLITE_INSERT = (
"INSERT OR IGNORE INTO llm_calls (call_id, parent_call_id, session_id, model, provider, "
"source_name, messages, response, thinking, prompt_tokens, completion_tokens, usage_source, "
"latency_ms, ttft_ms, max_inter_token_ms, cache_hit, error, cost, cached_prompt_tokens, "
"model_reported, sampling, reasoning_tokens, tenant_id, meta, thinking_observation) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"model_reported, sampling, reasoning_tokens, tenant_id, meta, thinking_observation, "
"reasoning_effort) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
_FROZEN_PG_INSERT = (
"INSERT INTO llm_calls (call_id, parent_call_id, session_id, model, provider, source_name, "
"messages, response, thinking, prompt_tokens, completion_tokens, usage_source, latency_ms, "
"ttft_ms, max_inter_token_ms, cache_hit, error, cost, cached_prompt_tokens, model_reported, "
"sampling, reasoning_tokens, tenant_id, meta, thinking_observation) "
"sampling, reasoning_tokens, tenant_id, meta, thinking_observation, reasoning_effort) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, "
"$19, $20, $21, $22, $23, $24, $25) "
"$19, $20, $21, $22, $23, $24, $25, $26) "
# 无冲突目标(issue #13 Task 2): 带 `(call_id)` 的版本在按 created_at 分区、
# 主键为 (call_id, created_at) 的表上匹配不到约束,PG 直接拒收整条写入
"ON CONFLICT DO NOTHING"
@@ -212,7 +217,7 @@ class TestSchemaModule:
# COLUMNS 是 INSERT 字段序,不含数据库自填的 created_at
assert list(COLUMNS) == [c for c in _EXPECTED_COLUMNS if c != "created_at"]
assert len(COLUMNS) == 25
assert len(COLUMNS) == 26
# 两端 DDL 的列出现顺序 == 物理列序(created_at 在第 19 位)
for ddl in (SQLITE_DDL, PG_DDL):
assert _first_occurrence_order(ddl, _EXPECTED_COLUMNS) == _EXPECTED_COLUMNS
@@ -226,8 +231,8 @@ class TestSchemaModule:
"ALTER TABLE llm_calls ADD COLUMN cached_prompt_tokens INTEGER",
)
assert PG_BACKFILL[-1] == (
"thinking_observation",
"ALTER TABLE llm_calls ADD COLUMN thinking_observation TEXT",
"reasoning_effort",
"ALTER TABLE llm_calls ADD COLUMN reasoning_effort TEXT",
)
assert all("IF NOT EXISTS" not in stmt for _, stmt in PG_BACKFILL)
@@ -275,7 +280,7 @@ class TestSchemaModule:
pg = telemetry_schema_sql("postgres")
lite = telemetry_schema_sql("sqlite")
for script in (pg, lite):
# 25 个 INSERT 字段 + created_at 全在,且首次出现顺序与建表 DDL 一致
# 26 个 INSERT 字段 + created_at 全在,且首次出现顺序与建表 DDL 一致
assert _first_occurrence_order(script, _EXPECTED_COLUMNS) == _EXPECTED_COLUMNS
assert "CREATE TABLE IF NOT EXISTS llm_calls" in script
# 人执行的那份必须幂等: PG 用 ADD COLUMN IF NOT EXISTS(与库内那份有意不同)
@@ -313,7 +318,7 @@ class TestBackendColumnParity:
"""新列只能追加在末尾: 旧表经 ALTER 补列必落末尾,插在中间会让两条路径分叉。"""
from polygateway.telemetry.schema import COLUMNS
assert COLUMNS[-3:] == ("tenant_id", "meta", "thinking_observation")
assert COLUMNS[-4:] == ("tenant_id", "meta", "thinking_observation", "reasoning_effort")
class TestSQLiteRecorder:
@@ -405,6 +410,27 @@ class TestSQLiteRecorder:
assert rows["t-absent"] == "absent" # 观测到"确实没推理",与"看不出来"不是一回事
assert rows["t-unknown"] == "unknown"
async def test_reasoning_effort_column_round_trips(self, tmp_path):
"""issue #20: 实际档位落库,事后才分得清"这一行跑在哪档"
断言裸串而非枚举,理由与 `thinking_observation` 逐字相同: `StrEnum` 是
`str` 子类,而 asyncpg 对子类编码不保证接受,遥测写失败只降级一条 warning
——PG 那一路会静默少一列,SQLite 本地全绿也发现不了。
"""
recorder = SQLiteRecorder(tmp_path / "t.db", auto_migrate=True)
await _record_minimal(recorder, call_id="e-low", reasoning_effort="low")
await _record_minimal(recorder, call_id="e-max", reasoning_effort="max")
await _record_minimal(recorder, call_id="e-silent")
recorder.close()
rows = dict(
sqlite3.connect(tmp_path / "t.db")
.execute("SELECT call_id, reasoning_effort FROM llm_calls")
.fetchall()
)
assert rows["e-low"] == "low"
assert rows["e-max"] == "max"
assert rows["e-silent"] is None # 不表态是 NULL,与任何一档都分得开
async def test_sampling_column_round_trips(self, tmp_path):
"""issue #4: 采样参数落库,否则事后无法证明某批数据跑在什么温度下。"""
recorder = SQLiteRecorder(tmp_path / "t.db", auto_migrate=True)
@@ -568,7 +594,7 @@ class TestSQLiteCallerDimensionsAcceptance:
conn = sqlite3.connect(db)
cols = [r[1] for r in conn.execute("PRAGMA table_info(llm_calls)")]
assert cols == _EXPECTED_COLUMNS # 22 → 25 个 recorder 字段(+ created_at 共 26 物理列)
assert cols == _EXPECTED_COLUMNS # 22 → 26 个 recorder 字段(+ created_at 共 27 物理列)
rows = dict(conn.execute("SELECT call_id, tenant_id FROM llm_calls").fetchall())
assert rows["new-row"] == "tenant-a"
assert rows["old-row"] == "" # 不是 None: NULL 会被 RLS 静默吞掉
@@ -610,7 +636,7 @@ class TestSQLiteCallerDimensionsAcceptance:
stale = sqlite3.connect(db)
assert [r[1] for r in stale.execute("PRAGMA table_info(llm_calls)")] == (
_EXPECTED_COLUMNS[:-3]
_EXPECTED_COLUMNS[:-4]
) # 补列确实没成功,用例不是在只读库上空转
@@ -618,7 +644,7 @@ class TestSQLiteSchemaMode:
"""issue #13: `auto_migrate` 两档——auto 保持自动补列,manual 只裁剪写入不发 DDL。
列数断言一律按**物理列数**写: 旧表 22 个 INSERT 字段 + `created_at` = 23,
补齐后 25 + `created_at` = 26。混用 INSERT 字段数与物理列数是本处最易错的地方。
补齐后 26 + `created_at` = 27。混用 INSERT 字段数与物理列数是本处最易错的地方。
"""
def _physical_columns(self, db: Path) -> list[str]:
@@ -657,7 +683,7 @@ class TestSQLiteSchemaMode:
assert "ALTER TABLE" in message # 给出可直接执行的补列 SQL
async def test_auto_mode_still_upgrades_the_legacy_table(self, tmp_path):
"""auto + 同款旧表: 现状回归,补列后物理列数 23 → 26"""
"""auto + 同款旧表: 现状回归,补列后物理列数 23 → 27"""
db = tmp_path / "auto_legacy.db"
_make_pre_tenant_db(db)
@@ -666,10 +692,10 @@ class TestSQLiteSchemaMode:
recorder.close()
assert self._physical_columns(db) == _EXPECTED_COLUMNS
assert len(self._physical_columns(db)) == 26
assert len(self._physical_columns(db)) == 27
async def test_manual_mode_still_creates_a_fresh_table(self, tmp_path):
"""manual 只管 ALTER,不管 CREATE: 全新库照建,26 个物理列齐全(设计 §4.2)。"""
"""manual 只管 ALTER,不管 CREATE: 全新库照建,27 个物理列齐全(设计 §4.2)。"""
db = tmp_path / "manual_fresh.db"
recorder = SQLiteRecorder(db, auto_migrate=False)
await _record_minimal(recorder, call_id="c-fresh", tenant_id="tenant-a")
@@ -894,6 +920,7 @@ class TestPostgresBackfillDiscipline:
"tenant_id",
"meta",
"thinking_observation",
"reasoning_effort",
]
def _recorder(self, conn):
@@ -1120,6 +1147,7 @@ class TestEmitterRecorderContract:
latency_ms=42,
response=_resp(),
error=None,
reasoning_applies=True,
)
assert set(rec.rows[0]) == set(COLUMNS)
@@ -1137,6 +1165,7 @@ class TestEmitterRecorderContract:
latency_ms=1,
response=None,
error="boom",
reasoning_applies=True,
)
elif emit == "cache_hit":
await emitter.emit_cache_hit(request=_REQ, response=_resp())
@@ -1165,6 +1194,7 @@ class TestEmitterThinkingObservation:
latency_ms=1,
response=_resp(thinking_observation=ThinkingObservation.OBSERVED),
error=None,
reasoning_applies=True,
)
value = rec.rows[0]["thinking_observation"]
assert value == "observed"
@@ -1187,6 +1217,7 @@ class TestEmitterThinkingObservation:
latency_ms=1,
response=_resp(thinking_observation="observed"),
error=None,
reasoning_applies=True,
)
assert len(rec.rows) == 1, "整行被吞了"
value = rec.rows[0]["thinking_observation"]
@@ -1212,6 +1243,7 @@ class TestEmitterThinkingObservation:
latency_ms=1,
response=_resp(thinking_observation="OBSERVED"), # 大小写不符即域外
error=None,
reasoning_applies=True,
)
finally:
logger.remove(sink_id)
@@ -1250,10 +1282,190 @@ class TestEmitterThinkingObservation:
latency_ms=1,
response=None,
error="boom",
reasoning_applies=True,
)
assert rec.rows[0]["thinking_observation"] == "unknown"
class TestEmitterReasoningEffort:
"""issue #20: 每行记下这次调用**实际跑在哪档**,否则压测无从分组。
三个入口的取值口径**有意不同**,故逐个钉死: 只有 `emit_attempt` 手上有生效源,
它才谈得上"实际档";另两个入口没有选中源,源级档位无从谈起,只能记请求档。
与 `sampling` 列的现有做法同构。
"""
async def test_attempt_records_the_tier_the_transport_applied(self):
"""`nearest` 映射后成功行记的是**映射后**的档,不是请求档。
请求 `medium`、模型只有 low/high/max 时二者分叉(实发 `low`)。emitter 若
"顺手"重算 `effective_effort`,记的就是一个从未发出过的档,而两个值在没开
映射的源上恒等——本地跑不开映射的源永远看不出这个错。
"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=ChatRequest(
messages=[{"role": "user", "content": "hi"}], reasoning_effort=Effort.MEDIUM
),
source=_source(effort_fallback="nearest"),
call_id="c",
latency_ms=1,
response=_resp(applied_effort=Effort.LOW),
error=None,
reasoning_applies=True,
)
value = rec.rows[0]["reasoning_effort"]
assert value == "low" # 不是 medium: 那一档从未发出去过
assert type(value) is str # 不是 Effort: 子类实例不得下沉到 recorder
async def test_failed_attempt_falls_back_to_the_requested_tier(self):
"""失败尝试没有响应,实际档不可知,记请求档并接受这层含义差别。
档位错误(resolve 的 Phase 2/4/5)根本没发 HTTP,却照样经
`RequestRejectedError` 走到这里——记的正是**被拒绝的那一档**,这对
"哪一档配错了" 是有用信号,不该被过滤掉。
"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=_REQ,
source=_source(reasoning_effort=Effort.HIGH),
call_id="c",
latency_ms=1,
response=None,
error="boom",
reasoning_applies=True,
)
assert rec.rows[0]["reasoning_effort"] == "high"
async def test_failed_attempt_resolves_the_syntactic_sugar_too(self):
"""回落走 `effective_effort` 而非裸读字段: `enable_thinking` 也是表态。"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=_REQ,
source=_source(enable_thinking=True),
call_id="c",
latency_ms=1,
response=None,
error="boom",
reasoning_applies=True,
)
assert rec.rows[0]["reasoning_effort"] == "auto"
async def test_cache_hit_records_the_request_tier_not_the_replayed_one(self):
"""命中行没有选中源,故记请求档;与 model/prompt_tokens 的回放口径相反。"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_cache_hit(
request=ChatRequest(
messages=[{"role": "user", "content": "hi"}], reasoning_effort=Effort.MEDIUM
),
response=_resp(cache_hit=True, applied_effort=Effort.LOW),
)
assert rec.rows[0]["reasoning_effort"] == "medium"
async def test_terminal_failure_records_the_request_tier(self):
"""终态失败可能根本没选出源,源级档位无从谈起。"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_terminal_failure(
request=ChatRequest(
messages=[{"role": "user", "content": "hi"}], reasoning_effort=Effort.XHIGH
),
call_id="c",
latency_ms=1,
error="dead",
)
value = rec.rows[0]["reasoning_effort"]
assert value == "xhigh"
assert type(value) is str
@pytest.mark.parametrize("emit", ["attempt", "cache_hit", "terminal_failure"])
async def test_silence_lands_as_null(self, emit):
"""谁都没表态时落 `NULL`: `None` 与 `'low'` 必须分得开(设计 §6)。
库并不观测模型内部的默认档,记一个推定值等于把"没看见"说成"发生了"
"""
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec, text_cap=None)
if emit == "attempt":
await emitter.emit_attempt(
request=_REQ,
source=_source(),
call_id="c",
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
elif emit == "cache_hit":
await emitter.emit_cache_hit(request=_REQ, response=_resp(cache_hit=True))
else:
await emitter.emit_terminal_failure(
request=_REQ, call_id="c", latency_ms=1, error="dead"
)
assert rec.rows[0]["reasoning_effort"] is None
async def test_a_reasonless_path_never_records_a_tier(self):
"""embedding/OCR 走同一个 emitter,但它们的 payload 里没有推理参数。
源上误配了 `ENABLE_THINKING` 时,回落若照算就会给一次 embedding 失败
挂上 `auto` ——那一档从来没有、也不可能被发出去。
"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=_REQ,
source=_source(enable_thinking=True),
call_id="c",
latency_ms=1,
response=None,
error="boom",
reasoning_applies=False,
)
assert rec.rows[0]["reasoning_effort"] is None
async def test_an_out_of_domain_tier_degrades_but_keeps_the_row(self):
"""域外取值降级为 `NULL` 且**不丢整行**(遥测必录);与缓存回放同一方向。
`LLMResponse` 无运行时校验,测试替身写裸串完全自然;直接 `Effort(raw)` 会
抛 `ValueError`,被 `_record` 的 `except Exception` 吞成丢整行。
"""
rec = _MemoryRecorder()
messages: list[str] = []
sink_id = logger.add(messages.append, level="WARNING")
try:
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=_REQ,
source=_source(),
call_id="c",
latency_ms=1,
response=_resp(applied_effort="lowest"),
error=None,
reasoning_applies=True,
)
finally:
logger.remove(sink_id)
assert len(rec.rows) == 1, "整行被吞了"
assert rec.rows[0]["reasoning_effort"] is None
hits = [m for m in messages if "lowest" in m]
assert len(hits) == 1, f"域外取值必须单独告警: {messages}"
assert [m for m in messages if "遥测记录失败" in m] == []
async def test_a_bare_string_tier_still_lands(self):
"""裸串在域内时照常归一并落库,整行不得丢失。"""
rec = _MemoryRecorder()
await TelemetryEmitter(rec, text_cap=None).emit_attempt(
request=_REQ,
source=_source(),
call_id="c",
latency_ms=1,
response=_resp(applied_effort="max"),
error=None,
reasoning_applies=True,
)
assert len(rec.rows) == 1, "整行被吞了"
value = rec.rows[0]["reasoning_effort"]
assert value == "max"
assert type(value) is str
class TestEmitterObservabilityFields:
"""issue #3: 三个入口各自的取值口径(设计 §5 表)。"""
@@ -1266,6 +1478,7 @@ class TestEmitterObservabilityFields:
latency_ms=42,
response=_resp(cached_prompt_tokens=64, model_reported="m-real", reasoning_tokens=7),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cached_prompt_tokens"] == 64
assert rec.rows[0]["model_reported"] == "m-real"
@@ -1280,6 +1493,7 @@ class TestEmitterObservabilityFields:
latency_ms=7,
response=None,
error="boom",
reasoning_applies=True,
)
assert rec.rows[0]["cached_prompt_tokens"] is None
assert rec.rows[0]["model_reported"] is None
@@ -1330,6 +1544,7 @@ class TestEmitterSamplingColumn:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
assert json.loads(rec.rows[0]["sampling"]) == {"seed": 42, "temperature": 0}
@@ -1344,6 +1559,7 @@ class TestEmitterSamplingColumn:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
await emitter.emit_cache_hit(request=self._SAMPLED, response=_resp())
await emitter.emit_terminal_failure(
@@ -1376,6 +1592,7 @@ class TestEmitterSamplingColumn:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["sampling"] is None
@@ -1408,6 +1625,7 @@ class TestEmitterCallerDimensions:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
elif emit == "cache_hit":
await emitter.emit_cache_hit(request=self._REQ_A, response=_resp(cache_hit=True))
@@ -1461,6 +1679,7 @@ class TestEmitterCallerDimensions:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
)
row = rec.rows[0]
assert row["tenant_id"] == ""
@@ -1521,6 +1740,7 @@ class TestCostWithCachedTier:
latency_ms=1,
response=full,
error=None,
reasoning_applies=True,
)
await emitter.emit_attempt(
request=_REQ,
@@ -1531,6 +1751,7 @@ class TestCostWithCachedTier:
prompt_tokens=1_000_000, completion_tokens=0, cached_prompt_tokens=600_000
),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] == pytest.approx(10.0)
assert rec.rows[1]["cost"] == pytest.approx(5.2) # 400k×10 + 600k×2
@@ -1553,6 +1774,7 @@ class TestCostWithCachedTier:
latency_ms=1,
response=_resp(usage_source="unavailable", cached_prompt_tokens=5),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] is None
@@ -1568,6 +1790,7 @@ class TestEmitter:
latency_ms=42,
response=_resp(),
error=None,
reasoning_applies=True,
)
row = rec.rows[0]
assert row["call_id"] == "cid-1" and row["error"] is None
@@ -1584,6 +1807,7 @@ class TestEmitter:
latency_ms=7,
response=None,
error="TransientError: boom",
reasoning_applies=True,
)
row = rec.rows[0]
assert row["error"].startswith("TransientError")
@@ -1615,6 +1839,7 @@ class TestEmitter:
usage_source="unavailable", prompt_tokens=prompt, completion_tokens=completion
),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] is None
@@ -1628,6 +1853,7 @@ class TestEmitter:
latency_ms=42,
response=_resp(prompt_tokens=0, completion_tokens=4000),
error=None,
reasoning_applies=True,
)
assert rec.rows[0]["cost"] == pytest.approx(0.032)
@@ -1661,6 +1887,7 @@ class TestEmitter:
latency_ms=1,
response=None,
error="x",
reasoning_applies=True,
)
assert len(rec.rows[0]["messages"]) < 500 # base64 不整段进库(VT R12)
@@ -1677,6 +1904,7 @@ class TestEmitter:
latency_ms=1,
response=_resp(),
error=None,
reasoning_applies=True,
) # 不抛(降级不冒泡)
@@ -1773,6 +2001,7 @@ async def _emit_with_cap(messages, *, cap, response=_LONG, thinking=_LONG):
latency_ms=1,
response=_resp(content=response, thinking=thinking),
error=None,
reasoning_applies=True,
)
return rec.rows[0]
+2
View File
@@ -262,6 +262,7 @@ async def test_emit_attempt_success_stays_in_domain(emitted):
latency_ms=10,
response=_resp(emitted),
error=None,
reasoning_applies=True,
)
assert recorder.rows[0]["usage_source"] in USAGE_SOURCES
@@ -276,6 +277,7 @@ async def test_emit_attempt_failed_attempt_stays_in_domain():
latency_ms=10,
response=None,
error="boom",
reasoning_applies=True,
)
assert recorder.rows[0]["usage_source"] in USAGE_SOURCES