diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9159c55..24fc5e4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,7 +50,8 @@ brew install go node uv
3. Add or update tests for changed behavior.
4. Update the documentation when you change commands, flags, output, or
requirements.
-5. Run `make test`.
+5. Run `make test`. It runs the engine tests in parallel; set
+ `PYTEST_XDIST_AUTO_NUM_WORKERS` to choose the worker count.
## Open a pull request
diff --git a/Makefile b/Makefile
index 5a88593..cccfb76 100644
--- a/Makefile
+++ b/Makefile
@@ -3,10 +3,10 @@
# The OpenCode client is built for Apple Silicon only.
ifeq ($(shell uname -sm),Darwin arm64)
test: test-cli test-engine
-CLIENTS = opencode pi
+CLIENTS = opencode pi omp
else
test: test-cli
-CLIENTS = pi
+CLIENTS = pi omp
endif
test-cli:
@@ -24,7 +24,7 @@ PYTHON_ROOT = $(shell uv python dir)/cpython-3.12.12-macos-aarch64-none
test-engine:
cd engine && uv venv --allow-existing --python 3.12 && uv pip install '.[validation]'
- cd engine && .venv/bin/pytest -v -rs
+ cd engine && nice -n 10 .venv/bin/pytest -v -rs -n auto --dist loadgroup
uv python install 3.12.12
rm -rf .build/bundle-mlx .build/smoke
$(PYTHON_ROOT)/bin/python3.12 packaging/build-mlx-runtime.py --python-root $(PYTHON_ROOT) --output .build/bundle-mlx
diff --git a/README.md b/README.md
index 0e08a2a..a1d106f 100644
--- a/README.md
+++ b/README.md
@@ -48,11 +48,12 @@ terminals. All other data lives under `~/.local/share/rec`.
We find the best performance from our models when paired with the
[pi](https://pi.dev/) harness. For convenience, rec provides pre-configured
-recipes for running pi and [OpenCode](https://opencode.ai) harnesses. Run
+recipes for running pi, omp, and [OpenCode](https://opencode.ai) harnesses. Run
them using:
```bash
rec launch pi # preferred
+rec launch omp
rec launch opencode
```
@@ -65,7 +66,7 @@ run downloads the model and harness. The model server continues to run after
you close the terminal. Run `rec stop` to stop it.
These harnesses are kept separate from anything on your system. Use
-`rec exec pi|opencode ARGS` to run their own commands, for example:
+`rec exec pi|omp|opencode ARGS` to run their own commands, for example:
```bash
rec exec pi install npm:PACKAGE
@@ -109,6 +110,18 @@ The response contains generated text in `choices`. For other clients, use
base URL `http://127.0.0.1:11500/v1`, model `flint`, and the API key from
`rec auth token`.
+To bound the reasoning of a chat request, add `"thinking_budget": N`, a
+nonnegative integer. It needs `grammar=flint` (the default). N counts the
+tokens after the model's own ``; when N is reached, Flint closes the
+`` block itself. A budget of 0 skips reasoning. A prompt that already
+ends with `` is rejected with HTTP 400. A prompt with an
+unclosed `` anywhere is also rejected. `/v1/messages` rejects only
+`thinking_budget` with 400; `thinking.budget_tokens` is ignored as before. When
+the request sets `thinking_budget` (0 included), the response `usage` reports
+`reasoning_tokens` and `thinking_forced_close`. Streaming responses report them
+only in the final usage chunk, so set `stream_options.include_usage`. Without a
+budget these keys do not appear.
+
The Anthropic-compatible API is available at
`http://127.0.0.1:11500/v1/messages`. Point `ANTHROPIC_BASE_URL` at
`http://127.0.0.1:11500` and send the token from `rec auth token` as a bearer
@@ -119,8 +132,8 @@ token or as `x-api-key`. The model is `flint`.
| Command | What it does |
|---|---|
-| `rec launch pi\|opencode` | Start the server if needed and open a harness |
-| `rec exec pi\|opencode ARGS` | Run a harness command, such as installing a plugin |
+| `rec launch pi\|omp\|opencode` | Start the server if needed and open a harness |
+| `rec exec pi\|omp\|opencode ARGS` | Run a harness command, such as installing a plugin |
| `rec serve` | Start the model server in this terminal |
| `rec status` | Show whether the server is running |
| `rec stop` | Stop the server |
diff --git a/cli/clients.go b/cli/clients.go
index 5c01be7..4ef8b3b 100644
--- a/cli/clients.go
+++ b/cli/clients.go
@@ -14,6 +14,12 @@ var openCodeLogo embed.FS
//go:embed clients/pi/package.json
var piPackage []byte
+//go:embed clients/omp/package.json
+var ompPackage []byte
+
+//go:embed clients/omp/package-lock.json
+var ompLock []byte
+
//go:embed clients/pi/package-lock.json
var piLock []byte
diff --git a/cli/clients/omp/package-lock.json b/cli/clients/omp/package-lock.json
new file mode 100644
index 0000000..fe3d293
--- /dev/null
+++ b/cli/clients/omp/package-lock.json
@@ -0,0 +1,2564 @@
+{
+ "name": "omp",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@oh-my-pi/pi-coding-agent": "18.12.6"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz",
+ "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.11"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.11",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@huggingface/jinja": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz",
+ "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@huggingface/tokenizers": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.2.0.tgz",
+ "integrity": "sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/@huggingface/transformers": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.1.tgz",
+ "integrity": "sha512-5+voSeUCFK6PCKlY15dPhNOKq3/zOTxDN7oC+Wy9dFb1QZ0TSQomZRRy7URRK8YRuUoE9fGMQTTPC5d/pWBJhA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@huggingface/jinja": "^0.5.10",
+ "@huggingface/tokenizers": "^0.2.0",
+ "onnxruntime-node": "1.30.0",
+ "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0",
+ "sharp": "^0.35.4"
+ }
+ },
+ "node_modules/@huggingface/transformers/node_modules/global-agent": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.6.tgz",
+ "integrity": "sha512-V9u791atI/bS7RryfRTK2oixDWo6/RCrIikjwuQ4kADPRZZml1C6LUJXc7y6qRhaAF8geo3bIvE/siAGJCw38A==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "globalthis": "^1.0.2",
+ "matcher": "^4.0.0",
+ "semver": "^7.3.5",
+ "serialize-error": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/@huggingface/transformers/node_modules/matcher": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
+ "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.30.0.tgz",
+ "integrity": "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@huggingface/transformers/node_modules/onnxruntime-node": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.30.0.tgz",
+ "integrity": "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32",
+ "darwin",
+ "linux"
+ ],
+ "dependencies": {
+ "adm-zip": "^0.6.0",
+ "global-agent": "^4.1.3",
+ "onnxruntime-common": "1.30.0"
+ }
+ },
+ "node_modules/@huggingface/transformers/node_modules/serialize-error": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
+ "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@huggingface/transformers/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
+ "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
+ "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
+ "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
+ "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
+ "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
+ "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
+ "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
+ "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
+ "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
+ "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
+ "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
+ "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
+ "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
+ "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
+ "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
+ "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
+ "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
+ "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
+ "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
+ "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
+ "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
+ "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
+ "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
+ "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
+ "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
+ "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@oh-my-pi/omp-stats": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/omp-stats/-/omp-stats-18.12.6.tgz",
+ "integrity": "sha512-BvRe/++D5UOTSkxLc2DWYVnrFzkEk0wIxUHKIGQcirO0ovFqUbEr25lbqE0vnP1KhU5NW24MzvDmBBkAr8bUbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "lucide-react": "^1.24.0",
+ "react": "19.2.7",
+ "react-dom": "19.2.7"
+ },
+ "bin": {
+ "omp-stats": "src/index.ts"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/omptype": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/omptype/-/omptype-18.12.6.tgz",
+ "integrity": "sha512-s0KVs/0RrBwS01TGZKm30R10npWG+WWnr2uzs2zqpX//cLucHus7B3VXeZ1dCwVUggmnFofgS3dKo2oUrLMVog==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.3.14",
+ "node": ">=20"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-agent-core": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-agent-core/-/pi-agent-core-18.12.6.tgz",
+ "integrity": "sha512-BS85MS7yDPXC3ooSWMKGFZIFywECGfZHaeTmx56iV9ZBaJEw4f3jAEteoOo6imWTY9klZZlmoi79e2N9Um4ANw==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "@oh-my-pi/pi-wire": "18.12.6",
+ "@oh-my-pi/snapcompact": "18.12.6",
+ "@opentelemetry/api": "^1.9.1"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-ai": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-ai/-/pi-ai-18.12.6.tgz",
+ "integrity": "sha512-Ld54zgvMLYPONS1aqj4dIyhwFsNBvjP7vLosP2HoKNfW4q2lTObdGcHFsshoA7TtgVVgUh0R8zjsmoET1AX3JQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/omptype": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "@oh-my-pi/pi-wire": "18.12.6"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-catalog": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-catalog/-/pi-catalog-18.12.6.tgz",
+ "integrity": "sha512-3f6K8AgDTHGoGDkxUB2+NkSc+HTF4NA5b2EWjqbObxiZQTJtBK58DBMH78anRSptdgAt/xH5a4TOLo4dyK9sWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/omptype": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-coding-agent": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-coding-agent/-/pi-coding-agent-18.12.6.tgz",
+ "integrity": "sha512-Wo+FcJ7A3RY/OFuafj4hcdIAXc/W7NMxbanwQh3Cmy8s57+ihLOLgAv+HROxHBkssEMg0MaScwkQ9xwqLPiC/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@oh-my-pi/omp-stats": "18.12.6",
+ "@oh-my-pi/omptype": "18.12.6",
+ "@oh-my-pi/pi-agent-core": "18.12.6",
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-mnemopi": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-tui": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "@oh-my-pi/pi-wire": "18.12.6",
+ "@oh-my-pi/snapcompact": "18.12.6",
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/api-logs": "^0.220.0",
+ "@opentelemetry/context-async-hooks": "^2.9.0",
+ "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0",
+ "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0",
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0",
+ "@opentelemetry/resources": "^2.9.0",
+ "@opentelemetry/sdk-logs": "^0.220.0",
+ "@opentelemetry/sdk-metrics": "^2.9.0",
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
+ "@opentelemetry/sdk-trace-node": "^2.9.0",
+ "axe-core": "4.13.0",
+ "puppeteer-core": "25.3.0"
+ },
+ "bin": {
+ "omp": "dist/cli.js"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ },
+ "optionalDependencies": {
+ "@huggingface/transformers": "^4.2.0",
+ "sherpa-onnx-node": "1.13.2"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-mnemopi": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-mnemopi/-/pi-mnemopi-18.12.6.tgz",
+ "integrity": "sha512-DFwZbuo5uWz2K2qZ4jGaUgAwg6o6rDPcd4RrbRVbAsVLx8bKpKGF2XRBHTUO3ddtkYt/9KMa7fnKZGcnny+ewQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6"
+ },
+ "bin": {
+ "mnemopi": "src/cli.ts"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ },
+ "peerDependencies": {
+ "fastembed": "3.0.0",
+ "onnxruntime-node": "1.21.0"
+ },
+ "peerDependenciesMeta": {
+ "fastembed": {
+ "optional": true
+ },
+ "onnxruntime-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives/-/pi-natives-18.12.6.tgz",
+ "integrity": "sha512-+Wkya/qqxnH2PlukMunxovOb1brrY+A/kIl0dgU1oFkYa6/4C1GsPrc5TIpR04MvJWZb6aH9uR5DmxlmTsR0QA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.3.14"
+ },
+ "optionalDependencies": {
+ "@oh-my-pi/pi-natives-darwin-arm64": "18.12.6",
+ "@oh-my-pi/pi-natives-darwin-x64": "18.12.6",
+ "@oh-my-pi/pi-natives-linux-arm64": "18.12.6",
+ "@oh-my-pi/pi-natives-linux-x64": "18.12.6",
+ "@oh-my-pi/pi-natives-win32-arm64": "18.12.6",
+ "@oh-my-pi/pi-natives-win32-x64": "18.12.6"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-darwin-arm64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-arm64/-/pi-natives-darwin-arm64-18.12.6.tgz",
+ "integrity": "sha512-sslGAG+6Zi9o/AA9RF1zkqb4hKGv+l3ccFB/xy4v9d83Y+GUeHn53y7HzBUVIDfX53wWTJBBeDu2sBWBjZvl+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-darwin-x64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-darwin-x64/-/pi-natives-darwin-x64-18.12.6.tgz",
+ "integrity": "sha512-GjPk+rerUu7uKgEn37TVVYVUQ799uptvTQUHXJVik1KCtWadob90fEmMiSl7Fnu9xm4eJo1bxe9QBKHMWqE+HA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-linux-arm64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-arm64/-/pi-natives-linux-arm64-18.12.6.tgz",
+ "integrity": "sha512-5T06xr0GZLiC6GQMoqaInJDOk7YbciBpR4K6eCvLlg07KAo7tDD29B0BTp0iSEMxXYcav8H/rLqV3K1IaR+ReQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-linux-x64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-linux-x64/-/pi-natives-linux-x64-18.12.6.tgz",
+ "integrity": "sha512-Dr03a4R2sU03/Qn5I9JgY8c3dnt7O2JNeV8FaFEJsiAlp8DKgfKfrsbVHDk4kqYgiNVfD4Zbyr20e3eFra6rSg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-win32-arm64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-arm64/-/pi-natives-win32-arm64-18.12.6.tgz",
+ "integrity": "sha512-u/S6NUS3t0Na+SeSpD+/vCArWI6mD2EaA9WoT4b008c/exjitqybDsvUcda8F+vW50hWRzcFOONji2rAVbN2uQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-natives-win32-x64": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-natives-win32-x64/-/pi-natives-win32-x64-18.12.6.tgz",
+ "integrity": "sha512-i7ggXk1MD/toSvkdBImtr15pEkIwL/+h6MjVNVR9b5zfJJv9vBEpGLqgKymV/FisI2K1U8rb3EvSDkSgiD4y9A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-tui": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-tui/-/pi-tui-18.12.6.tgz",
+ "integrity": "sha512-3jW1LDk6PcmfktGop6Q2+NJBtvMaBgwLmTBKYb2mKFMOTubpnhDs50djKp/zdc2UIEYcEck6Yh83gZA+H8qrdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/omptype": "18.12.6",
+ "@oh-my-pi/pi-agent-core": "18.12.6",
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "@oh-my-pi/pi-wire": "18.12.6",
+ "@oh-my-pi/snapcompact": "18.12.6"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-utils": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-utils/-/pi-utils-18.12.6.tgz",
+ "integrity": "sha512-+dX5BgX3p50HoeuItmXwiOkcX8EbLMfvObhfjYBYTUCB5qznAxkbKMVu2ZjM0MRx44yzXuYAG8Okk7OOcvreKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/pi-natives": "18.12.6"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/pi-wire": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/pi-wire/-/pi-wire-18.12.6.tgz",
+ "integrity": "sha512-NPREkkLEUqbcqJ5T/NOg2xF3d5wYSE92CWoEwxMvbyBerdyU0JhZKFgciiAiHTg5F/cH9oYJNr3Dm8MRVrpDWA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@oh-my-pi/snapcompact": {
+ "version": "18.12.6",
+ "resolved": "https://registry.npmjs.org/@oh-my-pi/snapcompact/-/snapcompact-18.12.6.tgz",
+ "integrity": "sha512-w34sYdM5vw6Pilqf5krZZgcFVpLnfaYsLT2yA/G30j+YNlg8oB6WxmShmY8LqNFlxbP373Fet8bLkuqRdHuBCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oh-my-pi/pi-ai": "18.12.6",
+ "@oh-my-pi/pi-catalog": "18.12.6",
+ "@oh-my-pi/pi-natives": "18.12.6",
+ "@oh-my-pi/pi-utils": "18.12.6",
+ "@oh-my-pi/pi-wire": "18.12.6"
+ },
+ "engines": {
+ "bun": ">=1.3.14"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz",
+ "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.12.0.tgz",
+ "integrity": "sha512-iK/1nD7mmZVkuYjVjkdFBiCvzntJdFny1S3CYbUYyOq7V5enL6XX+gJ7eh965L1LnS11VA8mQ7Ej/ck04SB0MQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-logs-otlp-proto": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.220.0.tgz",
+ "integrity": "sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0",
+ "@opentelemetry/sdk-logs": "0.220.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-http": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz",
+ "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/sdk-metrics": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz",
+ "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-proto": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.220.0.tgz",
+ "integrity": "sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/exporter-metrics-otlp-http": "0.220.0",
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/sdk-metrics": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz",
+ "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-proto": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz",
+ "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/sdk-trace": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz",
+ "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/otlp-transformer": "0.220.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz",
+ "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.220.0",
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/sdk-logs": "0.220.0",
+ "@opentelemetry/sdk-metrics": "2.9.0",
+ "@opentelemetry/sdk-trace": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz",
+ "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.0.tgz",
+ "integrity": "sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.12.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz",
+ "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz",
+ "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.220.0",
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.0.tgz",
+ "integrity": "sha512-mVMaEHwCUJuY3S4Esn+nT0yQA3qCEGh+SOPJsXygU0Gl453WcsDNjtEa8EfdewMwoLyr1tlofB4fXytVpiXuow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.12.0",
+ "@opentelemetry/resources": "2.12.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz",
+ "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz",
+ "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.0.tgz",
+ "integrity": "sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==",
+ "deprecated": "Use @opentelemetry/sdk-trace package",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.12.0",
+ "@opentelemetry/resources": "2.12.0",
+ "@opentelemetry/sdk-trace": "2.12.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz",
+ "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz",
+ "integrity": "sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.12.0",
+ "@opentelemetry/resources": "2.12.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.12.0.tgz",
+ "integrity": "sha512-9DufH1IlmQMoKAfot5y/FhTP1RnmhLb5K6eKmJO/bvzPjYwPAazoiAUbTZFdPbqxOXN0FEbhZx/e6SB1Og4jmg==",
+ "deprecated": "Use @opentelemetry/sdk-trace package",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/context-async-hooks": "2.12.0",
+ "@opentelemetry/core": "2.12.0",
+ "@opentelemetry/sdk-trace-base": "2.12.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz",
+ "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/@puppeteer/browsers": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz",
+ "integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "modern-tar": "^0.7.6",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "browsers": "lib/main-cli.js"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "peerDependencies": {
+ "proxy-agent": ">=8.0.1",
+ "yauzl": "^2.10.0 || ^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "proxy-agent": {
+ "optional": true
+ },
+ "yauzl": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "26.6.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
+ "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "undici-types": "~8.9.0"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz",
+ "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
+ "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
+ "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==",
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
+ "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "^3.0.1",
+ "zod": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=20.19.0 <22.0.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1638949",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz",
+ "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flatbuffers": {
+ "version": "25.9.23",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
+ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz",
+ "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/guid-typescript": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/lucide-react": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz",
+ "integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
+ "node_modules/modern-tar": {
+ "version": "0.7.7",
+ "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz",
+ "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/onnxruntime-common": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
+ "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/onnxruntime-node": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz",
+ "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32",
+ "darwin",
+ "linux"
+ ],
+ "peer": true,
+ "dependencies": {
+ "global-agent": "^3.0.0",
+ "onnxruntime-common": "1.21.0",
+ "tar": "^7.0.1"
+ }
+ },
+ "node_modules/onnxruntime-web": {
+ "version": "1.31.0-dev.20260914-8d85527a0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.31.0-dev.20260914-8d85527a0.tgz",
+ "integrity": "sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "flatbuffers": "^25.1.24",
+ "guid-typescript": "^1.0.9",
+ "long": "^5.2.3",
+ "onnxruntime-common": "1.31.0-dev.20260911-2a43ec07e",
+ "platform": "^1.3.6",
+ "protobufjs": "^7.2.4"
+ }
+ },
+ "node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
+ "version": "1.31.0-dev.20260911-2a43ec07e",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.0-dev.20260911-2a43ec07e.tgz",
+ "integrity": "sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/protobufjs": {
+ "version": "7.6.6",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz",
+ "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "25.3.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz",
+ "integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "3.0.6",
+ "chromium-bidi": "16.0.1",
+ "devtools-protocol": "0.0.1638949",
+ "typed-query-selector": "^2.12.2",
+ "webdriver-bidi-protocol": "0.4.2",
+ "ws": "^8.21.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
+ "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.5",
+ "@img/sharp-darwin-x64": "0.35.5",
+ "@img/sharp-freebsd-wasm32": "0.35.5",
+ "@img/sharp-libvips-darwin-arm64": "1.3.4",
+ "@img/sharp-libvips-darwin-x64": "1.3.4",
+ "@img/sharp-libvips-linux-arm": "1.3.4",
+ "@img/sharp-libvips-linux-arm64": "1.3.4",
+ "@img/sharp-libvips-linux-ppc64": "1.3.4",
+ "@img/sharp-libvips-linux-riscv64": "1.3.4",
+ "@img/sharp-libvips-linux-s390x": "1.3.4",
+ "@img/sharp-libvips-linux-x64": "1.3.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4",
+ "@img/sharp-linux-arm": "0.35.5",
+ "@img/sharp-linux-arm64": "0.35.5",
+ "@img/sharp-linux-ppc64": "0.35.5",
+ "@img/sharp-linux-riscv64": "0.35.5",
+ "@img/sharp-linux-s390x": "0.35.5",
+ "@img/sharp-linux-x64": "0.35.5",
+ "@img/sharp-linuxmusl-arm64": "0.35.5",
+ "@img/sharp-linuxmusl-x64": "0.35.5",
+ "@img/sharp-webcontainers-wasm32": "0.35.5",
+ "@img/sharp-win32-arm64": "0.35.5",
+ "@img/sharp-win32-ia32": "0.35.5",
+ "@img/sharp-win32-x64": "0.35.5"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sherpa-onnx-darwin-arm64": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.13.8.tgz",
+ "integrity": "sha512-FPNgJMgnWVl/KhRTIhG3KL3A4Om63Rn4YKXc9/uHY7SzLcvqLJLc/h7UBWJwduXvv7K18t5NpxHR6XgXn4sjWw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/sherpa-onnx-darwin-x64": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.13.8.tgz",
+ "integrity": "sha512-7BLRpjM6w4f9W46/nmkmq8lEKUayhebvcpslCVQ+6QN2uReYlZEMDZlSpXMjme+hUFrPfRz8P3UNq8ep/4d19g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/sherpa-onnx-linux-arm64": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.13.8.tgz",
+ "integrity": "sha512-Tlg7a70b/Wge3OF8IgTHF9jhSVCsLyKQKhwc4BsJ5A+dL/SrFtGBjzuHp4XeLhiiOT7afCxX5PdSn/D4c8Lnuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/sherpa-onnx-linux-x64": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.13.8.tgz",
+ "integrity": "sha512-6plnhjagsSeTntCgnlag86hWbs/uZE9Crms1LgOb68/1nKsIQjMd+WG519m+aPwT6TrsBOiEMzrx41t8sL5L5g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/sherpa-onnx-node": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.13.2.tgz",
+ "integrity": "sha512-uIH6SA5Or4pb8HlCYWB3K54XkMtzdef4/tkw1amtIf8GB1tt6hQLpur9p2jSFNfTYRyzZ8XrXofxefXQ0A7EUA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "optionalDependencies": {
+ "sherpa-onnx-darwin-arm64": "^1.13.2",
+ "sherpa-onnx-darwin-x64": "^1.13.2",
+ "sherpa-onnx-linux-arm64": "^1.13.2",
+ "sherpa-onnx-linux-x64": "^1.13.2",
+ "sherpa-onnx-win-ia32": "^1.13.2",
+ "sherpa-onnx-win-x64": "^1.13.2"
+ }
+ },
+ "node_modules/sherpa-onnx-win-ia32": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.13.8.tgz",
+ "integrity": "sha512-H0Ojln9hfvM+pxWqW0cnjB/XK8/JPq4/k0IWu2JpY5Z458M3zv+XC3Hu+wmot3AoH/K1Bgt23n/tTKtPV9x8qg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/sherpa-onnx-win-x64": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.13.8.tgz",
+ "integrity": "sha512-oZF1c9VPOKtMwn83Bboc5XSWL+76BRoyB3eUuVnCknBKxwSULZU2Foia9VHWzU+n4I12rPsP6z6H9Rp1hD9o8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/string-width": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz",
+ "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-query-selector": {
+ "version": "2.12.3",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz",
+ "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/webdriver-bidi-protocol": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
+ "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz",
+ "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^8.2.1",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/cli/clients/omp/package.json b/cli/clients/omp/package.json
new file mode 100644
index 0000000..0a629ee
--- /dev/null
+++ b/cli/clients/omp/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "@oh-my-pi/pi-coding-agent": "18.12.6"
+ }
+}
diff --git a/cli/exec.go b/cli/exec.go
index 58242a0..1b15fb4 100644
--- a/cli/exec.go
+++ b/cli/exec.go
@@ -15,8 +15,8 @@ func execClient(p Paths, args []string) error {
fmt.Println("the server or the Flint provider. Arguments go to the harness unchanged.")
return nil
}
- if args[0] != "opencode" && args[0] != "pi" {
- return fmt.Errorf("unsupported harness %q; use rec exec pi or rec exec opencode", args[0])
+ if args[0] != "opencode" && args[0] != "pi" && args[0] != "omp" {
+ return fmt.Errorf("unsupported harness %q; use rec exec pi, rec exec omp, or rec exec opencode", args[0])
}
rest := args[1:]
if args[0] == "opencode" && len(rest) > 0 && (rest[0] == "upgrade" || rest[0] == "uninstall") {
@@ -31,6 +31,9 @@ func execClient(p Paths, args []string) error {
return err
}
h := piUser(user)
+ if args[0] == "omp" {
+ h = ompUser(user)
+ }
if args[0] == "opencode" {
h = openCodeUser(user)
}
diff --git a/cli/help.go b/cli/help.go
index 2bc4474..5524f8a 100644
--- a/cli/help.go
+++ b/cli/help.go
@@ -20,10 +20,10 @@ var developmentOptions = [][2]string{
}
var commands = []commandHelp{
- {"launch", "pi|opencode", "Start the server if needed and open a harness", [][2]string{
+ {"launch", "pi|omp|opencode", "Start the server if needed and open a harness", [][2]string{
{"--args", "Print the harness command instead of running it; needs a running server"},
}},
- {"exec", "pi|opencode ARGS", "Run a harness command, such as installing a plugin", nil},
+ {"exec", "pi|omp|opencode ARGS", "Run a harness command, such as installing a plugin", nil},
{"serve", "[MODEL]", "Start the model server in this terminal", append([][2]string{
{"--context TOKENS", "Total input and output tokens (default: set by the release)"},
{"--port PORT", "Local port (default: 11500)"},
diff --git a/cli/launch.go b/cli/launch.go
index 57b6e59..b19b1a3 100644
--- a/cli/launch.go
+++ b/cli/launch.go
@@ -29,11 +29,11 @@ func launch(p Paths, args []string) error {
printUsage(os.Stdout, "launch")
fmt.Println("\nArguments after -- go to the harness. Reuses a running server, which keeps")
fmt.Println("running after the harness exits; run rec stop to stop it. Pi requires Node.js")
- fmt.Println("and npm. The first launch installs the pinned harness.")
+ fmt.Println("and npm; omp also requires Bun. The first launch installs the pinned harness.")
return nil
}
- if args[0] != "opencode" && args[0] != "pi" {
- return fmt.Errorf("unsupported harness %q; use rec launch pi or rec launch opencode", args[0])
+ if args[0] != "opencode" && args[0] != "pi" && args[0] != "omp" {
+ return fmt.Errorf("unsupported harness %q; use rec launch pi, rec launch omp, or rec launch opencode", args[0])
}
rest := args[1:]
if len(rest) > 0 && rest[0] == "--" {
@@ -59,7 +59,7 @@ func launch(p Paths, args []string) error {
if err != nil {
return err
}
- openingClient(map[string]string{"pi": "Pi", "opencode": "OpenCode"}[args[0]], s)
+ openingClient(map[string]string{"pi": "Pi", "omp": "omp", "opencode": "OpenCode"}[args[0]], s)
return syscall.Exec(h.argv[0], h.argv, h.environ(os.Environ()))
}
@@ -87,6 +87,16 @@ func harnessCommand(p Paths, name string, s Status, env []string, key string, re
if err != nil {
return harness{}, err
}
+ if name == "omp" {
+ config, err := ompConfig(p, s, user)
+ if err != nil {
+ return harness{}, err
+ }
+ h := ompUser(user)
+ h.argv = append([]string{ompExecutable(p), "--config", config, "--model", "rec/flint"}, rest...)
+ h.set = append(h.set, [2]string{"REC_API_KEY", key})
+ return h, nil
+ }
if name == "pi" {
extension, config, err := piConfig(p, s)
if err != nil {
@@ -389,6 +399,9 @@ func installClient(p Paths, name string) (string, error) {
if name == "pi" {
return installPi(p, os.Stderr)
}
+ if name == "omp" {
+ return installOmp(p, os.Stderr)
+ }
pin, err := loadOpenCodePin()
if err != nil {
return "", err
diff --git a/cli/omp.go b/cli/omp.go
new file mode 100644
index 0000000..32ab1e6
--- /dev/null
+++ b/cli/omp.go
@@ -0,0 +1,151 @@
+//go:build darwin || linux
+
+package main
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func ompInstallDir(p Paths) string {
+ sum := sha256.Sum256(append(append([]byte{}, ompPackage...), ompLock...))
+ return filepath.Join(p.Data, "clients", "omp", "npm-"+hex.EncodeToString(sum[:8]))
+}
+
+func ompExecutable(p Paths) string {
+ return filepath.Join(ompInstallDir(p), "node_modules", ".bin", "omp")
+}
+
+func installOmp(p Paths, out io.Writer) (string, error) {
+ dir := ompInstallDir(p)
+ parent := filepath.Dir(dir)
+ executable := ompExecutable(p)
+ if _, err := os.Stat(executable); err == nil {
+ return executable, nil
+ }
+ if _, err := exec.LookPath("bun"); err != nil {
+ return "", fmt.Errorf("rec launch omp requires Bun; install Bun, then retry")
+ }
+ npm, err := exec.LookPath("npm")
+ if err != nil {
+ return "", fmt.Errorf("rec launch omp requires Node.js/npm; install Node.js, then retry")
+ }
+ if err := os.MkdirAll(parent, 0700); err != nil {
+ return "", err
+ }
+ l, err := lock(filepath.Join(p.State, "omp-install.lock"))
+ if err != nil {
+ return "", fmt.Errorf("another launcher is installing omp; retry when it finishes")
+ }
+ defer unlock(l)
+ if _, err := os.Stat(executable); err == nil {
+ return executable, nil
+ }
+ // Remove staging left by an interrupted install. Earlier pins are kept,
+ // since a running omp may still load modules from them.
+ leftovers, err := filepath.Glob(filepath.Join(parent, ".install-*"))
+ if err != nil {
+ return "", err
+ }
+ for _, path := range leftovers {
+ if err := os.RemoveAll(path); err != nil {
+ return "", err
+ }
+ }
+ staging, err := os.MkdirTemp(parent, ".install-")
+ if err != nil {
+ return "", err
+ }
+ defer os.RemoveAll(staging)
+ if err := os.WriteFile(filepath.Join(staging, "package.json"), ompPackage, 0600); err != nil {
+ return "", err
+ }
+ if err := os.WriteFile(filepath.Join(staging, "package-lock.json"), ompLock, 0600); err != nil {
+ return "", err
+ }
+ installing := startActivity(out, "Installing omp", "Installed omp")
+ cmd := exec.Command(npm, "ci", "--no-audit", "--no-fund", "--loglevel=error")
+ cmd.Dir = staging
+ if output, err := cmd.CombinedOutput(); err != nil {
+ installing.fail()
+ _, _ = out.Write(output)
+ return "", fmt.Errorf("installing omp with npm ci: %w", err)
+ }
+ installing.succeed()
+ if err := os.Rename(staging, dir); err != nil {
+ return "", err
+ }
+ return executable, nil
+}
+
+func ompConfig(p Paths, s Status, user string) (string, error) {
+ if err := validateClientStatus(s); err != nil {
+ return "", err
+ }
+ dir := filepath.Join(p.Data, "clients", "omp", Version)
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ return "", err
+ }
+ if err := os.MkdirAll(user, 0700); err != nil {
+ return "", err
+ }
+ models := fmt.Sprintf(`providers:
+ rec:
+ baseUrl: %s
+ apiKey: REC_API_KEY
+ api: openai-completions
+ models:
+ - id: flint
+ name: Flint
+ reasoning: false
+ input: [text]
+ contextWindow: %d
+ maxTokens: %d
+ compat:
+ supportsStore: false
+ supportsDeveloperRole: false
+ supportsReasoningEffort: false
+ maxTokensField: max_tokens
+`, s.Endpoint, s.Context, s.Context)
+ if err := atomicWrite(filepath.Join(user, "models.yml"), []byte(models), 0600); err != nil {
+ return "", err
+ }
+ settings := `modelRoles:
+ default: rec/flint
+ smol: rec/flint
+ slow: rec/flint
+ plan: rec/flint
+ task: rec/flint
+ advisor: rec/flint
+ tiny: rec/flint
+retry:
+ modelFallback: false
+ fallbackChains:
+ default: []
+ smol: []
+ slow: []
+ plan: []
+ task: []
+ advisor: []
+ tiny: []
+enabledProviders:
+ - rec
+`
+ path := filepath.Join(dir, "rec-omp.yml")
+ if err := atomicWrite(path, []byte(settings), 0600); err != nil {
+ return "", err
+ }
+ return path, nil
+}
+
+func ompUser(user string) harness {
+ return harness{
+ unset: []string{"PI_CODING_AGENT_SESSION_DIR"},
+ set: [][2]string{{"PI_CODING_AGENT_DIR", user}},
+ }
+}
diff --git a/cli/omp_test.go b/cli/omp_test.go
new file mode 100644
index 0000000..75fbd48
--- /dev/null
+++ b/cli/omp_test.go
@@ -0,0 +1,180 @@
+//go:build darwin || linux
+
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestOmpConfig(t *testing.T) {
+ p := Paths{Data: t.TempDir()}
+ user := filepath.Join(p.Data, "clients", "omp", "user")
+ s := Status{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"}
+ path, err := ompConfig(p, s, user)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if path != ompConfigPath(p) {
+ t.Fatalf("wrong overlay path: %s", path)
+ }
+ settings, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ models, err := os.ReadFile(filepath.Join(user, "models.yml"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, want := range []string{
+ "default: rec/flint",
+ "modelFallback: false",
+ "enabledProviders:\n - rec",
+ } {
+ if !strings.Contains(string(settings), want) {
+ t.Fatalf("omp settings missing %q:\n%s", want, settings)
+ }
+ }
+ for _, want := range []string{
+ "baseUrl: http://127.0.0.1:11500/v1",
+ "apiKey: REC_API_KEY",
+ "api: openai-completions",
+ "contextWindow: 8192",
+ "maxTokens: 8192",
+ "maxTokensField: max_tokens",
+ } {
+ if !strings.Contains(string(models), want) {
+ t.Fatalf("omp models.yml missing %q:\n%s", want, models)
+ }
+ }
+ if strings.Contains(string(settings)+string(models), "openrouter") {
+ t.Fatal("omp config must not reference outside providers")
+ }
+}
+
+func TestOmpEnvUsesRecUserDirectory(t *testing.T) {
+ p := Paths{Data: t.TempDir()}
+ h, err := harnessCommand(p, "omp", Status{Ready: true, Model: "flint", Context: 8192, Endpoint: "http://127.0.0.1:11500/v1"}, nil, "secret", []string{"-p", "hi"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := h.argv[0]; got != ompExecutable(p) {
+ t.Fatalf("wrong omp executable: %s", got)
+ }
+ if !reflect.DeepEqual(h.argv[1:4], []string{"--config", ompConfigPath(p), "--model"}) {
+ t.Fatalf("wrong omp argv prefix: %q", h.argv)
+ }
+ if h.argv[4] != "rec/flint" || !reflect.DeepEqual(h.argv[5:], []string{"-p", "hi"}) {
+ t.Fatalf("wrong omp model/args: %q", h.argv)
+ }
+ got := h.environ([]string{"PATH=/bin", "REC_API_KEY=old", "PI_CODING_AGENT_DIR=/custom", "PI_CODING_AGENT_SESSION_DIR=/sessions"})
+ want := []string{"PATH=/bin", "PI_CODING_AGENT_DIR=" + filepath.Join(p.Data, "clients", "omp", "user"), "REC_API_KEY=secret"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("incorrect environment merge: %q", got)
+ }
+}
+
+func ompConfigPath(p Paths) string {
+ return filepath.Join(p.Data, "clients", "omp", Version, "rec-omp.yml")
+}
+
+func TestOmpPinIsLocked(t *testing.T) {
+ var pkg struct{ Dependencies map[string]string }
+ var lock struct {
+ Packages map[string]struct {
+ Version string
+ Dependencies map[string]string
+ }
+ }
+ if err := json.Unmarshal(ompPackage, &pkg); err != nil {
+ t.Fatal(err)
+ }
+ if err := json.Unmarshal(ompLock, &lock); err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(lock.Packages[""].Dependencies, pkg.Dependencies) {
+ t.Fatal("package-lock.json is stale; run npm install --package-lock-only in cli/clients/omp")
+ }
+ for name, version := range pkg.Dependencies {
+ if lock.Packages["node_modules/"+name].Version != version {
+ t.Fatalf("%s must be pinned to an exact locked version", name)
+ }
+ }
+}
+
+func TestInstallOmp(t *testing.T) {
+ bin := t.TempDir()
+ calls := filepath.Join(t.TempDir(), "calls")
+ script := "#!/bin/sh\necho \"$*\" >> " + calls + "\n" +
+ "cmp -s package.json \"$OMP_PACKAGE\" && cmp -s package-lock.json \"$OMP_LOCK\" || exit 3\n" +
+ "[ -z \"$FAIL\" ] || exit 4\n" +
+ "mkdir -p node_modules/.bin && touch node_modules/.bin/omp\n"
+ for _, name := range []string{"npm", "bun"} {
+ if err := os.WriteFile(filepath.Join(bin, name), []byte(script), 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ fixtures := t.TempDir()
+ for name, data := range map[string][]byte{"OMP_PACKAGE": ompPackage, "OMP_LOCK": ompLock} {
+ path := filepath.Join(fixtures, name)
+ if err := os.WriteFile(path, data, 0600); err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv(name, path)
+ }
+ t.Setenv("PATH", bin+":/usr/bin:/bin")
+ p := dataPaths(t.TempDir())
+ if err := os.MkdirAll(p.State, 0700); err != nil {
+ t.Fatal(err)
+ }
+ parent := filepath.Join(p.Data, "clients", "omp")
+ earlier := filepath.Join(parent, "npm-0000000000000000")
+ interrupted := filepath.Join(parent, ".install-interrupted")
+ for _, dir := range []string{earlier, interrupted} {
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ t.Setenv("FAIL", "1")
+ if _, err := installOmp(p, io.Discard); err == nil {
+ t.Fatal("failed npm ci was accepted")
+ }
+ entries, err := os.ReadDir(parent)
+ if err != nil || len(entries) != 1 || entries[0].Name() != filepath.Base(earlier) {
+ t.Fatalf("want only the earlier install after a failed install, got %v %v", entries, err)
+ }
+
+ t.Setenv("FAIL", "")
+ first, err := installOmp(p, io.Discard)
+ if err != nil {
+ t.Fatal(err)
+ }
+ second, err := installOmp(p, io.Discard)
+ if err != nil || second != first {
+ t.Fatalf("reinstall: %v %s %s", err, first, second)
+ }
+ if _, err := os.Stat(earlier); err != nil {
+ t.Fatal("an earlier install may still be running and must be kept")
+ }
+ data, err := os.ReadFile(calls)
+ if err != nil || string(data) != "ci --no-audit --no-fund --loglevel=error\nci --no-audit --no-fund --loglevel=error\n" {
+ t.Fatalf("want one failed and one successful npm ci, got %q", data)
+ }
+}
+
+func TestInstallOmpRequiresBun(t *testing.T) {
+ t.Setenv("PATH", t.TempDir()+":/usr/bin:/bin")
+ p := dataPaths(t.TempDir())
+ if err := os.MkdirAll(p.State, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := installOmp(p, io.Discard); err == nil || !strings.Contains(err.Error(), "Bun") {
+ t.Fatalf("want a Bun requirement error, got %v", err)
+ }
+}
diff --git a/engine/pyproject.toml b/engine/pyproject.toml
index 59b1dde..b359f5f 100644
--- a/engine/pyproject.toml
+++ b/engine/pyproject.toml
@@ -1,5 +1,5 @@
[build-system]
-requires = ["hatchling>=1.27,<2", "mlx==0.32.0", "nanobind==2.13.0"]
+requires = ["hatchling>=1.27,<2", "mlx==0.32.4", "nanobind==3.1.0"]
build-backend = "hatchling.build"
[project]
@@ -8,14 +8,14 @@ version = "0.1.0"
description = "Flint inference engine for Apple Silicon"
requires-python = ">=3.12"
dependencies = [
- "mlx==0.32.0",
+ "mlx==0.32.4",
"mlx-lm==0.31.3",
"transformers==5.16.1",
"xgrammar==0.2.1",
]
[project.optional-dependencies]
-validation = ["pytest==8.4.2"]
+validation = ["pytest==8.4.2", "pytest-xdist==3.8.0"]
[project.scripts]
rec-mlx = "rec_mlx.cli:main"
@@ -26,6 +26,7 @@ packages = ["src/rec_mlx"]
[tool.ruff]
extend = "../ruff.toml"
extend-exclude = ["src/rec_mlx/kernels"]
+src = ["src", "tests"]
[tool.pytest.ini_options]
testpaths = ["tests"]
diff --git a/engine/src/rec_mlx/constants.py b/engine/src/rec_mlx/constants.py
new file mode 100644
index 0000000..7ef183a
--- /dev/null
+++ b/engine/src/rec_mlx/constants.py
@@ -0,0 +1,14 @@
+# Thinking budget.
+THINK_OPEN = ""
+THINK_CLOSE = ""
+THINKING_BUDGET_FIELD = "thinking_budget"
+# A forced token has probability 1 in the constrained process.
+FORCED_TOKEN_LOGPROB = 0.0
+
+# Engine metric keys read for usage when thinking_budget is set.
+METRIC_REASONING_TOKENS = "reasoning_tokens"
+METRIC_FORCED_CLOSE = "thinking_forced_close"
+
+# Usage fields added to chat completions that set thinking_budget.
+USAGE_REASONING_TOKENS = "reasoning_tokens"
+USAGE_FORCED_CLOSE = "thinking_forced_close"
diff --git a/engine/src/rec_mlx/engine.py b/engine/src/rec_mlx/engine.py
index 6ba06f1..a7a2b54 100644
--- a/engine/src/rec_mlx/engine.py
+++ b/engine/src/rec_mlx/engine.py
@@ -11,8 +11,10 @@
from .chat import load_tokenizer, normalize_messages
from .checkpoint import validate
+from .constants import FORCED_TOKEN_LOGPROB
from .detokenize import StreamingText
from .kernels.device import profile_for_device
+from .thinking import ThinkBudget, resolve_think_ids
class ContextLimitError(ValueError):
@@ -77,6 +79,11 @@ def __init__(
self._prefix_preparation = None
self.mx = mx
self.tokenizer = load_tokenizer(directory)
+ try:
+ self.think_ids = resolve_think_ids(self.tokenizer)
+ except ValueError:
+ # Without / the engine serves requests that carry no thinking budget.
+ self.think_ids = None
self._constraints = None
self.enforce_tool_schemas = enforce_tool_schemas
self._constraints_lock = threading.Lock()
@@ -110,6 +117,13 @@ def __init__(
"kv_format": self.identity["kv_format"],
}
+ def validate_thinking_budget(self, ids, budget):
+ if self.think_ids is None:
+ raise ValueError("thinking_budget requires and tokenizer tokens")
+ opens = [i for i, t in enumerate(ids) if t == self.think_ids[0]]
+ if opens and self.think_ids[1] not in ids[opens[-1] + 1 :]:
+ raise ValueError("thinking_budget requires a prompt without an unclosed ")
+
@property
def constraints(self):
with self._constraints_lock:
@@ -149,6 +163,7 @@ def generate_tokens(
should_stop=lambda: False,
grammar=None,
return_logprobs=False,
+ thinking_budget=None,
):
# Metal streams are thread-local; HTTP handlers run outside the model-loading thread.
with self.mx.stream(self.mx.new_stream(self.mx.gpu)):
@@ -162,6 +177,7 @@ def generate_tokens(
should_stop=should_stop,
grammar=constraint,
return_logprobs=return_logprobs,
+ thinking_budget=thinking_budget,
)
except PrefillCancelled:
self.mx.synchronize()
@@ -221,7 +237,21 @@ def _take_prefix(self, ids):
prefix = prepared
return prefix
- def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_logprobs=False):
+ def _generate_tokens(
+ self,
+ ids,
+ sampling,
+ *,
+ should_stop,
+ grammar=None,
+ return_logprobs=False,
+ thinking_budget=None,
+ ):
+ if thinking_budget is not None and grammar is None:
+ raise ValueError("thinking_budget requires the flint grammar")
+ if thinking_budget is not None:
+ # The server validates before any response header; this is the backstop.
+ self.validate_thinking_budget(ids, thinking_budget)
if len(ids) + sampling.max_tokens > self.context_limit:
raise ContextLimitError(
"Prompt plus output allowance exceeds the configured context limit"
@@ -273,6 +303,8 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l
top_p=sampling.top_p,
top_k=sampling.top_k,
)
+
+ think = ThinkBudget(thinking_budget, *(self.think_ids or (None, None)))
prefill_end = time.perf_counter()
prefill_peak = mx.get_peak_memory()
first = None
@@ -282,9 +314,32 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l
if should_stop():
reason = "stop"
break
- if index:
+ forced = think.forced_token() if index else None
+ if forced is not None:
+ # The budget caps every packet, so a forced close always starts a new one.
+ # The last reasoning token is returned but unfed; feed it before .
+ # feed_only runs one forward pass: no sampling, grammar, presence or RNG
+ # change, and the packet seed schedule (seed + index) is untouched.
+ self.resident_sampler.block(
+ session._state,
+ generated[-1],
+ 1,
+ sampling.seed + index,
+ feed_only=True,
+ presence_penalty=sampling.presence_penalty,
+ )
+ token = forced
+ grammar.accept_first(token, session._state)
+ if sampling.presence_penalty:
+ session._state["presence_mask"][token] = 1.0
+ if return_logprobs:
+ token_logprobs.append(FORCED_TOKEN_LOGPROB)
+ computed_tokens += 1
+ elif index:
if not packet:
count = min(8, sampling.max_tokens - index)
+ if think.remaining() is not None:
+ count = min(count, think.remaining())
options = {"logprobs": token_logprobs} if return_logprobs else {}
packet = self.resident_sampler.block(
session._state,
@@ -306,6 +361,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l
flat = logits.reshape(-1).astype(mx.float32)
if grammar is not None:
flat = grammar.first_logits(flat)
+ flat = think.mask_first_logits(flat)
if sampling.temperature == 0:
token = int(mx.argmax(flat).item())
elif unfiltered_t1:
@@ -326,6 +382,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l
if first is None:
first = time.perf_counter()
generated.append(token)
+ think.accept(token)
details = {"logprob": token_logprobs[index]} if return_logprobs else {}
if token in stops:
reason = "stop"
@@ -386,6 +443,7 @@ def _generate_tokens(self, ids, sampling, *, should_stop, grammar=None, return_l
"process_peak_rss_bytes": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss,
"computed_tokens": computed_tokens,
"decode_strategy": "resident_sampling",
+ **think.report(),
**(grammar.metrics() if grammar is not None else {}),
},
}
diff --git a/engine/src/rec_mlx/grammar_builder.py b/engine/src/rec_mlx/grammar_builder.py
index 6c25f93..a56159b 100644
--- a/engine/src/rec_mlx/grammar_builder.py
+++ b/engine/src/rec_mlx/grammar_builder.py
@@ -14,6 +14,8 @@
TokenFormat,
)
+from .token_ids import dedicated_token_id
+
_PROTOCOL_MARKERS = (
"",
"",
@@ -41,7 +43,8 @@ def build_flint_glm_grammar(
enforce_tool_schemas: bool = False,
) -> StructuralTag:
ids = {
- marker: _token_id(tokenizer, marker) for marker in (*_STOP_MARKERS, "", "")
+ marker: dedicated_token_id(tokenizer, marker)
+ for marker in (*_STOP_MARKERS, "", "")
}
if tokenizer.eos_token_id != ids["<|end_of_text|>"]:
raise ValueError("Flint grammar requires end-of-text as the tokenizer EOS")
@@ -80,14 +83,3 @@ def build_flint_glm_grammar(
]
)
)
-
-
-def _token_id(tokenizer: PreTrainedTokenizerBase, marker: str) -> int:
- token_id = tokenizer.convert_tokens_to_ids(marker)
- if (
- not isinstance(token_id, int)
- or tokenizer.convert_ids_to_tokens(token_id) != marker
- or tokenizer.encode(marker, add_special_tokens=False) != [token_id]
- ):
- raise ValueError(f"Flint grammar requires {marker!r} to be a dedicated vocabulary token")
- return token_id
diff --git a/engine/src/rec_mlx/kernels/masked_sampling.py b/engine/src/rec_mlx/kernels/masked_sampling.py
index daa9370..d1aca47 100644
--- a/engine/src/rec_mlx/kernels/masked_sampling.py
+++ b/engine/src/rec_mlx/kernels/masked_sampling.py
@@ -205,7 +205,8 @@ def block(
if logprobs is not None:
logprobs.extend(out[7 if presence_penalty else 6][:produced].tolist())
# Restore the exact scalar sampler's key when EOS truncates the packet.
- mx.random.state[0] = out[5][produced - 1]
+ hi, lo = out[5][produced - 1].tolist()
+ mx.random.seed((int(hi) << 32) | int(lo))
if presence_penalty:
state["presence_mask"] = out[6]
state["position"] += produced
diff --git a/engine/src/rec_mlx/kernels/sampling.py b/engine/src/rec_mlx/kernels/sampling.py
index 6a83633..6ca1147 100644
--- a/engine/src/rec_mlx/kernels/sampling.py
+++ b/engine/src/rec_mlx/kernels/sampling.py
@@ -72,8 +72,15 @@ def block(
presence_penalty=0.0,
grammar=None,
logprobs=None,
+ feed_only=False,
):
module, engine = self.module, self.engine
+ if feed_only:
+ # Write `token` into the KV and advance one position without sampling anything.
+ if count != 1:
+ raise ValueError("feed_only feeds exactly one token")
+ engine.forward(state, token)
+ return []
if not 1 <= count <= WIDTH:
raise ValueError("Resident packet must contain between one and eight tokens")
engine.ensure_capacity(state, count)
diff --git a/engine/src/rec_mlx/scheduler.py b/engine/src/rec_mlx/scheduler.py
index 17e5a94..fcc8947 100644
--- a/engine/src/rec_mlx/scheduler.py
+++ b/engine/src/rec_mlx/scheduler.py
@@ -14,6 +14,7 @@
from mlx_lm.tokenizer_utils import NaiveStreamingDetokenizer, TokenizerWrapper
from .chat import parse_tool_call, seeded_tool_parser
+from .constants import THINKING_BUDGET_FIELD
from .engine import Sampling
@@ -168,6 +169,8 @@ def cancelled():
options["grammar"] = request.constraint
if args.logprobs:
options["return_logprobs"] = True
+ if request.thinking_budget is not None:
+ options[THINKING_BUDGET_FIELD] = request.thinking_budget
raw = self.engine.generate_tokens(
ids,
sampling,
@@ -186,6 +189,7 @@ def responses(self, raw, ctx, sm, sampling, cancelled, logprobs=False):
for event in raw:
if "metrics" in event:
ctx.prompt_cache_count = event["metrics"]["cached_prompt_tokens"]
+ ctx.metrics = event["metrics"]
self.record(
sampling,
"cancelled" if cancelled() else "complete",
diff --git a/engine/src/rec_mlx/server.py b/engine/src/rec_mlx/server.py
index 4b81ab0..5c0b2eb 100644
--- a/engine/src/rec_mlx/server.py
+++ b/engine/src/rec_mlx/server.py
@@ -15,6 +15,13 @@
chat_to_message,
messages_to_chat,
)
+from .constants import (
+ METRIC_FORCED_CLOSE,
+ METRIC_REASONING_TOKENS,
+ THINKING_BUDGET_FIELD,
+ USAGE_FORCED_CLOSE,
+ USAGE_REASONING_TOKENS,
+)
from .engine import Sampling
@@ -26,6 +33,17 @@ class Server(ThreadingHTTPServer):
# The default backlog of 5 resets a burst of connections before the accept loop takes them.
request_queue_size = 32
+class MissingMetricsError(RuntimeError):
+ pass
+
+
+def reject_messages_budget(body):
+ """/v1/messages has no thinking budget; refuse the field instead of dropping it."""
+ if THINKING_BUDGET_FIELD in body:
+ raise ValueError(
+ f"{THINKING_BUDGET_FIELD} is not supported on /v1/messages; use /v1/chat/completions"
+ )
+
def make_server(
engine, address, metrics_path, *, defaults=Sampling(), api_key=None, grammar_mode="flint"
@@ -118,6 +136,14 @@ def do_POST(self):
super().do_POST()
except RequestError as error:
self._reject(400, str(error))
+ except MissingMetricsError as error:
+ if self.stream:
+ # Headers are already sent; report the failure inside the stream.
+ self.wfile.write(f"data: {json.dumps({'error': str(error)})}\n\n".encode())
+ else:
+ # mlx-lm buffered a 200 status line before building the response.
+ self._headers_buffer = []
+ self._reject(500, str(error))
except (BrokenPipeError, ConnectionResetError):
logging.info("Client disconnected; cancelling its generation")
finally:
@@ -127,6 +153,7 @@ def _prepare_messages(self):
length = int(self.headers["Content-Length"])
try:
body = json.loads(self.rfile.read(length).decode())
+ reject_messages_budget(body)
chat, ignored, include_thinking = messages_to_chat(body)
except (json.JSONDecodeError, UnicodeDecodeError) as error:
raise RequestError(f"Invalid JSON in request body: {error}") from error
@@ -162,8 +189,36 @@ def _set_completion_headers(self, status):
self.completion_status = status
super()._set_completion_headers(status)
+ def usage_extras(self):
+ # Usage keys exist only when the request set thinking_budget (0 counts).
+ if self.current_request.thinking_budget is None:
+ return {}
+ metrics = getattr(self.current_request.generation_context, "metrics", None)
+ if metrics is None:
+ raise MissingMetricsError(
+ "Generation ended without a metrics event; usage is unavailable"
+ )
+ missing = [
+ key for key in (METRIC_REASONING_TOKENS, METRIC_FORCED_CLOSE) if key not in metrics
+ ]
+ if missing:
+ raise MissingMetricsError(
+ f"thinking_budget was set but metrics lack {missing}; usage is unavailable"
+ )
+ return {
+ USAGE_REASONING_TOKENS: metrics[METRIC_REASONING_TOKENS],
+ USAGE_FORCED_CLOSE: metrics[METRIC_FORCED_CLOSE],
+ }
+
+ def completion_usage_response(self, *args, **kwargs):
+ response = super().completion_usage_response(*args, **kwargs)
+ response["usage"].update(self.usage_extras())
+ return response
+
def generate_response(self, *args, **kwargs):
response = super().generate_response(*args, **kwargs)
+ if "usage" in response:
+ response["usage"].update(self.usage_extras())
for choice in response["choices"]:
if "logprobs" in choice:
for entry in choice["logprobs"]["content"]:
@@ -209,6 +264,7 @@ def drop_unknown_fields(self):
"store",
"grammar",
"parallel_tool_calls",
+ THINKING_BUDGET_FIELD,
}
unknown = set(self.body) - allowed
for field in unknown:
@@ -234,6 +290,9 @@ def validate_request(self):
super().validate_model_parameters()
if self.stream and self.logprobs:
raise ValueError("Token logprobs require stream=false")
+ budget = self.body.get(THINKING_BUDGET_FIELD)
+ if THINKING_BUDGET_FIELD in self.body and (type(budget) is not int or budget < 0):
+ raise ValueError(f"{THINKING_BUDGET_FIELD} must be a nonnegative integer")
if self.requested_model != "flint":
raise ValueError("The served model is flint")
for key in ("frequency_penalty", "min_p"):
@@ -260,6 +319,8 @@ def validate_request(self):
mode = self.body["grammar"] if "grammar" in self.body else grammar_mode
if mode not in ("none", "flint"):
raise ValueError("grammar must be none or flint")
+ if budget is not None and mode != "flint":
+ raise ValueError(f"{THINKING_BUDGET_FIELD} requires grammar=flint")
if mode == "flint" and self.body.get("stop"):
raise ValueError("Flint grammar cannot be combined with custom stop strings")
if (
@@ -307,6 +368,8 @@ def handle_completion(self, request, stop_words):
self.max_tokens = (
remaining if explicit_maximum is None else min(self.max_tokens, remaining)
)
+ if self.body.get(THINKING_BUDGET_FIELD) is not None:
+ engine.validate_thinking_budget(ids, self.body[THINKING_BUDGET_FIELD])
Sampling(
temperature=self.temperature,
top_p=self.top_p,
@@ -331,6 +394,8 @@ def handle_completion(self, request, stop_words):
else:
self.wfile = ErrorTranslator(self.wfile, status)
request.prompt_tokens = ids
+ self.current_request = request
+ request.thinking_budget = self.body.get(THINKING_BUDGET_FIELD)
request.is_disconnected = self.is_disconnected
request.generation_context = None
try:
diff --git a/engine/src/rec_mlx/thinking.py b/engine/src/rec_mlx/thinking.py
new file mode 100644
index 0000000..51cc4ba
--- /dev/null
+++ b/engine/src/rec_mlx/thinking.py
@@ -0,0 +1,60 @@
+from .constants import METRIC_FORCED_CLOSE, METRIC_REASONING_TOKENS, THINK_CLOSE, THINK_OPEN
+from .token_ids import dedicated_token_id
+
+
+def resolve_think_ids(tokenizer):
+ return dedicated_token_id(tokenizer, THINK_OPEN), dedicated_token_id(tokenizer, THINK_CLOSE)
+
+
+class ThinkBudget:
+ """Counts tokens between and and says when must be forced.
+
+ The engine feeds every emitted token (sampled or forced) to accept(). A budget of None
+ counts but never forces; a budget of 0 only masks at the first position.
+ """
+
+ def __init__(self, budget, think_id, close_id):
+ if budget is not None and (type(budget) is not int or budget < 0):
+ raise ValueError("thinking budget must be None or a nonnegative integer")
+ self.budget = budget
+ self.think_id = think_id
+ self.close_id = close_id
+ self.reasoning_tokens = 0
+ self.forced_close = False
+ self.inside_think = False
+ self._opened = False
+
+ def remaining(self):
+ """Tokens that may still be sampled before the close is forced; None = unlimited."""
+ if self.budget is None or not self.inside_think:
+ return None
+ left = self.budget - self.reasoning_tokens
+ if left < 0:
+ raise RuntimeError("The thinking budget was exceeded without a forced close")
+ return left
+
+ def forced_token(self):
+ return self.close_id if self.remaining() == 0 else None
+
+ def accept(self, token):
+ if self.inside_think:
+ if token == self.close_id:
+ self.forced_close = self.forced_token() is not None
+ self.inside_think = False
+ else:
+ self.reasoning_tokens += 1
+ elif token == self.think_id and not self._opened:
+ self._opened = self.inside_think = True
+
+ def report(self):
+ return {
+ METRIC_REASONING_TOKENS: self.reasoning_tokens,
+ METRIC_FORCED_CLOSE: self.forced_close,
+ }
+
+ def mask_first_logits(self, logits):
+ if self.budget != 0:
+ return logits
+ import mlx.core as mx
+
+ return mx.where(mx.arange(logits.shape[-1]) == self.think_id, -float("inf"), logits)
diff --git a/engine/src/rec_mlx/token_ids.py b/engine/src/rec_mlx/token_ids.py
new file mode 100644
index 0000000..5dc9225
--- /dev/null
+++ b/engine/src/rec_mlx/token_ids.py
@@ -0,0 +1,9 @@
+def dedicated_token_id(tokenizer, marker: str) -> int:
+ token_id = tokenizer.convert_tokens_to_ids(marker)
+ if (
+ not isinstance(token_id, int)
+ or tokenizer.convert_ids_to_tokens(token_id) != marker
+ or tokenizer.encode(marker, add_special_tokens=False) != [token_id]
+ ):
+ raise ValueError(f"The tokenizer must define {marker!r} as a dedicated vocabulary token")
+ return token_id
diff --git a/engine/tests/conftest.py b/engine/tests/conftest.py
new file mode 100644
index 0000000..042268c
--- /dev/null
+++ b/engine/tests/conftest.py
@@ -0,0 +1,10 @@
+import os
+
+import pytest
+
+from parallel_workers import auto_worker_count, available_cores
+
+
+@pytest.hookimpl(optionalhook=True)
+def pytest_xdist_auto_num_workers(config):
+ return auto_worker_count(os.environ, available_cores())
diff --git a/engine/tests/e2e/conftest.py b/engine/tests/e2e/conftest.py
new file mode 100644
index 0000000..511d9a1
--- /dev/null
+++ b/engine/tests/e2e/conftest.py
@@ -0,0 +1,20 @@
+"""Shared session-scoped (one resident engine for all e2e modules) fixtures for the real-weights e2e tests."""
+
+import pytest
+from e2e_config import CONTEXT_LIMIT, MANIFEST_NAME, MODEL_DIR
+
+from rec_mlx import thinking
+
+
+@pytest.fixture(scope="session")
+def engine():
+ if MODEL_DIR is None:
+ pytest.skip("REC_E2E_MODEL_DIR is not set (flint-int4 weights unavailable)")
+ from rec_mlx.engine import Engine
+
+ return Engine(MODEL_DIR, MODEL_DIR / MANIFEST_NAME, context_limit=CONTEXT_LIMIT)
+
+
+@pytest.fixture(scope="session")
+def think(engine):
+ return thinking.resolve_think_ids(engine.tokenizer)
diff --git a/engine/tests/e2e/e2e_config.py b/engine/tests/e2e/e2e_config.py
new file mode 100644
index 0000000..8df1345
--- /dev/null
+++ b/engine/tests/e2e/e2e_config.py
@@ -0,0 +1,43 @@
+"""Shared constants and skip rule for the real-weights e2e tests (no test module imports)."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+from pathlib import Path
+
+MODEL_DIR = Path(os.environ["REC_E2E_MODEL_DIR"]) if os.environ.get("REC_E2E_MODEL_DIR") else None
+MANIFEST_NAME = "flint-int4.json"
+CONTEXT_LIMIT = 16_384
+MIN_AVAILABLE_BYTES = 4 * 1024**3 # free + inactive + speculative + purgeable pages
+AVAILABLE_PAGE_KINDS = ("Pages free", "Pages inactive", "Pages speculative", "Pages purgeable")
+
+
+def _vm_available_bytes() -> int:
+ out = subprocess.check_output(["vm_stat"], text=True)
+ page = int(re.search(r"page size of (\d+) bytes", out).group(1))
+ total = 0
+ for kind in AVAILABLE_PAGE_KINDS:
+ total += int(re.search(rf"{kind}:\s+(\d+)\.", out).group(1))
+ return total * page
+
+
+def _skip_reason() -> str | None:
+ if MODEL_DIR is None:
+ return "REC_E2E_MODEL_DIR is not set (flint-int4 weights unavailable)"
+ if not (MODEL_DIR / MANIFEST_NAME).exists():
+ return f"{MODEL_DIR / MANIFEST_NAME} not found (flint-int4 weights unavailable)"
+ try:
+ import mlx.core as mx
+ except ImportError:
+ return "mlx is not installed"
+ if not mx.metal.is_available():
+ return "no Apple GPU (Metal) available"
+ available = _vm_available_bytes()
+ if available < MIN_AVAILABLE_BYTES:
+ return f"memory preflight: vm_stat available {available} B < {MIN_AVAILABLE_BYTES} B"
+ return None
+
+
+SKIP_REASON = _skip_reason()
diff --git a/engine/tests/e2e/test_thinking_budget_e2e.py b/engine/tests/e2e/test_thinking_budget_e2e.py
new file mode 100644
index 0000000..05afbcf
--- /dev/null
+++ b/engine/tests/e2e/test_thinking_budget_e2e.py
@@ -0,0 +1,126 @@
+"""End-to-end thinking budget tests on the real Flint weights and an Apple GPU.
+
+They skip, with a reason, when the weights or the GPU are unavailable or free memory is low:
+
+ REC_E2E_MODEL_DIR=/path/to/flint-int4 python -m pytest tests/e2e
+
+The tests call Engine.generate_tokens directly, so no server or port is involved.
+"""
+
+from __future__ import annotations
+
+import numpy as np
+import pytest
+from e2e_config import SKIP_REASON
+
+PARITY_BUDGET = 100_000 # larger than any generation here: the model must close on its own
+PARITY_MAX_TOKENS = 384
+FORCED_BUDGET = 20
+PROBE_MAX_TOKENS = 1024 # baseline run proving the prompt reasons past FORCED_BUDGET
+TAIL = 24 # tokens generated after the forced close
+FOLLOW_UP_TOKENS = 24
+PLAIN_PROMPTS = [
+ "What is 17 times 23? Answer with the number.",
+ "In one sentence, what does the Unix command `ls -la` do?",
+]
+REASONING_PROMPT = (
+ "A farmer has 17 sheep. All but 9 run away, then he buys twice as many as remain. "
+ "How many sheep does he have? Think carefully and show the arithmetic."
+)
+
+pytestmark = pytest.mark.skipif(SKIP_REASON is not None, reason=SKIP_REASON or "")
+
+
+def _user(text):
+ return [{"role": "user", "content": text}]
+
+
+def _run(engine, text, *, max_tokens, **options):
+ """Greedy generation through Engine.generate_tokens; returns (prompt ids, token ids, metrics)."""
+ from rec_mlx.engine import Sampling
+
+ ids = engine.prompt(_user(text))
+ events = list(
+ engine.generate_tokens(
+ ids,
+ Sampling(temperature=0.0, top_p=1.0, top_k=0, max_tokens=max_tokens, seed=0),
+ grammar=engine.constraints.tools(None),
+ **options,
+ )
+ )
+ metrics = next(e["metrics"] for e in events if "metrics" in e)
+ return ids, list(metrics["token_ids"]), metrics
+
+
+def _cold_logits(engine, ids):
+ """Last-position logits of a cold prefill (no prefix) as float32 numpy."""
+ mx = engine.mx
+ with mx.stream(mx.new_stream(mx.gpu)):
+ logits = engine.runtime.make_session(max_new_tokens=1).prefill(ids, prefix=None)
+ mx.eval(logits)
+ out = np.array(logits.reshape(-1).astype(mx.float32))
+ mx.synchronize()
+ return out
+
+
+def test_large_budget_is_token_identical_to_no_budget(engine):
+ for prompt in PLAIN_PROMPTS:
+ _, base, _ = _run(engine, prompt, max_tokens=PARITY_MAX_TOKENS)
+ _, budgeted, metrics = _run(
+ engine, prompt, max_tokens=PARITY_MAX_TOKENS, thinking_budget=PARITY_BUDGET
+ )
+ assert budgeted == base
+ assert metrics["thinking_forced_close"] is False
+
+
+def test_forced_close_continues_exactly_as_a_cold_model_would(engine, think):
+ think_id, close_id = think
+ _, base, _ = _run(engine, REASONING_PROMPT, max_tokens=PROBE_MAX_TOKENS)
+ assert base[0] == think_id
+ assert close_id not in base[: 2 + FORCED_BUDGET], "prompt must reason past the budget"
+ ids, tokens, metrics = _run(
+ engine, REASONING_PROMPT, max_tokens=1 + FORCED_BUDGET + 1 + TAIL,
+ thinking_budget=FORCED_BUDGET,
+ ) # fmt: skip
+ assert metrics["reasoning_tokens"] == FORCED_BUDGET
+ assert metrics["thinking_forced_close"] is True
+ assert metrics["computed_tokens"] == metrics["generated_tokens"]
+ close_at = tokens.index(close_id)
+ assert close_at == 1 + FORCED_BUDGET and tokens[close_at + 1 :]
+ # The KV written around the forced close must predict what a cold prefill predicts.
+ for end in range(close_at, len(tokens) - 1):
+ logits = _cold_logits(engine, ids + tokens[: end + 1])
+ assert int(np.argmax(logits)) == tokens[end + 1], f"divergence after position {end}"
+
+
+def test_budget_zero_has_no_think_token(engine, think):
+ _, tokens, metrics = _run(engine, REASONING_PROMPT, max_tokens=64, thinking_budget=0)
+ assert think[0] not in tokens
+ assert metrics["reasoning_tokens"] == 0 and metrics["thinking_forced_close"] is False
+
+
+def test_follow_up_reusing_the_prefix_equals_a_cold_run(engine, think):
+ """A follow-up turn that reuses the KV of a forced-close answer decodes as a cold run does."""
+ from rec_mlx.engine import Sampling
+
+ ids, tokens, metrics = _run(
+ engine, REASONING_PROMPT, max_tokens=1 + FORCED_BUDGET + 1 + TAIL,
+ thinking_budget=FORCED_BUDGET,
+ ) # fmt: skip
+ assert metrics["thinking_forced_close"] is True, "prompt must reason past the budget"
+ engine.prepare_prefix()
+ sampling = Sampling(temperature=0.0, top_p=1.0, top_k=0, max_tokens=FOLLOW_UP_TOKENS, seed=0)
+
+ def follow():
+ events = list(
+ engine.generate_tokens(ids + tokens, sampling, grammar=engine.constraints.tools(None))
+ )
+ return events[-1]["metrics"]
+
+ warm = follow()
+ assert warm["cached_prompt_tokens"] > 0, "the follow-up must reuse the prefix"
+ engine._prompt_cache = None
+ engine._prefix_preparation = None
+ cold = follow()
+ assert cold["cached_prompt_tokens"] == 0
+ assert warm["token_ids"] == cold["token_ids"]
diff --git a/engine/tests/parallel_workers.py b/engine/tests/parallel_workers.py
new file mode 100644
index 0000000..0930559
--- /dev/null
+++ b/engine/tests/parallel_workers.py
@@ -0,0 +1,44 @@
+"""Worker count for `pytest -n auto` that leaves the desktop responsive."""
+
+import os
+import subprocess
+import sys
+
+# Cores left for the OS and apps during `pytest -n auto`.
+RESERVED_CORES = 2
+# Same variable pytest-xdist reads for its own auto count.
+WORKERS_ENV = "PYTEST_XDIST_AUTO_NUM_WORKERS"
+MACOS_PERFORMANCE_CORES_KEY = "hw.perflevel0.logicalcpu"
+MACOS_LOGICAL_CORES_KEY = "hw.logicalcpu"
+
+
+def _sysctl(key):
+ result = subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, check=False)
+ if result.returncode != 0:
+ return None
+ return int(result.stdout.strip())
+
+
+def available_cores():
+ if sys.platform == "darwin":
+ # Efficiency cores are slow for this suite; Intel Macs have no perflevel key.
+ cores = _sysctl(MACOS_PERFORMANCE_CORES_KEY)
+ if cores is None:
+ cores = _sysctl(MACOS_LOGICAL_CORES_KEY)
+ elif sys.platform.startswith("linux"):
+ cores = len(os.sched_getaffinity(0))
+ else:
+ cores = os.cpu_count()
+ if not cores or cores < 1:
+ raise RuntimeError("cannot determine the number of CPU cores")
+ return cores
+
+
+def auto_worker_count(environ, cores):
+ override = environ.get(WORKERS_ENV)
+ if override is not None:
+ count = int(override)
+ if count < 1:
+ raise ValueError(f"{WORKERS_ENV} must be >= 1, got {override!r}")
+ return count
+ return max(1, cores - RESERVED_CORES)
diff --git a/engine/tests/test_parallel_workers.py b/engine/tests/test_parallel_workers.py
new file mode 100644
index 0000000..3e3fc84
--- /dev/null
+++ b/engine/tests/test_parallel_workers.py
@@ -0,0 +1,33 @@
+import os
+import unittest
+
+import pytest
+
+from parallel_workers import RESERVED_CORES, WORKERS_ENV, auto_worker_count, available_cores
+
+
+class AutoWorkerCountTest(unittest.TestCase):
+ def test_leaves_reserved_cores_for_the_desktop(self):
+ self.assertEqual(auto_worker_count({}, RESERVED_CORES + 6), 6)
+
+ def test_never_drops_below_one_worker(self):
+ for cores in range(1, RESERVED_CORES + 1):
+ self.assertEqual(auto_worker_count({}, cores), 1)
+
+ def test_environment_overrides_the_core_count(self):
+ self.assertEqual(auto_worker_count({WORKERS_ENV: "3"}, 64), 3)
+
+ def test_invalid_override_fails_instead_of_being_ignored(self):
+ for value in ("many", "0", "-2"):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ auto_worker_count({WORKERS_ENV: value}, 8)
+
+ def test_available_cores_is_positive(self):
+ self.assertGreaterEqual(available_cores(), 1)
+
+
+def test_xdist_auto_uses_this_worker_count(pytestconfig):
+ if not pytestconfig.pluginmanager.hasplugin("xdist"):
+ pytest.skip("pytest-xdist is not installed")
+ result = pytestconfig.hook.pytest_xdist_auto_num_workers(config=pytestconfig)
+ assert result == auto_worker_count(os.environ, available_cores())
diff --git a/engine/tests/test_thinking_budget.py b/engine/tests/test_thinking_budget.py
new file mode 100644
index 0000000..2496d4a
--- /dev/null
+++ b/engine/tests/test_thinking_budget.py
@@ -0,0 +1,145 @@
+"""ThinkBudget state machine, think-token resolution and the budget-0 first-token mask."""
+
+import numpy as np
+import pytest
+
+from rec_mlx import thinking
+from rec_mlx.token_ids import dedicated_token_id
+
+THINK_ID = 7
+CLOSE_ID = 8
+VOCAB = 12
+TEXT = 3
+
+
+def tb(budget):
+ return thinking.ThinkBudget(budget, THINK_ID, CLOSE_ID)
+
+
+def feed(state, *tokens):
+ for token in tokens:
+ state.accept(token)
+
+
+class DedicatedTokenizer:
+ def __init__(self, vocab):
+ self.vocab = vocab
+
+ def convert_tokens_to_ids(self, token):
+ return self.vocab.get(token)
+
+ def convert_ids_to_tokens(self, idx):
+ return next((k for k, v in self.vocab.items() if v == idx), None)
+
+ def encode(self, text, add_special_tokens=False):
+ return [self.vocab[text]] if text in self.vocab else [101, 102]
+
+
+def test_resolve_think_ids_needs_two_dedicated_tokens():
+ vocab = {"": THINK_ID, "": CLOSE_ID}
+ assert thinking.resolve_think_ids(DedicatedTokenizer(vocab)) == (THINK_ID, CLOSE_ID)
+ for missing in vocab: # a marker that encodes to several ids is not a vocabulary token
+ partial = {k: v for k, v in vocab.items() if k != missing}
+ with pytest.raises(ValueError):
+ thinking.resolve_think_ids(DedicatedTokenizer(partial))
+
+
+@pytest.mark.parametrize("n", [1, 5])
+def test_close_is_required_after_exactly_n_reasoning_tokens(n):
+ s = tb(n)
+ feed(s, THINK_ID) # does not count
+ for i in range(n):
+ assert s.forced_token() is None, f"forced early after {i} tokens"
+ s.accept(TEXT)
+ assert s.forced_token() == CLOSE_ID
+ assert s.forced_token() == CLOSE_ID # a query, not a state change
+ s.accept(CLOSE_ID) # does not count
+ assert (s.reasoning_tokens, s.forced_close, s.inside_think) == (n, True, False)
+ assert s.forced_token() is None
+
+
+def test_tokens_outside_think_are_neither_counted_nor_forced():
+ cases = { # tokens -> (reasoning_tokens, forced_close)
+ "natural close": ((THINK_ID, TEXT, 4, CLOSE_ID), (2, False)),
+ "empty reasoning": ((THINK_ID, CLOSE_ID), (0, False)),
+ "after close": ((THINK_ID, TEXT, 4, CLOSE_ID, *[TEXT] * 50), (2, True)),
+ "no think": ((TEXT, 4, 5, 6), (0, False)),
+ }
+ for name, (tokens, expected) in cases.items():
+ s = tb(2 if name == "after close" else 5)
+ feed(s, *tokens)
+ assert (s.reasoning_tokens, s.forced_close) == expected, name
+ assert s.forced_token() is None, name
+
+
+def test_absent_budget_counts_but_never_forces():
+ s = tb(None)
+ feed(s, THINK_ID, *[TEXT] * 10_000)
+ assert s.forced_token() is None and s.reasoning_tokens == 10_000
+ s.accept(CLOSE_ID)
+ assert s.forced_close is False
+
+
+def test_remaining_raises_when_the_budget_is_overshot():
+ s = tb(2)
+ feed(s, THINK_ID, TEXT, TEXT)
+ assert s.remaining() == 0
+ s.accept(TEXT) # a third reasoning token without a forced close
+ with pytest.raises(RuntimeError):
+ s.remaining()
+
+
+def test_invalid_budget_values_are_rejected():
+ for value in (-1, 1.5, "5", True, False, []):
+ with pytest.raises(ValueError):
+ tb(value)
+
+
+def test_only_budget_zero_masks_think_at_the_first_token():
+ logits = np.linspace(-1.0, 1.0, VOCAB).astype(np.float32)
+ logits[THINK_ID] = 50.0 # the model would pick
+ before = logits.copy()
+ out = np.asarray(tb(0).mask_first_logits(logits))
+ assert out[THINK_ID] == -np.inf and int(np.argmax(out)) != THINK_ID
+ keep = [i for i in range(VOCAB) if i != THINK_ID]
+ np.testing.assert_array_equal(out[keep], before[keep])
+ np.testing.assert_array_equal(logits, before)
+ for budget in (None, 1, 64):
+ np.testing.assert_array_equal(np.asarray(tb(budget).mask_first_logits(logits)), before)
+
+
+class BosTokenizer(DedicatedTokenizer):
+ """encode() prepends a BOS id unless add_special_tokens is False."""
+
+ def encode(self, text, add_special_tokens=False):
+ return ([100] if add_special_tokens else []) + super().encode(text)
+
+
+@pytest.mark.parametrize("tokenizer", [BosTokenizer, DedicatedTokenizer])
+def test_dedicated_token_id_returns_the_vocabulary_id(tokenizer):
+ assert dedicated_token_id(tokenizer({"": THINK_ID}), "") == THINK_ID
+
+
+class NonIntTokenizer(DedicatedTokenizer):
+ def convert_tokens_to_ids(self, token):
+ return str(THINK_ID)
+
+
+class WrongRoundTripTokenizer(DedicatedTokenizer):
+ def convert_ids_to_tokens(self, idx):
+ return ""
+
+
+class SplitEncodeTokenizer(DedicatedTokenizer):
+ def encode(self, text, add_special_tokens=False):
+ return [THINK_ID, 1]
+
+
+@pytest.mark.parametrize(
+ "tokenizer",
+ [NonIntTokenizer, WrongRoundTripTokenizer, SplitEncodeTokenizer, DedicatedTokenizer],
+)
+def test_dedicated_token_id_rejects_markers_that_are_not_one_vocabulary_token(tokenizer):
+ vocab = {} if tokenizer is DedicatedTokenizer else {"": THINK_ID} # {} -> unknown id
+ with pytest.raises(ValueError, match="dedicated vocabulary token"):
+ dedicated_token_id(tokenizer(vocab), "")
diff --git a/engine/tests/test_thinking_budget_engine.py b/engine/tests/test_thinking_budget_engine.py
new file mode 100644
index 0000000..c116c88
--- /dev/null
+++ b/engine/tests/test_thinking_budget_engine.py
@@ -0,0 +1,380 @@
+"""Thinking budget in the decode loop, driven through a stub session, sampler and grammar."""
+
+import json
+import subprocess
+import sys
+from types import SimpleNamespace
+
+import mlx.core as mx
+import pytest
+from tokenizers import Tokenizer, decoders, models, pre_tokenizers
+from transformers import PreTrainedTokenizerFast
+
+from rec_mlx import constants
+from rec_mlx.engine import Engine, Sampling
+from rec_mlx.grammar import ConstraintCompiler
+from rec_mlx.grammar_builder import _PROTOCOL_MARKERS
+from rec_mlx.kernels.sampling import WIDTH, ResidentSampler
+from test_thinking_budget import CLOSE_ID, THINK_ID, DedicatedTokenizer
+
+VOCAB = 16
+EOS = 0
+TEXT = 3
+FIRST_TEXT = 4
+REASON = 5
+AFTER = 9
+SEED = 100
+PROMPT = [1, 2]
+LOGPROB = -0.5
+PACKET = 8 # the decode packet size, a literal in Engine._generate_tokens
+
+
+class RecordingSampler:
+ """Scripted tokens with the real block() feed semantics, recording every call.
+
+ block(state, token, count) feeds `token` at state["position"], returns the packet, feeds all
+ but its last token and advances the position. feed_only feeds `token` and returns nothing.
+ """
+
+ def __init__(self, stream):
+ self.stream = list(stream)
+ self.calls = []
+ self.fed = []
+
+ def block(self, state, token, count, seed, *, logprobs=None, feed_only=False, **options):
+ self.calls.append(
+ {"last": token, "count": count, "seed": seed, "feed_only": feed_only, **options}
+ )
+ position = state["position"]
+ if feed_only:
+ self.fed.append((position, token))
+ state["position"] = position + 1
+ return []
+ out = [self.stream.pop(0) if self.stream else TEXT for _ in range(count)]
+ self.fed.append((position, token))
+ self.fed.extend((position + 1 + i, t) for i, t in enumerate(out[:-1]))
+ state["position"] = position + len(out)
+ if logprobs is not None:
+ logprobs.extend([LOGPROB] * len(out))
+ return out
+
+
+def stub_session(first):
+ logits = [0.0] * VOCAB
+ logits[first] = 10.0
+ state = {"kv": SimpleNamespace(nbytes=0), "capacity": 0, "growth_count": 0}
+ state |= {"growth_seconds": 0.0, "position": len(PROMPT)}
+ return SimpleNamespace(
+ prefill=lambda ids, prefix=None, should_stop=None: mx.array([logits], mx.float32),
+ cached_tokens=0,
+ common_prefix_tokens=0,
+ prompt_cache=SimpleNamespace(layers=[], checkpoint_bytes=0, tokens=[]),
+ _state=state,
+ )
+
+
+class StubGrammar:
+ stops = (EOS,)
+
+ def __init__(self):
+ self.accepted = []
+
+ def first_logits(self, logits):
+ return logits
+
+ def accept_first(self, token, state):
+ self.accepted.append(token)
+
+ def metrics(self):
+ return {}
+
+
+def build(monkeypatch, stream, first=FIRST_TEXT):
+ monkeypatch.setattr(
+ "rec_mlx.engine.StreamingText",
+ lambda tokenizer: SimpleNamespace(add_token=lambda t: "", finish=lambda toks: ""),
+ )
+ engine = Engine.__new__(Engine)
+ engine.mx = mx
+ engine.eos = {EOS}
+ engine.context_limit = 10_000
+ engine.think_ids = (THINK_ID, CLOSE_ID)
+ engine.tokenizer = DedicatedTokenizer({"": THINK_ID, "": CLOSE_ID})
+ engine.sampler = engine.resident_sampler = RecordingSampler(stream)
+ engine.session = stub_session(first)
+ engine.runtime = SimpleNamespace(
+ make_session=lambda max_new_tokens: engine.session,
+ prefill_chunk_size=10**9,
+ metadata=[],
+ device_profile=SimpleNamespace(chip="x"),
+ )
+ engine._prompt_cache = engine.session.prompt_cache
+ engine._prefix_preparation = None
+ return engine
+
+
+def run(
+ engine, max_tokens, *, budget=None, grammar=True, ids=PROMPT, logprobs=False, seed=SEED, **kw
+):
+ if grammar is True:
+ grammar = StubGrammar() if budget is not None else None # a budget requires a grammar
+ options = {"temperature": 0.0, "top_p": 1.0, "top_k": 0, **kw}
+ extra = {} if budget is None else {"thinking_budget": budget}
+ events = list(
+ engine._generate_tokens(
+ ids,
+ Sampling(max_tokens=max_tokens, seed=seed, **options),
+ should_stop=lambda: False,
+ grammar=grammar,
+ return_logprobs=logprobs,
+ **extra,
+ )
+ )
+ return [e["token"] for e in events[:-1]], events[:-1], events[-1]["metrics"]
+
+
+def assert_fed_once_in_order(engine, tokens):
+ """Every generated token except the last is written to the KV exactly once, in order."""
+ fed = engine.sampler.fed
+ assert [p for p, _ in fed] == list(range(len(PROMPT), len(PROMPT) + len(fed)))
+ assert [t for _, t in fed] == tokens[: len(fed)]
+ assert len(fed) == len(tokens) - 1
+
+
+def test_unbudgeted_run_is_unchanged(monkeypatch):
+ stream = [5, 6, 7, 5, 6, 7, 5, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
+ engine = build(monkeypatch, stream)
+ tokens, events, metrics = run(engine, 20, logprobs=True)
+ assert tokens == [FIRST_TEXT, *stream]
+ calls = engine.sampler.calls
+ assert [c["count"] for c in calls] == [PACKET, PACKET, 3]
+ assert [c["seed"] for c in calls] == [SEED + 1, SEED + 9, SEED + 17]
+ assert [c["last"] for c in calls] == [FIRST_TEXT, stream[7], stream[15]]
+ assert events[-1]["finish_reason"] == "length"
+ assert metrics["generated_tokens"] == metrics["computed_tokens"] == 20
+ assert metrics["token_ids"] == tokens
+ assert metrics["thinking_forced_close"] is False
+ assert_fed_once_in_order(engine, tokens)
+
+
+def construct(monkeypatch, tmp_path, tokenizer):
+ """The real Engine.__init__ with hardware, weights and decoder replaced by stubs."""
+ (tmp_path / "config.json").write_text(json.dumps({"eos_token_id": [EOS]}))
+ tokenizer.chat_template = "template"
+ runtime = SimpleNamespace(
+ model=SimpleNamespace(native=SimpleNamespace(parameters=lambda: {})),
+ prefill_chunk_size=10**9,
+ metadata=[],
+ )
+ patches = {
+ "rec_mlx.engine.platform.system": lambda: "Darwin",
+ "rec_mlx.engine.platform.machine": lambda: "arm64",
+ "rec_mlx.engine.subprocess.check_output": lambda *a, **k: str(64 * 1024**3),
+ "rec_mlx.engine.profile_for_device": lambda device: SimpleNamespace(chip="x"),
+ "rec_mlx.engine.validate": lambda *a: {
+ "max_context": 4096,
+ "weight_format": "w",
+ "kv_format": "k",
+ },
+ "rec_mlx.engine.load_tokenizer": lambda directory: tokenizer,
+ "rec_mlx.kernels.build_decoder": lambda model, optimized: (runtime, None),
+ }
+ for target, value in patches.items():
+ monkeypatch.setattr(target, value)
+ monkeypatch.setattr(
+ "rec_mlx.quantized.QATModel",
+ type("QAT", (), {"context_limit": 4096, "__init__": lambda s, d: None}),
+ )
+ return Engine(tmp_path, tmp_path / "m.json", context_limit=4096)
+
+
+def test_engine_resolves_think_ids_and_tolerates_a_tokenizer_without_them(monkeypatch, tmp_path):
+ vocab = {"": THINK_ID, "": CLOSE_ID}
+ assert construct(monkeypatch, tmp_path, DedicatedTokenizer(vocab)).think_ids == (
+ THINK_ID,
+ CLOSE_ID,
+ )
+ engine = construct(monkeypatch, tmp_path, DedicatedTokenizer({}))
+ assert engine.think_ids is None # such an engine still serves requests without a budget
+
+
+def test_unbudgeted_run_needs_no_think_tokens(monkeypatch):
+ engine = build(monkeypatch, [])
+ engine.think_ids = None
+ assert len(run(engine, 4)[0]) == 4
+
+
+@pytest.mark.parametrize("budget", [3, 2 * PACKET + 1])
+def test_forced_close_after_n_reasoning_tokens(monkeypatch, budget):
+ reasoning = [100 + i for i in range(budget)] # distinct, so a wrong t_k is visible
+ engine = build(monkeypatch, reasoning + [AFTER] * 30, first=THINK_ID)
+ grammar = StubGrammar()
+ tokens, _, metrics = run(engine, 1 + budget + 1 + 12, budget=budget, grammar=grammar)
+ assert tokens[: 2 + budget] == [THINK_ID, *reasoning, CLOSE_ID]
+ assert tokens[2 + budget :] == [AFTER] * 12
+ assert grammar.accepted.count(CLOSE_ID) == 1
+ assert metrics["reasoning_tokens"] == budget and metrics["thinking_forced_close"] is True
+ assert metrics["computed_tokens"] == metrics["generated_tokens"] == len(tokens)
+ # t_k, the last reasoning token, is fed alone before ; no sampling past the close.
+ (feed,) = [c for c in engine.sampler.calls if c["feed_only"]]
+ assert (feed["count"], feed["last"], feed["seed"]) == (1, reasoning[-1], SEED + 1 + budget)
+ assert_fed_once_in_order(engine, tokens)
+
+
+def test_forced_close_logprobs_stay_aligned_with_tokens(monkeypatch):
+ engine = build(monkeypatch, [REASON, REASON] + [AFTER] * 6, first=THINK_ID)
+ tokens, events, metrics = run(engine, 6, budget=2, logprobs=True)
+ assert tokens == [THINK_ID, REASON, REASON, CLOSE_ID, AFTER, AFTER]
+ reported = [e["logprob"] for e in events]
+ assert reported[1:4] == [LOGPROB, LOGPROB, constants.FORCED_TOKEN_LOGPROB]
+ assert metrics["token_logprobs"] == reported
+
+
+def test_forced_close_marks_presence_state_and_keeps_penalty(monkeypatch):
+ engine = build(monkeypatch, [REASON, REASON, AFTER, AFTER], first=THINK_ID)
+ run(engine, 5, budget=2, presence_penalty=0.5)
+ mask = engine.session._state["presence_mask"]
+ assert float(mask[CLOSE_ID]) == 1.0 and float(mask[THINK_ID]) == 1.0
+ assert all(call["presence_penalty"] == 0.5 for call in engine.sampler.calls)
+
+
+@pytest.mark.parametrize(("max_tokens", "forced"), [(4, True), (3, False)])
+def test_max_tokens_decides_whether_the_close_is_reached(monkeypatch, max_tokens, forced):
+ engine = build(monkeypatch, [REASON, REASON, AFTER], first=THINK_ID)
+ tokens, _, metrics = run(engine, max_tokens, budget=2)
+ assert tokens == [THINK_ID, REASON, REASON, CLOSE_ID][:max_tokens]
+ assert metrics["thinking_forced_close"] is forced
+ assert_fed_once_in_order(engine, tokens)
+
+
+def test_natural_close_before_budget_is_not_forced(monkeypatch):
+ engine = build(monkeypatch, [REASON, CLOSE_ID] + [AFTER] * 10, first=THINK_ID)
+ tokens, _, metrics = run(engine, 6, budget=4)
+ assert tokens[:3] == [THINK_ID, REASON, CLOSE_ID]
+ assert metrics["thinking_forced_close"] is False and metrics["reasoning_tokens"] == 1
+ assert_fed_once_in_order(engine, tokens)
+
+
+def test_budget_zero_never_opens_think(monkeypatch):
+ for sampling in ({"temperature": 0.0}, {"temperature": 0.7, "top_p": 0.9, "top_k": 5}):
+ for seed in range(5):
+ engine = build(monkeypatch, [REASON] * 4, first=THINK_ID) # the model wants
+ tokens, _, metrics = run(engine, 2, budget=0, seed=seed, **sampling)
+ assert tokens[0] != THINK_ID, f"seed {seed}"
+ assert metrics["reasoning_tokens"] == 0
+
+
+def test_budget_without_a_grammar_raises_before_any_decode(monkeypatch):
+ for budget in (0, 2):
+ engine = build(monkeypatch, [])
+ with pytest.raises(ValueError, match="grammar"):
+ run(engine, 20, budget=budget, grammar=None)
+ assert engine.sampler.calls == []
+
+
+@pytest.mark.parametrize(
+ ("budget", "max_tokens", "counts"),
+ [(3, 100, [3]), (PACKET + 2, 100, [PACKET, 2]), (1000, 100, [PACKET]), (1000, 4, [3])],
+)
+def test_packets_never_sample_past_the_forced_close(monkeypatch, budget, max_tokens, counts):
+ engine = build(monkeypatch, [], first=THINK_ID)
+ run(engine, max_tokens, budget=budget)
+ assert [c["count"] for c in engine.sampler.calls][: len(counts)] == counts
+
+
+def test_validate_rejects_an_unclosed_think_in_the_prompt():
+ engine = SimpleNamespace(think_ids=(THINK_ID, CLOSE_ID))
+ for ids in ([1, THINK_ID], [1, THINK_ID, 5, 6], [1, THINK_ID, 5, CLOSE_ID, THINK_ID, 6]):
+ with pytest.raises(ValueError, match="think"):
+ Engine.validate_thinking_budget(engine, ids, 3)
+ # An empty closed block ( right after ) is closed.
+ for ids in ([1, THINK_ID, 5, CLOSE_ID, 6], [1, THINK_ID, CLOSE_ID, 6], [1, 2]):
+ Engine.validate_thinking_budget(engine, ids, 3)
+
+
+class FakeKernelEngine:
+ """Records every call; any engine call other than forward fails the feed_only tests."""
+
+ def __init__(self):
+ self.calls = []
+
+ def forward(self, state, token):
+ self.calls.append(("forward", state, token))
+
+ def __getattr__(self, name):
+ raise AssertionError(f"feed_only must not touch engine.{name}")
+
+
+def resident_sampler():
+ sampler = object.__new__(ResidentSampler) # skips the Metal kernel compilation
+ sampler.module, sampler.engine = None, FakeKernelEngine()
+ return sampler
+
+
+def test_resident_feed_only_runs_one_forward_pass_and_samples_nothing():
+ sampler, state = resident_sampler(), {"position": 3}
+ assert sampler.block(state, REASON, 1, SEED, feed_only=True, grammar=object()) == []
+ assert sampler.engine.calls == [("forward", state, REASON)]
+
+
+@pytest.mark.parametrize(
+ ("options", "message"),
+ [
+ ({"feed_only": True, "count": 2}, "exactly one"),
+ ({"feed_only": True, "count": 0}, "exactly one"),
+ ({"count": WIDTH + 1}, "between one and eight"), # feed_only defaults to False
+ ({"feed_only": False, "count": 0}, "between one and eight"),
+ ],
+)
+def test_resident_block_rejects_bad_counts_before_touching_the_engine(options, message):
+ sampler = resident_sampler()
+ with pytest.raises(ValueError, match=message):
+ sampler.block({}, REASON, options.pop("count"), SEED, **options)
+ assert sampler.engine.calls == []
+
+
+def test_validate_rejects_a_tokenizer_without_think_tokens():
+ with pytest.raises(ValueError, match="think"):
+ Engine.validate_thinking_budget(SimpleNamespace(think_ids=None), [1, 2], 3)
+
+
+def test_decode_backstop_rejects_an_open_think_before_sampling(monkeypatch):
+ engine = build(monkeypatch, [])
+ with pytest.raises(ValueError, match="think"):
+ run(engine, 4, budget=2, ids=[*PROMPT, THINK_ID])
+ assert engine.sampler.calls == []
+
+
+@pytest.fixture(scope="module")
+def tokenizer():
+ alphabet = sorted(pre_tokenizers.ByteLevel.alphabet())
+ model = Tokenizer(models.BPE(vocab={c: i for i, c in enumerate(alphabet)}, merges=[]))
+ model.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
+ model.decoder = decoders.ByteLevel()
+ tok = PreTrainedTokenizerFast(tokenizer_object=model, eos_token="<|end_of_text|>")
+ tok.add_tokens([x for x in _PROTOCOL_MARKERS if x != "<|end_of_text|>"])
+ return tok
+
+
+def test_flint_grammar_accepts_a_forced_close_after_reasoning(tokenizer):
+ think, close = (tokenizer.convert_tokens_to_ids(x) for x in ("", ""))
+ text = tokenizer.encode("abc", add_special_tokens=False)
+ with ConstraintCompiler(tokenizer).tools().session() as session:
+ state = {}
+ for token in [think, *text, close, *text, tokenizer.eos_token_id]:
+ session.accept_first(token, state)
+
+
+def test_budget_modules_import_without_mlx():
+ probe = """
+import importlib, json, sys
+for name in ("rec_mlx.engine", "rec_mlx.thinking", "rec_mlx.grammar_builder"):
+ importlib.import_module(name)
+b = sys.modules["rec_mlx.thinking"].ThinkBudget(1, 7, 8)
+for token in (7, 3, 8):
+ b.accept(token)
+print(json.dumps(sorted(m for m in sys.modules if m == "mlx" or m.startswith("mlx."))))
+"""
+ done = subprocess.run([sys.executable, "-c", probe], capture_output=True, text=True)
+ assert done.returncode == 0, done.stderr
+ assert done.stdout.strip() == "[]"
diff --git a/engine/tests/test_thinking_budget_server.py b/engine/tests/test_thinking_budget_server.py
new file mode 100644
index 0000000..1fe199c
--- /dev/null
+++ b/engine/tests/test_thinking_budget_server.py
@@ -0,0 +1,218 @@
+"""thinking_budget over HTTP: request validation, early 400s, /v1/messages and usage fields."""
+
+import http.client
+import json
+import tempfile
+import threading
+import unittest
+from pathlib import Path
+
+from rec_mlx.server import make_server
+from test_serving import Engine as BaseEngine
+
+THINK_ID = 7
+BUDGET = 3
+OK, BAD, ERROR = 200, 400, 500
+JSON_HEADERS = {"Content-Type": "application/json"}
+# Usage keys before the thinking budget existed (origin/main); mlx-lm omits
+# prompt_tokens_details when no metrics event arrives.
+BASE_USAGE = {"prompt_tokens", "completion_tokens", "total_tokens", "prompt_tokens_details"}
+THINKING_USAGE = {"reasoning_tokens", "thinking_forced_close"}
+
+
+class ThinkEngine(BaseEngine):
+ """BaseEngine that records extra generate_tokens kwargs and reports thinking metrics."""
+
+ def __init__(self):
+ super().__init__()
+ self.extra = []
+ self.reasoning, self.forced = 7, True
+ self.drop_metrics = False
+ self.drop_key = None
+ self.think_ids = (THINK_ID, 8)
+ self.prompt_ids = None
+ self.validated = []
+
+ def prompt(self, messages, tools=None):
+ return self.prompt_ids or super().prompt(messages, tools)
+
+ def validate_thinking_budget(self, ids, budget):
+ self.validated.append((list(ids), budget))
+ if self.think_ids is None:
+ raise ValueError("thinking_budget requires and tokenizer tokens")
+ if ids and ids[-1] == self.think_ids[0]:
+ raise ValueError("thinking_budget requires a prompt that does not already open ")
+
+ def generate_tokens(self, ids, sampling, *, should_stop, grammar=None, **extra):
+ self.calls.append((chr(ids[0]), sampling.max_tokens))
+ self.grammars.append(grammar)
+ self.extra.append(extra)
+ for index in range(sampling.max_tokens):
+ yield {
+ "token": ord("x"),
+ "text": "x",
+ "finish_reason": "length" if index + 1 == sampling.max_tokens else None,
+ }
+ if not self.drop_metrics:
+ metrics = {
+ "prompt_tokens": len(ids),
+ "cached_prompt_tokens": 1,
+ "generated_tokens": sampling.max_tokens,
+ "computed_tokens": sampling.max_tokens,
+ "reasoning_tokens": self.reasoning,
+ "thinking_forced_close": self.forced,
+ }
+ metrics.pop(self.drop_key, None)
+ yield {"metrics": metrics}
+
+
+class ServerCase(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.engine = ThinkEngine()
+ self.server = make_server(self.engine, ("127.0.0.1", 0), Path(self.temp.name) / "metrics")
+ self.thread = threading.Thread(
+ target=self.server.serve_forever, kwargs={"poll_interval": 0.01}
+ )
+ self.thread.start()
+
+ def tearDown(self):
+ self.server.generator.cancel()
+ self.server.shutdown()
+ self.server.generator.join()
+ self.server.server_close()
+ self.thread.join()
+ self.temp.cleanup()
+
+ def post(self, path, body):
+ conn = http.client.HTTPConnection(*self.server.server_address, timeout=5)
+ try:
+ conn.request("POST", path, json.dumps(body), JSON_HEADERS)
+ response = conn.getresponse()
+ return response.status, response.read().decode()
+ finally:
+ conn.close()
+
+ def chat(self, **options):
+ body = {"model": "flint", "messages": [{"role": "user", "content": "BB"}], **options}
+ return self.post("/v1/chat/completions", body)
+
+ def usage(self, **options):
+ """The usage object of a non-stream response, or of the final chunk of a stream."""
+ status, text = self.chat(max_tokens=2, **options)
+ self.assertEqual(status, OK, text)
+ return json.loads(text)["usage"]
+
+ def stream_usage(self, **options):
+ options["stream_options"] = {"include_usage": True}
+ status, text = self.chat(max_tokens=2, stream=True, **options)
+ self.assertEqual(status, OK, text)
+ chunks = [json.loads(x[6:]) for x in text.splitlines() if x.startswith("data: {")]
+ self.assertTrue(all("usage" not in chunk for chunk in chunks[:-1]))
+ return chunks[-1]["usage"]
+
+
+class RequestTest(ServerCase):
+ def test_invalid_budget_values_are_400_and_never_reach_the_engine(self):
+ for value in (-1, 1.5, "5", True, False, None, [], {}):
+ with self.subTest(value=value):
+ status, text = self.chat(max_tokens=1, thinking_budget=value)
+ self.assertEqual(status, BAD, text)
+ self.assertEqual(self.engine.calls, [])
+
+ def test_valid_budgets_reach_the_engine_and_are_not_dropped(self):
+ with self.assertNoLogs(level="WARNING"):
+ for value in (0, 1, 4096):
+ self.assertEqual(self.chat(max_tokens=1, thinking_budget=value)[0], OK)
+ self.assertEqual(self.engine.extra[-1]["thinking_budget"], value)
+
+ def test_budget_needs_the_flint_grammar(self):
+ status, text = self.chat(max_tokens=1, grammar="none", thinking_budget=2)
+ self.assertEqual(status, BAD, text)
+ self.assertEqual(self.engine.calls, [])
+
+ def test_unusable_prompt_or_tokenizer_is_400_before_any_header(self):
+ self.engine.prompt_ids = [ord("B"), THINK_ID] # the prompt already opens
+ for stream in (False, True):
+ status, text = self.chat(max_tokens=2, thinking_budget=BUDGET, stream=stream)
+ self.assertEqual((status, "think" in text), (BAD, True), text)
+ self.engine.prompt_ids, self.engine.think_ids = None, None
+ for stream in (False, True):
+ self.assertEqual(self.chat(max_tokens=2, thinking_budget=BUDGET, stream=stream)[0], BAD)
+ self.assertEqual(self.engine.calls, [], "generation must not start")
+
+ def test_validation_gets_the_prompt_and_only_runs_with_a_budget(self):
+ self.chat(max_tokens=1, thinking_budget=BUDGET)
+ self.assertEqual(self.engine.validated, [([ord("B"), ord("B")], BUDGET)])
+ self.engine.think_ids = None
+ status, text = self.chat(max_tokens=1)
+ self.assertEqual(status, OK, text)
+ self.assertEqual(len(self.engine.validated), 1)
+ self.assertNotIn("thinking_budget", self.engine.extra[-1])
+
+
+class MessagesTest(ServerCase):
+ def messages(self, **extra):
+ body = {"model": "flint", "max_tokens": 4, "messages": [{"role": "user", "content": "BB"}]}
+ return self.post("/v1/messages", {**body, **extra})
+
+ def test_top_level_thinking_budget_is_rejected_not_dropped(self):
+ status, text = self.messages(thinking_budget=BUDGET)
+ self.assertEqual(status, BAD, text)
+ self.assertIn("thinking_budget", text)
+ self.assertEqual(self.engine.calls, [])
+
+ def test_anthropic_budget_tokens_is_accepted_and_not_forwarded(self):
+ status, text = self.messages(thinking={"type": "enabled", "budget_tokens": 1024})
+ self.assertEqual(status, OK, text)
+ self.assertEqual(self.engine.extra, [{}])
+
+
+class UsageTest(ServerCase):
+ def test_usage_reports_reasoning_only_when_a_budget_is_set(self):
+ cases = [ # (options, engine reasoning, engine forced)
+ ({"thinking_budget": 64}, 7, True),
+ ({"thinking_budget": 64}, 3, False),
+ ({"thinking_budget": 0}, 0, False),
+ ]
+ for options, reasoning, forced in cases:
+ self.engine.reasoning, self.engine.forced = reasoning, forced
+ for read in (self.usage, self.stream_usage):
+ with self.subTest(read=read.__name__, reasoning=reasoning, forced=forced):
+ usage = read(**options)
+ self.assertEqual(set(usage), BASE_USAGE | THINKING_USAGE)
+ self.assertEqual(usage["reasoning_tokens"], reasoning)
+ self.assertIs(usage["thinking_forced_close"], forced)
+ self.assertEqual(usage["completion_tokens"], 2)
+ self.assertEqual(usage["prompt_tokens_details"]["cached_tokens"], 1)
+ for read in (self.usage, self.stream_usage):
+ self.assertEqual(set(read()), BASE_USAGE)
+
+ def test_missing_metrics_fail_cleanly_only_with_a_budget(self):
+ self.engine.drop_metrics = True
+ status, text = self.chat(max_tokens=2, thinking_budget=1)
+ self.assertEqual(status, ERROR, text)
+ self.assertIn("metrics", text.lower())
+ self.assertNotIn("NoneType", text)
+ status, text = self.chat(
+ max_tokens=2, stream=True, thinking_budget=1, stream_options={"include_usage": True}
+ )
+ self.assertIn('"error"', text) # the header is sent; the stream carries the failure
+ self.assertIn("metrics", text.lower())
+ self.assertEqual(set(self.usage()), BASE_USAGE - {"prompt_tokens_details"})
+ self.engine.drop_metrics = False # the server keeps serving after the error
+ self.assertEqual(set(self.usage(thinking_budget=1)), BASE_USAGE | THINKING_USAGE)
+
+ def test_a_metrics_event_missing_a_thinking_key_is_a_500_naming_the_key(self):
+ for key in sorted(THINKING_USAGE):
+ self.engine.drop_key = key
+ with self.subTest(key=key):
+ status, text = self.chat(max_tokens=2, thinking_budget=1)
+ self.assertEqual(status, ERROR, text)
+ self.assertIn("usage is unavailable", text)
+ self.assertIn(key, text)
+ self.assertEqual(set(self.usage()), BASE_USAGE) # without a budget it still serves
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/engine/tests/validate_resident.py b/engine/tests/validate_resident.py
index bfed5c5..e154325 100644
--- a/engine/tests/validate_resident.py
+++ b/engine/tests/validate_resident.py
@@ -58,7 +58,8 @@ def pick(logits, index):
for token in tokens[:begin]:
reference.forward_token(token)
candidate.forward_token(token)
- mx.random.state[0] = keys[begin]
+ hi, lo = keys[begin].tolist()
+ mx.random.seed((int(hi) << 32) | int(lo))
expected = [
pick(reference.forward_token(tokens[begin + i]), begin + i + 1)
for i in range(stop)
@@ -67,7 +68,8 @@ def pick(logits, index):
model.eos_tokens = [expected[-1]]
# Each compiled graph must capture this fixture's EOS, not a previous one.
resident.filtered = FilteredResidentSampler(resident.module, engine, specification)
- mx.random.state[0] = keys[begin]
+ hi, lo = keys[begin].tolist()
+ mx.random.seed((int(hi) << 32) | int(lo))
actual = resident.block(candidate._state, tokens[begin], 8, 43 + begin, **settings)
assert actual == expected and len(actual) == stop, "EOS truncation mismatch"
assert mx.array_equal(mx.random.state[0], expected_key).item(), "EOS RNG mismatch"
@@ -227,7 +229,8 @@ def choose(logits, index):
count = min((1, 3, 8)[packets % 3], steps - position)
packet_input = pending
expected, logits, traces = [], [], []
- mx.random.state[0] = scalar_key
+ hi, lo = scalar_key.tolist()
+ mx.random.seed((int(hi) << 32) | int(lo))
for offset in range(count):
value, trace = engine.forward(reference._state, pending)
if temperature == 0 and position + offset < 8:
@@ -252,7 +255,8 @@ def choose(logits, index):
if pending in model.eos_tokens:
break
scalar_key = mx.random.state[0]
- mx.random.state[0] = packet_key
+ hi, lo = packet_key.tolist()
+ mx.random.seed((int(hi) << 32) | int(lo))
actual = resident.block(
candidate._state,
packet_input,
diff --git a/tests/launch_client_test.py b/tests/launch_client_test.py
index 55c62bf..c2673c1 100644
--- a/tests/launch_client_test.py
+++ b/tests/launch_client_test.py
@@ -12,8 +12,8 @@
import time
binary = str(pathlib.Path(sys.argv[1]).resolve())
-clients = sys.argv[2:] or ["opencode", "pi"]
-assert all(client in ("opencode", "pi") for client in clients)
+clients = sys.argv[2:] or ["opencode", "pi", "omp"]
+assert all(client in ("opencode", "pi", "omp") for client in clients)
class Quiet(http.server.BaseHTTPRequestHandler):
@@ -176,10 +176,12 @@ def do_GET(self):
(extensions / "legacy.mjs").write_text(
"export default function(pi) { pi.on('before_provider_request', (e) => ({...e.payload, temperature:0.1, top_p:0.95, top_k:20, max_tokens:128})); }\n"
)
- client_args = (
- ["run", "--format", "json", "Answer with 42."]
- if client == "opencode"
- else [
+ if client == "opencode":
+ client_args = ["run", "--format", "json", "Answer with 42."]
+ elif client == "omp":
+ client_args = ["--no-skills", "--mode", "json", "-p", "Answer with 42."]
+ else:
+ client_args = [
"--no-skills",
"--no-context-files",
"--no-prompt-templates",
@@ -190,7 +192,6 @@ def do_GET(self):
"-p",
"Answer with 42.",
]
- )
try:
r = subprocess.run(
[binary, "launch", client, "--", *client_args],
@@ -226,8 +227,9 @@ def do_GET(self):
)
assert r.returncode == 0, (r.returncode, r.stderr[-2000:], r.stdout[-2000:])
assert len(requests) == 2, "rec launch --args must reproduce the launched turn"
- assert auth.read_bytes() == original_auth, "Pi auth.json was modified"
- assert models.read_bytes() == original_models, "Pi models.json was modified"
+ if client == "pi":
+ assert auth.read_bytes() == original_auth, "Pi auth.json was modified"
+ assert models.read_bytes() == original_models, "Pi models.json was modified"
assert (root / "data/rec/clients" / client / "user").is_dir(), "No rec user directory"
assert not (root / "data/opencode").exists(), "OpenCode used the system data directory"
for path, auth, body in requests:
@@ -237,7 +239,12 @@ def do_GET(self):
actual = tuple(body.get(k) for k in ("temperature", "top_p", "top_k")) + (
body.get("max_tokens", body.get("max_completion_tokens")),
)
- assert actual == want, (actual, want)
+ if client == "omp":
+ # omp reserves headroom for its system prompt and tools.
+ assert actual[:3] == want[:3], (actual, want)
+ assert 0 < actual[3] <= want[3], (actual, want)
+ else:
+ assert actual == want, (actual, want)
print(
json.dumps(
{