Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
Review findings on #1977, each verified before acting: the 2026-08-03
declared-provider-catalog note is updated in place and cross-linked both
ways now that reasoningEfforts/compat reopened half of its rejected
alternative; resolveModelCompat inherits the catalog entry's compat only
while the resolved api still is the entry's own, so a route-level api
repoint no longer merges another protocol's shape as a completions base;
the off-with-value promise gains a request-boundary test proving pi-ai
reads thinkingLevelMap.off when the reasoning option is absent (and the
catalog-level test name stops overclaiming); the cannot-stop-thinking
wording narrows to what is actually enforced (no Off offered, explicit
Off refused — an effortless request goes out bare); the z.const(null)
comment attributes null passthrough to schemastery's nullable
short-circuit; the baseten drift-gate claim names its verification
source; and the layered-merge delete gap for dict keys is documented
under Known Limitations with the atomic-leaf follow-up in #2003.
A model entry's reasoningEfforts dict declares its selectable thinking
levels — key = offered level, value = the wire spelling dispatch sends;
only off may leave the value empty (supported, send nothing). false
strips reasoning from a catalog model; every level is materialized
explicitly into pi-ai's thinkingLevelMap so nobody has to know pi-ai's
asymmetric absent-key defaulting. compat.thinkingFormat and
compat.supportsReasoningEffort become configurable on the route and per
model (model > route > catalog entry > pi-ai's URL-derived guess),
openai-completions only, so a private gateway speaking the DeepSeek
reasoning dialect no longer depends on its URL being recognizable.
Record-typed drift gates pin both enums to pi-ai's, and an unserviceable
declaration is refused at the write that produced it, naming route,
model, and level.
`resolveModel` validated the profile's reasoning level against the exact
model and threw when it did not fit. That call builds the model catalog,
and the catalog build catches per PROVIDER — so one mis-set field took the
whole provider out of every picker behind a single error row, hiding even
the models that do support the level. Measured: `anthropic` set to `max`
threw for six of its eight models.
Describing what a model can do now reports an unusable profile level as no
default rather than throwing; the request path still refuses it, which is
where a bad configuration belongs. The existing spec asserted the old
throw and now asserts both halves of that split.
Known gap, left deliberately: a model that cannot take the route's level
still fails its first request while the picker shows 「Default」 for it,
because the request path keeps using the profile level as the fallback.
Reaching that needs a hand-written `settings.yaml` — the Models page no
longer writes the field — and the error names the model and the level, so
selecting a supported level is a way out. Closing it properly means giving
`AgentOptions` a `reasoningEffort` so compositions without a model picker
keep an entry point, then dropping the provider-scoped field altogether;
that is its own change.
Three defects surfaced while driving the Models page.
A hand-declared model needed an explicit contextWindow and maxTokens,
but a provider listing usually returns ids and nothing else — so the
page happily wrote a profile the adapter then rejected, which took the
whole namespace down silently. Capacities now fall back to the route's
`defaultContextWindow` (262,144) and `defaultMaxTokens` (32,768). Both
are guesses by construction, which is why they are route fields a
deployment corrects once rather than constants buried in the adapter;
the fallback sizes the model and never becomes a per-request cap.
That silent failure was the second defect. A schema-valid profile the
adapter could not serve was stored and only rejected later, disabling
every route in the namespace with nothing said. `dsh-settings` gains an
optional `validate` on registration — a check for what a schema cannot
express — and `llm-pi-ai` refuses an unserviceable section at the write
that produced it. A stored section that fails keeps the namespace's last
good value, as a schema failure already did, so an externally edited
document still cannot strand the owner. The plugin's own last-good
fallback goes with it: nothing reaching it can fail any more.
Third, a model with no reasoning metadata advertised the single level
`off`, which pi-ai translates to *omitting* the reasoning option — the
same request naming no effort produces. Selecting it disabled nothing,
so a provider whose default is to think kept thinking with `off` shown
as selected. Such a model now reports no reasoning capability at all,
which is the seam's way of saying the control is unavailable, and the
per-model `reasoning` flag is gone: without a thinkingLevelMap to spell
levels it could only invent them.
The protocol table narrows to the three a hand-declared route reaches
today, most-reached first so a surface offering a choice defaults to the
one gateways actually speak.
A pi-ai route had to name an installed catalog provider, served that
catalog's models verbatim, and could override only the endpoint. An
OpenAI-compatible gateway, a self-hosted server, or a model newer than
the pinned pi-ai release was therefore unreachable, and a stale context
window could not be corrected without upgrading the package.
A route is now a declaration whose defaults come from the installed
catalog. `catalog.ts` merges that catalog under the profile's own model
entries, `provider.ts` builds the pi-ai Provider (reusing the catalog
provider when the route keeps its protocol, so implementations this
package cannot reconstruct keep working), and the adapter serves every
operation from one `createModels()` collection. That also retires the
`@earendil-works/pi-ai/compat` import, which pi-ai documents as a
temporary entry point it deletes with its ModelManager migration.
Credentials stay on the harness seam: the resolved key rides the request
as pi-ai's highest-priority auth override, so `Models` holds no
credential store and a named-but-missing reference still fails loud
instead of falling back to an unrelated ambient key.
A model's configured maxTokens now reaches the seam as defaultMaxTokens.
Two had real security consequences:
The bootstrap rejection ran on npm dotenv's parser while process.loadEnvFile
applied the file with Node's own. Two independently maintained dialects meant
the check and the thing it guards could disagree: a name Node accepts but the
checker misses would reach process.env unchecked, and BASH_ENV there runs a
file of the project's choosing on every `bash -c` the bash tool issues. Parse
once with node:util's parseEnv — the same engine loadEnvFile uses — and assign
the entries already checked, which also drops the dotenv dependency.
llm-pi-ai still returned a literal profile.apiKey ahead of everything, and it
registers a settings namespace, so the defect removed from llm-deepseek
survived intact in its design twin. The field is gone from the profile schema,
the resolution path, and the tests.
The rest are consistency and documentation defects the review named:
- verify-config-source-ownership did not scan the Python runtime's bundled
cordis.yml, which still inlined apiKey and baseURL. Both are covered now, and
the line-anchored INLINE_DENY documents that it is a tripwire, not a parser.
- The deny list missed NODE_TLS_REJECT_UNAUTHORIZED, the askpass hooks, the
GIT_CONFIG_* redirections, and PYTHONHOME — all implied by its own stated
rule about what a variable does.
- Snapshot lookups folded case on Windows, where environment names are
case-insensitive and an exact-match Map could miss a higher-ranked layer.
- The credentials note claimed a read-time permission check was "not taken"
while this PR implemented it; the credentials-local README still described
two layers, live process.env reads, dotenv-era limitations, and a renamed
anchor; the llm-deepseek README still advertised the removed literal apiKey;
and web.ts and base.cordis.yml kept personal-overlay wording.
- The ownership note's literal-apiKey claim now names its scope: the
web-search providers keep a literal field but register no settings
namespace, so nothing can shadow a stored credential through them.
Four review findings across the seam and both adapters.
registerAdapter now returns a handle carrying replace(providers): the
candidate route set is validated in full before anything moves, so a
route another adapter owns leaves the previous registration intact, and
the swap itself is one synchronous section with no observable gap. pi-ai
uses it instead of dispose-then-register — the old shape dropped every
route when the new set conflicted, and its facts cache could then equal
the registry's, so reverting to a working configuration never re-applied.
Its registration facts are also sorted by provider, so a settings
document that merely reorders keys no longer triggers a swap.
DeepSeek's per-request snapshot now carries the credential facts, and
resolveApiKey receives it instead of re-reading the raw config: a
settings generation the resolver rejects can no longer contribute its
literal key to a request the previous generation's endpoint serves.
pi-ai only defers to the SDK's provider-native discovery when a profile
names no credential at all; a configured apiKeyEnv that misses now fails
with MISSING_CREDENTIAL naming the route and the reference, instead of
handing pi-ai undefined and letting it authenticate with an unrelated
ambient key.
The eager boot-time credential probe is gone: it could run before the
credentials service mounted and reported every failure as a missing key.
The route stays registered and browsable; the first request gives the
accurate error, whose guidance now leads with the credential store and
mentions a literal apiKey last.
- gate model selection on steering-placement image carriers from enqueue
until their steering/message event publishes; release the gate when an
admission ends idle without publication (both behaviorally asserted)
- reject session.updateQueue edits carrying non-text blocks at the RPC
boundary (queue edits cannot bypass image admission)
- extend the durable-directory walk past a first-created DSH_HOME to the
deepest pre-existing ancestor
- strip Windows-style separators from attachment display names on POSIX
- verify attachment reads with a header-only probe (digest already proves
the bytes decoded fully at admission); document the read path
- make SessionInputShell.addImages refusal observable and keep workspace
transfers/composer intake from leaking refused drafts
- own ONE recursive image walk (dsh-llm contentHasImage) across apiproxy,
pi-ai, compact-basic, and the DeepSeek text-only assertion
- drop the redundant canonical-base64 regex and the no-op role read
- move AttachmentId/AttachmentError out of types.ts (brand.ts/error.ts);
document why AttachmentError does not extend HarnessError
- document the hard attachments inject in both consumer READMEs
An empty or omitted providers dict is now the valid dormant posture: the
adapter mounts with zero routes and no catalog entries, registers routes
the moment the llm-pi-ai settings section supplies profiles, and drops
them when it empties. The TUI demo mounts the adapter bare, so adding an
openai/anthropic provider is purely a settings.yaml (or, next PR, web
form) operation with per-request apiKeyEnv credential resolution.
providers becomes a dict keyed by provider route, so the composition base
and the llm-pi-ai settings section merge per provider and the route set is
structural; the pre-release array shape and per-profile provider field fail
loud with migration directions. The adapter reads a profiles thunk once per
operation and resolves the credential per stream call (literal apiKey, then
apiKeyEnv through ctx.credentials with an ambient env fallback, then pi-ai's
provider-native discovery), so key, endpoint, and knob changes reach the
next request without restarts. Route-set or captured-retry-policy changes
re-register the same adapter instance in one synchronous section; an invalid
settings snapshot keeps the last good profiles.
- dsh web gains --provider/--model: a non-deepseek provider mounts the
matching pi-ai catalog route (ambient credentials), making image input
reachable from the shipped Web assembly; requires an explicit --model
- attachment-local syncs the publication directories after the hard-link
publish so a reported durable reference survives a crash (POSIX; Windows
relies on filesystem metadata journaling)
- the attachment seam gains storage-free validateImage; the host validates a
complete multi-image prompt before persisting any member, so one malformed
image cannot strand valid members as unreferenced objects
- startSession sends before navigating: a rejected first send keeps the empty
state, its error strip, and the complete draft mounted
- the webserver rejects an undeclared-length body the moment it crosses the
configured limit instead of draining a potentially endless stream to EOF
pi-ai 0.80 restructured its entrypoints: the static catalog reads now
live on /providers/all as getBuiltinModels/getBuiltinProviders (keyed by
the catalog-only BuiltinProvider type, replacing KnownProvider at those
call sites), and the global streamSimple moved to /compat. The config
schema gains the new 'max' reasoning level.
Pass each resolved catalog model capacity into pi-ai stream conversion so the upstream full-message classifier can recognize provider-specific, silent, and length-stop overflow signals. Retain the harness text fallback for legacy provider wording and cover the catalog-resolution path with a mock-provider regression.