Repository navigation
Releases: Lokeraar/pi-opendesign-bridge
Release list
0.3.1 — the README describes the package, the releases describe its history
The README carried every version's history inline: 203 lines, from 0.1.0 to 0.3.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.3.0 still shipped all 203 lines; 0.3.1 is the same code with the README the repository now has.
No code changed. Tests: 22 refresh checks and 47 donor checks, all passing.
0.3.0 — discovery stops costing money
A brand-new model id was measured on every refresh: up to sixteen POST /chat/completions calls per unknown id, run automatically at interactive startup and on /model. That was billable inference on models you never selected (#2), and because the full probe could not fit Pi's 15 s refresh budget, a large uncurated catalog could never converge — the aborted refresh returned before publishing, so the store stayed empty and every retry restarted from zero (#1).
Discovery is now free and instant: one GET /v1/models, already required for membership, plus the catalogs Pi ships.
metadata.output_budgetsupplies the answer ceiling,context_limitthe window,architecture.*_modalitiesthe input and output types.- A donor catalog supplies reasoning support and the thinking map.
- The ceiling is the minimum of every free claim and is never derived from
context_budget, which measures prompt capacity rather than answer capacity — that derivation produced HTTP 400 on the Claude family. - The catalog is published before anything slow could run, so an aborted refresh persists what it knew.
Probing still exists as explicit maintenance only (scripts/probe-models.mjs, PI_OPENDESIGN_REPROBE=1); nothing on the refresh path calls it.
Measured against the live gateway: 1 GET, 0 POST, ~1.3 s, 12 models.
Tests: 22 new offline checks with a blocked network that fail on any POST during refresh and on any context_budget-derived ceiling; 47 donor checks unchanged.
0.2.9 — resolve vendor reasoning maps key by key
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: 47 passed, up from 40.
0.2.8 — the vendor's catalog decides
amr-link is a different gateway, but the resolution order is the same one its sibling bridge uses, so the reasoning is identical and the details differ only where the endpoint differs.
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 amr-link 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
amr-link, 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 listskimi-k3withcontextWindow: 1048576and
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, andkimi-k3stays at 131072. - A vendor catalog that says nothing about reasoning levels does not erase
what the others state.nvidianames a ceiling fornemotron-ultraand 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 handautoevery 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: 40 passing, up from 29, 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.2.7 — script paths that work on Windows, and git install
0.2.7 — script paths that work on Windows, and git install
The same two path bugs reported against the sibling bridge, found here by
reading its scripts rather than running them on Windows.
new URL('.', import.meta.url).pathname is not a path on Windows: it keeps a
leading slash and percent-encodes spaces, so Node reads '/C:/Users/My%20Name/...'
as a package name and dies with ERR_INVALID_MODULE_SPECIFIER. And a dynamic
import() refuses a bare absolute Windows path with ERR_UNSUPPORTED_ESM_URL_SCHEME.
fileURLToPath and pathToFileURL are the portable forms, and both are now used.
Removed scripts/sync-models.mjs: nothing referenced it, it was not in the
package files, and it was a copy left over from the sibling repo carrying that
repo's module names — enclave-live.ts and donors.ts, neither of which exists
here. It could only fail.
Also documents installing from git, which was missing entirely:
pi install git:github.com/Lokeraar/pi-opendesign-bridge
pi install git:github.com/Lokeraar/pi-opendesign-bridge#v0.2.7
npm is still the right default; git tracks main and can change under you. It is
the only way to get an unpublished fix, which is why it is now documented.
32 offline checks pass.
0.2.6 — a version you can read, and a bug report you can fill
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:
[opendesign-bridge] 0.2.6 · 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 — the part nobody can guess from the outside, and the reason a
catalog that was never found looks identical to one that does not exist.
It also asks for OS, Node version, Pi version, and how the extension was
installed. That last one is not a formality: a copy sitting 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-opendesign-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.2.5 — a broken catalog tree no longer takes the provider down
0.2.5 — 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.2.4 — read catalogs from a flat install too
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. The extension was working; it
just had nothing to work with.
Both shapes are accepted now, and the flat node_modules tree is searched
alongside the pnpm stores, so whichever exists is used and the other is the
fallback. OpenRouter is still read from whichever location is found first, and
the rest of that location still only corroborate.
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.
Verify yours
npm run diagnosela extension usa shows what the extension resolved. If that line says NINGUNO,
no catalog was found and every value came from a default.
Upgrade note
No behaviour change where the catalog was already being found. This release only
adds the layout that was returning nothing.
0.2.3 — find the catalog by content, and stop the two bridges colliding
Two fixes that together explain why some people saw neither their tier's models
nor any inherited values.
The catalog is now found by content
This was the reported bug. The store beside the catalogs can be named after a
different package than the one holding them — Pi installs the same catalogs under
different names depending on the version and the launcher — and the lookup was
keyed on the name. It found nothing, so every model fell back to its defaults:
no context, no ceiling, no reasoning levels.
Discovery is now by content: any package that contains
dist/providers/data/*.json counts. It also descends into store entries, which
sit directly under the store root rather than under a node_modules, and dedupes
by real path because the store links one copy under many entry folders.
And the store is searched under the running agent directory and its siblings,
so a wrapper that relocates it resolves too. Reporters on Pi 1.0.1 through
gentle-shell, which installs into ~/.gentle-shell/agent, are covered.
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. It was not visible
from either side: EnClave's model cards were being served to opendesign, and
opendesign's empty table would equally have been served to EnClave, silently
dropping the vendor corrections for glm-5.3 and glm-5.2.
Renamed per bridge: donors-opendesign.ts and donors-enclave.ts.
Verify yours
npm run diagnosela extension usa is what the extension resolved. If that says NINGUNO, no
donor ran. faltantes compares what your key is served against what is
published — a non-empty line is the subscription-tier problem.
Upgrade note
0.2.2 shipped the content-based discovery but still had the module collision.
This is the first release with both.
0.2.1 — documentation
0.2.1 — 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.