pi-claude-native 0.0.0-stage → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/LICENSE +22 -0
  3. package/README.md +115 -2
  4. package/docs/adr/0001-replay-input-contract.md +29 -0
  5. package/docs/adr/0002-per-turn-effort-placement.md +43 -0
  6. package/docs/architecture.md +182 -0
  7. package/docs/maintaining.md +88 -0
  8. package/docs/startup.md +25 -0
  9. package/docs/testing.md +146 -0
  10. package/docs/transport-measurements.md +296 -0
  11. package/docs/troubleshooting.md +77 -0
  12. package/docs/usage.md +104 -0
  13. package/extensions/claude-native/index.ts +33 -0
  14. package/package.json +56 -4
  15. package/scripts/trace-native.mjs +66 -0
  16. package/scripts/verify.py +223 -0
  17. package/scripts/verify_warm.py +255 -0
  18. package/src/admission.ts +743 -0
  19. package/src/billing.ts +87 -0
  20. package/src/child-cwd.ts +38 -0
  21. package/src/completion.ts +445 -0
  22. package/src/discovery.ts +159 -0
  23. package/src/docs-section.ts +64 -0
  24. package/src/errors.ts +89 -0
  25. package/src/inert-mcp.ts +69 -0
  26. package/src/lazy-stream.ts +41 -0
  27. package/src/measurements.ts +149 -0
  28. package/src/model-compat.ts +9 -0
  29. package/src/models.ts +28 -0
  30. package/src/native-context.ts +491 -0
  31. package/src/native-output.ts +279 -0
  32. package/src/native-rewrite.ts +70 -0
  33. package/src/process.ts +334 -0
  34. package/src/provider.ts +82 -0
  35. package/src/replay.ts +527 -0
  36. package/src/request-plan.ts +79 -0
  37. package/src/request.ts +480 -0
  38. package/src/setup.ts +205 -0
  39. package/src/stream.ts +441 -0
  40. package/src/transcript.ts +193 -0
  41. package/src/turn-effort.ts +138 -0
  42. package/src/types.ts +125 -0
  43. package/src/upstream-agent.ts +22 -0
  44. package/src/web-catalog.ts +253 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,111 @@
1
+ # Changelog
2
+
3
+ Pi shows the entries newer than the last version it saw on the first fresh launch after an update. The [release notes](https://github.com/yamatakeru/pi-claude-native/releases) start from the same text and add details and how to pin a release.
4
+
5
+ ## [0.9.1] - 2026-11-12
6
+
7
+ Claude Native is now published on npm as `pi-claude-native` and can be installed with `pi install npm:pi-claude-native`. Nothing else changed.
8
+
9
+ ### Changed
10
+
11
+ - To move a Git installation to npm, run `pi remove git:github.com/yamatakeru/pi-claude-native`, then `pi install npm:pi-claude-native`. Installing from both loads the extension twice.
12
+
13
+ ## [0.9.0] - 2026-11-12
14
+
15
+ Checked with Pi 1.5.2 and Claude Code 2.1.322. `/model` lists the models your account can use, and changing the thinking level during a session keeps the prompt cache.
16
+
17
+ ### Changed
18
+
19
+ - The model list comes from the picker Claude Code serves your account, so `/model` also shows the older models the account still runs and the `fable` alias. To get that picker, discovery talks to Anthropic directly with Claude Code's nonessential traffic and feature flags left on; error reports and self-updates stay off, and a value you export is kept. Discovery never starts a model turn. Behind `ANTHROPIC_BASE_URL` or another backend it keeps the generic list ([#57](https://github.com/yamatakeru/pi-claude-native/issues/57), [#84](https://github.com/yamatakeru/pi-claude-native/pull/84)).
20
+ - The `pi-claude-native/meta` diagnostic keeps the upstream HTTP status as `upstreamStatus`, so a saved session shows whether a failed turn was, for example, a 429 or a 400 ([#64](https://github.com/yamatakeru/pi-claude-native/issues/64), [#83](https://github.com/yamatakeru/pi-claude-native/pull/83)).
21
+
22
+ ### Fixed
23
+
24
+ - Changing the thinking level during a session no longer makes the next request miss the whole prompt cache, on models where Claude Code states the effort per turn. Each earlier turn's level is stated where Claude Code places it in its own sessions, and `nativeContext` reports `turnEffortsPlaced`. The design is recorded in [ADR 0002](docs/adr/0002-per-turn-effort-placement.md) ([#5](https://github.com/yamatakeru/pi-claude-native/issues/5), [#85](https://github.com/yamatakeru/pi-claude-native/pull/85)).
25
+ - On Windows, `PI_CLAUDE_NATIVE_ENTRYPOINT` is read under any letter case instead of being ignored ([#29](https://github.com/yamatakeru/pi-claude-native/issues/29), [#83](https://github.com/yamatakeru/pi-claude-native/pull/83)).
26
+
27
+ ## [0.8.0] - 2026-11-11
28
+
29
+ Checked with Pi 1.5.2 and Claude Code 2.1.322 (a fresh request, and a session written by another provider continued on Claude Native). Replay now accepts every history that Pi's own Anthropic provider can send. Switching models during a session, extensions that edit the history, and side requests such as an advisor no longer fail replay.
30
+
31
+ ### Changed
32
+
33
+ - Parts of a history that Claude cannot take natively are adjusted without losing content, and each adjustment is listed under `replayAdjustments` in the `pi-claude-native/meta` diagnostic. The rule is recorded in [ADR 0001](docs/adr/0001-replay-input-contract.md) ([#76](https://github.com/yamatakeru/pi-claude-native/issues/76), [#78](https://github.com/yamatakeru/pi-claude-native/pull/78)).
34
+
35
+ ### Fixed
36
+
37
+ - A request that declares fewer tools than the history used no longer fails with "Tool call outside the current inventory". This covers a tool-less side request, a session that started on another provider, and a removed tool on a model without mid-conversation tool changes. Those calls and their results are replayed as text ([#72](https://github.com/yamatakeru/pi-claude-native/issues/72), [#73](https://github.com/yamatakeru/pi-claude-native/pull/73)).
38
+ - An assistant turn with nothing left to send is omitted instead of failing with "Empty assistant message cannot be replayed". For example, an advisor called with no text before it leaves such a turn ([#74](https://github.com/yamatakeru/pi-claude-native/issues/74), [#78](https://github.com/yamatakeru/pi-claude-native/pull/78)).
39
+ - A tool call left without a result, as after a failed or interrupted turn, gets the result `No result provided` instead of making the request invalid. A result that arrives after a user message is kept as text ([#75](https://github.com/yamatakeru/pi-claude-native/issues/75), [#78](https://github.com/yamatakeru/pi-claude-native/pull/78)).
40
+ - Pi's docs section is replaced in every request, including sessions that started on another provider and side requests made while another model is active ([#28](https://github.com/yamatakeru/pi-claude-native/issues/28), [#77](https://github.com/yamatakeru/pi-claude-native/pull/77)).
41
+
42
+ ## [0.7.1] - 2026-11-10
43
+
44
+ ### Fixed
45
+
46
+ - On Fable 5.1, switching between a tool-result step and a new user turn no longer writes the whole conversation to the prompt cache again. On tool-result steps Claude Code sends a batching reminder after the date line, and the date was then left out of the start of the conversation. The same applies on any model where Anthropic turns that reminder on ([#41](https://github.com/yamatakeru/pi-claude-native/issues/41), [#70](https://github.com/yamatakeru/pi-claude-native/pull/70)).
47
+
48
+ ## [0.7.0] - 2026-11-06
49
+
50
+ Checked with Pi 1.5.0 and Claude Code 2.1.318 (startup, a fresh request, warm cache and replayed thinking). An `ANTHROPIC_API_KEY` set for Pi's own Anthropic provider no longer blocks Claude Native, and on models that support them, mid-conversation tool changes work again and keep the cache when a tool is redefined.
51
+
52
+ ### Changed
53
+
54
+ - API credentials in Pi's environment (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_FOUNDRY_API_KEY`) are left out of Claude Code instead of refusing every turn, so Claude Code always signs in with your subscription ([#55](https://github.com/yamatakeru/pi-claude-native/issues/55), [#65](https://github.com/yamatakeru/pi-claude-native/pull/65)). Backend overrides such as `ANTHROPIC_BASE_URL` are still refused.
55
+ - On models that support mid-conversation tool changes, a tool redefined under the same name no longer makes the next request miss the whole cache. Later tools are now sent by value, as Pi's own Anthropic provider does ([#56](https://github.com/yamatakeru/pi-claude-native/issues/56), [#66](https://github.com/yamatakeru/pi-claude-native/pull/66)).
56
+
57
+ ### Fixed
58
+
59
+ - Mid-conversation tool changes were turned off, with a warning on every catalog refresh, after Anthropic reworded the page that lists the supported models ([#51](https://github.com/yamatakeru/pi-claude-native/issues/51), [#59](https://github.com/yamatakeru/pi-claude-native/pull/59)).
60
+ - A reply cut off at the output limit inside a tool call no longer fails with a JSON error. The turn ends as `length` when the reply already has text or another call; otherwise the error names the tool that was cut off ([#52](https://github.com/yamatakeru/pi-claude-native/issues/52), [#60](https://github.com/yamatakeru/pi-claude-native/pull/60)).
61
+ - When a failed upstream request makes Claude Code end without a result, for example at a usage limit, the error shows Anthropic's reason instead of "Expected exactly one native result" ([#54](https://github.com/yamatakeru/pi-claude-native/issues/54), [#62](https://github.com/yamatakeru/pi-claude-native/pull/62)).
62
+
63
+ ## [0.6.0] - 2026-11-02
64
+
65
+ Verified with Pi 1.5.0 and Claude Code 2.1.316. Sessions saved by pi-claude-directsdk no longer replay their signed thinking.
66
+
67
+ ### Changed
68
+
69
+ - The prose that replaces Pi's `<docs>` section again says that the codemode guide covers classifiers and image models, and that Pi's docs are needed only for questions about Pi itself. Both were in Pi's section but had been dropped ([#47](https://github.com/yamatakeru/pi-claude-native/pull/47)).
70
+ - Pi shows this changelog on the first fresh launch after an update ([#48](https://github.com/yamatakeru/pi-claude-native/pull/48)).
71
+
72
+ ### Removed
73
+
74
+ - Signed thinking saved by pi-claude-directsdk (carrier type `pi-claude-directsdk/native`) is no longer restored, as announced in 0.4.0. Such sessions still continue: their earlier turns replay as the visible text and tool calls, without the thinking.
75
+
76
+ ## [0.5.0] - 2026-11-01
77
+
78
+ Replayed thinking now reaches the model, and the cache keeps the conversation after long thinking loops.
79
+
80
+ ### Changed
81
+
82
+ - Signed thinking from earlier turns is sent again ([#42](https://github.com/yamatakeru/pi-claude-native/issues/42), [#43](https://github.com/yamatakeru/pi-claude-native/pull/43)). Through v0.4.0 the extension restored it from the session but Claude Code dropped it before the request, so in a tool loop the model did not see its earlier reasoning.
83
+ - After a tool loop longer than the cache lookback, the next user turn reads the conversation before the loop from the cache instead of only the system prompt and tools ([#10](https://github.com/yamatakeru/pi-claude-native/issues/10), [#44](https://github.com/yamatakeru/pi-claude-native/pull/44)).
84
+
85
+ ### What to expect
86
+
87
+ Requests in tool loops now carry the earlier thinking, so their input grows by those tokens. After the first write they are mostly read from the cache. The net effect on subscription usage has not been measured.
88
+
89
+ ## [0.4.0] - 2026-11-01
90
+
91
+ First release under the name pi-claude-native. It continues pi-claude-directsdk v0.3.1 with the direct `claude -p` transport.
92
+
93
+ ### Moving from pi-claude-directsdk
94
+
95
+ - Remove the old package (`pi remove npm:pi-claude-directsdk` or `pi remove git:github.com/yamatakeru/pi-claude-directsdk`), then run `pi install git:github.com/yamatakeru/pi-claude-native`.
96
+ - The provider id is now `claude-native`. Select the model again in `/model` when you resume an older session.
97
+ - Environment variables are now `PI_CLAUDE_NATIVE_*`. The old `CLAUDE_DIRECTSDK_*` and `PI_DIRECTSDK_*` names are not read.
98
+ - Signed thinking saved by the old extension still replays for now. A later version removes this.
99
+
100
+ ### Changed since pi-claude-directsdk v0.3.1
101
+
102
+ - Cache reuse across calls ([#1](https://github.com/yamatakeru/pi-claude-native/issues/1), [#13](https://github.com/yamatakeru/pi-claude-native/pull/13)).
103
+ - Pi's thinking level passed to Claude Code as `--effort` ([#2](https://github.com/yamatakeru/pi-claude-native/issues/2)).
104
+ - Clearer relay failures ([#4](https://github.com/yamatakeru/pi-claude-native/issues/4), [#7](https://github.com/yamatakeru/pi-claude-native/issues/7)).
105
+ - Tool changes placed on the right assistant boundary ([#18](https://github.com/yamatakeru/pi-claude-native/issues/18)).
106
+ - Pi's docs guidance kept in the replacement `<docs>` section ([#30](https://github.com/yamatakeru/pi-claude-native/issues/30)).
107
+ - Quoted rejection text no longer fails a successful turn as a billing error ([#20](https://github.com/yamatakeru/pi-claude-native/issues/20), [#33](https://github.com/yamatakeru/pi-claude-native/pull/33)).
108
+ - Claude Code runs with native artifacts off, so its scratchpad line cannot reach requests under the custom client label ([#34](https://github.com/yamatakeru/pi-claude-native/issues/34), [#36](https://github.com/yamatakeru/pi-claude-native/pull/36)).
109
+ - `PI_CLAUDE_NATIVE_ALLOW_USAGE_CREDITS` allows charges only when set to `1`. Before, a typo or `disabled` turned the guard off ([#21](https://github.com/yamatakeru/pi-claude-native/issues/21), [#37](https://github.com/yamatakeru/pi-claude-native/pull/37)).
110
+ - The model catalog survives a failed or stalled fetch of the mid-conversation page. Only tool changes are withheld ([#25](https://github.com/yamatakeru/pi-claude-native/issues/25), [#39](https://github.com/yamatakeru/pi-claude-native/pull/39)).
111
+ - The docs match the code for the thinking-only truncation error and for failed-request cost reporting ([#26](https://github.com/yamatakeru/pi-claude-native/issues/26), [#38](https://github.com/yamatakeru/pi-claude-native/pull/38)).
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joao Marcos
4
+ Copyright (c) 2026 yamatakeru
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # Pi Claude Native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/yamatakeru/pi-claude-native/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/yamatakeru/pi-claude-native/actions/workflows/ci.yml?query=branch%3Amain)
4
+ [![License](https://img.shields.io/github/license/yamatakeru/pi-claude-native)](LICENSE)
5
+
6
+ Use your Claude subscription as a model in Pi. Requests go through the official Claude Code executable and its login, while Pi stays in charge of the conversation, the system prompt, tools, and approvals.
7
+
8
+ This is an independent project, not made or endorsed by Anthropic.
9
+
10
+ ## How it differs
11
+
12
+ - **Claude works as Pi's agent.** Pi's system prompt is the only instruction layer; Claude Code's own prompt, tools, settings, and memory are not added.
13
+ - **Pi's history is the only history.** Every call replays Pi's current transcript, signed thinking included, into a fresh `claude -p` process. There is no Claude-side session to drift from Pi's, so branching, editing, and compaction need no resynchronisation.
14
+ - **Pi runs every tool.** A response's tool calls, parallel ones included, reach Pi together after the response completes.
15
+ - **Plan limits are protected.** Requests carry the extension's own client label instead of an SDK one, which [anthropics/claude-code#97074](https://github.com/anthropics/claude-code/issues/97074) measured as drawing the 5-hour window faster. A turn charged to usage credits stops the session unless you opt in.
16
+ - **The prompt cache is reused** across calls despite the replay: 98.6% to 99.9% cache-read share on warm turns in the recorded [measurements](docs/transport-measurements.md). The lower values come from short sessions, where each new turn is a larger share of the prompt.
17
+
18
+ The costs: each call starts a new Claude Code process, which adds about half a second. The transport depends on Claude Code's internal `stream-json` and request formats, so only a qualified range of versions is supported. A local relay rewrites each request to keep the cache stable and admits one upstream request per call. Anthropic does not document this kind of use, and a policy or billing change could stop it working.
19
+
20
+ ### Alternatives
21
+
22
+ Other Pi extensions that use a Claude subscription through Claude Code:
23
+
24
+ - [pi-claude-directsdk](https://github.com/joaomj/pi-claude-directsdk) 1.0 and later: the official Agent SDK. Pi's prompt is appended to Claude Code's, and a live Claude session is kept per conversation and rebuilt when Pi's history diverges.
25
+ - [pi-claude-bridge](https://github.com/elidickinson/pi-claude-bridge): the official Agent SDK, with a Claude session that mirrors Pi's history.
26
+ - [pi-claude-code](https://github.com/earendil-works/pi-claude-code): `claude -p` behind a relay, like this extension, reusing a warm process for text-only turns.
27
+
28
+ ## Requirements
29
+
30
+ - Pi. Verified with Pi 1.5.2.
31
+ - Node 20 or later for Node-based Pi installations and local development. Compiled Pi uses its bundled runtime.
32
+ - Claude Code in the qualified range `>=2.1.263 <2.2.0` (see `QUALIFIED_CLI_RANGE` in `src/models.ts`). Verified with 2.1.322.
33
+ - A Claude paid plan. The free plan excludes the CLI.
34
+ - A Claude Code login via `claude auth login`.
35
+
36
+ ## Install
37
+
38
+ Install the verified Claude Code version, then sign in through the official CLI:
39
+
40
+ ```sh
41
+ npm install -g --ignore-scripts=false @anthropic-ai/claude-code@2.1.322
42
+ claude auth login
43
+ ```
44
+
45
+ Install the extension from npm. It follows the latest release:
46
+
47
+ ```sh
48
+ pi install npm:pi-claude-native
49
+ ```
50
+
51
+ To stay on a release instead, add its version, for example `npm:pi-claude-native@0.9.1`; `pi update` does not move a pinned version. Releases are listed on [GitHub](https://github.com/yamatakeru/pi-claude-native/releases).
52
+
53
+ To follow `main` between releases, install from Git instead with `pi install git:github.com/yamatakeru/pi-claude-native`, or add a tag such as `@v0.9.1` to stay on a release. Install from one source only: Pi identifies an npm package by its name and a Git package by its URL, so both would load.
54
+
55
+ Start or restart interactive Pi, open `/model`, and wait for the catalog refresh. On a fresh install, this step must complete before selecting the model from the command line; later invocations can use the saved catalog, for example `pi --model claude-native/haiku`. `pi update --models` does not refresh this catalog.
56
+
57
+ The extension uses the CLI login, not the Pi Anthropic login. The `npm install` flag enables the Claude Code installer for that command only.
58
+
59
+ The model catalog does not guarantee account entitlement. Start with `haiku` for a small request. Models that require usage credits can incur separate charges, and the extension cannot undo a charge that Claude Code already reported. See [usage credits](docs/usage.md#usage-credit-guard).
60
+
61
+ ## Upgrade
62
+
63
+ ```sh
64
+ pi update npm:pi-claude-native
65
+ ```
66
+
67
+ For a Git installation, use `pi update git:github.com/yamatakeru/pi-claude-native`. A bare `pi update` updates Pi itself, not its packages; `pi update --extensions` updates every installed package. Restart Pi afterwards, then open `/model` and wait for catalog refresh.
68
+
69
+ ### Moving from a Git installation
70
+
71
+ Releases up to 0.9.0 were installed from Git. To move to npm, remove the Git package, then install from npm:
72
+
73
+ ```sh
74
+ pi remove git:github.com/yamatakeru/pi-claude-native
75
+ pi install npm:pi-claude-native
76
+ ```
77
+
78
+ For a project installation (`pi install -l`), add `-l` to both commands. The provider id is unchanged, so saved sessions continue.
79
+
80
+ ### Moving from pi-claude-directsdk
81
+
82
+ Remove the old package (`pi remove npm:pi-claude-directsdk` or `pi remove git:github.com/yamatakeru/pi-claude-directsdk`), then install this one. The provider id and the environment variable names changed; see [0.4.0 in the changelog](CHANGELOG.md#040---2026-11-01). Since [0.6.0](CHANGELOG.md#060---2026-11-02), sessions saved by the old extension continue without their signed thinking; earlier turns replay as visible text and tool calls.
83
+
84
+ ## Known issues
85
+
86
+ - Plan billing depends on two undocumented Anthropic checks: the custom client label must stay out of the SDK bucket, and the replacement `<docs>` section must not look like Pi's prompt ([#23](https://github.com/yamatakeru/pi-claude-native/issues/23), joaomj/pi-claude-directsdk [#27](https://github.com/joaomj/pi-claude-directsdk/issues/27)). If rejections return, the extension can go back to the `sdk-cli` label; if [anthropics/claude-code#97074](https://github.com/anthropics/claude-code/issues/97074) is fixed, the label and the prompt change can both go.
87
+ - Changing the thinking level mid-session keeps the cache only where Claude Code states the effort per turn for the model. Elsewhere the next request misses the whole cache once ([Per-turn effort](docs/architecture.md#per-turn-effort-srcturn-effortts)).
88
+ - After a long tool loop with thinking, the next user turn may miss part of the cache; under investigation ([#10](https://github.com/yamatakeru/pi-claude-native/issues/10)).
89
+ - A tool change still causes a full cache miss on models that Anthropic's documentation does not list for mid-conversation tool changes, such as Haiku 4.5 and Sonnet 5 ([Tool changes](docs/troubleshooting.md#tool-changes)). Anthropic does not support the blocks that keep the cache on those models.
90
+
91
+ ## Documentation
92
+
93
+ - [Usage and configuration](docs/usage.md): local checkout, environment settings, costs, diagnostics, and verification.
94
+ - [Architecture](docs/architecture.md): request lifecycle, model routing, tool transport, and admission relay.
95
+ - [Troubleshooting](docs/troubleshooting.md): missing CLI, login, version, and conflicting environment settings.
96
+ - [Testing](docs/testing.md), [startup](docs/startup.md), and [transport measurements](docs/transport-measurements.md): verified environments, offline and paid checks, and cache measurements.
97
+
98
+ ## Issues and security
99
+
100
+ Report bugs and suggestions in [this repository's issues](https://github.com/yamatakeru/pi-claude-native/issues). See [CONTRIBUTING.md](CONTRIBUTING.md).
101
+
102
+ Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md); please do not post vulnerabilities or credentials in public issues.
103
+
104
+ ## History
105
+
106
+ This is a Pi port of [Hermes Claude Subscription DirectSDK](https://github.com/NousResearch/hermes-official-plugins/tree/main/claude-subscription-directsdk). It began as a fork of [joaomj/pi-claude-directsdk](https://github.com/joaomj/pi-claude-directsdk), where fixes were tried in everyday use and reported upstream. When that project moved to the official Agent SDK in v1.0.0, this one continued from v0.3.1 with the direct transport and was renamed to avoid confusion with the npm package `pi-claude-directsdk`.
107
+
108
+ v0.4.0 is the first release under this name. Changes since v0.3.1 include cache reuse across calls ([#1](https://github.com/yamatakeru/pi-claude-native/issues/1), [#13](https://github.com/yamatakeru/pi-claude-native/pull/13)), Pi's thinking level passed as `--effort` ([#2](https://github.com/yamatakeru/pi-claude-native/issues/2)), clearer relay failures ([#4](https://github.com/yamatakeru/pi-claude-native/issues/4), [#7](https://github.com/yamatakeru/pi-claude-native/issues/7)), tool changes placed on the right assistant boundary ([#18](https://github.com/yamatakeru/pi-claude-native/issues/18)), Pi's docs guidance kept in the replacement `<docs>` section ([#30](https://github.com/yamatakeru/pi-claude-native/issues/30)), quoted rejection text no longer failing as a billing error ([#20](https://github.com/yamatakeru/pi-claude-native/issues/20)), and a stricter usage-credit opt-in ([#21](https://github.com/yamatakeru/pi-claude-native/issues/21)). Later changes are in [CHANGELOG.md](CHANGELOG.md) and the [release notes](https://github.com/yamatakeru/pi-claude-native/releases).
109
+
110
+ ## Acknowledgments and license
111
+
112
+ Based on [Pi Claude DirectSDK](https://github.com/joaomj/pi-claude-directsdk), Copyright (c) 2026 Joao Marcos, under the MIT license. Thanks to Joao Marcos for the original implementation.
113
+
114
+ Behavioral reference and source of ported cache-stabilization logic: [Hermes Claude Subscription DirectSDK](https://github.com/NousResearch/hermes-official-plugins/tree/main/claude-subscription-directsdk), Copyright (c) 2026 Nous Research, under the [MIT license](https://github.com/NousResearch/hermes-official-plugins/blob/main/LICENSE).
115
+
116
+ MIT. See `LICENSE`.
@@ -0,0 +1,29 @@
1
+ # Replay accepts any history pi-ai's Anthropic provider can send
2
+
3
+ Replay accepts every Pi history that pi-ai's built-in Anthropic provider turns into a valid request. The reference is the pi-ai version this repository verifies against (1.5.0, the `devDependencies` pin). Parts the native form cannot represent are adjusted so that no content is lost, and each adjustment is recorded in the `pi-claude-native/meta` diagnostic (`replayAdjustments`). Replay rejects a history only when the result would describe an event that did not happen, could not pair a tool call with its result, or would change what the request asks for. A carrier keeps a replay faithful and cacheable; replay does not depend on it.
4
+
5
+ This replaces "reject unsupported history instead of flattening it into prose". That rule came from the upstream fork's original PRD (`35d50a3`, removed in `ed14966`), which copied the behavior contract of the Hermes DirectSDK plugin and gave no reason. The rule assumed that Claude Native had written the history itself. In Pi, other writers edit the history too: other providers after a model switch, extensions through the `context` event and side requests, compaction, and tool changes during a session. Every failure the rule caused was for a history that pi-ai's providers already send (#72, #74). Hermes itself has since relaxed the rule (`directsdk.py`, checked 2026-10-10). It replaces remote image URLs with a text hint ("A hint keeps the turn alive where an error would fail every retry of the same history") and accepts host-edited history.
6
+
7
+ ## Considered Options
8
+
9
+ These options were considered for historical tool calls the request does not declare (#72):
10
+
11
+ - **Reject the history** (the original rule). This fails side requests and provider switches, even though the content is still meaningful.
12
+ - **Send the call as a native `tool_use` without declaring the tool** (Hermes #39). Whether upstream accepts this when other tools are declared is unverified, and an error would come from upstream as an opaque 400.
13
+ - **Declare stub tools for the missing names.** This makes the tools callable in a request that is meant to have none.
14
+ - **Replay the call and its result as labeled text** (chosen). No content is lost, and the request stays valid whatever tools it declares.
15
+
16
+ ## Consequences
17
+
18
+ - Adjustments follow pi-ai where pi-ai has a rule:
19
+ - A tool call without a result gets the result `No result provided`, marked as an error, before the next user message, the next assistant turn, or the end of the history. A call replayed as text gets it as text.
20
+ - Images from an error result move after the turn's tool results.
21
+ - An assistant turn with nothing left to send is omitted.
22
+ - Replay keeps more than pi-ai in one place. pi-ai drops failed and aborted assistant turns; replay keeps whatever content they have and closes their calls as above, so the content is not lost.
23
+ - Where pi-ai would send a request upstream rejects, replay goes further and keeps the content as labeled text: a tool result without an open call, for example one that arrives after a user message (#75). These rejections by upstream are inferred from pi-ai's own workarounds, not observed.
24
+ - Adding a missing tool result and remapping a tool-call ID do not count as describing an event that did not happen. The first only states that a result is missing; the second only changes an identifier.
25
+ - Replay still rejects these histories:
26
+ - one that ends in an assistant turn without tool calls (prefill);
27
+ - a tool call with an empty ID;
28
+ - content types outside Pi's message types.
29
+ - The tests in `tests/replay-contract.test.ts` check each case against this contract.
@@ -0,0 +1,43 @@
1
+ # Per-turn effort follows Claude Code's own placement
2
+
3
+ When Pi's thinking level changes mid-session, the relay states each replayed turn's effort where Claude Code itself would state it in one of its own sessions. It does not adopt pi-ai's managed-effort design.
4
+
5
+ Claude Code 2.1.322 sends the effort inside the conversation when per-turn effort is active for a model (beta `per-turn-control-2026-07-01`). Its own description of the field says:
6
+
7
+ > true: an effort change keeps the prompt cache, because the effort is sent inside the conversation, at the turn where it changes.
8
+
9
+ Its builder (`Xat` in the 2.1.322 bundle) reads the effort each recorded assistant turn ran at. The first statement rides on the environment message after the first user message. Each later change is a system message, `{"role": "system", "content": [], "output_config": {"effort": …}}`, placed after the user message and before the reply it governs. It is merged into a system message already at that position. The request-level `output_config.effort` is the last stated level.
10
+
11
+ Replayed history reaches a fresh child through stream-json, which drops any per-turn level on an assistant frame. This was checked on 2.1.302 and 2.1.322, with the level at the frame's top level and inside `message`. The CLI therefore states only the current level, on the environment message. A level change rewrites that message and missed the whole cache in both recorded cases (#5).
12
+
13
+ ## Decision
14
+
15
+ - **Levels.** Each replayed assistant frame gets the effort it ran at:
16
+ - the `providerThinkingLevel` this provider recorded, which is now set on every response to the effort sent as `--effort`, when the current model has that level. A level from another model that the current one lacks, such as `max`, is never restated.
17
+ - otherwise Pi's `thinkingLevel`, mapped through the current model's `thinkingLevelMap`.
18
+ - A turn with neither takes the next known level after it, as the CLI's own builder does. A session with one turn from another provider therefore keeps working, and the result is the same in every later request.
19
+ - **Placement.** The relay places the statements as the builder above would, after the native-context steps and the tool-change system messages.
20
+ - **When the relay changes nothing.** The request is forwarded as built when:
21
+ - the CLI did not put exactly one statement, at the current level, on the system message where the first statement goes;
22
+ - the assistant count differs from the replay;
23
+ - every turn already ran at the current level.
24
+ - **Reporting.** The step reports `turnEffortsPlaced` or a skip reason in `nativeContext`, and never fails a call.
25
+
26
+ ## Considered options
27
+
28
+ - **pi-ai's managed-effort design** (`supportsMidConvoEffort` in pi-ai 1.5.0). It puts a statement before every historical assistant message and one at the end. It fixes the request-level effort at `low`, adds the `mid-conversation-output-config-2026-07-01` and `thinking-binding-controls-2026-08-01` betas, and sets `thinking.block_binding.prefix_mismatch_behavior: "drop_block"`. Rejected.
29
+ - **Why it fits pi-ai.** pi-ai calls the API directly and owns the whole request, so choosing the shape itself is natural there, and `drop_block` is a reasonable guard for a direct client.
30
+ - **Why it does not fit here.** Claude Native sends what Claude Code builds. The subscription route is built for Claude Code's requests, and Anthropic keeps the shapes the CLI sends working as the CLI changes. Each part of pi-ai's design is something the CLI does not send: the betas, the thinking setting, and the fixed request-level effort. Nothing would show if one of them stopped working after a CLI update.
31
+ - **What is kept.** The goal and the API mechanism are the same as pi-ai's. Pi's recorded level of each turn is stated inside the conversation, so a change keeps the cache.
32
+ - **Labeling replayed frames with their level.** The CLI drops the label when it reads stream-json, so this has no effect.
33
+ - **Leaving the miss.** #5 was once closed because level changes were rare. The recorded sessions were biased: the maintainer avoided changes because of this miss.
34
+
35
+ ## Consequences
36
+
37
+ - `drop_block` is not sent. Replayed signed thinking now runs behind the statements it was generated with, so a prefix mismatch is less likely than before. If mismatch errors appear, adding it is a separate decision.
38
+ - **The request-level effort follows the current level, as in the CLI's own requests.** Paid measurements on 2026-11-12 showed that this keeps the cache. Each ran on Haiku 5.5, one session, at levels high → high → medium. In both, the third request read everything the second request had read and written.
39
+ - Without thinking: 15,137 tokens read and 12 written.
40
+ - With signed thinking in every reply, replayed into the third request: 15,233 tokens read and 402 written. The third request was accepted.
41
+ - **One intended difference from the CLI's builder.** The CLI delays a change that arrives while the last user message holds only tool results, until the next user message. Here the statement follows the last user message, so the level Pi asked for, which is also the `--effort` value, governs the reply.
42
+ - **Adjacent assistant turns share the earlier statement.** Two assistant frames with no user message between them get no statement between them, as in the CLI's builder: the second turn's historical level is stated by the first's. This happens, for example, when replay omits an empty user message. The reply being generated always has its own statement after the last user message.
43
+ - **Revisit triggers.** Watch `nativeContext` skip reasons after a CLI update. A model the CLI does not give per-turn effort keeps today's behavior.
@@ -0,0 +1,182 @@
1
+ # Architecture
2
+
3
+ One Pi model call spawns one request-scoped `claude` process and admits at most one upstream Messages request.
4
+
5
+ ```mermaid
6
+ flowchart TD
7
+ A["Pi calls streamSimple(model, transcript)"] --> B["Provider reads system prompt + tools from transcript messages"]
8
+ B --> C["Provider writes private files: system.md, settings.json (CLAUDE_CODE_EXTRA_BODY), tools.json"]
9
+ C --> D["Provider starts admission relay on 127.0.0.1 with per-request token route"]
10
+ D --> E["Provider spawns one request-scoped claude process (ANTHROPIC_BASE_URL points at relay, child env only)"]
11
+ E --> F["Provider replays history on child stdin (shouldQuery: false, final frame queries)"]
12
+ F --> G["CLI sends POST /v1/messages through relay"]
13
+ G -->|first request| H["Relay forwards to api.anthropic.com, captures SSE"]
14
+ G -->|later requests| I["Relay denies locally, no upstream contact"]
15
+ E --> J["Provider converts child stdout stream-json to Pi events"]
16
+ H --> K["Provider publishes tool calls only after upstream completes and child exits"]
17
+ K --> L["Pi executes tools (inert MCP advertises them, never executes)"]
18
+ L --> M["done with exact usage, or error"]
19
+ ```
20
+
21
+ ## Components
22
+
23
+ ### Provider entry (`src/provider.ts`, `extensions/claude-native/index.ts`)
24
+
25
+ The extension registers a complete Pi provider with id `claude-native` and an initially empty catalog. Cache-only refresh restores verified snapshots from Pi’s model store without CLI probes or network work. Live refresh runs after editor startup and reports failures instead of returning pinned models.
26
+
27
+ `streamSimple` uses `src/lazy-stream.ts` to return a stream immediately. The wrapper loads `src/stream.ts` asynchronously and preserves the model, transcript, and request options. See [startup.md](startup.md).
28
+
29
+ ### History replay (`src/replay.ts`)
30
+
31
+ Pi owns the transcript. Each call replays prior assistant, user, and tool-result frames in native `stream-json` form through a fresh child.
32
+
33
+ Replay follows the input contract in [ADR 0001](adr/0001-replay-input-contract.md): it accepts every history that pi-ai's Anthropic provider can send. Parts the native form cannot represent are adjusted without losing content, and each adjustment is listed in the meta diagnostic (`replayAdjustments`).
34
+
35
+ Rules:
36
+
37
+ - Historical user frames carry `shouldQuery: false` and expect a zero-turn acknowledgment. Only the final frame may generate.
38
+ - An assistant turn with nothing left to send is omitted (`turnOmitted`). Such turns come from failed and aborted responses, and from edits that leave only empty text or thinking. Whitespace-only text is never sent.
39
+ - A tool call without a result gets the result `No result provided`, marked as an error (`toolResultAdded`), or as text for a call replayed as text. Failed and aborted turns keep their content, unlike in pi-ai, which drops them. It is added after the call's turn: before the next user message, before the next assistant turn, or at the end of the history.
40
+ - Tool-call IDs that Claude cannot accept are mapped to valid native IDs. Tool results use the same mapping. Stored Pi IDs remain unchanged.
41
+ - Thinking without a matching native carrier is replayed as ordinary assistant text. This allows switching providers during a session.
42
+ - A historical call to a tool outside the declared tools, in a turn without a matching carrier, is replayed as assistant text (`[Tool call: <name>, id <id>] <arguments>`; `toolCallAsText`), and its result as user text (`[Tool result: <name>, id <id>]` or `[Tool error: …]`). Calls to declared tools stay native.
43
+ - A tool result without an open call, for example one that arrives after a user message, is replayed as user text in the same form (`toolResultAsText`).
44
+ - In each user frame, native tool results come first, then added results, then content replayed as text, then user content, in history order. Images from an error tool result follow the turn's tool results, and a placeholder takes their place in the result (`errorImagesMoved`). An image without data becomes the text `[Image omitted: no data]` (`imageOmitted`).
45
+ - The history must not end in an assistant turn without tool calls: assistant prefill is unsupported. Replay also rejects tool calls with empty IDs and content types outside Pi's message types.
46
+ - Signed native thinking survives only inside a durable carrier (`pi-claude-native/native`) attached to the Pi assistant message. The carrier is restored only when the visible projection (text, thinking, tool calls) still matches. Any edit drops the native blocks. Session files persist the carrier, so resumed turns replay exactly. A restored frame keeps the carrier's `model`: the CLI drops thinking from an assistant message without one, as written by another model (see [transport-measurements.md](transport-measurements.md#replayed-thinking-and-the-assistant-model)).
47
+ - Models with verified mid-conversation tool-change support keep later system messages. The initial system message supplies the system prompt and initial tools. Later tool additions and removals are injected as native system content at their assistant boundaries, and historical calls to any tool declared earlier in the transcript remain replayable. Response validation still uses the active tools.
48
+
49
+ ### Request assembly (`src/request-plan.ts`, `src/request.ts`)
50
+
51
+ `planRequest` decides what one call sends without touching files, processes, or the network: the system prompt, the request body, the native transcript plan, and the replay frames. `src/request.ts` builds the parts.
52
+
53
+ Before the request is built, every `docs` section in the transcript's system messages is replaced with Claude Native's prose version (`src/docs-section.ts`, see [Plan usage metering](usage.md#plan-usage-metering)), whichever provider was active when Pi recorded it. A later update that the replacement leaves unchanged is dropped, so switching providers adds no mid-conversation section update.
54
+
55
+ The system prompt and tool schemas travel through per-request private files with owner-only permissions. Later system messages in the transcript travel through the relay rewrite when the model supports mid-conversation tool changes:
56
+
57
+ - `system.md`: the system prompt.
58
+ - `settings.json`: carries `CLAUDE_CODE_EXTRA_BODY`.
59
+ - `tools.json`: the inert MCP manifest.
60
+ - `inert-mcp.mjs`: the inert MCP server source.
61
+
62
+ Files avoid OS argument and environment-string limits. Authentication and identity headers are never replaced. The relay preserves them.
63
+
64
+ When the model supports mid-conversation tool changes, the request body carries the initial tools and a deferred placeholder, and this list never changes. Later tools are defined by value in `tool_addition` blocks, including a tool redefined under the same name, and withdrawn by `tool_removal`. The relay adds the `inline-tools-2026-09-15` beta value to the admitted request and injects these native system messages before their assistant boundaries; a system message with no assistant message after it goes at the end of the request. The collapsed system message and current tool set are used instead when the model is unsupported, when the leading system message declares no tools (Anthropic rejects a tool list where every tool is deferred), or when the request sets `toolChoice: "none"`.
65
+
66
+ The child argv (`buildArgv`) runs the CLI with `--input-format stream-json`, `--output-format stream-json`, `--verbose`, `--include-partial-messages`, `--max-turns 1`, `--permission-mode dontAsk`, `--no-session-persistence`, `--strict-mcp-config`, `--disable-slash-commands`, and an empty `--setting-sources`. When the request body carries an effort level, the argv also passes it as `--effort`. `--max-turns 1` is accepted by the qualified CLI even though recent `--help` output hides it. The installed-Pi harness verifies the request path with the user’s real CLI.
67
+
68
+ The child environment (`buildChildEnv`) points `ANTHROPIC_BASE_URL` at the relay. It sets native isolation flags (`ENABLE_TOOL_SEARCH=false`, `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, `CLAUDE_CODE_MAX_RETRIES=0`, `DISABLE_AUTO_COMPACT=1`, `DISABLE_COMPACT=1`, `CLAUDE_CODE_TOTAL_TOKENS_REMINDER=off`, `CLAUDE_CODE_DISABLE_ARTIFACT=1`). It removes an inherited `CLAUDE_CODE_EXTRA_BODY` and `CLAUDE_CODE_EFFORT_LEVEL`; on Windows the match ignores case, because the child reads environment names case-insensitively. The `PI_CLAUDE_NATIVE_ENTRYPOINT` override is read the same way. Sampling controls from Pi options (`temperature`, `top_p`) are dropped because subscription routes reject them.
69
+
70
+ ### Admission relay (`src/admission.ts`)
71
+
72
+ The relay binds an ephemeral loopback port with a random per-request route (`/admit/<token>`). It forwards only the first upstream Messages request and rejects later native recovery or retry attempts locally with `PI_MODEL_ADMISSION_CONSUMED`. The relay changes HTTP transfer encoding. Request identity stays native. The transport may rewrite the request body for replay-stable native context and mid-conversation system messages, both described below, and add beta values to `anthropic-beta`; the payload otherwise stays native.
73
+
74
+ ### Replay-stable native context (`src/native-context.ts`)
75
+
76
+ Every Pi model call replays history through a fresh child, and the CLI adds per-request context: its session-context reminder on the final user or tool-result frame, and today's date split out of its environment message into a trailing system message. Its message cache breakpoint lands on that context. None of it recurs in the next request, so the prefix cached there would never be read.
77
+
78
+ Before forwarding, the relay's payload hook (`src/native-rewrite.ts`) parses the admitted body once, applies `stabilizeNativeRequest` and then, on supported models, the tool-change system messages, and serializes once. It restores one invariant: everything in request N up to its message breakpoint appears unchanged at the start of request N+1. Pi-owned content is identified structurally, as the final frame `prepareHistory` built. The CLI's wording is never matched, except for the exact date line.
79
+
80
+ - Tool results: text the CLI appended to the queried tool results is removed, only when the ordered tool-use IDs anchor the whole frame. The CLI also trims surrounding whitespace from the newest tool result before appending, so Pi's original text is restored there too.
81
+ - Later user turns: plain text the CLI placed before the queried frame is dropped, only when the frame occurs exactly once. A continuous native session carries this context once; per-call replay would otherwise repeat it on every call and replay it nowhere.
82
+ - Opening request: that text is moved after the frame instead, so the opening turn is cached once.
83
+ - The trailing date line returns to the end of the environment message, where the opening request had it. On Fable the date leads a trailing message that also holds a per-step reminder on tool-result steps; only the date moves, and the reminder stays trailing.
84
+ - Message breakpoints after the replay-stable span move onto its last cacheable block.
85
+ - After a tool loop longer than the cache lookback (20 blocks), the block before the open turn's first thinking keeps a marker of its own. Once a later user message carries more than tool results, the API stops matching cache entries written after that thinking, so without this marker the next turn would read only system and tools. The marker is added only within the four-breakpoint limit; at the limit a CLI marker between it and the recurring breakpoint moves there instead.
86
+
87
+ Pi content, assistant blocks (including signed thinking), system blocks, and tools are never edited. Markers only move earlier, except the turn-start marker above, which adds at most one. A request that does not match exactly is forwarded as the CLI built it; this step never fails a call. The `nativeContext` field of the meta diagnostic lists the applied steps and skip reasons, never content. These are CLI-version-sensitive behaviours; see [transport-measurements.md](transport-measurements.md#replay-stable-native-context).
88
+
89
+ The relay reconstructs the upstream assistant message from the SSE stream (`SseCapture`) and records status, request id, the first relay failure, and an error body (capped at 64 KiB).
90
+
91
+ The failure record (`relayFailure` in the `pi-claude-native/meta` diagnostic, also summarized in the error text) holds the error name, the Node `code`, `syscall`, address, and port. For an `AggregateError`, such as a failed connection to every resolved address, it also holds each inner error's code and address (at most eight). It also records the phase: `request-body`, `connect` (no request bytes reached upstream), `request`, or `response`. It never contains request or response content. Messages are kept only for the relay's own fixed errors. Upstream targets must be HTTPS, except loopback HTTP endpoints. Request bodies are capped at 512 MiB.
92
+
93
+ ### Tool-change injection boundaries
94
+
95
+ The relay inserts the mid-conversation system messages (`src/transcript.ts`) after the native-context steps, in the same hook. Insertion points count the assistant frames the CLI sends. A carried assistant frame that holds only empty text is not counted, because the CLI drops it. The plan also records the expected number of assistant frames; a request with a different number throws, and the relay answers 500 before contacting upstream rather than inserting at a shifted boundary.
96
+
97
+ ### Per-turn effort (`src/turn-effort.ts`)
98
+
99
+ Within one session, Claude Code states the effort inside the conversation, at each turn where it changes, so a level change keeps the cached prefix. Replayed history reaches a fresh child without those levels, so the CLI states only the current level, on its environment message. The relay restores the CLI's own placement last in the same hook, after the native-context steps and the tool-change system messages, from the effort each replayed turn ran at: the `providerThinkingLevel` this provider recorded, or Pi's `thinkingLevel` mapped for the model. A request the CLI did not build that way is forwarded as built. The `nativeContext` field reports `turnEffortsPlaced` or the skip reason; this step never fails a call. [ADR 0002](adr/0002-per-turn-effort-placement.md) records the placement, the skip conditions, one intended difference from the CLI, and why this follows Claude Code instead of pi-ai's managed-effort design.
100
+
101
+ ### Child supervision (`src/process.ts`)
102
+
103
+ Each Pi model call owns one child in its own process group. Abort and failure kill the full tree. This matters on Windows, where the npm `claude` shim is `cmd.exe` -> `node` and a plain `kill()` would orphan the node child that holds the request open. Stdout is consumed as newline-delimited JSON with an idle deadline that resets on every received line. Each stdin write and child-closure wait also has a deadline, using the request's `timeoutMs`. These are phase limits, not one total runtime limit. Timeout kills the process tree and preserves the first failure. Cancellation rejects pending reads and writes; exit observation remains bounded. Expired read waiters are removed by identity, and every completed operation clears its timer.
104
+
105
+ The caller receives the child final exit status after its output streams close. Executable tool calls still wait for that exit and the complete upstream response. Discovery cleanup attempts every resource release even if observing child closure fails.
106
+
107
+ ### Native working directory (`src/child-cwd.ts`)
108
+
109
+ Native model-request children share an empty, persistent directory. Its path remains stable across Pi processes, avoiding a changing native working-directory prefix after restart. Request files, credentials, and transcripts are not stored there.
110
+
111
+ - Linux: `${XDG_CACHE_HOME:-~/.cache}/pi-claude-native/child`.
112
+ - macOS: `~/Library/Caches/pi-claude-native/child`.
113
+ - Windows: `%LOCALAPPDATA%/pi-claude-native/child`, with the user's local app-data directory as the fallback.
114
+
115
+ On POSIX, the provider verifies that its cache root and child directory belong to the current effective user and have mode `0700`. It rejects symlinks and nonempty child directories instead of reading unexpected project instructions or deleting files. Windows uses the access controls inherited from the user's cache parent. Relative cache-directory settings fail with the setting name and a remedy.
116
+
117
+ The first request after this directory change can miss a previously cached native prefix. A stable path removes this source of cache invalidation; legitimate Pi prompt and tool changes still affect reuse. Linux verification resumed the same session in a new Pi process and measured 99.84% cache-read share with stable Pi prompt and tool hashes. See [transport-measurements.md](transport-measurements.md#linux-lifecycle-and-cross-restart-verification).
118
+
119
+ ### Stream conversion (`src/stream.ts`, `src/native-output.ts`, `src/completion.ts`)
120
+
121
+ The provider converts child stdout `stream-json` to Pi events. `src/stream.ts` runs the call from setup to cleanup. `src/native-output.ts` publishes text and thinking as the child prints them. `src/completion.ts` is the completion gate: it chooses the response, from the relay capture or the child's stdout, maps failures to typed errors, and then publishes the rest of the message and its tool calls. Native result errors are preserved even when stderr is empty. Prompt-length errors report the selected route and configured context window. Native tool calls are published only after the first upstream response is complete and the child has exited. A tool call whose streamed arguments are not valid JSON when the response stops at `max_tokens` is dropped, never run with guessed arguments. The turn ends as `length` when the reply already has text or another call; otherwise the error names the tool that was cut off. Anthropic does not validate streamed tool input, so a cut-off reply can end inside one. Usage and stop reason come from the authoritative native usage. Incomplete native or upstream responses produce `incomplete` or `upstream` errors.
122
+
123
+ The provider reads `rate_limit_event.rate_limit_info`. If the authoritative response reports usage-credit overage and `PI_CLAUDE_NATIVE_ALLOW_USAGE_CREDITS` is not enabled, the turn fails with a non-retryable `billing` error after usage is captured. Completed and failed turns carry a `pi-claude-native/rate-limit` diagnostic. Upstream usage-credit rejection text maps to the same error.
124
+
125
+ ### Inert MCP (`src/inert-mcp.ts`)
126
+
127
+ The MCP server name in the native child is `pi`. Tool names are `mcp__pi__<name>`. The server advertises Pi tools so the CLI can call them. It never executes them. Pi alone executes tools.
128
+
129
+ The helper uses `process.execPath`. Under Bun, its MCP configuration sets `BUN_BE_BUN=1` only for that helper, so a compiled Pi executable runs the JavaScript rather than starting another agent. Node launches remain unchanged.
130
+
131
+ ### Catalog discovery (`src/discovery.ts`, `src/web-catalog.ts`)
132
+
133
+ Discovery runs the Claude Code `initialize` handshake through an asynchronous supervised child. It never calls synchronous authentication or version probes.
134
+
135
+ Claude Code serves the account's own picker only first-party and only with its nonessential traffic and feature flags on. Behind another `ANTHROPIC_BASE_URL` it logs `[servedCatalog] off (not_first_party)`, and with either switch off it answers a generic list of each family's current model. The served picker also lists older models the account still runs and models gated per account. The handshake therefore talks to Anthropic directly, and its child does not set `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or `DISABLE_TELEMETRY`. A value the user exports is kept. The child sets `DISABLE_ERROR_REPORTING=1` and `DISABLE_AUTOUPDATER=1`. Only the first-party handshake differs: model calls, `auth status`, and request-time probes keep nonessential traffic off.
136
+
137
+ Discovery never starts a model turn. It writes only `initialize` and closes stdin, so there is no user turn, and an `assistant`, `stream_event`, or `result` record on stdout fails discovery. That check finds model turns, not every request Claude Code makes: on 2.1.322 the handshake's own traffic is its catalog, feature-flag, telemetry, and MCP-directory requests, and none of them is a Messages request.
138
+
139
+ When the environment points Claude Code at another backend (`ANTHROPIC_BASE_URL`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) or at the test upstream, the served picker is out of reach anyway. The handshake then keeps nonessential traffic off, drops the backend switches, which would take Claude Code to its own cloud client, and points `ANTHROPIC_BASE_URL` at a loopback guard. The guard denies all HTTP requests and detects attempted Messages requests.
140
+
141
+ Cleanup closes the guard's connections, then closes the server only if it is still listening. Bun can stop the server when closing connections; Node leaves it listening. Other cleanup failures remain errors.
142
+
143
+ Public Anthropic Markdown documents supply current model IDs, model pages, prices, effort levels, and cache lifetimes. Model pages must confirm the canonical Claude API ID, input modalities, context window, output limit, and thinking capability. The account picker supplies resolved aliases and effort constraints. Current public models can appear even before the CLI picker includes them; their labels state that account availability is unverified.
144
+
145
+ Missing metadata excludes only the affected model with a warning. Mid-conversation tool changes are optional: when the mid-conversation page cannot be read or no longer names its models, they stay off on every model, under a separate warning, and the catalog is kept. Shared-source failures reject refresh and remove the snapshot. Refresh uses a shared deadline and abort signal. Pi owns persistence and generation-checked publication.
146
+
147
+ ### Request-time probes (`src/setup.ts`)
148
+
149
+ CLI resolution and version qualification run only when a model call needs them. They are not part of catalog discovery or editor startup.
150
+
151
+ ## Guarantees
152
+
153
+ - The provider never executes native tools.
154
+ - The provider never manages login or reads credentials.
155
+ - The provider never falls back to another billing route.
156
+ - A turn that Claude Code reports as charged to usage credits stops later turns unless the user explicitly allows usage credits.
157
+ - Missing CLI and logged-out CLI fail with actionable errors. See [troubleshooting.md](troubleshooting.md).
158
+
159
+ ## Model routing
160
+
161
+ The catalog has no hand-written model inventory, aliases, limits, or price table. Its sources are:
162
+
163
+ - Claude Code’s account picker for resolved aliases and account-visible models.
164
+ - [Anthropic’s model overview](https://platform.claude.com/docs/en/about-claude/models/overview) and linked model pages for canonical routes and capabilities.
165
+ - [Anthropic’s pricing documentation](https://platform.claude.com/docs/en/about-claude/pricing) for token prices, request-wide tiers, and cache lifetimes.
166
+ - [Anthropic’s effort documentation](https://platform.claude.com/docs/en/build-with-claude/effort) for web-only model effort levels.
167
+ - [Anthropic’s mid-conversation documentation](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages) for the models that accept mid-conversation tool additions and removals.
168
+
169
+ No imported model table sets a capability. `supportsNativeToolChanges` reads the flag the catalog writes on the model; a model that Anthropic does not list, or a page the catalog cannot load within five seconds or parse, keeps the collapsed request shape and reports a warning instead of a guess.
170
+
171
+ Verified context windows control native `[1m]` selection. Claude Code owns thinking mode; the request body sends only an effort level that the catalog supports. The CLI also puts a per-turn effort statement on the environment message after the first user message (`per-turn-control` beta) at its own session level, and that statement overrides the request-level `output_config.effort`. The provider therefore passes the same level as `--effort` and drops an inherited `CLAUDE_CODE_EFFORT_LEVEL`, which the CLI would read ahead of the flag. Without a Pi reasoning level, neither is sent and the CLI default applies. For earlier turns' levels, see [Per-turn effort](#per-turn-effort-srcturn-effortts). Levels that map to the same effort, such as `minimal` and `low`, send the same request. Missing metadata is never replaced with a guessed limit or a zero price.
172
+
173
+ `QUALIFIED_CLI_RANGE` in `src/models.ts` still controls transport qualification. Run the installed-Pi harness with fresh and complete-session scenarios before widening it.
174
+
175
+ ## Tool transport
176
+
177
+ Pi owns tool execution. The native child advertises Pi tools through the inert MCP server and never executes them. The authority is `src/request.ts` (`buildRequestBody`, `normalizeInputSchema`). The stable rules:
178
+
179
+ - Only `tool_choice` `auto` and `none` are supported.
180
+ - Constrained decoding is not enforced. Grammar and best-effort (`strict: "prefer"`) annotations fall back to normal JSON-schema tool calling. Required strict (`strict: "require"`) stays rejected. Malformed native tool input still fails downstream.
181
+ - Native tool calls reach Pi only after the first upstream response is complete and the child has exited.
182
+ - On models with verified mid-conversation tool-change support, historical calls to tools declared earlier in the transcript are replayed natively, including tools no longer active. Pi never executes a tool outside the active tools.