Repository navigation
feat: the plugin owns the opencode and opencode-go routes - #113
Merged
Merged
Conversation
- Stop the run on a Node below engines.node, before collecting a file. Below the floor, failures came from the engine (node:sqlite, Intl.DurationFormat, ICU strings) and looked like code regressions. .node-version names the Node CI uses. - Add a CI job that runs the unit and mock suites on Node 24, the floor engines.node promises but no job ran. - The zh fallback duration unit is now 分钟, the platform's own narrow output, so the meter row reads the same with or without Intl.DurationFormat. The DurationFormat tests restore the engine as they found it, rather than leaving a stand-in installed or asserting the engine ships the constructor. - Move the node:sqlite store fixture out of the helpers that the jsdom tests share, into test/support/opencode-store-fixture.ts. - Add an architecture test that walks the client bundle's runtime import graph from settings-page.tsx. It forbids node built-ins, the cordis runtime, pi, and process.env. Checked by planting a violation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…n discovery - A rejected internal-route mount ran from a `void` IIFE with no catch, so a Google bridge failure became an unhandled rejection, which Node's default mode answers by exiting the process. mountInternalRoutes now owns the mount and its withdrawal, and reports a rejection. - Three early exits in registerResponsesProvider (internalRoutes off, every route declared, no llm-pi-ai) answered `undefined` after the Gemini bridges had already registered. That left them with no disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand back the bridges' disposer. - A mount that failed to start, or that captured nothing, kept its fiber and every route it had registered. Both paths now release the routes and dispose the fiber. - Model discovery was registered on every activation, and the host's duplicate refusal was swallowed, so the first activation's settings answered for the life of the process. It is now registered once per root and answers from the latest activation's settings. Each namespace registers on its own, so one refusal no longer skips the rest. - Move three doc blocks back above the functions they describe. Each fix has a regression test. The two mount tests fail without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…e the suite Request path and meter: - Price a turn on its route's plane. Go-first lookups priced every Zen turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48. - The listing and model-info patches now act only on the routes this plugin claims. Before, a direct anthropic or openai route lost rows the OpenCode routing called "unservable", and a refused foreign id was answered with OpenCode's catalog. - A relay's Go /models is answered with the Go catalog. The enrichment re-checked the URL against the default gateway markers and fell back to Zen. - The free-model check reads a Request-carried body. That is how DSH sends a turn, and every keyless free turn was reported as a missing credential. The body is now read once for both uses. - The meter's session fallback is the session that most recently ran a turn, not the most recently created one. - A captured key's tier comes from the observed URL, then the route, and never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a working Go key was filed under Zen. An OpenAI sk- key on a claimed relay was filed under Go. Suite isolation: - The unit and mock suites refuse non-loopback requests. Each unit run sent 54 requests to models.opencode.ai through the catalog's background revalidation, so on a networked runner a result could depend on the vendor's live catalog. - Captured keys, session spend and the live catalog are reset after every test (resetCatalogState is new). Shuffled runs failed catalog cases on seeds 42 and 1234. test:shuffle runs the suite in random order, and the Node 24 CI job uses it. Each behaviour change has a test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…egration - The card's store-inspection row called remote.opencodeStore.inspect, but no client contribution carried that namespace's descriptor. The row reported "unavailable" on a host that had the service. storeRemote now carries it, with a whitelist codec, and the client mounts it. - The host/client parity test missed this because it matched only a string literal in super(ctx, "name"), and the store passes a constant. It now resolves constants, and checks every declared Remote method against a client descriptor, not just every namespace. - inspectStore parses through the whitelist instead of a cast, so a field the host did not mean to send (a token) is dropped. An ok:false envelope reports the remote's own message. - The credential reader's legacy-row fallback was uncorrelated: one flagged opencode-go row hid every legacy opencode row, and the Zen key disappeared. The fallback is now per integration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… keeps - AGENTS.md's repo map described nine modules this build does not have (the Console login, token store, org cache and sign-in row). They are replaced with one line saying sign-in is v2 work. The go-limits duplication note is gone, because attachAllowance calls allowanceFor. adapter-replacement.ts is marked as not wired. The client-bundle line names the boundary test. - CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock, the offline guard, the per-test state reset, where host-only fixtures live, and that a regression test must fail without its fix. - The no-credential warning no longer tells the reader to "sign in through the settings card", which this build does not have. It points at readOpencodeStore instead, and a test asserts the advice. - CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor and its parser (about 1.2 KB of source, not a dependency), following the gate's own instruction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
docs/improvement-plan.md groups the review's findings by cause and orders the fixes in nine phases after this one. Each phase lists its problems, its steps, the test that has to fail first, its acceptance criteria and any decision the owner needs to make. Findings are marked "measured" (reproduced) or "traced" (read in the code). Among the measured ones: the picker and dispatch derive reasoning levels differently for 9 of 95 catalog models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Replace docs/improvement-plan.md with the plan of record. It now holds: - the owner's decisions (D1-D11); - what the DeepSeek Harness host supports, with references into packages/llm, llm-pi-ai, session-controller, gateway and ui-settings-models; - the defects this audit found that are not yet fixed; - the target architecture and the phases, one PR each, with a status table that each PR updates. The decisions that change the code: - Settings offers the whole catalog, and the composer offers only the declared models. - The plugin never writes the user's profile and follows the user's configuration at dispatch. - The adapter patch exists to merge provider modes and nothing else. It is generalized to every mode, and its catalog fill is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Investigation of the host's source and of the pi catalog it pins (1.1.0) changed the routing design. - llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed `opencode` catalog already spans Anthropic, Google, Completions and Responses with per-protocol endpoints. One route serves every mode for every model pi knows. - The plugin fills only three gaps: models newer than pi's catalog (including all Gemini on Go), endpoint drift, and profile-level `api`/`baseURL` collapsing the modes. - The new design is one unregistered catalog adapter per plane, keyed by the user's route id, and the adapter patch (merge-modes) delegates only gap models to it. That deletes the hidden routes, the listing hide, the stream redirect, the child mount and the Gemini bridge registration. - A real-host test harness (P-T1) comes before any of it. - One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1) would remove the routing layer entirely. It would also let Settings save a model pi doesn't know, which pi-ai's strict write validation rejects today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
pi-ai registers exactly the providers in its resolved config, and the fiber keeps whatever the internal/config waterfall returns, while the config editor writes the raw value. A prepended listener that resolves pi-ai's config without opencode/opencode-go therefore keeps pi-ai from claiming them, without touching the user's file, and the plugin registers its own PiAiAdapter under the same ids through the public API. Measured on the real host with pi 1.1.0 and the harness's patch: both routes held by the plugin, pi-ai's other routes unchanged, every protocol on both planes (Gemini on Go included) sent to its own endpoint, a catalog-unknown model id saved, listed and dispatched, and both routes handed back to pi-ai on release. Replaces the upstream asks (no upstream changes) and the save-gate design, which is kept only as the fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
A nine-lens review of the routing design, with two independent skeptics per finding (78 of 82 upheld), changed five things before any code moves: - Settings reads pi-ai's resolved config, so a read-only wrapper on settings.describe puts the plane entries back into the displayed value; writes were never affected, the Models page edits by path. - The route claim gets an explicit lifecycle: commits are told from validations by identity, reconcile runs on commit, mount and teardown are ordered, and pi-ai restarts at most once per transition. - The plugin's adapter is built from public pieces only, with pi-ai's credential rule and no captured keys on the adapter path. - The protocol comes from the catalog map, never from a guess at the model's name; metadata and levels are catalog first. - Releases stay on 1.x, phases land as stacked pull requests, and a real host harness plus a binary e2e job pin the behaviour first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
barrel does not have, an ESM link error on every host load. It now
forwards `export *` only, written by one shared helper that a test links
against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
Those statuses now pass through; a 404 is still filled from the catalog,
because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
purge result" and cast the answer; it now shows the host's message and
parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
never sends. It now names the per-protocol inference family, and auto as
its alias, which is what the code already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- Delete adapter-replacement.ts and its test. Nothing imported it, and deleting another plugin's registry entry is not a host API; the engineering notes record why, and what the plugin does instead. A gate fails the build if src/ writes the llm registry map directly. - Delete the stream hook's `prepared` handling: the host passes the prepared dispatch as its own argument and never reads options.prepared. - Untrack .freebuff/ and ignore it. - The settings partition test walks Config.dict, which is how debugFile went missing from it; a manifest test checks cordis.patch.yml against the schema and the field registry, and the manifest's phantom showUsagePrice, wrong marker text and incomplete lists are fixed. - utils/retry.ts takes its timer and logger as parameters, and the utils purity gate now also rejects console and global timers, walking the folder recursively. - tool-fallback reads free-tier ids from the request's own plane (Go iff opencode-go or opencode-go-*) and loses its unreachable Anthropic branch. A Zen-only free model is no longer treated as free on Go. - test/support/src.ts is the one door tests use into src/, so later module moves touch one file; CONTRIBUTING.md states the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The barrel is the host's contract and nothing more: apply, Config, inject, name and the config types. Everything else is internal, and test/public-api.test.ts pins the list, so widening it is a reviewed change. The 1.x line keeps its version scheme: nobody consumes the removed exports (D6). - Every test reaches src through test/support/src.ts, importing each name from the module that defines it; a gate rejects a static src import anywhere else outside its named exemptions. - package.json no longer exports ./src/*, and check.ts fails when an exports target is outside the published files. - resolveRoutedKey is deleted: it was exported and never called. - The internalRoutes setting is deleted; the per-protocol routes always mount until the plugin owns the plane routes. A row that still carries it validates and the key is ignored. - @viztor/dsh-opencode is no longer published. The deprecation message already on npm stays as its final word, and check.ts asserts that the release workflow verifies exactly what publish-scoped.ts publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The test install now runs the host users run. - pi is pinned to 1.1.0, the version the harness's own lockfile pins, with the harness's patch applied verbatim (patches/, hash recorded by pnpm). A pnpm override holds every copy in the tree to it, not just the direct devDependency. - The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the version a registry install of the published dsh resolves (cordis 4.0.5, loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor, dsh-settings, cordis-plugin-loader) are added as devDependencies. - test/support/host-versions.ts names those versions once. test/host-versions.test.ts checks that the pi the plugin loads at runtime is 1.1.0 and the same copy the tests import, that the patch is applied, and that the installed cluster matches. - mount-protocols now checks that every catalog SDK maps to a protocol pi's exported api modules implement, and records as a characterization row that pi 1.1.0's opencode-go provider has no Google API. No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and schemastery are code-identical to alpha.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.
test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.
On top of it:
- contract tests for the waterfall, the profile and Config shapes,
credentials, and the config editor (host-waterfall, host-shapes,
host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
where P-R1 is expected to flip it, with S0_TODAY shared by the
binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
e2e-host CI job (a pinned, patched cell on PRs that touch the host
seams or vite.config.ts, and a floating cell nightly and on
dispatch), and the test:e2e:host script.
The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add src/host/patch-method.ts (patchMethod), now the only way the plugin wraps a method on a host object, and move the four instance patch sites in src/models-discovery.ts onto it: the three listings hidden by hideResponsesRoute, decorateModelDiscovery, decorateModelInfo and decoratePlaneAdapters. On the real host the three llm-level disposers never restored: ctx.llm is cordis's traceable proxy, every method read returns a new shadow proxy, so the `===` identity checks were never true and each reload stacked a wrapper over the last. The helper reads identity from own descriptors instead. A dispose restores only while its wrapper is still current (otherwise it becomes a pass-through), deletes the own property when the original was inherited, and a same-site reinstall wraps the unpatched method rather than the earlier install, so either disposal order ends at the exact original. decoratePlaneAdapters becomes one patch per (adapter, method) that composes the adapter's plane routes in registry order, which keeps today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is built and tested but wired to no site yet. test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and the reverse order) for every site on own and inherited originals, on the real host (tier A) and through apply(); test/patch-method.test.ts covers the helper's contract. The existing per-site pins in test/models-discovery.test.ts stay unchanged; five new cases there record the deliberate edge changes on hosts that refuse the patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness` went red on every branch of this stack. This takes the two generated files exactly as the nightly refresh produced them (#111), with no hand edits, and updates the counts that hard-code the catalog's size: the Go shim test (28 -> 29), the plane table in redirect-planes.md (Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go completions list in both READMEs. The listed half of "29 served / 33 listed" is models.dev's count, inferred from the one added entry, because models.dev cannot be reached from here. It no-ops once main carries #111. P-T2 turns these counts into invariants so a refresh stops needing this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plugin now serves the opencode and opencode-go routes itself, through supported host paths only, and the hidden-route workaround is gone. A prepended internal/config listener hands pi-ai a copy of its config without the two plane entries, so pi-ai keeps every other route. On a commit the plugin reconciles: it builds one plane adapter, a subclass of the host's PiAiAdapter, over pure profiles (utils/plane-profile, plane-model, model-metadata and protocols) and registers only the planes that are declared. A Settings candidate has the plane entries it changes validated strictly. A read-only settings.describe graft puts the committed plane entries back, so the host still sees them. Deleted: responses-provider, responses-routes, google-bridge, pi-provider and utils/google-replay; the listing hide and the discovery decoration; the stream redirect; the D19 settings; and the hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml. What users see: any saved id is accepted and served, and an id no catalog knows dispatches as completions. The protocol comes from the catalog and model info follows the user's own row first. A model whose reasoning is only on or off offers Off/On. The default endpoint is the per-protocol inference family. Keys resolve by pi-ai's own rule, and a plane lists its declared ids even when no key resolves. When several pi-ai rows declare planes, the row the plugin serves stays the same while it is live and still declares one, so no row is left stripped with nobody serving it. If the host's pi-ai module cannot be read, the plugin steps aside even before any plane is declared, so a later Settings save goes to pi-ai's own check. The characterization rows this changes are flipped to new baselines; every other row stays green unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
# Conflicts: # README.md # README.zh-CN.md # docs/redirect-planes.md
The vendor renamed the Go plane's one Gemini model (dropping its contributor suffix, halving its price and publishing an allowance for it where the limits table had none) and added a free model with no SDK, so `catalog freshness` went red on every branch of this stack. This takes the two generated files exactly as the nightly refresh produced them (#111), with no hand edits, and follows them: - the Go Gemini fixtures in six test files (attachment-services, catalog, go-google, go-google-prepare, models-discovery, responses-routes) now name the renamed id, since each means "the Go Gemini model the catalog serves"; - the usage panel test built its unknown allowance from the table's one unpriced row, which the vendor has now priced, so it builds the `Number.NaN` allowance the generator emits itself; - the Go shim count in catalog.test.ts (29 -> 30); - redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell 18 -> 19, and "0 of 29" -> "0 of 30" in the prose; - both READMEs: the renamed Gemini id, and the new model in the Go completions list, since it declares no SDK. The listed half of "30 served / 34 listed" is models.dev's count, inferred from one rename (no change) and one addition (+1), because models.dev cannot be reached from here. The dated S0 measurement row in improvement-plan.md keeps the id it was measured with. It no-ops once main carries #111. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The live routing suite picks its free-tier subjects from the catalog by asking the gateway, but it counted "400 Upstream request failed: Model is unavailable" as an observable answer. The upstream withdrew nemotron-3-ultra-free while the catalog still lists it, so the picker chose it and two cases asserted on a model the gateway no longer serves; the reachability case also failed on a 429 throttle. A withdrawn model (that 400, or a 410) is now skipped like a throttled one. The free-tier routing case only accepts a candidate whose answer is the free-tier gate it asserts, so an unmetered model cannot become its subject, and the reachability walk moves past gated, withdrawn and throttled candidates before it reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The review ran three rounds through the correctness, rules and matrix lenses. What it found, and what changed: - The binary e2e's beforeAll budget was smaller than the blocking subprocess deadlines it serializes. Each plugin install now has its own deadline, the hook's budget is derived from the installs and the boot, and afterAll stops a host a failed hook left running. - providerWithApiKey claimed to be a line-for-line restatement but skipped upstream's credentialRef(ref) check. It now throws the same TypeError for a ref outside the grammar (a host-editor-contracts case pins it), and its comment names the deliberate differences. - The pinned e2e-host cell let cordis and the loader float at patch level. The overrides now force both, the cache key includes them, and the job asserts them against HOST_VERSIONS. - The pi-ai-first characterization asserted every facet under one KNOWN_DEFECT title. Each apply order now generates one test per row and facet, titled with that order's own tags. - CONTRIBUTING said the contract tests pin F11-F20, and the plan said P-T1 pins every fact. Both now give the same accounting: which facts are pinned by number, which P-R1 exercises first, and why F9 and F17 are not pinned. - CONTRIBUTING put the e2e's dummy keys in the profile's credentials file; they are in the temporary home's credentials document. - F5 had no real-host pin: host-shapes now pins that a route-level api and baseURL collapse pi's protocols onto one. F6 is recorded as first exercised by P-R1. - The credential-mapping row pinned only the api-key branch of toPiCredential. host-credentials now covers all three branches, and the env branch is asserted through pi's Models.getAuth, so the case fails without it. - The plan's reason for restating hasProviderApiKey was inaccurate; it now says no installed copy reaches the function. - The new credential cases cited toPiCredential at the wrong lines; they now cite auth.ts:74-84. - F4's listing and dispatch halves were pinned only by KNOWN_DEFECT rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins that an id pi cannot place is not listed and that a turn on it is INVALID_CONFIG with no request; the save and deferred-read halves are tagged F4 too. - The one facet that depends on the apply order lost its tag when the pi-ai-first block was split. The fixture case now checks only structure (the facets whose value moves are the facets whose tag moves), and the exact pin is its own KNOWN_DEFECT case that boots no host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's override and turns that case into a BASELINE check in the same diff. Nothing under src/ changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
A plane entry in a pi-ai row that resolves credentials in a realm the plugin's row does not share, or one whose headers carry an x-opencode-plugin key, is left to pi-ai: it stays in pi-ai's config and pi-ai serves it with its own PiAiAdapter. The OpenCode Console setup writes exactly that shape, and the claim used to strip those entries and serve them through the plugin's credentials, which do not hold the Console's token, so every Console turn on a plane failed MISSING_CREDENTIAL and key capture recorded the workspace token. - src/utils/plane-ownership.ts (pure, 100%): the realm as the loader reads it, the marker, the taken and stepped-aside planes, the log line. - route-claim: every reading of "pi-ai still holds a plane" or of the plane set counts only the planes the claim takes (served row, rebuild, mount and handover restarts, reconcile's wait, validate, teardown handback); one log line per stepped-aside entry; a read-only steppedAside() accessor. - stream hook: no decoration of a turn on a stepped-aside plane. - fetch patch: a request carrying the marker passes through untouched, with no header, key fill or key capture. - tier B gains a realm option (realmCredentialsPlugin), and test/host-planes-foreign.test.ts performs it on the real host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plan records the decision (D27) and the route-claim design reads the taken planes; P3's discovery complement must exclude stepped-aside planes. Both READMEs gain "With OpenCode Console installed" and a 1.x bullet; AGENTS.md and CONTRIBUTING name the new module, the accessor and tier B's realm option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The claim's step-aside (D27) left four gaps, each reproduced on tier B: - A plane stepped aside by the realm alone (no x-opencode-plugin header) was still claimed by the fetch patch through its gateway URL: our session, origin and User-Agent headers went out on it and the realm's token was captured for the meter. The stream hook now runs such a turn under a `foreign` turn state, which the fetch patch passes through before its URL claim. - A live save that added the marker to a plane the plugin served left the route with no adapter: pi-ai registered it while the plugin's handle still held it. A commit run now releases a plane its candidate no longer takes through the live handle before it returns. - The realm was read off each row's own options, so a group's isolate was missed both ways. It is now compared by the key each context resolves credentials under (cordis's realm map), the labels kept as the fallback. - The meter's discovery read a stepped-aside entry's key reference and base. It now skips a row in a realm its context does not resolve and any marked provider row; the 1.x Settings discovery wrapper also leaves a stepped-aside plane's Fetch to pi-ai. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…he meter and live saves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…restart A loader edit of llm-pi-ai's isolate alone swaps its realm map in place with no config run and no pi-ai restart. The reconcile it schedules then released a plane the claim no longer takes while pi-ai still held the stripped config, leaving the route with no adapter; the reverse left pi-ai holding a plane the claim takes, for good. The reconcile now restarts pi-ai once it settles in both directions (after releasing, when pi-ai lacks a plane it now keeps), asked once per need. Tier-B cases perform the edit both ways on both apply orders and send a turn after each, plus the reason change the log promises (marker, then realm). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
It skipped neither the x-opencode-plugin marker nor a row whose credentials resolve in a realm its context does not, so a click could rewrite the Console's override row (or fail on it in a home patch). Such entries, and references to a route only they declare, are now left alone; with no plane entry of the user's own the purge refuses and writes nothing. Unit cases and tier-B cases on a real realm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… real host The served row is chosen by the planes the claim takes (a realm-isolated or marked row ahead of it is passed over), the steppedAside accessor subtracts what the served row takes, and on tier B a save to a marked entry beside a taken plane is judged by pi-ai's check alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… planes The claim reconciled on `loader/partial-dispose` only for pi-ai's row. A live edit of the plugin's own row's `isolate`, or of a group around either row, swaps a realm map in place with no config run, no restart and no registry change, so the plane stayed where it was while the live `steppedAside()` already disagreed: joining pi-ai's realm left the plane on PiAiAdapter with this plugin's decorations on its turns, and leaving it kept the plane on PlaneAdapter, resolving the route's key in the process-wide realm. Every row's partial dispose now schedules the reconcile, whose give/take restart moves the plane once. Tier B cases, both apply orders: the plugin's row joining and leaving pi-ai's realm, and a group around the plugin's row moving into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…he commit run On the loader's volatile path the served row's raw config already holds the candidate when its commit run arrives. Once the marker leaves that row nothing the claim takes, `servedRow()` moves to another live pi-ai row (a plane-less one ahead of it, or one declaring a plane of its own), and `claim()` returned before releasing the plane the plugin still held: pi-ai's registration of it, right after the run, met a duplicate. The release is now decided for the row the registration was built from, before the served-row check, and a reconcile is scheduled. Stand-in cases for both neighbour shapes beside the single-row known answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…plugin Settings -> Models -> Fetch on a stepped-aside plane (by realm, or by marker beside an unmarked plane) answers pi-ai's own list on a real host, a catalog-retired id included, while the plugin's own plane drops it (the known answer); dropping the accessor in `installDiscovery` now fails. The meter's realm rule is performed alone: unmarked entries in a realm the plugin does not share, with the realm's token exported, are neither adopted nor replayed, which a disabled realm skip in go-discovery now fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…rue 1.x history The plan's D27 row and route-claim text name the reconcile on every row's partial dispose and the in-run release decided for the row the registration was built from. engineering-notes says the listener strips only the planes it takes. Both READMEs describe what the released 1.x actually did on a Console route: its hidden-route turns failed for want of the Console's token, and the rest carried this plugin's headers and had their key recorded for the meter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Owner
Author
|
Needed: new values for the repository's Actions secrets Generated by Claude Code |
The nightly refresh (#171) regenerated src/catalog-data.ts: Zen 87 -> 88 with gemini-4-hydrogen ("Gemini 4 Hydrogen", @ai-sdk/google, paid), Go unchanged at 32, src/go-limits-data.ts unchanged. Every document and test that pins what moved follows it: - README.md: the routing table's @ai-sdk/google row 9 -> 10, and the stated total 87 -> 88 (both mentions) with its date 2026-11-12 -> 2026-11-13; the catalog tests parse both the table and the sentence. - README.zh-CN.md: the same three changes, kept in step with the English README for the docs-pairing gate and the bilingual prose-total test. - docs/protocol-routing-and-merge.md: every "N of 87" becomes "of 88", the Google row 9 -> 10, and the counts-as-of note names #171 and its date. - docs/redirect-planes.md: Zen "87 served" -> 88, the shapes table's Google Zen cell 9 -> 10, "0 of 87 on Zen" -> 88, and both counts-as-of notes name #171; the listed halves stay the ones #156 recorded, as #159 left them, since the registry was not re-read. - test/catalog.test.ts: the bundled Zen shim's pinned length 87 -> 88. The free count (12), the Responses/Anthropic buckets and the both-planes overlap are unchanged, because the new model is paid, Google and Zen-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… restating them The header still stated the split of an 85-model Zen catalog (28/30/18/8/1); the shipped catalog is 88 (27/32/18/10/1). Restating the counts here is a second copy the refreshes stopped keeping true, so the header now names the SDKs and points to docs/redirect-planes.md section 2, which test/catalog.test.ts holds to the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The routes module header said the counts are stated once, in docs/redirect-planes.md §2. The README pair and docs/protocol-routing-and-merge.md §1 state them too, each held to the catalog by its own test in test/catalog.test.ts. The header now names §2 as the per-plane table and says the test holds that table, the README's and the protocol doc's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…READMEs The intro's model list and the opencode-responses example named muse-spark-1.3-contributor-free, which the shipped catalog no longer carries. Both READMEs now name muse-spark-1.3, a served Zen @ai-sdk/openai model with the example's context window, output limit and modalities, and the example's display name drops "Free". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh adds the Zen model gemini-4-hydrogen (@ai-sdk/google), so Zen
goes from 87 to 88 served models and its Google count from 9 to 10; Go and
the Go limits are unchanged. Its port on main also replaces the retired
muse-spark-1.3-contributor-free id in both READMEs and stops the routes
module header from restating per-SDK counts. Conflicts, resolved by keeping
P-R1's text and carrying the refresh's facts into it:
- README.md, README.zh-CN.md: P-R1 rewrote the protocol table as three
columns (no "served from" column, Mistral filed as Chat Completions) and
replaced the declare-the-route-yourself section and its YAML example with
"Settings stays the host's". Kept P-R1's table with the Google row at 10,
and P-R1's section, so the example's muse-spark id change has no passage
to land in. The 88 / 2026-11-13 total sentence and the intro's
muse-spark-1.3 merged cleanly from the port.
- docs/protocol-routing-and-merge.md: P-R1 moved the endpoints to the
modern family ("shown on the default (modern) family", /inference/...
paths) and refiled Mistral as Chat Completions; the port moved the counts
to N of 88, Google to 10, the date to 2026-11-13 and added #171 to the
refresh history. Kept P-R1's paths and rows with the port's counts, date
and history clause.
- docs/redirect-planes.md: P-R1 rewrote the plane table's credential
column (the entry's apiKeyEnv, then pi's OPENCODE_API_KEY) and refiled
Mistral as openai-completions in the shape table; the port moved Zen to
88 served and Google to 10. Kept P-R1's columns and protocol with the
port's counts; the dated prose and the 0 of 88 sentence merged cleanly.
- src/responses-routes.ts: P-R1 deleted the module (its table lives in
src/utils/protocols.ts); the port only rewrote its header comment. Kept
the deletion.
src/catalog-data.ts and test/catalog.test.ts (Zen length 88) merged without
conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh's gemini-4-hydrogen is a Zen @ai-sdk/google reasoning model whose effort list is low/medium/high/xhigh. xhigh is outside Google's four thinking levels, so usesCatalogThinkingLevels answers false for it, and the bundled-catalog oracle in test/thinking-levels.test.ts, which expected every Google reasoning entry to be level-only, failed on it: the flip its comment anticipates for "a future Google entry with ... an xhigh". The oracle now names the exception by id, with the value that excludes it, and checks that the exception is still a Google reasoning entry the catalog lists with that value in its effort list, so a renamed or reshaped entry fails rather than leaving a stale exception. Every other entry is held to the same expectation as before, and hydrogen is still asserted (false). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
main's tree equals the catalog refresh port (152d0c0) this branch already merged, so the merge changes no file and only joins the histories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor
pushed a commit
that referenced
this pull request
Nov 13, 2026
main is P-R1 (#113) squashed; its tree equals the P-R1 head this branch already merged (ce889ef), so the merge changes no file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor
pushed a commit
that referenced
this pull request
Nov 13, 2026
…to claude/vigilant-mendel-bctz9n-p3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor
pushed a commit
that referenced
this pull request
Nov 13, 2026
… claude/vigilant-mendel-bctz9n-p-t2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor
added a commit
that referenced
this pull request
Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary
- Stop the run on a Node below engines.node, before collecting a file.
Below the floor, failures came from the engine (node:sqlite,
Intl.DurationFormat, ICU strings) and looked like code regressions.
.node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
output, so the meter row reads the same with or without
Intl.DurationFormat. The DurationFormat tests restore the engine as they
found it, rather than leaving a stand-in installed or asserting the
engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
graph from settings-page.tsx. It forbids node built-ins, the cordis
runtime, pi, and process.env. Checked by planting a violation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(lifecycle): contain the internal-route mount and follow reloads in discovery
- A rejected internal-route mount ran from a `void` IIFE with no catch, so
a Google bridge failure became an unhandled rejection, which Node's
default mode answers by exiting the process. mountInternalRoutes now
owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
every route declared, no llm-pi-ai) answered `undefined` after the
Gemini bridges had already registered. That left them with no
disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
and every route it had registered. Both paths now release the routes
and dispose the fiber.
- Model discovery was registered on every activation, and the host's
duplicate refusal was swallowed, so the first activation's settings
answered for the life of the process. It is now registered once per
root and answers from the latest activation's settings. Each namespace
registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.
Each fix has a regression test. The two mount tests fail without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: price, list and key requests on the plane they belong to; isolate the suite
Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
plugin claims. Before, a direct anthropic or openai route lost rows the
OpenCode routing called "unservable", and a refused foreign id was
answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
re-checked the URL against the default gateway markers and fell back to
Zen.
- The free-model check reads a Request-carried body. That is how DSH
sends a turn, and every keyless free turn was reported as a missing
credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
working Go key was filed under Zen. An OpenAI sk- key on a claimed
relay was filed under Go.
Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
sent 54 requests to models.opencode.ai through the catalog's background
revalidation, so on a networked runner a result could depend on the
vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
test (resetCatalogState is new). Shuffled runs failed catalog cases on
seeds 42 and 1234. test:shuffle runs the suite in random order, and the
Node 24 CI job uses it.
Each behaviour change has a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(store): mount the inspection remote, and read legacy keys per integration
- The card's store-inspection row called remote.opencodeStore.inspect, but
no client contribution carried that namespace's descriptor. The row
reported "unavailable" on a host that had the service. storeRemote now
carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
string literal in super(ctx, "name"), and the store passes a constant.
It now resolves constants, and checks every declared Remote method
against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
the host did not mean to send (a token) is dropped. An ok:false
envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
flagged opencode-go row hid every legacy opencode row, and the Zen key
disappeared. The fallback is now per integration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: describe the tree that exists, and the guarantees the suite now keeps
- AGENTS.md's repo map described nine modules this build does not have
(the Console login, token store, org cache and sign-in row). They are
replaced with one line saying sign-in is v2 work. The go-limits
duplication note is gone, because attachAllowance calls allowanceFor.
adapter-replacement.ts is marked as not wired. The client-bundle line
names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
the offline guard, the per-test state reset, where host-only fixtures
live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
through the settings card", which this build does not have. It points
at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
and its parser (about 1.2 KB of source, not a dependency), following
the gate's own instruction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: add the improvement plan from the architecture review
docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the approved refactor plan, with the host's own contracts
Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
packages/llm, llm-pi-ai, session-controller, gateway and
ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
table that each PR updates.
The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
declared models.
- The plugin never writes the user's profile and follows the user's
configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
is generalized to every mode, and its catalog fill is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): route every mode through the user's own provider
Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.
- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
`opencode` catalog already spans Anthropic, Google, Completions and
Responses with per-protocol endpoints. One route serves every mode for
every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
(including all Gemini on Go), endpoint drift, and profile-level
`api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
the user's route id, and the adapter patch (merge-modes) delegates only
gap models to it. That deletes the hidden routes, the listing hide,
the stream redirect, the child mount and the Gemini bridge
registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
would remove the routing layer entirely. It would also let Settings
save a model pi doesn't know, which pi-ai's strict write validation
rejects today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0
pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.
Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.
Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): fold the reviewed design and the build order into the plan
A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:
- Settings reads pi-ai's resolved config, so a read-only wrapper on
settings.describe puts the plane entries back into the displayed value;
writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
validations by identity, reconcile runs on commit, mount and teardown
are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
host harness plus a binary e2e job pin the behaviour first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: repair what shipped broken in 1.4.1
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
barrel does not have, an ESM link error on every host load. It now
forwards `export *` only, written by one shared helper that a test links
against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
Those statuses now pass through; a 404 is still filled from the catalog,
because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
purge result" and cast the answer; it now shows the host's message and
parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
never sends. It now names the per-protocol inference family, and auto as
its alias, which is what the code already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): P1a is in review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: remove dead code and tighten the repository's gates
- Delete adapter-replacement.ts and its test. Nothing imported it, and
deleting another plugin's registry entry is not a host API; the
engineering notes record why, and what the plugin does instead.
A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
went missing from it; a manifest test checks cordis.patch.yml against
the schema and the field registry, and the manifest's phantom
showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
purity gate now also rejects console and global timers, walking the
folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
opencode-go or opencode-go-*) and loses its unreachable Anthropic
branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
module moves touch one file; CONTRIBUTING.md states the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: a smaller public API
The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).
- Every test reaches src through test/support/src.ts, importing each
name from the module that defines it; a gate rejects a static src
import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
mount until the plugin owns the plane routes. A row that still carries
it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
already on npm stays as its final word, and check.ts asserts that the
release workflow verifies exactly what publish-scoped.ts publishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: pin the host the plugin runs on
The test install now runs the host users run.
- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
with the harness's patch applied verbatim (patches/, hash recorded by
pnpm). A pnpm override holds every copy in the tree to it, not just the
direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
version a registry install of the published dsh resolves (cordis 4.0.5,
loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
test/host-versions.test.ts checks that the pi the plugin loads at
runtime is 1.1.0 and the same copy the tests import, that the patch is
applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
pi's exported api modules implement, and records as a characterization
row that pi 1.1.0's opencode-go provider has no Google API.
No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: a harness that is the real host
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.
test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.
On top of it:
- contract tests for the waterfall, the profile and Config shapes,
credentials, and the config editor (host-waterfall, host-shapes,
host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
where P-R1 is expected to flip it, with S0_TODAY shared by the
binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
e2e-host CI job (a pinned, patched cell on PRs that touch the host
seams or vite.config.ts, and a floating cell nightly and on
dispatch), and the test:e2e:host script.
The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* refactor: one way to patch a host method
Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.
On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.
decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.
test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.
It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: the plugin owns the opencode and opencode-go routes
The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.
A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.
Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.
What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.
When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.
The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: measure which endpoint families the gateway serves
A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.
- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
src/utils/plane-model.ts, which is what `auto` resolves to, and pin
it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
over four protocols and two planes, add LEGACY_DISPATCH, and add one
keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
e2e): one tiny request per plane, protocol and shape, sent with the
plane's own key and the plugin's identity from one shared constant,
PROBE_IDENTITY. Only the cells the default sends to are asserted;
the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
full; unset means full). CI arms `probes` on a pull request that
touches what the probes send, `full` nightly and on dispatch, and
passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
run summary and an `endpoint-families` artifact, and add §13 to
docs/redirect-planes.md with every cell pending the first CI run.
The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:
- the Go Gemini fixtures in six test files (attachment-services,
catalog, go-google, go-google-prepare, models-discovery,
responses-routes) now name the renamed id, since each means "the
Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
one unpriced row, which the vendor has now priced, so it builds the
`Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
completions list, since it declares no SDK.
The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: skip free models the gateway has withdrawn in the live suite
The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.
A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-T1b
The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:
- The binary e2e's beforeAll budget was smaller than the blocking
subprocess deadlines it serializes. Each plugin install now has its
own deadline, the hook's budget is derived from the installs and the
boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
skipped upstream's credentialRef(ref) check. It now throws the same
TypeError for a ref outside the grammar (a host-editor-contracts
case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
level. The overrides now force both, the cache key includes them,
and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
KNOWN_DEFECT title. Each apply order now generates one test per row
and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
P-T1 pins every fact. Both now give the same accounting: which facts
are pinned by number, which P-R1 exercises first, and why F9 and F17
are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
and baseURL collapse pi's protocols onto one. F6 is recorded as first
exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
toPiCredential. host-credentials now covers all three branches, and
the env branch is asserted through pi's Models.getAuth, so the case
fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
that an id pi cannot place is not listed and that a turn on it is
INVALID_CONFIG with no request; the save and deferred-read halves
are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
pi-ai-first block was split. The fixture case now checks only
structure (the facets whose value moves are the facets whose tag
moves), and the exact pin is its own KNOWN_DEFECT case that boots no
host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
override and turns that case into a BASELINE check in the same diff.
Nothing under src/ changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-H
What the review found, and what changed:
- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
teardown and on every foreign registry change. Its JSDoc in
src/host/patch-method.ts now says so, counts the reinstalls a release
causes, says a caller disposes it before releasing its adapters, and
notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
the real host for one reason; the third (the live-reload listing) is
unreachable for a different one. docs/improvement-plan.md now gives
each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
row only structurally. test/reload.test.ts now gives every site a
tier-A probe whose decorated answer must differ from the host's own,
so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
asserted in docs and enforced by nothing. test/architecture.test.ts
now has a textual tripwire over everything apply() reaches, with a
self-check of planted violations, and the apply()-twice reload case
requires a patch record on every own function apply() leaves on the
stand-in.
- The tripwire missed several write forms (Object.assign,
Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
Object.setPrototypeOf, and an Object.* target reached through a
property path such as registration?.adapter or this.llm). The
patterns now catch them, and the self-check plants each one (17
planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
set. The test docstring, AGENTS.md and the plan's P-H section now
name the four Reflect and four Object methods matched, and why a
prototype swap is listed.
No runtime behaviour changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R1
The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:
- A completions model whose effort list names none lost its Off.
utils/model-metadata.ts now offers Off with none as its wire value
on completions, as pi 1.1.0's own entries do; Responses unchanged.
Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
(pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
while the host modules were unloaded. host/route-claim.ts now judges
it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
config and drop both planes. Recorded as a constraint in
route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
CONTRIBUTING (field count, the host-modules floor), and historical
banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
protocol's required fields and that the gateway answered 200, or
the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
live handle, and "booting with none" waits for the plugin's row to
be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
the Zen key. README.md, README.zh-CN.md, deep-dive.md,
credentials.md and redirect-planes.md now give the real order and
the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
(en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
withdrawn. They now walk up to three candidates per protocol
(support/live-walk.ts), pass a "model is unavailable" turn only when
a raw probe confirms it (support/live-probe.ts), and skip naming a
protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.
Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* ci: gate freshness only on a pull request's own generator changes
The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.
A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.
The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
Chinese table had read 27 before this change, and now matches the
English one.
"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: give the freshness walk's dynamic import a script-local target
The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.
The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R2
What the review found, and what changed:
- The keyed flip rule read a default cell's retired-model answer as a
retired model without checking the same model on the other shapes.
planeOutcome now reads each default cell beside the legacy and root
cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
it. It is now "check the CI secret" when a sibling refused the key
and none accepted it, and "nothing else measured the key" only when
no sibling refused it; a path-gone, rate-limited or busy sibling no
longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
Google, which no flip moves) were called flip evidence, and the
"protocol" and "gone" reasons named causes nothing measured. A
wrong-protocol or path-gone cell is now decided by the cell that
endpoint: legacy dispatches to: "refused" (a flip would not serve
it) when that cell refused readably, and always for Go's Google;
"unmeasured" (re-run) when it measured nothing. Neither is flip
evidence.
- An account answer counted as the flip target accepting a protocol or
a model, though the account's gate answers before either is read. It
now counts only beside a refused key or a gone path, and a retired
model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
keyed classifier treats as routed. It now moves on past a busy
candidate, and its dated record no longer pairs a derived
upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
the verdict definitions in redirect-planes.md section 13. Section 13
now says what the code does (no answer on the last attempt is
transport-error unless an earlier attempt was a 429), and a test pins
that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
section 13 is pending its first CI run; said models.opencode.ai
declares the root shape, which the generated catalog contradicts;
said legacy cells are asserted nowhere live; and said a keyed
auth-refused means the plugin's own turns would be refused, though a
default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
conditional flip a phase, and says the keyed probes decide a plane
from the default cells read beside their siblings, not alone, and
that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
probe following a redirect, the push-to-none arm and the publish and
upload steps of the CI wiring, the free probe moving on past a busy
upstream, and the record being rewritten after every cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.
It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.
"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.
(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: send Google's thinking level to Gemini models the catalog gives levels
The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.
The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.
Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: correct the Go shape count, the README date and the install range
Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.
- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
shapes other than completions" restores the count its own table gives
(5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
no-SDK models). The 12 was right when written (6 + 6) and a later
refresh moved the cells but not the sentence. test/catalog.test.ts now
holds the sentence to the table's non-completions Go sum, so the next
refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
"^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
^0.12.0, neither of which can resolve to a 1.x release.
test/readme-parity.test.ts pins the two snippets to one range on
package.json's major.
Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: date the refreshed model count and correct three stale figures
The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:
- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
cannot resolve to any 1.x release; both now name package.json's
^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
ships; it now says "the models it ships", which a refresh cannot
strand.
P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the audit's remaining findings on P-R1
Each finding the third audit round left open, re-verified on this head:
- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
model: already fixed (0cceb81). walkPerProtocol walks up to three
candidates per protocol and names an unreached protocol in a skip;
test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
describes auto backwards: already fixed (0cceb81) in deep-dive.md and
both hint strings. The same claim survived in a source comment,
KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
commit corrects it: a key is filed by the plane its request went to,
so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
skip decided by message text: already fixed (0cceb81). A "gone"
verdict needs a raw probe of the same id to confirm it, and a
protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
"Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
Zen-only setup hides the ring: already fixed (0cceb81).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: follow the vendor's retirement of nemotron-3-ultra-free
The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:
- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
redirect-planes.md, protocol-routing-and-merge.md and both READMEs
("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
ships: the Zen and Go lists had kept seven ids the vendor has since
retired. The lists are main's; this branch's own prose around them
(how the Go Google base is served) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: walk past a model the gateway answers ModelDeprecated for
The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.
Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: install the plane-lookup world before each test
The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(scripts): move the generated-table skip into the CLI-version rules
The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement
The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(testing): describe the suite this branch keeps
Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): keep the free-tier case's narrowing in the subject picker
Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore(vendor): follow opencode 1.19.5
The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): restack the plan onto 1.8.0 and the alpha host
Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: raise the client bundle ceiling to 136 KB
Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: complete the restack of P0
Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.
- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
it names the `packageManager` field, which main's bump had made stale.
release.yml carries the same stale sentence on main and is left to a
main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
causes that can reach it (gated, withdrawn, refused for the account,
never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
403 FreeTierError (the headers are not its cause and the patch does
not open the free tier), carries the Auto Review route clause and the
two paragraphs on the API-key-only build and the host restart; the
English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
note; the sources line names both harness tags; the P1a-P2 test matrix
says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
tables are dated 2026-10-31 (the shipped shim), apart from the
documents' measurement stamps; both READMEs name
`catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
as a retirement, and rule 2 says the body, not the status, makes it one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the restack review of P0
Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.
- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
the shim was said to suppress (main's regenerated shim serves one of
them on Zen). It states the rule instead: a retired model is a row the
registry marks deprecated, the generator drops it per plane, and ids
are not restated; the enrichment item says "registry-deprecated rows".
- rules-docs-2-R2-1: the PR body is replaced at push time from the
updated draft, with the verification rows re-measured on this head
and a restack section (what merged from main, what this PR dropped,
the main-side follow-ups). Nothing is pushed here.
- rules-docs-2-R2-2: the plan's defects table is retitled to where each
defect now stands; the four rows main fixed are struck with the fix
named, the purge row keeps only what is still open (a RemoteError
object shows the generic message, P1a), and D19 records that the
README's hiddenModels was removed in P0.
- rules-docs-2-R2-3: README.zh-CN.md's merge rule follows the English
and the code: a row is dropped only when the registry marks it
deprecated, not when a per-provider list names it. One sentence, so
the pairing gate's counts are unchanged.
- rules-docs-2-R2-4: CONTRIBUTING.md no longer contradicts the
repository: warnings are reported and gate in P12 (as its own command
list says); a manual release creates the GitHub Release and never
touches CHANGELOG.md; the release step points at the "Verified on
DeepSeek Harness" line in both READMEs; the e2e section lists all
five files.
- rules-docs-2-R2-5: both READMEs drop the test total nothing pinned;
engineering-notes.md's "every count" paragraph records it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: hold the meter's allowance figures to the generated table
The third review round of the P0 restack. Each outcome:
- The meter mock-ups in both READMEs and both quota-meter guides paired
one model with another model's monthly allowance. They now name the
model whose row the figures are, and the panel mock-up's rate line
prints that model's catalog rates in the format the panel uses. A new
catalog test reads every "<id> Go $a · Plus $b" figure in the four
documents back against the generated allowance table; it failed on
the old mock-ups and holds a known minimum of matches, so a regex that
stops matching cannot pass as clean.
- engineering-notes: the "deeper half" measurement is scoped to the Go
plane it was taken on. Both ids are Zen rows and neither has a row in
the Go catalog, which is what the plane-aware lookup misses; the
sentence no longer claims they are absent from both shims.
- deep-dive: the two "up to" figures for context window and max output
are dropped, per the same file's rule that counts which move with the
vendor are not restated.
- docs/README.md indexes every document in the directory: the test
system guide, upstream findings, the CLI API note, the parity plan,
and the plugin page pair, whose two languages the opening sentence
now names.
No generated file moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* style: lint and format the merged purge row and token test
The merge resolution read the RemoteError's message by property access,
which the lint gate's prefer-destructuring rule rejects as an error, and
left the merged host-tokens test unformatted. Same behaviour, now through
the gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: complete the restack of P1a
Outcomes of the merge review:
- both-kept-1-F1, resolutions-1-P1A-2, rules-docs-1-P1A-DOCS-1:
docs/redirect-planes.md now states D19 (`auto`, the default, is an alias
of `modern`; `legacy` pins the zen family) instead of "follows the
catalog". The models-discovery repair comment is left for P-R1, which
deletes that block, and is named in the PR's follow-ups.
- resolutions-1-P1A-1: the catalog's refusal describe gains the Zen-plane
404 fill, the non-JSON 200 answer, and a retry-after header that must
survive the passthrough (mutation-checked).
- resolutions-1-P1A-3: the plan's defect row for the RemoteError purge
message is struck as fixed by P1a.
- rules-docs-1-P1A-DOCS-2: the plan's status and restack rows name the
warning token's dropped fallback, matching the build-order item.
- rules-docs-1-P1A-DOCS-4: the host-tokens header names usage-pill among
the meter files the card scan leaves out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: state the measured meter-scan reasons and P1a's refusal test cases
- fixed: rules-docs-2-P1A-1 — the host-tokens header drops the template
clause (no meter file builds a token name from a template) and names the
measured reasons: usage-pill.tsx would pass the scan; usage-ui.ts and
usage-panel.tsx each spell the `--dsw-alias-state-*` wildcard in prose, and
usage-ui.ts names `--dsw-radius-full` behind a literal 999px fallback.
CARD_FILES is unchanged.
- fixed: rules-docs-2-P1A-2 — the plan's status row, struck defect row,
build-order item and restack row name the three refusal-passthrough test
cases P1a adds (the Zen-plane 404 fill, the non-JSON 200 answer, the
retry-after passthrough); the passthrough itself is on main (#133).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: one catalogue fixture, behind the one door
P1b's divergent-catalog seed and main's synthetic rows (#138) built the
same thing twice, and main's copy imported src directly. They are now
one module, test/support/catalogs.ts, which reaches src through
test/support/src.ts: the seed is built from main's payload builder, and
the plane-lookup and plane-divergence tests take their src names from
the door too (coreToolDecision is added to it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: describe debugFile by the gate main chose (D24)
The settings registry and the manifest still said the file is written
only while debug is on. Under main's rule, which this stack adopted as
D24, naming debugFile writes both halves of the log and debug governs
the console alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* style: satisfy main's promise lint in the …
viztor
added a commit
that referenced
this pull request
Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary
- Stop the run on a Node below engines.node, before collecting a file.
Below the floor, failures came from the engine (node:sqlite,
Intl.DurationFormat, ICU strings) and looked like code regressions.
.node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
output, so the meter row reads the same with or without
Intl.DurationFormat. The DurationFormat tests restore the engine as they
found it, rather than leaving a stand-in installed or asserting the
engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
graph from settings-page.tsx. It forbids node built-ins, the cordis
runtime, pi, and process.env. Checked by planting a violation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(lifecycle): contain the internal-route mount and follow reloads in discovery
- A rejected internal-route mount ran from a `void` IIFE with no catch, so
a Google bridge failure became an unhandled rejection, which Node's
default mode answers by exiting the process. mountInternalRoutes now
owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
every route declared, no llm-pi-ai) answered `undefined` after the
Gemini bridges had already registered. That left them with no
disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
and every route it had registered. Both paths now release the routes
and dispose the fiber.
- Model discovery was registered on every activation, and the host's
duplicate refusal was swallowed, so the first activation's settings
answered for the life of the process. It is now registered once per
root and answers from the latest activation's settings. Each namespace
registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.
Each fix has a regression test. The two mount tests fail without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: price, list and key requests on the plane they belong to; isolate the suite
Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
plugin claims. Before, a direct anthropic or openai route lost rows the
OpenCode routing called "unservable", and a refused foreign id was
answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
re-checked the URL against the default gateway markers and fell back to
Zen.
- The free-model check reads a Request-carried body. That is how DSH
sends a turn, and every keyless free turn was reported as a missing
credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
working Go key was filed under Zen. An OpenAI sk- key on a claimed
relay was filed under Go.
Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
sent 54 requests to models.opencode.ai through the catalog's background
revalidation, so on a networked runner a result could depend on the
vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
test (resetCatalogState is new). Shuffled runs failed catalog cases on
seeds 42 and 1234. test:shuffle runs the suite in random order, and the
Node 24 CI job uses it.
Each behaviour change has a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(store): mount the inspection remote, and read legacy keys per integration
- The card's store-inspection row called remote.opencodeStore.inspect, but
no client contribution carried that namespace's descriptor. The row
reported "unavailable" on a host that had the service. storeRemote now
carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
string literal in super(ctx, "name"), and the store passes a constant.
It now resolves constants, and checks every declared Remote method
against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
the host did not mean to send (a token) is dropped. An ok:false
envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
flagged opencode-go row hid every legacy opencode row, and the Zen key
disappeared. The fallback is now per integration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: describe the tree that exists, and the guarantees the suite now keeps
- AGENTS.md's repo map described nine modules this build does not have
(the Console login, token store, org cache and sign-in row). They are
replaced with one line saying sign-in is v2 work. The go-limits
duplication note is gone, because attachAllowance calls allowanceFor.
adapter-replacement.ts is marked as not wired. The client-bundle line
names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
the offline guard, the per-test state reset, where host-only fixtures
live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
through the settings card", which this build does not have. It points
at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
and its parser (about 1.2 KB of source, not a dependency), following
the gate's own instruction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: add the improvement plan from the architecture review
docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the approved refactor plan, with the host's own contracts
Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
packages/llm, llm-pi-ai, session-controller, gateway and
ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
table that each PR updates.
The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
declared models.
- The plugin never writes the user's profile and follows the user's
configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
is generalized to every mode, and its catalog fill is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): route every mode through the user's own provider
Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.
- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
`opencode` catalog already spans Anthropic, Google, Completions and
Responses with per-protocol endpoints. One route serves every mode for
every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
(including all Gemini on Go), endpoint drift, and profile-level
`api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
the user's route id, and the adapter patch (merge-modes) delegates only
gap models to it. That deletes the hidden routes, the listing hide,
the stream redirect, the child mount and the Gemini bridge
registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
would remove the routing layer entirely. It would also let Settings
save a model pi doesn't know, which pi-ai's strict write validation
rejects today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0
pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.
Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.
Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): fold the reviewed design and the build order into the plan
A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:
- Settings reads pi-ai's resolved config, so a read-only wrapper on
settings.describe puts the plane entries back into the displayed value;
writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
validations by identity, reconcile runs on commit, mount and teardown
are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
host harness plus a binary e2e job pin the behaviour first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: repair what shipped broken in 1.4.1
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
barrel does not have, an ESM link error on every host load. It now
forwards `export *` only, written by one shared helper that a test links
against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
Those statuses now pass through; a 404 is still filled from the catalog,
because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
purge result" and cast the answer; it now shows the host's message and
parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
never sends. It now names the per-protocol inference family, and auto as
its alias, which is what the code already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): P1a is in review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: remove dead code and tighten the repository's gates
- Delete adapter-replacement.ts and its test. Nothing imported it, and
deleting another plugin's registry entry is not a host API; the
engineering notes record why, and what the plugin does instead.
A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
went missing from it; a manifest test checks cordis.patch.yml against
the schema and the field registry, and the manifest's phantom
showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
purity gate now also rejects console and global timers, walking the
folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
opencode-go or opencode-go-*) and loses its unreachable Anthropic
branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
module moves touch one file; CONTRIBUTING.md states the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: a smaller public API
The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).
- Every test reaches src through test/support/src.ts, importing each
name from the module that defines it; a gate rejects a static src
import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
mount until the plugin owns the plane routes. A row that still carries
it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
already on npm stays as its final word, and check.ts asserts that the
release workflow verifies exactly what publish-scoped.ts publishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: pin the host the plugin runs on
The test install now runs the host users run.
- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
with the harness's patch applied verbatim (patches/, hash recorded by
pnpm). A pnpm override holds every copy in the tree to it, not just the
direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
version a registry install of the published dsh resolves (cordis 4.0.5,
loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
test/host-versions.test.ts checks that the pi the plugin loads at
runtime is 1.1.0 and the same copy the tests import, that the patch is
applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
pi's exported api modules implement, and records as a characterization
row that pi 1.1.0's opencode-go provider has no Google API.
No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: a harness that is the real host
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.
test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.
On top of it:
- contract tests for the waterfall, the profile and Config shapes,
credentials, and the config editor (host-waterfall, host-shapes,
host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
where P-R1 is expected to flip it, with S0_TODAY shared by the
binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
e2e-host CI job (a pinned, patched cell on PRs that touch the host
seams or vite.config.ts, and a floating cell nightly and on
dispatch), and the test:e2e:host script.
The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* refactor: one way to patch a host method
Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.
On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.
decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.
test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.
It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: the plugin owns the opencode and opencode-go routes
The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.
A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.
Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.
What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.
When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.
The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: measure which endpoint families the gateway serves
A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.
- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
src/utils/plane-model.ts, which is what `auto` resolves to, and pin
it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
over four protocols and two planes, add LEGACY_DISPATCH, and add one
keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
e2e): one tiny request per plane, protocol and shape, sent with the
plane's own key and the plugin's identity from one shared constant,
PROBE_IDENTITY. Only the cells the default sends to are asserted;
the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
full; unset means full). CI arms `probes` on a pull request that
touches what the probes send, `full` nightly and on dispatch, and
passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
run summary and an `endpoint-families` artifact, and add §13 to
docs/redirect-planes.md with every cell pending the first CI run.
The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: Settings offers the catalog's models pi does not list
For the opencode and opencode-go routes, the Settings model list now
shows pi-ai's own rows first, in pi's order, and then every model the
plane's catalog can serve that pi does not list and the catalog has
not retired. Ids that pi's registry has retired are dropped, a field a
row lacks is filled from the catalog, and every row is cut down to the
five fields the host's discovery contract accepts, with absent values
omitted rather than set to undefined.
While pi-ai restarts and its discovery is withdrawn (NO_DISCOVERY),
the list answers the catalog rows alone instead of the error.
This rewrites the existing discoverModels wrapper rather than adding a
second one. Other routes keep today's enrich-only merge, because pi-ai
serves them and cannot serve an id it does not list. The plugin's own
model-discovery namespaces, which Settings never asked, are deleted
with their lifecycle tests, and RouteClaim gains a claiming() seam.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:
- the Go Gemini fixtures in six test files (attachment-services,
catalog, go-google, go-google-prepare, models-discovery,
responses-routes) now name the renamed id, since each means "the
Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
one unpriced row, which the vendor has now priced, so it builds the
`Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
completions list, since it declares no SDK.
The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: skip free models the gateway has withdrawn in the live suite
The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.
A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-T1b
The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:
- The binary e2e's beforeAll budget was smaller than the blocking
subprocess deadlines it serializes. Each plugin install now has its
own deadline, the hook's budget is derived from the installs and the
boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
skipped upstream's credentialRef(ref) check. It now throws the same
TypeError for a ref outside the grammar (a host-editor-contracts
case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
level. The overrides now force both, the cache key includes them,
and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
KNOWN_DEFECT title. Each apply order now generates one test per row
and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
P-T1 pins every fact. Both now give the same accounting: which facts
are pinned by number, which P-R1 exercises first, and why F9 and F17
are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
and baseURL collapse pi's protocols onto one. F6 is recorded as first
exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
toPiCredential. host-credentials now covers all three branches, and
the env branch is asserted through pi's Models.getAuth, so the case
fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
that an id pi cannot place is not listed and that a turn on it is
INVALID_CONFIG with no request; the save and deferred-read halves
are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
pi-ai-first block was split. The fixture case now checks only
structure (the facets whose value moves are the facets whose tag
moves), and the exact pin is its own KNOWN_DEFECT case that boots no
host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
override and turns that case into a BASELINE check in the same diff.
Nothing under src/ changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-H
What the review found, and what changed:
- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
teardown and on every foreign registry change. Its JSDoc in
src/host/patch-method.ts now says so, counts the reinstalls a release
causes, says a caller disposes it before releasing its adapters, and
notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
the real host for one reason; the third (the live-reload listing) is
unreachable for a different one. docs/improvement-plan.md now gives
each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
row only structurally. test/reload.test.ts now gives every site a
tier-A probe whose decorated answer must differ from the host's own,
so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
asserted in docs and enforced by nothing. test/architecture.test.ts
now has a textual tripwire over everything apply() reaches, with a
self-check of planted violations, and the apply()-twice reload case
requires a patch record on every own function apply() leaves on the
stand-in.
- The tripwire missed several write forms (Object.assign,
Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
Object.setPrototypeOf, and an Object.* target reached through a
property path such as registration?.adapter or this.llm). The
patterns now catch them, and the self-check plants each one (17
planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
set. The test docstring, AGENTS.md and the plan's P-H section now
name the four Reflect and four Object methods matched, and why a
prototype swap is listed.
No runtime behaviour changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R1
The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:
- A completions model whose effort list names none lost its Off.
utils/model-metadata.ts now offers Off with none as its wire value
on completions, as pi 1.1.0's own entries do; Responses unchanged.
Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
(pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
while the host modules were unloaded. host/route-claim.ts now judges
it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
config and drop both planes. Recorded as a constraint in
route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
CONTRIBUTING (field count, the host-modules floor), and historical
banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
protocol's required fields and that the gateway answered 200, or
the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
live handle, and "booting with none" waits for the plugin's row to
be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
the Zen key. README.md, README.zh-CN.md, deep-dive.md,
credentials.md and redirect-planes.md now give the real order and
the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
(en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
withdrawn. They now walk up to three candidates per protocol
(support/live-walk.ts), pass a "model is unavailable" turn only when
a raw probe confirms it (support/live-probe.ts), and skip naming a
protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.
Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* ci: gate freshness only on a pull request's own generator changes
The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.
A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.
The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
Chinese table had read 27 before this change, and now matches the
English one.
"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: give the freshness walk's dynamic import a script-local target
The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.
The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R2
What the review found, and what changed:
- The keyed flip rule read a default cell's retired-model answer as a
retired model without checking the same model on the other shapes.
planeOutcome now reads each default cell beside the legacy and root
cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
it. It is now "check the CI secret" when a sibling refused the key
and none accepted it, and "nothing else measured the key" only when
no sibling refused it; a path-gone, rate-limited or busy sibling no
longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
Google, which no flip moves) were called flip evidence, and the
"protocol" and "gone" reasons named causes nothing measured. A
wrong-protocol or path-gone cell is now decided by the cell that
endpoint: legacy dispatches to: "refused" (a flip would not serve
it) when that cell refused readably, and always for Go's Google;
"unmeasured" (re-run) when it measured nothing. Neither is flip
evidence.
- An account answer counted as the flip target accepting a protocol or
a model, though the account's gate answers before either is read. It
now counts only beside a refused key or a gone path, and a retired
model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
keyed classifier treats as routed. It now moves on past a busy
candidate, and its dated record no longer pairs a derived
upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
the verdict definitions in redirect-planes.md section 13. Section 13
now says what the code does (no answer on the last attempt is
transport-error unless an earlier attempt was a 429), and a test pins
that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
section 13 is pending its first CI run; said models.opencode.ai
declares the root shape, which the generated catalog contradicts;
said legacy cells are asserted nowhere live; and said a keyed
auth-refused means the plugin's own turns would be refused, though a
default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
conditional flip a phase, and says the keyed probes decide a plane
from the default cells read beside their siblings, not alone, and
that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
probe following a redirect, the push-to-none arm and the publish and
upload steps of the CI wiring, the free probe moving on past a busy
upstream, and the record being rewritten after every cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.
It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.
"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.
(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The merged catalog renames the Go Gemini model from
gemini-4-argon-contributor to gemini-4-argon; the Settings fetch
assertions (tier B and the binary e2e) now name the refreshed id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: send Google's thinking level to Gemini models the catalog gives levels
The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.
The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.
Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: correct the Go shape count, the README date and the install range
Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.
- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
shapes other than completions" restores the count its own table gives
(5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
no-SDK models). The 12 was right when written (6 + 6) and a later
refresh moved the cells but not the sentence. test/catalog.test.ts now
holds the sentence to the table's non-completions Go sum, so the next
refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
"^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
^0.12.0, neither of which can resolve to a 1.x release.
test/readme-parity.test.ts pins the two snippets to one range on
package.json's major.
Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: date the refreshed model count and correct three stale figures
The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:
- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
cannot resolve to any 1.x release; both now name package.json's
^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
ships; it now says "the models it ships", which a refresh cannot
strand.
P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P3
The second review of P3 ran three rounds across the correctness, rules
and test-matrix lenses, and upheld 11, 4 and 4 findings. What it found,
and what changed:
- On a served plane, Settings' fetch offered pi's stale capacities, and
adopting a row saved them over the catalog's, contrary to D17 and the
README. Every row on a served plane is now read catalog-first, with
pi filling only what the catalog lacks; a route the plugin does not
serve stays pi-first (src/models-discovery.ts).
- enrichModels=false turns off the D3 complement and the NO_DISCOVERY
fallback, but the README and the P3 spec stated both unconditionally.
The README, the AGENTS.md map, the deep-dive and the plan's P3 landed
note now name the switch.
- Retired pi rows are dropped only once a live catalog read has landed,
but the docs said always. They now say so, and that offline pi's list
passes through.
- The enrichModels copy said the picker shows declared models only,
which contradicts D3. It now says the picker shows the declared
models, or the plane's catalog when the entry declares none
(src/settings-copy.ts and both READMEs).
- The plan listed the defect P3 fixes as not yet fixed, with stale line
references. It is struck through and marked fixed by P3.
- F21 was a host fact missing from both lists of pinned facts, and no
test named it. Both lists (the plan and CONTRIBUTING.md) now name it,
and so does the pi-ai restart test.
- The gap-row save test never showed the id was missing from the
composer before the save. It now asserts that first.
- The README's "Changed in 1.x" Fetch entry described the complement
unconditionally and left out the catalog-first values. It now states
both.
- README troubleshooting gave no cause for a retired pi id staying in
Fetch while offline. It now names an unread catalog. Its fix says the
first fetch once models.opencode.ai is reachable starts the refresh
and still returns the old list, because the refresh runs behind the
read and nothing runs it on a timer, so fetch again a moment later
(README.md and README.zh-CN.md).
Carried to landing, because only a push can close it: the binary e2e
Settings fetch row has not run in the floating e2e-host cell on this
head, and the PR body lacks the plan's records and describes the merge
as it was before the catalog-first change. That needs a dispatch of
ci.yml on the pushed head and a rewrite of the PR body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the audit's remaining findings on P-R1
Each finding the third audit round left open, re-verified on this head:
- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
model: already fixed (0cceb81). walkPerProtocol walks up to three
candidates per protocol and names an unreached protocol in a skip;
test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
describes auto backwards: already fixed (0cceb81) in deep-dive.md and
both hint strings. The same claim survived in a source comment,
KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
commit corrects it: a key is filed by the plane its request went to,
so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
skip decided by message text: already fixed (0cceb81). A "gone"
verdict needs a raw probe of the same id to confirm it, and a
protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
"Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
Zen-only setup hides the ring: already fixed (0cceb81).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: follow the vendor's retirement of nemotron-3-ultra-free
The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:
- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
redirect-planes.md, protocol-routing-and-merge.md and both READMEs
("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
ships: the Zen and Go lists had kept seven ids the vendor has since
retired. The lists are main's; this branch's own prose around them
(how the Go Google base is served) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: walk past a model the gateway answers ModelDeprecated for
The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.
Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: install the plane-lookup world before each test
The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(scripts): move the generated-table skip into the CLI-version rules
The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement
The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(testing): describe the suite this branch keeps
Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): keep the free-tier case's narrowing in the subject picker
Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore(vendor): follow opencode 1.19.5
The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): restack the plan onto 1.8.0 and the alpha host
Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: raise the client bundle ceiling to 136 KB
Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: complete the restack of P0
Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.
- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
it names the `packageManager` field, which main's bump had made stale.
release.yml carries the same stale sentence on main and is left to a
main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
causes that can reach it (gated, withdrawn, refused for the account,
never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
403 FreeTierError (the headers are not its cause and the patch does
not open the free tier), carries the Auto Review route clause and the
two paragraphs on the API-key-only build and the host restart; the
English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
note; the sources line names both harness tags; the P1a-P2 test matrix
says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
tables are dated 2026-10-31 (the shipped shim), apart from the
documents' measurement stamps; both READMEs name
`catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
as a retirement, and rule 2 says the body, not the status, makes it one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the restack review of P0
Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.
- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
the shim was said to suppress (main's regenerated shim serves one of
them on Zen). It states the rule instead: a retired model is a row the
registry marks deprecated, the generator drops it per plane, and ids
are not restated; the enrichment item says "registry-deprecated rows".
- rules-docs-2-R2-1: the PR body is replaced at push time from the
updated draft, with the verification rows re-measured on this head
and a restack section (what merged from main, what this PR dropped,
the main-side follow-ups). Nothing is pushed here.
- rules-docs-2-R2-2: the plan's defects table is retitled to where each
defect now stands; the four rows main fixed are struck with the fix
named, the purge row keeps only what is still open (a RemoteError
object shows the generic message, P1a), and D19 records that the
README's hiddenModels was removed in P0.
- rules-docs-2-R2-3: README.zh-CN.md's merge rule follows the English
and the code: a row is dropped only when the registry marks it
deprecated, not when a per-provider list names it. One sentence, so
the pairing gate's counts are unchanged.
- rules-docs-2-R2-4: CONTRIBUTING.md no longer contradicts the
repository: warnings are reported and gate in P12 (as its own command
list says); a manual release creates the GitHub Release and never
touches CHANGELOG.md; the release step points at the "Verified on
DeepSeek Harness" line in both READMEs; the e2e section lists all
five files.
- rules-docs-2-R2-5: both READMEs drop the test total nothing pinned;
engineering-notes.md's "every count" paragraph records it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: hold the meter's allowance figures to the generated table
The third review round of the P0 restack. Each outcome:
- The meter mock-ups in both READMEs and both quota-meter guides paired
one model with another model's monthly allowance. They now name the
model whose row the figures are, and the panel mock-up's rate line
prints that model's catalog rates in the format the panel uses. A new
catalog test reads every "<id> Go $a · Plus $b" figure in the four
documents back against the generated allowance table; it failed on
the old mock-ups and holds a known minimum of matches, so a regex that
stops matching cannot pass as clean.
- engineering-notes: the "deeper half" measurement is scoped to the Go
plane it was taken on. Both ids are Zen rows and neither has a row in
the Go catalog, whic…
viztor
added a commit
that referenced
this pull request
Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary
- Stop the run on a Node below engines.node, before collecting a file.
Below the floor, failures came from the engine (node:sqlite,
Intl.DurationFormat, ICU strings) and looked like code regressions.
.node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
output, so the meter row reads the same with or without
Intl.DurationFormat. The DurationFormat tests restore the engine as they
found it, rather than leaving a stand-in installed or asserting the
engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
graph from settings-page.tsx. It forbids node built-ins, the cordis
runtime, pi, and process.env. Checked by planting a violation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(lifecycle): contain the internal-route mount and follow reloads in discovery
- A rejected internal-route mount ran from a `void` IIFE with no catch, so
a Google bridge failure became an unhandled rejection, which Node's
default mode answers by exiting the process. mountInternalRoutes now
owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
every route declared, no llm-pi-ai) answered `undefined` after the
Gemini bridges had already registered. That left them with no
disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
and every route it had registered. Both paths now release the routes
and dispose the fiber.
- Model discovery was registered on every activation, and the host's
duplicate refusal was swallowed, so the first activation's settings
answered for the life of the process. It is now registered once per
root and answers from the latest activation's settings. Each namespace
registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.
Each fix has a regression test. The two mount tests fail without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: price, list and key requests on the plane they belong to; isolate the suite
Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
plugin claims. Before, a direct anthropic or openai route lost rows the
OpenCode routing called "unservable", and a refused foreign id was
answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
re-checked the URL against the default gateway markers and fell back to
Zen.
- The free-model check reads a Request-carried body. That is how DSH
sends a turn, and every keyless free turn was reported as a missing
credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
working Go key was filed under Zen. An OpenAI sk- key on a claimed
relay was filed under Go.
Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
sent 54 requests to models.opencode.ai through the catalog's background
revalidation, so on a networked runner a result could depend on the
vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
test (resetCatalogState is new). Shuffled runs failed catalog cases on
seeds 42 and 1234. test:shuffle runs the suite in random order, and the
Node 24 CI job uses it.
Each behaviour change has a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix(store): mount the inspection remote, and read legacy keys per integration
- The card's store-inspection row called remote.opencodeStore.inspect, but
no client contribution carried that namespace's descriptor. The row
reported "unavailable" on a host that had the service. storeRemote now
carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
string literal in super(ctx, "name"), and the store passes a constant.
It now resolves constants, and checks every declared Remote method
against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
the host did not mean to send (a token) is dropped. An ok:false
envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
flagged opencode-go row hid every legacy opencode row, and the Zen key
disappeared. The fallback is now per integration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: describe the tree that exists, and the guarantees the suite now keeps
- AGENTS.md's repo map described nine modules this build does not have
(the Console login, token store, org cache and sign-in row). They are
replaced with one line saying sign-in is v2 work. The go-limits
duplication note is gone, because attachAllowance calls allowanceFor.
adapter-replacement.ts is marked as not wired. The client-bundle line
names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
the offline guard, the per-test state reset, where host-only fixtures
live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
through the settings card", which this build does not have. It points
at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
and its parser (about 1.2 KB of source, not a dependency), following
the gate's own instruction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: add the improvement plan from the architecture review
docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the approved refactor plan, with the host's own contracts
Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
packages/llm, llm-pi-ai, session-controller, gateway and
ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
table that each PR updates.
The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
declared models.
- The plugin never writes the user's profile and follows the user's
configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
is generalized to every mode, and its catalog fill is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): route every mode through the user's own provider
Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.
- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
`opencode` catalog already spans Anthropic, Google, Completions and
Responses with per-protocol endpoints. One route serves every mode for
every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
(including all Gemini on Go), endpoint drift, and profile-level
`api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
the user's route id, and the adapter patch (merge-modes) delegates only
gap models to it. That deletes the hidden routes, the listing hide,
the stream redirect, the child mount and the Gemini bridge
registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
would remove the routing layer entirely. It would also let Settings
save a model pi doesn't know, which pi-ai's strict write validation
rejects today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0
pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.
Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.
Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): fold the reviewed design and the build order into the plan
A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:
- Settings reads pi-ai's resolved config, so a read-only wrapper on
settings.describe puts the plane entries back into the displayed value;
writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
validations by identity, reconcile runs on commit, mount and teardown
are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
host harness plus a binary e2e job pin the behaviour first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: repair what shipped broken in 1.4.1
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
barrel does not have, an ESM link error on every host load. It now
forwards `export *` only, written by one shared helper that a test links
against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
Those statuses now pass through; a 404 is still filled from the catalog,
because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
purge result" and cast the answer; it now shows the host's message and
parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
never sends. It now names the per-protocol inference family, and auto as
its alias, which is what the code already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): P1a is in review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: remove dead code and tighten the repository's gates
- Delete adapter-replacement.ts and its test. Nothing imported it, and
deleting another plugin's registry entry is not a host API; the
engineering notes record why, and what the plugin does instead.
A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
went missing from it; a manifest test checks cordis.patch.yml against
the schema and the field registry, and the manifest's phantom
showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
purity gate now also rejects console and global timers, walking the
folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
opencode-go or opencode-go-*) and loses its unreachable Anthropic
branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
module moves touch one file; CONTRIBUTING.md states the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: a smaller public API
The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).
- Every test reaches src through test/support/src.ts, importing each
name from the module that defines it; a gate rejects a static src
import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
mount until the plugin owns the plane routes. A row that still carries
it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
already on npm stays as its final word, and check.ts asserts that the
release workflow verifies exactly what publish-scoped.ts publishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: pin the host the plugin runs on
The test install now runs the host users run.
- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
with the harness's patch applied verbatim (patches/, hash recorded by
pnpm). A pnpm override holds every copy in the tree to it, not just the
direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
version a registry install of the published dsh resolves (cordis 4.0.5,
loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
test/host-versions.test.ts checks that the pi the plugin loads at
runtime is 1.1.0 and the same copy the tests import, that the patch is
applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
pi's exported api modules implement, and records as a characterization
row that pi 1.1.0's opencode-go provider has no Google API.
No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: a harness that is the real host
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.
test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.
On top of it:
- contract tests for the waterfall, the profile and Config shapes,
credentials, and the config editor (host-waterfall, host-shapes,
host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
where P-R1 is expected to flip it, with S0_TODAY shared by the
binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
e2e-host CI job (a pinned, patched cell on PRs that touch the host
seams or vite.config.ts, and a floating cell nightly and on
dispatch), and the test:e2e:host script.
The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* refactor: one way to patch a host method
Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.
On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.
decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.
test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.
It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: the plugin owns the opencode and opencode-go routes
The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.
A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.
Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.
What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.
When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.
The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: measure which endpoint families the gateway serves
A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.
- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
src/utils/plane-model.ts, which is what `auto` resolves to, and pin
it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
over four protocols and two planes, add LEGACY_DISPATCH, and add one
keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
e2e): one tiny request per plane, protocol and shape, sent with the
plane's own key and the plugin's identity from one shared constant,
PROBE_IDENTITY. Only the cells the default sends to are asserted;
the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
full; unset means full). CI arms `probes` on a pull request that
touches what the probes send, `full` nightly and on dispatch, and
passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
run summary and an `endpoint-families` artifact, and add §13 to
docs/redirect-planes.md with every cell pending the first CI run.
The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* feat: Settings offers the catalog's models pi does not list
For the opencode and opencode-go routes, the Settings model list now
shows pi-ai's own rows first, in pi's order, and then every model the
plane's catalog can serve that pi does not list and the catalog has
not retired. Ids that pi's registry has retired are dropped, a field a
row lacks is filled from the catalog, and every row is cut down to the
five fields the host's discovery contract accepts, with absent values
omitted rather than set to undefined.
While pi-ai restarts and its discovery is withdrawn (NO_DISCOVERY),
the list answers the catalog rows alone instead of the error.
This rewrites the existing discoverModels wrapper rather than adding a
second one. Other routes keep today's enrich-only merge, because pi-ai
serves them and cannot serve an id it does not list. The plugin's own
model-discovery namespaces, which Settings never asked, are deleted
with their lifecycle tests, and RouteClaim gains a claiming() seam.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: restructure the suite
Phase P-T2 of docs/improvement-plan.md. Test-only: no source file
changes and no behaviour changes.
- Every socket-bound case is in the mock suite. The nine unit files
that booted the mock gateway through testing-kit.ts are merged into
test/mock/request-shapes.mock.ts and a new protocol-endpoints.mock.ts
table. The two offline e2e files that bind a port
(patched-fetch-headers, refusal-classification) moved there too. The
device-login routes left the gateway mock.
- createMockContext, HarnessSimulator and the rest of testing-kit.ts
are gone. Services are built on a real cordis Context, and
lifecycle.test.ts mounts the plugin the way the Loader does
(test/support/composition.ts). degradedHost remains only for the
degraded-composition diagnostics.
- 43 cases that tested only the harness or the mock gateway are
deleted. 33 duplicated cases are merged and 16 restatements are
deleted; the plan names each one.
- Hard-coded catalog counts and vendor ids are now invariants, or a
row picked by shape, so a vendor catalog refresh no longer breaks
the suite by construction.
- The seven largest test files are split by subject, the live suites
read their environment through test/support/e2e-env.ts, and the
duplicated helpers and hard-coded ses_ ids are removed.
- src/utils/session-id.ts, src/opencode-store-contract.ts and the e2e
environment reader have their own tests, and src/lifecycle.ts,
src/stream-hook.ts and src/fetch-patch.ts have per-file coverage
floors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog and limits tables
The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:
- the Go Gemini fixtures in six test files (attachment-services,
catalog, go-google, go-google-prepare, models-discovery,
responses-routes) now name the renamed id, since each means "the
Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
one unpriced row, which the vendor has now priced, so it builds the
`Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
completions list, since it declares no SDK.
The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: skip free models the gateway has withdrawn in the live suite
The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.
A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-T1b
The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:
- The binary e2e's beforeAll budget was smaller than the blocking
subprocess deadlines it serializes. Each plugin install now has its
own deadline, the hook's budget is derived from the installs and the
boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
skipped upstream's credentialRef(ref) check. It now throws the same
TypeError for a ref outside the grammar (a host-editor-contracts
case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
level. The overrides now force both, the cache key includes them,
and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
KNOWN_DEFECT title. Each apply order now generates one test per row
and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
P-T1 pins every fact. Both now give the same accounting: which facts
are pinned by number, which P-R1 exercises first, and why F9 and F17
are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
and baseURL collapse pi's protocols onto one. F6 is recorded as first
exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
toPiCredential. host-credentials now covers all three branches, and
the env branch is asserted through pi's Models.getAuth, so the case
fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
that an id pi cannot place is not listed and that a turn on it is
INVALID_CONFIG with no request; the save and deferred-read halves
are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
pi-ai-first block was split. The fixture case now checks only
structure (the facets whose value moves are the facets whose tag
moves), and the exact pin is its own KNOWN_DEFECT case that boots no
host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
override and turns that case into a BASELINE check in the same diff.
Nothing under src/ changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-H
What the review found, and what changed:
- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
teardown and on every foreign registry change. Its JSDoc in
src/host/patch-method.ts now says so, counts the reinstalls a release
causes, says a caller disposes it before releasing its adapters, and
notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
the real host for one reason; the third (the live-reload listing) is
unreachable for a different one. docs/improvement-plan.md now gives
each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
row only structurally. test/reload.test.ts now gives every site a
tier-A probe whose decorated answer must differ from the host's own,
so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
asserted in docs and enforced by nothing. test/architecture.test.ts
now has a textual tripwire over everything apply() reaches, with a
self-check of planted violations, and the apply()-twice reload case
requires a patch record on every own function apply() leaves on the
stand-in.
- The tripwire missed several write forms (Object.assign,
Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
Object.setPrototypeOf, and an Object.* target reached through a
property path such as registration?.adapter or this.llm). The
patterns now catch them, and the self-check plants each one (17
planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
set. The test docstring, AGENTS.md and the plan's P-H section now
name the four Reflect and four Object methods matched, and why a
prototype swap is listed.
No runtime behaviour changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R1
The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:
- A completions model whose effort list names none lost its Off.
utils/model-metadata.ts now offers Off with none as its wire value
on completions, as pi 1.1.0's own entries do; Responses unchanged.
Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
(pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
while the host modules were unloaded. host/route-claim.ts now judges
it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
config and drop both planes. Recorded as a constraint in
route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
CONTRIBUTING (field count, the host-modules floor), and historical
banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
protocol's required fields and that the gateway answered 200, or
the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
live handle, and "booting with none" waits for the plugin's row to
be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
the Zen key. README.md, README.zh-CN.md, deep-dive.md,
credentials.md and redirect-planes.md now give the real order and
the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
(en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
withdrawn. They now walk up to three candidates per protocol
(support/live-walk.ts), pass a "model is unavailable" turn only when
a raw probe confirms it (support/live-probe.ts), and skip naming a
protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.
Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* ci: gate freshness only on a pull request's own generator changes
The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.
A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.
The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
Chinese table had read 27 before this change, and now matches the
English one.
"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.
It no-ops once main carries #111.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: give the freshness walk's dynamic import a script-local target
The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.
The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P-R2
What the review found, and what changed:
- The keyed flip rule read a default cell's retired-model answer as a
retired model without checking the same model on the other shapes.
planeOutcome now reads each default cell beside the legacy and root
cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
it. It is now "check the CI secret" when a sibling refused the key
and none accepted it, and "nothing else measured the key" only when
no sibling refused it; a path-gone, rate-limited or busy sibling no
longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
Google, which no flip moves) were called flip evidence, and the
"protocol" and "gone" reasons named causes nothing measured. A
wrong-protocol or path-gone cell is now decided by the cell that
endpoint: legacy dispatches to: "refused" (a flip would not serve
it) when that cell refused readably, and always for Go's Google;
"unmeasured" (re-run) when it measured nothing. Neither is flip
evidence.
- An account answer counted as the flip target accepting a protocol or
a model, though the account's gate answers before either is read. It
now counts only beside a refused key or a gone path, and a retired
model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
keyed classifier treats as routed. It now moves on past a busy
candidate, and its dated record no longer pairs a derived
upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
the verdict definitions in redirect-planes.md section 13. Section 13
now says what the code does (no answer on the last attempt is
transport-error unless an earlier attempt was a 429), and a test pins
that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
section 13 is pending its first CI run; said models.opencode.ai
declares the root shape, which the generated catalog contradicts;
said legacy cells are asserted nowhere live; and said a keyed
auth-refused means the plugin's own turns would be refused, though a
default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
conditional flip a phase, and says the keyed probes decide a plane
from the default cells read beside their siblings, not alone, and
that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
probe following a redirect, the push-to-none arm and the publish and
upload steps of the CI wiring, the free probe moving on past a busy
upstream, and the record being rewritten after every cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.
It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:
- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.
"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.
(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: follow the vendor catalog refresh
The merged catalog renames the Go Gemini model from
gemini-4-argon-contributor to gemini-4-argon; the Settings fetch
assertions (tier B and the binary e2e) now name the refreshed id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: send Google's thinking level to Gemini models the catalog gives levels
The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.
The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.
Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: correct the Go shape count, the README date and the install range
Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.
- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
shapes other than completions" restores the count its own table gives
(5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
no-SDK models). The 12 was right when written (6 + 6) and a later
refresh moved the cells but not the sentence. test/catalog.test.ts now
holds the sentence to the table's non-completions Go sum, so the next
refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
"^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
^0.12.0, neither of which can resolve to a 1.x release.
test/readme-parity.test.ts pins the two snippets to one range on
package.json's major.
Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: date the refreshed model count and correct three stale figures
The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:
- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
cannot resolve to any 1.x release; both now name package.json's
^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
ships; it now says "the models it ships", which a refresh cannot
strand.
P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* fix: address the second review of P3
The second review of P3 ran three rounds across the correctness, rules
and test-matrix lenses, and upheld 11, 4 and 4 findings. What it found,
and what changed:
- On a served plane, Settings' fetch offered pi's stale capacities, and
adopting a row saved them over the catalog's, contrary to D17 and the
README. Every row on a served plane is now read catalog-first, with
pi filling only what the catalog lacks; a route the plugin does not
serve stays pi-first (src/models-discovery.ts).
- enrichModels=false turns off the D3 complement and the NO_DISCOVERY
fallback, but the README and the P3 spec stated both unconditionally.
The README, the AGENTS.md map, the deep-dive and the plan's P3 landed
note now name the switch.
- Retired pi rows are dropped only once a live catalog read has landed,
but the docs said always. They now say so, and that offline pi's list
passes through.
- The enrichModels copy said the picker shows declared models only,
which contradicts D3. It now says the picker shows the declared
models, or the plane's catalog when the entry declares none
(src/settings-copy.ts and both READMEs).
- The plan listed the defect P3 fixes as not yet fixed, with stale line
references. It is struck through and marked fixed by P3.
- F21 was a host fact missing from both lists of pinned facts, and no
test named it. Both lists (the plan and CONTRIBUTING.md) now name it,
and so does the pi-ai restart test.
- The gap-row save test never showed the id was missing from the
composer before the save. It now asserts that first.
- The README's "Changed in 1.x" Fetch entry described the complement
unconditionally and left out the catalog-first values. It now states
both.
- README troubleshooting gave no cause for a retired pi id staying in
Fetch while offline. It now names an unread catalog. Its fix says the
first fetch once models.opencode.ai is reachable starts the refresh
and still returns the old list, because the refresh runs behind the
read and nothing runs it on a timer, so fetch again a moment later
(README.md and README.zh-CN.md).
Carried to landing, because only a push can close it: the binary e2e
Settings fetch row has not run in the floating e2e-host cell on this
head, and the PR body lacks the plan's records and describes the merge
as it was before the catalog-first change. That needs a dispatch of
ci.yml on the pushed head and a rewrite of the PR body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: refresh the generated catalog after Zen retired deepseek-v4-flash
Ported from P-R2 (29b43ca, itself from d13cbe2 on #105) so this pull
request's own `catalog freshness` goes green now, rather than waiting
for P3 to carry it up. It no-ops once the merge from P3 brings the same
change.
It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111), and the counts the docs restate follow it (Zen 81 served,
no-SDK 24, "of 81"). test/catalog.test.ts keeps this branch's
invariants, which hold the shim to its own rules rather than to a
count, so it needed no change.
(cherry picked from commit 29b43cadeba7699b421855251c267f5365dc2f5f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the audit's remaining findings on P-R1
Each finding the third audit round left open, re-verified on this head:
- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
model: already fixed (0cceb81). walkPerProtocol walks up to three
candidates per protocol and names an unreached protocol in a skip;
test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
describes auto backwards: already fixed (0cceb81) in deep-dive.md and
both hint strings. The same claim survived in a source comment,
KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
commit corrects it: a key is filed by the plane its request went to,
so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
skip decided by message text: already fixed (0cceb81). A "gone"
verdict needs a raw probe of the same id to confirm it, and a
protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
"Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
Zen-only setup hides the ring: already fixed (0cceb81).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore: follow the vendor's retirement of nemotron-3-ultra-free
The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:
- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
redirect-planes.md, protocol-routing-and-merge.md and both READMEs
("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
ships: the Zen and Go lists had kept seven ids the vendor has since
retired. The lists are main's; this branch's own prose around them
(how the Go Google base is served) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: walk past a model the gateway answers ModelDeprecated for
The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.
Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test: install the plane-lookup world before each test
The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(scripts): move the generated-table skip into the CLI-version rules
The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement
The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(testing): describe the suite this branch keeps
Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* test(e2e): keep the free-tier case's narrowing in the subject picker
Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* chore(vendor): follow opencode 1.19.5
The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs(plan): restack the plan onto 1.8.0 and the alpha host
Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* build: raise the client bundle ceiling to 136 KB
Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: complete the restack of P0
Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.
- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
it names the `packageManager` field, which main's bump had made stale.
release.yml carries the same stale sentence on main and is left to a
main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
causes that can reach it (gated, withdrawn, refused for the account,
never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
403 FreeTierError (the headers are not its cause and the patch does
not open the free tier), carries the Auto Review route clause and the
two paragraphs on the API-key-only build and the host restart; the
English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
note; the sources line names both harness tags; the P1a-P2 test matrix
says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
tables are dated 2026-10-31 (the shipped shim), apart from the
documents' measurement stamps; both READMEs name
`catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
as a retirement, and rule 2 says the body, not the status, makes it one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
* docs: close the restack review of P0
Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.
- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
the shim was said to suppress (main's regenerated shim serves one of
them on Zen). It states the rule instead: a r…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase P-R1 of docs/improvement-plan.md. Its parent, P-H (#112), is merged, and this PR targets
main.The plugin now serves the
opencodeandopencode-goroutes itself (the D13 design), using only supported host paths. The hidden-route workaround is gone.Steps aside for a route another plugin owns (D27)
The official OpenCode Console plugin (
@opencode/console) writes its ownopencode/opencode-goentries into pi-ai's config. Its setup gives pi-ai a private credentials realm whose only token isOPENCODE_CONSOLE_TOKEN, and marks each entry with anx-opencode-pluginheader. Before this change, the route claim took those entries over and served them without the realm's token. That would have failed every Console turn withMISSING_CREDENTIAL, and our headers and key capture would have touched Console traffic.Detection. It reads the host's own data, never a vendor name or a version. A plane is left to pi-ai when either rule holds:
credentialsin a realm the plugin's own row does not share. The realm is resolved the way the loader resolves it, including a realm inherited from an enclosing group.headerscarryx-opencode-plugin, in any case.What steps aside:
settings.describegraft;Live edits. A live edit that adds or removes the marker, or moves a realm on pi-ai's row, the plugin's own row or an enclosing group, moves the plane with at most one pi-ai restart. Each stepped-aside entry is logged once per row, plane and reason, at info level.
New pure module:
src/utils/plane-ownership.ts, held at 100% coverage.5ecc037e21f99d6527780d63a24ca1db1d251f46267052ab0574604e140c9c93c85d97d58994bb2d5a86Tests.
test/host-planes-foreign.test.ts, uses tier B's newrealmoption. It writes the Console's patch shape with our own fixture plugin; no vendor file is read or run.Red first. With
src/at33925efand the new tests in place, 14 tests failed: 9 inhost-planes-foreign(including the tier-A marker case), 3 infetch-patch, 1 instream-hookand 1 inplane-profile. Mutation checks: removing the stream-hook skip fails 2 tests, removing the fetch passthrough fails 4, removing the discovery wiring fails 2, and removing the meter's realm skip fails 1.Review. Three rounds:
Gate on
b2d5a86:vp check: 0 errors, 11 warnings.HOST_LIMITATIONrows).Behaviour change, in both READMEs: a new section, "With OpenCode Console installed", and a "Changed in 1.x" bullet.
Known limit: a live edit that adds or removes the marker on a plane pi-ai has already registered takes effect at pi-ai's next restart, because pi-ai registers its routes when it activates (F11, F15).
Follow-up for P3 (#115): its discovery complement must leave out stepped-aside planes. That goes into the cascade.
Merged main (#170, #159)
33925efmerges main7504fe2. That brings in:There were six conflicts: the README pair, three docs and
test/catalog.test.ts. Each was resolved by keeping this PR's structure and taking #159's numbers. The merge's review found 0 findings, and the gate passed on both channels.Rebased onto main 1.9.2 and the latest hosts
This section supersedes "Restacked onto main 1.8.0" below wherever they disagree.
(Planned as main 1.9.1; by the time the rebase ran, main had cut 1.9.2, and the branch sits on main
1b4ebf0,v1.9.2plus four commits, among them #110 (P-T1b) and #112 (P-H).)The PR is now its own change on its parent. The branch is one rebase commit plus its adaptation and review-fix commits on top of main
1b4ebf0:d90f0660ab3fa79ba2290d4f65575f82bd1bbd924daa70a431f80a510b6fc99974b43a26d1e74. Its tree is0b6fc99's, so the branch moved forward without a force-pushThe rebase had no conflicts.
Dropped because main already has it: nothing. Every hunk of the earlier branch that main now carries (P-T1b, P-H) went away with the parent's history, and none of this PR's own change was already on main.
Host adaptation. Both channels' newest builds, rc
0.2.2-rc.2and alpha0.2.2-alpha.3, run on the new cordis core. That core calls aninternal/configlistener as(fiber, next)withthisnull, where the old core bound the fiber asthisand passed the raw value.src/utils/config-run.ts, held at 100%). It callsnext()exactly once and strips whatnext()answers.next()in every listener, so the same listener serves both cores.maxRequestImages, andimageOffloadCountQuantum(min(10, bound)unless the entry sets one), with pi-ai's own defaults and messages. They carry these only where pi-ai's Config answers the bound (the 0.2.2 bridges), and neither where it does not.[KNOWN_DEFECT]row (pi-ai keeps the planes on the new core) is now a[BASELINE]row: both planes are the plugin'sPlaneAdapteron either core. The probe counts everyinternal/configcall by shape (configHooks), and the row requires one known shape throughout.group.remove(loader 1.1.0 deletes the store entry before it emitsloader/partial-dispose). It pins that every route is withdrawn, the plugin's row stays ACTIVE, nothing restarts, and nothing logs at warn or error.Measured cells (binary e2e, built from the head tree):
internal/configshape0.2.2-rc.2(cordis 4.1.0, pi 1.5.2)0.2.2-alpha.3(cordis 4.0.6-alpha.2, pi 1.5.2)0.2.2-rc.2, pi 1.3.00.2.2-alpha.3, pi 1.3.00.2.1-rc.5(cordis 4.0.5, pi 1.5.0), run against the rc expectationsopencodeandopencode-goare bothPlaneAdapter, the planes settled, and the control edit was accepted. Each floor cell asserted that every floor override was applied.[HOST_LIMITATION]"the host's pi sends the Go Gemini level model a thinking budget", and[HOST_LIMITATION]"no config-editor save succeeds, because the host's own code throws". Every host did save in practice.beforeAllbecause the prefix holds another channel's host. It ran with a temporary, uncommitted override behindDSH_HOST_LEGACY_OLD_CORE=1that skips only the dsh-version and declared-range record pins.PlaneAdapter, and the save was accepted.vp check: 0 errors, 11 warnings (limit 18).check passed, and the consumer install succeeded.lib/found nothing, and planted canaries confirmed that it read its input.What remains.
keyless-matrix.e2e,protocol-routing.e2eand thee2e-hostjob for both channels' npm and floor cells. The alpha npm row is now expected to be[BASELINE], not[KNOWN_DEFECT].[HOST_LIMITATION]rows stay until the host fixes them upstream (D26).Restacked onto main 1.8.0
This branch now carries main 1.8.0 through its parent (P-H) and the two-channel host: rc
0.2.1-rc.5and alpha0.2.2-alpha.2, with pi unpatched. Some sections below describe the branch before the restack; where they disagree, this section wins.Scope now. The plugin owns the
opencodeandopencode-goroutes: the route claim,PlaneAdapter, the describe graft and the host modules. One characterization table serves every host cell (S0_TODAY_BY_CELL, built per channel bys0PluginFor). Rows differ only where the host decides.Dropped because main or the host now does it (D26).
src/host/google-levels.ts) is deleted, with its wiring inplane-provider,plane-profile,host-modulesandcordis-context, its tests, its 100% coverage floor and its doc lines. The thinking control is pi's own. The catalog level rule now lives only intest/support/thinking-levels.ts, where it picks test subjects. The "Behaviour changes" bullet aboutthinkingLevelabove no longer holds.src/utils/plane-model.ts, including thelegacybranch that took pi's own/zenbase (D16). The protocol order is catalog SDK, then the route's api, then completions. Bases come only from the endpoint table.plane-profileno longer restates pi-ai's default window and output tokens (D17).test/google-thinking.test.tsimported the deleted bridge. Its intents moved to the mock suite and to theHOST_LIMITATIONrows.decoratePlaneAdapterscase and its stream-hook "cannot be served" case are both obsolete after this PR.withMoved, the keyless/keyed listings,saveRejectedUnknown,SAVE_BLOCKED_BY_ANY_UNDESCRIBED, the pi-ai-first override andNEXT_DISPATCH. Every host cell now reachesTODAY_DISPATCH.Decisions applied.
D14': the route claim and the plane adapter are built from the host's own pi on each channel, and nothing is bundled or patched.
D22': a channel bump re-measures F8/F12/F14/F20 per channel against that channel's own expectation, and the route claim adopts a moved
internal/configcontract by detecting the shape the host dispatches, never its version.D24:
debug/debugFilefollow main's wording incordis.patch.yml, and the lifecycle test usesapply(ctx, { debugFile }).D25: host limitations are recorded and not fixed:
0.2.2-alpha.2installed by npm cannot save.Both are detected from the host's own answers, and both READMEs list them.
Known defect (resolved by the rebase above). On cordis
4.0.6-alpha.2(alpha's npm cell) the route claim did not engage, so pi-ai served the planes. This was a[KNOWN_DEFECT]row; the rebase's claim on both call shapes turned it into[BASELINE].Live re-measures CI must confirm.
protocol-routing.e2e: the Google level case on Go with a key, on the newest pi. It must skip asHOST_LIMITATIONon the rc floor.thinkingBudgetfor the Go level-only model. TheHOST_LIMITATIONrows rest on this, and Zen is expected to refuse too but is not yet measured.keyless-matrix.e2e:TODAY_DISPATCH, all four protocols on the modern family on both planes.protocol-routing.e2e: the keyed adapter walks throughPlaneAdapteron tier A for both planes.e2e-hostfor both channels' npm and floor cells. It recordsconfigHookper cell and confirms the rc floorHOST_LIMITATIONrows.x-opencode-request,x-opencode-projectandRetry-Afterrows, and the combined Google User-Agent.What changed
inject: [].src/host/route-claim.ts(new)internal/configlistener gives pi-ai's fiber a copy of its config with the planes the plugin takes removed (both, unless D27 steps aside). On a commit it schedulesreconcile. On a Settings candidate it strictly validates only the taken plane entries that changed. The served pi-ai row is sticky: it stays the same while it is live and still declares a plane.src/host/plane-provider.ts(new)reconcilebuilds onePlaneAdapter, a subclass of the host'sPiAiAdapter, and registers only the taken planes throughregisterAdapter/handle.replace. pi-ai keeps every other route.src/utils/plane-profile.ts,plane-model.ts,model-metadata.ts,protocols.ts,plane-ownership.ts(new)utils/purity gate covers them.src/host/describe-graft.ts(new)settings.describewrapper, built onpatchMethod, that grafts the committed plane entries back (D18).src/host/host-modules.ts(new)cordis-context.ts. pi-ai's module comes from pi-ai's own loader entry during its commit run, so nothing waits on a timer.responses-provider.ts,responses-routes.ts,google-bridge.ts,pi-provider.ts,utils/google-replay.ts(removed after a mock-gateway replay test showed a single provider label); frommodels-discovery.ts: the listing hide,decoratePlaneAdaptersanddecorateModelInfo; the stream redirect; the D19 settings; the hidden ids inDEFAULT_PROVIDERSandcordis.patch.yml.host-planes,host-planes-foreign), contract (describe-graft,catalog-change) and mock (plane-dispatch) suites. Characterization rows flipped to new[BASELINE]values: 14S0_TODAYfacets, 14 namedhost-characterizationcases and 5 binary-e2e rows, each named under "Characterization rows this PR flips" below. Every other row stays green unchanged.Some details refine the plan text on purpose, and the plan's P-R1 entry records each one:
infothe way D17 does, so theINFO_NOT_FROM_CATALOGBASELINE guard stays intact.Behaviour changes
After this phase:
/inference/go/google/v1beta.apiKeyEnvnow sends the Zen key. pi's own lookup readsOPENCODE_API_KEYon both planes, so such an entry authenticates every Go protocol with it. Before, Go's Responses, Messages and Google turns defaulted toOPENCODE_GO_API_KEY(completions already took pi's rule). The meter follows: it prefers the key a Go turn carried, so after the first such turn it sends the Zen key to the Go usage endpoint, is refused, and falls back to the Zen pill or "subscription required". The fix for a user isapiKeyEnv: OPENCODE_GO_API_KEYon the Go entry; README step 3 and "Changed in 1.x" (both languages) say so, andtest/host-planes.test.tspins both cases.x-opencode-pluginmarker) is left to pi-ai and untouched by this plugin (D27, above).Two changes from the last review round before the rebase:
Characterization rows this PR flips
Every row below was
[KNOWN_DEFECT]or[INTENDED_CHANGE]on the base and is[BASELINE]here. No[BASELINE]row on the base moved.S0_TODAYfacets (test/support/host.ts; facet order info / callConfig / wire), 14:opencode/deepseek-v4-pro[INTENDED_CHANGE]/zen/v1/chat/completions/inference/openai/v1/chat/completionsopencode/gpt-5.3-codex-spark[KNOWN_DEFECT]user row ignored (128000),INVALID_CONFIGokopencode/not-in-any-catalog[KNOWN_DEFECT]INVALID_CONFIG, nothing sentok, sent as completions on/inference/openai/v1(D15, D16)opencode-go/deepseek-v4-pro[INTENDED_CHANGE]/zen/go/v1/chat/completions/inference/go/openai/v1/chat/completionsopencode-go/minimax-m3[KNOWN_DEFECT]toggle-only model offered Minimal…Maxopencode-go/gemini-4-argon[KNOWN_DEFECT]INVALID_CONFIGokopencode-go/minimax-m3.1-flash[KNOWN_DEFECT]INVALID_CONFIG, nothing sentok, sent on/inference/go/openai/v1opencode-go/not-in-any-catalog[KNOWN_DEFECT]INVALID_CONFIG, nothing sentok, sent on/inference/go/openai/v1The listing rows
keylessListing/keyedListingare replaced bylistingBeforeTurn/listingAfterTurn(every declared id, D21), andsaveRejectedUnknownis gone (D15).Named
host-characterizationcases, 14 (base title → head title):[INTENDED_CHANGE]registry: both planes are pi-ai's, six hidden routes →[BASELINE]registry: both planes are the plugin's adapter, no hidden route (D13)[INTENDED_CHANGE]keyless listing hides every paid model →[BASELINE]listing before any turn: every declared id (D21)[KNOWN_DEFECT]keyed listing: only pi-resolved ids →[BASELINE]listing after a turn: gap ids and the unknown id included (G1)[KNOWN_DEFECT]toggle-only model offered Minimal/Xhigh/Max →[BASELINE]offered Off / On, an untaken level refused (D1)[INTENDED_CHANGE]completions dispatch to pi 1.1.0's legacy bases →[BASELINE]completions dispatch to the modern family on both planes[KNOWN_DEFECT]the adapter decoration never runs in this order →[BASELINE]the plugin patches nothing on its ownPlaneAdapter[KNOWN_DEFECT]Zen gpt-5.3-codex-spark: user row ignored,resolveCallConfigrefuses →[BASELINE]user row reaches model info, sent on Responses (D17)[KNOWN_DEFECT]Go minimax-m3.1-flashINVALID_CONFIG→[BASELINE]validates, sent once as completions on Go's modern base (G1)[KNOWN_DEFECT]Go gemini-4-argon refused while served through the bridge →[BASELINE]validates, sent on Go's Google base with the Go key (F10)[KNOWN_DEFECT]a stored gap id blocks every save of its plane →[BASELINE]a stored gap id blocks no save; the unknown id is listed, validates and is sent (D15)[KNOWN_DEFECT]the probe's save sequence needs the gap ids dropped first →[BASELINE]the first append of the unknown id is accepted[INTENDED_CHANGE]the adapter decoration runs in pi-ai-first order →[BASELINE]pi-ai held the planes first, so the claim restarted it exactly once[KNOWN_DEFECT]the apply order decides one facet (Go gemini-4-argon) →[BASELINE]no facet depends on the apply order (todayRowsFor's override dropped)[KNOWN_DEFECT]the unknown id is what the save check names →[BASELINE]the save check accepts it and the file holds it (D15)Binary-e2e rows (
test/e2e/host-binary.e2e.ts), 5:[INTENDED_CHANGE]registry: the planes are pi-ai's, hidden routes registered →[BASELINE]both planes are the plugin'sPlaneAdapter, no hidden route (D13)[INTENDED_CHANGE]keyless listing hides every paid model →[BASELINE]listing before any turn: every declared id (D21)[KNOWN_DEFECT]keyed listing: the gap ids are missing →[BASELINE]listing after a turn on each plane: every declared id (G1)[KNOWN_DEFECT]a stored gap id blocks saving any new id →[BASELINE]a stored gap id blocks no save (D15)[KNOWN_DEFECT]a catalog-unknown id cannot be saved →[BASELINE]it saves, is listed, validates and is sent as completions on the modern base (D15, D16)The binary e2e also gained rows with no base counterpart: the plane provider is built from the host's own pi, and the Settings graft (below).
Records the plan asks for
docs/improvement-plan.md(Test plan, P-R1) asks this PR body to record four things.auto=modern) dispatch: Zenhttps://opencode.ai/inference/{openai/v1, anthropic/v1, google/v1beta}, Gohttps://opencode.ai/inference/go/{openai/v1, anthropic/v1}andhttps://opencode.ai/inference/go/google/v1beta(the catalog's family map, elseMODERN_FALLBACKinsrc/utils/protocols.ts).legacy:/zen/v1and/zen/go/v1(/zen,/zen/gofor Messages), with Go's Google staying on/inference/go/google/v1betaunder every family. The keyless matrix (every PR in CI) asserts that no cell the default endpoint dispatches to answers404; the binary e2e records the URL each characterized model was sent to through the shipped bundle (the wire facets above).describegraft.src/host/describe-graft.tsgrafts the committed plane entries back into pi-ai'ssettings.describevalue (D18). Held by the binary-e2e row "the Settings graft shows each plane's own entry, and the Models page reads both planes configured under their apiKeyEnv", by the[BASELINE]hasProviderApiKeyrow in both apply orders, and bytest/describe-graft.test.ts.PiAiAdapterowns them again), and pi-ai's own save check is back in force; dispose then apply hands them to the plugin again (test/host-planes.test.ts, tiers B and A). When the host's modules cannot be read the plugin stands down and says so; a failing handback restart is reported, not thrown (test/route-claim.test.ts).Review
In rounds 2 and 3, reviewers reported some of the same defects (the catalog-listener unsubscribe; the store-opt-in case reading the shell's
OPENCODE_API_KEY; the modules-unreadable refusal). Round 3's fixes:Three new route-claim cases fail under the old behaviour and pass with the fix.
The audit round's eight findings repeat four defects. The README.zh-CN.md credential wording, the live adapter walk (a withdrawn model now moves to the protocol's next candidate, a withdrawal is confirmed by a raw probe, and a protocol no candidate reached skips by name) and
docs/deep-dive.md's key-isolation claim were already fixed on this branch; a source comment insrc/go-discovery.tsstill claimed the isolation and is corrected; the description findings are this body's flipped-row and record sections and the D20 Go entry above.Before each commit, the full diff and the new files were checked for absolute local paths and anything shaped like a credential. Nothing was found, and the pattern was confirmed to match a known positive.
Note: the
catalog freshnesscheck gates a pull request only when its own diff touches that check's generator inputs (D23). This PR touches them, so a vendor catalog change since the last refresh turns it red until the nightly refresh lands on main and is merged in.🤖 Generated with Claude Code
https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD