Build Trueup Fake Provider and Scenarios

Continue building Trueup in this repository. Read README.md, hackathon.md and src/ first. This session is recorded by Entire and published, so work only inside this repository and never mention anything outside it. Do not git commit or push.
Reference code: .ref/ (gitignored) holds a rough harness written earlier today during the event: fake.py/fake2.py (fake OpenAI + Anthropic server, scenario chosen by the request's model name), run.py (26 plain-SDK scenarios) and run2.py (22 framework scenarios). Rebuild it properly inside the package; do not copy it as is.
Task: the truth side.
src/trueup/fake/: a local HTTP fake provider (stdlib http.server in a thread, or similar; random free port) speaking: OpenAI Chat Completions (plain, streaming with and without usage, tool calls incl. 2 parallel and split streamed arguments, refusal, n=2, HTTP 500, error event mid-stream, dropped connection), OpenAI Responses (plain, function call, reasoning summary, status failed, status incomplete, streaming event sequence, error event, cut), Anthropic Messages (plain, thinking, tool_use with input_json_delta, streaming, error event, cut, 529). Scenario is chosen by the request's model name. Every response id is unique per call. It logs every request and what it sent back (the truth log: number of calls, tokens per call).- Fixture tests: each format parsed by the real
openaiandanthropicSDKs (add them as dev dependencies) pointed at the fake, asserting the SDK accepts it and the values match. Errors and cuts must raise the SDK's own exceptions. src/trueup/scenarios/: the scenario library as data (id, provider/api, framework, what the app does, expected truth fields), starting with the plain-SDK cases (OpenAI chat, OpenAI Responses, Anthropic). Framework cases (LangChain/LangGraph, OpenAI Agents SDK) may be listed but marked not_built if time is short; say so honestly.src/trueup/runner/: runs one scenario in a fresh subprocess with a target library's recommended setup, captures every exported span in memory (OpenTelemetry InMemorySpanExporter) plus the app outcome (result or exception type and message), and returns an Observation (truth + trace) as JSON. Per-library venvs under.trueup/venvs/<lib>created with uv; libraries: neatlogs (neatlogs.init with its openai/anthropic instrumentation, export disabled), openinference (OpenAIInstrumentor, AnthropicInstrumentor), openllmetry (Traceloop.init with an in-memory exporter, disable_batch=True). Memory is limited: build and run one library at a time. A scenario that can't run is reported NOT RUN with the reason, never skipped.- Known traps, each needs a regression test: two scenarios reusing a response id makes a library skip a span; reusing one async client across asyncio.run loops gives "Event loop is closed"; a fake that sends no usage makes missing tokens look like a bug; neatlogs export to a dead endpoint makes a run take 200 s.
trueup sweep <library>runs the plain-SDK scenarios against one library and writes the observations to.trueup/runs/<run_id>/with a short text summary. Comparing truth to trace is the next step, not this one.- Add a dated entry to hackathon.md and update CHANGELOG.md. Plain words, no marketing filler.
Run uv run pytest -q (keep unit tests offline and fast; mark venv/subprocess tests as e2e) and then actually run uv run trueup sweep for each of the three libraries, one at a time. Finish with a summary: tests passed, scenarios run per library, any NOT RUN with reasons.
The fake provider, scenario library, runner and trueup sweep are built, and all three libraries ran all 31 plain-SDK scenarios. Nothing was committed.
Tests
uv run pytest -q: 104 passed (offline, about 9 s).uv run pytest -q -m e2e: 9 passed (about 22 s). This covers the venv and subprocess tests across the three libraries, including a neatlogs run that must finish in under 30 s.
Sweeps
| Library | Ran | NOT RUN |
|---|---|---|
| neatlogs | 31/31 (≈97 s) | 0 |
| openinference | 31/31 (≈71 s) | 0 |
| openllmetry | 31/31 (≈114 s) | 0 |
The first openllmetry sweep was 0/31 NOT RUN, and I've left that run directory in place. The cause was traceloop-sdk importing httpx without declaring it, and this openai version doesn't pull httpx in. I added httpx to that library's venv packages, and the second sweep ran 31/31. NOT RUN reasons are now the final error line, with the full traceback kept in the JSON.
What's in place
- Fake provider (
src/trueup/fake/): all the requested formats, with the scenario chosen by model name. Every id is unique per call and per fake instance. The truth log records each request, what was sent back, and the tokens per call, or that no usage was sent. - Fixture tests: 34, using the real
openaiandanthropicSDKs. Two behaviours don't meet the "raise the SDK's own exception" rule. I recorded them as they are rather than hiding them:- The
openaiSDK hands a Responseserrorevent to the app instead of raising. - The
anthropicSDK lets a dropped stream through as a rawRemoteProtocolError. A dropped non-streamed call does raiseAPIConnectionError.
- The
- Scenarios: 31 plain-SDK cases: 12 chat, 9 Responses, 10 Anthropic. A unit test runs each app with no tracing library and checks the expected truth against what really happens; it caught one wrong expectation of mine. The 20 LangChain/LangGraph/OpenAI Agents cases are listed but not built.
- Traps: all four have regression tests. The async-client trap is reproduced against the fake ("Event loop is closed"). For neatlogs I use
neatlogs.initwithdisable_export=Trueand pass in my own tracer provider, so no endpoint is ever set.
Leads for the comparison step (seen in the sweeps, not judged yet):
- neatlogs adds
stream_options.include_usageto the app's streamed chat requests. As a result, the three "no usage" scenarios flag that the truth differs from the scenario's expectation; that comes from the library changing the request, not from the harness. - openinference exports no span when the app closes a stream early, or when a Responses stream ends with an error event.
- openllmetry:
- Chat streams that raised (error event, dropped connection) are marked OK.
- No span is exported for Responses failed or incomplete.
- No span is exported for Anthropic stream errors or cuts.
Dated entries are in hackathon.md and CHANGELOG.md, and I updated the README's status section.
Two claude.ai connectors, Paybox and arvix mcp, need authorising in your claude.ai connector settings; they're unavailable until then. Nothing in this task used them.