@lokeraar/pi-enclave-bridge
EnClave provider bridge for Pi: /login once and the live router catalog appears in /model with real context windows, real per-token prices, live membership and the router's task aliases — resolved from the model catalog Pi already ships.
Package details
Install @lokeraar/pi-enclave-bridge from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@lokeraar/pi-enclave-bridge- Package
@lokeraar/pi-enclave-bridge- Version
0.1.10- Published
- Oct 11, 2026
- Downloads
- 884/mo · 884/wk
- Author
- lokeraar10
- License
- MIT
- Types
- extension
- Size
- 148 KB
- Dependencies
- 0 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@lokeraar/pi-enclave-bridge
EnClave provider for Pi.
Pi is a coding assistant that talks to AI models. EnClave is a gateway: one address that reaches many models from many companies. The problem is that EnClave tells Pi very little about each model — how much text it can read, how much it can write back, whether it understands images, which "thinking effort" settings it accepts. Without those numbers, Pi has to guess.
This package supplies them. When you run /model you see each model with its
real context window, its real price per million tokens, whether your key can
actually reach it, and the router's task aliases. Every number is read from a
catalog Pi already has on disk, so no extra account is needed.
🔗 Where to find me
| 📦 npm | @lokeraar/pi-enclave-bridge |
| 🌐 Pi catalog | pi.dev/packages/@lokeraar/pi-enclave-bridge |
| ⭐ Source | github.com/Lokeraar/pi-EnClave-bridge |
Install it in Pi:
pi install npm:@lokeraar/pi-enclave-bridge
Straight from the repository, if you would rather follow main than a release:
pi install git:github.com/Lokeraar/pi-EnClave-bridge
The git form tracks main, so it can change under you. Prefer npm unless you are
testing something specific, or a fix has not been published yet — during that
window this is the only way to get it. To pin a release:
pi install git:github.com/Lokeraar/pi-EnClave-bridge#v0.1.10
💛 If this bridge ever saved you from guessing a model's limits, a ⭐ on the repo goes a long way — it is the only thing that helps somebody else find it. Every star is read by a human, and so is every bug report.
📋 Releases
0.1.10 — vendor reasoning levels resolve key by key
An official catalog can declare a partial thinkingLevelMap. That does not mean
it owns levels it omitted. Each level now follows the same source rule as other
fields: the vendor decides if it explicitly declares that level (including an
explicit null); if it is silent, corroboration decides that key. A partial
Anthropic map can therefore supply max without erasing low or high that
other catalogs agree on. Added regression coverage for full official maps,
partial official maps, and explicit vendor nulls.
Tests: 107 passing, up from 100.
0.1.9 — the vendor's catalog decides, and agreement settles the rest
The order that decides a model's values has changed, and it was worth getting wrong twice before it was right.
The vendor's own catalog now decides. For deepseek-v4.1-flash, eight
catalogs say maxTokens 384000 — DeepSeek's own first among them — and
openrouter alone says 943718, a figure this endpoint rejects. The published ceiling
followed openrouter, so it was wrong. A catalog the vendor maintains records
what the model implements; a reseller's records what one gateway accepts, and
gateways disagree.
Models are matched to their vendor by family: kimi → moonshotai,
claude → anthropic, gpt → openai, glm → zai, deepseek → deepseek,
mimo → xiaomi, nemotron → nvidia. It cannot be derived automatically —
every catalog stamps provider on its entries, but a reseller stamps its own
name on a model it resells — so it is data, one line per family.
When there is no vendor catalog, the value most catalogs agree on stands.
qwen3.8-max has no first-party catalog in Pi. It now publishes
minimal: null and high: null, which is exactly what this endpoint accepts:
low, medium and xhigh work; minimal, off and high return
502 ... provider returned HTTP 400. The rule found that without being told.
A ceiling that reaches the model's own window is treated as silence.
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, so it cannot describe output; the field falls through to the catalogs
that state a ceiling a model can serve, and kimi-k3 stays at 131072.
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", so the
endpoint's id and that name are one key once punctuation is stripped. This is
an exact match on a different field, not a similarity guess: dated variants
stay apart because the date is in the name too.
cost is still never published from a catalog, and never will be — a
price belongs to a reseller, so averaging prices from several would produce a
number nobody charges. What is available instead is a range, asked for
explicitly, showing the spread across every catalog.
0.1.8 — the maintenance script runs anywhere, and each alias is published once
scripts/sync-models.mjs is the only writer of providers.EnClave.models, and it
could not run at all in two independent ways. The result was a picker with ids and
names only: no context window, no ceiling, no price, no reasoning levels, because
the values were never written.
Windows could not even 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 is not enough on its own: a dynamic import() also refuses a bare
absolute Windows path (C:\...) with ERR_UNSUPPORTED_ESM_URL_SCHEME. Both are
now fileURLToPath / pathToFileURL, and scripts/test-sync.mjs carried the
same bug, so npm test ran nowhere but Linux.
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. enclave-live.ts, package.json and test-sync.mjs were updated;
sync-models.mjs was missed, so the script has failed with
Cannot find module .../donors.ts since that release.
Each router alias was published twice. 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 — 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.
At this release, the suite has 107 offline checks and runs on Windows. It covers one entry per router alias, the endpoint's own window and name, catalog matching by id and normalized name, vendor authority, key-by-key reasoning resolution, majority fallback, catalog discovery and the output ceiling clamp.
0.1.7 — a version you can read, and a bug report you can fill
Three gaps that made an incoming report unusable.
No version. A user running loose files in ~/.pi/agent/extensions had no way
to tell which build they were on, and the diagnose script read the repository's
package.json, which does not exist next to an npm install. The version is now
exported, printed on load, and shown in the provider name in /model.
No form. .github/ISSUE_TEMPLATE/bug-report.yml asks for the one thing that
locates a cause: the diagnose output, which lists every directory searched for
the catalogs. Plus OS, Node version, Pi version, install method, and the provider
block with the key hidden.
The diagnose was not in the package. The form pointed at
scripts/diagnose.mjs, which existed on disk but was never committed, so the
command in the form would have failed on every version before this one. It ships
now, and runs with plain node, no flags, no repository.
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 — catalogs from a flat install too
Catalogs can occupy several locations depending on how Pi was installed. The bridge first discovers the active Pi package at runtime, then checks supported agent-local store and flat-install layouts as fallbacks:
<active Pi package>/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 first path is not hardcoded: Pi may be global, local, under a custom prefix,
or installed by a different package manager. A flat install may have no .pnpm
and no dist. If Pi upgrades its bundled pi-ai, restart Pi or refresh the
provider so the bridge reads the updated catalogs.
Two bugs fixed alongside it. The sibling roots were built from
dirname(agentDir) as if that were the home directory, which for
agentDir = ~/.pi/agent produces the nonsense ~/.pi/.pi/agent. And the root
walk tested an entry for @ before testing it for +, so a pnpm entry like
@earendil-works+pi-ai@0.85.1_hash — which is both — was treated as an empty
scope and skipped, losing the very copy being looked for.
0.1.4 — find the catalog by content, and stop the two bridges colliding
Discovery was keyed on the package name, so a store named after a different
package returned nothing. It is now by content: any package containing
dist/providers/data/*.json counts, whatever it is named.
Two 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: the model cards for glm-5.3 and glm-5.2 are what keep their
ceilings at 131,072, a value this endpoint actually accepts. With an empty
table instead, both would have dropped to a placeholder and started failing every
call with a 400.
Renamed per bridge, so installing one can never displace the other.
0.1.3 — find the catalog by content
First pass at the same fix, before the flat install shape was known.
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.
0.1.1 — a clamp that left room for an actual prompt
The ceiling clamp kept back 2,048 tokens for the prompt. That router counts
messages + tools + max_tokens against the window, not the output alone, and a
real Pi call carries the system prompt plus every tool schema — order 20k
tokens. The reserve only covered a toy request.
gpt-oss-120b exposed it: a 131,072 window with a 117,964 ceiling left 13,108
tokens for input, below what Pi sends, so every call failed with 400 as
soon as the conversation carried anything. Measured with the same prompt:
input ~16k ceiling 117964 HTTP 400 "needs about 133,972 tokens"
input ~16k ceiling 98304 HTTP 200
input ~28k ceiling 98304 HTTP 200
The published value is a ceiling, not a fixed request: Pi reduces it per turn to
min(published, contextWindow − prompt − reserve). The reserve just had to be
realistic.
0.1.0 — first release
Values resolved from the model catalog Pi ships, with the vendor's model card
outranking every catalog. /login once and the live router catalog appears in
/model with real context windows, real per-token prices, live membership and
the router's task aliases.
⚡ Quick Start
pi install npm:@lokeraar/pi-enclave-bridge
Then log in once:
/login EnClave
That is all. The model list builds itself, and /model shows every model the
router serves for your key. Each one carries a note saying where its numbers came
from, so you can check them instead of trusting them.
First check how the older copy was installed. Pi can load an extension as an installed package, or directly from a
.tsfile in~/.pi/agent/extensions/. If both copies are present, both can register EnClave and conflict. Updating a package does not remove a loose file.
pi listshows installed packages. If this package is already listed from npm, update it in place:pi update npm:@lokeraar/pi-enclave-bridgeIf
pi listshows it was installed from git, update that same source instead:pi update git:github.com/Lokeraar/pi-EnClave-bridgeDo not run both commands. Use the one that matches the source already installed; switching sources can leave two package entries. If you also have an old loose EnClave file in
~/.pi/agent/extensions/, remove it or move it outside that folder, then restart Pi.PI_ENCLAVE_LIVE=0only turns off model refresh; it does not remove a duplicate extension.
Why this package exists
EnClave's router lives at https://router.enclave.ai/v1. It speaks the same
language as OpenAI's API, so any tool built for OpenAI can talk to it. Its
/models address is unusually helpful: for each model it reports the context
window, the price per million tokens, and whether your key has a working route
to it.
But three things it never mentions matter just as much to a coding assistant:
- Which thinking-effort values the model really accepts. "Think harder" is a real setting, and models disagree about which values they honour.
- Whether it can read images.
- How much output it will actually produce.
When a tool is not told these, it fills them with round, plausible-looking defaults — numbers that look like facts but were never measured. This package finds them in Pi's own catalog and reports them. When no source states a value, it leaves the value alone rather than inventing one.
⚙️ How it works
Every model needs values for the same handful of fields. Where each value comes from follows one priority order, decided field by field — not model by model, and not averaged:
1. a hand-written vendor card (VENDOR_SPEC)
2. the catalog belonging to the model's own vendor
3. a simple majority among catalogs that state the field (only if step 2 is absent)
4. OpenRouter (only if step 3 has no majority)
5. whatever models.json already says (only if no catalog states the field)
Why the vendor comes first. Many of the catalogs Pi ships belong to resellers — companies that host other companies' models. A reseller's catalog describes what one gateway happens to accept. The vendor's own catalog describes the model itself. When they disagree, the vendor is describing the thing; the reseller is describing one doorway to it.
A concrete case: for deepseek-v4.1-flash, eight catalogs state an output ceiling
of 384,000 tokens — DeepSeek's own first among them — while OpenRouter alone says
943,718. The old rule published OpenRouter's figure, and this gateway rejected it.
The vendor's number is now the one published.
How the vendor is found. By the model's family name: kimi belongs to
Moonshot, claude to Anthropic, gpt to OpenAI, glm to zai, deepseek to
DeepSeek, mimo to Xiaomi, nemotron to NVIDIA. This cannot be worked out from
the files, because every catalog stamps its own name as the provider — including a
reseller stamping its name on a model it did not make. So the list is written down
explicitly, one line per family, exactly like the dated-model aliases.
When there is no vendor catalog, the catalogs vote. A value needs a simple majority of those that state the field at all: three votes out of ten is not agreement, it is a split, and a split falls back to OpenRouter. Nothing is ever averaged — an average of two real numbers is a third number that nobody published.
Thinking levels are decided one level at a time. A model's
thinkingLevelMap says which effort levels work. It is not one value but seven
independent ones (off, minimal, low, medium, high, xhigh, max). If
the vendor states a level — even to say "not supported" with an explicit null —
that answer stands. If the vendor simply does not mention a level, that silence
says nothing, and only that single level is decided by the other catalogs. Two
consequences worth knowing:
- A reseller listing only two levels cannot erase the five its vendor declared.
- A vendor listing only one level does not block the others from being filled in.
A ceiling as large as the window is treated as impossible, not generous. Some
catalogs write the row that way: Moonshot lists kimi-k3 with a context window of
1,048,576 and an output ceiling of 1,048,576 — the same number twice. But a prompt
must fit in the window alongside the output, so a ceiling that fills the whole
window cannot be served. That figure is treated as no answer at all, and the field
falls through to the catalogs that state a ceiling a model can keep.
Two fields are never taken from a catalog at all.
| Field | Why it belongs to EnClave |
|---|---|
contextWindow |
It describes what this gateway serves, not what the model can do. |
cost |
A price belongs to whoever charges it. Combining prices from several resellers would produce a number EnClave may never charge. |
The donor module can report an approximate minimum-to-maximum cost range when asked directly, for inspection only. It is not published as a price.
No account is needed to read these catalogs. They are plain JSON files that
ship inside Pi itself, under pi-ai/dist/providers/data/. An API key is needed to
call a model; reading what Pi already installed on your disk needs nothing. A
brand-new Pi with no logins at all still gets the full catalog.
🔎 Two refresh phases
The list is built in two separate steps, and worth understanding because they do different jobs.
| Phase | When it runs | What it does |
|---|---|---|
| Restore from cache | Every time Pi starts the provider | Rebuilds the list from models.json plus Pi's saved state. Instant, and works with no network. |
| Refresh membership | Interactive startup, and when you search in /model |
Asks the endpoint which models exist right now, adds any that appeared, and drops any that disappeared. |
The second step only ever changes who is on the list, never what their numbers
are. The numbers come from models.json, which scripts/sync-models.mjs is the
only writer of.
One environment variable controls this: setting PI_ENCLAVE_LIVE=0 freezes the
list, which is useful when you want to test without the network changing things
underneath you.
🏷️ Which models get published
A model is published only if all three of these hold: the router lists it, your key has a working route to it, and it actually answers a request.
Listing is not enough, and there are three separate ways a listed model can turn out to be unusable. Each is handled:
| What you see | What is really happening |
|---|---|
Listed, but routeable_endpoint_count: 0 — the check is routeable_endpoint_count > 0 |
You have no working route. Every request returns 404. |
Answers 502 "provider returned HTTP 410" |
The catalog calls it healthy, but the provider behind it is gone. 410 Gone means permanently, not "try again later". |
Simply absent from /models |
Retired upstream. |
The third case caused a real failure worth naming: cyberouter/remediation is a
router alias that picks a model by task quality score. It kept failing because the
highest-scoring remediation model was one of the dead ones.
🔀 Router aliases
Five entries are not ordinary models. cyberouter/auto and one alias per security
task (vuln-discovery, exploit-dev, remediation, triage) are instructions:
you pick one and the router decides which concrete model to use, per request. They
behave like models, so they are published like models.
They are also the reason a naive parser breaks. The endpoint lists each of them
twice — once inside data and once in a sibling field of the catalog. Code
that reads only the sibling field loses all five. Code that reads only data
publishes each of them twice, with different windows, names and prices. This
package reads both and publishes each exactly once, taking the data entry
because it carries the endpoint's own window and name.
Their price cannot be known in advance, because the router picks the model later. Rather than guess, the answer is bounded: priced at the catalog ceiling. If the endpoint states no window either, the context falls back to the catalog's floor value.
One thing is deliberately refused. These aliases are never looked up in a
catalog. OpenRouter also has a model called auto, advertising a 2,000,000-token
window. It is a different thing that happens to share a name, and using its numbers
here would be a lie.
🐞 Fixes
These are real problems that were found by using the thing, not by reading it.
A ceiling bigger than the window is not generous, it is impossible.
OpenRouter listed inkling with a 471,859-token ceiling against a 262,144-token
window here, and the endpoint refused every request with "This request needs
about N tokens (messages + tools + max_tokens)". Fixed by clamping the ceiling to
the window minus a reserve for the prompt.
The reserve has to survive a real conversation, not a test one. The first
version held back 2,048 tokens, which is fine for the word "hi" and useless in
practice. The router counts messages + tools + max_tokens together against the
window, and a real Pi request carries the system prompt plus every tool's schema —
on the order of 20,000 tokens. gpt-oss-120b was publishing a 117,964-token
ceiling inside a 131,072-token window, leaving about 13,000 tokens for input.
Every call failed with 400 as soon as the conversation had anything in it.
maxTokens must never be null. Pi's model list calls .toString() on it,
crashes with "Cannot read properties of undefined", and takes the entire list
down with it.
compat is never copied from a catalog. OpenRouter ships a field called
thinkingFormat: "openrouter" plus seven other flags describing how OpenRouter
wants reasoning requests framed. This gateway speaks the plain OpenAI shape,
verified by sending reasoning_effort and watching what came back. Copying those
flags would silently change the request format on a gateway where nobody tested
them.
Accepting a value is not the same as implementing it. For glm-5.3, the
gateway accepts all six effort levels. The vendor's own card says the model only
implements three of them. The extra three are accepted and then quietly ignored —
which is worse than refusing them, because Pi would offer you a thinking level
that does nothing at all.
A claim and a silence are different things, and this applies per level. If a
vendor states a thinking level — even null, meaning "not supported" — that
answer is final. If the vendor does not mention the level, that is silence:
another catalog's answer is used for that single level. So a reseller's short
list cannot delete levels the vendor declared, and a vendor's short list does not
block the missing ones from being filled in.
A scoped package publishes as private by default. npm publish failed with
E402 "You must sign up for private packages", which sounds like a billing
problem and is not one: private packages need a paid plan. Fixed by declaring
"publishConfig": { "access": "public" } in the manifest, so a plain npm publish does the right thing.
🔑 Authentication
/login EnClave
Your key is stored by Pi in ~/.pi/agent/auth.json; you never edit it by hand.
The catalog itself needs no key — only the two network steps do: checking which
models your key can reach, and confirming they still answer.
📊 Models
Each model's values are worked out and written to providers.EnClave.models in
models.json. Every entry also records where its numbers came from, so you can
audit them rather than trust them:
"donor": {
"source": "openrouter",
"matchedId": "qwen/qwen3.8-max-0902",
"corroborating": ["opencode", "opencode-go", "qwen-token-plan", "…"],
"rule": "corroborated"
}
matchedId is the complete id, prefix included — not the shortened name. That
matters because the same model carries different prefixes in different catalogs.
Keeping the full id means you can see exactly which entry was used, including
when it was found through a dated variant.
What a donor may not set
| Field | Owner | Why |
|---|---|---|
contextWindow |
the live catalog | It states what this endpoint actually serves. |
cost |
the live catalog | A catalog's price is for a different reseller. |
compat |
never inherited | thinkingFormat: "openrouter" and friends describe how OpenRouter wants reasoning framed. EnClave speaks the OpenAI shape. |
The ceiling clamp
An output ceiling larger than the context window is not an ambitious claim, it is an impossible one: a request cannot ask for more room than the window contains. The endpoint says so plainly:
This request needs about N tokens (messages + tools + max_tokens)
So a catalog value is clamped to the window minus a reserve for the prompt. It cannot be equal to the window either — measured directly, 262,144 was rejected while 261,120 passed.
A value that already fits is used exactly as given. The clamp only removes impossibilities; it never second-guesses a catalog that stayed inside the limit.
It helps to know what the published number actually means: it is a ceiling, not
a fixed request size. On every turn, Pi lowers it to
min(published, contextWindow − prompt − 4096).
🧠 Reasoning controls
Every model gets a thinkingLevelMap, which says which thinking-effort levels it
really accepts. Think of it as a row of switches. A level set to null is a
switch that is simply not offered, so Pi picks the closest supported level instead
of sending one the gateway will reject. When off: null is set, thinking cannot
be turned off at all, and Pi hides that option completely.
Two models carry a hand-written vendor card that overrides what the catalogs claim, because the catalogs are more optimistic than the models actually are:
| Model | Card says | Catalog claims |
|---|---|---|
glm-5.3 |
131 072 out · low, high, max · text only | 943 718 out · six levels |
glm-5.2 |
131 072 out · high, max · text only | 943 718 out · six levels |
⚙️ Configuration
| Variable | Effect |
|---|---|
ENCLAVE_API_KEY |
Credential for the maintenance script. The extension takes it from /login. |
PI_ENCLAVE_LIVE=0 |
Kill switch — freezes the catalog. |
🚀 Development
git clone https://github.com/Lokeraar/pi-EnClave-bridge
cd pi-EnClave-bridge
npm test
Where each value comes from
Pi bundles one catalog per provider inside pi-ai:
<active Pi package>/node_modules/@earendil-works/pi-ai/dist/providers/data/<provider>.json
The bridge discovers Pi's active package at runtime; it does not hardcode a Termux, Windows, global-prefix or agent-specific path. It reads the catalogs in that active package whether or not the user has logged into those providers. Supported agent-local store and flat-install layouts are fallbacks. Updating Pi updates this source of model knowledge; restart Pi or refresh the provider to load the new catalogs.
Models whose id ends in free are skipped: they routinely ship with capabilities
cut down, so their numbers describe a reduced product.
The order
the vendor's own model card
> the catalog named after the model's vendor
> the value most catalogs agree on
> openrouter
> what is already written
The vendor's own catalog decides, and it is not one vote among many. A
catalog the vendor maintains records what the model implements. A reseller's
records what one gateway happens to accept, and gateways disagree: for
deepseek-v4.1-flash, eight catalogs say maxTokens 384000 — DeepSeek's own
first among them — while openrouter alone says 943718, a figure this endpoint
rejects. So when the vendor's number differs from everybody else's, the
vendor's number is the one published.
A model is matched to its vendor by family, not by guessing: kimi belongs
to Moonshot, claude to Anthropic, gpt to OpenAI, glm to zai, deepseek to
DeepSeek. This cannot be derived automatically — every catalog stamps
provider on its entries, but a reseller stamps its own name on a model it
resells. So it is recorded as data, one line per family, the same way the dated
model aliases are recorded.
When there is no vendor catalog, agreement decides. A model like
qwen3.8-max has no first-party catalog in Pi, and there the value most
catalogs agree on is what stands. It needs a simple majority of the catalogs
that state the field: three votes out of ten is a plurality, not corroboration,
and a plurality falls back to openrouter. Nothing is averaged — a number nobody
published is not a consensus, it is an invention.
A ceiling that reaches the model's own window is treated as silence. Some
catalogs write the row that way: 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 be a statement about output, and the field falls through to the
catalogs that state a ceiling a model can serve.
A model is found by its human name as well as its id. Pi's vendor catalog
writes id: "deepseek-flash" with name: "DeepSeek V4.1 Flash", so the id the
endpoint uses and that name are the same key once punctuation is stripped. This
is an exact match on a different field, not a similarity guess, which is what
keeps it safe: 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.
Maintaining the curated values
This script is the only thing that writes to models.json. Run it without
arguments and it reports only — it shows what it would change and stops. Adding
--dry-run makes that explicit:
node --experimental-strip-types scripts/sync-models.mjs --dry-run
node --experimental-strip-types scripts/sync-models.mjs
Its report answers four questions: which source supplied each value, which other
catalogs agreed, what is still unknown, and what it deliberately excluded. When
you run it for real, it writes models.json and leaves a backup beside it.
To record a correction from a vendor's documentation, add it to VENDOR_SPEC in
donors-enclave.ts — and write down the reason in the same entry, so the next
person can check it against the model card instead of taking it on faith.
Tests
node --experimental-strip-types scripts/test-sync.mjs
107 checks that run offline, with no network and no API key. They cover: matching
a model by its id and by its written name, near misses that must not match
(dated variants), authority of the model card and the vendor's family, reasoning
maps resolved level by level including partial vendor maps and explicit null
values, majority fallback, exclusion of router aliases, exclusion of free
models, rejection of impossible ceilings, handling of router aliases, and the
different folder layouts Pi can be installed into.
License
MIT