Implement Planner and Model Client

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. Run every command in the foreground and wait for it (no background commands; use long timeouts). Do not git commit or push. Memory is limited: one library at a time.
Secrets: the environment has TRUEUP_MODEL_KEY, TRUEUP_MODEL_BASE_URL, NEATLOGS_API_KEY and NEATLOGS_PROJECT. Never print, log, echo or write their values anywhere (not even partly). Check only whether they are set.
Task 1: the model client and the planner.
src/trueup/model.py: one client using theopenaiSDK against TRUEUP_MODEL_BASE_URL (an OpenAI-compatible LiteLLM proxy). Models:claude-sonnet-4.6for planning and fixing,claude-haikufor cheap jobs. Never use any opus model. Retries with backoff on errors; invalid JSON from the model -> retry, then stop that worker with a clear error.- Spending guard: before every model call, GET
<base without /v1>/key/infowith the key as Bearer and readinfo.spend; refuse to call if spend >= 15.0 (USD) and stop with a clear message. Record spend before/after each run in the run directory. Unit-test the guard with a fake HTTP response. - Planner (
src/trueup/planner.py): reads a tracing library's wrapper source (from its installed venv), the scenario library and the current findings, and proposes NEW scenarios as JSON data only (never code), each naming the expected truth in fields the comparer can check. Code validates every proposal against the scenario schema and the fake's supported behaviours; invalid ones are rejected with the reason and counted. At most 2 revision tries per rejected proposal (send the rejection reason back). Accepted proposals go toscenarios/proposed/<lib>/and can be swept withtrueup sweep <lib> --proposed. - CLI:
trueup plan <lib> [--max N](default N=5).
Task 2: trace Trueup itself with neatlogs.
- Add
neatlogsas a dependency. At the start of every trueup command that calls the model (plan now, fix later): neatlogs.init with the API key from the environment, the openai instrumentation, and tags (run id, library, trueup version). One trueup run = one neatlogs session. Each worker gets its own workflow name (trueup-planner,trueup-runner,trueup-comparer,trueup-checker,trueup-deduper) and custom attributestrueup.run_id,trueup.library,trueup.verdictwhere it applies. Flush on exit, including on a crash (test this). If NEATLOGS_API_KEY is not set, run without it and say so once. IMPORTANT: this outer tracing must not interfere with the inner per-scenario capture, which runs in separate subprocesses with export disabled; keep it that way and test that the inner spans never leave the machine. trueup doctorreports whether neatlogs and the model are reachable (one cheap GET each; no model call).
Then actually run trueup plan openllmetry --max 3 and trueup plan neatlogs --max 3 once each (real model calls; keep total cost low), sweep the accepted proposals, and run the checker on any lead they produce. Record in hackathon.md: proposals made, accepted, rejected with reasons, leads and findings from them, model tokens and cost (from the key's spend), and the neatlogs session ids (not URLs with secrets). Be honest if the planner found nothing new.
Update CHANGELOG.md and README status. Plain words, no filler. uv run pytest -q must pass. Finish with a summary.
Both tasks are built and both real planning runs are done. uv run pytest -q passes (273 tests), and the new e2e test that checks scenario spans stay on the machine passes too. Nothing is committed, and no secret value appears in any file.
Task 1: model client and planner
- Model client (
src/trueup/model.py): uses theopenaiSDK against the proxy, withclaude-sonnet-4.6for planning andclaude-haikufor cheap jobs. Any opus model is refused. Failed calls are retried with backoff; invalid JSON is retried twice, then that worker stops with a clear error. - Spending guard: before every call it reads
info.spendfrom<base>/key/infoand refuses at $15 or more. Spend before and after each run goes intospend.jsonin the run directory. It's unit-tested with fake HTTP responses. - Planner (
src/trueup/planner.py): a proposal can only pair an existing app with an existing fake behaviour, plus the truth it expects.- Code then checks:
- the schema is valid
- the app exists
- the fake supports that behaviour on the app's API
- the pair is new
- the truth uses only fields the comparer checks
- an untraced run in a subprocess gives that same truth
- A rejected proposal goes back to the model with the reason, at most twice.
- Accepted proposals go to
scenarios/proposed/<lib>/.
- Code then checks:
- CLI:
trueup plan <lib> [--max N](default 5) andtrueup sweep <lib> --proposed [FROM], where FROM lets you sweep one library's proposals under another.trueup checkworks on proposed sweeps.
Task 2: tracing Trueup with neatlogs (src/trueup/tracing.py)
- One run is one neatlogs session, with the session id equal to the run id. Each of the five workers gets its own trace with
trueup.run_id,trueup.libraryandtrueup.verdict. - It flushes on normal exit and on a crash; both are tested, the crash case in a separate process. Without
NEATLOGS_API_KEYit says so once and runs untraced. - The scenario subprocesses are unchanged: no neatlogs key, export off. The e2e test records every connection a scenario process makes to another machine and finds none.
trueup doctormakes one GET to neatlogs and one to the proxy, with no model call.
Real runs
| run | neatlogs session | proposals | accepted | finally rejected | tokens in / out |
|---|---|---|---|---|---|
| openllmetry | 20261010T234842Z-plan-openllmetry | 3 | 3 | 0 | 29,329 / 1,729 |
| neatlogs | 20261010T235208Z-plan-neatlogs | 3 | 3 | 0 | 30,972 / 1,529 |
- First tries: 6 of 9 were rejected before revision. The model left out the exception class, guessed the wrong class, or guessed the wrong fake outcome.
- Cost: the key's total spend is now $0.2528. The per-run figures read lower ($0.09 and $0.10) because the proxy's spend figure lags: $0.06 appeared when Trueup made no calls. At list prices the token counts come to about $0.23, which matches.
What the proposals found
- neatlogs, new: when a
messages.stream()stream fails (an error event or a dropped connection), the span shows OK with no error recorded. This passed all checker rules, and the deduper found no matching issue or PR. - openllmetry: no span when
messages.stream()fails, and an async chat stream that hits an error event shows OK. These look like the same causes as earlier findings, reached through other code paths, so I count them as more evidence rather than new bugs. - No leads: OpenInference recorded all 6 new scenarios correctly, and 3 of the 6 produced no lead in any library.
- Honestly: all 6 accepted proposals are close variations on existing scenarios, mostly a failure behaviour paired with a different app.
Fixed after the openllmetry run
- Wrong description: one accepted proposal said it streams, but its app doesn't. The truth was right; code can't check prose. I corrected that file's description by hand (the file says so), and the prompt now gives each app's real description.
- False note: another proposal led to a wrong "library changed the request" note. The planner now copies the app's own usage request from the untraced run.
Changes outside the new files: the checker's comparison against other libraries now looks through all their sweeps, not just the newest; openai and neatlogs are now runtime dependencies. hackathon.md has the full record (rejection reasons, leads per sweep, cost), and CHANGELOG.md and the README status are updated.
Separately: the Paybox and arvix connectors need authorising in your claude.ai connector settings before they can be used. Nothing in this task needed them.