samrito-pi-preset 1.0.0 → 1.0.2

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 (30) hide show
  1. package/{node_modules/pi-cliproxyapi-provider/LICENSE → LICENSE} +1 -1
  2. package/README.md +129 -10
  3. package/node_modules/@samrito/pi-cliproxyapi-provider/LICENSE +28 -0
  4. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/README.md +78 -5
  5. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/package.json +8 -5
  6. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/model-capabilities.ts +12 -1
  7. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/model-ui.ts +21 -1
  8. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/provider.ts +12 -3
  9. package/node_modules/@samrito/pi-cliproxyapi-provider/src/reasoning-levels.ts +59 -0
  10. package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/types.ts +22 -0
  11. package/package.json +16 -6
  12. package/scripts/pi-package-lib.mjs +41 -1
  13. package/scripts/verify-tarball.mjs +200 -0
  14. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/data/models-dev-fallback.json +0 -0
  15. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/extensions/index.ts +0 -0
  16. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/auth.ts +0 -0
  17. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/cache.ts +0 -0
  18. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/catalog.ts +0 -0
  19. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/codex-compat.ts +0 -0
  20. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/commands.ts +0 -0
  21. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/config.ts +0 -0
  22. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/cpa.ts +0 -0
  23. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/discovery.ts +0 -0
  24. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/matching.ts +0 -0
  25. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/model-api.ts +0 -0
  26. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/models-dev.ts +0 -0
  27. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/network.ts +0 -0
  28. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/registration.ts +0 -0
  29. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/runtime.ts +0 -0
  30. /package/node_modules/{pi-cliproxyapi-provider → @samrito/pi-cliproxyapi-provider}/src/settings.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Richard Hao
3
+ Copyright (c) 2026 xiangsam
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # samrito-pi-preset
2
2
 
3
3
  A portable [pi](https://pi.dev) package that bundles this machine's extension
4
- collection (including the `pi-cliproxyapi-provider` provider) under a single
4
+ collection (including the `@samrito/pi-cliproxyapi-provider` provider) under a single
5
5
  `pi install`-able package.
6
6
 
7
7
  Install it from npm in one command:
@@ -55,7 +55,7 @@ managed separately, or copy the directory to the target machine.
55
55
  | `@juicesharp/rpiv-todo` | ^2.9.0 | `index.ts` |
56
56
  | `@narumitw/pi-btw` | ^0.58.1 | `dist/index.ts` |
57
57
  | `pi-background-tasks` | ^2.5.0 | `extensions/*.ts` (2) |
58
- | `pi-cliproxyapi-provider` | ^0.15.30 | `extensions/index.ts` |
58
+ | `@samrito/pi-cliproxyapi-provider` | ^0.16.0 | `extensions/index.ts` |
59
59
  | `pi-goal-x` | ^0.31.2 | `extensions/goal.ts` |
60
60
  | `pi-tool-display` | ^0.5.0 | `index.ts` |
61
61
  | `pi-zentui` | ^0.23.0 | `extensions/zentui/index.ts` |
@@ -111,14 +111,11 @@ Because it does not work, and cannot ask:
111
111
  ## Publish and install from npm
112
112
 
113
113
  The package is a normal, publishable npm package (`private` is not set), and
114
- `bundledDependencies` makes npm pack every plugin into the tarball. Publish
115
- once, then install anywhere with `pi install npm:samrito-pi-preset`.
114
+ `bundledDependencies` makes npm pack every plugin into the tarball. Install it
115
+ anywhere with `pi install npm:samrito-pi-preset`.
116
116
 
117
- ```bash
118
- npm login # once per machine
119
- node scripts/verify.mjs # optional but recommended before publishing
120
- npm publish # runs prepack -> sync-manifest --check
121
- ```
117
+ Releases are driven by tags: pushing `vX.Y.Z` makes GitHub Actions publish that
118
+ exact version. See [Releasing](#releasing) for the one-time npm setup.
122
119
 
123
120
  On the target machine:
124
121
 
@@ -141,6 +138,108 @@ bundled `node_modules/` keeps every `pi.extensions` path resolvable.
141
138
  `prepack` runs `sync-manifest.mjs --check`, so a stale `pi.extensions` fails the
142
139
  publish instead of shipping.
143
140
 
141
+ ## CI
142
+
143
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs on every push to
144
+ `main` and every pull request. It never touches the registry.
145
+
146
+ | Job | Checks |
147
+ | --- | --- |
148
+ | `verify` (Node 22 + 24) | manifest integrity, pi resolution, real extension loading via `verify.mjs` |
149
+ | `tarball` | packs, installs, resolves and loads the **published artifact** via `verify-tarball.mjs` |
150
+
151
+ The `tarball` job exists because the checkout and the published package differ in
152
+ a way that matters: in the checkout npm hoists the plugins into a sibling
153
+ `node_modules/`, but in the tarball `bundledDependencies` must embed them
154
+ *inside* the package root, since `pi.extensions` paths are relative to it. A
155
+ drift between `dependencies` and `bundledDependencies` therefore breaks only the
156
+ published artifact, and `verify.mjs` cannot see it. Run it locally with:
157
+
158
+ ```bash
159
+ npm run verify # checks the checkout
160
+ npm run verify:tarball # packs, installs, and loads the real artifact
161
+ ```
162
+
163
+ Both jobs also guard a repo requirement: a vendor-scoped provider package that
164
+ was deliberately excluded from this bundle must never be referenced in tracked
165
+ files. The search term is assembled at runtime inside the workflow so the guard
166
+ cannot match its own source, and it deliberately does **not** scan the packed
167
+ tarball — the bundled upstream `models.dev` dataset contains vendor model ids
168
+ that are none of our business.
169
+
170
+ ## Releasing
171
+
172
+ [`.github/workflows/publish.yml`](.github/workflows/publish.yml) publishes on a
173
+ `vX.Y.Z` tag using npm **trusted publishing** (OIDC): no long-lived `NPM_TOKEN`
174
+ secret exists, and npm attaches a provenance attestation linking the tarball to
175
+ the repository and commit.
176
+
177
+ ```bash
178
+ npm version patch # or minor / major — bumps package.json and tags vX.Y.Z
179
+ git push --follow-tags # workflow runs, verifies, publishes
180
+ ```
181
+
182
+ The workflow refuses a tag that disagrees with `package.json`, runs
183
+ `scripts/verify.mjs` (loading every extension through pi's real startup path),
184
+ and treats re-tagging an already published version as a no-op rather than a
185
+ failure.
186
+
187
+ ### One-time setup
188
+
189
+ A trusted publisher can only be configured for a package that already exists, so
190
+ the first version was published manually. `samrito-pi-preset@1.0.0` exists, so
191
+ configure once at
192
+ [npmjs.com](https://www.npmjs.com/package/samrito-pi-preset/access):
193
+
194
+ | Field | Value |
195
+ | --- | --- |
196
+ | Publisher | GitHub Actions |
197
+ | Organization | `xiangsam` |
198
+ | Repository | `samrito-pi-preset` |
199
+ | Workflow name | `publish.yml` |
200
+ | Environment | *(leave empty)* |
201
+ | Allow direct publish | **enabled** |
202
+
203
+ `publish.yml` must match the workflow **filename** exactly — renaming the file
204
+ means updating this setting too. All fields are case-sensitive.
205
+
206
+ Trusted publishing requires **npm CLI ≥ 11.5.1 and Node ≥ 22.14.0**; the workflow
207
+ pins Node 24 and asserts the npm version before publishing. Provenance is
208
+ generated automatically for GitHub Actions, so `--provenance` is explicit rather
209
+ than required.
210
+
211
+ > **Trusted publisher connections cannot be edited.** npm fixes the provider and
212
+ > its fields once a connection is created, so changing *Allow direct publish*
213
+ > later means deleting the connection and adding a new one.
214
+
215
+ This package deliberately enables *direct publish* so a tag ships unattended. npm
216
+ recommends the opposite (stage only, then approve with 2FA); npm's own staged
217
+ publishing docs say to enable only `npm stage publish` and disable `npm publish`.
218
+ The trade-off, stated plainly: with direct publish enabled, anyone who can push a
219
+ tag or edit this workflow can publish to npm, and every consumer of the preset
220
+ pulls that version. To tighten it later:
221
+
222
+ 1. Delete the trusted publisher above and recreate it with **stage-only**
223
+ permissions (`npm trust github … --allow-stage-publish`, no `--allow-publish`).
224
+ 2. Change the workflow's last step to `npm stage publish --provenance --access public`.
225
+ 3. After each tag, finish the release locally:
226
+
227
+ ```bash
228
+ npm stage list samrito-pi-preset
229
+ npm stage approve <stage-id> # prompts for 2FA
230
+ npm stage reject <stage-id> # to back out instead
231
+ ```
232
+
233
+ ### Publishing by hand
234
+
235
+ Still possible if OIDC is unavailable; `prepack` keeps the manifest honest.
236
+
237
+ ```bash
238
+ npm login # once per machine
239
+ node scripts/verify.mjs # optional but recommended
240
+ npm publish # runs prepack -> sync-manifest --check
241
+ ```
242
+
144
243
  After the first restart, run `/preset` to apply the shipped config templates —
145
244
  they are deliberately not written automatically.
146
245
 
@@ -262,10 +361,13 @@ re-run `setup.mjs`.
262
361
  | --- | --- |
263
362
  | `extensions/preset.ts` | the bundled `/preset` command (apply config templates) |
264
363
  | `scripts/setup.mjs` | install deps, regenerate manifest, copy configs, report conflicts |
265
- | `scripts/verify.mjs` | 4-stage check: manifest, pi resolution, real loading, startup conflicts |
364
+ | `scripts/verify.mjs` | 4-stage check of the checkout: manifest, pi resolution, real loading, startup conflicts |
365
+ | `scripts/verify-tarball.mjs` | packs the tarball, installs it, and loads it through pi — catches `bundledDependencies` drift |
266
366
  | `scripts/sync-manifest.mjs` | regenerate `pi.extensions` (`--check` for drift) |
267
367
  | `scripts/pack.sh` | build a tarball (`--with-deps` to include `node_modules`) |
268
368
  | `scripts/pi-package-lib.mjs` | shared helpers mirroring pi's resolution rules |
369
+ | `.github/workflows/ci.yml` | verify + artifact checks on push/PR |
370
+ | `.github/workflows/publish.yml` | publish to npm on `vX.Y.Z` tags (trusted publishing) |
269
371
 
270
372
  ### `verify.mjs`
271
373
 
@@ -325,6 +427,23 @@ node scripts/setup.mjs [--skip-install] [--force-config]
325
427
  - **Paths are relative to this package.** `pi.extensions` points into
326
428
  `node_modules/`, so the directory must stay where it was installed (or be
327
429
  re-installed). Moving it requires re-running `pi install`.
430
+ - **The provider is a fork.** `@samrito/pi-cliproxyapi-provider` is a fork of
431
+ the upstream `pi-cliproxyapi-provider`, adding thinking levels derived from
432
+ models.dev `reasoning_options`. It keeps the upstream settings namespace,
433
+ config paths, cache directory, and `/cliproxyapi` command, so it is a drop-in
434
+ replacement — but the two must not be installed together, since both register
435
+ the same provider and command and pi rejects the duplicate.
436
+ - **No vendor-locked provider is bundled.** The vendor-scoped provider package
437
+ that was previously excluded from this bundle must not be re-introduced. CI
438
+ enforces this with a grep over tracked files (the term is assembled at runtime
439
+ in the workflow so the guard cannot match its own source), and
440
+ `scripts/pi-package-lib.mjs`'s `EXCLUDED_PACKAGES` lists what is deliberately
441
+ skipped — currently empty.
442
+ - **Traditional token publishing should stay disabled.** npm's *Require
443
+ two-factor authentication and disallow tokens* setting affects only traditional
444
+ token auth: "Your trusted publishers will continue to work normally, as they
445
+ use OIDC tokens." So it is safe — and recommended — to enable it while relying
446
+ on the trusted publisher.
328
447
  - **Do not commit `node_modules/`** unless you intend to distribute via tarball
329
448
  with `--with-deps`.
330
449
  - **`/preset` assumes the bundled layout.** It resolves templates relative to
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Richard Hao
4
+ Copyright (c) 2026 samrito (modifications to this fork)
5
+
6
+ This package is a fork of pi-cliproxyapi-provider
7
+ (https://github.com/0xRichardH/pi-cliproxyapi-provider) by Richard Hao,
8
+ distributed under the MIT License reproduced below. The original copyright
9
+ notice is retained as the license requires; the modifications in this fork are
10
+ copyright their respective author.
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
@@ -1,25 +1,32 @@
1
- # pi-cliproxyapi-provider
1
+ # @samrito/pi-cliproxyapi-provider
2
2
 
3
- `pi-cliproxyapi-provider` registers one [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) instance as a pi model provider. It discovers models from CLIProxyAPI's OpenAI-compatible `/v1/models` endpoint and enriches them with provider-specific metadata from [models.dev](https://models.dev/). Mixed catalogs use OpenAI Completions by default, while GPT-5.6 family models (including Codex variants) use the Responses API so pi can read their usage data. Canonical `/v1/models` owners such as `openai` select the matching provider metadata; aliases can override that selection when a proxy routes billing differently.
3
+ > A fork of [pi-cliproxyapi-provider](https://github.com/0xRichardH/pi-cliproxyapi-provider)
4
+ > by Richard Hao (MIT). This fork adds model thinking levels sourced from
5
+ > models.dev. It is a drop-in replacement: the settings namespace, config paths,
6
+ > cache directory, and `/cliproxyapi` command are unchanged, so existing
7
+ > configuration keeps working. Do not install it alongside the original — both
8
+ > register the same provider and command, and pi rejects the duplicate.
9
+
10
+ `@samrito/pi-cliproxyapi-provider` registers one [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) instance as a pi model provider. It discovers models from CLIProxyAPI's OpenAI-compatible `/v1/models` endpoint and enriches them with provider-specific metadata from [models.dev](https://models.dev/). Mixed catalogs use OpenAI Completions by default, while GPT-5.6 family models (including Codex variants) use the Responses API so pi can read their usage data. Canonical `/v1/models` owners such as `openai` select the matching provider metadata; aliases can override that selection when a proxy routes billing differently.
4
11
 
5
12
  ## Install
6
13
 
7
14
  Install from npm:
8
15
 
9
16
  ```bash
10
- pi install npm:pi-cliproxyapi-provider
17
+ pi install npm:@samrito/pi-cliproxyapi-provider
11
18
  ```
12
19
 
13
20
  Or install from GitHub:
14
21
 
15
22
  ```bash
16
- pi install git:github.com/0xRichardH/pi-cliproxyapi-provider@master
23
+ pi install git:github.com/xiangsam/pi-cliproxyapi-provider@master
17
24
  ```
18
25
 
19
26
  You can omit `@master`, but pinning a branch, tag, or commit makes Git installs reproducible:
20
27
 
21
28
  ```bash
22
- pi install git:github.com/0xRichardH/pi-cliproxyapi-provider@a28f326
29
+ pi install git:github.com/xiangsam/pi-cliproxyapi-provider@a28f326
23
30
  ```
24
31
 
25
32
  Restart pi after installing, then run:
@@ -96,6 +103,72 @@ The same setting can be placed in project `.pi/settings.json`; project settings
96
103
 
97
104
  Use `"full"` only when the selected CLIProxyAPI route and upstream account actually support that limit. Requests above `272000` input tokens also use the higher models.dev context-pricing tier where one is defined.
98
105
 
106
+ ### Thinking levels
107
+
108
+ The provider derives each model's selectable thinking levels from the
109
+ `reasoning_options` field in models.dev metadata. When a model publishes an
110
+ effort list, exactly those levels appear in Pi's thinking selector:
111
+
112
+ ```text
113
+ deepseek-flash reasoning_options: [{"type":"effort","values":["low","high","max"]}]
114
+ -> Pi offers off, low, high, max
115
+ ```
116
+
117
+ Two details make this more than cosmetic:
118
+
119
+ - **Absent levels are hidden, not defaulted.** models.dev publishes an
120
+ exhaustive list, so every level it omits is explicitly marked unsupported. This
121
+ matters because Pi otherwise offers levels up to `high` using the provider
122
+ default, and a proxy that validates the level rejects the request. CLIProxyAPI
123
+ does exactly that: an unsupported level comes back as
124
+ `400 level "medium" not supported, valid levels: low, high, max`.
125
+ - **`xhigh` and `max` only appear when the list names them.** Pi hides extended
126
+ levels unless a model maps them explicitly, which is why `max` was previously
127
+ unreachable for models that support it.
128
+
129
+ Only `type: "effort"` publishes levels. `toggle` and `budget_tokens` describe
130
+ other reasoning shapes and are ignored, so those models keep Pi's default. A
131
+ built-in rule for the GPT-5.6 family remains as a fallback for models whose
132
+ metadata carries no effort list; where models.dev publishes one, it wins, because
133
+ it tracks the model's current capability and the rule does not.
134
+
135
+ > **Levels describe the model, not your proxy.** models.dev publishes the
136
+ > canonical capability, and a CLIProxyAPI route can accept a different set. A
137
+ > mismatch that offers a level the proxy rejects makes Pi send a request that
138
+ > fails with `400`. Correct it with the override below.
139
+
140
+ #### Correcting a level list
141
+
142
+ When a proxy serves a model differently from its published metadata, or when
143
+ the models.dev match lands on a different provider, override the map in Pi's own
144
+ `~/.pi/agent/models.json`:
145
+
146
+ ```json
147
+ {
148
+ "providers": {
149
+ "cpa": {
150
+ "modelOverrides": {
151
+ "deepseek-flash": {
152
+ "thinkingLevelMap": {
153
+ "off": "none",
154
+ "minimal": null,
155
+ "low": "low",
156
+ "medium": null,
157
+ "high": "high",
158
+ "xhigh": null,
159
+ "max": "max"
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ This layer is applied by Pi after the provider registers its models, so it wins
169
+ over both models.dev and the built-in rules. Use `null` to hide a level the
170
+ proxy rejects. Replace `cpa` with your configured provider name.
171
+
99
172
  ### Model and display configuration
100
173
 
101
174
  Run `/cliproxyapi config` in Pi TUI mode to edit every package-level `settings.json` value. The tabbed panel has `Connection`, `Models`, and `Display` sections; it controls the GPT-5.6 context-window mode and whether the model selector shows the published strict tool-schema capability.
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "pi-cliproxyapi-provider",
3
- "version": "0.15.30",
2
+ "name": "@samrito/pi-cliproxyapi-provider",
3
+ "version": "0.16.0",
4
4
  "description": "Pi provider package for CLIProxyAPI with automatic model discovery and models.dev enrichment.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,12 +15,15 @@
15
15
  ],
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/0xRichardH/pi-cliproxyapi-provider.git"
18
+ "url": "git+https://github.com/xiangsam/pi-cliproxyapi-provider.git"
19
19
  },
20
20
  "bugs": {
21
- "url": "https://github.com/0xRichardH/pi-cliproxyapi-provider/issues"
21
+ "url": "https://github.com/xiangsam/pi-cliproxyapi-provider/issues"
22
+ },
23
+ "homepage": "https://github.com/xiangsam/pi-cliproxyapi-provider#readme",
24
+ "publishConfig": {
25
+ "access": "public"
22
26
  },
23
- "homepage": "https://github.com/0xRichardH/pi-cliproxyapi-provider#readme",
24
27
  "files": [
25
28
  "src",
26
29
  "extensions",
@@ -15,9 +15,20 @@ interface ModelCapabilityRule {
15
15
  overrides: ModelCapabilityOverrides;
16
16
  }
17
17
 
18
+ /**
19
+ * Fallback level map for the GPT-5.6 family, used only when models.dev publishes
20
+ * no effort list for the model.
21
+ *
22
+ * `minimal` is explicitly unsupported. Measured against a live CLIProxyAPI
23
+ * instance, gpt-5.6-sol, -terra, and -luna all answer
24
+ * `400 level "minimal" not supported, valid levels: low, medium, high, xhigh, max`,
25
+ * so offering it produces a failing request. models.dev omits `minimal` for the
26
+ * same models, which is why this rule is the fallback rather than the source of
27
+ * truth; it stays correct by hiding what the model does not accept.
28
+ */
18
29
  const GPT_5_6_THINKING_LEVEL_MAP: ThinkingLevelMap = {
19
30
  off: "none",
20
- minimal: "minimal",
31
+ minimal: null,
21
32
  low: "low",
22
33
  medium: "medium",
23
34
  high: "high",
@@ -75,13 +75,33 @@ function detailItems(model: ProviderModelConfigLike, override: ProviderModelOver
75
75
  ];
76
76
  }
77
77
 
78
+ /**
79
+ * Render the thinking levels pi will actually offer.
80
+ *
81
+ * Listing `Object.keys(thinkingLevelMap)` would be misleading: a key is present
82
+ * with value `null` precisely to mean "unsupported, hidden", so the keys alone
83
+ * include levels the user can never select. Show the mapped values instead and
84
+ * state the count of explicitly unsupported levels.
85
+ */
86
+ function thinkingLevelSummary(map: ProviderModelConfigLike["thinkingLevelMap"]): string {
87
+ if (!map) return "none";
88
+
89
+ const offered = Object.entries(map)
90
+ .filter(([, value]) => typeof value === "string")
91
+ .map(([level]) => level);
92
+ const hidden = Object.values(map).filter((value) => value === null).length;
93
+ if (offered.length === 0) return "none";
94
+
95
+ return hidden > 0 ? `${offered.join(", ")} (${hidden} unsupported)` : offered.join(", ");
96
+ }
97
+
78
98
  function details(model: ProviderModelConfigLike): string[] {
79
99
  return [
80
100
  `Name: ${model.name}`,
81
101
  `API: ${model.api ?? "openai-completions (provider default)"}`,
82
102
  `Input: ${model.input.join(", ")}`,
83
103
  `Cost: in ${model.cost.input}, out ${model.cost.output}, cache read ${model.cost.cacheRead}, cache write ${model.cost.cacheWrite}`,
84
- `Thinking map: ${model.thinkingLevelMap ? Object.keys(model.thinkingLevelMap).join(", ") : "none"}`,
104
+ `Thinking map: ${thinkingLevelSummary(model.thinkingLevelMap)}`,
85
105
  `Other compat: ${formattedOtherCompat(model)}`,
86
106
  ];
87
107
  }
@@ -2,6 +2,7 @@ import type { CpaModel } from "./cpa.ts";
2
2
  import { findMetadataMatch, type MetadataMatchMethod } from "./matching.ts";
3
3
  import { getModelApiOverride, isGpt56Model, type ModelApiContext } from "./model-api.ts";
4
4
  import { getModelCapabilityOverrides } from "./model-capabilities.ts";
5
+ import { thinkingLevelMapFromReasoningOptions } from "./reasoning-levels.ts";
5
6
  import type { Gpt56ContextWindowMode } from "./settings.ts";
6
7
  import type {
7
8
  InputModality,
@@ -83,14 +84,22 @@ function modelFromMetadata(
83
84
  const capabilityOverrides = getModelCapabilityOverrides(capabilityContext);
84
85
  const api = getModelApiOverride(capabilityContext);
85
86
 
87
+ // Precedence: models.dev wins when it publishes a level list, because that
88
+ // list tracks the model's current capability while the rules were written
89
+ // against an older catalog. Measured against a live CLIProxyAPI instance, the
90
+ // gpt-5.6 rule is now stale: it maps `minimal`, which the proxy rejects with
91
+ // `400 level "minimal" not supported`, whereas the models.dev list for the
92
+ // same model omits `minimal` and matches the proxy exactly. The rules still
93
+ // fill the gap when metadata is absent or carries no effort list.
94
+ const thinkingLevelMap =
95
+ thinkingLevelMapFromReasoningOptions(metadata.reasoning_options) ?? capabilityOverrides.thinkingLevelMap;
96
+
86
97
  return {
87
98
  id: cpaModel.id,
88
99
  name: metadata.name ?? cpaModel.id,
89
100
  reasoning: capabilityOverrides.reasoning ?? metadata.reasoning ?? PI_MODEL_DEFAULTS.reasoning,
90
101
  ...(api ? { api } : {}),
91
- ...(capabilityOverrides.thinkingLevelMap
92
- ? { thinkingLevelMap: capabilityOverrides.thinkingLevelMap }
93
- : {}),
102
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
94
103
  input: inputFromMetadata(metadata),
95
104
  cost: costFromMetadata(metadata),
96
105
  contextWindow: contextWindowForModel(capabilityContext, metadata.limit?.context, gpt56ContextWindow),
@@ -0,0 +1,59 @@
1
+ import type { ThinkingLevelMap } from "@earendil-works/pi-ai";
2
+ import type { ModelsDevReasoningOption } from "./types.ts";
3
+
4
+ /** pi thinking levels in ascending depth, excluding `off`. */
5
+ const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
6
+
7
+ /** The models.dev effort value that means "thinking disabled". */
8
+ const EFFORT_OFF = "none";
9
+
10
+ /**
11
+ * Convert models.dev `reasoning_options` into a pi `ThinkingLevelMap`.
12
+ *
13
+ * models.dev publishes an exhaustive `type: "effort"` list for models whose
14
+ * reasoning depth is selectable, using the same vocabulary pi exposes (`none`
15
+ * plus `minimal`..`max`). The conversion is total in both directions:
16
+ *
17
+ * - a level present in the list maps to itself, so pi sends that exact value
18
+ * - a level absent from the list maps to `null`, which hides it in pi's UI
19
+ * - pi's `off` maps to models.dev `none`, which the list may signal either as
20
+ * an effort value or as a separate `toggle` entry
21
+ *
22
+ * Null-ing absent levels is the point of this function. Because the list is
23
+ * exhaustive, leaving a level `undefined` would let pi offer it using the
24
+ * provider default, and a proxy that validates the level rejects the request.
25
+ * CLIProxyAPI does exactly that: it answers an unsupported level with
26
+ * `400 level "medium" not supported, valid levels: low, high, max`.
27
+ *
28
+ * `off` needs both signals because models.dev splits the ability to disable
29
+ * thinking from the level list. A model can publish `["low","high","max"]`
30
+ * alongside `{type: "toggle"}`, meaning thinking is selectable only within those
31
+ * levels but can still be switched off; reading the effort list alone would hide
32
+ * `off` from a model that supports it. 296 catalog entries have this shape,
33
+ * including `deepseek/deepseek-flash`, which a live CLIProxyAPI instance accepts
34
+ * `reasoning_effort: "none"` for.
35
+ *
36
+ * Returns `undefined` when the model publishes no effort list, so callers keep
37
+ * their own default. A lone `toggle` or `budget_tokens` entry enumerates no
38
+ * levels and cannot be expressed as a level map, so it is ignored.
39
+ */
40
+ export function thinkingLevelMapFromReasoningOptions(
41
+ options: ModelsDevReasoningOption[] | undefined,
42
+ ): ThinkingLevelMap | undefined {
43
+ const effort = options?.find(
44
+ (option) => option.type === "effort" && Array.isArray(option.values) && option.values.length > 0,
45
+ );
46
+ const values = effort?.values;
47
+ if (!values) return undefined;
48
+
49
+ const offered = new Set(values);
50
+ const canDisableThinking =
51
+ offered.has(EFFORT_OFF) || (options?.some((option) => option.type === "toggle") ?? false);
52
+ const map: ThinkingLevelMap = {
53
+ off: canDisableThinking ? EFFORT_OFF : null,
54
+ };
55
+ for (const level of PI_THINKING_LEVELS) {
56
+ map[level] = offered.has(level) ? level : null;
57
+ }
58
+ return map;
59
+ }
@@ -36,6 +36,12 @@ export interface ModelsDevMetadata {
36
36
  sourceProvider?: string;
37
37
  name?: string;
38
38
  reasoning?: boolean;
39
+ /**
40
+ * How the model exposes reasoning control. Only `type: "effort"` entries carry
41
+ * a level list that maps onto pi thinking levels; `toggle` and `budget_tokens`
42
+ * describe other shapes and are not converted.
43
+ */
44
+ reasoning_options?: ModelsDevReasoningOption[];
39
45
  modalities?: {
40
46
  input?: string[];
41
47
  output?: string[];
@@ -64,6 +70,22 @@ export interface ModelsDevMetadata {
64
70
 
65
71
  export type ModelsDevCatalog = Record<string, ModelsDevMetadata>;
66
72
 
73
+ /**
74
+ * One entry of a models.dev `reasoning_options` array.
75
+ *
76
+ * `type` is kept as a plain string because models.dev adds shapes over time
77
+ * (currently `effort`, `toggle`, and `budget_tokens`) and unknown ones must be
78
+ * ignored rather than rejected.
79
+ */
80
+ export interface ModelsDevReasoningOption {
81
+ type: string;
82
+ /** Levels for `type: "effort"`, drawn from the same vocabulary pi uses. */
83
+ values?: string[];
84
+ /** Token bounds for `type: "budget_tokens"`. */
85
+ min?: number;
86
+ max?: number;
87
+ }
88
+
67
89
  export interface ProviderModelConfigLike {
68
90
  id: string;
69
91
  name: string;
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "samrito-pi-preset",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Portable pi agent setup: bundles the extension collection and user config templates so another machine can be brought up with one install.",
6
6
  "keywords": [
7
7
  "pi-package",
8
8
  "pi-extensions"
9
9
  ],
10
- "license": "UNLICENSED",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/xiangsam/samrito-pi-preset.git"
14
+ },
15
+ "homepage": "https://github.com/xiangsam/samrito-pi-preset#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/xiangsam/samrito-pi-preset/issues"
18
+ },
11
19
  "publishConfig": {
12
20
  "access": "public"
13
21
  },
@@ -15,7 +23,8 @@
15
23
  "config",
16
24
  "extensions",
17
25
  "scripts",
18
- "README.md"
26
+ "README.md",
27
+ "LICENSE"
19
28
  ],
20
29
  "pi": {
21
30
  "extensions": [
@@ -24,9 +33,9 @@
24
33
  "node_modules/@juicesharp/rpiv-ask-user-question/index.ts",
25
34
  "node_modules/@juicesharp/rpiv-todo/index.ts",
26
35
  "node_modules/@narumitw/pi-btw/dist/index.ts",
36
+ "node_modules/@samrito/pi-cliproxyapi-provider/extensions/index.ts",
27
37
  "node_modules/pi-background-tasks/extensions/anthropic-attribution.ts",
28
38
  "node_modules/pi-background-tasks/extensions/background-tasks.ts",
29
- "node_modules/pi-cliproxyapi-provider/extensions/index.ts",
30
39
  "node_modules/pi-goal-x/extensions/goal.ts",
31
40
  "node_modules/pi-tool-display/index.ts",
32
41
  "node_modules/pi-zentui/extensions/zentui/index.ts"
@@ -35,6 +44,7 @@
35
44
  "scripts": {
36
45
  "setup": "node scripts/setup.mjs",
37
46
  "verify": "node scripts/verify.mjs",
47
+ "verify:tarball": "node scripts/verify-tarball.mjs",
38
48
  "pack": "bash scripts/pack.sh",
39
49
  "sync": "node scripts/sync-manifest.mjs",
40
50
  "prepack": "node scripts/sync-manifest.mjs --check"
@@ -44,8 +54,8 @@
44
54
  "@juicesharp/rpiv-ask-user-question": "^2.9.0",
45
55
  "@juicesharp/rpiv-todo": "^2.9.0",
46
56
  "@narumitw/pi-btw": "^0.58.1",
57
+ "@samrito/pi-cliproxyapi-provider": "^0.16.0",
47
58
  "pi-background-tasks": "^2.5.0",
48
- "pi-cliproxyapi-provider": "^0.15.30",
49
59
  "pi-goal-x": "^0.31.2",
50
60
  "pi-tool-display": "^0.5.0",
51
61
  "pi-zentui": "^0.23.0"
@@ -55,8 +65,8 @@
55
65
  "@juicesharp/rpiv-ask-user-question",
56
66
  "@juicesharp/rpiv-todo",
57
67
  "@narumitw/pi-btw",
68
+ "@samrito/pi-cliproxyapi-provider",
58
69
  "pi-background-tasks",
59
- "pi-cliproxyapi-provider",
60
70
  "pi-goal-x",
61
71
  "pi-tool-display",
62
72
  "pi-zentui"
@@ -17,7 +17,8 @@
17
17
  * collectExtensionFiles() before being written to a bundle manifest.
18
18
  */
19
19
 
20
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
20
+ import { execFileSync } from "node:child_process";
21
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
21
22
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
22
23
  import { homedir } from "node:os";
23
24
  import { fileURLToPath } from "node:url";
@@ -237,3 +238,42 @@ export function directorySize(dir) {
237
238
  export function relativeToPackage(path) {
238
239
  return relative(PACKAGE_ROOT, path);
239
240
  }
241
+
242
+ /**
243
+ * Locate pi's ESM entry point so its own package manager and loader can be
244
+ * imported. Returns undefined when pi is not installed.
245
+ *
246
+ * Set PI_MODULE_PATH to point at pi explicitly; otherwise `pi` is looked up on
247
+ * PATH and its package root walked up to.
248
+ */
249
+ export function findPiModule() {
250
+ const override = process.env.PI_MODULE_PATH;
251
+ if (override && existsSync(override)) return override;
252
+
253
+ let binary;
254
+ try {
255
+ binary = execFileSync("which", ["pi"], { encoding: "utf-8" }).trim();
256
+ } catch {
257
+ return undefined;
258
+ }
259
+
260
+ let current;
261
+ try {
262
+ current = dirname(realpathSync(binary));
263
+ } catch {
264
+ return undefined;
265
+ }
266
+
267
+ while (current !== dirname(current)) {
268
+ const manifestPath = join(current, "package.json");
269
+ if (existsSync(manifestPath)) {
270
+ const manifest = readJson(manifestPath);
271
+ if (manifest?.name === "@earendil-works/pi-coding-agent") {
272
+ const entry = join(current, manifest.exports?.["."]?.import ?? "dist/index.js");
273
+ return existsSync(entry) ? entry : undefined;
274
+ }
275
+ }
276
+ current = dirname(current);
277
+ }
278
+ return undefined;
279
+ }
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify the *published artifact*, not the source tree.
4
+ *
5
+ * `verify.mjs` checks the checkout: its `node_modules/` is a normal npm tree
6
+ * where hoisting is allowed. What users actually receive is the packed tarball,
7
+ * where `bundledDependencies` must have embedded every plugin *inside* the
8
+ * package root, because pi resolves each `pi.extensions` entry relative to that
9
+ * root. A drift between `dependencies` and `bundledDependencies`, or a plugin
10
+ * that npm decides not to bundle, breaks only the tarball — the source tree
11
+ * still looks perfect.
12
+ *
13
+ * So this script reproduces the real delivery path:
14
+ * 1. `npm pack` (runs prepack -> sync-manifest --check)
15
+ * 2. `npm install <tgz>` into a throwaway prefix, like pi does
16
+ * 3. resolve + load through pi's own DefaultPackageManager / loadExtensions
17
+ *
18
+ * Usage:
19
+ * node scripts/verify-tarball.mjs [--verbose] [--keep]
20
+ *
21
+ * Exits 1 on any failure. Skips (exit 0, with a note) when pi cannot be found,
22
+ * matching verify.mjs.
23
+ */
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
27
+ import { tmpdir } from "node:os";
28
+ import { dirname, join, relative } from "node:path";
29
+ import { pathToFileURL } from "node:url";
30
+ import {
31
+ PACKAGE_ROOT,
32
+ findPiModule,
33
+ manifestExtensionEntries,
34
+ readPackageJson,
35
+ } from "./pi-package-lib.mjs";
36
+
37
+ const verbose = process.argv.includes("--verbose");
38
+ const keep = process.argv.includes("--keep");
39
+ const problems = [];
40
+
41
+ const ok = (message) => console.log(` \u001b[32m✓\u001b[0m ${message}`);
42
+ const note = (message) => console.log(` \u001b[33m!\u001b[0m ${message}`);
43
+ const heading = (title) => console.log(`\n\u001b[1m${title}\u001b[0m`);
44
+
45
+ function problem(message) {
46
+ problems.push(message);
47
+ console.log(` \u001b[31m✗\u001b[0m ${message}`);
48
+ }
49
+
50
+ const npm = process.env.PI_NPM_COMMAND ?? "npm";
51
+ const manifest = readPackageJson();
52
+ const declared = manifestExtensionEntries();
53
+ const workDir = mkdtempSync(join(tmpdir(), "pi-tarball-"));
54
+
55
+ /** Thrown after a problem() call so the report is not duplicated in the catch. */
56
+ class AlreadyReported extends Error {}
57
+
58
+ /** npm pack/install can fail for reasons the output already explains. */
59
+ function run(command, args, options = {}) {
60
+ try {
61
+ return execFileSync(command, args, { encoding: "utf-8", ...options });
62
+ } catch (error) {
63
+ problem(`${command} ${args[0]} failed: ${String(error.stderr ?? error.message).trim().split("\n")[0]}`);
64
+ throw new AlreadyReported("command failed");
65
+ }
66
+ }
67
+
68
+ try {
69
+ // ------------------------------------------------------------- 1. pack
70
+
71
+ heading("1. npm pack");
72
+
73
+ const packOutput = run(npm, ["pack", "--silent", "--pack-destination", workDir], { cwd: PACKAGE_ROOT }).trim();
74
+ const archiveName = packOutput.split("\n").filter(Boolean).pop() ?? "";
75
+ const tarball = join(workDir, archiveName);
76
+ if (!archiveName || !existsSync(tarball)) {
77
+ problem(`npm pack did not produce a tarball (output: ${packOutput || "empty"})`);
78
+ throw new AlreadyReported("pack failed");
79
+ }
80
+ ok(`${archiveName} (${(statSync(tarball).size / 1024 / 1024).toFixed(1)} MB)`);
81
+
82
+ // ------------------------------------------- 2. install like pi does
83
+
84
+ heading("2. install into a throwaway prefix");
85
+
86
+ const installRoot = join(workDir, "agent", "npm");
87
+ run(
88
+ npm,
89
+ ["install", tarball, "--prefix", installRoot, "--legacy-peer-deps", "--no-audit", "--no-fund"],
90
+ { stdio: verbose ? "inherit" : "pipe" },
91
+ );
92
+
93
+ const installedRoot = join(installRoot, "node_modules", manifest.name);
94
+ if (!existsSync(installedRoot)) {
95
+ problem(`${manifest.name} is not present in the install prefix`);
96
+ throw new AlreadyReported("install failed");
97
+ }
98
+ ok(`installed to node_modules/${manifest.name}`);
99
+
100
+ // Every bundled plugin must exist *inside* the package root: that is what
101
+ // makes the relative pi.extensions paths resolve for an npm-installed user.
102
+ const missing = declared.filter((entry) => !existsSync(join(installedRoot, entry)));
103
+ if (missing.length > 0) {
104
+ problem(`${missing.length} pi.extensions entry/entries missing from the tarball`);
105
+ for (const entry of missing.slice(0, 10)) console.log(` ${entry}`);
106
+ console.log(" fix: keep `dependencies` and `bundledDependencies` in sync");
107
+ } else {
108
+ ok(`all ${declared.length} pi.extensions entries are contained in the tarball`);
109
+ }
110
+
111
+ // ------------------------------------------------- 3. pi resolve + load
112
+
113
+ heading("3. resolve and load through pi");
114
+
115
+ const piModule = findPiModule();
116
+ if (!piModule) {
117
+ note("pi not found on PATH; skipped (set PI_MODULE_PATH to enable)");
118
+ } else {
119
+ const agentDir = join(workDir, "agent");
120
+ writeFileSync(
121
+ join(agentDir, "settings.json"),
122
+ `${JSON.stringify({ packages: [`npm:${manifest.name}`] }, null, 2)}\n`,
123
+ "utf-8",
124
+ );
125
+
126
+ const { DefaultPackageManager, SettingsManager } = await import(pathToFileURL(piModule).href);
127
+ const settingsManager = SettingsManager.create(installedRoot, agentDir);
128
+ const manager = new DefaultPackageManager({
129
+ cwd: installedRoot,
130
+ agentDir,
131
+ settingsManager,
132
+ });
133
+
134
+ const resolved = await manager.resolve();
135
+ const extensions = resolved.extensions.filter((resource) => resource.enabled);
136
+
137
+ if (extensions.length !== declared.length) {
138
+ problem(`pi resolved ${extensions.length} extensions, expected ${declared.length}`);
139
+ } else {
140
+ ok(`pi resolved ${extensions.length} extensions from the installed tarball`);
141
+ }
142
+
143
+ // A path outside the package root means npm hoisted a plugin instead of
144
+ // bundling it, which is exactly the failure this script exists to catch.
145
+ const outside = extensions.filter((resource) => !resource.path.startsWith(installedRoot));
146
+ if (outside.length > 0) {
147
+ problem(`${outside.length} extension(s) resolved outside the installed package`);
148
+ for (const resource of outside) console.log(` ${resource.path}`);
149
+ }
150
+
151
+ const loader = await import(pathToFileURL(join(dirname(piModule), "core/extensions/loader.js")).href);
152
+ const result = await loader.loadExtensions(
153
+ extensions.map((resource) => resource.path),
154
+ installedRoot,
155
+ );
156
+
157
+ for (const error of result.errors) {
158
+ problem(`load error in ${relative(installedRoot, error.path)}: ${error.error.split("\n")[0]}`);
159
+ }
160
+ if (result.errors.length === 0) {
161
+ const tools = result.extensions.reduce((total, extension) => total + extension.tools.size, 0);
162
+ const commands = result.extensions.reduce((total, extension) => total + extension.commands.size, 0);
163
+ ok(`all ${result.extensions.length} extensions loaded, 0 errors`);
164
+ console.log(` · registered: ${tools} tools, ${commands} commands`);
165
+ }
166
+
167
+ // The bundled /preset command has to survive packaging too; without it the
168
+ // config templates shipped in config/ would be unreachable.
169
+ if (result.extensions.some((extension) => extension.commands.has("preset"))) {
170
+ ok("/preset command is registered");
171
+ } else {
172
+ problem("/preset command is missing from the packaged extensions");
173
+ }
174
+
175
+ if (verbose) {
176
+ for (const extension of result.extensions) {
177
+ console.log(` ${relative(installedRoot, extension.resolvedPath ?? extension.path)}`);
178
+ }
179
+ }
180
+ }
181
+ } catch (error) {
182
+ if (!(error instanceof AlreadyReported)) {
183
+ problem(`verification failed: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ } finally {
186
+ if (keep) {
187
+ console.log(`\n kept: ${workDir}`);
188
+ } else {
189
+ rmSync(workDir, { recursive: true, force: true });
190
+ }
191
+ }
192
+
193
+ console.log("");
194
+ if (problems.length > 0) {
195
+ console.log(`\u001b[31m${problems.length} problem(s)\u001b[0m`);
196
+ for (const item of problems) console.log(` - ${item}`);
197
+ process.exitCode = 1;
198
+ } else {
199
+ console.log("\u001b[32mOK\u001b[0m — the packed tarball installs, resolves, and loads.");
200
+ }