Skip to content

Releases: Lokeraar/pi-EnClave-bridge

0.2.1 — the README describes the package, the releases describe its history

Choose a tag to compare

@Lokeraar Lokeraar released this 19 Oct 23:34

The README carried every version's history inline: 223 lines, from 0.1.0 to 0.2.0, a second copy of what the release pages already said. Each version now lives on its own release page and the README keeps what a reader needs — what the package does, how it works, how to install it, and where its values come from.

This release exists because the first half of that change stopped at the repository. npm serves the README from the published tarball, so 0.2.0 still shipped all 223 lines; 0.2.1 is the same code with the README the repository now has.

No code changed. Tests: 6 refresh checks and 107 sync checks, all passing.

0.2.0 — free automatic refresh, explicit paid liveness

Choose a tag to compare

@Lokeraar Lokeraar released this 19 Oct 23:01

The refresh path already issued exactly one GET /models and no inference. This release pins that property down instead of trusting it, because the sibling OpenDesign bridge shipped the opposite: up to sixteen POST /chat/completions per unknown id, on every automatic refresh.

scripts/test-refresh.mjs replaces fetch before the module loads and throws on any non-GET request, so a paid call introduced later fails the suite rather than arriving as a bill.

Six checks:

  • exactly one GET /models per refresh
  • catalog ids are published
  • the cache-only phase issues no request and restores stored models
  • router aliases are not published twice
  • a missing credential makes no request at all

liveness() still exists and still spends inference: it is reachable from scripts/sync-models.mjs, which is explicit maintenance. Nothing on the refresh path calls it.

Measured against the live router: 1 GET, 0 POST, ~1.1 s, 23 models.

0.1.10 — resolve vendor reasoning maps key by key

Choose a tag to compare

@Lokeraar Lokeraar released this 09 Oct 15:43

A vendor's thinkingLevelMap is a set of claims, one per effort level, not one
indivisible map. A field can be official and complete in one model, but partial
in another.

Pi 1.1.0's anthropic.json lists claude-haiku-5-5 with all seven levels.
Other catalogs, including opencode, state only xhigh and max. The complete
Anthropic map was already safe; the gap was the general resolution rule: if the
official catalog omitted one key, that key had to fall through to corroboration
without downgrading the keys Anthropic did state.

Now each key resolves independently. An explicit value from the vendor
wins — including null, which says that level is unsupported. A key the vendor
omits is silence; for that key alone, remaining catalogs vote by the existing
majority/OpenRouter fallback rule. Models with no vendor map still resolve
every key by corroboration.

Regression tests cover complete and partial official maps, explicit null
against a reseller majority, silence falling through, and a majority deciding
an omitted key. The official seven-level Haiku map remains intact.

Tests: 107 passed, up from 100.

0.1.9 — the vendor's catalog decides

Choose a tag to compare

@Lokeraar Lokeraar released this 09 Oct 04:15

The order that decided a model's values put openrouter first, on the principle
that it is the largest router in the world and the catalog is its core business.
For one model that principle cost us the wrong number.

The vendor's own catalog now decides outright.

For deepseek-v4.1-flash, eight catalogs say maxTokens 384000 — DeepSeek's
own first among them — while openrouter alone says 943718. The published
ceiling followed openrouter, and that is a figure this gateway rejects. A
catalog the vendor maintains records what the model implements; a reseller's
records what one gateway happens to accept, and gateways disagree. When the
vendor's number differs from everybody else's, the vendor's number is published.

Models are matched to their vendor by family: kimi → moonshotai,
claude → anthropic, gpt → openai, glm → zai, deepseek → deepseek,
mimo → xiaomi, nemotron → nvidia, mistral → mistral.

This cannot be derived automatically. Every catalog stamps provider on its
entries, but a reseller stamps its own name on a model it resells — opencode
says provider: opencode on a kimi-k3 it does not make. What marks the real
thing is that the vendor's catalog lists the model under its bare name while
resellers carry a vendor/ prefix. So it is data, one line per family, the
same way the dated model aliases already were.

Where there is no vendor catalog, agreement decides.

qwen3.8-max has no first-party catalog in Pi. It now publishes minimal: null
and high: null. That is not a guess: those levels were checked by hand against
this endpoint, where low, medium and xhigh work and minimal, off and
high return 502 ... provider returned HTTP 400. The rule reproduced a manual
measurement without being told the answer.

A simple majority of the catalogs that state the field is required. Three votes
out of ten is a plurality, not corroboration, and falls back to openrouter.
Nothing is averaged — a number nobody published is not a consensus, it is an
invention.

Three guards, each one a failure in the making:

  • A ceiling that reaches the model's own window is treated as silence, not a
    claim
    . Moonshot lists kimi-k3 with contextWindow: 1048576 and
    maxTokens: 1048576 — the same number twice. A ceiling equal to the window
    leaves no room for the prompt that goes with it, so it cannot describe
    output. The field falls through, and kimi-k3 stays at 131072.
  • A vendor catalog that says nothing about reasoning levels does not erase
    what the others state
    . nvidia names a ceiling for nemotron-ultra and no
    levels at all; dropping the levels every other catalog gives would be data
    loss dressed as authority.
  • Router aliases stay out of the count. With one catalog stating a value, a
    bare majority would otherwise hand auto every field.

A model is found by its human name as well as its id.

DeepSeek's catalog writes id: "deepseek-flash" with
name: "DeepSeek V4.1 Flash". The endpoint's deepseek-v4.1-flash and that
name are one key once punctuation is stripped, which is how the vendor's own
catalog becomes reachable without a hardcoded alias. It is an exact match on a
different field, not a similarity guess — so deepseek-v4-flash and
deepseek-v4-flash-0731 stay apart, because the date is in the name too, and
glm-5.3 never reaches glm-5.3-flash.

cost is still never published from a catalog, and never will be. A price
belongs to a reseller; averaging prices from several produces a number nobody
charges. estimateCostRange() gives the spread across every catalog when it is
asked for explicitly, which answers "what order of magnitude is this" honestly.

Also removes a leftover /** that never closed, silently swallowing a dead
function from an earlier design. Leaving code inside a comment is how it came
to be disabled without anyone noticing.

Tests: 100 passing, up from 67.

model before now
deepseek-v4.1-flash 943718 384000
deepseek-v4-flash 131072 384000
glm-5.3-flash 943717 131072
nemotron-ultra 182520 65536
qwen3.8-max minimal, high offered both removed

kimi-k3, glm-5.3, glm-5.2, minimax-m3, gpt-oss-120b and inkling
keep the ceiling they already had. Reasoning levels shift on six models, all
toward what the vendor and the majority agree on.

0.1.8 — the maintenance script runs anywhere, and each alias is published once

Choose a tag to compare

@Lokeraar Lokeraar released this 08 Oct 01:41

The maintenance script that writes the values could not run, and the router's task aliases were published twice. Both are fixed.

The script that writes providers.EnClave.models

scripts/sync-models.mjs is the only writer of the model values, and it failed in two independent ways. The visible result was a picker with ids and names only — no context window, no ceiling, no price, no reasoning levels — because nothing was ever written.

Windows could not start it. new URL(".", import.meta.url).pathname returns /C:/Users/My%20Name/..., a leading slash and percent-encoded spaces, which Node rejects as a package name:

ERR_INVALID_MODULE_SPECIFIER: Invalid module
"\C:\Users\My%20Name\...\donors-enclave.ts" is not a valid package name

Fixing that alone is not enough: a dynamic import() also refuses a bare absolute Windows path (C:\...) with ERR_UNSUPPORTED_ESM_URL_SCHEME. Both are now fileURLToPath / pathToFileURL.

scripts/test-sync.mjs carried the same bug, so npm test ran nowhere but Linux. It runs on Windows now.

On every platform it imported a name that no longer existed. The donor module became donors-enclave.ts in 0.1.4, when the two bridges stopped overwriting each other's file. enclave-live.ts, package.json and test-sync.mjs were updated; sync-models.mjs was missed, so it has failed with Cannot find module .../donors.ts since that release.

Each router alias is published once

The live catalog returns the five cyberouter/* task aliases in both data and the sibling aliases array, and both loops published them: the same id twice, with different windows, names and prices.

The alias fallback now only covers ids data does not carry, so the entry keeps the endpoint's own window and name.

The price needed the other half of the same idea. An alias states no price — which model the router picks decides it, per request — so the data entry published it as 0, i.e. free. The alias rule was already "price at the ceiling" for exactly that reason, and now applies however the alias was reached. A value nobody published is not a price of zero, it is an unstated one.

The suite

67 offline checks

Seven are new and pin the alias behaviour: one entry per alias, the endpoint's own window and name kept, the ceiling applied when the price is absent, every published id unique, and an alias absent from data still taking the fallback.

Upgrade note

Nothing about value resolution moved. If you already have values written, the script now reads them, matches donors by bare name as before, and rewrites the same fields.

Verified

Windows 10/11, Node v26.10.0, Pi 1.0.4, against the published 0.1.7 artifact as well as a checkout. Not verified on Linux — the code path is the same before and after (fileURLToPath on a Linux file: URL is the identity, pathToFileURL a no-op round trip), but a Linux run of the suite is still worth having.

0.1.7 — a version you can read, and a bug report you can fill

Choose a tag to compare

@Lokeraar Lokeraar released this 07 Oct 23:47

For making a bug report useful, rather than a guess.

What was missing

No version. A user running loose files in ~/.pi/agent/extensions had no way
to tell which build they were on. The diagnose script read the repository's
package.json, which does not exist next to an npm install — so the one tool
meant to help could not run for the people who needed it.

The version is now exported, printed on every load, and shown in the provider
name inside /model:

[enclave-bridge] 0.1.7 · agent dir /home/you/.pi/agent

And the diagnose script reads the files actually deployed in
extensions/, not the repository.

A bug report form

.github/ISSUE_TEMPLATE/bug-report.yml renders a form on GitHub. The field that
matters most is the diagnose output, because it lists every directory searched
for the catalogs
— which is the part nobody can guess from the outside.

It also asks for OS, Node version, Pi version, and how the extension was
installed, which is not a formality: a copy in extensions/ and a package
installed with pi install npm: are not the same code.

The diagnose was not in the package

The form points at scripts/diagnose.mjs. That file existed in the repository but
was never committed, so the command in the form would have failed on every
version published before this one. It ships now, and the command is verified
against the published tarball.

node ~/.pi/agent/npm/node_modules/@lokeraar/pi-enclave-bridge/scripts/diagnose.mjs

Plain node: no flags, no repository needed, and it honours
PI_CODING_AGENT_DIR.

Upgrade note

The robustness fix from the previous release is unchanged. Nothing about value
resolution moved.

0.1.6 — a broken catalog tree no longer takes the provider down

Choose a tag to compare

@Lokeraar Lokeraar released this 19 Oct 23:28

0.1.6 — a broken catalog tree no longer takes the provider down

Pi refreshes every provider in one batch. An exception thrown while looking for
catalogs therefore aborted the batch for all of them, and Pi reported
could not refresh N model catalogs for providers that were working fine —
right after a successful /login:

Saved API key for EnClave, but local model state could not be synchronized

The credential was saved; the refresh that followed it failed. Every donor lookup
is now wrapped, and a catalog that cannot be read degrades to "no donors" instead
of throwing. A donor layer is an enhancement and is not allowed to take the
provider down with it.

The version comparison also split the path on /, which is wrong on Windows where
the separator is a backslash, so the pick was arbitrary there.

0.1.5 — read catalogs from a flat install too

Choose a tag to compare

@Lokeraar Lokeraar released this 07 Oct 20:52

Where Pi keeps its provider catalogs depends on how it was installed, and there
is a third shape this bridge was not reading.

The catalogs can live in three places

<global>/…/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/data
<agent>/npm/node_modules/.pnpm/@earendil-works+pi-ai@…/node_modules/…/pi-ai/dist/providers/data
<agent>/npm/node_modules/@earendil-works/pi-ai/providers/data

The third is what a flat install gives you: no .pnpm, and no dist either.
The lookup only ever accepted dist/providers/data, so on those machines it
found nothing at all — and every model fell back to its defaults. No context
window, no output ceiling, no reasoning levels, and no vendor corrections.

That last part mattered most here. The vendor cards for glm-5.3 and glm-5.2
are what keep their ceilings at 131,072, a value this endpoint actually
accepts. With no catalog found, both would have dropped to a placeholder and
started failing every call with a 400.

Both shapes are accepted now, and the flat tree is searched alongside the pnpm
stores, so whichever exists is used and the other is the fallback.

Two bugs fixed alongside it

Sibling roots built from the wrong base. They used dirname(agentDir) as if
that were the home directory, which for agentDir = ~/.pi/agent produces the
nonsense ~/.pi/.pi/agent. Those roots never resolved.

A store entry mistaken for a scope. The root walk tested an entry for @
before testing it for +. A pnpm entry like
@earendil-works+pi-ai@0.85.1_hash is both — so it was treated as an empty scope
directory and skipped, losing the very copy being looked for.

Upgrade note

No behaviour change where the catalog was already being found. This release only
adds the layout that was returning nothing.

0.1.4 — stop the two bridges overwriting each other's donor module

Choose a tag to compare

@Lokeraar Lokeraar released this 06 Oct 18:46

Two fixes that together explain why some people saw neither their tier's models
nor any inherited values.

Each bridge gets its own module

Both bridges installed a file called donors.ts into the same extensions
directory, so whichever was copied last overwrote the other. Here it was worse
than lost time: EnClave's model cards were being served to opendesign, and
opendesign's empty table would have been served here
— silently dropping the
vendor corrections for glm-5.3 and glm-5.2, which are the two entries keeping
their ceilings at a value this endpoint actually accepts.

Renamed per bridge: donors-opendesign.ts and donors-enclave.ts.

The catalog is now found by content

Discovery was keyed on the package name. A store named after a different
package — which is what a launcher like gentle-shell produces — returned
nothing, and every model fell back to its defaults. Now any package containing
dist/providers/data/*.json counts, whatever it is called, under the running
agent directory or its siblings.

Upgrade note

0.1.3 shipped the content-based discovery but still had the module collision.
This is the first release with both.

0.1.2 — documentation

Choose a tag to compare

@Lokeraar Lokeraar released this 19 Oct 23:28

0.1.2 — documentation

Links in both directions, so the npm page and the source repo point at each
other, and a note asking for a star. No behaviour change.