Skip to content

Commit ea6fffa

Browse files
fix(evals): bind HTTP resumes to experiment identity (#4707)
* fix(evals): bind HTTP resumes to experiment identity * fix(evals): publish only validated complete HTTP samples Serialize each result and raw sample before writing them. Stage samples in the destination directory and publish them without overwriting existing captures, so malformed responses or failed writes cannot leave a poisoned sample that prevents a later valid resume. Cover invalid probabilities, non-finite raw extras, successful retry, and existing sample preservation. * fix(evals): refuse dangling samples and honor file umask Reject unusable existing sample destinations before inference and after an atomic-link race. Create staged files with ordinary exclusive-open permissions instead of mkstemp's forced 0600; preserve the current umask without modifying process-global settings. Test dangling links, clean staging after refusal, and normal shared-readable sample modes. * fix(evals): score sparse subsets without fabricated coverage * fix(evals): validate resumed metrics before publishing results Use one row admission path for fresh responses and resumed rows. Reject nonfinite JSON, invalid timings and malformed token counts before inference, partial-log appends, samples or final-output creation. Preserve absent server timing and valid zero counts. The new regressions fail on the previous implementation. All 23 HTTP runner tests and Python compilation pass locally on hash-verified source; hosted Bazel and the full platform matrix remain required. * fix(evals): validate calibrated replay evidence Require calibrated partial rows to retain the exact raw probabilities that produce their reported calibrated values. Refuse fabricated raw probabilities on uncalibrated runs, before HTTP calls or file writes. Add behavioral resume regressions for missing, changed and unexpected raw values, tampered calibrated values and successful exact replay. * fix(evals): preserve unknown usage in resumed reports Keep input, state and dropped-token counts distinct. Missing server telemetry stays null; reported zeroes remain measured zeroes. Score observed values with explicit coverage and refuse full-cohort truncation claims when some cases are unmeasured. Advance HTTP experiment identity to version 2 and refuse ambiguous older partial and completed results instead of relabeling their metrics. All 43 Python runner and scoring tests pass, including interrupted resume and CLI reporting with controlled HTTP responses. Baseline source reproduced the telemetry failures. Python parsing and whitespace checks pass; Bazel execution and live model qualification were not performed. * fix(evals): put intent engine modules on the test import path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 68c1dd5 commit ea6fffa

7 files changed

Lines changed: 1153 additions & 83 deletions

File tree

‎evals/intent_engine/BUILD.bazel‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("@rules_python//python:defs.bzl", "py_test")
2+
3+
py_test(
4+
name = "http_runner_test",
5+
size = "small",
6+
srcs = [
7+
"common.py",
8+
"run_http.py",
9+
"score.py",
10+
"test_run_http.py",
11+
],
12+
imports = ["."],
13+
main = "test_run_http.py",
14+
)
15+
16+
py_test(
17+
name = "scoring_test",
18+
size = "small",
19+
srcs = [
20+
"common.py",
21+
"compare.py",
22+
"score.py",
23+
"test_score.py",
24+
],
25+
imports = ["."],
26+
main = "test_score.py",
27+
)

‎evals/intent_engine/HTTP_RUNNER.md‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Resumable HTTP evaluations
2+
3+
`run_http.py` requires a serving declaration in addition to the endpoint and tag:
4+
5+
```sh
6+
python evals/intent_engine/run_http.py \
7+
--url http://127.0.0.1:8009/v1/systemone \
8+
--tag pinned-run \
9+
--server-config /path/to/serving.json \
10+
--send-model kev-latest \
11+
B_noul_text
12+
```
13+
14+
The JSON declaration must contain nonempty `checkpoint`, `server_revision`,
15+
`backend`, and `dtype` strings, and the actual finite, nonnegative server
16+
`temperature`. Include other settings that affect inference as additional keys.
17+
Record immutable checkpoint/server revisions, not only a moving model alias.
18+
Do not put credentials in this file. The declaration is operator-supplied
19+
provenance, **not remote attestation**: update it whenever the deployment changes.
20+
The optional `--noul-temperature` is a separate client-side calibration step;
21+
it must be finite and strictly positive.
22+
23+
The existing `.partial` log starts with the experiment identity. This binds the
24+
whole dataset hash, selected case IDs and order, the exact rendered HTTP request
25+
bytes for each case, endpoint hash, requested model, client calibration, and the
26+
serving declaration. A completed result retains the same identity and each row's
27+
request fingerprint. The endpoint itself is not copied into the identity.
28+
29+
An interrupted run resumes only with matching identity. Changed questions,
30+
question order, renderer output, labels, subset contents, or serving settings
31+
refuse before any HTTP request or append. Headerless old logs, incomplete rows,
32+
duplicate/foreign cases, and malformed probability maps also refuse; no migration
33+
or silent salvage is performed. Choose a new tag for another experiment or an
34+
old unidentifiable log. Completed output files are never overwritten.
35+
36+
Use only one writer per tag/configuration. Rows are flushed after each response;
37+
this is not a crash-durable database or a concurrent-writer protocol. If final
38+
output publication itself is interrupted, the partial log remains, but an
39+
incomplete final file requires operator inspection before retrying that tag.
40+
41+
## Token and truncation evidence
42+
43+
Each result records `usage.input_tokens` as `input_tokens`,
44+
`usage.state_tokens` as `state_tokens`, and `usage.state_tokens_dropped` as
45+
`dropped`. These are distinct server measurements; total input tokens are never
46+
substituted for state tokens. Omitted or null telemetry stays JSON `null`.
47+
Reported zeroes stay zero, and reported counts must be nonnegative integers.
48+
49+
`score.py` computes token statistics only from measured cases and reports
50+
`input_tokens_measured_cases`, `state_tokens_measured_cases`, and
51+
`truncation_measured_cases` alongside `total_cases`. The full-cohort
52+
`cases_truncated` is unknown unless every case reports a dropped-token count;
53+
`observed_cases_truncated` counts only confirmed truncation in measured cases.
54+
No measurements means unknown token statistics, not a zero-token result.
55+
56+
The experiment identity is version 2. Version-1 partial logs are refused before
57+
inference or append: their default-zero truncation and mislabeled input counts
58+
cannot be recovered as measurements. Use a new tag; do not relabel old logs.
59+
Version-1 completed HTTP results are also refused by the scorer. Historical
60+
reports are not rewritten or retrospectively qualified.
61+
62+
The runner still uses the historical nine-category evaluation in `common.py`.
63+
Runtime-renderer parity, qualified labels, held-out quality, and independent
64+
verification of server-reported telemetry remain separate qualification work.
65+
66+
Run the no-inference persistence regressions with:
67+
68+
```sh
69+
bazel test //evals/intent_engine:http_runner_test //evals/intent_engine:scoring_test --test_output=errors
70+
```
71+
72+
The tests exercise the actual runner and filesystem with substituted HTTP
73+
responses. They do not download models, spend inference tokens, or qualify a
74+
model's quality or performance.

‎evals/intent_engine/compare.py‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
"""Head-to-head tables from summary.json for the report. usage: compare.py CONFIG [CONFIG ...]"""
2-
import json, os, sys, collections
2+
import json, os, sys
33
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
4-
from common import CATS, load
4+
from common import CATS
55
S = json.load(open(os.path.join(HERE, "summary.json")))
66
cfgs = [c for c in sys.argv[1:] if c in S]
7-
ds = load(os.path.join(HERE, "dataset.json"))
8-
cnt = collections.Counter(l for c in ds for l in c["labels"])
9-
print("n=%d none=%d ctx=%d" % (len(ds), sum(not c["labels"] for c in ds), sum(c["context_needed"] for c in ds)), dict(cnt))
7+
for c in cfgs:
8+
sample = S[c]["t0.5"]
9+
print("%s: n=%d none=%d ctx=%d" % (c, sample["all"]["n"], sample["all"]["none_n"], sample["ctx"]["n"]))
1010
f = lambda x: "-" if x is None else "%.2f" % x
1111
print("\n| metric | " + " | ".join(cfgs) + " |\n|---|" + "---|" * len(cfgs))
1212
rows = [("macro AUC", lambda s: s["macro_auc"]), ("macro AUC (ctx subset)", lambda s: s["macro_auc_ctx"]),
13+
("AUC eligible categories", lambda s: s["macro_auc_n_categories"]),
14+
("AUC eligible categories (ctx subset)", lambda s: s["macro_auc_ctx_n_categories"]),
15+
("LOO evaluated cases", lambda s: s["loo_evaluated_cases"]),
1316
("macro F1 @ best t (in-sample)", lambda s: s["best_t_in_sample"]["macro_f1"]),
1417
("macro F1 @ LOO-chosen t", lambda s: s["macro_loo_f1"]),
1518
("macro F1 @ 0.6", lambda s: s["t0.6"]["macro_f1"]),
@@ -24,14 +27,14 @@
2427
("median ms / call", lambda s: s["latency"]["median_ms"])]
2528
for name, fn in rows:
2629
print("| %s | %s |" % (name, " | ".join(f(fn(S[c])) if name != "median ms / call" else str(fn(S[c])) for c in cfgs)))
27-
print("\nper-category F1 at best t (t) / F1 at 0.6 / AUC")
28-
print("| category (n) | " + " | ".join(cfgs) + " |\n|---|" + "---|" * len(cfgs))
30+
print("\nper-category positive count; F1 at best t (t) / F1 at 0.6 / AUC")
31+
print("| category | " + " | ".join(cfgs) + " |\n|---|" + "---|" * len(cfgs))
2932
for k in CATS:
3033
cells = []
3134
for c in cfgs:
3235
p = S[c]["per_cat"][k]
33-
cells.append("%s (%.2g) / %s / %s" % (f(p["best_f1"]), p["best_t"], f(p["sweep"]["0.6"]["f1"]), f(p["auc"])))
34-
print("| %s (%d) | %s |" % (k, cnt[k], " | ".join(cells)))
35-
print("\nrecommended thresholds (in-sample best-F1 t; LOO F1)")
36+
cells.append("n=%d; %s (%.2g) / %s / %s" % (p["n_pos"], f(p["best_f1"]), p["best_t"], f(p["sweep"]["0.6"]["f1"]), f(p["auc"])))
37+
print("| %s | %s |" % (k, " | ".join(cells)))
38+
print("\nin-sample threshold selections (not held-out qualification; LOO F1)")
3639
for c in cfgs:
37-
print(c, {k: (S[c]["per_cat"][k]["best_t"], round(S[c]["per_cat"][k]["loo_f1"] or 0, 2)) for k in CATS})
40+
print(c, {k: (S[c]["per_cat"][k]["best_t"], None if S[c]["per_cat"][k]["loo_f1"] is None else round(S[c]["per_cat"][k]["loo_f1"], 2)) for k in CATS})

0 commit comments

Comments
 (0)