Trace Ecosystem (v0.63.0)
Pull traces from any SaaS observability dashboard, mine prompts, sample the most uncertain rows, run sequential A/B with martingale-controlled Type-I error, and watch production for distributional drift — all from one CLI, all offline, no per-trace fees.
Five new top-level commands, every one of them LIVE in v0.63.0.
soup ingest — universal trace importer
cp ~/Downloads/langfuse_export.jsonl ./langfuse_export.jsonl
soup ingest --source langfuse --logs ./langfuse_export.jsonl \
--output ./traces.jsonl--logs and --output must both resolve inside the current directory, so a path under ~/Downloads is refused (exit 1, "outside cwd") unless you happen to run from there. Copy the export into the project first, as above.
Six sources at launch:
| Source | Env var | Notes |
|---|---|---|
langfuse | LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY | Dashboard export, or a live pull (below) |
langsmith | LANGSMITH_API_KEY | LangSmith API traces |
helicone | HELICONE_API_KEY | Helicone observability |
openpipe | OPENPIPE_API_KEY | OpenPipe production traces |
otel | OTEL_EXPORTER_OTLP_HEADERS | OpenTelemetry OTLP |
openai-stored | OPENAI_API_KEY | OpenAI Stored Completions |
Langfuse needs a key pair, not one key. The hint this path printed used to name LANGFUSE_KEY, which is a variable Langfuse does not read at all; it names LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY as of v0.75.0. If you followed the old hint and got nowhere, that is why.
By default there are no network calls: you export from the SaaS dashboard, then soup ingest normalises the file. Langfuse is the one source Soup can also fetch for itself, with --pull (below). A PII warning prints once per invocation either way.
Live pull from Langfuse (v0.75.0)
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
soup ingest --source langfuse --pull --since 24h --output traces.jsonlNo export step. --pull replaces --logs and reads Langfuse's Observations API v2 directly, writing one row per GENERATION observation into the same TraceRecord schema the file path produces. A chat message list becomes a prompt of every message's content joined by newlines, including the system prompt, which is the same flattening the local parser already applies to a {"messages": [...]} export, so the two paths agree. An agent trace therefore yields one row per LLM call it made, and its spans and tool calls yield none. A row's trace_id is the observation id.
| Flag | Default | Notes |
|---|---|---|
--pull | off | --source langfuse only |
--since | 7d | 30m, 24h, 7d; maximum 365d |
--max-pages | 100 | Pages of 100 generations. Maximum 10,000 |
--allow-private-host | off | For a self-hosted Langfuse on a private, loopback or shared-address (100.64.0.0/10, which includes Tailscale) host. The shared-address range needs it as of v0.75.2. HTTPS is still required |
Credentials never take a flag, so they never reach the audit log's argv. They come from LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY, with LANGFUSE_BASE_URL or LANGFUSE_HOST for another region or a self-hosted instance (LANGFUSE_BASE_URL wins, matching the Langfuse SDK's own precedence), and they are kept out of the output file, the console, debug logs, the audit log and error messages. A host URL with credentials embedded in it is refused, naming the two variables instead.
Every loop is bounded, and the bounds are the point:
- HTTPS only, through the same SSRF validator
--slack-urluses. Redirects are refused rather than followed with credentials attached. - 30 s per request, 64 MiB per response.
- Hitting
--max-pagesexits 1 and writes nothing rather than handing you a silently truncated dataset. Output streams to a staging file, so a failed pull leaves an earlier file at--outputuntouched. - HTTP 429 is retried up to five times honouring
Retry-After, capped at 60 s.
Generations with no input or no output are skipped and counted in the summary line, so a pull that matched nothing usable says so instead of writing an empty file. It uses the standard library over HTTPS, so there is no new dependency, and without --pull the pull code is never imported and no connection is opened.
One timing note worth acting on: it reads the v2 Observations endpoint because /api/public/traces leaves Langfuse Cloud on 16 November 2026.
LangSmith, Helicone, OpenPipe and OpenAI Stored Completions have no live pull yet. Export them and pass --logs.
Output schema (frozen TraceRecord with MappingProxyType metadata):
{
"trace_id": "trace_abc123",
"prompt": "What is the capital of France?",
"output": "The capital of France is Paris.",
"source": "langfuse",
"signal": "none",
"metadata": {"user_id": "user_789", "timestamp": "2026-05-20T..."}
}Feeds directly into v0.26 soup data from-traces for preference-pair building.
soup prune-prompt — system-prompt mining
soup prune-prompt --input ./traces.jsonl --output ./traces_pruned.jsonl --min-frequency 0.95Detects the longest shared system-prompt prefix across rows via binary search over up to 32 candidate lengths (v0.63 fixed an O(N²) early-exit bug from the prototype). Strips it from training data so the fine-tuned model internalizes the boilerplate instead of repeating it — OpenPipe's signature trick.
Pass --tokenizer <model> and the prefix is found over token ids rather than characters. That is not a refinement: a character-level strip can cut a BPE multi-byte sequence in half when the shared prefix ends mid-token, and the tokenizer-aware mode finds the longest shared token-id prefix and decodes what remains. --slack-url and --discord-url post the result through the same SSRF-hardened webhook validator the rest of the CLI uses.
Two-pass file read; capped at 100k rows to prevent DoS.
soup data active-sample — uncertainty sampling
soup data active-sample --input ./prod_traces.jsonl --budget 200Two modes auto-detected from the JSONL schema:
- Single score — max-entropy on
rm_score(peaks at 0.5). - Dual scores — pairwise disagreement on
rm_scoreslist.
Output is a drop-in eval prompt set for human judging.
soup ab — Wald-SPRT sequential A/B
soup ab --input ./ab_results.jsonl --metric judge_score \
--alpha 0.05 --beta 0.20 --effect-size 0.1--slack-url and --discord-url post the verdict through the same SSRF-hardened validator the rest of the CLI uses, and the interesting half is when they stay quiet: the webhook fires only when the test actually decides, accepting or rejecting the null. A run that is still gathering evidence says nothing. The same pair is on soup ingest, soup prune-prompt and soup data active-sample.
Wald's SPRT (Sequential Probability Ratio Test) is a martingale under the null hypothesis — Type-I error is controlled at every stopping time, not just at a fixed sample size. Early-stops when the log-likelihood ratio crosses A = log((1-β)/α) (reject H0) or B = log(β/(1-α)) (accept H0).
Input rows (the numeric field must be named after --metric, here judge_score):
{"arm": "control", "judge_score": 7.4}
{"arm": "treatment", "judge_score": 8.2}Decisions: reject_h0 / accept_h0 / continue. v0.63 ships a CRITICAL fix for a sign error in the historical mSPRT implementation.
Three metrics at launch: latency, judge_score, retry_rate.
What the test needs from the file:
armmust be exactlycontrolortreatment. A row with any other arm, a non-numeric value in the metric field, or invalid JSON is skipped without a message.- The verdict stays
continueuntil each arm has at least two rows, and also when both arms have zero variance, because with no measurement noise the test cannot bound its error. --effect-sizeis in the metric's own units, not a fraction.retry_rateshould be pre-aggregated per prompt: the test assumes roughly Gaussian values, so feed it per-prompt rates rather than a 0/1 per attempt.- The exit code is 0 for every decision,
continueandreject_h0included. Unlikesoup drift-alarm(exit 3 on drift) it is not a CI gate by exit code; read thedecisionfield.
soup drift-alarm — KL drift watch with webhooks
soup drift-alarm \
--reference ./ft_output_dist.jsonl --live ./prod_output_dist.jsonl \
--threshold 0.2 \
--slack-url "https://hooks.slack.com/services/T.../B.../..."Rolls KL divergence between word-frequency snapshots of two JSONL files: model outputs from fine-tune time (--reference) and from production (--live). Each row is one model output; Soup splits it on whitespace and compares the two distributions (a word missing from one side gets a small epsilon, so the divergence stays finite). It surfaces both behavioral drift ("model now outputs JSON") and vocabulary drift ("same 20 phrases repeated"). There is no pluggable tokenizer: whitespace splitting is the only mode, and the soup drift-alarm options are --reference, --live, --threshold (default 0.2), --slack-url and --discord-url. Both files must sit under the working directory.
Input rows (one per model output; the text may be under output, response or text):
{"output": "The capital of France is Paris."}Check n_reference and n_live in the panel. A row without one of those string fields is skipped silently, and when either file yields no usable rows the command reports KL 0.0000, OK and exit 0. A file of the wrong shape (for example per-token {"token": ..., "log_prob": ...} rows) therefore looks like a healthy model, which is the worst way for an alarm to fail.
Optional Slack / Discord webhooks fire on drift and are SSRF-validated: plain HTTP is accepted only for loopback, HTTPS hosts are accepted, and private, link-local, cloud-metadata and shared-space (100.64.0.0/10) IP addresses are rejected on both schemes. Hostnames are not resolved. Exit code 3 on drift for cron-friendly automation.
Numbers
+219 new tests in v0.63.0 (9816 → 10035). Security: 1 CRITICAL (mSPRT sign error → Wald SPRT), 2 HIGH, 3 MEDIUM, 2 LOW.
See also
- Soup loop — the
HarvestFnhalf of the v0.58 production flywheel is now powered bysoup ingest. - Trace-to-preference — convert normalised traces into DPO / KTO pairs.
- Eval design — derive a goal-conditioned eval suite from the active-sampled prompts.
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.