// Article · September 26, 2026 · 17 min read
SoL-Pi Isn't a Wrapper. Here's How to Use It Anyway.
NVIDIA's token-cutting release is four extensions for one specific open-source harness, Pi. You can run it as shipped with Claude or OpenAI models, or you can port its ideas into Claude Code and Codex. We read the source, checked the numbers against the paper, and tested two of the four mechanisms inside a live Claude Code session.
// Contents
Start with a correction, including to our own coverage. SoL-Pi does not wrap your coding agent in a research loop. The auto-research loop is the method NVIDIA used to discover the mechanisms. Agents proposed 152 candidate efficiency ideas, tested them on 535 executable environments, and threw away every idea that cost capability. The product is what survived: four mechanisms, shipped as an MIT-licensed TypeScript extension for Pi, the open-source coding agent formerly developed as badlogic/pi-mono.
The repository says it plainly: SoL-Pi is "a standalone extension for Pi", it "installs on top of an unmodified Pi release", and "every mechanism is opt-in and disabled by default" (NVlabs/SoL-Pi README).
That distinction decides the question we set out to answer: how do you use SoL-Pi with Claude and with Codex? There are two honest answers:
- Run it as shipped. Use Pi as your harness with SoL-Pi on top, pointed at a Claude model or an OpenAI model. This is what NVIDIA benchmarked.
- Port the ideas. Rebuild the mechanisms inside Claude Code or the Codex CLI using their hook systems. This is what most teams would actually want. It is partly possible, and the gaps are instructive.
This piece covers both. We cloned the repository at commit 1559b5c (22 September 2026), read the mechanism source, checked the headline numbers against the paper, and ran two of our Claude Code ports in a live session. Where we didn't test something, we say so.
Part One: What's actually in the box
The four mechanisms each attack a different kind of waste. The table below uses NVIDIA's own descriptions and adds the thresholds from the source code.
| Mechanism | What it removes | How it works (from the source) |
|---|---|---|
| Action Fusion | The model turn between "edit a file" and "run the tests" | Replaces Pi's edit and write tools with versions that take an optional then_run command. The harness applies the edit, checks with a hash that nothing else changed the file, runs the command, and returns one combined result. |
| ObservationPack | Re-sending large tool output on every later request | Any text tool result over 10 KB goes to the model in full for its first 2 requests. After that it is replaced by a short placeholder with a stable ID, and the agent pulls exact pages back with an obs_recall tool. The original bytes are archived locally. |
| Evidence-Preserving Reducer | Paying a frontier model to read a 40,000-line build log | Diagnostic output over 4 KB from commands like pytest, cargo test and make is handed to a cheaper model. By default that is gpt-5.6-luna over the openai-codex route. The cheap model returns a "receipt" of at most 12 quotations. The receipt is accepted only if every quotation matches the archived log exactly, and anything that looks like a secret is skipped. |
| Online Context Compact | Compacting too late, or compacting when it doesn't pay | The agent keeps a plan through an update_plan tool. Each completed step is a candidate point for compaction. Compaction runs only if the saved cache reads pay back the cost of writing the new cache before the task ends, or if the context window is about to overflow. |
Two design rules make this more than a bag of tricks. Nothing is destroyed: originals stay on disk, and if a reducer check fails the agent simply sees the unreduced output. ObservationPack never edits stored history. It rewrites only the copy of the conversation sent to the model, through Pi's context event.
That second rule is the most important detail for anyone porting SoL-Pi. It is also why the config file contains a pricing number.
The prompt-cache catch
Swapping an old tool result for a placeholder changes the middle of the conversation. Claude and OpenAI models both cache prompts by prefix, so changing an earlier message invalidates the cache for everything after it. Anthropic's docs state it directly: modifications at each level "invalidate that level and all subsequent levels" (Anthropic, prompt caching). Every pack, and every compaction, triggers a fresh cache write.
SoL-Pi prices this with one setting, cacheWriteReadRatio: how much more a cache write costs than a cache read. Online Context Compact's decision rule, from economics.ts, is:
compact when
write_tokens × (ratio − 1) ÷ tokens_saved_per_request≤ expected remaining requests
The default of 12.5 follows OpenAI's GPT-5.6 Sol pricing as of 21 August. It also happens to match older Claude models (1.25× for a five-minute cache write ÷ 0.1× for a read). It is wrong for current Claude models, because Anthropic charges less for cache reads on its newest models:
| Model | 5-min write | Read | cacheWriteReadRatio |
|---|---|---|---|
| GPT-5.6 Sol (SoL-Pi default) | — | — | 12.5 |
| Claude Opus 5 / Sonnet 5 (0.1× read) | 1.25× | 0.1× | 12.5 |
| Claude Opus 5.5 | 1.25× | 0.05× | 25 |
| Claude Fable 5.1 | 1.25× | 0.025× | 50 |
Multipliers from Anthropic's prompt-caching docs. With a one-hour cache TTL the write multiplier is 2×, so double the ratio.
A higher ratio makes each compaction more expensive to earn back, so SoL-Pi compacts less often. If you run SoL-Pi on Opus 5.5 with the default setting, it will compact more often than your pricing justifies.
Part Two: The numbers, read carefully
The paper (arXiv 2609.20519, submitted 17 September) states the headline precisely. On the 51-task EdgeBench evaluation, SoL-Pi "achieves performance comparable to Pi across GPT-5.6 Sol and Opus 5 while reducing recorded token traffic by 44.7–49.0% and API cost by about one third."
Three things in that sentence got lost in last week's coverage, ours included:
- The comparison is against Pi, not against Claude Code or Codex. Pi is already a lean harness.
- Tokens fell by about half. Cost fell by about a third. Cached tokens are cheap, so removing them saves less money than the token count suggests.
- "Comparable" means about 94% of Pi's average score. NVIDIA's capability floor allowed each mechanism a small tolerated loss, and those losses add up.
The comparison against the vendors' own harnesses is where it gets interesting. These are the EdgeBench figures as reported by MarkTechPost from the paper:
| Harness | Model | Tokens | API cost | Avg. score |
|---|---|---|---|---|
| Codex | GPT-5.6 Sol | 3.05B | $1,787 | 34.7 |
| SoL-Pi | GPT-5.6 Sol | 1.10B | $894 | 42.0 |
| Claude Code | Opus 5 | 2.00B | $2,535 | 43.7 |
| SoL-Pi | Opus 5 | 1.31B | $1,158 | 42.2 |
The two rows tell different stories:
- On OpenAI's model, SoL-Pi beat Codex outright: 64% fewer tokens, half the cost, and a higher score.
- On Claude, it was a trade: 35% fewer tokens and 54% lower cost than Claude Code, for 1.5 points less score.
That 64% is where one exchange-news headline got its "64%" figure. It is real, but it is the best case against the weakest baseline.
Caveats before you promise anyone these numbers:
- Terminal-Bench 4 went the other way. On 63 CPU-only tasks, SoL-Pi solved 15 while Pi and Codex each solved 18, although SoL-Pi was cheapest at $211 against $286 and $272 (SoL-Pi blog). The authors' explanation is plausible: on short tasks there's little context replay to remove. But it means the savings depend on the workload.
- Part of the evaluation set was used during selection. Coverage of the paper reports 11 EdgeBench tasks used for acceptance and 40 held out.
- A token-accounting critique is still open and untested. One public critique argues that three of the four mechanisms move tokens out of the measured context rather than removing them. For example, the reducer's own reading happens in a separate context. The critique predicts under 20% savings under full provider-side accounting, but it is a hypothesis with no experiment run (jjakimoto/research-issues#1625). We couldn't confirm from public material whether the paper's cost figure includes the reducer's calls. The dollar figure is the one to trust over the token figure, because a provider bill is harder to game.
- We found no independent reproduction as of 26 September.
What this does to last week's "compounding" argument: our W39 briefing stacked a 50% price cut on a 49% traffic cut and got roughly 75% off the cost of an agentic task. The honest SoL-Pi input is about a third off cost against Pi, and only on long tasks. The direction holds; the size was overstated.
Part Three: Route 1 — run SoL-Pi as shipped
If you want NVIDIA's measured results, use NVIDIA's measured setup: Pi 0.85.1 with SoL-Pi on top. Pi supports about 33 providers, including Anthropic and OpenAI, and SoL-Pi leaves model choice to Pi.
# Requires Node.js >= 22.19. Check first — 22.17 is not enough.
npm install --global --ignore-scripts @earendil-works/pi-coding-agent@0.85.1
pi install git:github.com/NVlabs/SoL-Pi --local --approve # per-project
Pin the version. Pi's npm latest is already 0.87.1, and SoL-Pi is tested against 0.85.1 and 0.84.2 only. NVIDIA's install protocol treats any other version as "a compatibility change" that needs the full test suite re-run (agents-install.md).
Then create .pi/sol-pi.json. Start with NVIDIA's own conservative preset. It turns on only the two mechanisms that make no extra model calls and never interrupt a run:
{
"version": 1,
"actionFusion": true,
"observationPack": true,
"evidencePreservingReducer": false,
"onlineContextCompact": false,
"cacheWriteReadRatio": 12.5
}
A nice detail: the repository ships its own CLAUDE.md and AGENTS.md. You can open the SoL-Pi checkout in Claude Code or Codex and ask it to install SoL-Pi, and it will follow NVIDIA's four-phase protocol: validate, install, configure, verify. That's the only sense in which SoL-Pi "supports" Claude Code and Codex out of the box. They are the installers, not the harness.
With Claude models
Pi selects models with --provider / --model or with defaultProvider / defaultModel in its settings (Pi CLI docs). The catch is authentication.
Pi ships a /login flow for Claude Pro/Max subscriptions. Don't use it. Anthropic's terms are explicit: subscription OAuth "is designed to support ordinary use of Claude Code and other native Anthropic applications," and Anthropic "does not permit third-party developers to offer Claude.ai login into their own applications, or to route requests through Free, Pro, or Max plan credentials" (Claude Code legal and compliance). Enforcement against third-party harnesses has been reported since April.
In practice, SoL-Pi on Claude means a metered ANTHROPIC_API_KEY. That changes the economics. If you're on a Max plan, Claude Code costs you a flat fee, and moving to pay-per-token Pi to save a third of the per-token cost may be a net loss. SoL-Pi pays off for Claude users who are already on API billing.
Two settings to change for Claude:
- Set
cacheWriteReadRatiofrom the table above: 25 for Opus 5.5. - If you enable the reducer, point
evidencePreservingReducerProviderandevidencePreservingReducerModelat a cheap model you actually have credentials for. The default reducer route is OpenAI's, and when it's unavailable the reducer quietly passes logs through unreduced. You'd lose the saving without any error.
With OpenAI / Codex models
This is the path of least resistance, because it is the setup NVIDIA built around. The default reducer is openai-codex/gpt-5.6-luna, and the default cache ratio is Sol's. Pi's /login includes an "OpenAI Codex (ChatGPT OAuth)" flow.
OpenAI's position on third-party harnesses is friendlier than Anthropic's. A Codex lead publicly endorsed ChatGPT sign-in in outside harnesses and said about 5% of Codex production traffic already runs through Pi. That is a public statement, not a contract (Manifest summary, 1 July).
For a Codex user, Route 1 is a defensible choice. It is also the one configuration where SoL-Pi beat the vendor harness on score.
Part Four: Route 2 — port it into Claude Code
Most Claude users won't switch harnesses. The practical question is how much of SoL-Pi Claude Code's hook system can reproduce. The answer is more than we expected, with one structural gap:
| Mechanism | Claude Code equivalent | Fidelity |
|---|---|---|
| Action Fusion | PostToolUse hook on Edit|Write returning additionalContext |
High. Tested live. The command comes from a file-type rule rather than the model's choice. |
| ObservationPack | PostToolUse hook on Bash returning updatedToolOutput |
Partial. Tested live. It packs on the first send, not after two. |
| Evidence-Preserving Reducer | Subagent pinned to a small model | Low. No exact-quote verification. |
| Online Context Compact | Compaction settings plus a PreCompact gate |
Low. Hooks can't trigger compaction. |
Action Fusion
Claude Code's PostToolUse hook can add text "to Claude's context alongside the tool result" through hookSpecificOutput.additionalContext (hooks reference). So after every edit, the harness can run the validation and hand back the result, and the model never spends a turn deciding to do it.
Map file patterns to commands in .claude/sol-fuse.json:
{ "*.py": "python -m py_compile {file}", "*.ts": "npx tsc --noEmit -p ." }
.claude/sol_fuse.py:
import fnmatch, json, subprocess, sys
from pathlib import Path
event = json.load(sys.stdin)
file_path = event.get("tool_input", {}).get("file_path", "")
cwd = Path(event.get("cwd", "."))
rules_file = cwd / ".claude" / "sol-fuse.json"
if not file_path or not rules_file.exists():
sys.exit(0)
rules = json.loads(rules_file.read_text())
command = next((c for p, c in rules.items() if fnmatch.fnmatch(Path(file_path).name, p)), None)
if command is None:
sys.exit(0)
command = command.replace("{file}", file_path)
proc = subprocess.run(command, shell=True, cwd=cwd, capture_output=True, text=True, timeout=120)
tail = "\n".join((proc.stdout + proc.stderr).strip().splitlines()[-60:])
status = "succeeded" if proc.returncode == 0 else f"failed (exit {proc.returncode})"
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PostToolUse",
"additionalContext": f"[then_run:{status}] `{command}`\n{tail}".strip()}}))
In our live test, a headless Claude Code session wrote a file containing a syntax error. The model received [then_run:failed (exit 1)] and the SyntaxError traceback attached to the write result. No extra turn.
The difference from SoL-Pi: there, the model chooses the follow-up command for each edit. Here, your rules choose it. Our version is more predictable, but it runs the check on every matching edit, including edits that are halfway through a multi-file change. Keep the commands fast: a syntax or type check, not the full test suite.
ObservationPack
Claude Code's PostToolUse also supports updatedToolOutput, which "replaces the tool's output with the provided value before it is sent to Claude." This works for built-in tools too, as long as the value matches the tool's output shape. For Bash that shape is stdout, stderr, interrupted and isImage (hooks reference).
What Claude Code does not have is SoL-Pi's key hook: something that rewrites earlier messages before each request. So a port can't do "send in full twice, then collapse." It has to decide once, when the output first arrives. We think that's the better trade for Claude anyway, because packing up front never invalidates the prompt cache.
.claude/sol_obspack.py, using SoL-Pi's own threshold and the head/tail sizes from its tuned configuration:
import hashlib, json, sys
from pathlib import Path
THRESHOLD, HEAD, TAIL = 10 * 1024, 2048, 1536
event = json.load(sys.stdin)
resp = event.get("tool_response") or {}
stdout, stderr = resp.get("stdout", ""), resp.get("stderr", "")
full = stdout + (f"\n[stderr]\n{stderr}" if stderr else "")
# Past Claude Code's ~30k read-back window, stdout is already cut; the complete
# output sits in the file named by persistedOutputPath (observed, undocumented).
persisted = resp.get("persistedOutputPath")
if persisted and Path(persisted).is_file():
data = Path(persisted).read_bytes()
full = data.decode("utf-8", "replace")
else:
data = full.encode("utf-8")
if len(data) <= THRESHOLD or resp.get("isImage"):
sys.exit(0)
digest = hashlib.sha256(data).hexdigest()[:24]
archive = Path(event.get("cwd", ".")) / ".claude" / "obs" / f"obs_{digest}.log"
archive.parent.mkdir(parents=True, exist_ok=True)
archive.write_bytes(data)
excerpt = (
f"[obs_{digest}: {len(data)} bytes, {full.count(chr(10)) + 1} lines archived at {archive}. "
f"Use Read with offset/limit to recall exact lines.]\n"
f"{data[:HEAD].decode('utf-8', 'ignore')}\n"
f"... [{len(data) - HEAD - TAIL} bytes elided] ...\n"
f"{data[-TAIL:].decode('utf-8', 'ignore')}"
)
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PostToolUse",
"updatedToolOutput": {"stdout": excerpt, "stderr": "",
"interrupted": resp.get("interrupted", False), "isImage": False}}}))
Claude's built-in Read tool does the work of obs_recall, because it already takes a line offset and limit.
The bug our live test caught. Claude Code already does a crude version of ObservationPack. Bash output over about 30,000 characters is saved to a session file, and the model gets a 2,000-character preview plus the file path (tools reference). The hook sees the output after that cut.
Our first version archived a 3,000-line output as 2,394 lines and silently lost the tail. Printing the raw hook payload showed two fields the documentation doesn't mention, persistedOutputPath and persistedOutputSize. The script above reads the full file from that path. After the fix, the archive matched persistedOutputSize exactly (37,890 bytes), and the model correctly reported the true last line.
Because those fields are undocumented, re-check this after Claude Code updates.
If you'd rather skip a script, the zero-code approximation is BASH_MAX_OUTPUT_LENGTH=10240. It moves Claude Code's own "save to file, show a preview" behavior down to SoL-Pi's threshold. You lose the tail excerpt, which is often where the error is.
Wire both hooks into .claude/settings.json:
{"hooks": {"PostToolUse": [
{"matcher": "Bash", "hooks": [{"type": "command", "command": "python .claude/sol_obspack.py"}]},
{"matcher": "Edit|Write", "hooks": [{"type": "command", "command": "python .claude/sol_fuse.py"}]}
]}}
Evidence-Preserving Reducer
A subagent pinned to a small model gets you the delegation. Put this in .claude/agents/log-reader.md with model: haiku in the frontmatter, and instruct it to return only verbatim lines with line numbers from the archived log.
What you don't get is SoL-Pi's defining property: the harness itself checks every quotation against the source and throws away the receipt if one doesn't match. Without that, you're trusting a fluent summary, which is exactly what the mechanism was built to avoid.
You could add a PostToolUse hook on the Agent tool to check the quotations. We haven't built or tested that, so treat this mechanism as unported.
Online Context Compact
This is the structural gap. SoL-Pi compacts at the moment a plan step finishes. Claude Code hooks cannot start a compaction. An independent Claude Code port, ImKK666/SoL-ClaudeCode, reached the same conclusion and calls this mechanism "not faithfully portable."
What you do have:
CLAUDE_AUTOCOMPACT_PCT_OVERRIDEto compact earlier than the default (env vars).- Manual
/compact <focus>at the natural boundaries of a task. - A
PreCompacthook that can block an automatic compaction it judges uneconomical, using the breakeven rule above. This only works for proactive compactions: blocking one triggered by a context-limit error makes the request fail (hooks reference). - For headless pipelines, the crude but effective version of the whole idea: one
claude -psession per plan step, with the plan carried forward in a file.
A note on that community port: SoL-ClaudeCode goes further than hooks. It runs a local man-in-the-middle proxy on api.anthropic.com to rewrite requests in flight, and self-reports −36.7% tokens and −22.2% cost across five paired runs. Its optional reducer "reuses the intercepted subscription token." That runs straight into the Anthropic terms quoted above. We wouldn't run it on a subscription account.
Part Five: Route 3 — port it into Codex
We verified these settings against OpenAI's current Codex config reference and hooks documentation. We did not run them.
Codex has its own hook system, turned on with [features] hooks = true, with hooks.json in ~/.codex/ or the repository's .codex/. Its events have a similar shape to Claude Code's, but one difference matters for this port.
- Action Fusion ports cleanly. A
PostToolUsehook matchingapply_patch(Codex also acceptsEditandWriteas matcher aliases) can returnhookSpecificOutput.additionalContext, the same pattern as above. Codex's edit payload is a patch rather than a file path, so the script needs to parse which files the patch touched. - ObservationPack is awkward. Codex's
PostToolUsecannot rewrite a tool result. The only documented lever isdecision: "block", which "replaces the tool result with that feedback." You can use it to swap in a handle and excerpt, but the model then sees the replacement as hook feedback, not as command output. The existing community port, raydez/sol-codex, avoids this: it routes heavy commands through an MCP tool that returns handles. - Native settings cover the rest roughly:
# Top-level keys must come before any [table] header.
# The limits below are starting points to tune, not recommendations.
tool_output_token_limit = 2500 # cap per-tool output stored in history
model_auto_compact_token_limit = 180000 # compact earlier
compact_prompt = "Summarize completed plan steps only; keep the active step, open errors and file paths verbatim."
[features]
hooks = true
multi_agent = true
[agents]
default_subagent_model = "gpt-5.6-luna" # cheap model for delegated log reading
Codex's PreCompact hook can stop a compaction with continue: false, so the economic gate ports as well as it does in Claude Code. Neither harness lets a hook start one.
For most Codex users, though, Part Three is the better answer. SoL-Pi was built on OpenAI models, ships with an OpenAI reducer, and OpenAI tolerates ChatGPT sign-in in Pi.
What to actually do
- Measure your own traffic before you touch anything. SoL-Pi's gains come from long runs. On Terminal-Bench's shorter tasks it solved 15 against Codex's 18. If your agent sessions are mostly five-minute fixes, the prize is small.
- On Codex or OpenAI API billing: try Route 1. Use Pi 0.85.1 with the conservative preset, compare a week of real tasks against Codex, and judge by the provider bill, not the token count.
- On Claude with a subscription: stay in Claude Code and add the two hooks above. They're cheap, reversible, and neither touches the prompt cache. Don't move to Pi through subscription login.
- On Claude with API billing: Route 1 is viable. NVIDIA measured 54% lower cost than Claude Code for 1.5 points of score. Set
cacheWriteReadRatiofor your model, and point the reducer at a model you have credentials for. - Treat 44.7–49% as NVIDIA's ceiling, against Pi, on long tasks, before independent replication. The durable lesson is architectural. Most of an agent's bill is re-reading things it has already seen, and every harness now exposes enough hooks to stop some of it.
Sources: NVlabs/SoL-Pi (commit 1559b5c, read in full); SoL-Pi paper, arXiv 2609.20519; SoL-Pi blog; MarkTechPost, 2026-09-21; Pi and Pi docs; Claude Code hooks, env vars, tools reference and legal and compliance; Anthropic prompt caching; OpenAI Codex config reference and hooks; ImKK666/SoL-ClaudeCode; raydez/sol-codex; jjakimoto/research-issues#1625; Manifest, 2026-07-01. The Claude Code hooks were tested with Claude Code in headless mode on Windows on 2026-09-26. The Codex configuration was checked against documentation only.
// Related
September 25, 2026 · 11 min
OpenAI Cut Token Prices 50%. Every H1 Business Case Is Now Mispriced.
July 18, 2026 · 12 min
Kimi K3 vs Claude Code vs Codex Sol: a practical guide to the three agentic CLIs
September 25, 2026 · 3 min
Devices & Robotics — W39: the reflex-plus-planner robot brain shows up in Minecraft, and the voice tier gets its own model