pi-jev-model-router

Route pi prompts to task-appropriate model tiers with TypeSafe Jev typed judgments. Budget-aware, with automatic fallback.

Packages

Package details

extension

Install pi-jev-model-router from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-jev-model-router
Package
pi-jev-model-router
Version
0.6.0
Published
Sep 29, 2026
Downloads
1,045/mo · 292/wk
Author
da-vinci-noob
License
MIT
Types
extension
Size
154.3 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "image": "https://raw.githubusercontent.com/da-vinci-noob/pi-jev-model-router/main/assets/decision-entry.png",
  "extensions": [
    "./extensions/pi-jev-model-router/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-jev-model-router

A pi extension that routes every prompt to a task-appropriate model using TypeSafe Jev (System One) typed judgments. You type normally; before the turn starts, Jev reads the request and answers four narrow questions, code composes those into a capability tier, applies your budget policy, and pi switches to the matching model.

Routing decision shown in the transcript

  • Task-aware — planning goes to reasoners, implementation to coding specialists, chat to cheap fast models.
  • Budget-aware — daily/monthly caps downgrade tiers automatically instead of overspending.
  • Resilient — each tier is a candidate chain; if a model is unavailable or unauthenticated, the next one is used.
  • Visible — the transcript records the chosen model and the exact reason (kind, complexity, capability, reasoning, budget pressure).
  • Fails open — a missing key, timeout, or unknown model just warns and runs your prompt on the current model.

How it works

you type a prompt
        │
        ▼
   Jev (one request, 4 parallel questions)
     • task_kind            choice: plan / implement / debug / refactor / review / research / explain / operate / write / chat
     • complexity           score:  trivial → architectural
     • capability_deserved  score:  minimal → maximum (price ignored)
     • needs_deep_reasoning noul:   yes/no probability
        │
        ▼
   code composes the decision
     demand = 0.55·complexity + 0.45·capability (+ reasoning nudge)
     demand = max(demand, kind floor)          # planning/review never go cheap
     confidence guard → budget guard → availability guard → cache guard
        │
        ▼
   pi.setModel(...) + pi.setThinkingLevel(...)  → the turn runs on that model

Jev judges the task, code owns the budget. Changing your spend caps never invalidates the judgment, and the judgment stays a pure semantic read of the request.

Requirements

  • pi (@earendil-works/pi-coding-agent)
  • Node.js 20+
  • A TypeSafe API key with access to jev-latest — https://typesafe.ai

Install

1. Install pi

npm install -g --ignore-scripts @earendil-works/pi-coding-agent

Verify with pi --version. See the pi quickstart for other installation methods and authentication options.

2. Install this package

From npm (recommended):

pi install npm:pi-jev-model-router

From a pinned git ref:

pi install git:github.com/da-vinci-noob/pi-jev-model-router@v0.6.0

From a local checkout:

pi install /absolute/path/to/pi-jev-model-router

Try it once without installing:

pi -e npm:pi-jev-model-router

Manage it like any other pi package:

pi list                       # show installed packages
pi update --extensions        # update packages
pi remove npm:pi-jev-model-router

3. Set your TypeSafe API key

export TYPESAFE_API_KEY=...

Add it to your shell profile to persist it. You can also set apiKey directly in the config file (see below).

4. Use it

Start pi and type a request. Before the turn runs, the router announces the decision:

jev-router → high  openai/gpt-5.3-codex
implement · complexity 1.70/3 · capability 1.55/3 · reasoning 0.82 → high

The status bar shows jev-router:<tier> · $spend · %cap, or jev-router:off when disabled. No configuration is required — sensible defaults are built in.

Commands

Command What it does
/jev-router Status: mode, spend, tier chains, kind specialists, last decision
/jev-router on / off Enable/disable routing
/jev-router mode auto|confirm|notify auto switches silently; confirm asks each turn; notify only tells you
/jev-router budget daily 10 Session-only daily cap (persist it in the config file)
/jev-router budget monthly 150 Session-only monthly cap
/jev-router why Re-run Jev on the last prompt and show the full judgment + decision trace
/jev-router revert Switch back to the model that was active before the last auto-switch
/jev-router suggest Rank your scored models into proposed routes / kindModels and print them (see Ranking models from your own scores)
/jev-router suggest --write Same, and save it as a generated config layer that your own config overrides
/jev-route <text> Classify arbitrary text and show the recommendation without switching

The model can also call the jev_route tool to ask for a tier recommendation for a subtask.

What you see

Every decision is a durable entry in the transcript, so the chosen model and its justification are always available:

jev-router → high  openai/gpt-5.3-codex
implement · complexity 1.70/3 · capability 1.55/3 · reasoning 0.82 → high
· budget 12% of cap → one tier down

The glyph encodes the action: → switched, = already active (stickiness), • notify-only mode, × skipped. Expand the entry for the raw judgment: kind and confidence, complexity, capability deserved, deep-reasoning probability, composed demand, and budget pressure. Entries are stored in the session but never sent to the LLM, so they cost no context.

Prompts that are deliberately not routed are shown too, so behaviour is never silently missing:

jev-router · not routed
acknowledgement — staying on the current model
using openrouter/~anthropic/claude-opus-latest

Which models it uses by default

The defaults target OpenRouter, because it exposes a large catalogue through a single provider id. Four capability tiers, each an ordered fallback chain:

Tier Order tried
quick ~google/gemini-flash-latest → ~openai/gpt-luna-latest → ~z-ai/glm-flash-latest → ~deepseek/deepseek-v4-flash-latest
standard ~deepseek/deepseek-pro-latest → openai/gpt-5.4-mini → ~z-ai/glm-latest
high ~anthropic/claude-sonnet-latest → ~openai/gpt-terra-latest → ~google/gemini-pro-latest → ~x-ai/grok-latest
premium ~anthropic/claude-opus-latest → openai/gpt-5.5 → ~openai/gpt-astra-latest
xpremium empty: off until you configure it (see Extra-premium tier)

Plus kind specialists, tried before the tier chain when the chosen tier is high enough (minTier):

Kind Specialists
plan ~anthropic/claude-opus-latest (≥premium) → ~openai/gpt-astra-latest (≥premium) → ~openai/gpt-terra-latest (≥high) → ~google/gemini-pro-latest (≥standard)
implement openai/gpt-5.3-codex (≥standard) → moonshotai/kimi-k2.7-code → ~anthropic/claude-sonnet-latest
debug openai/gpt-5.3-codex (≥standard) → ~openai/gpt-terra-latest (≥high) → ~anthropic/claude-sonnet-latest
refactor openai/gpt-5.3-codex (≥standard) → moonshotai/kimi-k2.7-code
review ~anthropic/claude-opus-latest (≥high) → openai/gpt-5.5 (≥high) → ~anthropic/claude-sonnet-latest
research ~google/gemini-pro-latest (≥standard) → moonshotai/kimi-k3 → ~openai/gpt-terra-latest
explain ~google/gemini-flash-latest (≥quick) → openai/gpt-5.4-mini → ~google/gemini-pro-latest (≥standard)
operate openai/gpt-5.4-mini (≥standard) → ~deepseek/deepseek-pro-latest
chat ~google/gemini-flash-latest → ~openai/gpt-luna-latest → ~z-ai/glm-flash-latest
write ~google/gemini-flash-latest (≥quick) → openai/gpt-5.4-mini → ~anthropic/claude-sonnet-latest (≥standard)

Provider-maintained ~...-latest aliases are used wherever they exist, so the chains follow new model releases instead of going stale.

Run /jev-router to see this for your own setup, with a ✓/✗ per route showing what is actually available and authenticated.

Extending to more models and providers

The router is provider-agnostic: it only references models that pi already knows about, so if pi can use a model, the router can route to it. You are never limited to OpenRouter — the defaults are just a convenient starting point.

0. Drop the built-in models entirely (optional)

By default your config is merged over the built-in chains, so a tier you don't mention keeps its defaults. If you'd rather start from nothing and use only your own models, set:

{
  "useDefaultModels": false,
  "routes": {
    "quick": [{ "provider": "openrouter", "model": "~z-ai/glm-flash-latest" }],
    "high":  [{ "provider": "anthropic", "model": "claude-sonnet-4-5" }]
  },
  "kindModels": {
    "implement": [{ "provider": "openrouter", "model": "moonshotai/kimi-k2.7-code", "minTier": "standard" }]
  }
}

With useDefaultModels: false:

  • the built-in routes and kindModels are gone — not available as a base or as fallback;
  • tiers or kinds you don't configure are empty, and the router simply skips them (it never invents a model);
  • everything that isn't a model list still applies — endpoint, timeouts, budget, cache, and the kindMinimumTier floors.

/jev-router prints built-in models: off (config-only) and marks empty tiers as (none configured). If a tier you need is empty, pi warns on session start.

1. Find the model ids pi knows

pi --list-models                 # all providers
pi --list-models | grep anthropic
pi --list-models | grep -E 'gpt-5|codex'

The first column is the provider id and the second is the model id. Those are exactly the two fields the config uses.

Providers not yet configured can be added with pi /login, an API key environment variable, or a custom provider registered by another extension — including local servers such as Ollama or llama.cpp. See the providers docs.

2. Point the tiers at your models

Create ~/.pi/agent/pi-jev-model-router.json (or <project>/.pi/pi-jev-model-router.json). Anything you set is merged over the defaults, per tier.

{
  "routes": {
    "quick": [
      { "provider": "openrouter", "model": "~google/gemini-flash-latest" }
    ],
    "standard": [
      { "provider": "anthropic", "model": "claude-haiku-4-5" }
    ],
    "high": [
      { "provider": "anthropic", "model": "claude-sonnet-4-5" },
      { "provider": "openai", "model": "gpt-5.4" }
    ],
    "premium": [
      { "provider": "anthropic", "model": "claude-opus-4-5" },
      { "provider": "openai", "model": "gpt-5.5-pro" }
    ]
  }
}

Each tier is a candidate chain, tried top to bottom. The first model that exists in pi's catalogue and is authenticated wins; if none are usable the router steps to the nearest tier instead of failing. Add "thinkingLevel" to any entry to pin it ("off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; pi clamps it per model).

Mixing providers is fine — put an OpenRouter model and a direct-Anthropic model in the same chain.

3. Add or change task specialists

{
  "kindModels": {
    "implement": [
      { "provider": "openrouter", "model": "openai/gpt-5.3-codex", "minTier": "standard" },
      { "provider": "anthropic", "model": "claude-sonnet-4-5", "minTier": "standard" }
    ],
    "plan": [
      { "provider": "openai", "model": "gpt-5.5", "minTier": "high" },
      { "provider": "anthropic", "model": "claude-opus-4-5", "minTier": "premium" }
    ]
  }
}

minTier gates a model to a minimum capability tier, so a specialist is only used when the judgment justifies it. When priorities tie (the default), the eligible specialist whose minTier is closest to the chosen tier wins, so a cheap specialist does not win a premium-quality turn.

To rank a specialist up without changing when it is eligible, give it a numeric priority (default 0, higher first). priority is compared before minTier, so this keeps a free model eligible from quick and still tries it ahead of a paid high specialist:

{
  "kindModels": {
    "write": [
      { "provider": "opencode-go", "model": "space-bunny-free", "minTier": "quick", "priority": 1 },
      { "provider": "openrouter", "model": "~anthropic/claude-sonnet-latest", "minTier": "high" }
    ]
  }
}

Without any priority, ordering is exactly as before.

4. Set the floor per task kind

{
  "kindMinimumTier": {
    "plan": "high",
    "review": "high",
    "implement": "standard",
    "debug": "standard",
    "chat": "quick"
  }
}

Known kinds: plan, implement, debug, refactor, review, research, explain, operate, write, chat.

5. Add or change task kinds

Jev picks one kind per prompt from taskKinds: a label and a one-line description that Jev uses as the choice criteria. Add your own domains or reword a built-in description in config, then add matching kindModels and kindMinimumTier entries:

{
  "taskKinds": {
    "data": "Querying, cleaning, or transforming datasets and SQL",
    "infra": "Provisioning or changing cloud infrastructure and deploy config"
  },
  "kindModels": {
    "data": [{ "provider": "openrouter", "model": "~google/gemini-pro-latest", "minTier": "standard" }]
  },
  "kindMinimumTier": { "infra": "high" }
}

Entries merge over the built-in kinds (global, then project). A kind can't be removed, and entries without a non-empty string description are ignored. Because the question is a Jev choice, the option set is the taxonomy: no retraining, no prompt parsing.

Config resolution order

Later sources win:

  1. built-in defaults
  2. ~/.pi/agent/pi-jev-model-router.generated.json: only routes and kindModels, written by /jev-router suggest --write
  3. ~/.pi/agent/pi-jev-model-router.json
  4. <cwd>/.pi/pi-jev-model-router.json (trusted projects only)
  5. env: TYPESAFE_API_KEY, JEV_ROUTER_MODE (auto|confirm|notify), JEV_ROUTER_OFF=1

A full example lives at extensions/pi-jev-model-router/pi-jev-model-router.example.json. Run /reload after editing config.

Budget management

Cost is accumulated from each assistant message's computed cost into ~/.pi/agent/pi-jev-model-router-state.json, together with Jev request counts.

pressure = max(today ÷ dailyUsd, month ÷ monthlyUsd)
  • pressure ≥ softRatio (default 0.7) → drop one tier
  • pressure ≥ hardRatio (default 0.9) → force quick, unless the demand score is ≥ 2.5 (clearly architectural), which may stay at standard
{
  "budget": {
    "dailyUsd": 5,
    "monthlyUsd": 100,
    "softRatio": 0.7,
    "hardRatio": 0.9
  }
}

Omit either cap to disable that dimension. Caps are policy, not a hard stop — they redirect routing, they do not block turns.

Prompt-cache awareness

Switching models discards the provider's prompt cache, and caches are per-model. The next request then re-reads the entire prefix — system prompt, tool schemas, and conversation — at the new model's full input rate. Cache reads are ~10% of input on the major providers, so a switch effectively costs the whole context once, and a switch back costs it again. On a 50k context that is roughly $0.10 on Sonnet; at 200k, roughly $0.45.

The router therefore gates switches instead of making them freely:

  • Cache penalty cap — estimates the miss (contextTokens × new model's input rate, minus the cached rate) and refuses the switch when it exceeds maxPenaltyUsd.
  • Dead-band — demand has to clear the current tier's band (tier ± 0.5) by deadband before a tier change is considered, so prompts hovering on a boundary stop flapping between two models.
  • Big-jump bypass — a tier jump of bypassTierDelta or more still switches, because that is a genuine capability change rather than a marginal one.
  • Same-tier swaps count too — a specialist swap such as Sonnet → Codex at the same tier is still a model change, and is priced the same way.

Held turns still record the decision, and say so:

jev-router = high  openrouter/~anthropic/claude-sonnet-latest
explain · complexity 0.40/3 · capability 0.30/3 · reasoning 0.20 → standard, held on high to keep the cache
· cache penalty ~$0.186 on 120k tokens — keeping the warm cache

The estimate is a lower bound — real cacheable prefixes include the system prompt and tool schemas, which contextTokens does not count — and it is skipped entirely when a model's pricing is unknown, so it never blocks on guesses. Set cache.aware: false to restore unconditional switching.

{
  "cache": {
    "aware": true,
    "deadband": 0.25,
    "maxPenaltyUsd": 0.05,
    "bypassTierDelta": 2
  }
}

Ranking models from your own scores

Instead of maintaining routes by hand, you can give the router a scores file and let it propose chains from the models pi actually has. The router never fetches rankings itself; any script or board export that writes this JSON works.

~/.pi/agent/pi-jev-model-router.scores.json:

{
  "models": {
    "openrouter/~anthropic/claude-opus-latest": { "score": 0.92, "kinds": { "review": 0.95, "plan": 0.9 } },
    "openrouter/~google/gemini-flash-latest": { "score": 0.55, "kinds": { "write": 0.7 } },
    "grok-cli/grok-4.6": { "score": 0.8, "cost": { "input": 0, "output": 0 } }
  }
}
  • Keys are <provider>/<model id> and must match a model in pi's catalogue exactly. Scored models pi doesn't have are listed as not in the catalogue.
  • score places the model on a tier by fixed cut-offs (inclusive), below standard is quick. xpremium is never filled; set it by hand. Scores and cut-offs just need the same scale (0-1 by default, or e.g. 0-100 with matching cut-offs), and cut-offs that are not ascending are ignored.
  • Within a tier, models are ordered by score per cost, using pi's catalogue price (input + output USD per 1M tokens) unless the entry has a cost override in the same units. Use the override for flat subscriptions or quota limits: set your own effective price, or 0 to treat the model as free (free models rank first). Models without any known price go last.
  • kinds scores build kindModels for kinds in taskKinds, each gated (minTier) at the tier its kind score clears.
  • With spreadProviders on (the default), a chain never has two entries in a row from the same provider when it can be avoided.
{
  "ranking": {
    "cutoffs": { "standard": 0.5, "high": 0.7, "premium": 0.85 },
    "spreadProviders": true,
    "scoresFile": "~/.pi/agent/pi-jev-model-router.scores.json"
  }
}

/jev-router suggest prints the proposal. /jev-router suggest --write saves it to ~/.pi/agent/pi-jev-model-router.generated.json and applies it right away. If none of the scored models match pi's catalogue, --write warns and leaves the existing generated file untouched.

That file is loaded before your own config, and your config file is never written. So anything you set by hand wins over the generated file:

  • a tier or kind you define replaces the generated one
  • an explicit empty list clears it, e.g. "routes": { "high": [] } or "kindModels": { "plan": [] } (a list with only invalid entries is ignored)

Generated specialists carry a descending priority, so routing tries them in the proposed order. The built-in chains still fill any tier or kind the generated file doesn't cover. To route only from your scores and hand edits, set "useDefaultModels": false.

Extra-premium tier

xpremium sits above premium for the most expensive model class (around $50 / 1M output tokens). It is off until you give it a chain, and the built-in models never fill it:

{
  "routes": {
    "xpremium": [{ "provider": "openrouter", "model": "~openai/gpt-astra-latest", "thinkingLevel": "high" }]
  }
}

The demand scale is unchanged, so no prompt that routes to premium today moves up on its own. A turn is promoted to xpremium only when all of these hold:

  • demand already lands on premium
  • Jev's kind confidence is known and at least confidenceThreshold
  • the budget guards don't step it down (soft pressure drops it back to premium)

Otherwise the xpremium chain is never tried, not even as a fallback when premium is unavailable. kindMinimumTier: "xpremium" is treated as premium. A kind specialist with minTier: "xpremium" only serves promoted turns.

Free models

Tiers describe capability, not price, so there is nowhere in TIERS to put a model that costs nothing. A free pool is consulted outside the tier scale instead, so the judgment is untouched and a free model never has to pretend to be a tier.

{
  "free": {
    "enabled": true,
    "policy": "prefer",
    "pool": [
      { "provider": "opencode-go", "model": "space-bunny-free", "thinkingLevel": "medium" },
      { "provider": "opencode-go", "model": "longcat-2.5-preview-free", "thinkingLevel": "medium" }
    ]
  }
}
  • policy: "prefer" tries the pool first, then the kind chains and tier chains.
  • policy: "fallback-only" tries the pool last, after every tier chain.
  • enabled: false (the default) ignores the pool entirely, so an existing config routes exactly as before.

Pool entries match on provider and model exactly, so a missing free model never falls back to a paid model with the same ID. The first pool entry in the model catalogue wins; one that is missing falls through to the normal chains. When a pool model wins, the decision still reports the tier Jev judged, plus a free pool → <model> note, so the transcript shows why the tier and the model differ.

A pool model is not on any tier, so tierForModel reports no tier for it and the status line falls back to jev-router:on. That is deliberate: a free model has no capability tier.

Free models are usually temporary. If a provider withdraws one from the catalogue, routing skips it and continues down the chains, so a stale pool entry costs nothing but a wasted slot. Only the chosen model is tried: if switching to it fails (for example, no auth for the provider), the turn stays on the current model.

Free and preview models may log or train on prompts, and prefer sends every turn to the pool. Check the provider's data policy before enabling it.

Configuration reference

Key Default Purpose
enabled true Master switch
useDefaultModels true false drops the built-in model chains so only your config's models are used
mode "auto" auto | confirm | notify
apiKeyEnv / apiKey TYPESAFE_API_KEY TypeSafe credentials
endpointEnv / endpoint TYPESAFE_API_URL / https://api.typesafe.ai/v1/systemone TypeSafe-compatible evaluation endpoint (non-empty env wins over endpoint)
jevModel "jev-latest" Jev model alias
timeoutMs 3500 Jev request timeout (retries 429/529)
minPromptChars 12 Below this, a prompt counts as a continuation (a short first message is still routed)
historyTurns 4 Conversation turns included as Jev state
confidenceThreshold 0.34 Below this, fall back to standard instead of spending premium
stickiness true Keep the current model when it is already the chosen one
routes see above Capability tier candidate chains; xpremium is empty (off) unless you set it
kindModels see above Task-specialist chains with minTier (gate) and optional priority (rank)
kindMinimumTier see above Per-kind floor tier
taskKinds the 10 kinds above Kind labels and descriptions Jev chooses from; merged over the built-ins
ranking cut-offs 0.5 / 0.7 / 0.85, spreadProviders: true Scores file and cut-offs for /jev-router suggest
free disabled Free-model pool consulted outside the tier scale (prefer or fallback-only)
budget no caps Spend policy
cache aware, cap $0.05, deadband 0.25 Prompt-cache-aware switching
stateFile ~/.pi/agent/pi-jev-model-router-state.json Spend ledger

Failure behaviour

Routing never blocks your turn. A missing key, network error, timeout (default 3.5 s, retried on 429/529), or unknown model means: warn in the status line and run the prompt on the current model unchanged. Prompts starting with /, pure acknowledgements (yes, continue, …), and messages sent by other extensions are never routed.

Publishing to pi.dev/packages

The pi package gallery is built from npm: it lists packages that are tagged with the pi-package keyword. There is no separate submission form — publishing to npm is the submission.

This repository is already prepared for it:

  • package.json contains "keywords": ["pi-package", ...]
  • package.json contains a pi manifest pointing at the extension entry point
  • package.json contains pi.image, which the gallery uses as the preview card

To publish:

# 1. Log in to npm (once)
npm login

# 2. Sanity-check what will be shipped
npm pack --dry-run

# 3. Publish
npm publish --access public

Then:

  1. The gallery indexes it on its next crawl (usually minutes; allow a few hours).
  2. Check the listing at https://pi.dev/packages/pi-jev-model-router.
  3. Anyone can then install it with pi install npm:pi-jev-model-router.

Releases: bump version in package.json, commit, tag, and re-run npm publish. Keep the image URL pointed at a released tag or main so it never 404s.

If the name is taken, publish under a scope (@yourname/pi-jev-model-router) — the gallery indexes scoped packages too; install with pi install npm:@yourname/pi-jev-model-router.

Git-only distribution also works (pi install git:github.com/user/repo@v1), but only npm packages appear in the gallery.

You can also add a GIF or MP4 demo via pi.video (MP4 only, takes precedence over image).

Development

git clone https://github.com/da-vinci-noob/pi-jev-model-router
cd pi-jev-model-router

# load the package into a throwaway pi run (ignores auto-discovered extensions)
pi -ne -e "$PWD" -p "Explain what an idempotency key does."

# or copy into the auto-discovered location for hot reload
cp -R extensions/pi-jev-model-router ~/.pi/agent/extensions/

Tests and typecheck (the same checks CI runs on every PR):

bun install
bun run typecheck   # tsc against the real pi ExtensionAPI types
bun test            # router, config, Jev client, budget, and extension load/route tests

Tests stub fetch, so they never hit the network. Tests that load config mock os.homedir to a temp dir, so your real ~/.pi config doesn't affect them.

Layout:

File Role
extensions/pi-jev-model-router/index.ts pi wiring: events, commands, jev_route tool, model switching, transcript entries
extensions/pi-jev-model-router/config.ts config types, defaults, layered loading, task taxonomy
extensions/pi-jev-model-router/jev.ts TypeSafe HTTP client, question definitions, response parsing
extensions/pi-jev-model-router/router.ts composition (decide), tier/kind chains, availability fallback
extensions/pi-jev-model-router/budget.ts spend ledger, caps, pressure
extensions/pi-jev-model-router/ranking.ts scores file, tier cut-offs, provider spread for /jev-router suggest

No runtime dependencies: the extension talks to TypeSafe with plain fetch. It imports typebox (tool schema) and @earendil-works/pi-coding-agent (config directory path), and loads @earendil-works/pi-tui lazily, only when the host implements registerEntryRenderer. @earendil-works/pi-tui is declared as an optional peer dependency, so hosts that don't ship it still install and run.

Compatibility with pi builds and forks

ExtensionAPI surfaces differ across pi versions and downstream forks (for example omp). The extension probes the host at load time and degrades instead of failing installation:

Capability If the host lacks it
registerEntryRenderer or @earendil-works/pi-tui No transcript card; decisions still show in the status bar and notifications
appendEntry Decisions are not persisted as session entries
ctx.ui.notify / ctx.ui.setStatus Silently skipped
ctx.ui.select confirm mode falls back to auto-switching
ctx.modelRegistry.find / getAvailable Reports "model not available in this build" and leaves the current model in place
registerCommand / registerTool Commands and the tool are not registered; event-driven routing still works

Nothing in the extension throws during load if an optional API is missing, so pi install, omp install, or any plugin validator will accept it.

License

MIT