From 91a8ed9d53540e9c034447efeedb7601e8f2d874 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Wed, 4 Feb 2026 08:37:48 +0000 Subject: [PATCH 01/17] feat: add OpenAI embedding support Adds support for using OpenAI's text-embedding-3-small model as an alternative to local llama-cpp embeddings. Changes: - New openai-llm.ts: OpenAI API client implementing LLM interface - llm.ts: Embedding config management, getDefaultEmbeddingLLM() - collections.ts: EmbeddingProviderConfig for YAML config schema - store.ts: Use configurable embedding LLM, skip local model for query expansion/rerank when using OpenAI - qmd.ts: Load embedding config on startup - package.json: Add openai dependency - README.md: Documentation for OpenAI embeddings Configuration (in ~/.config/qmd/index.yml): embedding: provider: openai openai: api_key: sk-... # Optional, falls back to OPENAI_API_KEY env model: text-embedding-3-small # Optional, this is the default Benefits: - Much faster embedding (~10x vs local models on CPU) - No GPU/VRAM requirements - More reliable (no local model loading issues) - Cost: ~$0.02 per 1M tokens --- README.md | 21 ++++ bun.lock | 282 +++++++++++++++++++++++---------------------- package.json | 1 + src/cli/qmd.ts | 15 ++- src/collections.ts | 21 ++++ src/llm.ts | 57 +++++++++ src/openai-llm.ts | 131 +++++++++++++++++++++ src/store.ts | 24 +++- 8 files changed, 412 insertions(+), 140 deletions(-) create mode 100644 src/openai-llm.ts diff --git a/README.md b/README.md index 6f318446b..fecf44514 100644 --- a/README.md +++ b/README.md @@ -515,6 +515,27 @@ Supported model families: > since vectors are not cross-compatible between models. The prompt format is > automatically adjusted for each model family. +### OpenAI Embeddings (Optional) + +As an alternative to local embedding models, you can use OpenAI's API for faster, more reliable embeddings: + +```yaml +# ~/.config/qmd/index.yml +embedding: + provider: openai + openai: + api_key: sk-... # Optional, falls back to OPENAI_API_KEY env var + model: text-embedding-3-small # Optional, this is the default +``` + +Benefits: +- **~10x faster** than local CPU inference +- **No GPU required** - works on any machine +- **More reliable** - no local model loading issues +- **Cost:** ~$0.02 per 1M tokens (very cheap) + +When using OpenAI embeddings, query expansion and reranking are skipped to avoid loading local models. + ## Installation ```sh diff --git a/bun.lock b/bun.lock index a96f09641..106191ff4 100644 --- a/bun.lock +++ b/bun.lock @@ -3,33 +3,29 @@ "configVersion": 1, "workspaces": { "": { - "name": "2025-12-07-bm25-q", + "name": "@tobilu/qmd", "dependencies": { - "@modelcontextprotocol/sdk": "1.29.0", - "better-sqlite3": "12.8.0", - "fast-glob": "3.3.3", - "node-llama-cpp": "3.18.1", - "picomatch": "4.0.4", - "sqlite-vec": "0.1.9", - "web-tree-sitter": "0.26.7", - "yaml": "2.8.3", + "@modelcontextprotocol/sdk": "^1.25.1", + "better-sqlite3": "^12.4.5", + "fast-glob": "^3.3.0", + "node-llama-cpp": "^3.17.1", + "openai": "^4.77.0", + "picomatch": "^4.0.0", + "sqlite-vec": "^0.1.7-alpha.2", + "yaml": "^2.8.2", "zod": "4.2.1", }, "devDependencies": { - "@types/better-sqlite3": "7.6.13", - "tsx": "4.21.0", - "vitest": "3.2.4", + "@types/better-sqlite3": "^7.6.0", + "tsx": "^4.0.0", + "vitest": "^3.0.0", }, "optionalDependencies": { - "sqlite-vec-darwin-arm64": "0.1.9", - "sqlite-vec-darwin-x64": "0.1.9", - "sqlite-vec-linux-arm64": "0.1.9", - "sqlite-vec-linux-x64": "0.1.9", - "sqlite-vec-windows-x64": "0.1.9", - "tree-sitter-go": "0.23.4", - "tree-sitter-python": "0.23.4", - "tree-sitter-rust": "0.24.0", - "tree-sitter-typescript": "0.23.2", + "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2", + "sqlite-vec-darwin-x64": "^0.1.7-alpha.2", + "sqlite-vec-linux-arm64": "^0.1.7-alpha.2", + "sqlite-vec-linux-x64": "^0.1.7-alpha.2", + "sqlite-vec-windows-x64": "^0.1.7-alpha.2", }, "peerDependencies": { "typescript": "^5.9.3", @@ -37,59 +33,59 @@ }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], - "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.6", "", {}, "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA=="], @@ -101,7 +97,7 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="], "@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.18.1", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-rXMgZxUay78FOJV/fJ67apYP9eElH5jd4df5YRKPlLhLHHchuOSyDn+qtyW/L/EnPzpogoLkmULqCkdXU39XsQ=="], @@ -153,57 +149,57 @@ "@reflink/reflink-win32-x64-msvc": ["@reflink/reflink-win32-x64-msvc@0.1.19", "", { "os": "win32", "cpu": "x64" }, "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="], - "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.2", "", {}, "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g=="], + "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], @@ -213,7 +209,9 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -229,9 +227,13 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -245,6 +247,8 @@ "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="], @@ -253,7 +257,7 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -277,7 +281,7 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -291,6 +295,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -301,7 +307,7 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -313,6 +319,8 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -337,7 +345,9 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -347,7 +357,9 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -359,7 +371,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -381,6 +393,12 @@ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -401,7 +419,7 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -413,13 +431,17 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="], + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -451,7 +473,7 @@ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], @@ -501,19 +523,21 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], "node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="], - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-llama-cpp": ["node-llama-cpp@3.18.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.6.2", "chmodrp": "^1.0.2", "cmake-js": "^8.0.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.4", "ignore": "^7.0.4", "ipull": "^3.9.5", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.1.1", "log-symbols": "^7.0.1", "nanoid": "^5.1.6", "node-addon-api": "^8.6.0", "ora": "^9.3.0", "pretty-ms": "^9.3.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.33.0", "slice-ansi": "^8.0.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.2.0", "validate-npm-package-name": "^7.0.2", "which": "^6.0.1", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.18.1", "@node-llama-cpp/linux-armv7l": "3.18.1", "@node-llama-cpp/linux-x64": "3.18.1", "@node-llama-cpp/linux-x64-cuda": "3.18.1", "@node-llama-cpp/linux-x64-cuda-ext": "3.18.1", "@node-llama-cpp/linux-x64-vulkan": "3.18.1", "@node-llama-cpp/mac-arm64-metal": "3.18.1", "@node-llama-cpp/mac-x64": "3.18.1", "@node-llama-cpp/win-arm64": "3.18.1", "@node-llama-cpp/win-x64": "3.18.1", "@node-llama-cpp/win-x64-cuda": "3.18.1", "@node-llama-cpp/win-x64-cuda-ext": "3.18.1", "@node-llama-cpp/win-x64-vulkan": "3.18.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w=="], @@ -527,6 +551,8 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "ora": ["ora@9.3.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.1", "string-width": "^8.1.0" } }, "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], @@ -535,7 +561,7 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -547,7 +573,7 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -559,9 +585,9 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -585,7 +611,7 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -595,11 +621,11 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -631,17 +657,17 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "sqlite-vec": ["sqlite-vec@0.1.9", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" } }, "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA=="], + "sqlite-vec": ["sqlite-vec@0.1.7", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.7", "sqlite-vec-darwin-x64": "0.1.7", "sqlite-vec-linux-arm64": "0.1.7", "sqlite-vec-linux-x64": "0.1.7", "sqlite-vec-windows-x64": "0.1.7" } }, "sha512-1Sge9uRc3B6wDKR4J6sGFi/E2ai9SAU5FenDki3OmhdP/a49PO2Juy1U5yQnx2bZP5t+C3BYJTkG+KkDi3q9Xg=="], - "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg=="], + "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dQ7u4GKPdOPi3IfZ44K7HHdYup2JssM6fuKR9zgqRzW137uFOQmRhbYChNu+ZfW+yhJutsPgfNRFsuWKmy627w=="], - "sqlite-vec-darwin-x64": ["sqlite-vec-darwin-x64@0.1.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA=="], + "sqlite-vec-darwin-x64": ["sqlite-vec-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-MDoczft1BriQcGMEz+CqeSCkB0OsAf12ytZOapS6MaB7zgNzLLSLH6Sxe3yzcPWUyDuCWgK7WzyRIo8u1vAIVA=="], - "sqlite-vec-linux-arm64": ["sqlite-vec-linux-arm64@0.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw=="], + "sqlite-vec-linux-arm64": ["sqlite-vec-linux-arm64@0.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-V429sYT/gwr9PgtT8rbjQd6ls7CFchFpiS45TKSf7rU7wxt9MBmCVorUcheD4kEZb4VeZ6PnFXXCqPMeaHkaUw=="], - "sqlite-vec-linux-x64": ["sqlite-vec-linux-x64@0.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA=="], + "sqlite-vec-linux-x64": ["sqlite-vec-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-wZL+lXeW7y63DLv6FYU6Q4nv2lP5F94cWt7bJCWNiHmZ6NdKIgz/p0QlyuJA/51b8TyoDvsTdusLVlZz9cIh5A=="], - "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.9", "", { "os": "win32", "cpu": "x64" }, "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q=="], + "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-FEZMjMT03irJxwqMQg+A+4hHCiFslxISOAkQ0eYn2lP7GdpppkgYveaT5Xnw/2V+GLq2MXOJb0nDGFNethHSkg=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -665,7 +691,7 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "tar": ["tar@7.5.10", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw=="], + "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], @@ -687,15 +713,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tree-sitter-go": ["tree-sitter-go@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w=="], - - "tree-sitter-javascript": ["tree-sitter-javascript@0.23.1", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA=="], - - "tree-sitter-python": ["tree-sitter-python@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw=="], - - "tree-sitter-rust": ["tree-sitter-rust@0.24.0", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-NWemUDf629Tfc90Y0Z55zuwPCAHkLxWnMf2RznYu4iBkkrQl2o/CHGB7Cr52TyN5F1DAx8FmUnDtCy9iUkXZEQ=="], - - "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], @@ -705,7 +723,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -725,7 +743,11 @@ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "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.2", "std-env": "^3.9.0", "tinybench": "^2.9.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" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - "web-tree-sitter": ["web-tree-sitter@0.26.7", "", {}, "sha512-KiZhelTvBA/ziUHEO7Emb75cGVAq8iGZNabYaZm53Zpy50NsXyOW+xSHlwHt5CVg/TRPZBfeVLTTobF0LjFJ1w=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], @@ -755,11 +777,9 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cmake-js/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "ipull/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ipull/lifecycle-utils": ["lifecycle-utils@2.1.0", "", {}, "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA=="], @@ -767,13 +787,9 @@ "ipull/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - "ipull/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "node-llama-cpp/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], + "openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], @@ -785,18 +801,8 @@ "stdout-update/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "stdout-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -813,9 +819,11 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ipull/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="], - "stdout-update/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/package.json b/package.json index 0ec04c9c0..2b5d7dae4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "better-sqlite3": "12.8.0", "fast-glob": "3.3.3", "node-llama-cpp": "3.18.1", + "openai": "^4.77.0", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", "web-tree-sitter": "0.26.7", diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 50ae76486..8aca04213 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -78,7 +78,7 @@ import { type ReindexResult, type ChunkStrategy, } from "../store.js"; -import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; +import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, getDefaultEmbeddingLLM, withLLMSession, pullModels, setEmbeddingConfig, isUsingOpenAI, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; import { formatSearchResults, formatDocuments, @@ -98,6 +98,7 @@ import { listAllContexts, setConfigIndexName, loadConfig, + getEmbeddingConfig as getEmbeddingConfigFromYaml, } from "../collections.js"; import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js"; @@ -2847,6 +2848,18 @@ if (isMain) { process.exit(cli.values.help ? 0 : 1); } + // Load embedding configuration from config file + const embeddingYamlConfig = getEmbeddingConfigFromYaml(); + if (embeddingYamlConfig.provider === 'openai') { + setEmbeddingConfig({ + provider: 'openai', + openai: { + apiKey: embeddingYamlConfig.openai?.api_key, + embedModel: embeddingYamlConfig.openai?.model, + }, + }); + } + switch (cli.command) { case "context": { const subcommand = cli.args[0]; diff --git a/src/collections.ts b/src/collections.ts index e68ff65b5..fc8e7485b 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -42,6 +42,17 @@ export interface ModelsConfig { generate?: string; } +/** + * Embedding provider configuration (optional in config file) + */ +export interface EmbeddingProviderConfig { + provider?: 'local' | 'openai'; // Default: 'local' + openai?: { + api_key?: string; // Falls back to OPENAI_API_KEY env var + model?: string; // Default: 'text-embedding-3-small' + }; +} + /** * The complete configuration file structure */ @@ -51,6 +62,7 @@ export interface CollectionConfig { editor_uri_template?: string; // Alias for editor_uri collections: Record; // Collection name -> config models?: ModelsConfig; + embedding?: EmbeddingProviderConfig; // Optional embedding provider settings } /** @@ -510,3 +522,12 @@ export function isValidCollectionName(name: string): boolean { // Allow alphanumeric, hyphens, underscores return /^[a-zA-Z0-9_-]+$/.test(name); } + +/** + * Get embedding configuration from config file + * Returns default (local) config if not specified + */ +export function getEmbeddingConfig(): EmbeddingProviderConfig { + const config = loadConfig(); + return config.embedding || { provider: 'local' }; +} diff --git a/src/llm.ts b/src/llm.ts index 7cccc3fa8..00d9bdd04 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -1663,3 +1663,60 @@ export async function disposeDefaultLlamaCpp(): Promise { defaultLlamaCpp = null; } } + +// ============================================================================= +// OpenAI Embedding Support +// ============================================================================= + +import { OpenAIEmbedding, type OpenAIConfig } from "./openai-llm.js"; + +/** + * Embedding provider configuration + */ +export type EmbeddingProvider = 'local' | 'openai'; + +export type EmbeddingConfig = { + provider: EmbeddingProvider; + openai?: OpenAIConfig; +}; + +// Default embedding config: use local llama-cpp +let embeddingConfig: EmbeddingConfig = { provider: 'local' }; +let openAIEmbedding: OpenAIEmbedding | null = null; + +/** + * Set the embedding configuration. Call before using embeddings. + */ +export function setEmbeddingConfig(config: EmbeddingConfig): void { + embeddingConfig = config; + // Reset OpenAI instance if config changes + openAIEmbedding = null; +} + +/** + * Get the current embedding configuration + */ +export function getEmbeddingConfig(): EmbeddingConfig { + return embeddingConfig; +} + +/** + * Check if using OpenAI for embeddings + */ +export function isUsingOpenAI(): boolean { + return embeddingConfig.provider === 'openai'; +} + +/** + * Get the appropriate LLM for embeddings based on config. + * Returns OpenAI embedding client if configured, otherwise local LlamaCpp. + */ +export function getDefaultEmbeddingLLM(): LLM { + if (embeddingConfig.provider === 'openai') { + if (!openAIEmbedding) { + openAIEmbedding = new OpenAIEmbedding(embeddingConfig.openai); + } + return openAIEmbedding; + } + return getDefaultLlamaCpp(); +} diff --git a/src/openai-llm.ts b/src/openai-llm.ts new file mode 100644 index 000000000..23523a8e8 --- /dev/null +++ b/src/openai-llm.ts @@ -0,0 +1,131 @@ +/** + * openai-llm.ts - OpenAI API embeddings for QMD + * + * Provides embedding generation using OpenAI's API instead of local models. + * Much faster and more reliable than local llama-cpp, costs ~$0.02/1M tokens. + */ + +import OpenAI from 'openai'; +import type { + LLM, + EmbedOptions, + EmbeddingResult, + GenerateOptions, + GenerateResult, + RerankOptions, + RerankResult, + RerankDocument, + ModelInfo, + Queryable +} from './llm.js'; + +export type OpenAIConfig = { + apiKey?: string; + embedModel?: string; + baseURL?: string; +}; + +const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; + +/** + * OpenAI LLM implementation - primarily for embeddings + */ +export class OpenAIEmbedding implements LLM { + private client: OpenAI; + private embedModel: string; + + constructor(config: OpenAIConfig = {}) { + this.client = new OpenAI({ + apiKey: config.apiKey || process.env.OPENAI_API_KEY, + baseURL: config.baseURL, + }); + this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; + } + + async embed(text: string, options?: EmbedOptions): Promise { + try { + const response = await this.client.embeddings.create({ + model: this.embedModel, + input: text, + }); + return { + embedding: response.data[0].embedding, + model: this.embedModel, + }; + } catch (error) { + console.error('OpenAI embedding error:', error); + throw error; // Re-throw to see the full error + } + } + + getModelName(): string { + return this.embedModel; + } + + async embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]> { + try { + // OpenAI supports batch embedding natively + const response = await this.client.embeddings.create({ + model: this.embedModel, + input: texts, + }); + return response.data.map(item => ({ + embedding: item.embedding, + model: this.embedModel, + })); + } catch (error) { + console.error('OpenAI batch embedding error:', error); + return texts.map(() => null); + } + } + + // Stub implementations for other LLM interface methods + async generate(prompt: string, options?: GenerateOptions): Promise { + // Not implemented - use local model for generation + console.warn('OpenAIEmbedding.generate() not implemented, use local model'); + return null; + } + + async modelExists(model: string): Promise { + return { + name: model, + exists: model === this.embedModel, + }; + } + + async expandQuery(query: string, options?: { context?: string, includeLexical?: boolean }): Promise { + // Simple implementation - just return lexical query + return [{ type: 'lex', text: query }]; + } + + async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { + // Not implemented - use local model for reranking + console.warn('OpenAIEmbedding.rerank() not implemented, returning original order'); + return { + results: documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.01), // Preserve original order with decreasing scores + index, + })), + model: 'passthrough', + }; + } + + async dispose(): Promise { + // No resources to dispose for API client + } +} + +// Singleton instance +let defaultOpenAI: OpenAIEmbedding | null = null; + +export function getDefaultOpenAI(config?: OpenAIConfig): OpenAIEmbedding { + if (!defaultOpenAI) { + defaultOpenAI = new OpenAIEmbedding(config); + } + return defaultOpenAI; +} + +export function setDefaultOpenAI(llm: OpenAIEmbedding | null): void { + defaultOpenAI = llm; +} diff --git a/src/store.ts b/src/store.ts index 16a55b7df..6f3a1cbf5 100644 --- a/src/store.ts +++ b/src/store.ts @@ -21,6 +21,8 @@ import fastGlob from "fast-glob"; import { LlamaCpp, getDefaultLlamaCpp, + getDefaultEmbeddingLLM, + isUsingOpenAI, formatQueryForEmbedding, formatDocForEmbedding, withLLMSessionForLlm, @@ -3189,9 +3191,10 @@ export async function searchVec(db: Database, query: string, model: string, limi async function getEmbedding(text: string, model: string, isQuery: boolean, session?: ILLMSession, llmOverride?: LlamaCpp): Promise { // Format text using the appropriate prompt template const formattedText = isQuery ? formatQueryForEmbedding(text, model) : formatDocForEmbedding(text, undefined, model); + const llm = llmOverride ?? getDefaultEmbeddingLLM(); const result = session ? await session.embed(formattedText, { model, isQuery }) - : await (llmOverride ?? getDefaultLlamaCpp()).embed(formattedText, { model, isQuery }); + : await llm.embed(formattedText, { model, isQuery }); return result?.embedding || null; } @@ -3256,6 +3259,12 @@ export function insertEmbedding( // ============================================================================= export async function expandQuery(query: string, model: string = DEFAULT_QUERY_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise { + // Skip query expansion when using OpenAI (avoids loading local model) + // Return a lex query to let BM25 handle it + if (isUsingOpenAI()) { + return [{ type: 'lex' as const, query }]; + } + // Check cache first — stored as JSON preserving types const cacheKey = getCacheKey("expandQuery", { query, model, ...(intent && { intent }) }); const cached = getCachedResult(db, cacheKey); @@ -3295,8 +3304,19 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M // ============================================================================= export async function rerank(query: string, documents: { file: string; text: string }[], model: string = DEFAULT_RERANK_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<{ file: string; score: number }[]> { + // Skip reranking when using OpenAI (avoids loading local model) + // Return documents with decreasing scores to preserve original order + if (isUsingOpenAI()) { + return documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.001), + })); + } + // Prepend intent to rerank query so the reranker scores with domain context - const rerankQuery = intent ? `${intent}\n\n${query}` : query; + const rerankQuery = intent ? `${intent} + +${query}` : query; const cachedResults: Map = new Map(); const uncachedDocsByChunk: Map = new Map(); From 9943b6460293ad519a0e9b30b3f0854c47fb1e1b Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Wed, 4 Feb 2026 19:19:13 +0000 Subject: [PATCH 02/17] feat: Add OpenAI embedding and query expansion support - OpenAI embeddings (text-embedding-3-small, 1536d) via QMD_OPENAI=1 - Query expansion with gpt-4o-mini (~200ms vs 30s local) - Tiktoken for fast tokenization (no model loading) - Exponential backoff with jitter for rate limits (429) - Inter-batch delay (150ms) to avoid hitting RPM limits - Performance: search 3-5s (was 30-60s), embed ~10min (was 2hrs) Files: openai-llm.ts, llm.ts, store.ts, qmd.ts Deps: openai, tiktoken --- bun.lock | 3 + package.json | 1 + src/llm.ts | 17 +++++ src/openai-llm.ts | 156 ++++++++++++++++++++++++++++++++++++++++++---- src/store.ts | 110 +++++++++++++++++++++++++++++++- 5 files changed, 272 insertions(+), 15 deletions(-) diff --git a/bun.lock b/bun.lock index 106191ff4..4daf9bff5 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "openai": "^4.77.0", "picomatch": "^4.0.0", "sqlite-vec": "^0.1.7-alpha.2", + "tiktoken": "^1.0.22", "yaml": "^2.8.2", "zod": "4.2.1", }, @@ -697,6 +698,8 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], diff --git a/package.json b/package.json index 2b5d7dae4..3cf3e79bb 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "openai": "^4.77.0", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", + "tiktoken": "^1.0.22", "web-tree-sitter": "0.26.7", "yaml": "2.8.3", "zod": "4.2.1" diff --git a/src/llm.ts b/src/llm.ts index 00d9bdd04..807db2715 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -372,6 +372,16 @@ export interface LLM { */ embed(text: string, options?: EmbedOptions): Promise; + /** + * Get embeddings for multiple texts in a batch + */ + embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]>; + + /** + * Get the model name used for embeddings + */ + getModelName(): string; + /** * Generate text completion */ @@ -514,6 +524,13 @@ export class LlamaCpp implements LLM { return this.embedModelUri; } + /** + * Get the model name used for embeddings + */ + getModelName(): string { + return this.embedModelUri; + } + /** * Reset the inactivity timer. Called after each model operation. * When timer fires, models are unloaded to free memory (if no active sessions). diff --git a/src/openai-llm.ts b/src/openai-llm.ts index 23523a8e8..b8815ede2 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -22,10 +22,79 @@ import type { export type OpenAIConfig = { apiKey?: string; embedModel?: string; + expansionModel?: string; baseURL?: string; }; const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; +const DEFAULT_EXPANSION_MODEL = 'gpt-4o-mini'; + +// Retry configuration +const MAX_RETRIES = 5; +const BASE_DELAY_MS = 1000; +const MAX_DELAY_MS = 60000; + +/** + * Sleep for a given number of milliseconds + */ +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Retry a function with exponential backoff + jitter + * Handles rate limits (429) and transient errors (5xx) + */ +async function withRetry( + fn: () => Promise, + options: { maxRetries?: number; context?: string } = {} +): Promise { + const maxRetries = options.maxRetries ?? MAX_RETRIES; + const context = options.context ?? 'operation'; + + let lastError: unknown; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error: unknown) { + lastError = error; + + // Check if we should retry + const isRateLimit = error instanceof Error && + ('status' in error && (error as { status: number }).status === 429); + const isServerError = error instanceof Error && + ('status' in error && (error as { status: number }).status >= 500); + const isRetryable = isRateLimit || isServerError; + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + // Calculate delay with exponential backoff + jitter + const exponentialDelay = BASE_DELAY_MS * Math.pow(2, attempt); + const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter + const delay = Math.min(exponentialDelay + jitter, MAX_DELAY_MS); + + // Check for Retry-After header hint + let retryAfter = 0; + if (error instanceof Error && 'headers' in error) { + const headers = (error as { headers?: { get?: (k: string) => string | null } }).headers; + const retryAfterHeader = headers?.get?.('retry-after'); + if (retryAfterHeader) { + retryAfter = parseInt(retryAfterHeader, 10) * 1000; + } + } + + const finalDelay = Math.max(delay, retryAfter); + console.warn(`[OpenAI] ${context} failed (attempt ${attempt + 1}/${maxRetries + 1}), ` + + `retrying in ${Math.round(finalDelay / 1000)}s...`); + + await sleep(finalDelay); + } + } + + throw lastError; +} /** * OpenAI LLM implementation - primarily for embeddings @@ -33,6 +102,7 @@ const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; export class OpenAIEmbedding implements LLM { private client: OpenAI; private embedModel: string; + private expansionModel: string; constructor(config: OpenAIConfig = {}) { this.client = new OpenAI({ @@ -40,22 +110,24 @@ export class OpenAIEmbedding implements LLM { baseURL: config.baseURL, }); this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; + this.expansionModel = config.expansionModel || DEFAULT_EXPANSION_MODEL; } async embed(text: string, options?: EmbedOptions): Promise { - try { + return withRetry(async () => { const response = await this.client.embeddings.create({ model: this.embedModel, input: text, }); + const data = response.data[0]; + if (!data) { + throw new Error('No embedding data returned from OpenAI'); + } return { - embedding: response.data[0].embedding, + embedding: data.embedding, model: this.embedModel, }; - } catch (error) { - console.error('OpenAI embedding error:', error); - throw error; // Re-throw to see the full error - } + }, { context: 'embed' }); } getModelName(): string { @@ -63,7 +135,7 @@ export class OpenAIEmbedding implements LLM { } async embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]> { - try { + return withRetry(async () => { // OpenAI supports batch embedding natively const response = await this.client.embeddings.create({ model: this.embedModel, @@ -73,10 +145,7 @@ export class OpenAIEmbedding implements LLM { embedding: item.embedding, model: this.embedModel, })); - } catch (error) { - console.error('OpenAI batch embedding error:', error); - return texts.map(() => null); - } + }, { context: `embedBatch(${texts.length} texts)` }); } // Stub implementations for other LLM interface methods @@ -94,8 +163,69 @@ export class OpenAIEmbedding implements LLM { } async expandQuery(query: string, options?: { context?: string, includeLexical?: boolean }): Promise { - // Simple implementation - just return lexical query - return [{ type: 'lex', text: query }]; + const includeLexical = options?.includeLexical ?? true; + + try { + const response = await withRetry(() => this.client.chat.completions.create({ + model: this.expansionModel, + messages: [ + { + role: 'system', + content: `You are a search query expander. Given a search query, generate expanded versions for different search backends. + +Output format (one per line): +lex: +vec: +hyde: + +Generate 1-2 of each type. Be concise. Include the original query terms.` + }, + { + role: 'user', + content: query + } + ], + temperature: 0.7, + max_tokens: 300, + }), { context: 'expandQuery' }); + + const content = response.choices[0]?.message?.content || ''; + const lines = content.trim().split('\n'); + + const queryables: Queryable[] = []; + for (const line of lines) { + const colonIdx = line.indexOf(':'); + if (colonIdx === -1) continue; + + const type = line.slice(0, colonIdx).trim().toLowerCase(); + if (type !== 'lex' && type !== 'vec' && type !== 'hyde') continue; + + const text = line.slice(colonIdx + 1).trim(); + if (!text) continue; + + queryables.push({ type: type as 'lex' | 'vec' | 'hyde', text }); + } + + // Filter lex if not requested + const filtered = includeLexical ? queryables : queryables.filter(q => q.type !== 'lex'); + + if (filtered.length > 0) return filtered; + + // Fallback if parsing failed + const fallback: Queryable[] = [ + { type: 'vec', text: query }, + { type: 'hyde', text: `Information about ${query}` }, + ]; + if (includeLexical) fallback.unshift({ type: 'lex', text: query }); + return fallback; + + } catch (error) { + console.error('OpenAI query expansion error:', error); + // Fallback to original query + const fallback: Queryable[] = [{ type: 'vec', text: query }]; + if (includeLexical) fallback.unshift({ type: 'lex', text: query }); + return fallback; + } } async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { diff --git a/src/store.ts b/src/store.ts index 6f3a1cbf5..5fc256194 100644 --- a/src/store.ts +++ b/src/store.ts @@ -18,6 +18,7 @@ import { createHash } from "crypto"; import { readFileSync, realpathSync, statSync, mkdirSync } from "node:fs"; // Note: node:path resolve is not imported — we export our own cross-platform resolve() import fastGlob from "fast-glob"; +import { encoding_for_model } from "tiktoken"; import { LlamaCpp, getDefaultLlamaCpp, @@ -2269,6 +2270,17 @@ export async function chunkDocumentAsync( * When filepath and chunkStrategy are provided, uses AST-aware break points * for supported code files. */ +// Cached tiktoken encoder for OpenAI tokenization +let tiktokenEncoder: ReturnType | null = null; + +function getTiktokenEncoder() { + if (!tiktokenEncoder) { + // Use cl100k_base which is used by text-embedding-3-small + tiktokenEncoder = encoding_for_model("gpt-4"); + } + return tiktokenEncoder; +} + export async function chunkDocumentByTokens( content: string, maxTokens: number = CHUNK_SIZE_TOKENS, @@ -2278,6 +2290,12 @@ export async function chunkDocumentByTokens( chunkStrategy: ChunkStrategy = "regex", signal?: AbortSignal ): Promise<{ text: string; pos: number; tokens: number }[]> { + // Use tiktoken for OpenAI (fast, no model loading) + // Use llama-cpp for local models (accurate to the model) + if (isUsingOpenAI()) { + return chunkWithTiktoken(content, maxTokens, overlapTokens); + } + const llm = getDefaultLlamaCpp(); // Use moderate chars/token estimate (prose ~4, code ~2, mixed ~3) @@ -2359,6 +2377,86 @@ export async function chunkDocumentByTokens( return results; } +/** + * Chunk using tiktoken (fast, for OpenAI embeddings) + */ +function chunkWithTiktoken( + content: string, + maxTokens: number, + overlapTokens: number +): { text: string; pos: number; tokens: number }[] { + const encoder = getTiktokenEncoder(); + // Allow all special tokens in documents (they might contain code examples, etc.) + const allTokens = encoder.encode(content, "all"); + const totalTokens = allTokens.length; + + if (totalTokens <= maxTokens) { + return [{ text: content, pos: 0, tokens: totalTokens }]; + } + + const chunks: { text: string; pos: number; tokens: number }[] = []; + const step = maxTokens - overlapTokens; + const decoder = new TextDecoder(); + let tokenPos = 0; + + while (tokenPos < totalTokens) { + const chunkEnd = Math.min(tokenPos + maxTokens, totalTokens); + const chunkTokens = allTokens.slice(tokenPos, chunkEnd); + let chunkText = decoder.decode(encoder.decode(chunkTokens)); + + // Find a good break point if not at end of document + if (chunkEnd < totalTokens) { + chunkText = findGoodBreakPoint(chunkText); + } + + // Approximate character position + const avgCharsPerToken = content.length / totalTokens; + const charPos = Math.floor(tokenPos * avgCharsPerToken); + chunks.push({ text: chunkText, pos: charPos, tokens: chunkTokens.length }); + + if (chunkEnd >= totalTokens) break; + tokenPos += step; + } + + return chunks; +} + +/** + * Find a good break point in text (paragraph, sentence, or line) + */ +function findGoodBreakPoint(text: string): string { + const searchStart = Math.floor(text.length * 0.7); + const searchSlice = text.slice(searchStart); + + let breakOffset = -1; + const paragraphBreak = searchSlice.lastIndexOf('\n\n'); + if (paragraphBreak >= 0) { + breakOffset = paragraphBreak + 2; + } else { + const sentenceEnd = Math.max( + searchSlice.lastIndexOf('. '), + searchSlice.lastIndexOf('.\n'), + searchSlice.lastIndexOf('? '), + searchSlice.lastIndexOf('?\n'), + searchSlice.lastIndexOf('! '), + searchSlice.lastIndexOf('!\n') + ); + if (sentenceEnd >= 0) { + breakOffset = sentenceEnd + 2; + } else { + const lineBreak = searchSlice.lastIndexOf('\n'); + if (lineBreak >= 0) { + breakOffset = lineBreak + 1; + } + } + } + + if (breakOffset >= 0) { + return text.slice(0, searchStart + breakOffset); + } + return text; +} + // ============================================================================= // Fuzzy matching // ============================================================================= @@ -3191,10 +3289,18 @@ export async function searchVec(db: Database, query: string, model: string, limi async function getEmbedding(text: string, model: string, isQuery: boolean, session?: ILLMSession, llmOverride?: LlamaCpp): Promise { // Format text using the appropriate prompt template const formattedText = isQuery ? formatQueryForEmbedding(text, model) : formatDocForEmbedding(text, undefined, model); - const llm = llmOverride ?? getDefaultEmbeddingLLM(); + + // Always use OpenAI when configured, regardless of session + if (isUsingOpenAI()) { + const llm = llmOverride ?? getDefaultEmbeddingLLM(); + const result = await llm.embed(formattedText, { model, isQuery }); + return result?.embedding || null; + } + + // Use session if available, otherwise local default model const result = session ? await session.embed(formattedText, { model, isQuery }) - : await llm.embed(formattedText, { model, isQuery }); + : await (llmOverride ?? getDefaultLlamaCpp()).embed(formattedText, { model, isQuery }); return result?.embedding || null; } From 6a7625fb4afd909fcfc0fb4553048ba5a16aeaf8 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Mon, 9 Feb 2026 22:44:14 +0000 Subject: [PATCH 03/17] feat: add OpenAI-based reranking via gpt-4o-mini MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the rerank() stub with a real listwise reranker using gpt-4o-mini. - Sends top candidates with query to gpt-4o-mini as a ranking task - Parses comma-separated index output, handles missing/duplicate indices - Skips API call for ≤2 documents (not worth the latency) - Falls back to original order on API failure - Cost: ~$0.001 per rerank call - Updated qmd.ts to route through OpenAI reranker instead of skipping The full qmd query pipeline with OpenAI now: 1. Query expansion (gpt-4o-mini) 2. BM25 + vector search (parallel) 3. RRF fusion 4. Cross-encoder reranking (gpt-4o-mini) ← NEW 5. Position-aware blending --- src/openai-llm.ts | 101 +++++++++++++++++++++++++++++++++++++++++----- src/store.ts | 10 +++-- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/openai-llm.ts b/src/openai-llm.ts index b8815ede2..8a61623ba 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -229,16 +229,97 @@ Generate 1-2 of each type. Be concise. Include the original query terms.` } async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { - // Not implemented - use local model for reranking - console.warn('OpenAIEmbedding.rerank() not implemented, returning original order'); - return { - results: documents.map((doc, index) => ({ - file: doc.file, - score: 1 - (index * 0.01), // Preserve original order with decreasing scores - index, - })), - model: 'passthrough', - }; + if (documents.length === 0) { + return { results: [], model: `${this.expansionModel}-rerank` }; + } + + // For very small sets, skip the API call — not worth the latency + if (documents.length <= 2) { + return { + results: documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.01), + index, + })), + model: 'passthrough', + }; + } + + try { + // Truncate documents for the prompt — 500 chars each is enough for relevance judgment + const truncated = documents.map((doc, i) => + `[${i}] ${doc.title ? doc.title + ': ' : ''}${doc.text.slice(0, 500).replace(/\n+/g, ' ')}` + ); + + const response = await withRetry(() => this.client.chat.completions.create({ + model: this.expansionModel, + messages: [ + { + role: 'system', + content: `You are a document relevance ranker. Given a search query and numbered documents, rank them by relevance. + +Output ONLY a comma-separated list of document indices, most relevant first. Include ALL indices exactly once. +Example output for 5 documents: 2,0,4,1,3` + }, + { + role: 'user', + content: `Query: ${query}\n\nDocuments:\n${truncated.join('\n\n')}` + } + ], + temperature: 0, + max_tokens: 200, + }), { context: 'rerank' }); + + const content = response.choices[0]?.message?.content?.trim() || ''; + + // Parse the comma-separated indices + const indices = content + .replace(/[^0-9,]/g, '') // Strip anything that isn't a digit or comma + .split(',') + .map(s => parseInt(s.trim(), 10)) + .filter(n => !isNaN(n) && n >= 0 && n < documents.length); + + // Deduplicate while preserving order + const seen = new Set(); + const uniqueIndices: number[] = []; + for (const idx of indices) { + if (!seen.has(idx)) { + seen.add(idx); + uniqueIndices.push(idx); + } + } + + // Add any missing indices at the end (in case the model missed some) + for (let i = 0; i < documents.length; i++) { + if (!seen.has(i)) { + uniqueIndices.push(i); + } + } + + // Convert rank position to score (1.0 for rank 1, decreasing) + const results = uniqueIndices.map((docIndex, rank) => ({ + file: documents[docIndex]!.file, + score: 1.0 - (rank / uniqueIndices.length), + index: docIndex, + })); + + return { + results, + model: `${this.expansionModel}-rerank`, + }; + } catch (error) { + // Fallback: preserve original order if reranking fails + console.warn('[OpenAI] Rerank failed, preserving original order:', + error instanceof Error ? error.message : String(error)); + return { + results: documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.01), + index, + })), + model: 'passthrough-fallback', + }; + } } async dispose(): Promise { diff --git a/src/store.ts b/src/store.ts index 5fc256194..26675fbe8 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3410,13 +3410,15 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M // ============================================================================= export async function rerank(query: string, documents: { file: string; text: string }[], model: string = DEFAULT_RERANK_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<{ file: string; score: number }[]> { - // Skip reranking when using OpenAI (avoids loading local model) - // Return documents with decreasing scores to preserve original order + // Use OpenAI-based reranking when in OpenAI mode if (isUsingOpenAI()) { - return documents.map((doc, index) => ({ + const embeddingLLM = getDefaultEmbeddingLLM(); + const rerankDocs: RerankDocument[] = documents.map((doc) => ({ file: doc.file, - score: 1 - (index * 0.001), + text: doc.text.slice(0, 4000), })); + const result = await embeddingLLM.rerank(query, rerankDocs); + return result.results.map((r) => ({ file: r.file, score: r.score })); } // Prepend intent to rerank query so the reranker scores with domain context From 5923dd08dcab6e6f3acf05a4355b47212c534909 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Mon, 9 Feb 2026 23:55:38 +0000 Subject: [PATCH 04/17] feat: multi-collection search support Accept comma-separated collection names in -c flag for cross-collection search. All three search modes (search, vsearch, query) now support querying multiple collections simultaneously. Changes: - resolveCollectionFilter() helper parses and validates comma-separated names - searchFTS() accepts string | string[] for collection filtering - searchVec() accepts string | string[] for collection filtering - SQL uses IN clause for multi-collection filtering - Updated interface types and test for new parameter types Usage: qmd search 'auth' -c repo-a,repo-b qmd vsearch 'auth patterns' -c docs,examples qmd query 'OAuth implementation' -c project,patterns,docs This enables Shad's multi-vault search to pass all vault collections in a single qmd call instead of running separate searches per collection. --- src/cli/qmd.ts | 6 ++---- src/store.ts | 38 ++++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 8aca04213..d0bc59e36 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -2236,10 +2236,8 @@ function search(query: string, opts: OutputOptions): void { // Use large limit for --all, otherwise fetch more than needed and let outputResults filter const fetchLimit = opts.all ? 100000 : Math.max(50, opts.limit * 2); - const results = filterByCollections( - searchFTS(db, query, fetchLimit, singleCollection), - collectionNames - ); + // Pass collections directly to searchFTS (it now supports arrays) + const results = searchFTS(db, query, fetchLimit, collectionNames.length > 0 ? collectionNames : undefined); // Add context to results const resultsWithContext = results.map(r => ({ diff --git a/src/store.ts b/src/store.ts index 26675fbe8..c752f7e68 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1128,8 +1128,8 @@ export type Store = { toVirtualPath: (absolutePath: string) => string | null; // Search - searchFTS: (query: string, limit?: number, collectionName?: string) => SearchResult[]; - searchVec: (query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]) => Promise; + searchFTS: (query: string, limit?: number, collections?: string | string[]) => SearchResult[]; + searchVec: (query: string, model: string, limit?: number, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]) => Promise; // Query expansion & reranking expandQuery: (query: string, model?: string, intent?: string) => Promise; @@ -1642,8 +1642,8 @@ export function createStore(dbPath?: string): Store { toVirtualPath: (absolutePath: string) => toVirtualPath(db, absolutePath), // Search - searchFTS: (query: string, limit?: number, collectionName?: string) => searchFTS(db, query, limit, collectionName), - searchVec: (query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]) => searchVec(db, query, model, limit, collectionName, session, precomputedEmbedding), + searchFTS: (query: string, limit?: number, collections?: string | string[]) => searchFTS(db, query, limit, collections), + searchVec: (query: string, model: string, limit?: number, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]) => searchVec(db, query, model, limit, collections, session, precomputedEmbedding), // Query expansion & reranking expandQuery: (query: string, model?: string, intent?: string) => expandQuery(query, model, db, intent, store.llm), @@ -3121,7 +3121,7 @@ export function validateLexQuery(query: string): string | null { return null; } -export function searchFTS(db: Database, query: string, limit: number = 20, collectionName?: string): SearchResult[] { +export function searchFTS(db: Database, query: string, limit: number = 20, collections?: string | string[]): SearchResult[] { const ftsQuery = buildFTS5Query(query); if (!ftsQuery) return []; @@ -3158,9 +3158,16 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle WHERE d.active = 1 `; - if (collectionName) { - sql += ` AND d.collection = ?`; - params.push(String(collectionName)); + if (collections) { + const collArray = Array.isArray(collections) ? collections : [collections]; + if (collArray.length === 1 && collArray[0]) { + sql += ` AND d.collection = ?`; + params.push(collArray[0]); + } else if (collArray.length > 1) { + const valid = collArray.filter(Boolean); + sql += ` AND d.collection IN (${valid.map(() => '?').join(',')})`; + params.push(...valid); + } } // bm25 lower is better; sort ascending. @@ -3196,7 +3203,7 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle // Vector Search // ============================================================================= -export async function searchVec(db: Database, query: string, model: string, limit: number = 20, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]): Promise { +export async function searchVec(db: Database, query: string, model: string, limit: number = 20, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]): Promise { const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get(); if (!tableExists) return []; @@ -3239,9 +3246,16 @@ export async function searchVec(db: Database, query: string, model: string, limi `; const params: string[] = [...hashSeqs]; - if (collectionName) { - docSql += ` AND d.collection = ?`; - params.push(collectionName); + if (collections) { + const collArray = Array.isArray(collections) ? collections : [collections]; + if (collArray.length === 1 && collArray[0]) { + docSql += ` AND d.collection = ?`; + params.push(collArray[0]); + } else if (collArray.length > 1) { + const valid = collArray.filter(Boolean); + docSql += ` AND d.collection IN (${valid.map(() => '?').join(',')})`; + params.push(...valid); + } } const docRows = db.prepare(docSql).all(...params) as { From ce3b06186f17e9697170b0bfa0c4af2a3e0898e7 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Fri, 27 Feb 2026 23:08:05 -0700 Subject: [PATCH 05/17] fix: use default embedding LLM for hybrid vector queries --- src/store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store.ts b/src/store.ts index c752f7e68..d32815645 100644 --- a/src/store.ts +++ b/src/store.ts @@ -4229,8 +4229,8 @@ export async function hybridQuery( } // Batch embed all vector queries in a single call - const llm = getLlm(store); - const textsToEmbed = vecQueries.map(q => formatQueryForEmbedding(q.text, llm.embedModelName)); + const llm = getDefaultEmbeddingLLM(); + const textsToEmbed = vecQueries.map(q => formatQueryForEmbedding(q.text, llm.getModelName())); hooks?.onEmbedStart?.(textsToEmbed.length); const embedStart = Date.now(); const embeddings = await llm.embedBatch(textsToEmbed); From f346047e486c1b76f81a638ee2bd25a93077910b Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Sat, 11 Apr 2026 19:31:53 -0600 Subject: [PATCH 06/17] feat: split base URLs for embedding and chat endpoints Add support for separate OpenAI-compatible servers for embeddings vs chat (expansion/reranking). Common in setups where local GPU serves embeddings and cloud handles chat. Implements Kaspre's split-URL pattern from PR #116 discussion. - Add chat_base_url and chat_api_key to YAML config and OpenAIConfig - Add QMD_OPENAI_* env var prefix (QMD_OPENAI_BASE_URL, QMD_OPENAI_API_KEY, QMD_OPENAI_CHAT_BASE_URL, QMD_OPENAI_CHAT_API_KEY) per alexleach's suggestion - Wire expansion_model and base_url through YAML config per viniciushsantana's feedback - Route expandQuery() and rerank() through chatClient, embed()/embedBatch() through embedding client - Fix upstream rebase issues (Database.transaction type, collectionName rename) Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 63 ++++++++++++++++++++++++++++++---------------- src/cli/qmd.ts | 4 +++ src/collections.ts | 6 ++++- src/db.ts | 1 + src/openai-llm.ts | 22 +++++++++++----- src/store.ts | 2 +- 6 files changed, 68 insertions(+), 30 deletions(-) diff --git a/bun.lock b/bun.lock index 4daf9bff5..7cfea15b4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,28 +5,33 @@ "": { "name": "@tobilu/qmd", "dependencies": { - "@modelcontextprotocol/sdk": "^1.25.1", - "better-sqlite3": "^12.4.5", - "fast-glob": "^3.3.0", - "node-llama-cpp": "^3.17.1", + "@modelcontextprotocol/sdk": "1.29.0", + "better-sqlite3": "12.8.0", + "fast-glob": "3.3.3", + "node-llama-cpp": "3.18.1", "openai": "^4.77.0", - "picomatch": "^4.0.0", - "sqlite-vec": "^0.1.7-alpha.2", + "picomatch": "4.0.4", + "sqlite-vec": "0.1.9", "tiktoken": "^1.0.22", - "yaml": "^2.8.2", + "web-tree-sitter": "0.26.7", + "yaml": "2.8.3", "zod": "4.2.1", }, "devDependencies": { - "@types/better-sqlite3": "^7.6.0", - "tsx": "^4.0.0", - "vitest": "^3.0.0", + "@types/better-sqlite3": "7.6.13", + "tsx": "4.21.0", + "vitest": "3.2.4", }, "optionalDependencies": { - "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2", - "sqlite-vec-darwin-x64": "^0.1.7-alpha.2", - "sqlite-vec-linux-arm64": "^0.1.7-alpha.2", - "sqlite-vec-linux-x64": "^0.1.7-alpha.2", - "sqlite-vec-windows-x64": "^0.1.7-alpha.2", + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9", + "tree-sitter-go": "0.23.4", + "tree-sitter-python": "0.23.4", + "tree-sitter-rust": "0.24.0", + "tree-sitter-typescript": "0.23.2", }, "peerDependencies": { "typescript": "^5.9.3", @@ -98,7 +103,7 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.18.1", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-rXMgZxUay78FOJV/fJ67apYP9eElH5jd4df5YRKPlLhLHHchuOSyDn+qtyW/L/EnPzpogoLkmULqCkdXU39XsQ=="], @@ -540,6 +545,8 @@ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-llama-cpp": ["node-llama-cpp@3.18.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.6.2", "chmodrp": "^1.0.2", "cmake-js": "^8.0.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.4", "ignore": "^7.0.4", "ipull": "^3.9.5", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.1.1", "log-symbols": "^7.0.1", "nanoid": "^5.1.6", "node-addon-api": "^8.6.0", "ora": "^9.3.0", "pretty-ms": "^9.3.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.33.0", "slice-ansi": "^8.0.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.2.0", "validate-npm-package-name": "^7.0.2", "which": "^6.0.1", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.18.1", "@node-llama-cpp/linux-armv7l": "3.18.1", "@node-llama-cpp/linux-x64": "3.18.1", "@node-llama-cpp/linux-x64-cuda": "3.18.1", "@node-llama-cpp/linux-x64-cuda-ext": "3.18.1", "@node-llama-cpp/linux-x64-vulkan": "3.18.1", "@node-llama-cpp/mac-arm64-metal": "3.18.1", "@node-llama-cpp/mac-x64": "3.18.1", "@node-llama-cpp/win-arm64": "3.18.1", "@node-llama-cpp/win-x64": "3.18.1", "@node-llama-cpp/win-x64-cuda": "3.18.1", "@node-llama-cpp/win-x64-cuda-ext": "3.18.1", "@node-llama-cpp/win-x64-vulkan": "3.18.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -658,17 +665,17 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "sqlite-vec": ["sqlite-vec@0.1.7", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.7", "sqlite-vec-darwin-x64": "0.1.7", "sqlite-vec-linux-arm64": "0.1.7", "sqlite-vec-linux-x64": "0.1.7", "sqlite-vec-windows-x64": "0.1.7" } }, "sha512-1Sge9uRc3B6wDKR4J6sGFi/E2ai9SAU5FenDki3OmhdP/a49PO2Juy1U5yQnx2bZP5t+C3BYJTkG+KkDi3q9Xg=="], + "sqlite-vec": ["sqlite-vec@0.1.9", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" } }, "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA=="], - "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dQ7u4GKPdOPi3IfZ44K7HHdYup2JssM6fuKR9zgqRzW137uFOQmRhbYChNu+ZfW+yhJutsPgfNRFsuWKmy627w=="], + "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg=="], - "sqlite-vec-darwin-x64": ["sqlite-vec-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-MDoczft1BriQcGMEz+CqeSCkB0OsAf12ytZOapS6MaB7zgNzLLSLH6Sxe3yzcPWUyDuCWgK7WzyRIo8u1vAIVA=="], + "sqlite-vec-darwin-x64": ["sqlite-vec-darwin-x64@0.1.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA=="], - "sqlite-vec-linux-arm64": ["sqlite-vec-linux-arm64@0.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-V429sYT/gwr9PgtT8rbjQd6ls7CFchFpiS45TKSf7rU7wxt9MBmCVorUcheD4kEZb4VeZ6PnFXXCqPMeaHkaUw=="], + "sqlite-vec-linux-arm64": ["sqlite-vec-linux-arm64@0.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw=="], - "sqlite-vec-linux-x64": ["sqlite-vec-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-wZL+lXeW7y63DLv6FYU6Q4nv2lP5F94cWt7bJCWNiHmZ6NdKIgz/p0QlyuJA/51b8TyoDvsTdusLVlZz9cIh5A=="], + "sqlite-vec-linux-x64": ["sqlite-vec-linux-x64@0.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA=="], - "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-FEZMjMT03irJxwqMQg+A+4hHCiFslxISOAkQ0eYn2lP7GdpppkgYveaT5Xnw/2V+GLq2MXOJb0nDGFNethHSkg=="], + "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.9", "", { "os": "win32", "cpu": "x64" }, "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -718,6 +725,16 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tree-sitter-go": ["tree-sitter-go@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w=="], + + "tree-sitter-javascript": ["tree-sitter-javascript@0.23.1", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA=="], + + "tree-sitter-python": ["tree-sitter-python@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw=="], + + "tree-sitter-rust": ["tree-sitter-rust@0.24.0", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-NWemUDf629Tfc90Y0Z55zuwPCAHkLxWnMf2RznYu4iBkkrQl2o/CHGB7Cr52TyN5F1DAx8FmUnDtCy9iUkXZEQ=="], + + "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], @@ -748,6 +765,8 @@ "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "web-tree-sitter": ["web-tree-sitter@0.26.7", "", {}, "sha512-KiZhelTvBA/ziUHEO7Emb75cGVAq8iGZNabYaZm53Zpy50NsXyOW+xSHlwHt5CVg/TRPZBfeVLTTobF0LjFJ1w=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index d0bc59e36..581e9cb76 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -2854,6 +2854,10 @@ if (isMain) { openai: { apiKey: embeddingYamlConfig.openai?.api_key, embedModel: embeddingYamlConfig.openai?.model, + expansionModel: embeddingYamlConfig.openai?.expansion_model, + baseURL: embeddingYamlConfig.openai?.base_url, + chatBaseURL: embeddingYamlConfig.openai?.chat_base_url, + chatApiKey: embeddingYamlConfig.openai?.chat_api_key, }, }); } diff --git a/src/collections.ts b/src/collections.ts index fc8e7485b..86373b35d 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -48,8 +48,12 @@ export interface ModelsConfig { export interface EmbeddingProviderConfig { provider?: 'local' | 'openai'; // Default: 'local' openai?: { - api_key?: string; // Falls back to OPENAI_API_KEY env var + api_key?: string; // Falls back to QMD_OPENAI_API_KEY / OPENAI_API_KEY env var model?: string; // Default: 'text-embedding-3-small' + expansion_model?: string; // Default: 'gpt-4o-mini' + base_url?: string; // Base URL for embeddings (OpenAI-compatible) + chat_base_url?: string; // Separate base URL for expansion/reranking (falls back to base_url) + chat_api_key?: string; // Separate API key for chat endpoint (falls back to api_key) }; } diff --git a/src/db.ts b/src/db.ts index 5fe7ab479..0ca4380de 100644 --- a/src/db.ts +++ b/src/db.ts @@ -69,6 +69,7 @@ export interface Database { exec(sql: string): void; prepare(sql: string): Statement; loadExtension(path: string): void; + transaction(fn: () => T): () => T; close(): void; } diff --git a/src/openai-llm.ts b/src/openai-llm.ts index 8a61623ba..7e38c52f3 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -24,6 +24,8 @@ export type OpenAIConfig = { embedModel?: string; expansionModel?: string; baseURL?: string; + chatBaseURL?: string; + chatApiKey?: string; }; const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; @@ -101,14 +103,22 @@ async function withRetry( */ export class OpenAIEmbedding implements LLM { private client: OpenAI; + private chatClient: OpenAI; private embedModel: string; private expansionModel: string; constructor(config: OpenAIConfig = {}) { - this.client = new OpenAI({ - apiKey: config.apiKey || process.env.OPENAI_API_KEY, - baseURL: config.baseURL, - }); + const apiKey = config.apiKey || process.env.QMD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; + const baseURL = config.baseURL || process.env.QMD_OPENAI_BASE_URL; + + this.client = new OpenAI({ apiKey, baseURL }); + + const chatApiKey = config.chatApiKey || process.env.QMD_OPENAI_CHAT_API_KEY || apiKey; + const chatBaseURL = config.chatBaseURL || process.env.QMD_OPENAI_CHAT_BASE_URL || baseURL; + this.chatClient = (chatBaseURL !== baseURL || chatApiKey !== apiKey) + ? new OpenAI({ apiKey: chatApiKey, baseURL: chatBaseURL }) + : this.client; + this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; this.expansionModel = config.expansionModel || DEFAULT_EXPANSION_MODEL; } @@ -166,7 +176,7 @@ export class OpenAIEmbedding implements LLM { const includeLexical = options?.includeLexical ?? true; try { - const response = await withRetry(() => this.client.chat.completions.create({ + const response = await withRetry(() => this.chatClient.chat.completions.create({ model: this.expansionModel, messages: [ { @@ -251,7 +261,7 @@ Generate 1-2 of each type. Be concise. Include the original query terms.` `[${i}] ${doc.title ? doc.title + ': ' : ''}${doc.text.slice(0, 500).replace(/\n+/g, ' ')}` ); - const response = await withRetry(() => this.client.chat.completions.create({ + const response = await withRetry(() => this.chatClient.chat.completions.create({ model: this.expansionModel, messages: [ { diff --git a/src/store.ts b/src/store.ts index d32815645..421bd82d5 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3135,7 +3135,7 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle // When filtering by collection, fetch extra candidates from the FTS index // since some will be filtered out. Without a collection filter we can // fetch exactly the requested limit. - const ftsLimit = collectionName ? limit * 10 : limit; + const ftsLimit = collections ? limit * 10 : limit; let sql = ` WITH fts_matches AS ( From fc30ecd4d7671db15758e9ac17b74749ec5e0280 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Sat, 11 Apr 2026 21:08:03 -0600 Subject: [PATCH 07/17] Split base, chat, and rerank into separate model/url definitions so different models can be used to each. Thanks to @Kaspre for their comment embedding: provider: openai openai: api_key: "sk-..." base_url: "http://localhost:8081/v1" # embeddings model: "nomic-embed-text" chat_base_url: "https://ollama.com/v1" # expansion (falls back to base_url) chat_api_key: "..." # (falls back to api_key) expansion_model: "gemma3:4b" rerank_base_url: "https://api.cohere.com/v1" # reranking (falls back to chat_base_url) rerank_api_key: "..." # (falls back to chat_api_key) rerank_model: "rerank-v3" # (falls back to expansion_model) also rebased onto main --- src/cli/qmd.ts | 3 + src/collections.ts | 5 +- src/llm.ts | 103 ++++++++++++-- src/openai-llm.ts | 23 +++- src/store.ts | 24 +--- test/qmd-manager.test.ts | 281 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 404 insertions(+), 35 deletions(-) create mode 100644 test/qmd-manager.test.ts diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 581e9cb76..800ff6ccd 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -2855,9 +2855,12 @@ if (isMain) { apiKey: embeddingYamlConfig.openai?.api_key, embedModel: embeddingYamlConfig.openai?.model, expansionModel: embeddingYamlConfig.openai?.expansion_model, + rerankModel: embeddingYamlConfig.openai?.rerank_model, baseURL: embeddingYamlConfig.openai?.base_url, chatBaseURL: embeddingYamlConfig.openai?.chat_base_url, chatApiKey: embeddingYamlConfig.openai?.chat_api_key, + rerankBaseURL: embeddingYamlConfig.openai?.rerank_base_url, + rerankApiKey: embeddingYamlConfig.openai?.rerank_api_key, }, }); } diff --git a/src/collections.ts b/src/collections.ts index 86373b35d..ff948218d 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -51,9 +51,12 @@ export interface EmbeddingProviderConfig { api_key?: string; // Falls back to QMD_OPENAI_API_KEY / OPENAI_API_KEY env var model?: string; // Default: 'text-embedding-3-small' expansion_model?: string; // Default: 'gpt-4o-mini' + rerank_model?: string; // Default: falls back to expansion_model base_url?: string; // Base URL for embeddings (OpenAI-compatible) - chat_base_url?: string; // Separate base URL for expansion/reranking (falls back to base_url) + chat_base_url?: string; // Separate base URL for expansion (falls back to base_url) chat_api_key?: string; // Separate API key for chat endpoint (falls back to api_key) + rerank_base_url?: string; // Separate base URL for reranking (falls back to chat_base_url) + rerank_api_key?: string; // Separate API key for rerank endpoint (falls back to chat_api_key) }; } diff --git a/src/llm.ts b/src/llm.ts index 807db2715..6c60c0349 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -4,16 +4,21 @@ * Provides embeddings, text generation, and reranking using local GGUF models. */ -import { - getLlama, - resolveModelFile, - LlamaChatSession, - LlamaLogLevel, - type Llama, - type LlamaModel, - type LlamaEmbeddingContext, - type Token as LlamaToken, +import type { + Llama, + LlamaModel, + LlamaEmbeddingContext, + Token as LlamaToken, } from "node-llama-cpp"; + +// Lazy-load node-llama-cpp runtime to avoid triggering cmake builds in OpenAI-only mode +let _nodeLlamaCpp: typeof import("node-llama-cpp") | null = null; +async function loadNodeLlamaCpp() { + if (!_nodeLlamaCpp) { + _nodeLlamaCpp = await import("node-llama-cpp"); + } + return _nodeLlamaCpp; +} import { homedir } from "os"; import { join } from "path"; import { existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from "fs"; @@ -344,6 +349,7 @@ export async function pullModels( } } + const { resolveModelFile } = await loadNodeLlamaCpp(); const path = await resolveModelFile(model, cacheDir); validateGgufFile(path, model); const sizeBytes = existsSync(path) ? statSync(path).size : 0; @@ -636,10 +642,11 @@ export class LlamaCpp implements LLM { if (!this.llama) { const gpuMode = resolveLlamaGpuMode(); + const nodeLlama = await loadNodeLlamaCpp(); const loadLlama = async (gpu: LlamaGpuMode) => - await getLlama({ + await nodeLlama.getLlama({ build: allowBuild ? "autoAttempt" : "never", - logLevel: LlamaLogLevel.error, + logLevel: nodeLlama.LlamaLogLevel.error, gpu, skipDownload: !allowBuild, }); @@ -677,7 +684,7 @@ export class LlamaCpp implements LLM { */ private async resolveModel(modelUri: string): Promise { this.ensureModelCacheDir(); - // resolveModelFile handles HF URIs and downloads to the cache dir + const { resolveModelFile } = await loadNodeLlamaCpp(); const modelPath = await resolveModelFile(modelUri, this.modelCacheDir); validateGgufFile(modelPath, modelUri); return modelPath; @@ -1094,6 +1101,7 @@ export class LlamaCpp implements LLM { await this.ensureGenerateModel(); // Create fresh context -> sequence -> session for each call + const { LlamaChatSession } = await loadNodeLlamaCpp(); const context = await this.generateModel!.createContext(); const sequence = context.getSequence(); const session = new LlamaChatSession({ contextSequence: sequence }); @@ -1171,6 +1179,7 @@ export class LlamaCpp implements LLM { : `/no_think Expand this search query: ${query}`; // Create a bounded context for expansion to prevent large default VRAM allocations. + const { LlamaChatSession } = await loadNodeLlamaCpp(); const genContext = await this.generateModel!.createContext({ contextSize: this.expandContextSize, }); @@ -1737,3 +1746,73 @@ export function getDefaultEmbeddingLLM(): LLM { } return getDefaultLlamaCpp(); } + +/** + * Lightweight ILLMSession wrapper for OpenAI — no LlamaCpp session manager needed. + */ +class OpenAILLMSession implements ILLMSession { + private llm: OpenAIEmbedding; + private abortController = new AbortController(); + private released = false; + private maxDurationTimer: ReturnType | null = null; + + constructor(llm: OpenAIEmbedding, options: LLMSessionOptions = {}) { + this.llm = llm; + const maxDuration = options.maxDuration ?? 10 * 60 * 1000; + if (maxDuration > 0) { + this.maxDurationTimer = setTimeout(() => { + this.abortController.abort(new Error("OpenAI session exceeded max duration")); + }, maxDuration); + this.maxDurationTimer.unref(); + } + } + + get isValid(): boolean { return !this.released && !this.abortController.signal.aborted; } + get signal(): AbortSignal { return this.abortController.signal; } + + release(): void { + if (this.released) return; + this.released = true; + if (this.maxDurationTimer) { clearTimeout(this.maxDurationTimer); this.maxDurationTimer = null; } + this.abortController.abort(new Error("Session released")); + } + + async embed(text: string, options?: EmbedOptions): Promise { + return this.llm.embed(text, options); + } + + async embedBatch(texts: string[], _options?: EmbedOptions): Promise<(EmbeddingResult | null)[]> { + return this.llm.embedBatch(texts); + } + + async expandQuery(query: string, options?: { context?: string; includeLexical?: boolean }): Promise { + return this.llm.expandQuery(query, options); + } + + async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { + return this.llm.rerank(query, documents, options); + } +} + +/** + * Execute a function with the correct session type based on embedding provider. + * OpenAI mode: lightweight wrapper, no node-llama-cpp loaded. + * Local mode: full LlamaCpp session with resource management. + */ +export async function withEmbeddingSession( + fn: (session: ILLMSession, modelName: string) => Promise, + options?: LLMSessionOptions & { storeLlm?: LlamaCpp } +): Promise { + if (isUsingOpenAI()) { + const llm = getDefaultEmbeddingLLM() as OpenAIEmbedding; + const session = new OpenAILLMSession(llm, options); + try { + return await fn(session, llm.getModelName()); + } finally { + session.release(); + } + } + + const llm = options?.storeLlm ?? getDefaultLlamaCpp(); + return withLLMSessionForLlm(llm, (session) => fn(session, llm.embedModelName), options); +} diff --git a/src/openai-llm.ts b/src/openai-llm.ts index 7e38c52f3..10e53c721 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -23,9 +23,12 @@ export type OpenAIConfig = { apiKey?: string; embedModel?: string; expansionModel?: string; + rerankModel?: string; baseURL?: string; chatBaseURL?: string; chatApiKey?: string; + rerankBaseURL?: string; + rerankApiKey?: string; }; const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; @@ -104,8 +107,10 @@ async function withRetry( export class OpenAIEmbedding implements LLM { private client: OpenAI; private chatClient: OpenAI; + private rerankClient: OpenAI; private embedModel: string; private expansionModel: string; + private rerankModel: string; constructor(config: OpenAIConfig = {}) { const apiKey = config.apiKey || process.env.QMD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; @@ -119,8 +124,16 @@ export class OpenAIEmbedding implements LLM { ? new OpenAI({ apiKey: chatApiKey, baseURL: chatBaseURL }) : this.client; + // Rerank client: falls back to chat client, then base client + const rerankApiKey = config.rerankApiKey || process.env.QMD_OPENAI_RERANK_API_KEY || chatApiKey; + const rerankBaseURL = config.rerankBaseURL || process.env.QMD_OPENAI_RERANK_BASE_URL || chatBaseURL; + this.rerankClient = (rerankBaseURL !== chatBaseURL || rerankApiKey !== chatApiKey) + ? new OpenAI({ apiKey: rerankApiKey, baseURL: rerankBaseURL }) + : this.chatClient; + this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; this.expansionModel = config.expansionModel || DEFAULT_EXPANSION_MODEL; + this.rerankModel = config.rerankModel || config.expansionModel || DEFAULT_EXPANSION_MODEL; } async embed(text: string, options?: EmbedOptions): Promise { @@ -240,7 +253,7 @@ Generate 1-2 of each type. Be concise. Include the original query terms.` async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { if (documents.length === 0) { - return { results: [], model: `${this.expansionModel}-rerank` }; + return { results: [], model: `${this.rerankModel}-rerank` }; } // For very small sets, skip the API call — not worth the latency @@ -257,12 +270,12 @@ Generate 1-2 of each type. Be concise. Include the original query terms.` try { // Truncate documents for the prompt — 500 chars each is enough for relevance judgment - const truncated = documents.map((doc, i) => + const truncated = documents.map((doc, i) => `[${i}] ${doc.title ? doc.title + ': ' : ''}${doc.text.slice(0, 500).replace(/\n+/g, ' ')}` ); - const response = await withRetry(() => this.chatClient.chat.completions.create({ - model: this.expansionModel, + const response = await withRetry(() => this.rerankClient.chat.completions.create({ + model: this.rerankModel, messages: [ { role: 'system', @@ -315,7 +328,7 @@ Example output for 5 documents: 2,0,4,1,3` return { results, - model: `${this.expansionModel}-rerank`, + model: `${this.rerankModel}-rerank`, }; } catch (error) { // Fallback: preserve original order if reranking fails diff --git a/src/store.ts b/src/store.ts index 421bd82d5..f13b2237b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -27,6 +27,7 @@ import { formatQueryForEmbedding, formatDocForEmbedding, withLLMSessionForLlm, + withEmbeddingSession, type RerankDocument, type ILLMSession, } from "./llm.js"; @@ -1432,12 +1433,7 @@ export async function generateEmbeddings( const totalDocs = docsToEmbed.length; const startTime = Date.now(); - // Use store's LlamaCpp or global singleton, wrapped in a session - const llm = getLlm(store); - const embedModelUri = llm.embedModelName; - - // Create a session manager for this llm instance - const result = await withLLMSessionForLlm(llm, async (session) => { + const result = await withEmbeddingSession(async (session, embedModelUri) => { let chunksEmbedded = 0; let errors = 0; let bytesProcessed = 0; @@ -1581,7 +1577,7 @@ export async function generateEmbeddings( } return { chunksEmbedded, errors }; - }, { maxDuration: 30 * 60 * 1000, name: 'generateEmbeddings' }); + }, { maxDuration: 30 * 60 * 1000, name: 'generateEmbeddings', storeLlm: store.llm ?? undefined }); return { docsProcessed: totalDocs, @@ -3379,12 +3375,6 @@ export function insertEmbedding( // ============================================================================= export async function expandQuery(query: string, model: string = DEFAULT_QUERY_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise { - // Skip query expansion when using OpenAI (avoids loading local model) - // Return a lex query to let BM25 handle it - if (isUsingOpenAI()) { - return [{ type: 'lex' as const, query }]; - } - // Check cache first — stored as JSON preserving types const cacheKey = getCacheKey("expandQuery", { query, model, ...(intent && { intent }) }); const cached = getCachedResult(db, cacheKey); @@ -3402,8 +3392,7 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M } } - const llm = llmOverride ?? getDefaultLlamaCpp(); - // Note: LlamaCpp uses hardcoded model, model parameter is ignored + const llm = isUsingOpenAI() ? getDefaultEmbeddingLLM() : (llmOverride ?? getDefaultLlamaCpp()); const results = await llm.expandQuery(query, { intent }); // Map Queryable[] → ExpandedQuery[] (same shape, decoupled from llm.ts internals). @@ -4612,8 +4601,9 @@ export async function structuredSearch( s.type === 'vec' || s.type === 'hyde' ); if (vecSearches.length > 0) { - const llm = getLlm(store); - const textsToEmbed = vecSearches.map(s => formatQueryForEmbedding(s.query, llm.embedModelName)); + const llm = isUsingOpenAI() ? getDefaultEmbeddingLLM() : getLlm(store); + const modelName = isUsingOpenAI() ? llm.getModelName() : (llm as LlamaCpp).embedModelName; + const textsToEmbed = vecSearches.map(s => formatQueryForEmbedding(s.query, modelName)); hooks?.onEmbedStart?.(textsToEmbed.length); const embedStart = Date.now(); const embeddings = await llm.embedBatch(textsToEmbed); diff --git a/test/qmd-manager.test.ts b/test/qmd-manager.test.ts new file mode 100644 index 000000000..a779e690f --- /dev/null +++ b/test/qmd-manager.test.ts @@ -0,0 +1,281 @@ +/** + * qmd-manager.test.ts — Unit tests for QmdManager + * + * QmdManager is a high-level orchestrator that wraps QMDStore with: + * - configurable similarity thresholds (minScore, limit) + * - pluggable similarity engine (LlamaCpp) + * - pluggable storage (SQLite Database) + * - inactivity timer for automatic resource cleanup + * + * All three dependencies (timer, storage, similarity engine) are injected + * so tests run fully in-memory without touching the filesystem or GPU. + * + * Run with: bun test qmd-manager.test.ts + */ + +import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; +import type { QmdManager, QmdManagerOptions } from "../src/qmd-manager.js"; + +// ============================================================================= +// Dependency Mocks +// ============================================================================= + +/** + * Mock LlamaCpp (similarity engine). + * Mirrors the shape used by the real LlamaCpp in src/llm.ts: + * - embed() → number[][] + * - rerank() → scored candidates + * - dispose() + */ +function makeMockSimilarityEngine() { + return { + embed: vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]), + rerank: vi.fn().mockResolvedValue([]), + dispose: vi.fn().mockResolvedValue(undefined), + // Inactivity-timer hook — checked by QmdManager before auto-dispose + canUnload: vi.fn().mockReturnValue(true), + }; +} + +/** + * Mock SQLite Database (storage layer). + * Mirrors the minimal interface QmdManager uses from src/db.ts. + */ +function makeMockStorage() { + return { + prepare: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue(undefined), + all: vi.fn().mockReturnValue([]), + run: vi.fn(), + }), + exec: vi.fn(), + close: vi.fn(), + }; +} + +/** + * Mock timer — replaces the real inactivity setTimeout/clearTimeout so + * tests can advance time without waiting. + * + * The returned object exposes `fire()` to manually trigger the callback, + * which lets tests assert cleanup behaviour without `vi.useFakeTimers`. + */ +function makeMockTimer() { + let callback: (() => void) | null = null; + let scheduled = false; + + return { + schedule: vi.fn((fn: () => void, _ms: number) => { + callback = fn; + scheduled = true; + }), + cancel: vi.fn(() => { + callback = null; + scheduled = false; + }), + /** Manually trigger the scheduled callback (simulates timeout firing). */ + fire() { + if (callback) callback(); + }, + get isScheduled() { + return scheduled; + }, + }; +} + +// ============================================================================= +// Factory Helper +// ============================================================================= + +/** + * Instantiate a QmdManager with fully-mocked dependencies. + * + * @param overrides Partial QmdManagerOptions merged on top of safe defaults. + * + * Default thresholds: + * minScore = 0.0 (accept all results) + * limit = 10 + * inactivityTimeoutMs = 300_000 (5 min) + */ +async function createTestManager(overrides: Partial = {}): Promise<{ + manager: QmdManager; + similarityEngine: ReturnType; + storage: ReturnType; + timer: ReturnType; +}> { + const similarityEngine = overrides.similarityEngine ?? makeMockSimilarityEngine(); + const storage = overrides.storage ?? makeMockStorage(); + const timer = overrides.timer ?? makeMockTimer(); + + const { QmdManager } = await import("../src/qmd-manager.js"); + + const manager = new QmdManager({ + minScore: 0.0, + limit: 10, + inactivityTimeoutMs: 300_000, + ...overrides, + similarityEngine: similarityEngine as any, + storage: storage as any, + timer: timer as any, + }); + + return { manager, similarityEngine, storage, timer }; +} + +// ============================================================================= +// Lifecycle +// ============================================================================= + +describe("QmdManager — lifecycle", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("constructs without error given valid options", async () => { + const { manager } = await createTestManager(); + expect(manager).toBeDefined(); + }); + + test("close() disposes the similarity engine", async () => { + const { manager, similarityEngine } = await createTestManager(); + await manager.close(); + expect(similarityEngine.dispose).toHaveBeenCalledOnce(); + }); + + test("close() closes the storage", async () => { + const { manager, storage } = await createTestManager(); + await manager.close(); + expect(storage.close).toHaveBeenCalledOnce(); + }); + + test("close() cancels any pending inactivity timer", async () => { + const { manager, timer } = await createTestManager(); + await manager.close(); + expect(timer.cancel).toHaveBeenCalled(); + }); +}); + +// ============================================================================= +// Inactivity Timer +// ============================================================================= + +describe("QmdManager — inactivity timer", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("schedules inactivity timer after construction when timeout > 0", async () => { + const { timer } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + expect(timer.schedule).toHaveBeenCalledWith(expect.any(Function), 5_000); + }); + + test("does not schedule timer when inactivityTimeoutMs is 0", async () => { + const { timer } = await createTestManager({ inactivityTimeoutMs: 0 }); + expect(timer.schedule).not.toHaveBeenCalled(); + }); + + test("timer fires → calls dispose on similarity engine when idle", async () => { + const { timer, similarityEngine } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + // Simulate the timer firing while nothing is in-flight + similarityEngine.canUnload.mockReturnValue(true); + timer.fire(); + expect(similarityEngine.dispose).toHaveBeenCalled(); + }); + + test("timer fires → skips dispose when similarity engine is busy", async () => { + const { timer, similarityEngine } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + similarityEngine.canUnload.mockReturnValue(false); + timer.fire(); + expect(similarityEngine.dispose).not.toHaveBeenCalled(); + }); +}); + +// ============================================================================= +// Threshold Options +// ============================================================================= + +describe("QmdManager — threshold options", () => { + test("accepts default threshold options", async () => { + const { manager } = await createTestManager(); + expect(manager.options.minScore).toBe(0.0); + expect(manager.options.limit).toBe(10); + }); + + test("accepts custom minScore threshold", async () => { + const { manager } = await createTestManager({ minScore: 0.75 }); + expect(manager.options.minScore).toBe(0.75); + }); + + test("accepts custom result limit", async () => { + const { manager } = await createTestManager({ limit: 25 }); + expect(manager.options.limit).toBe(25); + }); + + test("rejects minScore outside [0, 1]", async () => { + await expect(createTestManager({ minScore: -0.1 })).rejects.toThrow(); + await expect(createTestManager({ minScore: 1.1 })).rejects.toThrow(); + }); + + test("rejects limit <= 0", async () => { + await expect(createTestManager({ limit: 0 })).rejects.toThrow(); + await expect(createTestManager({ limit: -1 })).rejects.toThrow(); + }); +}); + +// ============================================================================= +// Search Integration (wired through mocked dependencies) +// ============================================================================= + +describe("QmdManager — search", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("search() calls similarity engine embed with the query", async () => { + const { manager, similarityEngine } = await createTestManager(); + await manager.search("authentication flow"); + expect(similarityEngine.embed).toHaveBeenCalledWith( + expect.stringContaining("authentication flow"), + ); + }); + + test("search() filters results below minScore threshold", async () => { + const { manager, similarityEngine } = await createTestManager({ minScore: 0.8 }); + // Mock returns a mix of high and low-score docs + similarityEngine.rerank.mockResolvedValue([ + { path: "high.md", score: 0.9 }, + { path: "low.md", score: 0.5 }, + ]); + + const results = await manager.search("query"); + expect(results.every((r) => r.score >= 0.8)).toBe(true); + }); + + test("search() respects the limit option", async () => { + const { manager, similarityEngine } = await createTestManager({ limit: 3 }); + similarityEngine.rerank.mockResolvedValue([ + { path: "a.md", score: 0.9 }, + { path: "b.md", score: 0.85 }, + { path: "c.md", score: 0.8 }, + { path: "d.md", score: 0.75 }, + ]); + + const results = await manager.search("query"); + expect(results.length).toBeLessThanOrEqual(3); + }); + + test("search() returns empty array when similarity engine finds nothing", async () => { + const { manager, similarityEngine } = await createTestManager(); + similarityEngine.rerank.mockResolvedValue([]); + const results = await manager.search("unknown topic"); + expect(results).toEqual([]); + }); + + test("search() resets the inactivity timer after each call", async () => { + const { manager, timer } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + const callsBefore = (timer.schedule as ReturnType).mock.calls.length; + await manager.search("query"); + const callsAfter = (timer.schedule as ReturnType).mock.calls.length; + expect(callsAfter).toBeGreaterThan(callsBefore); + }); +}); From b7c52063cfcbd48e4d99babaa0626a5ff3c6038c Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Tue, 7 Apr 2026 11:35:06 -0600 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20address=20PR=20#116=20feedback=20?= =?UTF-8?q?=E2=80=94=20base=5Furl,=20expansion=5Fmodel,=20env=20rename,=20?= =?UTF-8?q?embed=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes based on PR comments: 1. Configurable base_url for OpenAI-compatible APIs (Ollama, vLLM, Azure) - collections.ts: EmbeddingProviderConfig already has base_url field - qmd.ts: now passes base_url and expansion_model from YAML to setEmbeddingConfig - openai-llm.ts: constructor accepts baseURL config 2. Env var rename: QMD_OPENAI_API_KEY takes priority over OPENAI_API_KEY - Avoids conflict with official openai-node SDK (per @alexleach) - Falls back to OPENAI_API_KEY for backwards compatibility 3. generateEmbeddings bypasses LlamaCpp when using OpenAI (per @viniciushsantana) - OpenAI path calls API directly, no local model session needed - Refactored to shared runEmbedding() with pluggable embed/embedBatch fns 4. expandQuery now actually calls OpenAI for query expansion - Was previously returning lex-only fallback when isUsingOpenAI() - Now uses gpt-4o-mini via openaiLLM.expandQuery() 5. README updated with base_url, expansion_model docs Addresses: @alexleach (env naming, base_url), @viniciushsantana (embed fix, expansion_model, base_url YAML wiring) --- README.md | 7 +++++-- src/store.ts | 30 ++++++++++++++++++------------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fecf44514..f05d3ae8e 100644 --- a/README.md +++ b/README.md @@ -524,8 +524,10 @@ As an alternative to local embedding models, you can use OpenAI's API for faster embedding: provider: openai openai: - api_key: sk-... # Optional, falls back to OPENAI_API_KEY env var + api_key: sk-... # Optional, falls back to QMD_OPENAI_API_KEY or OPENAI_API_KEY env var model: text-embedding-3-small # Optional, this is the default + expansion_model: gpt-4o-mini # Optional, model for query expansion/reranking + base_url: https://api.openai.com/v1 # Optional, for OpenAI-compatible APIs (Ollama, vLLM, etc.) ``` Benefits: @@ -533,8 +535,9 @@ Benefits: - **No GPU required** - works on any machine - **More reliable** - no local model loading issues - **Cost:** ~$0.02 per 1M tokens (very cheap) +- **OpenAI-compatible** - works with Ollama, vLLM, Azure, and other compatible APIs via `base_url` -When using OpenAI embeddings, query expansion and reranking are skipped to avoid loading local models. +When using OpenAI embeddings, query expansion and reranking use the OpenAI API instead of local models. ## Installation diff --git a/src/store.ts b/src/store.ts index f13b2237b..cfbd05e90 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1443,8 +1443,7 @@ export async function generateEmbeddings( const batches = buildEmbeddingBatches(docsToEmbed, maxDocsPerBatch, maxBatchBytes); for (const batchMeta of batches) { - // Abort early if session has been invalidated - if (!session.isValid) { + if (!isValid()) { console.warn(`⚠ Session expired — skipping remaining document batches`); break; } @@ -1462,7 +1461,7 @@ export async function generateEmbeddings( undefined, undefined, undefined, doc.path, options?.chunkStrategy, - session.signal, + signal, ); for (let seq = 0; seq < chunks.length; seq++) { @@ -1489,7 +1488,7 @@ export async function generateEmbeddings( if (!vectorTableInitialized) { const firstChunk = batchChunks[0]!; const firstText = formatDocForEmbedding(firstChunk.text, firstChunk.title, embedModelUri); - const firstResult = await session.embed(firstText, { model }); + const firstResult = await embedFn(firstText); if (!firstResult) { throw new Error("Failed to get embedding dimensions from first chunk"); } @@ -1501,15 +1500,13 @@ export async function generateEmbeddings( let batchChunkBytesProcessed = 0; for (let batchStart = 0; batchStart < batchChunks.length; batchStart += BATCH_SIZE) { - // Abort early if session has been invalidated (e.g. max duration exceeded) - if (!session.isValid) { + if (!isValid()) { const remaining = batchChunks.length - batchStart; errors += remaining; console.warn(`⚠ Session expired — skipping ${remaining} remaining chunks`); break; } - // Abort early if error rate is too high (>80% of processed chunks failed) const processed = chunksEmbedded + errors; if (processed >= BATCH_SIZE && errors > processed * 0.8) { const remaining = batchChunks.length - batchStart; @@ -1523,7 +1520,7 @@ export async function generateEmbeddings( const texts = chunkBatch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title, embedModelUri)); try { - const embeddings = await session.embedBatch(texts, { model }); + const embeddings = await embedBatchFn(texts); for (let i = 0; i < chunkBatch.length; i++) { const chunk = chunkBatch[i]!; const embedding = embeddings[i]; @@ -1536,16 +1533,14 @@ export async function generateEmbeddings( batchChunkBytesProcessed += chunk.bytes; } } catch { - // Batch failed — try individual embeddings as fallback - // But skip if session is already invalid (avoids N doomed retries) - if (!session.isValid) { + if (!isValid()) { errors += chunkBatch.length; batchChunkBytesProcessed += chunkBatch.reduce((sum, c) => sum + c.bytes, 0); } else { for (const chunk of chunkBatch) { try { const text = formatDocForEmbedding(chunk.text, chunk.title, embedModelUri); - const result = await session.embed(text, { model }); + const result = await embedFn(text); if (result) { insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now); chunksEmbedded++; @@ -3375,6 +3370,17 @@ export function insertEmbedding( // ============================================================================= export async function expandQuery(query: string, model: string = DEFAULT_QUERY_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise { + // Use OpenAI query expansion when configured (fast, ~200ms via gpt-4o-mini) + if (isUsingOpenAI()) { + const openaiLLM = getDefaultEmbeddingLLM(); + const results = await openaiLLM.expandQuery(query, { context: intent, includeLexical: true }); + const expanded = results + .filter(r => r.text !== query) + .map(r => ({ type: r.type, query: r.text })); + if (expanded.length > 0) return expanded; + return [{ type: 'lex' as const, query }]; + } + // Check cache first — stored as JSON preserving types const cacheKey = getCacheKey("expandQuery", { query, model, ...(intent && { intent }) }); const cached = getCachedResult(db, cacheKey); From 5a32e25b3cf7533776b94874088a45f095d939f5 Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Tue, 7 Apr 2026 11:55:10 -0600 Subject: [PATCH 09/17] feat: lazy-load node-llama-cpp, input truncation, OpenAI mode improvements - Lazy-load node-llama-cpp to skip native compilation in OpenAI mode - Add tiktoken-based input truncation (QMD_OPENAI_MAX_INPUT_TOKENS) - QMD_OPENAI_BASE_URL auto-activates OpenAI mode (no QMD_OPENAI=1 needed) - Skip LlamaCpp init in qmd status when using OpenAI - Restore terminal cursor on embed error (try/finally) - Bypass withLLMSession in vectorSearch/querySearch for OpenAI mode Co-authored-by: ALB.Leach --- src/cli/qmd.ts | 164 +++++++++++++++++++++++++++------------------- src/openai-llm.ts | 40 +++++++++-- src/store.ts | 14 ++-- 3 files changed, 139 insertions(+), 79 deletions(-) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 800ff6ccd..a388ad011 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -78,7 +78,7 @@ import { type ReindexResult, type ChunkStrategy, } from "../store.js"; -import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, getDefaultEmbeddingLLM, withLLMSession, pullModels, setEmbeddingConfig, isUsingOpenAI, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; +import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, getDefaultEmbeddingLLM, getEmbeddingConfig, withLLMSession, pullModels, setEmbeddingConfig, isUsingOpenAI, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; import { formatSearchResults, formatDocuments, @@ -455,8 +455,14 @@ async function showStatus(): Promise { console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`); } - // Models - { + // Models / Provider info + if (isUsingOpenAI()) { + const embCfg = getEmbeddingConfig(); + console.log(`\n${c.bold}Provider${c.reset}`); + console.log(` Mode: ${c.green}OpenAI-compatible${c.reset}`); + console.log(` Base URL: ${embCfg.openai?.baseURL || process.env.QMD_OPENAI_BASE_URL || '(default)'}`); + console.log(` Embed model: ${embCfg.openai?.embedModel || 'text-embedding-3-small'}`); + } else { // hf:org/repo/file.gguf → https://huggingface.co/org/repo const hfLink = (uri: string) => { const match = uri.match(/^hf:([^/]+\/[^/]+)\//); @@ -466,38 +472,37 @@ async function showStatus(): Promise { console.log(` Embedding: ${hfLink(DEFAULT_EMBED_MODEL_URI)}`); console.log(` Reranking: ${hfLink(DEFAULT_RERANK_MODEL_URI)}`); console.log(` Generation: ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`); - } - // Device / GPU info - console.log(`\n${c.bold}Device${c.reset}`); - try { - const llm = getDefaultLlamaCpp(); - const device = await llm.getDeviceInfo({ allowBuild: false }); - if (device.gpu) { - console.log(` GPU: ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`); - if (device.gpuDevices.length > 0) { - // Deduplicate and count GPUs - const counts = new Map(); - for (const name of device.gpuDevices) { - counts.set(name, (counts.get(name) || 0) + 1); + // Device / GPU info (local mode only — skip in OpenAI mode to avoid triggering compilation) + console.log(`\n${c.bold}Device${c.reset}`); + try { + const llm = getDefaultLlamaCpp(); + const device = await llm.getDeviceInfo({ allowBuild: false }); + if (device.gpu) { + console.log(` GPU: ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`); + if (device.gpuDevices.length > 0) { + const counts = new Map(); + for (const name of device.gpuDevices) { + counts.set(name, (counts.get(name) || 0) + 1); + } + const deviceStr = Array.from(counts.entries()) + .map(([name, count]) => count > 1 ? `${count}× ${name}` : name) + .join(', '); + console.log(` Devices: ${deviceStr}`); } - const deviceStr = Array.from(counts.entries()) - .map(([name, count]) => count > 1 ? `${count}× ${name}` : name) - .join(', '); - console.log(` Devices: ${deviceStr}`); + if (device.vram) { + console.log(` VRAM: ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`); + } + } else { + console.log(` GPU: ${c.yellow}none${c.reset} (running on CPU — models will be slow)`); + console.log(` ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`); } - if (device.vram) { - console.log(` VRAM: ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`); + console.log(` CPU: ${device.cpuCores} math cores`); + } catch (error) { + console.log(` Status: ${c.dim}skipped${c.reset} (status probe does not build llama.cpp backends)`); + if (error instanceof Error && error.message) { + console.log(` ${c.dim}${error.message}${c.reset}`); } - } else { - console.log(` GPU: ${c.yellow}none${c.reset} (running on CPU — models will be slow)`); - console.log(` ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`); - } - console.log(` CPU: ${device.cpuCores} math cores`); - } catch (error) { - console.log(` Status: ${c.dim}skipped${c.reset} (status probe does not build llama.cpp backends)`); - if (error instanceof Error && error.message) { - console.log(` ${c.dim}${error.message}${c.reset}`); } } @@ -1705,34 +1710,37 @@ async function vectorIndex( const startTime = Date.now(); - const result = await generateEmbeddings(storeInstance, { - force, - model, - maxDocsPerBatch: batchOptions?.maxDocsPerBatch, - maxBatchBytes: batchOptions?.maxBatchBytes, - chunkStrategy: batchOptions?.chunkStrategy, - onProgress: (info) => { - if (info.totalBytes === 0) return; - const percent = (info.bytesProcessed / info.totalBytes) * 100; - progress.set(percent); - - const elapsed = (Date.now() - startTime) / 1000; - const bytesPerSec = info.bytesProcessed / elapsed; - const remainingBytes = info.totalBytes - info.bytesProcessed; - const etaSec = remainingBytes / bytesPerSec; - - const bar = renderProgressBar(percent); - const percentStr = percent.toFixed(0).padStart(3); - const throughput = `${formatBytes(bytesPerSec)}/s`; - const eta = elapsed > 2 ? formatETA(etaSec) : "..."; - const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : ""; - - if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `); - }, - }); + let result: Awaited>; + try { + result = await generateEmbeddings(storeInstance, { + force, + model, + maxDocsPerBatch: batchOptions?.maxDocsPerBatch, + maxBatchBytes: batchOptions?.maxBatchBytes, + chunkStrategy: batchOptions?.chunkStrategy, + onProgress: (info) => { + if (info.totalBytes === 0) return; + const percent = (info.bytesProcessed / info.totalBytes) * 100; + progress.set(percent); - progress.clear(); - cursor.show(); + const elapsed = (Date.now() - startTime) / 1000; + const bytesPerSec = info.bytesProcessed / elapsed; + const remainingBytes = info.totalBytes - info.bytesProcessed; + const etaSec = remainingBytes / bytesPerSec; + + const bar = renderProgressBar(percent); + const percentStr = percent.toFixed(0).padStart(3); + const throughput = `${formatBytes(bytesPerSec)}/s`; + const eta = elapsed > 2 ? formatETA(etaSec) : "..."; + const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : ""; + + if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `); + }, + }); + } finally { + progress.clear(); + cursor.show(); + } const totalTimeSec = result.durationMs / 1000; @@ -2285,7 +2293,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string = checkIndexHealth(store.db); - await withLLMSession(async () => { + const llmSession = async () => { let results = await vectorSearchQuery(store, query, { collection: singleCollection, limit: opts.all ? 500 : (opts.limit || 10), @@ -2323,7 +2331,15 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string = context: r.context, docid: r.docid, })), query, { ...opts, limit: results.length }); - }, { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' }); + }; + + if (isUsingOpenAI()) { + await llmSession(); + } else { + await withLLMSession(async () => llmSession(), + { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' } + ); + } } async function querySearch(query: string, opts: OutputOptions, _embedModel: string = DEFAULT_EMBED_MODEL, _rerankModel: string = DEFAULT_RERANK_MODEL): Promise { @@ -2341,7 +2357,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri // Intent can come from --intent flag or from intent: line in query document const intent = opts.intent || parsed?.intent; - await withLLMSession(async () => { + const querySession = async () => { let results; if (parsed) { @@ -2461,7 +2477,15 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri docid: r.docid, explain: r.explain, })), displayQuery, { ...opts, limit: results.length }); - }, { maxDuration: 10 * 60 * 1000, name: 'querySearch' }); + }; + + if (isUsingOpenAI()) { + await querySession(); + } else { + await withLLMSession(async () => querySession(), + { maxDuration: 10 * 60 * 1000, name: 'querySearch' } + ); + } } // Parse CLI arguments using util.parseArgs @@ -2846,17 +2870,23 @@ if (isMain) { process.exit(cli.values.help ? 0 : 1); } - // Load embedding configuration from config file + // Load embedding configuration. + // Priority: YAML config > env vars > default (local). + // Setting QMD_OPENAI_BASE_URL alone is enough to activate OpenAI mode. const embeddingYamlConfig = getEmbeddingConfigFromYaml(); - if (embeddingYamlConfig.provider === 'openai') { + const useOpenAI = embeddingYamlConfig.provider === 'openai' + || !!process.env.QMD_OPENAI_BASE_URL + || process.env.QMD_OPENAI === '1'; + + if (useOpenAI) { setEmbeddingConfig({ provider: 'openai', openai: { - apiKey: embeddingYamlConfig.openai?.api_key, - embedModel: embeddingYamlConfig.openai?.model, + apiKey: embeddingYamlConfig.openai?.api_key || process.env.QMD_OPENAI_API_KEY, + embedModel: embeddingYamlConfig.openai?.model || process.env.QMD_OPENAI_EMBED_MODEL, expansionModel: embeddingYamlConfig.openai?.expansion_model, rerankModel: embeddingYamlConfig.openai?.rerank_model, - baseURL: embeddingYamlConfig.openai?.base_url, + baseURL: embeddingYamlConfig.openai?.base_url || process.env.QMD_OPENAI_BASE_URL, chatBaseURL: embeddingYamlConfig.openai?.chat_base_url, chatApiKey: embeddingYamlConfig.openai?.chat_api_key, rerankBaseURL: embeddingYamlConfig.openai?.rerank_base_url, diff --git a/src/openai-llm.ts b/src/openai-llm.ts index 10e53c721..822502b76 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -6,9 +6,10 @@ */ import OpenAI from 'openai'; -import type { - LLM, - EmbedOptions, +import { get_encoding } from 'tiktoken'; +import type { + LLM, + EmbedOptions, EmbeddingResult, GenerateOptions, GenerateResult, @@ -29,8 +30,33 @@ export type OpenAIConfig = { chatApiKey?: string; rerankBaseURL?: string; rerankApiKey?: string; + maxInputTokens?: number; }; +// Lazy tiktoken encoder (cl100k_base covers most OpenAI-compatible models) +let _enc: ReturnType | null = null; +function getEncoder() { + if (!_enc) _enc = get_encoding('cl100k_base'); + return _enc; +} + +function resolveMaxInputTokens(config?: number): number { + if (config !== undefined) return config; + const env = parseInt(process.env.QMD_OPENAI_MAX_INPUT_TOKENS ?? '', 10); + return Number.isFinite(env) && env > 0 ? env : 512; +} + +/** + * Truncate text to at most maxTokens tokens using tiktoken. + * Returns the original string if it's already within the limit. + */ +function truncateToTokenLimit(text: string, maxTokens: number): string { + const enc = getEncoder(); + const tokens = enc.encode(text); + if (tokens.length <= maxTokens) return text; + return new TextDecoder().decode(enc.decode(tokens.slice(0, maxTokens))); +} + const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; const DEFAULT_EXPANSION_MODEL = 'gpt-4o-mini'; @@ -111,6 +137,7 @@ export class OpenAIEmbedding implements LLM { private embedModel: string; private expansionModel: string; private rerankModel: string; + private maxInputTokens: number; constructor(config: OpenAIConfig = {}) { const apiKey = config.apiKey || process.env.QMD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; @@ -134,13 +161,15 @@ export class OpenAIEmbedding implements LLM { this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; this.expansionModel = config.expansionModel || DEFAULT_EXPANSION_MODEL; this.rerankModel = config.rerankModel || config.expansionModel || DEFAULT_EXPANSION_MODEL; + this.maxInputTokens = resolveMaxInputTokens(config.maxInputTokens); } async embed(text: string, options?: EmbedOptions): Promise { + const input = truncateToTokenLimit(text, this.maxInputTokens); return withRetry(async () => { const response = await this.client.embeddings.create({ model: this.embedModel, - input: text, + input, }); const data = response.data[0]; if (!data) { @@ -158,11 +187,12 @@ export class OpenAIEmbedding implements LLM { } async embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]> { + const inputs = texts.map(t => truncateToTokenLimit(t, this.maxInputTokens)); return withRetry(async () => { // OpenAI supports batch embedding natively const response = await this.client.embeddings.create({ model: this.embedModel, - input: texts, + input: inputs, }); return response.data.map(item => ({ embedding: item.embedding, diff --git a/src/store.ts b/src/store.ts index cfbd05e90..780c5d952 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1443,7 +1443,7 @@ export async function generateEmbeddings( const batches = buildEmbeddingBatches(docsToEmbed, maxDocsPerBatch, maxBatchBytes); for (const batchMeta of batches) { - if (!isValid()) { + if (!session.isValid) { console.warn(`⚠ Session expired — skipping remaining document batches`); break; } @@ -1461,7 +1461,7 @@ export async function generateEmbeddings( undefined, undefined, undefined, doc.path, options?.chunkStrategy, - signal, + session.signal, ); for (let seq = 0; seq < chunks.length; seq++) { @@ -1488,7 +1488,7 @@ export async function generateEmbeddings( if (!vectorTableInitialized) { const firstChunk = batchChunks[0]!; const firstText = formatDocForEmbedding(firstChunk.text, firstChunk.title, embedModelUri); - const firstResult = await embedFn(firstText); + const firstResult = await session.embed(firstText); if (!firstResult) { throw new Error("Failed to get embedding dimensions from first chunk"); } @@ -1500,7 +1500,7 @@ export async function generateEmbeddings( let batchChunkBytesProcessed = 0; for (let batchStart = 0; batchStart < batchChunks.length; batchStart += BATCH_SIZE) { - if (!isValid()) { + if (!session.isValid) { const remaining = batchChunks.length - batchStart; errors += remaining; console.warn(`⚠ Session expired — skipping ${remaining} remaining chunks`); @@ -1520,7 +1520,7 @@ export async function generateEmbeddings( const texts = chunkBatch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title, embedModelUri)); try { - const embeddings = await embedBatchFn(texts); + const embeddings = await session.embedBatch(texts); for (let i = 0; i < chunkBatch.length; i++) { const chunk = chunkBatch[i]!; const embedding = embeddings[i]; @@ -1533,14 +1533,14 @@ export async function generateEmbeddings( batchChunkBytesProcessed += chunk.bytes; } } catch { - if (!isValid()) { + if (!session.isValid) { errors += chunkBatch.length; batchChunkBytesProcessed += chunkBatch.reduce((sum, c) => sum + c.bytes, 0); } else { for (const chunk of chunkBatch) { try { const text = formatDocForEmbedding(chunk.text, chunk.title, embedModelUri); - const result = await embedFn(text); + const result = await session.embed(text); if (result) { insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now); chunksEmbedded++; From 8eab131ce49fd19c6b68fc756689a7d95b80693f Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Mon, 27 Apr 2026 10:25:06 -0600 Subject: [PATCH 10/17] fix: apply OpenAI embedding config in SDK mode --- src/index.ts | 30 +++++++++++++++++++++++ test/sdk.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 677234743..ec404538b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ import { } from "./store.js"; import { LlamaCpp, + setEmbeddingConfig, } from "./llm.js"; import { setConfigSource, @@ -335,6 +336,33 @@ export interface QMDStore { * await store.close() * ``` */ +function configureEmbeddingProvider(config?: CollectionConfig): void { + const embeddingYamlConfig = config?.embedding || { provider: 'local' as const }; + const useOpenAI = embeddingYamlConfig.provider === 'openai' + || !!process.env.QMD_OPENAI_BASE_URL + || process.env.QMD_OPENAI === '1'; + + if (useOpenAI) { + setEmbeddingConfig({ + provider: 'openai', + openai: { + apiKey: embeddingYamlConfig.openai?.api_key || process.env.QMD_OPENAI_API_KEY, + embedModel: embeddingYamlConfig.openai?.model || process.env.QMD_OPENAI_EMBED_MODEL, + expansionModel: embeddingYamlConfig.openai?.expansion_model, + rerankModel: embeddingYamlConfig.openai?.rerank_model, + baseURL: embeddingYamlConfig.openai?.base_url || process.env.QMD_OPENAI_BASE_URL, + chatBaseURL: embeddingYamlConfig.openai?.chat_base_url, + chatApiKey: embeddingYamlConfig.openai?.chat_api_key, + rerankBaseURL: embeddingYamlConfig.openai?.rerank_base_url, + rerankApiKey: embeddingYamlConfig.openai?.rerank_api_key, + }, + }); + return; + } + + setEmbeddingConfig({ provider: 'local' }); +} + export async function createStore(options: StoreOptions): Promise { if (!options.dbPath) { throw new Error("dbPath is required"); @@ -365,6 +393,8 @@ export async function createStore(options: StoreOptions): Promise { } // else: DB-only mode — no external config, use existing store_collections + configureEmbeddingProvider(config); + // Create a per-store LlamaCpp instance — lazy-loads models on first use, // auto-unloads after 5 min inactivity to free VRAM. const llm = new LlamaCpp({ diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 689da27b9..0994de90d 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -22,7 +22,7 @@ import { type VectorSearchOptions, type ExpandQueryOptions, } from "../src/index.js"; -import { setDefaultLlamaCpp } from "../src/llm.js"; +import { getEmbeddingConfig, setDefaultLlamaCpp, setEmbeddingConfig } from "../src/llm.js"; // ============================================================================= // Test Helpers @@ -66,6 +66,14 @@ function freshDbPath(): string { // ============================================================================= describe("createStore", () => { + afterEach(() => { + delete process.env.QMD_OPENAI; + delete process.env.QMD_OPENAI_API_KEY; + delete process.env.QMD_OPENAI_BASE_URL; + delete process.env.QMD_OPENAI_EMBED_MODEL; + setEmbeddingConfig({ provider: "local" }); + }); + test("creates store with inline config", async () => { const store = await createStore({ dbPath: freshDbPath(), @@ -146,6 +154,60 @@ describe("createStore", () => { expect(store.dbPath).toBe(dbPath); await store.close(); }); + + test("applies OpenAI embedding config in inline SDK mode", async () => { + const store = await createStore({ + dbPath: freshDbPath(), + config: { + collections: {}, + embedding: { + provider: "openai", + openai: { + api_key: "inline-key", + model: "text-embedding-3-small", + expansion_model: "gpt-4o-mini", + rerank_model: "rerank-v3", + base_url: "http://localhost:11434/v1", + }, + }, + }, + }); + + expect(getEmbeddingConfig()).toMatchObject({ + provider: "openai", + openai: { + apiKey: "inline-key", + embedModel: "text-embedding-3-small", + expansionModel: "gpt-4o-mini", + rerankModel: "rerank-v3", + baseURL: "http://localhost:11434/v1", + }, + }); + + await store.close(); + }); + + test("activates OpenAI embedding config from SDK env vars", async () => { + process.env.QMD_OPENAI_BASE_URL = "http://localhost:8080/v1"; + process.env.QMD_OPENAI_API_KEY = "env-key"; + process.env.QMD_OPENAI_EMBED_MODEL = "nomic-embed-text"; + + const store = await createStore({ + dbPath: freshDbPath(), + config: { collections: {} }, + }); + + expect(getEmbeddingConfig()).toMatchObject({ + provider: "openai", + openai: { + apiKey: "env-key", + embedModel: "nomic-embed-text", + baseURL: "http://localhost:8080/v1", + }, + }); + + await store.close(); + }); }); // ============================================================================= From 6d06cf8f54b4f56460c88dc7b89afc3bf2c9192f Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Mon, 27 Apr 2026 10:31:56 -0600 Subject: [PATCH 11/17] fix: close showStatus local model branch --- src/cli/qmd.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index bf6e9ea33..91049264a 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -472,6 +472,7 @@ async function showStatus(): Promise { console.log(` Embedding: ${hfLink(DEFAULT_EMBED_MODEL_URI)}`); console.log(` Reranking: ${hfLink(DEFAULT_RERANK_MODEL_URI)}`); console.log(` Generation: ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`); + } // Device / GPU info (local mode only — skip in OpenAI mode to avoid triggering compilation // Important: probing node-llama-cpp can abort the whole process on machines with From d46ea90e991477d1261004412b1be64ed8be2795 Mon Sep 17 00:00:00 2001 From: Jake Jones Date: Tue, 2 Jun 2026 12:10:14 -0600 Subject: [PATCH 12/17] embedding: prefer OpenAI defaults when configured --- src/collections.ts | 21 +++++++++++--- test/collections-config.test.ts | 50 +++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/collections.ts b/src/collections.ts index 5d31c35fc..8e6a7ef60 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -46,7 +46,7 @@ export interface ModelsConfig { * Embedding provider configuration (optional in config file) */ export interface EmbeddingProviderConfig { - provider?: 'local' | 'openai'; // Default: 'local' + provider?: 'local' | 'openai'; // Default: 'openai' when QMD_OPENAI or an OpenAI key is present, otherwise 'local' openai?: { api_key?: string; // Falls back to QMD_OPENAI_API_KEY / OPENAI_API_KEY env var model?: string; // Default: 'text-embedding-3-small' @@ -558,10 +558,23 @@ export function isValidCollectionName(name: string): boolean { } /** - * Get embedding configuration from config file - * Returns default (local) config if not specified + * Get embedding configuration from config file. + * + * QMD historically defaulted to the local node-llama-cpp embedding model when the + * config omitted `embedding`. That is a bad default for EdwinPAI installs: the + * first embed pass can download/load a local GGUF model and make setup feel + * frozen. Prefer OpenAI whenever the installer/runtime has explicitly enabled it + * (`QMD_OPENAI=1`) or an OpenAI-compatible key is available, while preserving the + * local fallback for standalone/offline installs. */ export function getEmbeddingConfig(): EmbeddingProviderConfig { const config = loadConfig(); - return config.embedding || { provider: 'local' }; + if (config.embedding) return config.embedding; + + const wantsOpenAI = + process.env.QMD_OPENAI === "1" || + Boolean(process.env.QMD_OPENAI_API_KEY?.trim()) || + Boolean(process.env.OPENAI_API_KEY?.trim()); + + return wantsOpenAI ? { provider: 'openai' } : { provider: 'local' }; } diff --git a/test/collections-config.test.ts b/test/collections-config.test.ts index ead770e87..cf7218181 100644 --- a/test/collections-config.test.ts +++ b/test/collections-config.test.ts @@ -10,7 +10,13 @@ import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { qmdHomedir } from "../src/paths.js"; -import { getConfigPath, loadConfig, setConfigIndexName } from "../src/collections.js"; +import { + getConfigPath, + getEmbeddingConfig, + loadConfig, + setConfigIndexName, + setConfigSource, +} from "../src/collections.js"; // Save/restore env vars around each test let savedEnv: Record; @@ -21,14 +27,18 @@ beforeEach(() => { USERPROFILE: process.env.USERPROFILE, QMD_CONFIG_DIR: process.env.QMD_CONFIG_DIR, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, + QMD_OPENAI: process.env.QMD_OPENAI, + QMD_OPENAI_API_KEY: process.env.QMD_OPENAI_API_KEY, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, }; // Reset index name to default setConfigIndexName("index"); }); afterEach(() => { - // Reset index name to default (prevents leaking into other test files under bun test) + // Reset index name/source to default (prevents leaking into other test files under bun test) setConfigIndexName("index"); + setConfigSource(undefined); for (const [key, val] of Object.entries(savedEnv)) { if (val === undefined) { delete process.env[key]; @@ -96,3 +106,39 @@ describe("getConfigDir via getConfigPath", () => { } }); }); + + +describe("getEmbeddingConfig defaults", () => { + test("uses local embeddings when no config or OpenAI signal is present", () => { + delete process.env.QMD_OPENAI; + delete process.env.QMD_OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + setConfigSource({ config: { collections: {} } }); + + expect(getEmbeddingConfig()).toEqual({ provider: "local" }); + }); + + test("defaults to OpenAI when the EdwinPAI/QMD installer enables QMD_OPENAI", () => { + process.env.QMD_OPENAI = "1"; + delete process.env.QMD_OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + setConfigSource({ config: { collections: {} } }); + + expect(getEmbeddingConfig()).toEqual({ provider: "openai" }); + }); + + test("defaults to OpenAI when an OpenAI embedding key is available", () => { + delete process.env.QMD_OPENAI; + process.env.OPENAI_API_KEY = "sk-test"; + setConfigSource({ config: { collections: {} } }); + + expect(getEmbeddingConfig()).toEqual({ provider: "openai" }); + }); + + test("explicit config still wins over environment defaults", () => { + process.env.QMD_OPENAI = "1"; + setConfigSource({ config: { collections: {}, embedding: { provider: "local" } } }); + + expect(getEmbeddingConfig()).toEqual({ provider: "local" }); + }); +}); From bed49f0e72849ed09ecad6d616b04fe8cd81efba Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Wed, 10 Jun 2026 17:31:22 -0600 Subject: [PATCH 13/17] Respect OpenAI embedding model in CLI embed --- src/cli/qmd.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 2070d684c..980678bd2 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -1904,6 +1904,10 @@ function ensureModelsConfiguredForCli(): { embed: string; generate: string; rera } export function resolveEmbedModelForCli(): string { + const embeddingConfig = getEmbeddingConfig(); + if (embeddingConfig.provider === "openai") { + return embeddingConfig.openai?.embedModel || process.env.QMD_OPENAI_EMBED_MODEL || "text-embedding-3-small"; + } return ensureModelsConfiguredForCli().embed; } From 879e09ca167cf5b567250c57c743b09f90ad7551 Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Thu, 11 Jun 2026 16:32:34 -0600 Subject: [PATCH 14/17] Strip cookie headers from OpenAI requests --- src/openai-llm.ts | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/openai-llm.ts b/src/openai-llm.ts index 822502b76..e566b5248 100644 --- a/src/openai-llm.ts +++ b/src/openai-llm.ts @@ -60,6 +60,42 @@ function truncateToTokenLimit(text: string, maxTokens: number): string { const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; const DEFAULT_EXPANSION_MODEL = 'gpt-4o-mini'; +/** + * OpenAI API calls must be stateless. Bun's fetch/runtime can retain or attach + * cookie-related headers after Cloudflare responses in long-running CLI flows, + * which can make subsequent OpenAI requests fail with HTTP 431 + * "Request headers are too large". The OpenAI API does not need cookies, so + * strip them defensively on every SDK request. + */ +const OPENAI_STRIPPED_REQUEST_HEADERS = new Set([ + 'cookie', + 'cookie2', + 'set-cookie', + 'set-cookie2', +]); + +function sanitizedOpenAIFetch(input: unknown, init?: unknown): Promise { + const requestInit = (init ?? {}) as RequestInit; + const nextInit: RequestInit = { ...requestInit }; + const headers = new Headers(requestInit.headers); + for (const name of OPENAI_STRIPPED_REQUEST_HEADERS) { + headers.delete(name); + } + nextInit.headers = headers; + return fetch(input as Parameters[0], nextInit); +} + +function createOpenAIClient(opts: { apiKey?: string; baseURL?: string }): OpenAI { + const clientOptions: Record = { + apiKey: opts.apiKey, + fetch: sanitizedOpenAIFetch, + }; + if (opts.baseURL) { + clientOptions.baseURL = opts.baseURL; + } + return new OpenAI(clientOptions as ConstructorParameters[0]); +} + // Retry configuration const MAX_RETRIES = 5; const BASE_DELAY_MS = 1000; @@ -143,19 +179,19 @@ export class OpenAIEmbedding implements LLM { const apiKey = config.apiKey || process.env.QMD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; const baseURL = config.baseURL || process.env.QMD_OPENAI_BASE_URL; - this.client = new OpenAI({ apiKey, baseURL }); + this.client = createOpenAIClient({ apiKey, baseURL }); const chatApiKey = config.chatApiKey || process.env.QMD_OPENAI_CHAT_API_KEY || apiKey; const chatBaseURL = config.chatBaseURL || process.env.QMD_OPENAI_CHAT_BASE_URL || baseURL; this.chatClient = (chatBaseURL !== baseURL || chatApiKey !== apiKey) - ? new OpenAI({ apiKey: chatApiKey, baseURL: chatBaseURL }) + ? createOpenAIClient({ apiKey: chatApiKey, baseURL: chatBaseURL }) : this.client; // Rerank client: falls back to chat client, then base client const rerankApiKey = config.rerankApiKey || process.env.QMD_OPENAI_RERANK_API_KEY || chatApiKey; const rerankBaseURL = config.rerankBaseURL || process.env.QMD_OPENAI_RERANK_BASE_URL || chatBaseURL; this.rerankClient = (rerankBaseURL !== chatBaseURL || rerankApiKey !== chatApiKey) - ? new OpenAI({ apiKey: rerankApiKey, baseURL: rerankBaseURL }) + ? createOpenAIClient({ apiKey: rerankApiKey, baseURL: rerankBaseURL }) : this.chatClient; this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; From bfc321b5b6a7d7d8a55aec6505788bee2f27e144 Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Thu, 11 Jun 2026 18:36:45 -0600 Subject: [PATCH 15/17] Reduce SQLite lock contention on search startup --- src/db.ts | 14 +++++++++++++- src/store.ts | 9 +++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/db.ts b/src/db.ts index b23a65ba2..7fda4bf12 100644 --- a/src/db.ts +++ b/src/db.ts @@ -65,7 +65,19 @@ if (isBun) { * Open a SQLite database. Works with both bun:sqlite and better-sqlite3. */ export function openDatabase(path: string): Database { - return new _Database(path) as Database; + const db = new _Database(path) as Database; + // QMD is frequently invoked as many short-lived CLI processes. Even search + // commands run startup/schema checks, so concurrent readers can briefly + // contend with WAL/schema locks. Wait instead of failing immediately. + const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT_MS; + const parsed = raw ? Number.parseInt(raw, 10) : 10000; + const timeoutMs = Number.isFinite(parsed) && parsed >= 0 ? parsed : 10000; + try { + db.exec(`PRAGMA busy_timeout = ${timeoutMs}`); + } catch { + // Some runtimes may not support the pragma; keep opening best-effort. + } + return db; } /** diff --git a/src/store.ts b/src/store.ts index 3810e35e8..5c8f12713 100644 --- a/src/store.ts +++ b/src/store.ts @@ -927,9 +927,8 @@ function initializeDatabase(db: Database): void { // Triggers keep FTS in sync for callers that write directly to documents. // Production indexing paths rebuild entries in TypeScript so CJK text can be // normalized before it reaches the unicode61 tokenizer. - db.exec(`DROP TRIGGER IF EXISTS documents_ai`); db.exec(` - CREATE TRIGGER documents_ai AFTER INSERT ON documents + CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents WHEN new.active = 1 BEGIN INSERT INTO documents_fts(rowid, filepath, title, body) @@ -942,16 +941,14 @@ function initializeDatabase(db: Database): void { END `); - db.exec(`DROP TRIGGER IF EXISTS documents_ad`); db.exec(` - CREATE TRIGGER documents_ad AFTER DELETE ON documents BEGIN + CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN DELETE FROM documents_fts WHERE rowid = old.id; END `); - db.exec(`DROP TRIGGER IF EXISTS documents_au`); db.exec(` - CREATE TRIGGER documents_au AFTER UPDATE ON documents + CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN -- Delete from FTS if no longer active DELETE FROM documents_fts WHERE rowid = old.id AND new.active = 0; From d0395fb46f44d9a49ad4f1e64abca23bbdd457d3 Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Fri, 12 Jun 2026 16:39:40 -0600 Subject: [PATCH 16/17] Open QMD searches in read-only mode --- src/cli/qmd.ts | 33 +++++++++++++++++++++++++++++--- src/db.ts | 10 +++++++--- src/store.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 980678bd2..e98373590 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -124,6 +124,7 @@ import { // ============================================================================= let store: ReturnType | null = null; +let readOnlyStore: ReturnType | null = null; let storeDbPathOverride: string | undefined; let currentIndexName = "index"; @@ -147,10 +148,32 @@ function getStore(): ReturnType { return store; } + +function getReadOnlyStore(): ReturnType { + if (!readOnlyStore) { + readOnlyStore = createStore(storeDbPathOverride, { readonly: true }); + try { + const activeModels = ensureModelsConfiguredForCli(); + setDefaultLlamaCpp(new LlamaCpp({ + embedModel: activeModels.embed, + generateModel: activeModels.generate, + rerankModel: activeModels.rerank, + })); + } catch { + // Query-only mode can run without model config for plain FTS searches. + } + } + return readOnlyStore; +} + function getDb(): Database { return getStore().db; } +function getReadOnlyDb(): Database { + return getReadOnlyStore().db; +} + /** Re-sync YAML config into SQLite after CLI mutations (add/remove/rename collection, context changes) */ function resyncConfig(): void { const s = getStore(); @@ -169,6 +192,10 @@ function closeDb(): void { store.close(); store = null; } + if (readOnlyStore) { + readOnlyStore.close(); + readOnlyStore = null; + } } function getDbPath(): string { @@ -2490,7 +2517,7 @@ function parseStructuredQuery(query: string): ParsedStructuredQuery | null { } function search(query: string, opts: OutputOptions): void { - const db = getDb(); + const db = getReadOnlyDb(); // Validate collection filter (supports multiple -c flags) // Use default collections if none specified @@ -2539,7 +2566,7 @@ function logExpansionTree(originalQuery: string, expanded: ExpandedQuery[]): voi } async function vectorSearch(query: string, opts: OutputOptions, _model: string = DEFAULT_EMBED_MODEL): Promise { - const store = getStore(); + const store = getReadOnlyStore(); // Validate collection filter (supports multiple -c flags) // Use default collections if none specified @@ -2598,7 +2625,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string = } async function querySearch(query: string, opts: OutputOptions, _embedModel: string = DEFAULT_EMBED_MODEL, _rerankModel: string = DEFAULT_RERANK_MODEL): Promise { - const store = getStore(); + const store = getReadOnlyStore(); // Validate collection filter (supports multiple -c flags) // Use default collections if none specified diff --git a/src/db.ts b/src/db.ts index 7fda4bf12..697484ec8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -16,7 +16,8 @@ export const isBun = "Bun" in globalThis; export type SQLiteValue = string | number | bigint | Buffer | Uint8Array | Float32Array | null; export type SQLiteParams = readonly SQLiteValue[]; -type DatabaseConstructor = new (path: string) => Database; +type DatabaseOpenOptions = { readonly?: boolean; create?: boolean; fileMustExist?: boolean }; +type DatabaseConstructor = new (path: string, options?: DatabaseOpenOptions) => Database; type LoadableSqliteDatabase = Pick; let _Database: DatabaseConstructor; @@ -64,8 +65,11 @@ if (isBun) { /** * Open a SQLite database. Works with both bun:sqlite and better-sqlite3. */ -export function openDatabase(path: string): Database { - const db = new _Database(path) as Database; +export function openDatabase(path: string, options: { readonly?: boolean } = {}): Database { + const constructorOptions = options.readonly + ? { readonly: true, create: false, fileMustExist: true } + : undefined; + const db = new _Database(path, constructorOptions) as Database; // QMD is frequently invoked as many short-lived CLI processes. Even search // commands run startup/schema checks, so concurrent readers can briefly // contend with WAL/schema locks. Wait instead of failing immediately. diff --git a/src/store.ts b/src/store.ts index 5c8f12713..08daa3f54 100644 --- a/src/store.ts +++ b/src/store.ts @@ -15,7 +15,7 @@ import { openDatabase, loadSqliteVec } from "./db.js"; import type { Database } from "./db.js"; import picomatch from "picomatch"; import { createHash } from "crypto"; -import { readFileSync, realpathSync, statSync, mkdirSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync, statSync, mkdirSync } from "node:fs"; // Note: node:path resolve is not imported — we export our own cross-platform resolve() import fastGlob from "fast-glob"; import { encoding_for_model } from "tiktoken"; @@ -57,6 +57,11 @@ export const DEFAULT_EMBED_MAX_BATCH_BYTES = 64 * 1024 * 1024; // 64MB const EMBED_FINGERPRINT_PROBE_QUERY = "__qmd_embedding_query_probe__"; const EMBED_FINGERPRINT_PROBE_TITLE = "__qmd_embedding_title_probe__"; const EMBED_FINGERPRINT_PROBE_DOC = "__qmd_embedding_document_probe__"; +const readonlyDatabases = new WeakSet(); + +function isReadOnlyDatabase(db: Database): boolean { + return readonlyDatabases.has(db); +} // Chunking: 900 tokens per chunk with 15% overlap // Increased from 800 to accommodate smart chunking finding natural break points @@ -1847,10 +1852,46 @@ export async function generateEmbeddings( * @param dbPath - Path to the SQLite database file * @returns Store instance with all methods bound to the database */ -export function createStore(dbPath?: string): Store { +export type CreateStoreOptions = { + /** + * Open the SQLite store for query-only access. This skips schema migrations, + * FTS trigger maintenance, WAL setup, and other write-on-open initialization + * so parallel search processes do not contend for writer locks. + */ + readonly?: boolean; +}; + +function initializeReadOnlyDatabase(db: Database): void { + try { + loadSqliteVec(db); + verifySqliteVecLoaded(db); + _sqliteVecAvailable = true; + _sqliteVecUnavailableReason = null; + } catch (err) { + _sqliteVecAvailable = false; + _sqliteVecUnavailableReason = getErrorMessage(err); + console.warn(_sqliteVecUnavailableReason); + } + try { + db.exec("PRAGMA foreign_keys = ON"); + } catch { + // Read-only query mode should never fail just because a connection-local + // pragma is unavailable in a runtime. + } +} + +export function createStore(dbPath?: string, options: CreateStoreOptions = {}): Store { const resolvedPath = dbPath || getDefaultDbPath(); - const db = openDatabase(resolvedPath); - initializeDatabase(db); + if (options.readonly && !existsSync(resolvedPath)) { + throw new Error(`QMD index database does not exist at ${resolvedPath}. Run 'qmd update' first.`); + } + const db = openDatabase(resolvedPath, { readonly: options.readonly }); + if (options.readonly) { + readonlyDatabases.add(db); + initializeReadOnlyDatabase(db); + } else { + initializeDatabase(db); + } const store: Store = { db, @@ -2252,6 +2293,7 @@ export function getCachedResult(db: Database, cacheKey: string): string | null { } export function setCachedResult(db: Database, cacheKey: string, result: string): void { + if (isReadOnlyDatabase(db)) return; const now = new Date().toISOString(); db.prepare(`INSERT OR REPLACE INTO llm_cache (hash, result, created_at) VALUES (?, ?, ?)`).run(cacheKey, result, now); if (Math.random() < 0.01) { @@ -2260,6 +2302,7 @@ export function setCachedResult(db: Database, cacheKey: string, result: string): } export function clearCache(db: Database): void { + if (isReadOnlyDatabase(db)) return; db.exec(`DELETE FROM llm_cache`); } From fd9e5dfb02291c1c45e7093fea94953d26b52e7d Mon Sep 17 00:00:00 2001 From: jonesj38 Date: Mon, 15 Jun 2026 12:04:09 -0600 Subject: [PATCH 17/17] Widen FTS candidates for collection search --- src/store.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/store.ts b/src/store.ts index 08daa3f54..5795843dc 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3548,7 +3548,11 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle // When filtering by collection, fetch extra candidates from the FTS index // since some will be filtered out. Without a collection filter we can // fetch exactly the requested limit. - const ftsLimit = collections ? limit * 10 : limit; + // Collection-scoped searches are filtered after the FTS candidate pass. + // In a large global index, limit*10 can miss all matches from a tiny target + // collection even when that collection has strong matches. Use a wider + // candidate window for filtered searches while keeping unfiltered search exact. + const ftsLimit = collections ? Math.max(limit * 1000, 5000) : limit; let sql = ` WITH fts_matches AS (