pi-jev-effort

September 21, 2026 · View on GitHub

Sets the thinking level of a pi session for every prompt, from a TypeSafe Jev judgement of how hard the prompt actually is — and caps it by how much of your quota is left.

jev ▁▁█▁ medium · 57% · resets 2h11m

The four blocks are Jev's probability for each rung of the rubric — trivial, routine, substantial, hard — so a split answer is visible rather than averaged away.

Jev is a System One model: it answers a typed question with a distribution instead of prose, so one call returns a score on your rubric plus the confidence behind it. A judgement takes about 250ms and costs about $0.000015, and Jev bills input only.

Why an extension and not a hook

Pi's command hooks can read the thinking level but not write it. setThinkingLevel exists only on the extension API, so the decision has to live inside pi's process. The same is true elsewhere: Claude Code hooks read $CLAUDE_EFFORT but cannot set it (that needs a Mod), and Codex hook output has no field that changes session settings at all.

Install

pi install npm:pi-jev-effort

Or work on it in place, which keeps /reload working:

git clone https://github.com/namenu/pi-jev-effort ~/src/pi-jev-effort
ln -s ~/src/pi-jev-effort ~/.pi/agent/extensions/jev-effort

Then give it a key. Either works, and auto prefers the first it finds:

TransportKeyEndpoint
TypeSafe directTYPESAFE_API_KEY, or apiKey in ~/.jev/config.jsonPOST https://api.typesafe.ai/v1/systemone
OpenRouterthe OpenRouter login pi already holds — nothing to setPOST https://openrouter.ai/api/alpha/decisions

The OpenRouter path needs no configuration: the extension asks pi for the key with modelRegistry.getApiKeyForProvider("openrouter") and re-resolves it every turn, so a refreshed OAuth token keeps working. If your OpenRouter workspace filters providers, allow TypeSafe under Guardrails first, or every call comes back 404 provider-not-allowed-by-guardrail.

There are no npm dependencies. Both transports take the same body and return the same answer shape.

How a level gets chosen

1. Jev answers one scored question about the prompt, against a four-level rubric — trivial, routine, substantial, hard — and returns a probability for each rung. Measured against ~typesafe/jev-latest (resolved as typesafe/jev-1.13-20260917), about 250ms per call:

PromptDistributionReads as
안녕 / show me the files in this directory1.00 / 0 / 0 / 0certainly trivial
fix the typo in the README0.93 / 0.07 / 0 / 0trivial
refactor this0.07 / 0.07 / 0.80 / 0.06substantial
fix this0.47 / 0.09 / 0.43 / 0.01could be either, and says so

2. Cumulative mass decides whether to move, not the average. The scale is ordinal, so the question is how much of the answer sits at or beyond a level: move up to the highest level that P(score ≥ level) ≥ 0.3 reaches, or down to the lowest that P(score ≤ level) ≥ 0.6 covers. Upgrades clear a lower bar because thinking too much costs tokens while thinking too little costs the answer.

The last row is why the average is the wrong summary. Its mean is 0.96 with a reported confidence of 0.04 — round that and you land on a rung nothing voted for, at a confidence no threshold will ever pass, and the level sticks wherever it happens to be. On cumulative mass the same answer moves a session down from high to medium (P(≤2) = 0.99) and leaves it there, which is what a genuinely ambiguous prompt deserves.

3. The budget caps the result. A ceiling from quota pressure is a hard cap, not an opinion — it applies whether or not the distribution could move anything.

Every prompt is judged, including one-word ones. A greeting is the easiest call Jev makes, and a short follow-up like "continue" is sent with the previous reply and the tools that ran, so it is judged against the work it continues rather than on its own two words.

Budget, burn rate and reset

The same places ccusage and CodexBar look: the transcripts the agent already writes, plus whatever the provider volunteers about its own limits.

SourceWhere it comes fromGives
headersrate-limit headers captured in after_provider_responseremaining fraction, reset time
openrouterGET /api/v1/creditsremaining fraction of purchased credits (no reset)
local~/.pi/agent/sessions/**/*.jsonl, which record usage.cost per messagespend in the window against limitUsd, reset at the window edge

auto tries them in that order. The burn rate is always local: the extension sums the cost of assistant messages inside a rolling window and divides by the span, re-reading a transcript only when its size changed. Windows sit on a UTC grid, the way ccusage treats Claude's 5-hour blocks, so the reset time is the edge of the current block.

Pressure lowers the ceiling as the quota drains, and a burn rate that would empty the window before it resets counts as one threshold worse:

"pressure": [
  { "remainingBelow": 0.3, "ceiling": "medium" },
  { "remainingBelow": 0.1, "ceiling": "low" }
]

The footer carries it: jev ▁▂█▁ medium · 57% · resets 2h11m. A ~ before the level — jev ▄▂▄▁ ~medium — means the distribution leans somewhere else but did not clear its threshold, so the level is being held rather than chosen.

Command

/jev-effort           # full readout: distribution, level, budget, burn, reset
/jev-effort last      # the last ten judgements, one sparkline each
/jev-effort on        # enable, and clear a manual pause
/jev-effort off       # disable for this session
jev-effort on · via openrouter · level medium
last: "make it cleaner" → medium → medium
  0 trivial     minimal ███·······  0.34
  1 routine     low     ██········  0.20
  2 substantial medium  █████·····  0.46
  3 hard        high    ··········  0.00
  score 1.13 · confidence 0.12 · P(≤2)=1.00
budget openrouter 57% · 1,203,164 tok / \$0.031 in 5h · \$0.00046/min

Change the level yourself with /effort or Ctrl+Shift+E and automatic routing pauses for the session — your hands beat the model's judgement. A level change caused by switching models does not count as manual, so clamping to a model's capabilities will not pause anything.

Configuration

Optional, at ~/.pi/agent/jev-effort.json. Defaults:

{
  "enabled": true,
  "provider": "auto",
  "apiKey": null,
  "baseUrl": null,
  "model": null,
  "timeoutMs": 4000,
  "levels": ["minimal", "low", "medium", "high"],
  "minUpgradeConfidence": 0.3,
  "minDowngradeConfidence": 0.6,
  "floor": null,
  "ceiling": null,
  "minPromptChars": 0,
  "budget": {
    "source": "auto",
    "windowHours": 5,
    "limitUsd": null,
    "pressure": [
      { "remainingBelow": 0.3, "ceiling": "medium" },
      { "remainingBelow": 0.1, "ceiling": "low" }
    ],
    "refreshMs": 60000
  },
  "notify": false,
  "debug": false
}

levels maps rubric rungs 0–3 onto pi thinking levels, so a model with xhigh and max can use them: ["low", "medium", "high", "max"]. minUpgradeConfidence and minDowngradeConfidence are shares of the distribution, not Jev's reported confidence. floor and ceiling clamp every result, and minPromptChars above 0 brings back a length guard if you want one. JEV_EFFORT_DEBUG=1 prints one line per judgement to stderr:

[jev-effort] ▁▁█▁ score=2.02 conf=0.85 P(<=3)=1.00 P(>=3)=0.09 budget=openrouter:57% ceiling=none medium -> medium

Pi clamps whatever it is given to what the model supports, so minimal can land as low. That is pi, not this extension, and the status line shows what actually took effect.

Failure is always open

No key, a timeout, an HTTP error, a guardrail rejection or a malformed answer leaves the level untouched and the turn running. A judgement layer that can cost you a turn is worse than no judgement layer.

Development

node --test

The tests cover the cumulative-mass rule against real distributions, the hard cap, the wire contract of both transports with fetch mocked, transcript parsing and burn rate, rate-limit header shapes, and the UTC window grid. For an end-to-end check, point baseUrl at a local server that answers

{ "answers": { "effort": { "type": "score", "score": 0, "confidence": 0.85 } } }

and run JEV_EFFORT_DEBUG=1 pi -p "<a prompt of at least 12 characters>".

License

MIT