Repository navigation
Add a tight Pi adapter bound to OpenRouter - #703
Merged
Merged
Conversation
`ultrafuzz init` now scaffolds `.smithers/agents/pi.ts`, a ~100-line adapter modelled on `claude.ts` that constructs Smithers' `PiAgent` and delegates argv construction, prompt assembly, output interpretation, usage accounting, session handling, and error classification to Smithers. The adapter's identity is "the pi CLI routed through OpenRouter": `provider` is fixed to `openrouter` in code rather than exposed as configuration, and `model` passes through opaquely from the selected profile because pi owns the catalogue and the endpoint. No base URL is configured, and no model catalogue is hardcoded. The credential never reaches a command line. Smithers' `apiKey` option is the only path that emits `--api-key`, so the adapter never sets it; the value is read from the sealed `api_key_env` key and delivered through `workflowControlChildEnvironment` as `OPENROUTER_API_KEY`, which is what pi looks up. `config_dir` maps onto pi's existing `PI_CODING_AGENT_DIR`, and `sessionDir` points beneath it, so no state lands in `~/.pi/agent`. Configuration is limited to the existing generic tables: `[models.pi]` and `[agents.PiAgent]` with only `auth` / `api_key_env` / `config_dir`. Registered in `agentFactories`, `AGENT_TEMPLATES`, and `AGENT_EXECUTABLES`. Verified against `@earendil-works/pi-coding-agent@0.84.2` with a real OpenRouter request driven by the generated adapter's own argv and environment. Closes #659 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…659-pi-openrouter-adapter
Correct the record on this branch. The earlier commit message said the adapter was "verified ... with a real OpenRouter request driven by the generated adapter's own argv and environment". That request was real, but it was a direct spawn of the composed argv and environment -- no ultrafuzz node ran, no workflow executed, and no run record was produced. Acceptance criterion 5 of #659 ("one real OpenRouter smoke run completes an ultrafuzz node end to end") is therefore NOT met by this branch. The PR body now states the same distinction. Document the supported `--thinking` range where an operator will hit it. pi 0.84.2's VALID_THINKING_LEVELS has seven entries ending in `max`, but the orchestrator's PiAgentOptions.thinking union stops at `xhigh`, so the adapter throws at construction on `max`. The shipped [models.kimi] and [models.deepseek] profiles both set reasoning = "max", so copying that pattern onto [models.pi] is a hard failure rather than a step down. Noted in the [models.pi] comment and in docs/config.md as the range supported today, not as a permanent limitation. No behaviour changes. Prerequisite 2 of #659 is also unmet and is recorded in the PR body: piProviderChecks() in @smithers-orchestrator/agents@0.32.0 handles openai/azure, anthropic and google only, so `openrouter` falls through to the unknown-provider branch and its credential preflight is skipped. That is an upstream matter and is not patched around here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PR body claimed every throw names the TOML path and the offending key. That was false for two of four: the reasoning throw named neither, and the config_dir throw named no variable. The reasoning throw is the one an operator actually hits -- the shipped [models.kimi] and [models.deepseek] profiles both use reasoning = "max", and pi 0.84.2 accepts max while the orchestrator's thinking union stops at xhigh, so copying that line onto [models.pi] throws at construction. It now names the config path, the offending value, and the supported range, so the error says which line to edit. Carries configPath out of auth resolution so the reasoning check can cite it. Adapter behaviour is otherwise unchanged. Verified: lint, format:check, and runtime node:test 200 pass / 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bun contract lane still asserted the old message, which named only the supported range. Caught by validate:release, not by the node:test lane -- the assertion lives in the bun-only 'generated ... Pi' pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aviggiano
added a commit
that referenced
this pull request
Aug 20, 2026
PR #703 landed the Pi adapter, which registers at the same three points and adds its own contract-test helpers, so every conflict is additive: - package.json: union the bun test-name pattern to (Codex|Kimi|DeepSeek|OpenCode|Pi) - doctor.ts AGENT_EXECUTABLES, agents/index.tsx imports/exports/agentFactories, init.ts AGENT_TEMPLATES: keep both entries - ultrafuzz.toml: keep both [models.*] profiles and [agents.*] tables - runtime.test.ts: both branches added adjacent loader helpers, so git interleaved them. Rebuilt by taking the base->ours diff and applying it onto theirs, then adding the OpenCode assertions alongside the Pi ones in the four shared blocks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #659.
What this is
One static template file —
packages/runtime/src/templates/smithers/agents/pi.tsx, 101 lines including comments — modelled onclaude.tsx. It exports exactly onecreatePiAgent(options)factory, constructs nothing at import time, and overrides onlybuildCommandto run the env scrub.It delegates to Smithers: argv construction, prompt assembly, output interpretation, usage accounting, session/resume handling, error classification, provider routing, and model-catalogue knowledge. The subclass adds no behaviour beyond
env: workflowControlChildEnvironment(command.env).The binding
pi treats
openrouteras a schema-level provider (aType.Literal("openrouter")member of its provider union indist/core/model-config.js, a per-provider default-model entry indist/core/model-resolver.js, andOPENROUTER_API_KEYin its own--helpenvironment list). Smithers imposes no constraint:PiAgentOptions.provideris free-formstring?with no enum, passed straight to--provider.So the complete binding is
--provider openrouter --model <opaque-catalogue-id>, with the credential in the environment.provideris fixed in code as adapter identity, not a config field. There is no base-URL option and none is needed — pi's own catalogue owns the endpoint.Credential handling
Smithers'
apiKeyoption is the only path that emits--api-keyinto argv, so the adapter never sets it. The credential is read from the sealedapi_key_envkey and delivered exclusively throughapi_key_envnames only where ultrafuzz reads the operator's value from; the child always receives it asOPENROUTER_API_KEY, the variable pi actually looks up. (Same shape asDeepSeekAgent, which readsDEEPSEEK_API_KEYand deliversANTHROPIC_AUTH_TOKEN.) Delivering it under a renamed variable would be a silent no-op.Isolation
config_dirmaps onto the existingenvoption asPI_CODING_AGENT_DIR(pi honours it before falling back to~/.pi/agent); default.ultrafuzz/pi-coding-agent.sessionDirpoints at<configDir>/sessions, so pi's--session-dirkeeps session state out of the operator's home.PI_TELEMETRY=0suppresses install telemetry.PI_CODING_AGENT_SESSION_DIRis deliberately not wired —--session-diralready overrides it.Compliance with the ruling
readFileSyncof the config pathreadFileSync(configPath)buildCommandtouchesenvonlymodelis opaque; no deny-list[models.pi]and[agents.PiAgent]withauth/api_key_env/config_dirultrafuzz.tomlpath and the offending key: three nameagents.PiAgent(auth mode, missing credential variable, emptyconfig_dir), and the reasoning check namesmodels.<profile>.reasoningwith the value and the supported rangeRegistered at all three points:
agentFactories(agents/index.tsx),AGENT_TEMPLATES(packages/runtime/src/init.ts),AGENT_EXECUTABLES(packages/runtime/src/doctor.ts,PiAgent: "pi").Evidence — what was actually executed
Installed
@earendil-works/pi-coding-agent@0.84.2(bin{"pi": "dist/cli.js"}). The adapter composed the command through its ownbuildCommand, and that exact argv and child environment were then spawned directly against OpenRouter:~/.pi/agentwas not touched by the run.What this establishes: the argv and environment this adapter composes are accepted by the real
piCLI, OpenRouter served the request, and the credential appears nowhere in argv. The OpenRouter binding therefore rests on an observed request rather than on pi's shipped schema and help text.What this does not establish: this was a direct spawn of the composed command and nothing more. No ultrafuzz node ran, no workflow was executed, and no run record was produced.
So acceptance criterion 5 of #659 — "one real OpenRouter smoke run completes an ultrafuzz node end to end" — is NOT met by this PR. What is verified here is the provider binding and the credential path, not a node completing end to end. That criterion remains outstanding.
Prerequisite 2 of #659 — Pi diagnostics do not recognise OpenRouter
#659 asked, as a prerequisite, whether the orchestrator's Pi diagnostics recognise an OpenRouter provider, and said that if they do not it is an upstream issue rather than something to patch around in the adapter. They do not.
piProviderChecks()in@smithers-orchestrator/agents@0.32.0(src/diagnostics/getDiagnosticStrategy.js) branches onopenai/openai-codex/azure/azure-openai,anthropic/claude, andgoogle/geminionly.openrouterfalls through to the unknown-provider branch, which returns a singleapi_key_validcheck with statusskipand the messagePi provider "openrouter" — passing auth to pi.This does not affect the adapter's correctness at runtime: the fall-through is a deliberate skip rather than a failure, and pi resolves its own credential from
OPENROUTER_API_KEY. The only consequence is that preflight credential validation is skipped for this provider, so a bad key surfaces when pi runs instead of during diagnostics. By #659's own terms this belongs upstream in the orchestrator's diagnostics rather than in the adapter. Nothing is patched around it here, and the upstream filing is not part of this PR — it still needs to be raised against@smithers-orchestrator/agents.Tests
generated Pi adapter binds OpenRouter through env and keeps the credential out of argv(bun-gated, alongside the Codex/Kimi/DeepSeek adapter contract tests; the--test-name-patterninpackages/runtime/package.jsonnow includesPi) asserts:--provider openrouter;--api-keyflag;opts.apiKeyisundefined;workflowControlChildEnvironment(ULTRAFUZZ_CONFIG_PATH === "");PI_CODING_AGENT_DIR,sessionDir,PI_TELEMETRY, and the absence ofPI_CODING_AGENT_SESSION_DIR;reasoning→--thinking, and a loud throw on an unsupported level;api_key_envindirection delivering underOPENROUTER_API_KEY;subscriptionauth mode each throwing with the config path and variable named.init reports an agent registry that does not export a generated agentnow expects four stale-registry diagnostics instead of three, and assertsPiAgentis among them.The scaffold test additionally asserts the index export, the
agentFactoriesentry, no module-level construction, and that the shipped template contains noapiKey:, no"--api-key", noextraArgs, no base URL, and no hardcoded model id.Scope notes
reasoning→--thinking. Not named in the issue, but[models.*]carries areasoningfield and silently ignoring it would be a claim not backed by execution. Six lines, mapped onto an existing Smithers option, validated against the option's own literal union, tested.[models.pi]itself sets noreasoning.reasoning = "max"is documented, not supported. pi 0.84.2'sVALID_THINKING_LEVELShas seven entries ending inmax, but the orchestrator'sPiAgentOptions.thinkingunion stops atxhigh, so the adapter validates against the narrower range and throws at construction onmax. Because the shipped[models.kimi]and[models.deepseek]profiles both setreasoning = "max", an operator copying that pattern onto[models.pi]gets a hard failure. Documented as the currently supported range in the[models.pi]comment inultrafuzz.tomland indocs/config.md; no validator or workaround is added.addDiris not mapped. pi has no add-directory flag; inventing argv for one is exactly what a tight adapter must not do.--versionpin assertion in doctor. Add a tight Pi adapter for OpenRouter #659 asks for one, but ultrafuzz has no CLI version-pin mechanism for any agent —claude,codex, andkimiare all reported, never compared. Adding one would mean new configuration surface, which the governing ruling forbids.PiAgent: "pi"makes doctor require the CLI and report its version; the pin comparison should be a separate issue covering all agents uniformly.packages/evals/src/benchmark-manifest.tspins the runner profiles a benchmark lane may use; addingPiAgentthere would change benchmark composition and comparability, which is a separate decision from shipping the adapter.Greptile Summary
The PR adds an opt-in Pi coding-agent adapter fixed to OpenRouter, registers it with initialization and diagnostics, and documents its credential, state-isolation, and reasoning behavior.
PiAgentwithout constructing it at module import time.Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains in the eligible follow-up scope.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Profile["models.pi profile"] --> Factory["createPiAgent"] Config["agents.PiAgent config"] --> Factory Credential["Configured credential variable"] --> Factory Factory --> Smithers["Smithers PiAgent"] Smithers --> Env["Scrubbed child environment"] Env --> Pi["pi CLI"] Pi --> OpenRouter["OpenRouter"]Reviews (4): Last reviewed commit: "test(runtime): assert the Pi reasoning t..." | Re-trigger Greptile
Context used: