Build Comparer and Reports for Trueup

Continue building Trueup in this repository. Read README.md, hackathon.md, CHANGELOG.md and src/ first. This session is recorded by Entire and published: work only inside this repository and never mention anything outside it. Do not git commit or push.
Task: the comparer and the reports (pure core, no I/O, fully unit tested).
src/trueup/core/compare.py: given an Observation (truth from the fake's log + app outcome, trace = exported spans), apply these checks and return Mismatches with severity:- Run status: app raised / got a result -> top-level span ERROR / not ERROR. HIGH if a crash shows OK at the top level.
- Call status: provider returned an error or the stream failed -> that LLM span ERROR with an exception event. MEDIUM.
- Call count: provider requests logged -> one LLM span per request. HIGH if duplicated or missing.
- Tokens: provider usage -> per-span tokens equal and the sum over LLM spans equals the real sum. HIGH if the sum is off. Only compare when the fake actually sent usage.
- Output: text / refusal / tool calls the app received -> recorded on the span, tool args exact. MEDIUM.
- Finish: finish reason / response status recorded; failed/incomplete visible. MEDIUM.
- Span closed: call ended -> span ended and exported. HIGH if a call disappears.
Severity rule: HIGH changes a number or status a dashboard shows by default (run status, cost, call count); MEDIUM is wrong or missing detail in a span; LOW is extra or cosmetic.
Each library names attributes differently (neatlogs, OpenInference
llm.*/openinference.*, OpenLLMetrygen_ai.*/llm.*): put the attribute mapping in one small adapter per library, data-driven, and test each adapter against real span JSON saved from the sweeps in.trueup/runs/(copy a few small samples intotests/fixtures/, no secrets in them). Where truth was changed by the library itself (e.g. a library adding stream_options.include_usage), compare against what the provider really sent, and record that the library changed the request as a LOW note, not a bug.
- Leads: a mismatch is a "lead" (never called a bug here).
trueup sweep <lib>now runs the comparer and prints a table: scenario, check, expected, actual, severity; writesleads.json; exits 1 if any lead, 0 if none, 2 if it could not run. trueup audit <agent.py>: run a user's script'smain()under each plain-SDK scenario with the fake provider via env base URLs (OPENAI_BASE_URL / ANTHROPIC_BASE_URL) and the chosen tracing library, then compare. Keep it simple and honest; document limits.- Re-run the sweeps for all three libraries (one at a time, memory is limited) and save the leads. Write the counts per library and per check into hackathon.md. Do not describe any lead as a confirmed bug; the self-check comes next.
- Update CHANGELOG.md. Plain words, no filler. Run
uv run pytest -q. Finish with a summary: tests, leads per library by severity, anything not done.
The comparer, the leads report, sweep comparing, and audit are all built. I re-ran the sweeps for all three libraries one at a time, and each ran 31/31. Nothing is committed.
Tests: uv run pytest -q gives 155 passed, 12 e2e deselected. I also ran the new e2e audit test for openinference only, and it passed. I didn't run it for neatlogs or openllmetry, and I didn't run the full e2e suite.
Leads per library (leads, not confirmed bugs):
| library | high | medium | low | total leads | notes |
|---|---|---|---|---|---|
| neatlogs | 4 | 18 | 0 | 22 | 5 |
| openinference | 2 | 8 | 0 | 10 | 0 |
| openllmetry | 11 | 3 | 0 | 14 | 0 |
- neatlogs: spans from Anthropic
messages.streamhave no tokens, text, tool calls or finish reason. Responses spans have no status or finish reason, and two have no function calls. The chat refusal isn't recorded. The Responses stream that ends with an error event stays UNSET. The 5 notes are neatlogs addingstream_options.include_usage; tokens are checked against what the fake really sent. - openinference: there's no span for a chat stream closed early, or for a Responses stream that ends with an error event. Responses spans have no status or finish reason, and the chat refusal isn't recorded.
- openllmetry: chat streams that raised, and the Responses error-event stream, end as OK. Some calls have no span at all: Responses failed/incomplete and Anthropic stream errors/cuts. Anthropic streamed spans show 4 output tokens where the provider sent 3.
The counts per check are in hackathon.md.
What changed:
- Comparer:
src/trueup/core/compare.pyapplies the seven checks plus the LOW "library changed the request" note. It's pure code with no I/O. - Adapters:
src/trueup/core/adapters.pyhas one data-driven adapter per library. They're tested against 21 trimmed span samples intests/fixtures/spans/. I removed stack traces, local URLs and paths from them and found no secrets. - Report:
src/trueup/core/report.pybuilds the leads table andleads.json. sweep: now compares and prints the table. It exits 1 if any lead, otherwise 2 if any scenario couldn't run, otherwise 0. If some scenarios fail to run and others have leads, it exits 1.audit:trueup audit <script> --library <lib>(insrc/trueup/audit.py) first runsmain()once to see which provider routes it calls. It then runsmain()once per fake scenario on those routes and compares. To make this work, the fake can now be told which scenario to answer with on a route, whatever model name the app sends. Onexamples/agent.pyunder openinference it made 8 runs and found 1 lead (the refusal isn't recorded).- Docs:
README.md,CHANGELOG.mdandhackathon.mdare updated.
Calls I made that you may want to review:
- Finish reasons are compared through a table of equivalent names (
end_turn=stop,tool_use=tool_call,incomplete=length), because OpenLLMetry renames them. - When the app closes a stream early, output, tokens and finish aren't compared, because the app never received them.
- Missing spans are counted under "span closed" and extra spans under "call count", so a missing span isn't counted twice.
- OpenLLMetry's span for a failed stream has no operation name, so its adapter identifies LLM spans by
gen_ai.provider.nameinstead.
Not done, or limited:
auditcan't tell when an app stops reading a stream early, and it can't produce the "request changed" note, because a user's script has no expected results.- SDK retries count as separate requests in
audit, so a script using the default retries can show call-count leads. auditonly works with the packages already in the library's environment.- Framework scenarios are still not built. The self-check of the leads comes next.
These limits are written down in src/trueup/audit.py.