Skip to content

test: measure which endpoint families the gateway serves - #114

Merged
viztor merged 202 commits into
mainfrom
claude/vigilant-mendel-bctz9n-p-r2
Nov 13, 2026
Merged

viztor merged 202 commits into
mainfrom
claude/vigilant-mendel-bctz9n-p-r2

Conversation

@viztor

@viztor viztor commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Phase P-R2 of docs/improvement-plan.md, stacked on #113.

P-R2 is a measurement phase. The default endpoint family stays modern, so the plugin's behaviour does not change and no S0_TODAY or characterization row flips. The title carries no ! and the commit has no breaking footer.

What changed

# Item Where
1 The default has a name. AUTO_ENDPOINT_FAMILY = "modern" is what auto resolves to. Before this, the choice sat inside a ternary. A later flip is a one-word edit, and the constant is pinned. src/utils/plane-model.ts, test/endpoint-family.test.ts
2 Wider keyless matrix. It now covers 3 URL shapes: legacy /zen(/go)/v1, a new root /inference(/go)/v1, and modern /inference(/go)/{openai,anthropic,google}/…. That is 24 cells over 4 protocols and 2 planes. It adds LEGACY_DISPATCH (the cells endpoint: legacy really sends to) and one keyless free-model probe with its own pure classifier. classifyKeylessProbe is unchanged. test/support/keyless-matrix.ts, test/e2e/keyless-matrix.e2e.ts
3 Minimal keyed probes. There is one probe per plane × protocol × shape. Each uses the plane's own key in the header its protocol reads, with tiny bodies (8/16/1/8 tokens) and a pure keyed classifier. The identity comes from one shared constant, PROBE_IDENTITY, and an offline test pins it against the identity a real turn sends. Only the cells the default sends to (TODAY_DISPATCH) are asserted. Every other cell is recorded. test/support/keyed-probes.ts, test/e2e/keyed-probes.e2e.ts, test/keyed-probes.test.ts
4 The live job is gated by tier. OPENCODE_E2E_KEYED takes none, probes or full. Unset means full, so local runs behave as before. CI picks the tier per event: probes for a pull request touching src/host/**, protocols.ts, plane-model.ts, identity.ts, request-shape.ts or the probes' own inputs; full nightly and on dispatch; none otherwise. Secrets are passed only when the tier is not none. The full keyed suites read their keys through armedKey(tier, "full", …). .github/workflows/ci.yml, test/e2e/*.e2e.ts
5 A dated record. Both live suites write a markdown table to $OPENCODE_E2E_RECORD_DIR. The job publishes it to the run summary and to an endpoint-families artifact. test/support/endpoint-record.ts, test/endpoint-record.test.ts
6 Docs. docs/redirect-planes.md §13 uses the same columns, and every cell reads "pending the first CI run". A unit test holds the doc's tables to the formatter's columns and row order. The tier scope, the skip reasons and the plan's status row are updated. docs/redirect-planes.md, docs/improvement-plan.md, CONTRIBUTING.md, AGENTS.md

Decision recorded in the plan: endpoint: legacy is recorded, not guarded, in the live job. Unit tests pin its mapping of every protocol on both planes. If a keyed modern cell fails in CI, that is the evidence for a later fix: that flips the default.

Behaviour changes

Plugin: none. The changes below are to the test suite and CI only:

  • The keyed probes take their identity from one shared constant, PROBE_IDENTITY in test/support/keyed-probes.ts. What they send live is unchanged: user-agent OPENCODE_UA, x-opencode-client, x-opencode-project, and the probe session id under both session headers. They deliberately leave out x-opencode-request, the per-request metric id.
  • CI arms the probes keyed tier on a pull request that touches src/utils/request-shape.ts, because every Messages probe sends its anthropic-version. This is in addition to src/host/**, protocols.ts, plane-model.ts, identity.ts and the probes' own inputs.
  • An earlier review round switched protocol-routing's PLUGIN_HEADERS to OPENCODE_UA. That user agent reaches the suite's keyless free-tier and unmetered cases on every tier, so on every pull request and every push to main. This PR's own e2e run is therefore the first live observation of the new user agent on keyless traffic, before the nightly run. docs/credentials.md records that this user agent did not change the free-tier verdict.

Review

Each round ran three reviewers (correctness, tests, rules) with none missing. Every upheld finding was fixed in the next round.

Round Found Upheld Fixed Headline findings
1 22 22 22 (round 2) (high) keyed probes sent no session header, so the Go half passed without measuring anything; (medium) a keyed probe passed green with no default-family cell measured; (medium) a rate-limit hang turned the probes red where protocol-routing skips
2 8 8 8 (round 3) (medium) probes sent the plugin's own-request user agent, not the one its turns carry, and the unit test checked against that same constant; 404 model-gone matched Anthropic's generic not_found_error; a pipefail hazard in the tier step
3 8 8 8 (round 4: 7 in the tree, 1 in wording) (medium) the live suite's identity was a second copy no offline test checked; upstream-busy clauses never decisive alone; the tier scope left out request-shape.ts

Round-4 mutation checks were each run, watched fail, then restored:

  • The e2e passing its own patch-UA identity: 1 test failed.
  • PROBE_IDENTITY bound to the patch UA: 4 failed.
  • Dropping the 529 clause: 1 failed.
  • Dropping the UNAVAILABLE and "model is overloaded" alternatives: 2 failed.
  • A case-insensitive UNAVAILABLE match: 1 failed.

Verification

Node Step Result
26 pnpm run check 0 errors, 11 warnings in 203 files (limit 21)
26 vp test run 94/94 files, 1454/1454 tests
26 vp test run --config vitest.mock.config.ts 4/4 files, 61/61 tests
26 pnpm run build exit 0
26 scripts/check.ts check passed, including "lib/ carries the current build, including the served client bundle"
26 shuffle, seeds 1, 42 and 1234 94/94 files, 1454/1454 tests each
26 vp test run --coverage statements 97.64 (ratchet 97.5), branches 94.31 (94), functions 97.51 (97.4), lines 97.63 (97.5); per-file thresholds pass
24 shuffle, seed 7 94/94 files, 1454/1454 tests
24 mock suite 4/4 files, 61/61 tests
— local-path grep over git diff, the 6 new files and lib/ 0 hits; a positive control matched 1
— credential-shape grep over the diff and new files only a pinned action SHA and the test fixture key sk-probe-0123456789abcdef

The first five Node 26 rows were re-run on the final tree right before the commit. The live keyed and keyless results are not in this PR: they come from this branch's CI run, and §13 stays "pending" until then.

Known red check: catalog freshness fails on every branch until the vendor-catalog refresh lands on main. It does not come from this change.

Restacked onto main 1.8.0

The branch now carries main 1.8.0 through its parent, #113 (P-R1), which merged main. P-R2 merged P-R1; it did not merge main itself.

Scope after the restack

test: measure the endpoint families against the live gateway, on both harness channels.

  • AUTO_ENDPOINT_FAMILY (modern) is the one value auto resolves to. test/endpoint-family.test.ts pins the bases of the default and of legacy for every plane × protocol, and the cells each setting reaches (TODAY_DISPATCH, LEGACY_DISPATCH). Since P-R1 these are the same on every host cell. No behaviour change, and no characterization row flips.
  • Keyless matrix: 3 shapes (legacy, root, modern) × 4 protocols × 2 planes, plus a keyless free-model probe and System One's three shapes (L13).
  • Keyed probes, tiered by OPENCODE_E2E_KEYED (none/probes/full). They add the keyed variants of the default completions cell: x-opencode-request (L4), x-opencode-project as global, folder or 40-hex (L5), and reasoning_effort: "none" (L10). They also record every 429's Retry-After (L9).
  • The usage cap is its own verdict. A 429 FreeUsageLimitError is now free-usage-limited and is never retried (L3). It is not a rate limit, and it is not free-tier-gated, the name already used for the 403 session gate.
  • The classifiers and outcome rules are pure and unit-tested offline. The mock serves every request that either endpoint setting dispatches and no root one. This is main's test(mock): the mock must serve every base the plugin can produce #142 mock-coverage gate, ported to work per request.
  • Both suites write dated tables to the run summary and to the endpoint-families artifact, for §13.
  • P-R2b (a conditional fix:) pastes the first CI run into §13. It flips the default only if that run shows flip evidence.

Dropped, because main or the parent now does it

  • ModelDeprecated as a P-R2 gone rule. P-R1 brought it with live-probe.ts's gone rule. After the merge, keyless-matrix.ts's isModelGoneMessage carries it, so there is nothing left for P-R2 to add.
  • "Probe the root family /inference/v1" (L3). P-R2 already probed the root shape as one of its three. The only new URL is System One's root.
  • The name free-tier-gated for the usage cap. That name already means a 403 FreeTierError (a pass). The cap is free-usage-limited (a skip).
  • D14' (pinned pi 1.3.0 with the harness patch) and the gate's patched alpha cell. The owner's no-patch, two-channel decision, which P-R1 carries, supersedes both.
  • The combined Google User-Agent re-measure. It was closed offline. The fetch patch replaces the User-Agent on every protocol's request, and test/mock/protocol-compat.mock.ts pins Google's wire value to OPENCODE_UA on both planes. docs/redirect-planes.md §9 now says so.

Decisions applied

  • D14'' / D22'' (supersede D14' / D22'): this repository carries no pi patch, and two channel records, rc and alpha, are named once. The dispatch set and the binary e2e cover both channels. patches/ is gone from the CI seam regex.
  • D26 (with D16 and D17 as it rewrites them): baseUrlFor takes no pi argument, and legacy reads the fixed table. Test fixtures restate none of pi-ai's defaults, so test/endpoint-family.test.ts now uses stand-in window and output values.
  • D24 and D25 do not touch this phase.

Live re-measures CI must confirm

  1. The first e2e run on this PR uses the probes tier, because the PR touches src/utils/plane-model.ts and ci.yml. Paste its keyless and keyed tables into §13 (P-R2b). If a modern cell shows flip evidence, flip AUTO_ENDPOINT_FAMILY, together with the copy, TODAY_DISPATCH and the re-tagged wire facets.
  2. From that run, record:
    • the free probe's verdict on the current gate (served, free-tier-gated or free-usage-limited);
    • the root /inference(/go)/v1 cells;
    • System One's three shapes;
    • x-opencode-request present vs absent (L4);
    • x-opencode-project as global, folder or 40-hex (L5);
    • Retry-After on Go's 429s (L9);
    • reasoning_effort none vs absent (L10).
  3. The nightly full run should measure whether the legacy /zen/v1 cells still answer for plane models under the current gate.
  4. These are inherited from P-R1 and were confirmed again locally. They belong to the alpha.3 bump and upstream, not to this PR:
    • On the alpha npm cell, the route claim does not engage (KNOWN_DEFECT).
    • On every cell, no config-editor save succeeds (HOST_LIMITATION).

Rebased onto main 1.9.2 and the latest hosts

Rebased onto P-R1 (main 1.9.2) as its own change.

(The section is named for main 1.9.2, the version the branch actually sits on; an earlier draft said 1.9.1, which review found wrong.)

This PR is now its own change on its parent

The pre-rebase history (44 commits, including six merges of the parent) is replaced by this PR's own change on P-R1's tip 26d1e74:

  • b54b1de test: measure which endpoint families the gateway serves: the rebase commit, the whole P-R2 change in one commit.
  • a706d44 docs: record P-R2's dispatch set re-measured on the new host core: the one adaptation commit. It records the measurement below in docs/improvement-plan.md (the P-R2 status row and a "Rebased onto P-R1 (main 1.9.2), on the new host core" bullet).
  • af1de77: a history-only merge whose tree is a706d44's. It keeps the pre-rebase tip 24fcf00 as a second parent, so the branch moved forward without a force-push.

Conflicts were documentation only: AGENTS.md (repo map: the parent's config-shape.ts and config-run.ts entries kept, the PR's keyless-matrix / keyed-probes / endpoint-record lines and its AUTO_ENDPOINT_FAMILY insertion added), docs/improvement-plan.md (the parent's status rows and P-R1 bullets kept; the PR's P-R2 and P-R2b rows, its section 8 and its adjustments row added) and docs/redirect-planes.md (the parent's rewritten "What is verified" paragraph kept; the PR's §13 appended).

Dropped because main already has it

Nothing. Every hunk of the P-R2 change survived the rebase.

Host adaptation

None was needed in the records, the contracts or the route claim. The hosts are rc 0.2.2-rc.2 (cordis 4.1.0) and alpha 0.2.2-alpha.3 (cordis 4.0.6-alpha.2), both on the new cordis core with the (fiber, next) internal/config shape. P-R1 makes the claim engage on both, so the alpha npm KNOWN_DEFECT in item 4 above no longer applies. The plugin builds every base from its own endpoint table (D26), so the core the claim runs on does not change where a turn goes, and no probe, classifier, cell or dispatch table had to move.

Measured cells

Each published-binary cell's probe turns were matched against the keyless matrix's cells:

Cell Core / shape Turn rows Cells reached Planes owned by
rc 0.2.2-rc.2, npm and floor cordis 4.1.0, fiber 12 8, exactly TODAY_DISPATCH PlaneAdapter (both)
alpha 0.2.2-alpha.3, npm and floor cordis 4.0.6-alpha.2, fiber 12 8, exactly TODAY_DISPATCH PlaneAdapter (both)
legacy 0.2.1-rc.5 cordis 4.0.5, config 12 8, exactly TODAY_DISPATCH PlaneAdapter (both)

No URL went unmatched. Host-binary e2e: each of the four new-core cells gave 13 passed, 2 skipped (the two HOST_LIMITATION rows: the Go Gemini thinking budget, and no config-editor save), 0 failed. The legacy old-core cell gave 12 passed, 3 skipped, 0 failed, its extra skip being the wire row that skips on pi 1.5.0, so on that cell only the probe shows the dispatch. In-process, tier B on both channels (newest pi and floor) passes the characterization's wire rows, and TODAY_DISPATCH's keyless-matrix row and the mock's every-dispatched-cell row pass on both channels.

Gate on the rebased tree: check 0 errors, 11 warnings in 234 files; unit 111/111 files, 1901 passed, 1 skipped (also seeds 1 and 42, and seed 7 on Node 24); mock 4/4 files, 69/69; build and scripts/check.ts passed; coverage statements 97.78, branches 94.45, functions 98.22, lines 97.77, all over the ratchet; the rc channel and both floors pass the unit, mock and host files; local-path grep 0 hits.

What remains

  • The live probe rows (keyless matrix, keyed probes, free probe, System One, the L4/L5/L9/L10 variants) were not sent from the rebase machine, because the gateway was unreachable there. §13 stays "pending the first CI run", and P-R2b fills it from this branch's CI.
  • The two HOST_LIMITATIONs belong upstream (D26).
  • Known red check: catalog freshness, as above.

Generated by Claude Code

claude added 28 commits October 22, 2026 22:24
- 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
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

viztor commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

catalog freshness fails on 4c491fe, in the Go limits half. The model catalog itself reports shim is current. The step that fails is limits:shim: it reports src/go-limits-data.ts as stale and notes a new row it resolves to gemini-4-argon, an id not in the bundled Go catalog.

This PR didn't cause it. scripts/generate-go-limits.ts and src/go-limits-data.ts are byte-identical to the base (#113), and the same check passed on #113 at 13:34 UTC. The vendor's Go limits page changed after that, so every branch, main included, fails this step on its next run.

No fix exists yet. Regenerating the table needs opencode.ai, which this machine can't reach, and the generated file is never edited by hand. The nightly scheduled run on main regenerates both tables into chore/generated-refresh (#111). Once it does, I'll port the new file down the stack the same way as 813e6f3. A re-run before then would fail the same way, so I haven't spent one.


Generated by Claude Code

viztor commented Nov 12, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto P-R1 (main 1.9.2) and the latest hosts

Rebase: 24fcf00 (pre-rebase tip, 44 commits with six parent merges) -> af1de77. The branch is now this PR's own change on P-R1's tip 26d1e74: the rebase commit b54b1de plus one adaptation commit a706d44. af1de77 is a history-only merge whose tree is a706d44's and whose second parent is 24fcf00, so the branch moved forward with a plain fast-forward push and no force-push.

Conflicts (documentation only):

  • AGENTS.md: the parent's config-shape.ts and config-run.ts entries kept; the PR's keyless-matrix / keyed-probes / endpoint-record lines and its AUTO_ENDPOINT_FAMILY insertion added.
  • docs/improvement-plan.md: the parent's status rows and P-R1 bullets kept; the PR's P-R2 row (now "rebased onto P-R1 as its own change"), P-R2b row, section 8 and adjustments row added.
  • docs/redirect-planes.md: the parent's rewritten "What is verified" paragraph kept; the PR's §13 appended.

Dropped because main has it: nothing.

Adaptation commits:

  • a706d44 docs: record P-R2's dispatch set re-measured on the new host core: records the measurement below. No records, contracts or claim code had to change: the plugin builds every base from its own endpoint table (D26), so the core the claim runs on does not change where a turn goes. No behaviour changed, so there is no red test.

Review rounds:

Round Found Upheld Fixed
1 4 3 3: all three said the rebase commit's message named main 1.9.1 while the branch sits on 1.9.2. Fixed by a message-only reword of the rebase commit to "Rebased onto P-R1 (main 1.9.2) as its own change." (same trees; git diff empty), carried into the PR body as well.
2 0 0 —

Measured cells (published-binary probe turns matched against the keyless matrix's cells):

  • rc 0.2.2-rc.2, npm and floor (cordis 4.1.0, fiber shape): 12 turn rows, 8 cells, exactly TODAY_DISPATCH, both planes on PlaneAdapter.
  • alpha 0.2.2-alpha.3, npm and floor (cordis 4.0.6-alpha.2, fiber shape): the same.
  • legacy 0.2.1-rc.5 (cordis 4.0.5, config shape): the same; on this cell only the probe shows it, because the e2e wire row skips on pi 1.5.0.
  • Host-binary e2e: 13 passed / 2 skipped / 0 failed on each new-core cell (the two skips are the HOST_LIMITATION rows), 12 / 3 / 0 on the legacy cell.
  • In-process tier B on both channels, newest pi and floor, passes the characterization's wire rows; the TODAY_DISPATCH and mock every-dispatched-cell rows pass on both channels.
  • Gate: check 0 errors / 11 warnings; unit 111 files, 1901 passed, 1 skipped (seeds 1, 42; seed 7 on Node 24); mock 69/69; build and bundle check passed; coverage over every ratchet; local-path grep 0 hits.

Remains: the live keyless and keyed rows could not be sent (the gateway was unreachable from the rebase machine), so §13 stays pending the first CI run (P-R2b). The two HOST_LIMITATIONs are upstream (D26).


Generated by Claude Code

Conflicts resolved with P-R1's structure and wording, carrying #159's facts:

- README.md, README.zh-CN.md: P-R1's Go list order (Google last) and
  three-column protocol table; #159's Go completions list (+ hy4,
  longcat-2.5-free) and counts 27/32/18/9/1 of 87 (the zh table's stale
  24 is now 27). P-R1's four properties kept over main's rewording of
  the passage P-R1 replaced.
- docs/deep-dive.md: P-R1's items 4-5; item 3 takes the renamed
  "DeepSeek V4.1 Flash 1017".
- docs/protocol-routing-and-merge.md: P-R1's modern-family table and
  mistral row; #159's date, PR provenance and 87-model counts.
- docs/redirect-planes.md: P-R1's credential column and mistral
  protocol; #159's served counts (87, 32) and shape counts (27/21,
  32, 18, 9).
- test/catalog.test.ts: P-R1's protocol-keyed union test with #159's
  counts (34 Responses, 21 Messages); the Zen-route test P-R1 removed
  stays removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

viztor commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

e2e · live gateway is red here because of the CI account, not this change.

Every Go cell of the keyed probes answers 403:

An active OpenCode Go subscription is required to use OpenCode Go models.

This holds on all four protocols:

Protocol Model
Chat Completions glm-5.2
Responses gpt-6-luna
Messages qwen3.8-flash
Google gemini-4-argon

The probe sends each plane its own secret, OPENCODE_GO_API_KEY (armedKey in test/e2e/keyed-probes.e2e.ts), and the probe's own verdict is "not flip evidence: check the CI secret". The same refusal reproduced about 13 hours later on #115, with different Go models. The Zen cells pass.

The fix is outside this PR: renew the Go subscription behind the CI key, or replace the OPENCODE_GO_API_KEY secret. Making the probe skip on this would hide exactly what it exists to catch, so it stays a failure.


Generated by Claude Code

claude and others added 15 commits November 13, 2026 06:04
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
) into claude/vigilant-mendel-bctz9n-p-r2

Brings P-R1's merge of main (#170, the nightly push fix; #159, the
catalog refresh to Zen 87 / Go 32) and D27 (the route claim steps aside
for a plane entry another plugin owns, by realm or marker) under P-R2.
The merged tree is P-R2's own change on top of the parent.

Conflicts:

- AGENTS.md, the pure-utils line for the plane's decisions. P-R1 added
  `src/utils/plane-ownership.ts` to the list and the D27 clause (which
  entries the claim takes and which another plugin owns, read by realm
  and marker; the meter asks the same questions). P-R2 added the family
  `auto` resolves to (`AUTO_ENDPOINT_FAMILY`, P-R2) to the plane-model
  clause. Kept the parent's line and carried P-R2's clause into it.

Every other file merged cleanly; src/catalog-data.ts,
src/go-limits-data.ts and CHANGELOG.md are the parent's exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
P-R2's `endpoint` family (`auto`, `modern`, `legacy`) decides the base of
every plane the route claim takes. After P-R1's D27, a plane entry
another plugin owns is not taken: pi-ai serves it at its own base. P-R2's
pins only read the pure dispatch, so nothing held that the setting stays
off such a plane.

test/endpoint-family.test.ts gains a tier-A block: under each setting,
with one plane marked `x-opencode-plugin` and one of the user's own, a
turn on the taken plane reaches the completions cell of the family the
setting names, and a turn on the marked plane leaves at the URL pi-ai
alone sends (the known answer, measured first on a host without the
plugin). A second case shows the move would be visible: that URL is not
the legacy cell `endpoint: legacy` would send it to.

docs/redirect-planes.md (section 1's endpoint passage, the gate table's
row and section 13's keyed reading) and the P-R2 entry in
docs/improvement-plan.md say the same: the setting moves only the planes
the claim takes, and the keyed probes do not measure a plane another
plugin owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

viztor commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

Merged P-R1's step-aside change (D27) and main's #170 and #159 into this branch. The branch moved fast-forward to d563282.

  • 219263f merges claude/vigilant-mendel-bctz9n-p-r1 at b2d5a86.
    • The only conflict was the AGENTS.md line for the plane's pure modules. The resolution keeps both sides' clauses.
    • git diff b2d5a86 HEAD is the same 25 files as this PR's own change before the merge.
  • d563282 (test:) adds a tier-A case for each of auto, modern and legacy. A plane carrying the x-opencode-plugin marker goes to the URL pi-ai alone sends; that URL is measured first, as the known answer. The plugin's own plane goes to the family the setting names.
    • No source change was needed. AUTO_ENDPOINT_FAMILY is only reached for planes the claim takes.
    • docs/redirect-planes.md and the plan say so.
  • Review: one round, no findings.
  • Gate:
    • vp check: 0 errors, 11 warnings.
    • Unit tests, alpha and rc: 1985 passed, 1 skipped.
    • Mock suite: 69 passed.
    • Coverage: every floor met.
    • Build and bundle gate: passed.

Live job: e2e · live gateway will be red until the repository's OPENCODE_API_KEY and OPENCODE_GO_API_KEY secrets are updated. Both keys are refused even by plain requests that skip the plugin (details in #113).


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
…claude/vigilant-mendel-bctz9n-p-r2

P-R1 took the nightly catalog refresh (#171: Zen 87 -> 88, adding the
@ai-sdk/google reasoning model gemini-4-hydrogen; Go unchanged at 32) and
followed it through the README pair, docs/protocol-routing-and-merge.md,
docs/redirect-planes.md, test/catalog.test.ts and the bundled-catalog
oracle in test/thinking-levels.test.ts.

Resolution: no conflicts. docs/redirect-planes.md is the one file both
sides touched; git merged it cleanly, because P-R1 changed the §1 counts
paragraph, the §2 stamp, the Google row and the 0-of-88 sentence, while
P-R2 changed the §1 endpoint-family paragraph, later sections and §13,
and the hunks do not overlap. The merged tree is P-R2's own change
(b2d5a86..d563282) applied on top of ce889ef unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
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
viztor changed the base branch from claude/vigilant-mendel-bctz9n-p-r1 to main November 13, 2026 16:11
@viztor
viztor merged commit 08088aa into main Nov 13, 2026
9 checks passed
viztor pushed a commit that referenced this pull request Nov 13, 2026
main is P-R2 (#114) squashed; its tree equals the P-R2 head this branch
already merged (3c79027), so the merge changes no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants