6d2645e64e
Diagnosing the 1,393-agent run's cache misses took a state.db join and a live
probe because none of the three were on the one line we log per call:
write=<n> cache_creation tokens; a write costs 50x a read, so this is the
money, and "read stuck, write large" on consecutive lines is a
routing miss visible without a probe
id=<id> the provider's response id (Anthropic msg_..., OpenRouter/Nous
gen-...): what a provider needs to look a request up
upstream=<n> who actually served, when the route reports it (OpenRouter's
`provider`); how we learned GMI was not serving
Fields are appended to the existing line and omitted when absent, so every
existing parser (evals/postmortem, the two cache_prefix probes) keeps
matching; the forensics parser reads them when present.
The streamed chat path fabricated `id="stream-<uuid>"` and dropped the
chunks' id/provider on the floor; it now keeps the first chunk's id and
provider, falling back to the fabricated id only when the stream never sent
one. Nothing depended on the prefix.
Live (Nous, Fable 5.1, both wires): chat -> `write=3759
id=gen-1788727882-... upstream=Anthropic`; native -> `id=gen-1788727891-...`
(the Anthropic Message object's id was already real). Tests: fields present
and omitted, prefix unchanged; forensics parser reads new and old lines.
148 lines
7.8 KiB
Python
148 lines
7.8 KiB
Python
"""Lane 1b: per-call cache behaviour from ``agent.log`` (OBSERVED provider usage per call).
|
|
|
|
Hermes logs one line per API call::
|
|
|
|
... INFO [<session_id>] agent.conversation_loop: API call #N: model=... in=<prompt> out=<out> total=... latency=..s cache=<hit>/<total> (pct) [write=<n>] [id=<response id>] [upstream=<name>]
|
|
|
|
Given the rotated logs, this reproduces: prompt-size distribution, cache hit-ratio buckets, the
|
|
"plateau" signature of a broken cache prefix (hit count stuck at the previous call's breakpoint while
|
|
``in`` grows), the share of uncached input those plateaus explain, and the sawtooth replay of an absolute
|
|
context cap on REAL per-call prompt sizes (the figure the tokens lane reported as -49%).
|
|
|
|
python -m evals.postmortem.forensics.logcalls --db state_copy.db --logs ~/.hermes/logs/agent.log*
|
|
|
|
Coverage caveat: logs rotate; report the fraction of the run's calls that were found before quoting
|
|
anything from this lane, and treat extrapolations as upper bounds.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import glob
|
|
import re
|
|
import statistics
|
|
from typing import Any, Dict, List
|
|
|
|
from evals.postmortem.forensics.common import Run
|
|
|
|
_LINE = re.compile(
|
|
r"^(\S+ \S+) INFO \[(\S+)\] agent\.conversation_loop: API call #(\d+): model=(\S+) provider=\S+ "
|
|
r"in=(\d+) out=(\d+) total=\d+ latency=([\d.]+)s cache=(\d+)/(\d+)"
|
|
)
|
|
|
|
|
|
_WRITE = re.compile(r" write=(\d+)")
|
|
_ID = re.compile(r" id=(\S+)")
|
|
_UPSTREAM = re.compile(r" upstream=(.+?)(?: [a-z_]+=|$)")
|
|
|
|
|
|
def parse_logs(paths: List[str], sids: set) -> List[Dict[str, Any]]:
|
|
calls: List[Dict[str, Any]] = []
|
|
for path in paths:
|
|
try:
|
|
fh = open(path, encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
continue
|
|
with fh:
|
|
for line in fh:
|
|
m = _LINE.match(line)
|
|
if m and m.group(2) in sids:
|
|
rec = {"ts": m.group(1), "sid": m.group(2), "n": int(m.group(3)), "model": m.group(4),
|
|
"inp": int(m.group(5)), "out": int(m.group(6)), "lat": float(m.group(7)),
|
|
"hit": int(m.group(8))}
|
|
# Newer lines (post 2026-09) also carry write= / id= / upstream=; optional.
|
|
for key, pat in (("write", _WRITE), ("id", _ID), ("upstream", _UPSTREAM)):
|
|
mm = pat.search(line)
|
|
if mm:
|
|
rec[key] = int(mm.group(1)) if key == "write" else mm.group(1)
|
|
calls.append(rec)
|
|
return calls
|
|
|
|
|
|
def sawtooth_real(by_sid: Dict[str, List[Dict[str, Any]]], cap: int, floor: int) -> float:
|
|
"""Replay on REAL prompt sizes: appended = in[i] - in[i-1]; compress to floor when above cap."""
|
|
total = 0.0
|
|
for calls in by_sid.values():
|
|
calls = sorted(calls, key=lambda c: c["n"])
|
|
ctx = None
|
|
for i, c in enumerate(calls):
|
|
appended = c["inp"] if i == 0 else max(0, c["inp"] - calls[i - 1]["inp"])
|
|
ctx = c["inp"] if ctx is None else ctx + appended
|
|
if ctx > cap:
|
|
ctx = float(floor)
|
|
total += ctx
|
|
return total
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
ap = Run.parser((__doc__ or "").split("\n\n")[0])
|
|
ap.add_argument("--logs", nargs="+", required=True, help="agent.log files (globs ok)")
|
|
ap.add_argument("--cap", type=int, default=200_000)
|
|
ap.add_argument("--floor", type=int, default=65_000)
|
|
a = ap.parse_args(argv)
|
|
run = Run.open(a.db, root=a.root, out=a.out)
|
|
paths = sorted(p for g in a.logs for p in glob.glob(g))
|
|
calls = parse_logs(paths, set(run.in_run))
|
|
total_calls = run.summary()["api_calls"]
|
|
if not calls:
|
|
print("[logcalls] no matching API-call lines found in", paths); return 1
|
|
inp = [c["inp"] for c in calls]
|
|
hit_total, in_total = sum(c["hit"] for c in calls), sum(inp)
|
|
buckets = collections.Counter()
|
|
for c in calls:
|
|
r = c["hit"] / c["inp"] if c["inp"] else 0
|
|
buckets["<50%" if r < .5 else "50-90%" if r < .9 else "90-97%" if r < .97 else "97-99%" if r < .99 else ">=99%"] += 1
|
|
by_sid: Dict[str, List[Dict[str, Any]]] = collections.defaultdict(list)
|
|
for c in calls:
|
|
by_sid[c["sid"]].append(c)
|
|
# Two definitions, reported separately (an independent review caught them being conflated):
|
|
# strict plateau: hit count EXACTLY equal to the previous call's (prefix stuck at the old breakpoint)
|
|
# non-advancing: hit count did not grow while input did (includes partial misses of other causes)
|
|
strict_pairs = strict_uncached = loose_pairs = loose_uncached = pairs = uncached_total = 0
|
|
for cs in by_sid.values():
|
|
cs.sort(key=lambda c: c["n"])
|
|
for prev, cur in zip(cs, cs[1:]):
|
|
pairs += 1
|
|
u = max(0, cur["inp"] - cur["hit"]); uncached_total += u
|
|
if cur["inp"] > prev["inp"]:
|
|
if cur["hit"] == prev["hit"]:
|
|
strict_pairs += 1; strict_uncached += u
|
|
if cur["hit"] <= prev["hit"]:
|
|
loose_pairs += 1; loose_uncached += u
|
|
price = run.price_per_token
|
|
real = sum(inp); capped = sawtooth_real(by_sid, a.cap, a.floor)
|
|
report = {
|
|
"coverage": {"calls_found": len(calls), "run_api_calls": total_calls, "fraction": round(len(calls) / total_calls, 4) if total_calls else None,
|
|
"first_ts": min(c["ts"] for c in calls), "last_ts": max(c["ts"] for c in calls),
|
|
"note": "rotated logs; extrapolations from this window are upper bounds"},
|
|
"observed": {
|
|
"prompt_tokens": {"median": int(statistics.median(inp)), "p90": sorted(inp)[int(.9 * len(inp))], "max": max(inp)},
|
|
"share_of_calls_above": {t: round(sum(1 for x in inp if x > t) / len(inp), 3) for t in (150_000, 200_000, 300_000)},
|
|
"cache_hit_ratio_overall": round(hit_total / in_total, 4),
|
|
"hit_ratio_buckets": dict(buckets),
|
|
"strict_plateau": {"pairs_share": round(strict_pairs / pairs, 4) if pairs else None,
|
|
"share_of_uncached_input": round(strict_uncached / uncached_total, 4) if uncached_total else None},
|
|
"non_advancing_hit": {"pairs_share": round(loose_pairs / pairs, 4) if pairs else None,
|
|
"share_of_uncached_input": round(loose_uncached / uncached_total, 4) if uncached_total else None},
|
|
"uncached_input_usd_in_window": round(uncached_total * price["cache_write_tokens"], 2),
|
|
},
|
|
"modeled": {
|
|
"sawtooth_on_real_prompt_sizes": {
|
|
"cap": a.cap, "floor": a.floor, "prompt_tokens_ratio": round(capped / real, 3) if real else None,
|
|
"caveats": ["compression-call cost excluded", "read/write ratio assumed unchanged", "window only"],
|
|
}
|
|
},
|
|
}
|
|
path = run.write("logcalls.json", report)
|
|
o = report["observed"]
|
|
print(f"[logcalls] coverage {len(calls):,}/{total_calls:,} calls ({report['coverage']['fraction']:.1%}) from {len(paths)} file(s)")
|
|
print(f"[logcalls] OBSERVED median prompt {o['prompt_tokens']['median']:,} p90 {o['prompt_tokens']['p90']:,}; >200K: {o['share_of_calls_above'][200000]:.0%}; hit ratio {o['cache_hit_ratio_overall']:.1%}")
|
|
print(f"[logcalls] OBSERVED strict plateau (hit unchanged): {o['strict_plateau']['pairs_share']:.1%} of pairs, {o['strict_plateau']['share_of_uncached_input']:.1%} of uncached input; "
|
|
f"non-advancing hit: {o['non_advancing_hit']['pairs_share']:.1%} / {o['non_advancing_hit']['share_of_uncached_input']:.1%}; uncached in window ${o['uncached_input_usd_in_window']:,}")
|
|
print(f"[logcalls] MODELED sawtooth cap {a.cap}/{a.floor} on real sizes: prompt volume x{report['modeled']['sawtooth_on_real_prompt_sizes']['prompt_tokens_ratio']}")
|
|
print(f"[logcalls] wrote {path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|