Build Comparer and Reports for Trueup

Claude Code·Opus 5.5·Jennycruzy·1w ago·13min·1 Checkpoint·41 file changes·+4096/-24·46.6K tokens

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).

  1. 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.*, OpenLLMetry gen_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 into tests/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.
  2. 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; writes leads.json; exits 1 if any lead, 0 if none, 2 if it could not run.
  3. trueup audit <agent.py>: run a user's script's main() 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.
  4. 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.
  5. 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.
1w ago·12m

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):

libraryhighmediumlowtotal leadsnotes
neatlogs4180225
openinference280100
openllmetry1130140
  • neatlogs: spans from Anthropic messages.stream have 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 adding stream_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.py applies the seven checks plus the LOW "library changed the request" note. It's pure code with no I/O.
  • Adapters: src/trueup/core/adapters.py has one data-driven adapter per library. They're tested against 21 trimmed span samples in tests/fixtures/spans/. I removed stack traces, local URLs and paths from them and found no secrets.
  • Report: src/trueup/core/report.py builds the leads table and leads.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> (in src/trueup/audit.py) first runs main() once to see which provider routes it calls. It then runs main() 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. On examples/agent.py under openinference it made 8 runs and found 1 lead (the refusal isn't recorded).
  • Docs: README.md, CHANGELOG.md and hackathon.md are 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.name instead.

Not done, or limited:

  • audit can'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.
  • audit only 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.