Skip to content

feat: the plugin owns the opencode and opencode-go routes - #113

Open
viztor wants to merge 10 commits into
claude/vigilant-mendel-bctz9n-p-hfrom
claude/vigilant-mendel-bctz9n-p-r1
Open

viztor wants to merge 10 commits into
claude/vigilant-mendel-bctz9n-p-hfrom
claude/vigilant-mendel-bctz9n-p-r1

Conversation

@viztor

@viztor viztor commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Phase P-R1 of docs/improvement-plan.md, stacked on #112.

The plugin now serves the opencode and opencode-go routes itself (the D13 design), using only supported host paths. The hidden-route workaround is gone.

What changed

Item Change
Root plugin Declares inject: [].
src/host/route-claim.ts (new) A prepended internal/config listener gives pi-ai's fiber a fresh copy of its config with providers.opencode and providers["opencode-go"] removed. It runs the nested waterfall on that copy behind a re-entrancy guard. On a commit it schedules reconcile. On a Settings candidate it strictly validates only the 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) reconcile builds one PlaneAdapter, a subclass of the host's PiAiAdapter, and registers only the declared planes through registerAdapter / handle.replace. pi-ai keeps every other route.
src/utils/plane-profile.ts, plane-model.ts, model-metadata.ts, protocols.ts (new) Pure profiles the adapter is built from. The utils/ purity gate covers them.
src/host/describe-graft.ts (new) A read-only settings.describe wrapper, built on patchMethod, that grafts the committed plane entries back (D18).
src/host/host-modules.ts (new) The host-module loaders, moved here. The seam stays in 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.
Deleted 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); from models-discovery.ts: the listing hide, decoratePlaneAdapters and decorateModelInfo; the stream redirect; the D19 settings; the hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.
Tests New unit, real-host (host-planes), contract (describe-graft, catalog-change) and mock (plane-dispatch) suites. Characterization rows flipped to new [BASELINE] values: 14 S0_TODAY facets, 14 named host-characterization cases and 5 binary-e2e rows, each named under "Characterization rows this PR flips" below. Every other row stays green unchanged.
Docs README (both languages), AGENTS.md, CONTRIBUTING, credentials, deep-dive, protocol routing and redirect planes now describe the new wiring. In the plan, P-R1 is set to in review.

Some details refine the plan text on purpose, and the plan's P-R1 entry records each one:

  • D20 follows pi-ai's exact rule: it uses the credentials service when one is mounted, and the launch environment only when none is.
  • legacy uses pi's own base only when pi files the model under the same protocol and that base is a /zen root.
  • The fixture-sanity case derives info the way D17 does, so the INFO_NOT_FROM_CATALOG BASELINE guard stays intact.

Behaviour changes

After this phase:

  • Any saved id is accepted and served (D15). An id that no catalog knows dispatches as completions.
  • The protocol comes from the catalog (D16). Model info follows the user's own row first (D17).
  • A model whose reasoning can only be switched on or off offers Off / On (D1). Off is offered only where the protocol can disable reasoning.
  • The default endpoint is modern: the per-protocol inference family. Go Gemini dispatches on /inference/go/google/v1beta.
  • Keys resolve by pi-ai's own rule (D20).
  • A Go entry without apiKeyEnv now sends the Zen key. pi's own lookup reads OPENCODE_API_KEY on both planes, so such an entry authenticates every Go protocol with it. Before, Go's Responses, Messages and Google turns defaulted to OPENCODE_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 is apiKeyEnv: OPENCODE_GO_API_KEY on the Go entry; README step 3 and "Changed in 1.x" (both languages) say so, and test/host-planes.test.ts pins both cases.
  • A Gemini model the catalog describes by level is sent Google's thinkingLevel, never a thinkingBudget (src/host/google-levels.ts); Go gemini-4-argon refused the budget live.
  • There is no keyless listing filter (D21). A plane lists its declared ids even when no key resolves.

Two changes from the last review round:

  • When several pi-ai rows declare OpenCode planes, the row whose planes the plugin serves stays the same while it is live and still declares a plane. If another row adds a plane later, the plugin does not take it over: that row keeps and serves its own plane, and the plugin reports this once. Before, the plugin could switch rows and leave the old row's planes served by nobody, without a warning.
  • If the host's pi-ai module cannot be read, the plugin now steps aside even when no OpenCode plane is declared yet. A plane added later in Settings then goes to pi-ai's own save check. Before, it was refused for good with "try the save again".

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_TODAY facets (test/support/host.ts; facet order info / callConfig / wire), 14:

Row Facet(s) Base Now
opencode/deepseek-v4-pro wire [INTENDED_CHANGE] /zen/v1/chat/completions /inference/openai/v1/chat/completions
opencode/gpt-5.3-codex-spark info, callConfig [KNOWN_DEFECT] user row ignored (128000), INVALID_CONFIG the user row's 400000 (D17), ok
opencode/not-in-any-catalog info, callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent 262144 / no efforts, ok, sent as completions on /inference/openai/v1 (D15, D16)
opencode-go/deepseek-v4-pro wire [INTENDED_CHANGE] /zen/go/v1/chat/completions /inference/go/openai/v1/chat/completions
opencode-go/minimax-m3 info [KNOWN_DEFECT] toggle-only model offered Minimal…Max Off / On (D1)
opencode-go/gemini-4-argon callConfig [KNOWN_DEFECT] INVALID_CONFIG ok
opencode-go/minimax-m3.1-flash callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent ok, sent on /inference/go/openai/v1
opencode-go/not-in-any-catalog info, callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent 262144 / no efforts, ok, sent on /inference/go/openai/v1

The listing rows keylessListing / keyedListing are replaced by listingBeforeTurn / listingAfterTurn (every declared id, D21), and saveRejectedUnknown is gone (D15).

Named host-characterization cases, 14 (base title → head title):

  1. [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)
  2. [INTENDED_CHANGE] keyless listing hides every paid model → [BASELINE] listing before any turn: every declared id (D21)
  3. [KNOWN_DEFECT] keyed listing: only pi-resolved ids → [BASELINE] listing after a turn: gap ids and the unknown id included (G1)
  4. [KNOWN_DEFECT] toggle-only model offered Minimal/Xhigh/Max → [BASELINE] offered Off / On, an untaken level refused (D1)
  5. [INTENDED_CHANGE] completions dispatch to pi 1.1.0's legacy bases → [BASELINE] completions dispatch to the modern family on both planes
  6. [KNOWN_DEFECT] the adapter decoration never runs in this order → [BASELINE] the plugin patches nothing on its own PlaneAdapter
  7. [KNOWN_DEFECT] Zen gpt-5.3-codex-spark: user row ignored, resolveCallConfig refuses → [BASELINE] user row reaches model info, sent on Responses (D17)
  8. [KNOWN_DEFECT] Go minimax-m3.1-flash INVALID_CONFIG → [BASELINE] validates, sent once as completions on Go's modern base (G1)
  9. [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)
  10. [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)
  11. [KNOWN_DEFECT] the probe's save sequence needs the gap ids dropped first → [BASELINE] the first append of the unknown id is accepted
  12. [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
  13. [KNOWN_DEFECT] the apply order decides one facet (Go gemini-4-argon) → [BASELINE] no facet depends on the apply order (todayRowsFor's override dropped)
  14. [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:

  1. [INTENDED_CHANGE] registry: the planes are pi-ai's, hidden routes registered → [BASELINE] both planes are the plugin's PlaneAdapter, no hidden route (D13)
  2. [INTENDED_CHANGE] keyless listing hides every paid model → [BASELINE] listing before any turn: every declared id (D21)
  3. [KNOWN_DEFECT] keyed listing: the gap ids are missing → [BASELINE] listing after a turn on each plane: every declared id (G1)
  4. [KNOWN_DEFECT] a stored gap id blocks saving any new id → [BASELINE] a stored gap id blocks no save (D15)
  5. [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, the Go Gemini level model is sent a thinking level and no thinkingBudget, 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.

  • The measured endpoints. Default (auto = modern) dispatch: Zen https://opencode.ai/inference/{openai/v1, anthropic/v1, google/v1beta}, Go https://opencode.ai/inference/go/{openai/v1, anthropic/v1} and https://opencode.ai/inference/go/google/v1beta (the catalog's family map, else MODERN_FALLBACK in src/utils/protocols.ts). legacy: /zen/v1 and /zen/go/v1 (/zen, /zen/go for Messages), with Go's Google staying on /inference/go/google/v1beta under every family. The keyless matrix (every PR in CI) asserts that no cell the default endpoint dispatches to answers 404; the binary e2e records the URL each characterized model was sent to through the shipped bundle (the wire facets above), including Go gemini-4-argon on /inference/go/google/v1beta with a thinking level.
  • The describe graft. src/host/describe-graft.ts grafts the committed plane entries back into pi-ai's settings.describe value (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] hasProviderApiKey row in both apply orders, and by test/describe-graft.test.ts.
  • The handback. Disabling the plugin's row hands both planes back to pi-ai (PiAiAdapter owns 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).
  • The save of a catalog-unknown id. On the first try, with a gap id already stored: accepted, written to the file, listed, validated, and sent as completions on the plane's modern base (D15, D16) — in tier B and on the binary.

Review

Round Found Upheld Fixed
1 31 31 31
2 8 8 8
3 5 5 5
audit 8 8 8

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:

  • the sticky served row;
  • the stand-down when the modules cannot be read;
  • a launch environment that answers nothing, for the store-opt-in case.

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 in src/go-discovery.ts still claimed the isolation and is corrected; the description findings are this body's flipped-row and record sections and the D20 Go entry above.

Verification

Node Step Result
26.12.1 pnpm run check 0 errors, 11 warnings (limit 21)
26.12.1 vp test run 95 files, 1467 tests passed
26.12.1 shuffled, seeds 1, 42 95 files, 1467 tests passed each time
26.12.1 mock suite (vitest.mock.config.ts) 4 files, 67 tests passed
26.12.1 vp test run --coverage 97.71 / 94.44 / 97.58 / 97.70 against the ratchet 97.5 / 94 / 97.4 / 97.5
26.12.1 pnpm run build lib/index.mjs 322.89 kB, build complete
26.12.1 scripts/check.ts (bundle gate) every check ok, check passed, exit 0
24.22.1 shuffled, seed 7; mock suite 95 files / 1467 tests; 4 files / 67 tests passed
scratch host binary e2e (DSH_HOST_E2E=1) 11 of 11 passed, probe file written fresh at run time

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 freshness check now gates a pull request only on its own generator changes (D23, from the base), and it passes on this branch.


Generated by Claude Code

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

viztor commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

catalog freshness is red on bffd3a4 because the vendor changed its catalog, not because of anything in this PR. The Go plane renamed gemini-4-argon-contributor to gemini-4-argon and added iceman-contributor-free, and its limits page changed with them.

The fix already exists. 2ebaffc takes the regenerated tables unchanged, and the follow-ups move the fixtures and counts. #105 through #110 carry it and are green. It is being merged into this PR's base branch (P-H, #112) now. This branch picks it up with its next push: either the fix commit from the review now running on this PR, or the final sync pass.


Generated by Claude Code

viztor commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

e2e · live gateway on bffd3a4 fails the same way it did on #105, because the vendor's live state changed. The gateway now answers 400 Model is unavailable for the free nemotron-3-ultra-free and 429 for beam-free. Nothing in this PR causes that. The fix is d51a3e6: the live suite now skips withdrawn and throttled free models, and the free-tier routing case only accepts a gated model. It is green on #105–#110 and reaches this branch along the same path as the catalog refresh. When the sync merges it here, it also checks that P-R1's retargeted suite keeps that behaviour.


Generated by Claude Code

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
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

viztor commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

Second review of P-R1

This phase's first reviews ran without the dedicated reviewer, which was unavailable at the time. It has now had that review: three rounds, through the correctness, rules and test-matrix lenses.

Round Found Correctness (upheld / fixed) Rules (upheld / fixed) Test matrix (upheld / fixed)
1 20 5 / 3 8 / 7 6 / 4
2 5 2 / 2 — 2 / 1
3 8 4 / 3 — 4 / 3

In total 31 findings were upheld and 23 fixed. Round 3's two lenses found the same two problems: the Chinese README was not ported, and the live walk dropped a protocol.

Fixed

  • src/utils/model-metadata.ts: a completions model whose effort list names none offers Off again, with none as its wire value, as pi 1.1.0's own entries do. Responses is unchanged.
  • src/host/route-claim.ts: a Settings save of a plane is judged with the last modules that loaded. It is no longer refused after a handover or a partial dispose.
  • README.md: "Changed in 1.x" and step 3 now say that a Go entry without apiKeyEnv sends OPENCODE_API_KEY (D20). test/host-planes.test.ts pins both cases.
  • README.md, README.zh-CN.md: the false "strict credential isolation" claim is replaced with the meter's real key order and the 403 remedies. The Chinese README now carries every change from rounds 1 and 2, and test/readme-parity.test.ts keeps the two in step.
  • docs/deep-dive.md: corrected four points: pi's Go lookup, who answers Fetch, the meter's auto order, and the 403 for a Go entry without apiKeyEnv. The "excludes Zen keys / ring stays hidden" claim is gone.
  • docs/credentials.md, docs/redirect-planes.md: each plane's credential is now given as the entry's apiKeyEnv, else pi's OPENCODE_API_KEY.
  • src/settings-copy.ts: the keySource hint (en and zh) gives Automatic's real order: literal key, then captured key, then the declared reference.
  • src/cordis-context.ts: comments no longer describe the stream redirect, the isolated child mount or an llm root inject.
  • CONTRIBUTING.md: the field count is now "every field of Config.dict", and the per-file floor list includes host/host-modules.ts.
  • docs/opencode-parity-plan.md, docs/upstream-findings.md: marked historical, or superseded by P-R1.
  • test/mock/plane-dispatch.mock.ts: the body-shape row checks each protocol's required fields and the gateway's answer, not only body.model. A comment claimed a shape check the mock does not have; it is corrected.
  • test/route-claim.test.ts: removing the last plane now pins replace([]) on the live handle.
  • test/host-planes.test.ts: "booting with none" waits for the plugin's row to become active instead of sleeping 50 ms.
  • test/e2e/protocol-routing.e2e.ts, test/e2e/dispatch-levels.e2e.ts: paid models go through the plane adapter. Each protocol tries up to three candidates (test/support/live-walk.ts). A withdrawn model is passed only when a raw probe confirms it (test/support/live-probe.ts), and a protocol no candidate reached makes the case skip, naming that protocol. The live blocks could not run here (no vendor access, no keys); the walk is tested offline in test/live-walk.test.ts.

Declined

  • The PR description (raised in all three rounds) still gives counts, not the flipped rows and the four records. It was not rewritten before this push because it would have described work the head commit did not contain. A replacement is prepared and can go in now.
  • Fiber.update(config) with noSave false would persist the stripped config. This is recorded as a host constraint in src/host/route-claim.ts and docs/improvement-plan.md. No caller does this today, so a guard waits for one.
  • A toggle beside an effort list still offers no Off. Changing that moves [BASELINE] facets and the no-level request on Messages and Responses, so it is the owner's decision. It is recorded in docs/improvement-plan.md.
  • The Models store's join stays a restatement, with its source lines documented. Importing a client UI package into a node test for a three-line join is not worth it.

Commit: 0cceb81. Gate before the commit: check, 1387 unit tests, 63 mock tests, build and bundle gate, all passing.


Generated by Claude Code

…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

viztor commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

Fix pushed: Google's thinking level for Gemini models the catalog describes by level (4dbd22e)

The live failure. The live suite caught a turn on the Go plane's Gemini model failing. The gateway refused a token thinking budget with 400 INVALID_ARGUMENT:

Thinking budget is not supported for this model. Use thinking level instead.

Cause. pi 1.1.0 picks Google's discrete thinkingLevel over a token thinkingBudget only when the model id matches its own pattern, so a level-only model that pattern does not name gets a budget it cannot accept.

Fix. The decision now comes from the catalog, as D16 requires. A model whose catalog entry offers discrete effort levels on the Google protocol (usesCatalogThinkingLevels) is sent a thinking level. src/host/google-levels.ts builds that request from pi's own public exports, loaded lazily through the host-module seam, on both the enabled path and the no-effort path. With no effort chosen, the model gets the level pi itself falls back to, not thinkingBudget: 0. Every other model, including ids pi's pattern already serves by level, goes through pi's own path unchanged. A profile that declares Off for such a model still gets pi's own disable, which the docs record as a profile error.

Tests that pin it.

  • The pure rule over the shipped catalog (test/model-metadata.test.ts).
  • A differential check (test/google-levels.test.ts), 16 cases. pi's own streamSimple on an id its pattern names is the oracle, and the wrapper on the same model under an id the pattern misses must build a deep-equal request. Each case also asserts its own premise and that pi's path would differ for the wrapped id. Planted mutations to the wrapper turn it red.
  • Lazy loading of pi's pieces (test/host-modules.test.ts). The check counts imports, and an eager load turns it red.
  • A mock-gateway case (test/mock/plane-dispatch.mock.ts). The mock refuses a budget with the measured message, and the case requires that the turn completes on both planes and that no request carries a budget. A control requires that a model pi already serves by level keeps pi's own request.

The full gate passed before the push: check, unit suite, mock suite, build and the bundle gate. The fix reaches the stacked PRs above through the sync.

These reviews ran on the general model because the dedicated reviewer was unavailable: review:tests#1, review:correctness#1, verify#1.1, review:tests#2, review:correctness#2, verify#2.1, review:tests#3, review:correctness#3, verify#3.1.


Generated by Claude Code

…h' into claude/vigilant-mendel-bctz9n-p-r1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
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
…-mendel-bctz9n-p-r1

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