595 lines
22 KiB
Python
Executable File
595 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Read-only retrospective comparison of routing against two trivial baselines.
|
|
|
|
Replays recent ``route_decisions`` rows against two counterfactuals that skip
|
|
routing's scoring entirely:
|
|
|
|
- **always_cheapest** — the eligible candidate with the lowest
|
|
``routing.estimated_cost`` for that request's shape.
|
|
- **always_best_proficiency** — the eligible candidate with the highest
|
|
``proficiency.blended_score`` for the decision's ``task_category``, ties
|
|
broken by lowest cost.
|
|
|
|
For each decision the eligible set is reconstructed from the current catalog by
|
|
re-running ``routing.select_candidates`` with the constraints that row actually
|
|
carried (``task_tier``, ``required_context_tokens``, ``latency_tolerance``,
|
|
and its ``tools``/``images``/``json_mode`` flags). This automates the "check for dominance" step in docs/routing.md: a high dominance share with a near-zero proficiency
|
|
delta means the real scoring is not earning its complexity for that slice of
|
|
traffic.
|
|
|
|
A third counterfactual is **incumbent-free dial**: for each decision the
|
|
session's prior selection (resolved via the incumbent lookup query below) is
|
|
fed to ``routing.rank_candidates`` with a ``challenger_cache_rate`` dial,
|
|
producing a rank order where the incumbent retains its measured pricing but
|
|
challengers are priced at ``min(dial, inc_rate)``. Two dials are always
|
|
reported:
|
|
|
|
- **neutral** — dial equals ``assumed_cache_rate``: the ranking is
|
|
byte-identical to the pre-incumbency ranking, so it reproduces the real
|
|
router's choice whenever the router ran at its neutral setting.
|
|
- **--incumbent-challenger (default 0.0)** — the "cold challenger" setting:
|
|
challengers lose every token of cache discount (full list price) while the
|
|
incumbent keeps its measured rate. Any dial at which the ranking would
|
|
have chosen a different model than the real one is an incumbency premium
|
|
the operator can now see.
|
|
|
|
Read-only, adds no schema, reuses ``routing.select_candidates`` and
|
|
``routing.estimated_cost``. Baselines are recomputed against the CURRENT
|
|
catalog and proficiency table, not a historical snapshot.
|
|
|
|
Run directly:
|
|
python baseline_report.py --since 2026-08-01
|
|
python baseline_report.py --since 2026-08-01 --category coding_refactor
|
|
python baseline_report.py --since 2026-08-01 --csv
|
|
python baseline_report.py --since 2026-08-01 --incumbent-challenger 0.5
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import sqlite3
|
|
import sys
|
|
from collections.abc import Sequence
|
|
|
|
from config import RouterConfig, load_config
|
|
from routing import estimated_cost, rank_candidates, select_candidates
|
|
|
|
|
|
def load_candidates(
|
|
conn: sqlite3.Connection, category: str, cfg: RouterConfig,
|
|
) -> list[dict]:
|
|
"""Current catalog rows joined with proficiency for ``category``.
|
|
|
|
Mirrors dispatcher's candidate-join shape (but without the eco/energy
|
|
columns this report does not need): each row gains ``proficiency`` (the
|
|
``blended_score`` for ``category``, or None when unmeasured) and
|
|
``tool_proficiency`` (the blend for the tool gate's category, so the
|
|
tool filter reconstructs the same way the router gates it).
|
|
"""
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT m.*,
|
|
p.blended_score AS proficiency,
|
|
tp.blended_score AS tool_proficiency
|
|
FROM models m
|
|
LEFT JOIN proficiency p
|
|
ON p.model_id = m.model_id
|
|
AND p.provider = m.provider
|
|
AND p.category = ?
|
|
LEFT JOIN proficiency tp
|
|
ON tp.model_id = m.model_id
|
|
AND tp.provider = m.provider
|
|
AND tp.category = ?
|
|
""",
|
|
(category, cfg.routing.tool_use_category),
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def load_decisions(
|
|
conn: sqlite3.Connection,
|
|
since: str | None,
|
|
category: str | None,
|
|
) -> list[sqlite3.Row]:
|
|
"""Recent routed decisions in the window, newest first.
|
|
|
|
``observed_at`` is ISO8601 UTC text, so a lexicographic comparison with a
|
|
``YYYY-MM-DD`` prefix is a valid time filter — a decision whose timestamp
|
|
starts at or after ``since`` is inside the window.
|
|
"""
|
|
clauses: list[str] = [
|
|
"kind IN ('route', 'chat', 'dispatch')",
|
|
"selected_model IS NOT NULL",
|
|
]
|
|
params: list[str] = []
|
|
if since is not None:
|
|
clauses.append("observed_at >= ?")
|
|
params.append(since)
|
|
if category is not None:
|
|
clauses.append("task_category = ?")
|
|
params.append(category)
|
|
sql = (
|
|
"SELECT * FROM route_decisions WHERE "
|
|
+ " AND ".join(clauses)
|
|
+ " ORDER BY observed_at DESC"
|
|
)
|
|
return conn.execute(sql, params).fetchall()
|
|
|
|
|
|
def baseline_selection(
|
|
candidates: Sequence[dict],
|
|
decision: sqlite3.Row,
|
|
cfg: RouterConfig,
|
|
) -> tuple[dict | None, dict | None]:
|
|
"""Pick the always_cheapest and always_best_proficiency candidates.
|
|
|
|
``candidates`` is the request's reconstructed eligible set (already run
|
|
through ``routing.select_candidates``). Both baselines are pure selections
|
|
over that set:
|
|
|
|
- cheapest: lowest ``estimated_cost`` priced for the request's own shape.
|
|
- best proficiency: highest ``proficiency`` for the decision's category,
|
|
ties broken by lowest cost, then model_id for determinism.
|
|
|
|
Returns ``(cheapest, best)``; either may be None when the set is empty.
|
|
"""
|
|
if not candidates:
|
|
return None, None
|
|
|
|
prompt_tokens = decision["required_context_tokens"] or 0
|
|
cache_rate = cfg.objective.assumed_cache_rate
|
|
completion_tokens = cfg.objective.assumed_completion_tokens
|
|
|
|
priced = []
|
|
for row in candidates:
|
|
cost = estimated_cost(row, prompt_tokens, completion_tokens, cache_rate)
|
|
priced.append((row, cost))
|
|
|
|
cheapest = min(
|
|
priced,
|
|
key=lambda rc: (rc[1] if rc[1] is not None else float("inf"), rc[0]["model_id"]),
|
|
)[0]
|
|
|
|
def prof_key(rc: tuple) -> tuple:
|
|
row, cost = rc
|
|
prof = row.get("proficiency")
|
|
return (
|
|
-(prof if prof is not None else 0.5),
|
|
cost if cost is not None else float("inf"),
|
|
row["model_id"],
|
|
)
|
|
|
|
best = min(priced, key=prof_key)[0]
|
|
return cheapest, best
|
|
|
|
|
|
def reconstruct_decision(
|
|
conn: sqlite3.Connection,
|
|
decision: sqlite3.Row,
|
|
cfg: RouterConfig,
|
|
) -> tuple[dict | None, dict | None]:
|
|
"""Re-run the decision's hard filters against the current catalog.
|
|
|
|
Returns ``(always_cheapest, always_best_proficiency)`` candidate rows the
|
|
real decision could have picked (or None when no candidate is eligible).
|
|
"""
|
|
category = decision["task_category"] or cfg.classifier.fallback_category
|
|
filters = {
|
|
"required_context_tokens": decision["required_context_tokens"] or 0,
|
|
"required_tier": decision["task_tier"] or 1,
|
|
"latency_tolerance": decision["latency_tolerance"]
|
|
or cfg.routing.default_latency_tolerance,
|
|
"allowed_access_levels": cfg.routing.allowed_access_levels,
|
|
"exclude_stale": cfg.freshness.exclude_stale,
|
|
"exclude_deprecated": cfg.freshness.exclude_deprecated,
|
|
}
|
|
if decision["tools"]:
|
|
filters["min_tool_proficiency"] = cfg.routing.min_tool_proficiency
|
|
if decision["images"]:
|
|
filters["require_vision"] = cfg.routing.require_vision
|
|
if decision["json_mode"]:
|
|
filters["require_json_mode"] = cfg.routing.require_json_mode
|
|
|
|
candidates = select_candidates(
|
|
load_candidates(conn, category, cfg), **filters,
|
|
)
|
|
return baseline_selection(candidates, decision, cfg)
|
|
|
|
|
|
def _reconstruct_candidates(
|
|
conn: sqlite3.Connection,
|
|
decision: sqlite3.Row,
|
|
cfg: RouterConfig,
|
|
) -> list[dict]:
|
|
"""Hard-filter the catalog using the decision's constraints.
|
|
|
|
Like ``reconstruct_decision`` but returns the eligible set without
|
|
running the baseline selectors — the caller picks the winners.
|
|
"""
|
|
category = decision["task_category"] or cfg.classifier.fallback_category
|
|
filters: dict[str, object] = {
|
|
"required_context_tokens": decision["required_context_tokens"] or 0,
|
|
"required_tier": decision["task_tier"] or 1,
|
|
"latency_tolerance": decision["latency_tolerance"]
|
|
or cfg.routing.default_latency_tolerance,
|
|
"allowed_access_levels": cfg.routing.allowed_access_levels,
|
|
"exclude_stale": cfg.freshness.exclude_stale,
|
|
"exclude_deprecated": cfg.freshness.exclude_deprecated,
|
|
}
|
|
if decision["tools"]:
|
|
filters["min_tool_proficiency"] = cfg.routing.min_tool_proficiency
|
|
if decision["images"]:
|
|
filters["require_vision"] = cfg.routing.require_vision
|
|
if decision["json_mode"]:
|
|
filters["require_json_mode"] = cfg.routing.require_json_mode
|
|
return select_candidates(
|
|
load_candidates(conn, category, cfg), **filters,
|
|
)
|
|
|
|
|
|
def _model_cost(row: dict | None, decision: sqlite3.Row, cfg: RouterConfig) -> float | None:
|
|
"""Estimated cost of a baseline row priced for the decision's shape."""
|
|
if row is None:
|
|
return None
|
|
return estimated_cost(
|
|
row,
|
|
decision["required_context_tokens"] or 0,
|
|
cfg.objective.assumed_completion_tokens,
|
|
cfg.objective.assumed_cache_rate,
|
|
)
|
|
|
|
|
|
def _model_proficiency(row: dict | None) -> float | None:
|
|
if row is None:
|
|
return None
|
|
prof = row.get("proficiency")
|
|
return prof if prof is not None else 0.5
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Incumbent-free counterfactual helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# The canonical session-incumbent query lives in dispatcher._session_incumbent_
|
|
# lookup. The baseline_report module must not import dispatcher (pulls in
|
|
# FastAPI, requests, openai, etc.), so we replicate the single SELECT here
|
|
# with a mirror docstring.
|
|
_SESS_INCUMBENT_SQL = """
|
|
SELECT selected_provider, selected_model
|
|
FROM route_decisions
|
|
WHERE session_key = ?
|
|
AND selected_model IS NOT NULL
|
|
AND kind IN ('chat')
|
|
ORDER BY id DESC
|
|
LIMIT 1
|
|
"""
|
|
|
|
|
|
def _session_incumbent_lookup(
|
|
conn: sqlite3.Connection, session_key: str,
|
|
) -> tuple[str, str] | None:
|
|
"""Return the session's most recent (provider, model_id) pair.
|
|
|
|
Mirrors ``dispatcher._session_incumbent_lookup``: an allowlist on ``kind``
|
|
means only explicit chat turns set the incumbent (not route probes or
|
|
dispatch-fallback rows). Returns ``None`` when the session has no chat
|
|
turns — in that case the caller should pass ``incumbent=None`` to
|
|
``rank_candidates``, restoring the pre-feature ranking.
|
|
"""
|
|
row = conn.execute(_SESS_INCUMBENT_SQL, (session_key,)).fetchone()
|
|
if row is None:
|
|
return None
|
|
return (row["selected_provider"], row["selected_model"])
|
|
|
|
|
|
def _load_cache_rates(conn: sqlite3.Connection) -> dict[tuple[str, str], float]:
|
|
"""Per-(provider, model_id) measured cache rates from the DB.
|
|
|
|
Mirrors ``dispatcher._measured_cache_rates`` without the periodic-cache
|
|
wrapper or dispatcher imports. Reads the same source table as
|
|
``metrics.cache_rate_series`` (``energy_observations``, reported
|
|
``cached_prompt_tokens``), token-weighted per (provider, model_id).
|
|
Unlike the dispatcher helper it applies no observation-count floor:
|
|
this is a retrospective report over a bounded window, not a live
|
|
routing input, and the caller (``incumbent_free_selection``) falls
|
|
back to the assumed rate for incumbents absent from the map, which
|
|
is the same behavior the floor produces.
|
|
|
|
Returns ``{}`` when no row reports cached tokens.
|
|
"""
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT provider, model_id,
|
|
SUM(cached_prompt_tokens) * 1.0
|
|
/ SUM(prompt_tokens) AS cache_rate
|
|
FROM energy_observations
|
|
WHERE cached_prompt_tokens IS NOT NULL
|
|
AND prompt_tokens IS NOT NULL
|
|
AND prompt_tokens > 0
|
|
GROUP BY provider, model_id
|
|
""",
|
|
).fetchall()
|
|
|
|
rates: dict[tuple[str, str], float] = {}
|
|
for row in rows:
|
|
cr = row["cache_rate"]
|
|
if cr is not None and cr > 0:
|
|
key = (row["provider"], row["model_id"])
|
|
rates[key] = cr
|
|
return rates
|
|
|
|
|
|
def incumbent_free_selection(
|
|
conn: sqlite3.Connection,
|
|
candidates: Sequence[dict],
|
|
decision: sqlite3.Row,
|
|
cfg: RouterConfig,
|
|
measured_rates: dict[tuple[str, str], float] | None,
|
|
challenger_dial: float = 0.0,
|
|
) -> dict[str, dict | None]:
|
|
"""Run the incumbent-counterfactual ranking for every dial.
|
|
|
|
Returns ``{dial_str: {winner, matched, cost}}`` for ``"neutral"`` and
|
|
``"challenger"``.
|
|
"""
|
|
if not candidates:
|
|
return {"neutral": None, "challenger": None}
|
|
|
|
pt = decision["required_context_tokens"] or 0
|
|
ct = cfg.objective.assumed_completion_tokens
|
|
cr = cfg.objective.assumed_cache_rate
|
|
qt = cfg.objective.quality_tolerance
|
|
result: dict[str, dict | None] = {}
|
|
|
|
# Neutral dial: incumbent=None → pre-feature ranking.
|
|
ranked = rank_candidates(
|
|
[dict(c) for c in candidates],
|
|
quality_tolerance=qt, prompt_tokens=pt, completion_tokens=ct,
|
|
cache_rate=cr, incumbent=None,
|
|
)
|
|
if ranked:
|
|
w = ranked[0]
|
|
wc = estimated_cost(w, pt, ct, cr)
|
|
result["neutral"] = {
|
|
"winner": w["model_id"],
|
|
"matched": w["model_id"] == (decision["selected_model"] or ""),
|
|
"cost": wc,
|
|
}
|
|
else:
|
|
result["neutral"] = {"winner": None, "matched": False, "cost": None}
|
|
|
|
# Challenger dial: incumbent priced at its measured rate, challengers
|
|
# clamped to min(dial, inc_rate). At dial == cr this is byte-identical
|
|
# to the neutral ranking.
|
|
incumbent = _session_incumbent_lookup(
|
|
conn, decision["session_key"] or "",
|
|
)
|
|
ranked = rank_candidates(
|
|
[dict(c) for c in candidates],
|
|
quality_tolerance=qt, prompt_tokens=pt, completion_tokens=ct,
|
|
cache_rate=cr, incumbent=incumbent,
|
|
measured_cache_rates=measured_rates,
|
|
challenger_cache_rate=challenger_dial,
|
|
)
|
|
if ranked:
|
|
w = ranked[0]
|
|
wc = estimated_cost(w, pt, ct, cr)
|
|
result["challenger"] = {
|
|
"winner": w["model_id"],
|
|
"matched": w["model_id"] == (decision["selected_model"] or ""),
|
|
"cost": wc,
|
|
}
|
|
else:
|
|
result["challenger"] = {"winner": None, "matched": False, "cost": None}
|
|
|
|
return result
|
|
|
|
|
|
def analyze(
|
|
conn: sqlite3.Connection,
|
|
cfg: RouterConfig,
|
|
since: str | None = None,
|
|
category: str | None = None,
|
|
challenger_dial: float = 0.0,
|
|
) -> tuple[dict, list[dict]]:
|
|
"""Compute aggregate and per-category baseline comparison.
|
|
|
|
Returns ``(aggregate_row, category_rows)`` where each row is a plain dict
|
|
keyed by column name, ready for both the human table and the CSV writer.
|
|
"""
|
|
decisions = load_decisions(conn, since, category)
|
|
measured_rates = _load_cache_rates(conn)
|
|
|
|
def empty_row(cat: str | None) -> dict:
|
|
return {
|
|
"category": cat if cat is not None else "total",
|
|
"count": 0,
|
|
"actual_cost": 0.0,
|
|
"cheapest_cost": 0.0,
|
|
"best_prof_cost": 0.0,
|
|
"actual_proficiency": 0.0,
|
|
"cheapest_proficiency": 0.0,
|
|
"best_prof_proficiency": 0.0,
|
|
"dominance_count": 0,
|
|
"dominance_pct": None,
|
|
# Incumbent-free counters:
|
|
"incumbent_neutral_match": 0,
|
|
"incumbent_challenger_match": 0,
|
|
"incumbent_challenger_winner": "",
|
|
}
|
|
|
|
by_cat: dict[str, dict] = {}
|
|
total = empty_row(None)
|
|
|
|
for d in decisions:
|
|
cat = d["task_category"] or cfg.classifier.fallback_category
|
|
row = by_cat.setdefault(cat, empty_row(cat))
|
|
candidates = _reconstruct_candidates(conn, d, cfg)
|
|
cheapest, best = baseline_selection(candidates, d, cfg)
|
|
|
|
for agg in (total, row):
|
|
agg["count"] += 1
|
|
agg["actual_cost"] += d["est_cost_usd"] or 0.0
|
|
cc = _model_cost(cheapest, d, cfg)
|
|
bc = _model_cost(best, d, cfg)
|
|
if cc is not None:
|
|
agg["cheapest_cost"] += cc
|
|
if bc is not None:
|
|
agg["best_prof_cost"] += bc
|
|
ap = d["est_proficiency"]
|
|
agg["actual_proficiency"] += ap if ap is not None else 0.0
|
|
cp = _model_proficiency(cheapest)
|
|
bp = _model_proficiency(best)
|
|
if cp is not None:
|
|
agg["cheapest_proficiency"] += cp
|
|
if bp is not None:
|
|
agg["best_prof_proficiency"] += bp
|
|
if cheapest is not None and cheapest["model_id"] == d["selected_model"]:
|
|
agg["dominance_count"] += 1
|
|
|
|
# Incumbent-free counterfactual for this decision
|
|
cf = incumbent_free_selection(
|
|
conn, candidates, d, cfg, measured_rates,
|
|
challenger_dial=challenger_dial,
|
|
)
|
|
for agg in (total, row):
|
|
if cf["neutral"] is not None and cf["neutral"]["matched"]:
|
|
agg["incumbent_neutral_match"] += 1
|
|
if cf["challenger"] is not None and cf["challenger"]["matched"]:
|
|
agg["incumbent_challenger_match"] += 1
|
|
# Track the most frequent challenger winner
|
|
cw = (cf["challenger"] or {}).get("winner")
|
|
if cw:
|
|
agg.setdefault("_challenger_counts", {}).setdefault(cw, 0)
|
|
agg["_challenger_counts"][cw] += 1
|
|
|
|
def finalize(r: dict) -> None:
|
|
if r["count"] == 0:
|
|
r["dominance_pct"] = None
|
|
return
|
|
r["actual_proficiency"] /= r["count"]
|
|
r["cheapest_proficiency"] /= r["count"]
|
|
r["best_prof_proficiency"] /= r["count"]
|
|
r["dominance_pct"] = 100.0 * r["dominance_count"] / r["count"]
|
|
# Pick the most common challenger winner
|
|
counts = r.pop("_challenger_counts", {})
|
|
if counts:
|
|
r["incumbent_challenger_winner"] = max(counts, key=counts.get)
|
|
else:
|
|
r["incumbent_challenger_winner"] = ""
|
|
|
|
finalize(total)
|
|
for row in by_cat.values():
|
|
finalize(row)
|
|
|
|
cat_rows = [by_cat[c] for c in sorted(by_cat)]
|
|
return total, cat_rows
|
|
|
|
|
|
def format_summary(
|
|
total: dict, cat_rows: list[dict], challenger_dial: float = 0.0,
|
|
) -> str:
|
|
"""Human-readable report text."""
|
|
lines = ["baseline routing comparator"]
|
|
lines.append(f" decisions: {total['count']}")
|
|
lines.append(
|
|
f" total cost: actual ${total['actual_cost']:.4f} "
|
|
f"cheapest ${total['cheapest_cost']:.4f} "
|
|
f"best-prof ${total['best_prof_cost']:.4f}"
|
|
)
|
|
pct = total["dominance_pct"]
|
|
dom = "n/a" if pct is None else f"{pct:.1f}%"
|
|
lines.append(
|
|
f" dominance (selected == cheapest): {total['dominance_count']}/{total['count']} "
|
|
f"({dom})"
|
|
)
|
|
lines.append(
|
|
f" mean proficiency: actual {total['actual_proficiency']:.3f} "
|
|
f"cheapest {total['cheapest_proficiency']:.3f} "
|
|
f"best-prof {total['best_prof_proficiency']:.3f}"
|
|
)
|
|
n = total["count"] or 1
|
|
lines.append(
|
|
f" incumbent-free: neutral-matched {total['incumbent_neutral_match']}/{n} "
|
|
f"challenger@{challenger_dial:.2f}-matched "
|
|
f"{total['incumbent_challenger_match']}/{n} "
|
|
f"challenger-top-choice {total['incumbent_challenger_winner'] or '(none)'}"
|
|
)
|
|
if cat_rows:
|
|
lines.append("")
|
|
lines.append(
|
|
f"{'category':18s}{'n':>5}{'dom%':>7}{'act$/dec':>10}"
|
|
f"{'cheap$/dec':>11}{'best$/dec':>11}{'act prof':>10}{'best prof':>10}"
|
|
)
|
|
for r in cat_rows:
|
|
dom = "n/a" if r["dominance_pct"] is None else f"{r['dominance_pct']:.1f}"
|
|
lines.append(
|
|
f"{r['category']:18s}{r['count']:>5}{dom:>7}"
|
|
f"{r['actual_cost'] / r['count']:>10.4f}"
|
|
f"{r['cheapest_cost'] / r['count']:>11.4f}"
|
|
f"{r['best_prof_cost'] / r['count']:>11.4f}"
|
|
f"{r['actual_proficiency']:>10.3f}"
|
|
f"{r['best_prof_proficiency']:>10.3f}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
CSV_COLUMNS = [
|
|
"category", "count", "actual_cost", "cheapest_cost", "best_prof_cost",
|
|
"actual_proficiency", "cheapest_proficiency", "best_prof_proficiency",
|
|
"dominance_count", "dominance_pct",
|
|
"incumbent_neutral_match", "incumbent_challenger_match",
|
|
"incumbent_challenger_winner",
|
|
]
|
|
|
|
|
|
def write_csv(total: dict, cat_rows: list[dict], out) -> None:
|
|
"""Write aggregate + per-category rows as CSV."""
|
|
writer = csv.DictWriter(out, fieldnames=CSV_COLUMNS)
|
|
writer.writeheader()
|
|
writer.writerow(total)
|
|
for r in cat_rows:
|
|
writer.writerow(r)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument(
|
|
"--since",
|
|
help="only consider decisions observed at/after this ISO date (YYYY-MM-DD)",
|
|
)
|
|
ap.add_argument("--category", help="only consider a single task_category")
|
|
ap.add_argument("--csv", action="store_true", help="emit CSV instead of the table")
|
|
ap.add_argument(
|
|
"--incumbent-challenger",
|
|
type=float,
|
|
default=0.0,
|
|
metavar="RATE",
|
|
help="challenger_cache_rate dial for the incumbent-free "
|
|
"counterfactual (default: 0.0, the cold-challenger setting). "
|
|
"The neutral dial (assumed_cache_rate) is always reported alongside.",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
cfg = load_config("config/config.yaml")
|
|
conn = sqlite3.connect(cfg.database.path)
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
total, cat_rows = analyze(
|
|
conn, cfg, since=args.since, category=args.category,
|
|
challenger_dial=args.incumbent_challenger,
|
|
)
|
|
|
|
if args.csv:
|
|
write_csv(total, cat_rows, sys.stdout)
|
|
else:
|
|
print(format_summary(total, cat_rows, challenger_dial=args.incumbent_challenger))
|
|
|
|
conn.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|