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

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

SourceEnv varNotes
langfuseLANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEYDashboard export, or a live pull (below)
langsmithLANGSMITH_API_KEYLangSmith API traces
heliconeHELICONE_API_KEYHelicone observability
openpipeOPENPIPE_API_KEYOpenPipe production traces
otelOTEL_EXPORTER_OTLP_HEADERSOpenTelemetry OTLP
openai-storedOPENAI_API_KEYOpenAI 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)

bash
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...

soup ingest --source langfuse --pull --since 24h --output traces.jsonl

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

FlagDefaultNotes
--pulloff--source langfuse only
--since7d30m, 24h, 7d; maximum 365d
--max-pages100Pages of 100 generations. Maximum 10,000
--allow-private-hostoffFor 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-url uses. Redirects are refused rather than followed with credentials attached.
  • 30 s per request, 64 MiB per response.
  • Hitting --max-pages exits 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 --output untouched.
  • 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):

json
{
  "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

bash
soup prune-prompt --input ./traces.jsonl --output ./traces_pruned.jsonl --min-frequency 0.95

Detects 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

bash
soup data active-sample --input ./prod_traces.jsonl --budget 200

Two modes auto-detected from the JSONL schema:

  • Single score — max-entropy on rm_score (peaks at 0.5).
  • Dual scores — pairwise disagreement on rm_scores list.

Output is a drop-in eval prompt set for human judging.

soup ab — Wald-SPRT sequential A/B

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

json
{"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:

  • arm must be exactly control or treatment. 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 continue until 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-size is in the metric's own units, not a fraction.
  • retry_rate should 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, continue and reject_h0 included. Unlike soup drift-alarm (exit 3 on drift) it is not a CI gate by exit code; read the decision field.

soup drift-alarm — KL drift watch with webhooks

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

json
{"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 HarvestFn half of the v0.58 production flywheel is now powered by soup 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.