mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Wire output shaping for OpenAI Responses traffic across HTTP `/v1/responses` and Codex WebSocket `response.create` frames. The change adds provider-specific shaping for `instructions`, `reasoning.effort`, and `text.verbosity` while keeping Anthropic request mutation separate. Review follow-up: merged byte-faithful `/v1/responses` forwarding from #1557 and marks shaped HTTP Responses payloads as `body_mutated=True`, so retry forwarding sends the shaped body instead of the original raw bytes. ## Type of Change - [ ] Bug fix (non-breaking change fixes an issue) - [x] New feature (non-breaking change adds functionality) - [ ] Breaking change (fix or feature would cause existing functionality change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added OpenAI Responses output shaping for `instructions`, `reasoning.effort`, and `text.verbosity`. - Wired shaping into `/v1/responses` HTTP and Codex WebSocket `response.create` paths. - Preserved `x-headroom-bypass` and `HEADROOM_OUTPUT_HOLDOUT` behavior. - Added output-shaper transform labels for verbosity, text verbosity, reasoning effort, holdout control, and strata. - Updated output-savings conversation keys for Responses payloads and WS `response.create` envelopes. - Counted WS frame payload tokens when assigning output-savings strata. - Merged byte-faithful `/v1/responses` forwarding from #1557 and kept shaped HTTP bodies on the mutated-forwarding path. - Added tests for classification, shaping, holdout, bypass, labels, WS strata, and byte-faithful forwarding compatibility. - Updated `CHANGELOG.md` for OpenAI Responses output-shaping support. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run --extra dev python -m pytest tests/test_openai_codex_ws_lifecycle.py tests/test_openai_responses_output_shaper.py tests/test_codex_responses_passthrough_bytes.py tests/test_output_shaper.py tests/test_output_savings.py -q 110 passed, 1 warning in 1.49s $ uv run --extra dev ruff check headroom/proxy/handlers/openai.py tests/test_openai_responses_output_shaper.py tests/test_codex_responses_passthrough_bytes.py tests/test_openai_codex_ws_lifecycle.py tests/test_output_shaper.py tests/test_output_savings.py All checks passed! $ git diff --check No whitespace errors. ``` ## Real Behavior Proof - Environment: local macOS checkout, branch `output-shaper-openai-responses`. - Exact command / steps: ran targeted pytest, ruff, and diff checks listed above. - Observed result: targeted tests passed with an existing FastAPI TestClient deprecation warning; ruff passed; diff check passed. - Not tested: full repository test suite, live OpenAI traffic, browser dashboard rendering, full `mypy headroom`. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows project's style guidelines - [x] I performed self-review of my code - [x] I commented my code, particularly in hard-to-understand areas - [x] I made corresponding changes to documentation - [x] My changes generate no new warnings - [x] I added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I updated `CHANGELOG.md` if applicable ## Screenshots N/A ## Additional Notes - Non-applicable Type Change items are left unchecked. - The pytest warning comes from `fastapi.testclient` importing Starlette TestClient and was not introduced by this change. - `CHANGELOG.md` includes entries for OpenAI Responses output-shaping support and byte-faithful `/v1/responses` forwarding compatibility. --------- Co-authored-by: obchain <riteshnikhoriya94@gmail.com>
484 lines
18 KiB
Python
484 lines
18 KiB
Python
"""Tests for headroom.proxy.output_savings — the counterfactual estimator."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from headroom.proxy.output_savings import (
|
|
BaselineModel,
|
|
SavingsLedger,
|
|
SavingsRecorder,
|
|
assign_arm,
|
|
conversation_key_from_body,
|
|
echo_ratio,
|
|
input_bucket,
|
|
model_family,
|
|
stratum_key,
|
|
stratum_label,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# stratification primitives
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestStratification:
|
|
def test_input_buckets_monotone(self):
|
|
assert input_bucket(0) == "xs"
|
|
assert input_bucket(1_999) == "xs"
|
|
assert input_bucket(2_000) == "s"
|
|
assert input_bucket(8_000) == "m"
|
|
assert input_bucket(32_000) == "l"
|
|
assert input_bucket(200_000) == "xl"
|
|
|
|
def test_model_family_collapses_point_releases(self):
|
|
assert model_family("claude-opus-4-8") == "opus"
|
|
assert model_family("claude-opus-4-7") == "opus"
|
|
assert model_family("claude-sonnet-4-6") == "sonnet"
|
|
assert model_family("gpt-4o") == "gpt"
|
|
assert model_family("something-weird") == "other"
|
|
|
|
def test_stratum_key_is_most_to_least_specific(self):
|
|
key = stratum_key(
|
|
turn_kind="new_user_ask", input_tokens=5000, model="claude-opus-4-8", has_tools=True
|
|
)
|
|
assert key == "opus|new_user_ask|s|tools"
|
|
|
|
def test_stratum_key_distinguishes_tools(self):
|
|
a = stratum_key(turn_kind="x", input_tokens=100, model="m", has_tools=True)
|
|
b = stratum_key(turn_kind="x", input_tokens=100, model="m", has_tools=False)
|
|
assert a != b
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# holdout arm assignment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestArmAssignment:
|
|
def test_zero_holdout_always_treatment(self):
|
|
assert assign_arm("anything", 0.0) == "treatment"
|
|
|
|
def test_full_holdout_always_control(self):
|
|
assert assign_arm("anything", 1.0) == "control"
|
|
|
|
def test_assignment_is_stable_for_same_key(self):
|
|
assert assign_arm("conv-123", 0.5) == assign_arm("conv-123", 0.5)
|
|
|
|
def test_roughly_matches_fraction(self):
|
|
keys = [f"conv-{i}" for i in range(4000)]
|
|
control = sum(1 for k in keys if assign_arm(k, 0.1) == "control")
|
|
# 10% holdout over 4000 keys — allow generous slack for hash noise.
|
|
assert 250 < control < 550
|
|
|
|
def test_conversation_key_stable_across_turns(self):
|
|
first = {
|
|
"model": "claude-opus-4-8",
|
|
"messages": [{"role": "user", "content": "build a cache"}],
|
|
}
|
|
later = {
|
|
"model": "claude-opus-4-8",
|
|
"messages": [
|
|
{"role": "user", "content": "build a cache"},
|
|
{"role": "assistant", "content": "ok"},
|
|
{"role": "user", "content": [{"type": "tool_result", "content": "x"}]},
|
|
],
|
|
}
|
|
assert conversation_key_from_body(first) == conversation_key_from_body(later)
|
|
|
|
def test_conversation_key_differs_by_first_message(self):
|
|
a = {"model": "m", "messages": [{"role": "user", "content": "task A"}]}
|
|
b = {"model": "m", "messages": [{"role": "user", "content": "task B"}]}
|
|
assert conversation_key_from_body(a) != conversation_key_from_body(b)
|
|
|
|
def test_conversation_key_uses_responses_stable_metadata(self):
|
|
a = {
|
|
"model": "gpt-5",
|
|
"client_metadata": {"session_id": "session-1"},
|
|
"input": "task A",
|
|
}
|
|
b = {
|
|
"model": "gpt-5",
|
|
"client_metadata": {"session_id": "session-2"},
|
|
"input": "task A",
|
|
}
|
|
assert conversation_key_from_body(a) != conversation_key_from_body(b)
|
|
|
|
def test_conversation_key_does_not_use_responses_delta_text(self):
|
|
user_turn = {
|
|
"model": "gpt-5",
|
|
"instructions": "same session instructions",
|
|
"input": "task A",
|
|
}
|
|
tool_turn = {
|
|
"model": "gpt-5",
|
|
"instructions": "same session instructions",
|
|
"input": [
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_1",
|
|
"output": "ok",
|
|
}
|
|
],
|
|
}
|
|
assert conversation_key_from_body(user_turn) == conversation_key_from_body(tool_turn)
|
|
|
|
def test_conversation_key_unwraps_ws_response_create(self):
|
|
http_body = {"model": "gpt-5", "input": "build a cache"}
|
|
ws_body = {
|
|
"type": "response.create",
|
|
"response": {"model": "gpt-5", "input": "build a cache"},
|
|
}
|
|
assert conversation_key_from_body(http_body) == conversation_key_from_body(ws_body)
|
|
|
|
def test_conversation_key_uses_responses_conversation_id(self):
|
|
a = {
|
|
"model": "gpt-5",
|
|
"conversation": "conv_1",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "task A"}],
|
|
}
|
|
],
|
|
}
|
|
b = {
|
|
"model": "gpt-5",
|
|
"conversation": "conv_2",
|
|
"input": [
|
|
{
|
|
"type": "message",
|
|
"role": "user",
|
|
"content": [{"type": "input_text", "text": "task B"}],
|
|
}
|
|
],
|
|
}
|
|
assert conversation_key_from_body(a) != conversation_key_from_body(b)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# baseline model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBaselineModel:
|
|
def test_observe_and_lookup_exact(self):
|
|
m = BaselineModel()
|
|
for v in (100, 200, 300):
|
|
m.observe("opus|new_user_ask|s|tools", v)
|
|
mean, var, n = m.lookup("opus|new_user_ask|s|tools")
|
|
assert mean == 200.0
|
|
assert n == 3
|
|
assert var > 0
|
|
|
|
def test_lookup_backs_off_to_prefix(self):
|
|
m = BaselineModel()
|
|
m.observe("opus|new_user_ask|s|tools", 500)
|
|
# Query a sibling stratum (different tools flag) — backs off on prefix.
|
|
mean, _, n = m.lookup("opus|new_user_ask|s|notools")
|
|
assert mean == 500.0
|
|
assert n == 1
|
|
|
|
def test_lookup_falls_back_to_global(self):
|
|
m = BaselineModel()
|
|
m.observe("opus|a|s|tools", 100)
|
|
m.observe("sonnet|b|m|notools", 300)
|
|
mean, _, n = m.lookup("gpt|totally|xl|tools")
|
|
assert mean == 200.0 # global mean of 100 and 300
|
|
assert n == 2
|
|
|
|
def test_roundtrip_serialization(self):
|
|
m = BaselineModel()
|
|
for v in (10, 20, 30):
|
|
m.observe("k|a|s|tools", v)
|
|
m2 = BaselineModel.from_dict(m.to_dict())
|
|
assert m2.lookup("k|a|s|tools") == m.lookup("k|a|s|tools")
|
|
assert m2.total_samples == 3
|
|
|
|
def test_merge_is_equivalent_to_observing_both_streams(self):
|
|
# Merging two baselines must equal observing every value against one
|
|
# model — same totals per stratum and same global fallback.
|
|
a = BaselineModel()
|
|
for v in (100, 200):
|
|
a.observe("opus|new_user_ask|s|tools", v)
|
|
b = BaselineModel()
|
|
b.observe("opus|new_user_ask|s|tools", 300)
|
|
b.observe("sonnet|unknown|m|notools", 50)
|
|
|
|
a.merge(b)
|
|
|
|
mean, _, n = a.lookup("opus|new_user_ask|s|tools")
|
|
assert n == 3
|
|
assert mean == 200.0 # (100 + 200 + 300) / 3
|
|
assert a.total_samples == 4 # 3 + 1 across both strata
|
|
|
|
reference = BaselineModel()
|
|
for v in (100, 200, 300):
|
|
reference.observe("opus|new_user_ask|s|tools", v)
|
|
reference.observe("sonnet|unknown|m|notools", 50)
|
|
assert a.to_dict() == reference.to_dict()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# synthetic-control estimate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEstimateFromBaseline:
|
|
def _ledger_with_baseline(self, baseline_val: float, n: int = 50) -> SavingsLedger:
|
|
ledger = SavingsLedger()
|
|
for _ in range(n):
|
|
ledger.baseline.observe("opus|new_user_ask|s|tools", baseline_val)
|
|
return ledger
|
|
|
|
def test_positive_savings_when_treatment_below_baseline(self):
|
|
ledger = self._ledger_with_baseline(1000.0)
|
|
for _ in range(20):
|
|
ledger.record("treatment", "opus|new_user_ask|s|tools", 700)
|
|
est = ledger.estimate_from_baseline()
|
|
assert est.kind == "estimated"
|
|
assert est.n_requests == 20
|
|
# 20 requests * (1000 - 700) = 6000 tokens saved.
|
|
assert abs(est.tokens_saved - 6000) < 1e-6
|
|
assert abs(est.pct - 30.0) < 1e-6
|
|
|
|
def test_signed_delta_not_clamped(self):
|
|
# A treatment request LARGER than baseline must reduce the total, not
|
|
# be clamped to zero (clamping would bias the estimate upward).
|
|
ledger = self._ledger_with_baseline(1000.0)
|
|
ledger.record("treatment", "opus|new_user_ask|s|tools", 700)
|
|
ledger.record("treatment", "opus|new_user_ask|s|tools", 1400)
|
|
est = ledger.estimate_from_baseline()
|
|
# (1000-700) + (1000-1400) = 300 - 400 = -100
|
|
assert abs(est.tokens_saved - (-100)) < 1e-6
|
|
|
|
def test_zero_baseline_samples_yields_zero(self):
|
|
ledger = SavingsLedger()
|
|
ledger.record("treatment", "opus|x|s|tools", 500)
|
|
est = ledger.estimate_from_baseline()
|
|
# No baseline at all -> global is empty -> nothing contributes.
|
|
assert est.n_requests == 0
|
|
assert est.tokens_saved == 0.0
|
|
|
|
def test_ci_band_brackets_point_estimate(self):
|
|
ledger = SavingsLedger()
|
|
for v in (900, 1000, 1100):
|
|
for _ in range(20):
|
|
ledger.baseline.observe("opus|new_user_ask|s|tools", v)
|
|
for v in (600, 700, 800):
|
|
for _ in range(20):
|
|
ledger.record("treatment", "opus|new_user_ask|s|tools", v)
|
|
est = ledger.estimate_from_baseline()
|
|
assert est.ci_low_pct <= est.pct <= est.ci_high_pct
|
|
assert est.ci_low_pct < est.ci_high_pct # nonzero band given spread
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# A/B measured estimate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEstimateFromHoldout:
|
|
def test_none_without_control_data(self):
|
|
ledger = SavingsLedger()
|
|
ledger.record("treatment", "opus|x|s|tools", 500)
|
|
assert ledger.estimate_from_holdout() is None
|
|
|
|
def test_measured_difference_of_means(self):
|
|
ledger = SavingsLedger()
|
|
for _ in range(30):
|
|
ledger.record("control", "opus|new_user_ask|s|tools", 1000)
|
|
ledger.record("treatment", "opus|new_user_ask|s|tools", 750)
|
|
est = ledger.estimate_from_holdout()
|
|
assert est is not None
|
|
assert est.kind == "measured"
|
|
# 30 * (1000 - 750) = 7500 saved; 25% of the 1000 baseline.
|
|
assert abs(est.tokens_saved - 7500) < 1e-6
|
|
assert abs(est.pct - 25.0) < 1e-6
|
|
|
|
def test_only_strata_present_in_both_arms_contribute(self):
|
|
ledger = SavingsLedger()
|
|
for _ in range(10):
|
|
ledger.record("control", "opus|a|s|tools", 1000)
|
|
ledger.record("treatment", "opus|a|s|tools", 800)
|
|
# Treatment-only stratum must not contribute (no control to compare).
|
|
ledger.record("treatment", "opus|b|m|notools", 50)
|
|
est = ledger.estimate_from_holdout()
|
|
assert est is not None
|
|
assert est.n_requests == 10
|
|
|
|
def test_best_estimate_prefers_measured(self):
|
|
ledger = SavingsLedger()
|
|
for _ in range(10):
|
|
ledger.baseline.observe("opus|a|s|tools", 1000)
|
|
ledger.record("control", "opus|a|s|tools", 1000)
|
|
ledger.record("treatment", "opus|a|s|tools", 900)
|
|
assert ledger.best_estimate().kind == "measured"
|
|
|
|
def test_best_estimate_falls_back_to_estimated(self):
|
|
ledger = SavingsLedger()
|
|
for _ in range(10):
|
|
ledger.baseline.observe("opus|a|s|tools", 1000)
|
|
ledger.record("treatment", "opus|a|s|tools", 900)
|
|
assert ledger.best_estimate().kind == "estimated"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# persistence
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLedgerPersistence:
|
|
def test_roundtrip(self, tmp_path):
|
|
ledger = SavingsLedger()
|
|
ledger.baseline.observe("opus|a|s|tools", 1000)
|
|
ledger.record("treatment", "opus|a|s|tools", 800)
|
|
ledger.record("control", "opus|a|s|tools", 1000)
|
|
path = tmp_path / "savings.json"
|
|
ledger.save(path)
|
|
loaded = SavingsLedger.load(path)
|
|
assert loaded.estimate_from_baseline().tokens_saved == (
|
|
ledger.estimate_from_baseline().tokens_saved
|
|
)
|
|
assert loaded.estimate_from_holdout() is not None
|
|
|
|
def test_load_missing_returns_empty(self, tmp_path):
|
|
ledger = SavingsLedger.load(tmp_path / "nope.json")
|
|
assert ledger.baseline.total_samples == 0
|
|
|
|
def test_load_corrupt_returns_empty(self, tmp_path):
|
|
p = tmp_path / "bad.json"
|
|
p.write_text("{not json")
|
|
ledger = SavingsLedger.load(p)
|
|
assert ledger.baseline.total_samples == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# echo ratio (direct waste signal)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEchoRatio:
|
|
def test_full_echo(self):
|
|
ctx = "the quick brown fox jumps over the lazy dog every single time"
|
|
assert echo_ratio(ctx, ctx, n=4) == 1.0
|
|
|
|
def test_no_echo(self):
|
|
out = "completely unrelated words appearing nowhere within the given source context here"
|
|
ctx = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda"
|
|
assert echo_ratio(out, ctx, n=4) == 0.0
|
|
|
|
def test_partial_echo_between_zero_and_one(self):
|
|
ctx = "alpha beta gamma delta epsilon zeta eta theta"
|
|
out = "alpha beta gamma delta brand new tokens here now"
|
|
r = echo_ratio(out, ctx, n=4)
|
|
assert 0.0 < r < 1.0
|
|
|
|
def test_short_output_returns_zero(self):
|
|
assert echo_ratio("a b", "a b c d e f g h", n=8) == 0.0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# recorder baseline reload (learn-while-running)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRecorderBaselineReload:
|
|
"""The recorder must pick up a baseline that ``learn --verbosity --apply``
|
|
writes while the proxy is already running, and a flush must never overwrite
|
|
that learned baseline with the recorder's own empty in-memory copy."""
|
|
|
|
@staticmethod
|
|
def _key() -> str:
|
|
return stratum_key(
|
|
turn_kind="code",
|
|
input_tokens=8000,
|
|
model="claude-opus-4-8",
|
|
has_tools=True,
|
|
)
|
|
|
|
def test_adopts_baseline_learned_after_start(self, tmp_path):
|
|
path = str(tmp_path / "output_savings.json")
|
|
key = self._key()
|
|
|
|
recorder = SavingsRecorder(path, flush_every=1)
|
|
for output_tokens in (200, 210, 190):
|
|
recorder.record_from_labels([stratum_label("treatment", key)], output_tokens)
|
|
|
|
# No baseline to compare against yet, so there is nothing to estimate.
|
|
assert recorder.estimate().n_requests == 0
|
|
|
|
# Simulate `learn --verbosity --apply` writing a baseline to the same
|
|
# file while the recorder is live (no restart).
|
|
learned = SavingsLedger.load(path)
|
|
for output_tokens in (400, 420, 380, 410):
|
|
learned.baseline.observe(key, output_tokens)
|
|
learned.save(path)
|
|
|
|
estimate = recorder.estimate()
|
|
assert estimate.n_requests > 0
|
|
assert estimate.kind == "estimated"
|
|
assert estimate.tokens_saved > 0
|
|
|
|
def test_flush_does_not_clobber_learned_baseline(self, tmp_path):
|
|
path = str(tmp_path / "output_savings.json")
|
|
key = self._key()
|
|
|
|
# Recorder starts before any baseline exists, so its in-memory baseline
|
|
# is empty.
|
|
recorder = SavingsRecorder(path, flush_every=1)
|
|
|
|
learned = SavingsLedger.load(path)
|
|
for output_tokens in (400, 420, 380, 410):
|
|
learned.baseline.observe(key, output_tokens)
|
|
learned.save(path)
|
|
assert SavingsLedger.load(path).baseline.total_samples == 4
|
|
|
|
recorder.record_from_labels([stratum_label("treatment", key)], 200)
|
|
recorder.flush()
|
|
|
|
# The flush must keep the learned baseline rather than writing the empty
|
|
# in-memory one over it.
|
|
assert SavingsLedger.load(path).baseline.total_samples == 4
|
|
|
|
def test_does_not_downgrade_to_empty_disk_baseline(self, tmp_path):
|
|
path = str(tmp_path / "output_savings.json")
|
|
key = self._key()
|
|
|
|
# Recorder already holds a learned baseline in memory.
|
|
recorder = SavingsRecorder(path, flush_every=1)
|
|
recorder._ledger.baseline.observe(key, 400)
|
|
recorder._ledger.baseline.observe(key, 420)
|
|
assert recorder._ledger.baseline.total_samples == 2
|
|
|
|
# A stale/empty file on disk must not erase a baseline we already hold.
|
|
SavingsLedger().save(path)
|
|
recorder.flush()
|
|
|
|
assert recorder._ledger.baseline.total_samples == 2
|
|
|
|
def test_relearn_with_same_sample_count_is_adopted(self, tmp_path):
|
|
path = str(tmp_path / "output_savings.json")
|
|
key = self._key()
|
|
|
|
recorder = SavingsRecorder(path, flush_every=1)
|
|
for output_tokens in (200, 210, 190):
|
|
recorder.record_from_labels([stratum_label("treatment", key)], output_tokens)
|
|
|
|
# First learn writes a baseline; the recorder adopts it.
|
|
first = SavingsLedger.load(path)
|
|
for output_tokens in (400, 400, 400, 400):
|
|
first.baseline.observe(key, output_tokens)
|
|
first.save(path)
|
|
baseline_tokens_v1 = recorder.estimate().baseline_tokens
|
|
assert baseline_tokens_v1 > 0
|
|
|
|
# Re-running learn replaces the baseline in place with the SAME number of
|
|
# samples but different values. A sample-count guard would miss this; the
|
|
# recorder must still pick the new baseline up.
|
|
relearned = SavingsLedger.load(path)
|
|
relearned.baseline = BaselineModel()
|
|
for output_tokens in (800, 800, 800, 800):
|
|
relearned.baseline.observe(key, output_tokens)
|
|
relearned.save(path)
|
|
|
|
assert recorder.estimate().baseline_tokens > baseline_tokens_v1
|