pi-nebius 0.3.0

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 (73) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CONTRIBUTING.md +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +239 -0
  5. package/SECURITY.md +47 -0
  6. package/benchmarks/add-api-endpoint/benchmark.yaml +13 -0
  7. package/benchmarks/add-api-endpoint/fixture/app.mjs +6 -0
  8. package/benchmarks/add-api-endpoint/fixture/app.test.mjs +8 -0
  9. package/benchmarks/add-api-endpoint/fixture/package.json +8 -0
  10. package/benchmarks/add-api-endpoint/validation/check.test.mjs +37 -0
  11. package/benchmarks/fix-auth-bug/benchmark.yaml +14 -0
  12. package/benchmarks/fix-auth-bug/fixture/auth.mjs +4 -0
  13. package/benchmarks/fix-auth-bug/fixture/auth.test.mjs +26 -0
  14. package/benchmarks/fix-auth-bug/fixture/package.json +8 -0
  15. package/benchmarks/fix-auth-bug/validation/check.test.mjs +25 -0
  16. package/benchmarks/multi-file-feature/benchmark.yaml +15 -0
  17. package/benchmarks/multi-file-feature/fixture/package.json +8 -0
  18. package/benchmarks/multi-file-feature/fixture/routes.mjs +11 -0
  19. package/benchmarks/multi-file-feature/fixture/routes.test.mjs +14 -0
  20. package/benchmarks/multi-file-feature/fixture/serialize.mjs +3 -0
  21. package/benchmarks/multi-file-feature/fixture/store.mjs +10 -0
  22. package/benchmarks/multi-file-feature/validation/check.test.mjs +52 -0
  23. package/benchmarks/refactor-module/benchmark.yaml +12 -0
  24. package/benchmarks/refactor-module/fixture/invoice.mjs +8 -0
  25. package/benchmarks/refactor-module/fixture/invoice.test.mjs +9 -0
  26. package/benchmarks/refactor-module/fixture/package.json +8 -0
  27. package/benchmarks/refactor-module/validation/check.test.mjs +36 -0
  28. package/dist/benchmark/cli.js +112 -0
  29. package/dist/benchmark/command.js +194 -0
  30. package/dist/benchmark/definition.js +109 -0
  31. package/dist/benchmark/host-worker.js +14 -0
  32. package/dist/benchmark/instrumentation.js +296 -0
  33. package/dist/benchmark/metrics.js +78 -0
  34. package/dist/benchmark/process.js +122 -0
  35. package/dist/benchmark/project.js +70 -0
  36. package/dist/benchmark/report.js +94 -0
  37. package/dist/benchmark/runner.js +376 -0
  38. package/dist/benchmark/types.js +1 -0
  39. package/dist/benchmark/worker.js +134 -0
  40. package/dist/benchmark/workspace.js +55 -0
  41. package/dist/discovery.js +154 -0
  42. package/dist/errors.js +32 -0
  43. package/dist/index.js +86 -0
  44. package/dist/model-settings-command.js +130 -0
  45. package/dist/model-settings.js +101 -0
  46. package/dist/models.js +62 -0
  47. package/dist/provider.js +48 -0
  48. package/docs/benchmark-research.md +35 -0
  49. package/docs/benchmarking.md +253 -0
  50. package/docs/security-review.md +49 -0
  51. package/docs/validation.md +51 -0
  52. package/examples/models.json +31 -0
  53. package/package.json +74 -0
  54. package/src/benchmark/cli.ts +118 -0
  55. package/src/benchmark/command.ts +218 -0
  56. package/src/benchmark/definition.ts +112 -0
  57. package/src/benchmark/host-worker.ts +14 -0
  58. package/src/benchmark/instrumentation.ts +298 -0
  59. package/src/benchmark/metrics.ts +101 -0
  60. package/src/benchmark/process.ts +120 -0
  61. package/src/benchmark/project.ts +71 -0
  62. package/src/benchmark/report.ts +111 -0
  63. package/src/benchmark/runner.ts +458 -0
  64. package/src/benchmark/types.ts +180 -0
  65. package/src/benchmark/worker.ts +150 -0
  66. package/src/benchmark/workspace.ts +64 -0
  67. package/src/discovery.ts +176 -0
  68. package/src/errors.ts +32 -0
  69. package/src/index.ts +96 -0
  70. package/src/model-settings-command.ts +151 -0
  71. package/src/model-settings.ts +129 -0
  72. package/src/models.ts +73 -0
  73. package/src/provider.ts +63 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ Versions use Semantic Versioning. Entries describe changes included in the named version;
4
+ a version is released only when its matching Git tag and GitHub release are published.
5
+
6
+ ## [0.3.0](https://github.com/PeterHdd/pi-nebius/compare/v0.2.1...v0.3.0) (2026-09-17)
7
+
8
+
9
+ ### Features
10
+
11
+ * add persistent per-model Nebius settings ([d1b3114](https://github.com/PeterHdd/pi-nebius/commit/d1b31146abf17d54aa43d57f5c9080072cea78e6))
12
+ * add persistent per-model Nebius settings ([b31dce1](https://github.com/PeterHdd/pi-nebius/commit/b31dce1087e41e41482a56634f3dc5f57213bae4))
13
+
14
+ ## [0.2.1](https://github.com/PeterHdd/pi-nebius/compare/v0.2.0...v0.2.1) (2026-09-17)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * clarify auth benchmark requirements and expand visible test ([557271b](https://github.com/PeterHdd/pi-nebius/commit/557271b74b30e457cdeca7b3e3d491e84903eca4))
20
+ * clarify auth benchmark requirements and expand visible test ([3d86156](https://github.com/PeterHdd/pi-nebius/commit/3d86156f41677cdd2672fe6b5196e44d2830b5c6))
21
+
22
+ ## [0.2.0](https://github.com/PeterHdd/pi-nebius/compare/v0.1.0...v0.2.0) (2026-09-17)
23
+
24
+
25
+ ### Features
26
+
27
+ * add in-Pi benchmarks for custom prompts and bundled tasks ([615c800](https://github.com/PeterHdd/pi-nebius/commit/615c800975288242530a362ee7fd954e1ae08502))
28
+ * add Nebius provider and coding benchmarks for Pi ([a770213](https://github.com/PeterHdd/pi-nebius/commit/a7702139910c11afec35235fea785f1b805ad478))
29
+ * improve benchmark reporting and automate releases ([fbbca35](https://github.com/PeterHdd/pi-nebius/commit/fbbca35ecf46156616b74a4707d09f2fd85dbe8b))
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * **ci:** stabilize benchmark tests and target main branch only ([7ffc127](https://github.com/PeterHdd/pi-nebius/commit/7ffc127e90defa063fb6c66352385b2c9a3cfff1))
35
+ * support Pi Git installs without dev dependencies ([18ec13b](https://github.com/PeterHdd/pi-nebius/commit/18ec13b6afba14d3948409e89a358aa4eae0eab4))
36
+
37
+ ## [0.1.0]
38
+
39
+ Development baseline before automated GitHub releases.
40
+
41
+ - Native Pi provider with authenticated Nebius model discovery and a private metadata cache.
42
+ - Agentic benchmark CLI with deterministic validation, isolated runs, request/tool traces,
43
+ cumulative usage and four coding fixtures.
44
+ - Credential-free integration tests using real Pi sessions and mocked provider responses.
45
+ - GitHub CI, dependency updates, secret scanning, workflow validation, and automated changelogs and GitHub releases.
46
+ - Filesystem checks reject overlapping validator paths and symlinked fixture roots/output aliases.
47
+
48
+ Live Nebius acceptance remains outstanding. Supported baseline: Pi 0.85.1, Node 22.19+,
49
+ macOS/Linux; benchmark execution is sequential.
@@ -0,0 +1,39 @@
1
+ # Contributing
2
+
3
+ Use Node 22.19+ on macOS or Linux. Pi development dependencies are locked to the
4
+ tested release; retain the lockfile and update both Pi packages together.
5
+
6
+ ```bash
7
+ npm ci
8
+ npm run check
9
+ npm run version:check
10
+ npm run security:check
11
+ npm run package:check
12
+ ```
13
+
14
+ `check` runs TypeScript, Biome, the build, and credential-free tests. Security checks
15
+ download checksum-pinned official tools; package checks install a temporary tarball
16
+ with the tested Pi peers. Both require network access. Tests need loopback sockets
17
+ and permission to launch/stop subprocesses. Do not provide live API keys to CI.
18
+
19
+ Keep changes focused and add regression tests for behavior changes. Use a descriptive
20
+ commit message; no special commit syntax or automatic version inference is required.
21
+ Update the changelog for user-visible changes. Report whether a test used mocks or
22
+ live Nebius; a scripted tool loop is not a hosted-model benchmark.
23
+
24
+ Never commit credentials, `.env` files, private fixtures, `node_modules`, or generated
25
+ benchmark results. The secret scanner scans Git-visible files and history, so a
26
+ credential committed and later deleted must still be revoked and removed from history.
27
+
28
+ Release Please generates changelog/version updates in a release PR; merging it publishes
29
+ the GitHub release after CI passes. Changes to workflow permissions,
30
+ tool checksum pins, dependency lockfiles, and authentication deserve particular review.
31
+
32
+ PR descriptions are maintained by the **PR description** workflow. Write meaningful
33
+ commit subjects and optional bullet points in commit bodies: these populate Changes.
34
+ Validation reports actual CI job/step outcomes for the latest PR commit and updates
35
+ when CI runs. Add context or live-testing evidence under Notes, outside the
36
+ `auto-pr` markers. The workflow preserves that text and leaves Release Please PRs
37
+ alone. It uses the built-in GitHub token; no additional secret or AI service is needed.
38
+ It becomes active after the workflow reaches the default branch. For existing PRs
39
+ without markers, it appends its generated section without deleting your description.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pi-nebius contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,239 @@
1
+ # pi-nebius
2
+
3
+ Nebius Token Factory models in Pi's normal model picker, discovered using your API key.
4
+
5
+ This package also includes **benchmarks inside Pi**: run `/nebius-benchmark` to enter your own task prompt and compare models on copies of your current project. Results appear in Pi. A standalone CLI also supports scripted tasks and deterministic validation. See [Benchmarking](docs/benchmarking.md) for the CLI, result schema, metrics, and four ready-to-run fixtures.
6
+
7
+ ```bash
8
+ npm ci
9
+ npm run build
10
+ npm run benchmark -- --benchmark benchmarks/fix-auth-bug \
11
+ --models 'EXACT_MODEL_ID_A,EXACT_MODEL_ID_B' --runs 3 \
12
+ --output benchmark-results/auth-comparison
13
+ ```
14
+
15
+ Results appear directly in the terminal: each run prints its pass/fail status, followed by a comparison table of success counts, mean input/output tokens, median task duration, observed time to first token (TTFT), end-to-end output throughput, turns, and tool calls. Detailed JSON reports, traces, and workspaces are also saved under `benchmark-results/`; `--output` chooses the directory.
16
+
17
+ Set `NEBIUS_API_KEY` first. `npm run benchmark:demo` exercises the real Pi runner with scripted model responses, without credentials.
18
+
19
+ ```text
20
+ Pi → pi-nebius → https://api.tokenfactory.nebius.com/v1 → model
21
+ ```
22
+
23
+ Requires **Node.js 22.19+ and Pi 0.85.1** (the tested version). This targets the current `@earendil-works` Pi packages, not older `@mariozechner` releases. Pi's package instructions require wildcard peer dependencies; that is not a claim that every Pi version is compatible.
24
+
25
+ ## Installation
26
+
27
+ With Pi already installed:
28
+
29
+ ```bash
30
+ pi install npm:pi-nebius
31
+ export NEBIUS_API_KEY="your-api-key"
32
+ pi
33
+ ```
34
+
35
+ Run `/model`, search for `nebius`, and select a model.
36
+
37
+ From this checkout:
38
+
39
+ ```bash
40
+ cd /path/to/pi-nebius
41
+ npm ci
42
+ pi install /absolute/path/to/pi-nebius
43
+ ```
44
+
45
+ Or try it without adding it to Pi's settings:
46
+
47
+ ```bash
48
+ pi -e /absolute/path/to/pi-nebius
49
+ ```
50
+
51
+ The provider uses Pi's `pi.extensions` manifest and loads TypeScript directly. Pi installs Git packages with `npm install --omit=dev`; the extension needs no build step. To use the benchmark CLI from a source checkout, run `npm ci` and `npm run build` to generate `dist/`. YAML is the benchmark definition parser; neither component needs a separate server.
52
+
53
+ GitHub installation is also supported:
54
+
55
+ ```bash
56
+ pi install git:github.com/PeterHdd/pi-nebius
57
+ ```
58
+
59
+ Git installs require no compiler or development dependencies. Benchmark development uses a separate source checkout with development dependencies installed. Append a published tag (for example, `@v0.3.0`) to pin a Git installation; install a newer tag explicitly to upgrade.
60
+
61
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [CHANGELOG.md](CHANGELOG.md), and [SECURITY.md](SECURITY.md).
62
+
63
+ ## Authentication and usage
64
+
65
+ ```bash
66
+ export NEBIUS_API_KEY="your-key"
67
+ pi
68
+ ```
69
+
70
+ Inside Pi, run `/model` and search for `nebius`. Choose an exact model from the discovered list. Provider ID: `nebius`; display name: `Nebius Token Factory`. Nebius IDs such as `vendor/model` remain unchanged; Pi identifies the pair as `nebius/vendor/model`.
71
+
72
+ You can also inspect the catalog and select a model explicitly:
73
+
74
+ ```bash
75
+ pi --list-models nebius
76
+ pi --provider nebius --model 'EXACT_ID_FROM_THE_LIST'
77
+ ```
78
+
79
+ `/nebius-refresh` forces discovery again and updates Pi's registered models. If a selected model was removed, use `/model` to choose a current one.
80
+
81
+ The extension reads `NEBIUS_API_KEY` at startup for discovery and resolves it through Pi's native authentication interface for inference. It has no `/login` flow and never writes the key to a file. Set the variable in the shell that launches Pi; changing a parent shell's environment cannot change an already-running process. Restart Pi after changing it. Pi's explicit CLI/runtime authentication overrides remain Pi features, but discovery specifically requires `NEBIUS_API_KEY`.
82
+
83
+ ## Benchmark inside Pi
84
+
85
+ After installing the extension, run this inside Pi:
86
+
87
+ ```text
88
+ /nebius-benchmark
89
+ ```
90
+
91
+ An editor asks for your own task prompt. The benchmark uses your selected Nebius model and a copy of the project directory where Pi is running. To compare models, supply their exact IDs:
92
+
93
+ ```text
94
+ /nebius-benchmark --models zai-org/GLM-5.3,moonshotai/Kimi-K2.6 --runs 3
95
+ ```
96
+
97
+ This runs the same prompt three times per model, sequentially, starting from the same project snapshot each time. It shows progress and a comparison of input/output tokens, task duration, observed TTFT, output throughput, turns, and tools directly in Pi. Custom prompts report **correctness not checked**; completion does not prove the task was solved.
98
+
99
+ The snapshot includes current working files, including uncommitted changes, and respects Git ignores. It excludes `.git`, `.pi`, dependency/build folders, prior benchmark results, `.env` files, and `.pem`/`.key` files. Dependencies are not preinstalled; include setup instructions in your task if needed. Links and special files are rejected. Snapshots are limited to 10,000 files / 50 MiB. Run Pi from the project directory you want to benchmark.
100
+
101
+ Runs use paid inference. Tools have normal host permissions: file copies are not an OS sandbox. Reports and each run's resulting files are retained under `benchmark-results/` in your project.
102
+
103
+ ```text
104
+ /nebius-benchmark cancel
105
+ /nebius-benchmark help
106
+ ```
107
+
108
+ For an optional bundled coding task with automatic correctness tests:
109
+
110
+ ```text
111
+ /nebius-benchmark --task fix-auth-bug --runs 3
112
+ ```
113
+
114
+ Other bundled tasks: `add-api-endpoint`, `refactor-module`, and `multi-file-feature`. No build step or separate terminal is needed for these Pi commands.
115
+
116
+ ## Dynamic model discovery
117
+
118
+ The async extension factory calls authenticated `GET /v1/models?verbose=true` before Pi finishes startup. This is the documented verbose variant of `/v1/models`, supplying metadata in addition to IDs. There is **no bundled model catalog**.
119
+
120
+ | Nebius field | Pi mapping |
121
+ | --- | --- |
122
+ | `id` | Exact, case-sensitive `model.id` |
123
+ | `name` | Display name; defaults to ID |
124
+ | `context_length` | `contextWindow`; defaults to 32,768 when absent/invalid |
125
+ | `architecture.modality` | Text/image inputs; known non-text-output models are excluded |
126
+ | `status` | Only `active` or unspecified models are included |
127
+ | `supported_sampling_parameters` contains `reasoning_effort` | Enables reasoning and Pi's standard reasoning-effort control |
128
+ | `supported_features` contains `reasoning` | Marks reasoning support without assuming an effort control |
129
+ | No documented maximum output length | `maxTokens`: 4,096, capped at a quarter of the context window |
130
+
131
+ Basic responses containing only IDs are supported. Missing capabilities default to text-only with no explicit reasoning control. Without modality metadata, the endpoint may include models unsuitable for chat; discovery cannot prove tool support. Pick a tool-capable model and run the integration test. Duplicate IDs are deduplicated; malformed lists are rejected rather than silently replacing a good cache with partial data.
132
+
133
+ ### Cache and offline fallback
134
+
135
+ The cache is under `getAgentDir()/cache/pi-nebius/` (normally `~/.pi/agent/cache/pi-nebius/`). Pi's `PI_CODING_AGENT_DIR` override is respected. Files contain normalized metadata and a timestamp, never the API key. Names use a SHA-256 fingerprint of the endpoint and key to avoid sharing one account's catalog with another key.
136
+
137
+ - Fresh metadata is used for 24 hours without a discovery request.
138
+ - Stale/missing metadata triggers one request, with an 8-second timeout and a 4 MiB response limit.
139
+ - Transient failures use stale metadata and display a warning. No cache means no discovered models; Pi continues running.
140
+ - A 401/403 invalidates this key's cache. A fresh cache does not validate credentials; inference can still reject a revoked key.
141
+ - Successful refresh replaces the catalog, including an authoritative empty list. Failed refresh never updates the cache timestamp.
142
+ - Cache writes are atomic and best-effort, with private directory/file permissions. Concurrent Pi processes may each perform discovery; there is no background service or cross-process lock.
143
+
144
+ Pi now has a shared native model store, but its general provider-level cache is not scoped by the discovery key. This small extension-owned cache makes key isolation explicit and guarantees discovery before model selection. It does not participate in Pi's general remote-catalog refresh; use `/nebius-refresh` or restart Pi.
145
+
146
+ ### Per-model settings
147
+
148
+ Run `/nebius-model` to choose a discovered model, or `/nebius-model MODEL_ID` to open it directly.
149
+ The menu saves temperature, reasoning effort, and maximum output tokens separately for each model.
150
+ Blank numeric values or **Inherit** restore defaults; **Reset all overrides** clears that model's settings.
151
+ Advanced settings let you override the context window and reasoning capability metadata. These do not
152
+ change the server's capabilities. Use verified limits; the default output limit remains 4,096 tokens
153
+ when the catalog does not provide a documented output limit.
154
+
155
+ Temperature and reasoning-effort overrides are sent only when discovery advertises support.
156
+ An advertised parameter does not guarantee every value is supported by every model; use its documented values.
157
+ Saved request values take precedence over Pi's generated values (including `/thinking` for reasoning effort).
158
+ Changes apply to new requests immediately and survive restarts in
159
+ `~/.pi/agent/pi-nebius/model-settings.json` (respecting `PI_CODING_AGENT_DIR`).
160
+ `/nebius-refresh` preserves saved overrides. External file edits require `/reload`.
161
+
162
+ Both benchmark entry points use saved settings. In-Pi benchmarks snapshot them when the run starts,
163
+ so changing the menu does not change an active comparison. Each run's `effectiveSettings` includes
164
+ saved overrides, model limits, and `requestParameters`: the outgoing temperature, reasoning effort,
165
+ output limit, and top-p where present, without prompt content. Omitted values mean the request did not
166
+ specify them; server defaults are unknown. These parameters are in `results.json`, not the summary table.
167
+
168
+ ### Metadata overrides and user-defined fallback models
169
+
170
+ Merge the `nebius` provider entry from [examples/models.json](examples/models.json) into `~/.pi/agent/models.json`, replacing the placeholder ID and limits with verified values. Do not overwrite unrelated providers. The extension must remain installed.
171
+
172
+ Use `modelOverrides` to adjust discovered models and `models` to add explicit models that remain available when discovery is offline. Native providers are composed beneath these settings by Pi. No `apiKey` field is needed in this configuration. Explicitly configured models remain until you remove them, even if the server no longer lists them.
173
+
174
+
175
+ For reasoning models, verify the model's Nebius-specific behavior before overriding `reasoning`, `thinkingLevelMap`, or `compat`. Pi supports model-specific template controls, but this extension does not guess them from model names. Always-on reasoning may still appear in Pi even when explicit effort selection is unavailable.
176
+
177
+ ## Architecture and compatibility
178
+
179
+ `src/index.ts` is Pi's async extension factory. It registers a native provider with `pi.registerProvider(provider)`, adds `/nebius-refresh`, and supplies provider-specific error guidance through `message_end`.
180
+
181
+ `src/provider.ts` uses Pi's `createProvider()` and its exported `openAICompletionsApi()` adapter. Pi owns request/message serialization, SSE parsing, multi-turn conversation state, function calls/results, usage accounting, abort handling, retries, and the tool loop. A small fetch decorator retains retry/rate-limit/request-ID headers on HTTP errors; successful response streams pass through unchanged.
182
+
183
+ Compatibility settings select `system` messages and `max_tokens`, request streamed usage, and disable unsupported assumptions about `store`, strict tools, grammar tools, and reasoning effort. These are model-level properties and can be overridden through Pi.
184
+
185
+
186
+ ## Testing
187
+
188
+ ```bash
189
+ npm ci
190
+ npm run typecheck
191
+ npm run lint
192
+ npm test
193
+ ```
194
+
195
+ Tests cover model parsing/mapping, discovery errors, missing credentials, cache behavior, native authentication, and Pi's actual extension loader. Adapter tests use Pi itself with mocked HTTP responses. The CLI integration test starts a local HTTP server, loads this package in the real Pi CLI, checks `--list-models`, executes a streamed `write` call, checks the generated file, and verifies that the next request contains the tool result. It also checks cache reuse and `models.json` composition. These tests do not require or use a real Nebius key.
196
+
197
+ ### Manual live integration test
198
+
199
+ In a scratch directory, start Pi, select a tool-capable Nebius model with `/model`, then ask:
200
+
201
+ > Create a file called nebius-test.txt containing 'Hello from Nebius'.
202
+
203
+ Verify that Pi shows a successful write-tool execution, the file contains the expected text, and the model responds after receiving the tool result. Check Token Factory's usage view for the request. Ordinary text claiming the file was created is not sufficient.
204
+
205
+ An opt-in automated equivalent runs a real Pi CLI in a temporary directory, limits its tools to `write`, requires an actual tool event and final response, checks exact file contents, and cleans up afterward:
206
+
207
+ ```bash
208
+ export NEBIUS_API_KEY="your-key"
209
+ export NEBIUS_MODEL="EXACT_TOOL_CAPABLE_MODEL_ID"
210
+ npm run test:live
211
+ ```
212
+
213
+ This makes paid inference requests and has a three-minute timeout. It is not part of `npm test`. See [docs/validation.md](docs/validation.md) for what has actually been run.
214
+
215
+ ## Troubleshooting
216
+
217
+ | Problem | Action |
218
+ | --- | --- |
219
+ | Missing API key | Run `export NEBIUS_API_KEY="..."` in the launching shell and restart Pi. |
220
+ | 401/403 | Token Factory rejected the key or project access. Check the key, permissions, and project; then restart Pi. |
221
+ | Model unavailable / 404 | Run `/nebius-refresh`, then `/model`; remove obsolete explicit entries from `models.json`. |
222
+ | 429 | Respect the preserved `retry-after` / rate-limit details. Pi handles its normal retry policy; discovery itself does not retry automatically. |
223
+ | 5xx | Provider service failure. Retry later; discovery can use stale cached metadata. |
224
+ | Connectivity / timeout | Check access to `api.tokenfactory.nebius.com` and Pi's network/proxy settings. Discovery times out after eight seconds. |
225
+ | No models without a cache | Add an explicit verified model via the example configuration, or retry discovery once connectivity returns. |
226
+ | Model talks but never uses tools | Verify that the chosen model supports functions on Nebius; run the live test or select another model. Discovery alone cannot certify this. |
227
+ | Context/output limit errors | Set verified `contextWindow` and `maxTokens` in `modelOverrides`. Defaults are conservative assumptions, not guaranteed endpoint limits. |
228
+
229
+ ## Current limitations
230
+
231
+ - Live Nebius inference has not been verified in the development environment because no API key was available. Mocked Pi integration tests prove adapter wiring and the tool loop, not every hosted model's capabilities.
232
+ - Metadata availability and capability vocabulary vary. Reasoning/vision detection is conservative; model-specific thinking templates require explicit configuration and testing.
233
+ - Maximum output limits are defaults; the verbose schema does not expose a documented equivalent of Pi's `maxTokens`.
234
+ - Pi's adapter handles `stop`, `length`, and `tool_calls` finish reasons. Its generic stream options do not expose custom stop sequences; advanced extensions can use Pi's `onPayload` hook if needed. No new stop-sequence API is invented here.
235
+ - The normal picker/listing integration is tested through Pi's CLI and registry. An interactive terminal screenshot/UI test was not performed.
236
+
237
+ ## License
238
+
239
+ MIT; see [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,47 @@
1
+ # Security policy
2
+
3
+ This project accepts security fixes for the latest tagged 0.x release and
4
+ the default branch. Older versions receive no separate backports.
5
+
6
+ ## Reporting
7
+
8
+ When this repository is hosted on GitHub, use **Security → Report a vulnerability**
9
+ if private vulnerability reporting is enabled. Do not put API keys, private code,
10
+ or exploitable details into a public issue. If private reporting is unavailable,
11
+ open an issue requesting a private contact without disclosing the vulnerability.
12
+ There is no dedicated security mailbox or response-time guarantee yet.
13
+
14
+ ## Trust boundaries
15
+
16
+ - Pi extensions and built-in tools execute with your host user's access. The benchmark's
17
+ file copies and separate processes are **not an OS sandbox**. Run untrusted tasks in
18
+ a disposable environment. Agents may access paths outside the workspace.
19
+ - Benchmark definitions, setup commands, and validators are executable code: review them.
20
+ Restored validators prevent ordinary accidental replacement, not malicious host access.
21
+ - The provider reads `NEBIUS_API_KEY`; its cache stores model metadata and a key fingerprint,
22
+ not the key. The normal interactive Pi process inherits your shell environment.
23
+ - The benchmark sends its key over private parent/child IPC. Tool and validation subprocesses
24
+ receive an allowlisted environment without that key. Traces redact the known key and bearer
25
+ strings; arbitrary secrets in user fixtures or generated files cannot be reliably identified.
26
+ - Network inference sends task context and tool results to Nebius. Follow your own data policy.
27
+ - Cache/filesystem checks assume the same host user is trusted. They do not defend against
28
+ an attacker concurrently replacing filesystem entries or controlling the parent process.
29
+
30
+ ## Repository controls
31
+
32
+ Test/security CI uses read-only tokens and SHA-pinned Actions, no live credentials, and ordinary
33
+ `pull_request` triggers. After successful main-branch CI, Release Please updates a
34
+ release PR or publishes the merged release. Its dedicated repository-scoped token
35
+ is confined to the release workflow, which checks out no code and executes no
36
+ repository build scripts. npm publication is disabled with `private: true`.
37
+
38
+ `npm run security:check` verifies pinned official Gitleaks/actionlint binary checksums,
39
+ scans Git-visible first-party files and available commit history with redacted output,
40
+ and validates workflows. It downloads executable tools from GitHub. `npm audit` checks
41
+ known dependency advisories separately. Neither scan proves the absence of vulnerabilities.
42
+
43
+ See [the review record](docs/security-review.md) for findings and residual limitations.
44
+
45
+ The PR-description workflow uses `pull_request_target` and `workflow_run` only to
46
+ read GitHub metadata and update the PR body with a scoped write token. It never
47
+ checks out PR code, installs dependencies, or consumes workflow artifacts.
@@ -0,0 +1,13 @@
1
+ schemaVersion: 1
2
+ name: add-api-endpoint
3
+ task: |
4
+ Implement GET /api/sum in app.mjs, preserving GET /health. Accept query parameters
5
+ a and b as finite numbers and return JSON {sum: a+b} with status 200. Missing,
6
+ blank, nonnumeric, infinite inputs or non-finite sums must return 400. Other
7
+ methods on /api/sum return 405. Unknown routes return 404. Keep the existing
8
+ Fetch Request/Response interface and do not change tests.
9
+ validation:
10
+ - command: node
11
+ args: [--test, "{validation}/check.test.mjs"]
12
+ timeout: 120
13
+ validationTimeout: 15
@@ -0,0 +1,6 @@
1
+ export function handle(request) {
2
+ if (new URL(request.url).pathname === "/health" && request.method === "GET") {
3
+ return Response.json({ ok: true });
4
+ }
5
+ return Response.json({ error: "Not found" }, { status: 404 });
6
+ }
@@ -0,0 +1,8 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { handle } from "./app.mjs";
4
+
5
+ test("health", async () =>
6
+ assert.deepEqual(await handle(new Request("http://local/health")).json(), { ok: true }));
7
+ test("sum endpoint", async () =>
8
+ assert.deepEqual(await handle(new Request("http://local/api/sum?a=2&b=3")).json(), { sum: 5 }));
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "api-fixture",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ }
8
+ }
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { resolve } from "node:path";
3
+ import { test } from "node:test";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ const { handle } = await import(pathToFileURL(resolve("app.mjs")));
7
+ test("HTTP endpoint contract", async () => {
8
+ assert.deepEqual(await handle(new Request("http://local/health")).json(), { ok: true });
9
+ for (const [a, b] of [
10
+ [2, 3],
11
+ [-4, 1.5],
12
+ [0, 0],
13
+ [100, -100],
14
+ ]) {
15
+ const response = await handle(new Request(`http://local/api/sum?a=${a}&b=${b}`));
16
+ assert.equal(response.status, 200);
17
+ assert.match(response.headers.get("content-type"), /application\/json/);
18
+ assert.deepEqual(await response.json(), { sum: a + b });
19
+ }
20
+ for (const query of [
21
+ "",
22
+ "a=1",
23
+ "b=2",
24
+ "a=&b=2",
25
+ "a=%20&b=2",
26
+ "a=no&b=2",
27
+ "a=Infinity&b=1",
28
+ "a=1e308&b=1e308",
29
+ ]) {
30
+ assert.equal((await handle(new Request(`http://local/api/sum?${query}`))).status, 400);
31
+ }
32
+ assert.equal(
33
+ (await handle(new Request("http://local/api/sum?a=1&b=2", { method: "POST" }))).status,
34
+ 405,
35
+ );
36
+ assert.equal((await handle(new Request("http://local/missing"))).status, 404);
37
+ });
@@ -0,0 +1,14 @@
1
+ schemaVersion: 1
2
+ name: fix-auth-bug
3
+ task: |
4
+ Fix refresh-token expiry handling in auth.mjs. A token is valid strictly before
5
+ its expiresAt timestamp (Unix seconds); nowMs is in milliseconds. Both expiresAt
6
+ and nowMs must be finite numbers: reject NaN, Infinity, -Infinity, missing values,
7
+ and strings without coercing them to numbers. Reject revoked and missing tokens.
8
+ Run the existing tests and ensure both timestamp inputs meet these requirements.
9
+ Do not change the tests.
10
+ validation:
11
+ - command: node
12
+ args: [--test, "{validation}/check.test.mjs"]
13
+ timeout: 120
14
+ validationTimeout: 15
@@ -0,0 +1,4 @@
1
+ export function canRefresh(token, nowMs) {
2
+ if (!token || token.revoked) return false;
3
+ return token.expiresAt > nowMs;
4
+ }
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { canRefresh } from "./auth.mjs";
4
+
5
+ test("unexpired refresh token", () => {
6
+ assert.equal(canRefresh({ expiresAt: 2000, revoked: false }, 1_999_000), true);
7
+ });
8
+ test("expiry boundary", () => {
9
+ assert.equal(canRefresh({ expiresAt: 2000, revoked: false }, 2_000_000), false);
10
+ });
11
+
12
+ test("expired, revoked, and missing tokens", () => {
13
+ assert.equal(canRefresh({ expiresAt: 2000 }, 2_000_001), false);
14
+ assert.equal(canRefresh({ expiresAt: 2000, revoked: true }, 1000), false);
15
+ assert.equal(canRefresh(null, 1000), false);
16
+ });
17
+
18
+ for (const invalid of [NaN, Infinity, -Infinity, undefined, "2000"]) {
19
+ test(`rejects invalid expiresAt: ${String(invalid)}`, () => {
20
+ assert.equal(canRefresh({ expiresAt: invalid }, 1000), false);
21
+ });
22
+
23
+ test(`rejects invalid nowMs: ${String(invalid)}`, () => {
24
+ assert.equal(canRefresh({ expiresAt: 2000 }, invalid), false);
25
+ });
26
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "auth-fixture",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ }
8
+ }
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert/strict";
2
+ import { resolve } from "node:path";
3
+ import { test } from "node:test";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ const { canRefresh } = await import(pathToFileURL(resolve("auth.mjs")));
7
+ test("refresh contract across boundaries and invalid inputs", () => {
8
+ assert.equal(canRefresh({ expiresAt: 2000 }, 1_999_999), true);
9
+ assert.equal(canRefresh({ expiresAt: 2000 }, 2_000_000), false);
10
+ assert.equal(canRefresh({ expiresAt: 2000 }, 2_000_001), false);
11
+ assert.equal(canRefresh({ expiresAt: 2000, revoked: true }, 1000), false);
12
+ assert.equal(canRefresh(null, 1000), false);
13
+ for (const bad of [NaN, Infinity, -Infinity, undefined, "2000"]) {
14
+ assert.equal(
15
+ canRefresh({ expiresAt: bad }, 1000),
16
+ false,
17
+ `expiresAt must be a finite number; received ${String(bad)}`,
18
+ );
19
+ assert.equal(
20
+ canRefresh({ expiresAt: 2000 }, bad),
21
+ false,
22
+ `nowMs must be a finite number; received ${String(bad)}`,
23
+ );
24
+ }
25
+ });
@@ -0,0 +1,15 @@
1
+ schemaVersion: 1
2
+ name: multi-file-feature
3
+ task: |
4
+ Add task completion across store.mjs, routes.mjs and serialize.mjs. New tasks
5
+ start with completed:false. Export setCompleted(id, completed) from store.mjs;
6
+ update the stored task and return it, or return null for an unknown ID.
7
+ Expose PATCH /tasks/:id accepting JSON {completed:boolean}, returning the
8
+ updated serialized task with status 200, 400 for invalid JSON/nonboolean values,
9
+ or 404 for missing tasks. Serialization must include completed. Preserve GET /tasks
10
+ and POST /tasks. Completion must persist into subsequent GET results.
11
+ validation:
12
+ - command: node
13
+ args: [--test, "{validation}/check.test.mjs"]
14
+ timeout: 180
15
+ validationTimeout: 15
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "multi-file-fixture",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ import { serialize } from "./serialize.mjs";
2
+ import { create, list } from "./store.mjs";
3
+ export async function handle(request) {
4
+ const path = new URL(request.url).pathname;
5
+ if (path === "/tasks" && request.method === "GET") return Response.json(list().map(serialize));
6
+ if (path === "/tasks" && request.method === "POST") {
7
+ const { title } = await request.json();
8
+ return Response.json(serialize(create(title)), { status: 201 });
9
+ }
10
+ return Response.json({ error: "Not found" }, { status: 404 });
11
+ }
@@ -0,0 +1,14 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { handle } from "./routes.mjs";
4
+
5
+ test("created task starts incomplete", async () => {
6
+ const response = await handle(
7
+ new Request("http://local/tasks", {
8
+ method: "POST",
9
+ body: JSON.stringify({ title: "A task" }),
10
+ }),
11
+ );
12
+ assert.equal(response.status, 201);
13
+ assert.equal((await response.json()).completed, false);
14
+ });
@@ -0,0 +1,3 @@
1
+ export function serialize(task) {
2
+ return { id: task.id, title: task.title };
3
+ }
@@ -0,0 +1,10 @@
1
+ const tasks = [];
2
+ let nextId = 1;
3
+ export function create(title) {
4
+ const task = { id: String(nextId++), title };
5
+ tasks.push(task);
6
+ return task;
7
+ }
8
+ export function list() {
9
+ return tasks;
10
+ }