From fa33e3a5675363f82be7d9aa4acef5aee9520d29 Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:16:32 +0530 Subject: [PATCH 1/6] feat: add opt-in TTL caching for getPrompt Co-authored-by: Cursor --- README.md | 36 + package-lock.json | 1213 +++++++++++++++++++++++++++++- package.json | 5 +- src/api/prompts/api.test.ts | 95 +++ src/api/prompts/api.ts | 32 +- src/api/prompts/models.ts | 4 + src/cache.test.ts | 55 ++ src/cache.ts | 35 + src/config.ts | 26 + src/index.shutdown-cache.test.ts | 38 + src/index.ts | 11 + 11 files changed, 1524 insertions(+), 26 deletions(-) create mode 100644 src/api/prompts/api.test.ts create mode 100644 src/cache.test.ts create mode 100644 src/cache.ts create mode 100644 src/index.shutdown-cache.test.ts diff --git a/README.md b/README.md index c99b256..079be41 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - 🔧 **Multi-Provider Support**: Works with OpenAI, Google GenAI, Mistral, Anthropic, and more - 📈 **Session Management**: Track user sessions and custom attributes - 🌐 **Automatic Instrumentation**: Zero-code instrumentation for popular frameworks and libraries +- ⚡ **Opt-in Read Caching**: In-memory TTL caching for read-heavy SDK calls (e.g. prompt fetches) ## 📦 Installation @@ -184,6 +185,40 @@ async function generateContent(prompt: string) { } ``` +## 📝 Prompts API + +Fetch prompt versions from Prompt Studio via `Netra.prompts.getPrompt()`. Caching is **opt-in per call** — omit `useCache` (or set it to `false`) to always hit the API. + +Set a global default TTL at init with `cacheTtlSeconds` (default: **60** seconds), or override TTL for a single call with `cacheTtl`. + +```typescript +import { Netra } from "netra-sdk"; + +await Netra.init({ + appName: "my-ai-app", + cacheTtlSeconds: 60, // optional; env: NETRA_CACHE_TTL_SECONDS +}); + +// Always fetches from the API (default) +const prompt = await Netra.prompts.getPrompt({ name: "my-prompt" }); + +// Cached for 60s (global default TTL) +const cached = await Netra.prompts.getPrompt({ + name: "my-prompt", + useCache: true, +}); + +// Cached for 30s for this call only +const shortLived = await Netra.prompts.getPrompt({ + name: "my-prompt", + label: "production", // default label when omitted + useCache: true, + cacheTtl: 30, +}); +``` + +> **Note**: Cached prompts may be stale for up to the TTL after dashboard edits. Use `useCache: false` when you need the latest version immediately. `Netra.shutdown()` clears in-memory caches. + ## 🔧 Environment Variables You can configure the SDK using environment variables: @@ -194,6 +229,7 @@ You can configure the SDK using environment variables: | `NETRA_APP_NAME` | Name of your application | | `NETRA_ENV` | Environment (e.g., prod, dev) | | `NETRA_TRACE_CONTENT` | Capture prompt/completion content (default: true) | +| `NETRA_CACHE_TTL_SECONDS` | Default TTL in seconds for opt-in SDK read caches (default: 60) | ## 🤝 License diff --git a/package-lock.json b/package-lock.json index e5d293f..098b134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,8 @@ "@types/shimmer": "^1.2.0", "ts-node": "^10.9.2", "tsup": "^8.5.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.10" }, "engines": { "node": ">=18.0.0" @@ -107,6 +108,40 @@ "node": ">=12" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -837,6 +872,25 @@ "@langchain/core": "^1.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2702,6 +2756,16 @@ "node": ">=14" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2766,6 +2830,288 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", @@ -3116,6 +3462,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3530,12 +3883,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3600,22 +3982,135 @@ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "license": "MIT" }, - "node_modules/a-sync-waterfall": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "license": "MIT" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=6.5" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" } }, "node_modules/acorn": { @@ -3702,6 +4197,16 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3842,6 +4347,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3970,6 +4485,13 @@ "simple-wcswidth": "^1.1.2" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4021,6 +4543,16 @@ "node": ">=0.4.0" } }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -4111,6 +4643,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4189,6 +4728,16 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -4225,6 +4774,16 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4787,6 +5346,279 @@ } } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4932,6 +5764,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -4996,6 +5847,20 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ollama": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", @@ -5110,9 +5975,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -5144,6 +6009,35 @@ "pathe": "^2.0.1" } }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -5319,6 +6213,40 @@ "node": ">=14" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/rollup": { "version": "4.55.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", @@ -5402,6 +6330,13 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", "license": "BSD-2-Clause" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-wcswidth": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", @@ -5418,6 +6353,30 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -5608,6 +6567,13 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -5616,14 +6582,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5632,6 +6598,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5841,6 +6817,184 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5864,6 +7018,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index c263922..37c87e1 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "tsup", "start:dev": "tsup --watch", "prepack": "npm run build", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest run" }, "keywords": [ "netra", @@ -121,6 +121,7 @@ "@types/shimmer": "^1.2.0", "ts-node": "^10.9.2", "tsup": "^8.5.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.10" } } diff --git a/src/api/prompts/api.test.ts b/src/api/prompts/api.test.ts new file mode 100644 index 0000000..e7c8e4f --- /dev/null +++ b/src/api/prompts/api.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Config } from "../../config"; +import { Prompts } from "./api"; +import { PromptsHttpClient } from "./client"; + +describe("Prompts.getPrompt caching", () => { + let prompts: Prompts; + let getPromptVersion: ReturnType; + + beforeEach(() => { + const config = new Config({ cacheTtlSeconds: 60 }); + prompts = new Prompts(config); + getPromptVersion = vi.fn(); + (prompts as unknown as { client: PromptsHttpClient }).client = { + getPromptVersion, + } as unknown as PromptsHttpClient; + }); + + it("calls HTTP on every request when useCache is omitted", async () => { + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + await prompts.getPrompt({ name: "my-prompt" }); + await prompts.getPrompt({ name: "my-prompt" }); + + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); + + it("serves from cache on second call with same name and label when useCache is true", async () => { + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + const first = await prompts.getPrompt({ name: "my-prompt", useCache: true }); + const second = await prompts.getPrompt({ name: "my-prompt", useCache: true }); + + expect(getPromptVersion).toHaveBeenCalledTimes(1); + expect(first).toEqual({ template: "v1" }); + expect(second).toEqual({ template: "v1" }); + }); + + it("keeps separate cache entries per label when useCache is true", async () => { + getPromptVersion + .mockResolvedValueOnce({ data: { template: "prod" } }) + .mockResolvedValueOnce({ data: { template: "staging" } }); + + const prod = await prompts.getPrompt({ + name: "my-prompt", + label: "production", + useCache: true, + }); + const staging = await prompts.getPrompt({ + name: "my-prompt", + label: "staging", + useCache: true, + }); + + expect(getPromptVersion).toHaveBeenCalledTimes(2); + expect(prod).toEqual({ template: "prod" }); + expect(staging).toEqual({ template: "staging" }); + }); + + it("does not cache null API responses", async () => { + getPromptVersion.mockResolvedValue(null); + + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); + + it("ignores cache when useCache is false even if cacheTtl is set", async () => { + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + await prompts.getPrompt({ + name: "my-prompt", + useCache: false, + cacheTtl: 30, + }); + await prompts.getPrompt({ + name: "my-prompt", + useCache: false, + cacheTtl: 30, + }); + + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); + + it("hits HTTP again after clearCache when useCache is true", async () => { + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + prompts.clearCache(); + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/api/prompts/api.ts b/src/api/prompts/api.ts index 7709cf2..90c3423 100644 --- a/src/api/prompts/api.ts +++ b/src/api/prompts/api.ts @@ -1,3 +1,4 @@ +import { TTLCache } from "../../cache"; import { Config } from "../../config"; import { Logger } from "../../logger"; import { PromptsHttpClient } from "./client"; @@ -6,17 +7,26 @@ import { GetPromptParams, PromptResponse } from "./models"; export class Prompts { private config: Config; private client: PromptsHttpClient; + private cache: TTLCache; constructor(config: Config) { this.config = config; this.client = new PromptsHttpClient(config); + this.cache = new TTLCache(config.cacheTtlSeconds); + } + + /** Clear all cached prompt entries. */ + clearCache(): void { + this.cache.clear(); } /** * Fetch prompt version by name and label. * - * @param params.name - Name of the prompt (required) - * @param params.label - Label of the prompt version (default: "production") + * @param params.name - Name of the prompt (required) + * @param params.label - Label of the prompt version (default: "production") + * @param params.useCache - When true, read/write the in-memory cache (default: false) + * @param params.cacheTtl - Per-call cache TTL in seconds (default: init cacheTtlSeconds) */ async getPrompt(params: GetPromptParams): Promise { if (!params || typeof params.name !== "string" || !params.name) { @@ -25,12 +35,28 @@ export class Prompts { } const label = params.label ?? "production"; + const useCache = params.useCache === true; + const cacheKey = `prompt:${params.name}:${label}`; + + if (useCache) { + const cached = this.cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + } + const result = await this.client.getPromptVersion(params.name, label); if (!result) { return null; } - return result.data ?? null; + const data = result.data ?? null; + + if (data !== null && useCache) { + this.cache.set(cacheKey, data, params.cacheTtl); + } + + return data; } } diff --git a/src/api/prompts/models.ts b/src/api/prompts/models.ts index 1ec6cb3..ae9c1d8 100644 --- a/src/api/prompts/models.ts +++ b/src/api/prompts/models.ts @@ -5,6 +5,10 @@ export interface GetPromptParams { name: string; label?: string; + /** When true, serve from in-memory cache when available (default: false). */ + useCache?: boolean; + /** Per-call TTL in seconds; falls back to init `cacheTtlSeconds` when omitted. */ + cacheTtl?: number; } /** diff --git a/src/cache.test.ts b/src/cache.test.ts new file mode 100644 index 0000000..af71f5e --- /dev/null +++ b/src/cache.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { TTLCache } from "./cache"; + +describe("TTLCache", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("get() returns undefined for missing key", () => { + const cache = new TTLCache(); + expect(cache.get("missing")).toBeUndefined(); + }); + + it("set() + get() returns stored value before TTL expires", () => { + const cache = new TTLCache(60); + cache.set("key", "value"); + expect(cache.get("key")).toBe("value"); + }); + + it("get() returns undefined after TTL expires", () => { + vi.useFakeTimers(); + const cache = new TTLCache(1); + cache.set("key", "value"); + vi.advanceTimersByTime(1001); + expect(cache.get("key")).toBeUndefined(); + }); + + it("per-entry ttl override expires independently of default", () => { + vi.useFakeTimers(); + const cache = new TTLCache(60); + cache.set("short", "a", 1); + cache.set("long", "b", 60); + vi.advanceTimersByTime(1001); + expect(cache.get("short")).toBeUndefined(); + expect(cache.get("long")).toBe("b"); + }); + + it("clear() removes all entries", () => { + const cache = new TTLCache(); + cache.set("a", "1"); + cache.set("b", "2"); + cache.clear(); + expect(cache.get("a")).toBeUndefined(); + expect(cache.get("b")).toBeUndefined(); + }); + + it("invalidate(key) removes single entry", () => { + const cache = new TTLCache(); + cache.set("a", "1"); + cache.set("b", "2"); + cache.invalidate("a"); + expect(cache.get("a")).toBeUndefined(); + expect(cache.get("b")).toBe("2"); + }); +}); diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..0bbf765 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,35 @@ +/** + * In-memory TTL cache for SDK read API responses. + */ + +export class TTLCache { + private store = new Map(); + private defaultTtl: number; + + constructor(defaultTtl = 60) { + this.defaultTtl = defaultTtl; + } + + get(key: string): T | undefined { + const entry = this.store.get(key); + if (!entry) return undefined; + if (performance.now() > entry.expiresAt) { + this.store.delete(key); + return undefined; + } + return entry.value; + } + + set(key: string, value: T, ttl?: number): void { + const ttlMs = (ttl ?? this.defaultTtl) * 1000; + this.store.set(key, { value, expiresAt: performance.now() + ttlMs }); + } + + invalidate(key: string): void { + this.store.delete(key); + } + + clear(): void { + this.store.clear(); + } +} diff --git a/src/config.ts b/src/config.ts index 9bf08b5..5cdbed4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,6 +19,8 @@ export interface NetraConfig { instruments?: Set; blockInstruments?: Set; rootInstruments?: Set; + /** Default TTL in seconds for opt-in SDK read caches (env: NETRA_CACHE_TTL_SECONDS). */ + cacheTtlSeconds?: number; } export enum NetraInstruments { @@ -128,6 +130,7 @@ export class Config { environment: string; resourceAttributes: Record; blockedSpans?: string[]; + cacheTtlSeconds: number; constructor(config: NetraConfig = {}) { this.appName = this._getAppName(config.appName); @@ -164,6 +167,11 @@ export class Config { config.resourceAttributes, ); this.blockedSpans = config.blockedSpans; + this.cacheTtlSeconds = this._getIntConfig( + config.cacheTtlSeconds, + "NETRA_CACHE_TTL_SECONDS", + 60, + ); this._validateApiKey(); this._setupAuthentication(); @@ -243,6 +251,24 @@ export class Config { } } + private _getIntConfig( + param: number | undefined, + envVar: string, + defaultValue: number, + ): number { + if (param !== undefined) { + return param; + } + + const envValue = process.env[envVar]; + if (envValue === undefined) { + return defaultValue; + } + + const parsed = parseInt(envValue, 10); + return Number.isNaN(parsed) ? defaultValue : parsed; + } + private _getBoolConfig( param: boolean | undefined, envVar: string, diff --git a/src/index.shutdown-cache.test.ts b/src/index.shutdown-cache.test.ts new file mode 100644 index 0000000..5b43d97 --- /dev/null +++ b/src/index.shutdown-cache.test.ts @@ -0,0 +1,38 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { PromptsHttpClient } from "./api/prompts/client"; + +vi.mock("./instrumentation", () => ({ + initInstrumentations: vi.fn(() => ({})), + instrumentationsReady: Promise.resolve(), + uninstrumentAll: vi.fn(() => Promise.resolve()), +})); + +import { Netra } from "./index"; + +describe("Netra.shutdown cache clearing", () => { + afterEach(async () => { + if (Netra.isInitialized()) { + await Netra.shutdown(); + } + }); + + it("clears prompts cache on shutdown so the next cached getPrompt hits HTTP", async () => { + await Netra.init({ cacheTtlSeconds: 60 }); + + const getPromptVersion = vi + .fn() + .mockResolvedValue({ data: { template: "v1" } }); + (Netra.prompts as unknown as { client: PromptsHttpClient }).client = { + getPromptVersion, + } as unknown as PromptsHttpClient; + + await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true }); + await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true }); + expect(getPromptVersion).toHaveBeenCalledTimes(1); + + await Netra.shutdown(); + + await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true }); + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/index.ts b/src/index.ts index 1cd80b1..29a74df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -153,6 +153,11 @@ export class Netra { return this._initialized; } + /** + * Initialize the Netra SDK. + * + * @param config.cacheTtlSeconds - Default TTL in seconds for opt-in read caches (default: 60, env: NETRA_CACHE_TTL_SECONDS) + */ static async init(config: NetraConfig = {}): Promise { if (this._initialized) { Logger.warn("Netra.init() called more than once; ignoring subsequent calls."); @@ -337,6 +342,12 @@ export class Netra { Logger.error("Error during Netra trace shutdown:", e); } + try { + this.prompts?.clearCache(); + } catch (e) { + Logger.error("Error clearing SDK API caches:", e); + } + this._initialized = false; this._tracer = undefined; } From 8127c573910d1b64689c1437965f99909375b4d2 Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:45:42 +0530 Subject: [PATCH 2/6] fix: downgrade vitest for Node 18 and tighten cache tests Use Vitest 3.x so dev test runs align with engines.node >=18, and add getPrompt cacheTtl integration coverage plus shutdown re-init behavior. Co-authored-by: Cursor --- package-lock.json | 1040 ++++++------------------------ package.json | 2 +- src/api/prompts/api.test.ts | 27 +- src/index.shutdown-cache.test.ts | 5 + 4 files changed, 241 insertions(+), 833 deletions(-) diff --git a/package-lock.json b/package-lock.json index 098b134..614776a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "ts-node": "^10.9.2", "tsup": "^8.5.1", "typescript": "^5.9.3", - "vitest": "^4.1.10" + "vitest": "^3.2.4" }, "engines": { "node": ">=18.0.0" @@ -108,40 +108,6 @@ "node": ">=12" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -872,25 +838,6 @@ "@langchain/core": "^1.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2756,16 +2703,6 @@ "node": ">=14" } }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2830,288 +2767,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", @@ -3462,13 +3117,6 @@ "win32" ] }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3883,17 +3531,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -3983,40 +3620,39 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -4028,42 +3664,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", "pathe": "^2.0.3" }, "funding": { @@ -4071,25 +3707,28 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4348,11 +3987,18 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, "engines": { "node": ">=18" } @@ -4400,8 +4046,18 @@ "node": ">=8" } }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" @@ -4485,13 +4141,6 @@ "simple-wcswidth": "^1.1.2" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4534,6 +4183,16 @@ "node": ">=0.10.0" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4543,16 +4202,6 @@ "node": ">=0.4.0" } }, - "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==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -4644,9 +4293,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", - "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -5281,6 +4930,13 @@ "base64-js": "^1.5.1" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -5346,279 +5002,6 @@ } } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5667,6 +5050,13 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5847,20 +5237,6 @@ "node": ">= 6" } }, - "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/ollama": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", @@ -5967,6 +5343,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6213,40 +5599,6 @@ "node": ">=14" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, "node_modules/rollup": { "version": "4.55.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", @@ -6371,9 +5723,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -6427,6 +5779,19 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -6598,10 +5963,30 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -6818,17 +6203,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", - "tinyglobby": "^0.2.17" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -6844,10 +6230,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", + "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -6860,18 +6245,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -6895,80 +6277,89 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@opentelemetry/api": { + "@types/debug": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { + "@vitest/browser": { "optional": true }, "@vitest/ui": { @@ -6979,22 +6370,9 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false } } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 37c87e1..b48d397 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,6 @@ "ts-node": "^10.9.2", "tsup": "^8.5.1", "typescript": "^5.9.3", - "vitest": "^4.1.10" + "vitest": "^3.2.4" } } diff --git a/src/api/prompts/api.test.ts b/src/api/prompts/api.test.ts index e7c8e4f..9044eb5 100644 --- a/src/api/prompts/api.test.ts +++ b/src/api/prompts/api.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Config } from "../../config"; import { Prompts } from "./api"; import { PromptsHttpClient } from "./client"; @@ -16,6 +16,10 @@ describe("Prompts.getPrompt caching", () => { } as unknown as PromptsHttpClient; }); + afterEach(() => { + vi.useRealTimers(); + }); + it("calls HTTP on every request when useCache is omitted", async () => { getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); @@ -83,6 +87,27 @@ describe("Prompts.getPrompt caching", () => { expect(getPromptVersion).toHaveBeenCalledTimes(2); }); + it("expires per-call cacheTtl before the global default TTL", async () => { + vi.useFakeTimers(); + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + await prompts.getPrompt({ + name: "my-prompt", + useCache: true, + cacheTtl: 1, + }); + expect(getPromptVersion).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1001); + + await prompts.getPrompt({ + name: "my-prompt", + useCache: true, + cacheTtl: 1, + }); + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); + it("hits HTTP again after clearCache when useCache is true", async () => { getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); diff --git a/src/index.shutdown-cache.test.ts b/src/index.shutdown-cache.test.ts index 5b43d97..f34bbcb 100644 --- a/src/index.shutdown-cache.test.ts +++ b/src/index.shutdown-cache.test.ts @@ -32,6 +32,11 @@ describe("Netra.shutdown cache clearing", () => { await Netra.shutdown(); + await Netra.init({ cacheTtlSeconds: 60 }); + (Netra.prompts as unknown as { client: PromptsHttpClient }).client = { + getPromptVersion, + } as unknown as PromptsHttpClient; + await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true }); expect(getPromptVersion).toHaveBeenCalledTimes(2); }); From 309c3ab3927532badb09af73d9be08964b48f880 Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:09:47 +0530 Subject: [PATCH 3/6] feat(models): add getModelPricing with opt-in TTL cache --- src/api/index.ts | 8 ++ src/api/models/api.test.ts | 136 +++++++++++++++++++++++++++++++ src/api/models/api.ts | 54 ++++++++++++ src/api/models/client.ts | 49 +++++++++++ src/api/models/index.ts | 13 +++ src/api/models/models.ts | 33 ++++++++ src/index.shutdown-cache.test.ts | 32 ++++++++ src/index.ts | 17 +++- 8 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 src/api/models/api.test.ts create mode 100644 src/api/models/api.ts create mode 100644 src/api/models/client.ts create mode 100644 src/api/models/index.ts create mode 100644 src/api/models/models.ts diff --git a/src/api/index.ts b/src/api/index.ts index 6dc1451..0e555e2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -68,3 +68,11 @@ export type { // Prompts API export { Prompts } from "./prompts"; export type { GetPromptParams, PromptResponse } from "./prompts"; + +// Models API +export { Models, MODEL_PRICING_CACHE_TTL_SECONDS } from "./models"; +export type { + GetModelPricingParams, + ModelPrice, + ModelPricing, +} from "./models"; diff --git a/src/api/models/api.test.ts b/src/api/models/api.test.ts new file mode 100644 index 0000000..a640c87 --- /dev/null +++ b/src/api/models/api.test.ts @@ -0,0 +1,136 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Config } from "../../config"; +import { Models } from "./api"; +import { ModelsHttpClient } from "./client"; +import { ModelPricing } from "./models"; + +const samplePricing: ModelPricing[] = [ + { + name: "gpt-4", + projectId: null, + matchPattern: "gpt-4*", + prices: [ + { + usageType: "input", + minUnits: 0, + maxUnits: 1000, + price: 0.03, + unitValue: 1000, + }, + ], + }, +]; + +describe("Models.getModelPricing caching", () => { + let models: Models; + let getModelPricing: ReturnType; + + beforeEach(() => { + const config = new Config({ cacheTtlSeconds: 60 }); + models = new Models(config); + getModelPricing = vi.fn(); + (models as unknown as { client: ModelsHttpClient }).client = { + getModelPricing, + } as unknown as ModelsHttpClient; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("calls HTTP on every request when useCache is omitted", async () => { + getModelPricing.mockResolvedValue(samplePricing); + + await models.getModelPricing(); + await models.getModelPricing(); + + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); + + it("serves from cache on second call with same name when useCache is true", async () => { + getModelPricing.mockResolvedValue(samplePricing); + + const first = await models.getModelPricing({ + name: "gpt-4", + useCache: true, + }); + const second = await models.getModelPricing({ + name: "gpt-4", + useCache: true, + }); + + expect(getModelPricing).toHaveBeenCalledTimes(1); + expect(getModelPricing).toHaveBeenCalledWith("gpt-4"); + expect(first).toEqual(samplePricing); + expect(second).toEqual(samplePricing); + }); + + it("keeps separate cache entries for different name and all", async () => { + const allPricing: ModelPricing[] = []; + getModelPricing + .mockResolvedValueOnce(samplePricing) + .mockResolvedValueOnce(allPricing); + + const named = await models.getModelPricing({ + name: "gpt-4", + useCache: true, + }); + const all = await models.getModelPricing({ useCache: true }); + + expect(getModelPricing).toHaveBeenCalledTimes(2); + expect(getModelPricing).toHaveBeenNthCalledWith(1, "gpt-4"); + expect(getModelPricing).toHaveBeenNthCalledWith(2, undefined); + expect(named).toEqual(samplePricing); + expect(all).toEqual(allPricing); + }); + + it("does not cache null API responses", async () => { + getModelPricing.mockResolvedValue(null); + + await models.getModelPricing({ useCache: true }); + await models.getModelPricing({ useCache: true }); + + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); + + it("caches empty arrays as successful responses", async () => { + getModelPricing.mockResolvedValue([]); + + await models.getModelPricing({ useCache: true }); + await models.getModelPricing({ useCache: true }); + + expect(getModelPricing).toHaveBeenCalledTimes(1); + }); + + it("ignores cache when useCache is false even if cacheTtl is set", async () => { + getModelPricing.mockResolvedValue(samplePricing); + + await models.getModelPricing({ useCache: false, cacheTtl: 30 }); + await models.getModelPricing({ useCache: false, cacheTtl: 30 }); + + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); + + it("expires per-call cacheTtl before the models default TTL", async () => { + vi.useFakeTimers(); + getModelPricing.mockResolvedValue(samplePricing); + + await models.getModelPricing({ useCache: true, cacheTtl: 1 }); + expect(getModelPricing).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1001); + + await models.getModelPricing({ useCache: true, cacheTtl: 1 }); + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); + + it("hits HTTP again after clearCache when useCache is true", async () => { + getModelPricing.mockResolvedValue(samplePricing); + + await models.getModelPricing({ useCache: true }); + models.clearCache(); + await models.getModelPricing({ useCache: true }); + + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/api/models/api.ts b/src/api/models/api.ts new file mode 100644 index 0000000..f7ee692 --- /dev/null +++ b/src/api/models/api.ts @@ -0,0 +1,54 @@ +import { TTLCache } from "../../cache"; +import { Config } from "../../config"; +import { ModelsHttpClient } from "./client"; +import { + GetModelPricingParams, + MODEL_PRICING_CACHE_TTL_SECONDS, + ModelPricing, +} from "./models"; + +export class Models { + private config: Config; + private client: ModelsHttpClient; + private cache: TTLCache; + + constructor(config: Config) { + this.config = config; + this.client = new ModelsHttpClient(config); + this.cache = new TTLCache(MODEL_PRICING_CACHE_TTL_SECONDS); + } + + /** Clear all cached model pricing entries. */ + clearCache(): void { + this.cache.clear(); + } + + /** + * Fetch model pricing from the backend. + * + * @param params.name - Optional model name filter + * @param params.useCache - When true, read/write the in-memory cache (default: false) + * @param params.cacheTtl - Per-call cache TTL in seconds (default: MODEL_PRICING_CACHE_TTL_SECONDS) + */ + async getModelPricing( + params: GetModelPricingParams = {}, + ): Promise { + const useCache = params.useCache === true; + const cacheKey = `model:pricing:${params.name ?? "all"}`; + + if (useCache) { + const cached = this.cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + } + + const data = await this.client.getModelPricing(params.name); + + if (data !== null && useCache) { + this.cache.set(cacheKey, data, params.cacheTtl); + } + + return data; + } +} diff --git a/src/api/models/client.ts b/src/api/models/client.ts new file mode 100644 index 0000000..9dfa4d3 --- /dev/null +++ b/src/api/models/client.ts @@ -0,0 +1,49 @@ +/** + * Internal HTTP client for Models APIs + */ + +import { Config } from "../../config"; +import { Logger } from "../../logger"; +import { NetraHttpClient } from "../http-client"; +import { ModelPricing } from "./models"; + +export class ModelsHttpClient extends NetraHttpClient { + constructor(config: Config) { + super(config, "NETRA_MODELS_TIMEOUT", 10.0); + } + + async getModelPricing(name?: string): Promise { + if (!this.isInitialized()) { + Logger.error( + "netra.models: Models client is not initialized; cannot fetch model pricing", + ); + return null; + } + + try { + const params = name ? { name } : undefined; + const response = await this.get("/sdk/models", params); + + if (!response.ok) { + const errorMessage = response.data?.error?.message ?? "Unknown error"; + Logger.error( + `netra.models: Failed to fetch model pricing: ${errorMessage}`, + ); + return null; + } + + const items = this.extractData(response, null); + if (items !== null && !Array.isArray(items)) { + Logger.error( + "netra.models: Unexpected response format; expected an array", + ); + return null; + } + return items; + } catch (err: any) { + const message = err?.response?.data?.error?.message ?? err?.message ?? ""; + Logger.error("netra.models: Failed to fetch model pricing:", message); + return null; + } + } +} diff --git a/src/api/models/index.ts b/src/api/models/index.ts new file mode 100644 index 0000000..138c4b9 --- /dev/null +++ b/src/api/models/index.ts @@ -0,0 +1,13 @@ +/** + * Models API exports + */ + +export { Models } from "./api"; + +export type { + GetModelPricingParams, + ModelPrice, + ModelPricing, +} from "./models"; + +export { MODEL_PRICING_CACHE_TTL_SECONDS } from "./models"; diff --git a/src/api/models/models.ts b/src/api/models/models.ts new file mode 100644 index 0000000..40ea2e1 --- /dev/null +++ b/src/api/models/models.ts @@ -0,0 +1,33 @@ +/** + * Models API Models + */ + +/** Default TTL (seconds) for model pricing cache when useCache is true and cacheTtl is omitted. */ +export const MODEL_PRICING_CACHE_TTL_SECONDS = 300; + +export interface ModelPrice { + usageType: string; + minUnits: number; + maxUnits: number; + price: number; + unitValue: number; +} + +export interface ModelPricing { + name: string; + projectId: string | null; + matchPattern: string; + prices: ModelPrice[]; +} + +export interface GetModelPricingParams { + name?: string; + /** When true, read/write in-memory cache (default: false). */ + useCache?: boolean; + /** + * Per-call TTL in seconds. + * When omitted with useCache: true, uses MODEL_PRICING_CACHE_TTL_SECONDS (300), + * NOT Config.cacheTtlSeconds. + */ + cacheTtl?: number; +} diff --git a/src/index.shutdown-cache.test.ts b/src/index.shutdown-cache.test.ts index f34bbcb..b37ddf2 100644 --- a/src/index.shutdown-cache.test.ts +++ b/src/index.shutdown-cache.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { ModelsHttpClient } from "./api/models/client"; import { PromptsHttpClient } from "./api/prompts/client"; vi.mock("./instrumentation", () => ({ @@ -40,4 +41,35 @@ describe("Netra.shutdown cache clearing", () => { await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true }); expect(getPromptVersion).toHaveBeenCalledTimes(2); }); + + it("clears models cache on shutdown so the next cached getModelPricing hits HTTP", async () => { + await Netra.init({ cacheTtlSeconds: 60 }); + + const pricing = [ + { + name: "gpt-4", + projectId: null, + matchPattern: "gpt-4*", + prices: [], + }, + ]; + const getModelPricing = vi.fn().mockResolvedValue(pricing); + (Netra.models as unknown as { client: ModelsHttpClient }).client = { + getModelPricing, + } as unknown as ModelsHttpClient; + + await Netra.models.getModelPricing({ useCache: true }); + await Netra.models.getModelPricing({ useCache: true }); + expect(getModelPricing).toHaveBeenCalledTimes(1); + + await Netra.shutdown(); + + await Netra.init({ cacheTtlSeconds: 60 }); + (Netra.models as unknown as { client: ModelsHttpClient }).client = { + getModelPricing, + } as unknown as ModelsHttpClient; + + await Netra.models.getModelPricing({ useCache: true }); + expect(getModelPricing).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/index.ts b/src/index.ts index 29a74df..4fbf8dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { context, Span, SpanKind, trace } from "@opentelemetry/api"; import { createRequire } from "module"; -import { Prompts, Dashboard, Evaluation, Usage } from "./api"; +import { Prompts, Dashboard, Evaluation, Usage, Models } from "./api"; import { Config, NetraConfig } from "./config"; import { initInstrumentations, instrumentationsReady, uninstrumentAll } from "./instrumentation"; import { Logger } from "./logger"; @@ -66,6 +66,9 @@ export { Usage, // Prompts API Prompts, + // Models API + Models, + MODEL_PRICING_CACHE_TTL_SECONDS, } from "./api"; export type { @@ -105,6 +108,10 @@ export type { TraceSummary, GetPromptParams, PromptResponse, + // Models API + GetModelPricingParams, + ModelPrice, + ModelPricing, } from "./api"; // Export simulation types and classes @@ -141,6 +148,7 @@ export class Netra { static dashboard: Dashboard; static simulation: Simulation; static prompts: Prompts; + static models: Models; static getConfig(): Config { if (!this._config) { @@ -212,6 +220,12 @@ export class Netra { Logger.warn("Netra: failed to initialize prompts client:", e); } + try { + this.models = new Models(cfg); + } catch (e) { + Logger.warn("Netra: failed to initialize models client:", e); + } + this._initialized = true; Logger.info("Netra successfully initialized."); @@ -344,6 +358,7 @@ export class Netra { try { this.prompts?.clearCache(); + this.models?.clearCache(); } catch (e) { Logger.error("Error clearing SDK API caches:", e); } From 044b0d402c2900f676240c615714f0022c76b147 Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:40:22 +0530 Subject: [PATCH 4/6] test(models): cover getModelPricing HTTP client failure paths Co-authored-by: Cursor --- src/api/models/client.test.ts | 169 ++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/api/models/client.test.ts diff --git a/src/api/models/client.test.ts b/src/api/models/client.test.ts new file mode 100644 index 0000000..fc15d7b --- /dev/null +++ b/src/api/models/client.test.ts @@ -0,0 +1,169 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Config } from "../../config"; +import { Logger } from "../../logger"; +import { ModelsHttpClient } from "./client"; +import { ModelPricing } from "./models"; + +const samplePricing: ModelPricing[] = [ + { + name: "gpt-4", + projectId: null, + matchPattern: "gpt-4*", + prices: [ + { + usageType: "input", + minUnits: 0, + maxUnits: 1000, + price: 0.03, + unitValue: 1000, + }, + ], + }, +]; + +describe("ModelsHttpClient.getModelPricing", () => { + let client: ModelsHttpClient; + let get: ReturnType; + let isInitialized: ReturnType; + let logError: ReturnType; + + beforeEach(() => { + client = new ModelsHttpClient(new Config({ cacheTtlSeconds: 60 })); + get = vi.spyOn(client, "get"); + isInitialized = vi.spyOn(client, "isInitialized").mockReturnValue(true); + logError = vi.spyOn(Logger, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("when the request succeeds", () => { + it("returns the pricing array from an enveloped API response", async () => { + get.mockResolvedValue({ + ok: true, + status: 200, + data: { data: samplePricing }, + }); + + const result = await client.getModelPricing(); + + expect(result).toEqual(samplePricing); + expect(get).toHaveBeenCalledWith("/sdk/models", undefined); + }); + + it("calls GET /sdk/models with the name query param when name is provided", async () => { + get.mockResolvedValue({ + ok: true, + status: 200, + data: { data: samplePricing }, + }); + + await client.getModelPricing("gpt-4"); + + expect(get).toHaveBeenCalledWith("/sdk/models", { name: "gpt-4" }); + }); + + it("calls GET /sdk/models with no query params when name is omitted", async () => { + get.mockResolvedValue({ + ok: true, + status: 200, + data: { data: samplePricing }, + }); + + await client.getModelPricing(); + + expect(get).toHaveBeenCalledWith("/sdk/models", undefined); + }); + + it("returns an empty array when the API returns an empty list", async () => { + get.mockResolvedValue({ + ok: true, + status: 200, + data: { data: [] }, + }); + + const result = await client.getModelPricing(); + + expect(result).toEqual([]); + expect(logError).not.toHaveBeenCalled(); + }); + }); + + describe("when the client is not initialized", () => { + it("returns null without calling GET", async () => { + isInitialized.mockReturnValue(false); + + const result = await client.getModelPricing("gpt-4"); + + expect(result).toBeNull(); + expect(get).not.toHaveBeenCalled(); + expect(logError).toHaveBeenCalledWith( + "netra.models: Models client is not initialized; cannot fetch model pricing", + ); + }); + }); + + describe("when the HTTP response fails", () => { + it("returns null and logs the API error message when response is not ok", async () => { + get.mockResolvedValue({ + ok: false, + status: 500, + data: { error: { message: "backend unavailable" } }, + }); + + const result = await client.getModelPricing(); + + expect(result).toBeNull(); + expect(logError).toHaveBeenCalledWith( + "netra.models: Failed to fetch model pricing: backend unavailable", + ); + }); + + it("returns null and logs a generic message when the error payload has no message", async () => { + get.mockResolvedValue({ + ok: false, + status: 502, + data: {}, + }); + + const result = await client.getModelPricing(); + + expect(result).toBeNull(); + expect(logError).toHaveBeenCalledWith( + "netra.models: Failed to fetch model pricing: Unknown error", + ); + }); + }); + + describe("when the response payload is unexpected", () => { + it("returns null and logs when the unwrapped payload is not an array", async () => { + get.mockResolvedValue({ + ok: true, + status: 200, + data: { data: { name: "not-an-array" } }, + }); + + const result = await client.getModelPricing(); + + expect(result).toBeNull(); + expect(logError).toHaveBeenCalledWith( + "netra.models: Unexpected response format; expected an array", + ); + }); + }); + + describe("when GET throws", () => { + it("returns null and logs the thrown error message", async () => { + get.mockRejectedValue(new Error("network down")); + + const result = await client.getModelPricing(); + + expect(result).toBeNull(); + expect(logError).toHaveBeenCalledWith( + "netra.models: Failed to fetch model pricing:", + "network down", + ); + }); + }); +}); From cc2578f2a9a76bb5d735bee7904ac969760f3e86 Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:58:37 +0530 Subject: [PATCH 5/6] NET-1329 simplify cache TTL to module const and per-call override (#112) * NET-1329 simplify cache TTL to module const and per-call override Co-authored-by: Cursor * test: cover prompt default TTL and drop removed cacheTtlSeconds Assert PROMPT_CACHE_TTL_SECONDS expiry when cacheTtl is omitted, and stop passing the removed Config option in models client tests. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- CHANGELOG.md | 6 ++++++ README.md | 6 ++---- src/api/index.ts | 2 +- src/api/models/api.test.ts | 2 +- src/api/models/client.test.ts | 2 +- src/api/models/models.ts | 3 +-- src/api/prompts/api.test.ts | 21 +++++++++++++++++++-- src/api/prompts/api.ts | 10 +++++++--- src/api/prompts/index.ts | 2 +- src/api/prompts/models.ts | 8 +++++++- src/config.ts | 26 -------------------------- src/index.shutdown-cache.test.ts | 8 ++++---- src/index.ts | 3 +-- 13 files changed, 51 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441524d..3335431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **Prompt cache TTL** — Default TTL is the module constant `PROMPT_CACHE_TTL_SECONDS` (60). Override per call with `cacheTtl`. Removed unused `cacheTtlSeconds` init config and `NETRA_CACHE_TTL_SECONDS` env var. + ## [1.1.0-beta.1] - 2026-06-09 ### Fixed diff --git a/README.md b/README.md index 079be41..d3bf196 100644 --- a/README.md +++ b/README.md @@ -189,20 +189,19 @@ async function generateContent(prompt: string) { Fetch prompt versions from Prompt Studio via `Netra.prompts.getPrompt()`. Caching is **opt-in per call** — omit `useCache` (or set it to `false`) to always hit the API. -Set a global default TTL at init with `cacheTtlSeconds` (default: **60** seconds), or override TTL for a single call with `cacheTtl`. +Default TTL is **60 seconds** (`PROMPT_CACHE_TTL_SECONDS`). Override TTL for a single call with `cacheTtl`. ```typescript import { Netra } from "netra-sdk"; await Netra.init({ appName: "my-ai-app", - cacheTtlSeconds: 60, // optional; env: NETRA_CACHE_TTL_SECONDS }); // Always fetches from the API (default) const prompt = await Netra.prompts.getPrompt({ name: "my-prompt" }); -// Cached for 60s (global default TTL) +// Cached for 60s (default TTL) const cached = await Netra.prompts.getPrompt({ name: "my-prompt", useCache: true, @@ -229,7 +228,6 @@ You can configure the SDK using environment variables: | `NETRA_APP_NAME` | Name of your application | | `NETRA_ENV` | Environment (e.g., prod, dev) | | `NETRA_TRACE_CONTENT` | Capture prompt/completion content (default: true) | -| `NETRA_CACHE_TTL_SECONDS` | Default TTL in seconds for opt-in SDK read caches (default: 60) | ## 🤝 License diff --git a/src/api/index.ts b/src/api/index.ts index 0e555e2..249df25 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -66,7 +66,7 @@ export type { } from "./dashboard"; // Prompts API -export { Prompts } from "./prompts"; +export { Prompts, PROMPT_CACHE_TTL_SECONDS } from "./prompts"; export type { GetPromptParams, PromptResponse } from "./prompts"; // Models API diff --git a/src/api/models/api.test.ts b/src/api/models/api.test.ts index a640c87..3acf8ff 100644 --- a/src/api/models/api.test.ts +++ b/src/api/models/api.test.ts @@ -26,7 +26,7 @@ describe("Models.getModelPricing caching", () => { let getModelPricing: ReturnType; beforeEach(() => { - const config = new Config({ cacheTtlSeconds: 60 }); + const config = new Config(); models = new Models(config); getModelPricing = vi.fn(); (models as unknown as { client: ModelsHttpClient }).client = { diff --git a/src/api/models/client.test.ts b/src/api/models/client.test.ts index fc15d7b..3fe917d 100644 --- a/src/api/models/client.test.ts +++ b/src/api/models/client.test.ts @@ -28,7 +28,7 @@ describe("ModelsHttpClient.getModelPricing", () => { let logError: ReturnType; beforeEach(() => { - client = new ModelsHttpClient(new Config({ cacheTtlSeconds: 60 })); + client = new ModelsHttpClient(new Config()); get = vi.spyOn(client, "get"); isInitialized = vi.spyOn(client, "isInitialized").mockReturnValue(true); logError = vi.spyOn(Logger, "error").mockImplementation(() => {}); diff --git a/src/api/models/models.ts b/src/api/models/models.ts index 40ea2e1..4ccf3a8 100644 --- a/src/api/models/models.ts +++ b/src/api/models/models.ts @@ -26,8 +26,7 @@ export interface GetModelPricingParams { useCache?: boolean; /** * Per-call TTL in seconds. - * When omitted with useCache: true, uses MODEL_PRICING_CACHE_TTL_SECONDS (300), - * NOT Config.cacheTtlSeconds. + * When omitted with useCache: true, uses MODEL_PRICING_CACHE_TTL_SECONDS (300). */ cacheTtl?: number; } diff --git a/src/api/prompts/api.test.ts b/src/api/prompts/api.test.ts index 9044eb5..75294de 100644 --- a/src/api/prompts/api.test.ts +++ b/src/api/prompts/api.test.ts @@ -2,13 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Config } from "../../config"; import { Prompts } from "./api"; import { PromptsHttpClient } from "./client"; +import { PROMPT_CACHE_TTL_SECONDS } from "./models"; describe("Prompts.getPrompt caching", () => { let prompts: Prompts; let getPromptVersion: ReturnType; beforeEach(() => { - const config = new Config({ cacheTtlSeconds: 60 }); + const config = new Config(); prompts = new Prompts(config); getPromptVersion = vi.fn(); (prompts as unknown as { client: PromptsHttpClient }).client = { @@ -87,7 +88,7 @@ describe("Prompts.getPrompt caching", () => { expect(getPromptVersion).toHaveBeenCalledTimes(2); }); - it("expires per-call cacheTtl before the global default TTL", async () => { + it("expires per-call cacheTtl before the module default TTL", async () => { vi.useFakeTimers(); getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); @@ -108,6 +109,22 @@ describe("Prompts.getPrompt caching", () => { expect(getPromptVersion).toHaveBeenCalledTimes(2); }); + it("expires cached prompts after PROMPT_CACHE_TTL_SECONDS when cacheTtl is omitted", async () => { + vi.useFakeTimers(); + getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); + + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + expect(getPromptVersion).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(PROMPT_CACHE_TTL_SECONDS * 1000 - 1); + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + expect(getPromptVersion).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(2); + await prompts.getPrompt({ name: "my-prompt", useCache: true }); + expect(getPromptVersion).toHaveBeenCalledTimes(2); + }); + it("hits HTTP again after clearCache when useCache is true", async () => { getPromptVersion.mockResolvedValue({ data: { template: "v1" } }); diff --git a/src/api/prompts/api.ts b/src/api/prompts/api.ts index 90c3423..3d8ca6d 100644 --- a/src/api/prompts/api.ts +++ b/src/api/prompts/api.ts @@ -2,7 +2,11 @@ import { TTLCache } from "../../cache"; import { Config } from "../../config"; import { Logger } from "../../logger"; import { PromptsHttpClient } from "./client"; -import { GetPromptParams, PromptResponse } from "./models"; +import { + GetPromptParams, + PROMPT_CACHE_TTL_SECONDS, + PromptResponse, +} from "./models"; export class Prompts { private config: Config; @@ -12,7 +16,7 @@ export class Prompts { constructor(config: Config) { this.config = config; this.client = new PromptsHttpClient(config); - this.cache = new TTLCache(config.cacheTtlSeconds); + this.cache = new TTLCache(PROMPT_CACHE_TTL_SECONDS); } /** Clear all cached prompt entries. */ @@ -26,7 +30,7 @@ export class Prompts { * @param params.name - Name of the prompt (required) * @param params.label - Label of the prompt version (default: "production") * @param params.useCache - When true, read/write the in-memory cache (default: false) - * @param params.cacheTtl - Per-call cache TTL in seconds (default: init cacheTtlSeconds) + * @param params.cacheTtl - Per-call cache TTL in seconds (default: PROMPT_CACHE_TTL_SECONDS) */ async getPrompt(params: GetPromptParams): Promise { if (!params || typeof params.name !== "string" || !params.name) { diff --git a/src/api/prompts/index.ts b/src/api/prompts/index.ts index bedb08f..0847ecf 100644 --- a/src/api/prompts/index.ts +++ b/src/api/prompts/index.ts @@ -3,5 +3,5 @@ */ export { Prompts } from "./api"; - +export { PROMPT_CACHE_TTL_SECONDS } from "./models"; export type { GetPromptParams, PromptResponse } from "./models"; diff --git a/src/api/prompts/models.ts b/src/api/prompts/models.ts index ae9c1d8..50ed7cc 100644 --- a/src/api/prompts/models.ts +++ b/src/api/prompts/models.ts @@ -2,12 +2,18 @@ * Prompts API Models */ +/** Default TTL (seconds) for prompt cache when useCache is true and cacheTtl is omitted. */ +export const PROMPT_CACHE_TTL_SECONDS = 60; + export interface GetPromptParams { name: string; label?: string; /** When true, serve from in-memory cache when available (default: false). */ useCache?: boolean; - /** Per-call TTL in seconds; falls back to init `cacheTtlSeconds` when omitted. */ + /** + * Per-call TTL in seconds. + * When omitted with useCache: true, uses PROMPT_CACHE_TTL_SECONDS (60). + */ cacheTtl?: number; } diff --git a/src/config.ts b/src/config.ts index 5cdbed4..9bf08b5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,8 +19,6 @@ export interface NetraConfig { instruments?: Set; blockInstruments?: Set; rootInstruments?: Set; - /** Default TTL in seconds for opt-in SDK read caches (env: NETRA_CACHE_TTL_SECONDS). */ - cacheTtlSeconds?: number; } export enum NetraInstruments { @@ -130,7 +128,6 @@ export class Config { environment: string; resourceAttributes: Record; blockedSpans?: string[]; - cacheTtlSeconds: number; constructor(config: NetraConfig = {}) { this.appName = this._getAppName(config.appName); @@ -167,11 +164,6 @@ export class Config { config.resourceAttributes, ); this.blockedSpans = config.blockedSpans; - this.cacheTtlSeconds = this._getIntConfig( - config.cacheTtlSeconds, - "NETRA_CACHE_TTL_SECONDS", - 60, - ); this._validateApiKey(); this._setupAuthentication(); @@ -251,24 +243,6 @@ export class Config { } } - private _getIntConfig( - param: number | undefined, - envVar: string, - defaultValue: number, - ): number { - if (param !== undefined) { - return param; - } - - const envValue = process.env[envVar]; - if (envValue === undefined) { - return defaultValue; - } - - const parsed = parseInt(envValue, 10); - return Number.isNaN(parsed) ? defaultValue : parsed; - } - private _getBoolConfig( param: boolean | undefined, envVar: string, diff --git a/src/index.shutdown-cache.test.ts b/src/index.shutdown-cache.test.ts index b37ddf2..ed9076e 100644 --- a/src/index.shutdown-cache.test.ts +++ b/src/index.shutdown-cache.test.ts @@ -18,7 +18,7 @@ describe("Netra.shutdown cache clearing", () => { }); it("clears prompts cache on shutdown so the next cached getPrompt hits HTTP", async () => { - await Netra.init({ cacheTtlSeconds: 60 }); + await Netra.init({}); const getPromptVersion = vi .fn() @@ -33,7 +33,7 @@ describe("Netra.shutdown cache clearing", () => { await Netra.shutdown(); - await Netra.init({ cacheTtlSeconds: 60 }); + await Netra.init({}); (Netra.prompts as unknown as { client: PromptsHttpClient }).client = { getPromptVersion, } as unknown as PromptsHttpClient; @@ -43,7 +43,7 @@ describe("Netra.shutdown cache clearing", () => { }); it("clears models cache on shutdown so the next cached getModelPricing hits HTTP", async () => { - await Netra.init({ cacheTtlSeconds: 60 }); + await Netra.init({}); const pricing = [ { @@ -64,7 +64,7 @@ describe("Netra.shutdown cache clearing", () => { await Netra.shutdown(); - await Netra.init({ cacheTtlSeconds: 60 }); + await Netra.init({}); (Netra.models as unknown as { client: ModelsHttpClient }).client = { getModelPricing, } as unknown as ModelsHttpClient; diff --git a/src/index.ts b/src/index.ts index 4fbf8dc..87aaffe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ export { Usage, // Prompts API Prompts, + PROMPT_CACHE_TTL_SECONDS, // Models API Models, MODEL_PRICING_CACHE_TTL_SECONDS, @@ -163,8 +164,6 @@ export class Netra { /** * Initialize the Netra SDK. - * - * @param config.cacheTtlSeconds - Default TTL in seconds for opt-in read caches (default: 60, env: NETRA_CACHE_TTL_SECONDS) */ static async init(config: NetraConfig = {}): Promise { if (this._initialized) { From a4663e3afe1febc08e720fe12e2cd329f80fc4ba Mon Sep 17 00:00:00 2001 From: Akhilesh Nair <72593014+AkhileshNair2201@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:31:39 +0530 Subject: [PATCH 6/6] docs: document Models API and expand caching changelog Co-authored-by: Cursor --- CHANGELOG.md | 7 +++++++ README.md | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3335431..f587592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Opt-in prompt caching** — `Netra.prompts.getPrompt()` accepts `useCache` and `cacheTtl`. When `useCache` is true, responses are served from an in-memory TTL cache (default TTL: `PROMPT_CACHE_TTL_SECONDS` = 60). Caching is off by default. +- **Models API** — `Netra.models.getModelPricing()` fetches model pricing (optional `name` filter) with the same opt-in cache pattern (`useCache`, `cacheTtl`; default TTL: `MODEL_PRICING_CACHE_TTL_SECONDS` = 300). +- **Cache lifecycle** — `Netra.shutdown()` clears prompts and models in-memory caches. `clearCache()` is also available on each client. +- **Exported cache constants** — `PROMPT_CACHE_TTL_SECONDS` and `MODEL_PRICING_CACHE_TTL_SECONDS` are public exports. + ### Changed - **Prompt cache TTL** — Default TTL is the module constant `PROMPT_CACHE_TTL_SECONDS` (60). Override per call with `cacheTtl`. Removed unused `cacheTtlSeconds` init config and `NETRA_CACHE_TTL_SECONDS` env var. diff --git a/README.md b/README.md index d3bf196..8a2b4a3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - 🔧 **Multi-Provider Support**: Works with OpenAI, Google GenAI, Mistral, Anthropic, and more - 📈 **Session Management**: Track user sessions and custom attributes - 🌐 **Automatic Instrumentation**: Zero-code instrumentation for popular frameworks and libraries -- ⚡ **Opt-in Read Caching**: In-memory TTL caching for read-heavy SDK calls (e.g. prompt fetches) +- ⚡ **Opt-in Read Caching**: In-memory TTL caching for read-heavy SDK calls (`getPrompt`, `getModelPricing`) ## 📦 Installation @@ -218,6 +218,40 @@ const shortLived = await Netra.prompts.getPrompt({ > **Note**: Cached prompts may be stale for up to the TTL after dashboard edits. Use `useCache: false` when you need the latest version immediately. `Netra.shutdown()` clears in-memory caches. +## 💰 Models API + +Fetch model pricing via `Netra.models.getModelPricing()`. Caching is **opt-in per call** — omit `useCache` (or set it to `false`) to always hit the API. + +Default TTL is **300 seconds** (`MODEL_PRICING_CACHE_TTL_SECONDS`). Override TTL for a single call with `cacheTtl`. + +```typescript +import { Netra } from "netra-sdk"; + +await Netra.init({ + appName: "my-ai-app", +}); + +// Always fetches from the API (default) +const pricing = await Netra.models.getModelPricing(); + +// Optional name filter +const gptPricing = await Netra.models.getModelPricing({ name: "gpt-4o" }); + +// Cached for 300s (default TTL) +const cached = await Netra.models.getModelPricing({ + useCache: true, +}); + +// Cached for 60s for this call only +const shortLived = await Netra.models.getModelPricing({ + name: "gpt-4o", + useCache: true, + cacheTtl: 60, +}); +``` + +> **Note**: Cached pricing may be stale for up to the TTL after dashboard edits. Use `useCache: false` when you need the latest values immediately. `Netra.shutdown()` clears in-memory caches. + ## 🔧 Environment Variables You can configure the SDK using environment variables: