Skip to content

Add a tight Pi adapter bound to OpenRouter - #703

Merged
aviggiano merged 5 commits into
release/v0.1.0from
feat/issue-659-pi-openrouter-adapter
Aug 20, 2026
Merged

aviggiano merged 5 commits into
release/v0.1.0from
feat/issue-659-pi-openrouter-adapter

Conversation

@aviggiano

@aviggiano aviggiano commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #659.

What this is

One static template file — packages/runtime/src/templates/smithers/agents/pi.tsx, 101 lines including comments — modelled on claude.tsx. It exports exactly one createPiAgent(options) factory, constructs nothing at import time, and overrides only buildCommand to 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 openrouter as a schema-level provider (a Type.Literal("openrouter") member of its provider union in dist/core/model-config.js, a per-provider default-model entry in dist/core/model-resolver.js, and OPENROUTER_API_KEY in its own --help environment list). Smithers imposes no constraint: PiAgentOptions.provider is free-form string? with no enum, passed straight to --provider.

So the complete binding is --provider openrouter --model <opaque-catalogue-id>, with the credential in the environment. provider is 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' apiKey option is the only path that emits --api-key into argv, so the adapter never sets it. The credential is read from the sealed api_key_env key and delivered exclusively through

workflowControlChildEnvironment({ [PI_CREDENTIAL_ENV]: workflowControlCredentialValue(value, name), ... })

api_key_env names only where ultrafuzz reads the operator's value from; the child always receives it as OPENROUTER_API_KEY, the variable pi actually looks up. (Same shape as DeepSeekAgent, which reads DEEPSEEK_API_KEY and delivers ANTHROPIC_AUTH_TOKEN.) Delivering it under a renamed variable would be a silent no-op.

Isolation

config_dir maps onto the existing env option as PI_CODING_AGENT_DIR (pi honours it before falling back to ~/.pi/agent); default .ultrafuzz/pi-coding-agent. sessionDir points at <configDir>/sessions, so pi's --session-dir keeps session state out of the operator's home. PI_TELEMETRY=0 suppresses install telemetry. PI_CODING_AGENT_SESSION_DIR is deliberately not wired — --session-dir already overrides it.

Compliance with the ruling

Hard limit Status
no fs beyond readFileSync of the config path only readFileSync(configPath)
no authoring the harness's own config file writes nothing
no argv rewriting buildCommand touches env only
no token math / no session-id capture none
no network calls none
no hardcoded model catalogue or vendor env-var deny-list model is opaque; no deny-list
NO NEW CONFIG SCHEMA only [models.pi] and [agents.PiAgent] with auth / api_key_env / config_dir
fails loudly naming the TOML path and env var all four throws name the resolved ultrafuzz.toml path and the offending key: three name agents.PiAgent (auth mode, missing credential variable, empty config_dir), and the reasoning check names models.<profile>.reasoning with the value and the supported range

Registered 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 own buildCommand, and that exact argv and child environment were then spawned directly against OpenRouter:

argv: ["--print","--provider","openrouter","--model","openai/gpt-mini-latest",
       "--session-dir",".../.ultrafuzz/pi-coding-agent/sessions",
       "Reply with exactly the word ULTRAFUZZOK and nothing else."]
argv contains credential: false
argv contains --api-key: false
child env OPENROUTER_API_KEY set: true
child env ULTRAFUZZ_CONFIG_PATH: ""          # controller-only variable blanked
child env PI_CODING_AGENT_DIR: .../.ultrafuzz/pi-coding-agent
exit: 0
stdout: "ULTRAFUZZOK"
session provider evidence: openrouter

~/.pi/agent was not touched by the run.

What this establishes: the argv and environment this adapter composes are accepted by the real pi CLI, 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 on openai / openai-codex / azure / azure-openai, anthropic / claude, and google / gemini only. openrouter falls through to the unknown-provider branch, which returns a single api_key_valid check with status skip and the message Pi 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-pattern in packages/runtime/package.json now includes Pi) asserts:

  • the complete argv, byte for byte, including --provider openrouter;
  • no credential value anywhere in argv and no --api-key flag;
  • opts.apiKey is undefined;
  • the credential is present in the composed child env, and both env layers went through workflowControlChildEnvironment (ULTRAFUZZ_CONFIG_PATH === "");
  • PI_CODING_AGENT_DIR, sessionDir, PI_TELEMETRY, and the absence of PI_CODING_AGENT_SESSION_DIR;
  • reasoning → --thinking, and a loud throw on an unsupported level;
  • api_key_env indirection delivering under OPENROUTER_API_KEY;
  • a missing credential and a subscription auth mode each throwing with the config path and variable named.

init reports an agent registry that does not export a generated agent now expects four stale-registry diagnostics instead of three, and asserts PiAgent is among them.

The scaffold test additionally asserts the index export, the agentFactories entry, no module-level construction, and that the shipped template contains no apiKey:, no "--api-key", no extraArgs, no base URL, and no hardcoded model id.

Scope notes

  • Profile reasoning → --thinking. Not named in the issue, but [models.*] carries a reasoning field 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 no reasoning.
  • reasoning = "max" is documented, not supported. 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 validates against the narrower range and throws at construction on max. Because the shipped [models.kimi] and [models.deepseek] profiles both set reasoning = "max", an operator copying that pattern onto [models.pi] gets a hard failure. Documented as the currently supported range in the [models.pi] comment in ultrafuzz.toml and in docs/config.md; no validator or workaround is added.
  • addDir is not mapped. pi has no add-directory flag; inventing argv for one is exactly what a tight adapter must not do.
  • No --version pin 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, and kimi are 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.
  • The eval benchmark lane allowlist is untouched. packages/evals/src/benchmark-manifest.ts pins the runner profiles a benchmark lane may use; adding PiAgent there 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.

  • Generates and registers PiAgent without constructing it at module import time.
  • Delivers the OpenRouter credential through the child environment rather than command-line arguments.
  • Adds Pi command detection, scaffold coverage, adapter contract tests, and default configuration documentation.

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

Filename Overview
packages/runtime/src/templates/smithers/agents/pi.tsx Adds the Pi factory, OpenRouter provider binding, environment-only credential delivery, isolated state directories, and thinking-level validation.
packages/runtime/src/init.ts Registers the Pi template in project scaffolding while preserving any pre-existing operator-owned pi.ts file.
packages/runtime/src/templates/smithers/agents/index.tsx Exports and registers the Pi factory for per-task construction.
packages/runtime/src/doctor.ts Maps PiAgent references to the pi executable for toolchain diagnostics.
packages/runtime/test/runtime.test.ts Extends scaffold, stale-registry, configuration, credential, command, and environment contract coverage for Pi.
ultrafuzz.toml Adds an opt-in Pi/OpenRouter model profile and API-key agent configuration.
docs/config.md Documents Pi provider binding, credential handling, state isolation, and supported reasoning levels.

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"]
Loading

Reviews (4): Last reviewed commit: "test(runtime): assert the Pi reasoning t..." | Re-trigger Greptile

Context used:

aviggiano and others added 5 commits August 20, 2026 16:02
`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>
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
aviggiano merged commit e63f9b2 into release/v0.1.0 Aug 20, 2026
6 checks passed
@aviggiano
aviggiano deleted the feat/issue-659-pi-openrouter-adapter branch August 20, 2026 20:22
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant