diff --git a/CHANGELOG.md b/CHANGELOG.md index e25880d..8c16728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 0.0.13 / 2026-10-05 + +### :tada: Enhancements +- `useLocalModel(id, engine)`: load any on-device runtime (WebLLM, the browser's built-in model, transformers.js, your own) with progress, switching and unloading; `useWebLLMModel` is it with `createWebLLMEngine` +- `useWebLLMModel({ contextWindowTokens })` loads a local model with a larger window than WebLLM's 4096 (a different window is a different load); `contextWindow` reports the loaded window +- Demo — windows: every local model is loaded with its own window and the agent fits its runs into it — no more "Prompt tokens exceed context window size"; the Window setting is "auto" (the model's) by default +- Demo — models: current Gemini (3.8 Flash, 3.5 Flash-Lite, 3.1 Pro), Claude (Haiku 4.5, Sonnet 5.5, Opus 5.5, Fable 5.1) and GPT-6 (Luna, Sol, Astra); new providers Kimi (K2.6, K3), Groq and Cerebras (fast), Mistral, OpenRouter (any model id); local Gemma 3 1B, Llama 3.2 1B, Ministral 3 3B, Phi-4 mini, Phi-3.5 Vision (WebLLM), Gemini Nano (Chrome's built-in model) and SmolVLM / Gemma 4 E2B (transformers.js); Llama 2 13B removed. Every provider and runtime loads on first use. +- Demo — speed: "Fast answers" (no replanner and no synthesizer: 2 model calls per turn) +- Updated dependencies: @dudko.dev/agent-web 0.0.22 (runs fitted to the model's window, local vision models, provider capabilities) + # 0.0.12 / 2026-10-03 ### :tada: Enhancements diff --git a/CLAUDE.md b/CLAUDE.md index c8e8e24..d20e193 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,8 @@ The repo also contains a **Vite demo** (`demo/`) that is auto-deployed to - `src/state.ts` — the pure `agentStateReducer` + `createInitialAgentState`. - `src/types.ts` — `AgentUiState`, `ChatMessage`, `StepView`, `ToolCallView`, … - `src/hooks/` — `use-agent` (the hook), `use-credentials` (vault), - `use-webllm-model`, `use-mcp` (remote MCP + OAuth round-trip), + `use-local-model` (any on-device runtime as an engine) + `use-webllm-model` + (its WebLLM engine), `use-mcp` (remote MCP + OAuth round-trip), `use-mcp-servers` (several servers), `use-chat-history`, `use-virtual-files`, `use-speech-to-text`. - `src/chat-history.ts` — `ChatHistoryStore` (raw IndexedDB, memory fallback). diff --git a/README.md b/README.md index 5e304b1..d837907 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ app with a single hook and (optionally) a set of pre-styled components: `useVirtualFiles`, `useSpeechToText`, `useMcpServers`. - 🔑 **`useCredentials`** — store BYOK API keys **encrypted at rest** (WebCrypto + IndexedDB). -- 🖥️ **`useWebLLMModel`** — load a local WebGPU model with download progress. +- 🖥️ **`useWebLLMModel`** / **`useLocalModel`** — load an on-device model (WebLLM, + the browser's built-in model, transformers.js, or your own runtime) with + download progress, switching and unloading. - 🎛️ **Headless-first** — the event→UI logic is a pure, exported reducer (`agentStateReducer`); the components are optional sugar on top. @@ -229,6 +231,48 @@ function LocalAgent() { > yet. The previous model stays in memory (`loadedModelId`; switching back is > instant) until the new one is loaded — that frees it first, so two models > never share the GPU — or until `unload()`. +> +> **Context window.** WebLLM loads its models with a 4096-token window, far +> below what Qwen3 or Llama 3.x were trained on. Pass `contextWindowTokens` to +> load with more (it costs KV-cache VRAM, not a new download); your `create` +> factory receives it and applies it with the core's `withWebLLMContextWindow` +> (see the demo's `providers.ts`). `local.contextWindow` is the window the model +> was loaded with, and the agent fits its runs into it on its own — compaction, +> tool lists and tool results are sized from it. +> +> **Vision.** `Phi-3.5-vision-instruct-q4f16_1-MLC` takes images (paste, drop +> or attach them) — a multimodal model that never sends them anywhere. + +### Any on-device runtime — `useLocalModel` + +`useWebLLMModel` is `useLocalModel` with the WebLLM engine. Describe another +runtime as an engine — `create`, and optionally `warmUp` (download now, with +progress), `unload`, `supported`, `contextWindowOf` — and the hook gives the +same `load` / `unload` / progress / switching: + +```tsx +import { useLocalModel, createWebLLMEngine, type LocalModelEngine } from '@dudko.dev/agent-web-react' + +const builtIn: LocalModelEngine = { + create: async () => (await import('@browser-ai/core')).browserAI('text', { + expectedInputs: [{ type: 'text' }, { type: 'image' }], + }), + warmUp: async (m, { onProgress }) => { + await m.createSessionWithProgress((p) => onProgress({ progress: p, text: 'Downloading' })) + }, + supported: () => 'LanguageModel' in globalThis, // Chrome's Prompt API + contextWindowOf: (m) => m.getContextWindow(), +} +const webllm = createWebLLMEngine({ create: createLocalModel }) + +// One hook, any runtime: switching frees the previous model with its own engine. +const local = useLocalModel(option.model, option.builtIn ? builtIn : webllm, { + contextWindowTokens: option.contextWindow, +}) +``` + +The demo runs WebLLM, Chrome's Gemini Nano and transformers.js this way — see +[`demo/src/local-engines.ts`](demo/src/local-engines.ts). ## Models in a bundler (Vite, Next, CRA) diff --git a/demo/.npmrc b/demo/.npmrc new file mode 100644 index 0000000..b9cd7cb --- /dev/null +++ b/demo/.npmrc @@ -0,0 +1,4 @@ +# @huggingface/transformers depends on onnxruntime-node (a native Node runtime the +# browser never loads) whose install script downloads binaries; nothing the demo +# uses needs an install script, so skip them all. +ignore-scripts=true diff --git a/demo/README.md b/demo/README.md index 41d586b..7aed136 100644 --- a/demo/README.md +++ b/demo/README.md @@ -2,7 +2,17 @@ A Vite + React app showcasing [`@dudko.dev/agent-web-react`](../): an in-browser LLM agent driving tools. Pick a cloud model (bring your own key, stored -encrypted) or load a local WebGPU model — everything runs in the browser. +encrypted) or an on-device one — everything runs in the browser. + +**Models** (October 2026): Gemini (3.8 Flash, 3.5 Flash-Lite, 3.1 Pro), Claude +(Haiku 4.5, Sonnet 5.5, Opus 5.5, Fable 5.1), GPT-6 (Luna, Sol, Astra), Kimi +(K2.6, K3), Groq and Cerebras for speed (GPT-OSS, Qwen3.8 with images), Mistral +(Small 4, Medium 3.5) and OpenRouter (type any model id). Every one of them +answers a page directly with your key (CORS). On the device: WebLLM (Gemma 3 1B, +Llama 3.2 1B/3B, Qwen3.5 0.8B–9B, Ministral 3 3B, Phi-4 mini, Llama 3.1 8B and +Phi-3.5 Vision for images), Chrome's built-in Gemini Nano (no download), and +transformers.js (SmolVLM 256M and Gemma 4 E2B, images). Each provider and +runtime is fetched the first time you pick one of its models. Three tabs, sharing one "Agent settings" panel: @@ -44,10 +54,19 @@ and in total, thoughts, subagents and consent prompts; the composer has attachments, "/" commands, a run timer, the running-agents count, the model + thinking chip, the consent chip and a speech-to-text mic. +For quick answers: a fast provider (Groq, Cerebras, Flash-Lite), a small local +model with Thinking "none", and **Fast answers** in the agent settings (no +replanner, no separate final answer: 2 model calls per turn instead of 3+). + Local models: pick one and press "Download & load". Switching to another one shows it as not loaded (the loaded one stays in memory, so switching back is instant); loading it frees the previous model's GPU memory first, and -"Unload" frees it on demand. +"Unload" frees it on demand. Each is loaded with its own context window — +WebLLM's default is 4096 tokens; Qwen3.5 0.8B/2B get 32k, 4B/9B 16k, Llama 3.2 +1B 16k, the rest 8k (each note says the VRAM it takes) — and the agent +fits its runs into it: compaction, tool lists and tool results are sized from +the window ("Window: auto"). **Phi-3.5 Vision** is a local multimodal model: +paste or drop an image and ask about it. **Live:** https://dudko-dev.github.io/agent-web-react/ diff --git a/demo/package-lock.json b/demo/package-lock.json index e2341b8..2374617 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -9,13 +9,21 @@ "version": "0.0.0", "dependencies": { "@ai-sdk/anthropic": "^4.0.71", + "@ai-sdk/cerebras": "^3.0.63", "@ai-sdk/google": "^4.0.87", + "@ai-sdk/groq": "^4.0.55", + "@ai-sdk/mistral": "^4.0.57", + "@ai-sdk/moonshotai": "^3.0.63", "@ai-sdk/openai": "^4.0.83", + "@browser-ai/core": "^3.0.4", + "@browser-ai/transformers-js": "^3.0.4", "@browser-ai/web-llm": "^3.0.4", - "@dudko.dev/agent-web": "^0.0.21", + "@dudko.dev/agent-web": "^0.0.22", "@dudko.dev/pdf-to-md-core": "^0.3.5", + "@huggingface/transformers": "^4.3.0", "@mlc-ai/web-llm": "^0.2.85", "@modelcontextprotocol/sdk": "^1.32.0", + "@openrouter/ai-sdk-provider": "^3.1.0", "ai": "^7.0.127", "chess.js": "^1.4.0", "react": "^19.3.0", @@ -46,6 +54,54 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/cerebras": { + "version": "3.0.63", + "resolved": "https://registry.npmjs.org/@ai-sdk/cerebras/-/cerebras-3.0.63.tgz", + "integrity": "sha512-BlYs9MrBVgfSV22TDSdvCQvmcoIFUUSIqmXiW7Gz4EmFqT3Y8HZCRRlFio9QEQENmkfcZrtk8z3MgOaIHXXR3g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/openai-compatible": "3.0.63", + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/cerebras/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/cerebras/node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/gateway": { "version": "4.0.103", "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.103.tgz", @@ -79,6 +135,147 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/groq": { + "version": "4.0.55", + "resolved": "https://registry.npmjs.org/@ai-sdk/groq/-/groq-4.0.55.tgz", + "integrity": "sha512-VoMqzkPz7KbD9KugYW71j0P6u4wNsmufrwuw0C7A5SUkqKzSiu/JTEw/xTFJwOiX5sWPymCh9L8G2BdrMAtbYA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/groq/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/groq/node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/mistral": { + "version": "4.0.57", + "resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-4.0.57.tgz", + "integrity": "sha512-ltdGnlFSrUbic4z0DQ3pczbDzqxmqO+AVdqP5NnUmPKy7jZRziO/rEAiaM8nHoePyLIeteBAOVu87UnYVhK/FQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/mistral/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/mistral/node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/moonshotai": { + "version": "3.0.63", + "resolved": "https://registry.npmjs.org/@ai-sdk/moonshotai/-/moonshotai-3.0.63.tgz", + "integrity": "sha512-XOPk9x2/+WPAN/caK/bMf8EAgZxY/VJrLCTV5C3jNxbBEYSMHWTCas+Z5ThO5v2sl5quAjRJRlxe2m3y+jERtg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/moonshotai/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/moonshotai/node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/openai": { "version": "4.0.83", "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-4.0.83.tgz", @@ -95,6 +292,53 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/openai-compatible": { + "version": "3.0.63", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.63.tgz", + "integrity": "sha512-Uv/peV+aamk/xKK4jM2hpoleEN+T6vbu/mxkz04ZRTpjyMkxflO3GdcjMpuM28TDPctYEkibTSmqpwYs4/Lq+g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@ai-sdk/provider-utils": "5.0.54" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.22.tgz", + "integrity": "sha512-1Jtmn36VNMOBo9CMUQrPqsR744qtqRloEiXpWCYS8ffPlhS2CrSNmoho5lPjp3R7EnkzBsGI6p51TA7fQJQZdA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": { + "version": "5.0.54", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.54.tgz", + "integrity": "sha512-amqDxFnw9+dpVrDACRDGmExUUNOH9C1D2bhg/DqWJnamEOieDTXBBXLHujSby/0MqHdAFksnpE8Pzfqx6ABMBw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.22", + "@standard-schema/spec": "^1.1.0", + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/provider": { "version": "4.0.21", "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.21.tgz", @@ -126,6 +370,28 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@browser-ai/core": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@browser-ai/core/-/core-3.0.4.tgz", + "integrity": "sha512-9A2a3ddfjeNZeLUTZW5CWeER+wn+JdcQCGRgk4Nv/wLIw5/2jxs+TxStcFMRJ1nB3Uiql/zcQdIyQRQvHxEV5Q==", + "license": "Apache-2.0", + "dependencies": { + "@mediapipe/tasks-text": "^0.10.22-rc.20250304" + }, + "peerDependencies": { + "ai": "^7.0.0" + } + }, + "node_modules/@browser-ai/transformers-js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@browser-ai/transformers-js/-/transformers-js-3.0.4.tgz", + "integrity": "sha512-EOAaoxEA8immgkXvlURDa5B48EPYHZKKbh/AnBPv7WUd3ibcbxSxIco/UbwUDh4dB22MyHkzUk2Xv9WeefdGNQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@huggingface/transformers": "^3.7.0 || ^4.0.0-next || ^4.0.0", + "ai": "^7.0.0" + } + }, "node_modules/@browser-ai/web-llm": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@browser-ai/web-llm/-/web-llm-3.0.4.tgz", @@ -137,9 +403,9 @@ } }, "node_modules/@dudko.dev/agent-web": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.21.tgz", - "integrity": "sha512-UDcJia0LXbZD3LIFwNICvdc/zdFMRN87YNZly/Ir4NAtUstK2xF9D7y/gwS9pd0jF3AyyB+EsZDzVO0BPF1j0w==", + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.22.tgz", + "integrity": "sha512-Y8+oOnhZpHiAphwZj9CU8pQYo8QOshB/ULQp9Cq9a3OdHhOdvdJf/IDXrURe6AEL3sgCpfdJ1dhiprHjT/h46g==", "funding": [ { "type": "individual", @@ -179,57 +445,601 @@ "@mlc-ai/web-llm": ">=0.2.70", "@modelcontextprotocol/sdk": "^1.30.0" }, - "peerDependenciesMeta": { - "@ai-sdk/anthropic": { - "optional": true - }, - "@ai-sdk/deepseek": { - "optional": true - }, - "@ai-sdk/google": { - "optional": true - }, - "@ai-sdk/openai": { - "optional": true - }, - "@ai-sdk/openai-compatible": { - "optional": true - }, - "@ai-sdk/xai": { - "optional": true - }, - "@browser-ai/core": { - "optional": true - }, - "@browser-ai/web-llm": { - "optional": true - }, - "@mlc-ai/web-llm": { - "optional": true - }, - "@modelcontextprotocol/sdk": { - "optional": true - } + "peerDependenciesMeta": { + "@ai-sdk/anthropic": { + "optional": true + }, + "@ai-sdk/deepseek": { + "optional": true + }, + "@ai-sdk/google": { + "optional": true + }, + "@ai-sdk/openai": { + "optional": true + }, + "@ai-sdk/openai-compatible": { + "optional": true + }, + "@ai-sdk/xai": { + "optional": true + }, + "@browser-ai/core": { + "optional": true + }, + "@browser-ai/web-llm": { + "optional": true + }, + "@mlc-ai/web-llm": { + "optional": true + }, + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@dudko.dev/pdf-to-md-core": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@dudko.dev/pdf-to-md-core/-/pdf-to-md-core-0.3.5.tgz", + "integrity": "sha512-m5NYUU+jznFOuMx8BUkqUWwlg7IV7VY3RP/BLp/xJpGVgL+V8khTc0ndES6PoHVpI3leskvUNC8FqiA8Lv/YLA==", + "license": "PolyForm-Noncommercial-1.0.0" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.2.0.tgz", + "integrity": "sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.0.tgz", + "integrity": "sha512-fL1A/WUZwouPrOlYxU5dzIwD2T5J781JiB2jDR8bFe5DwCj0Gfudq+NEXCMno49kQgajHA7xQkrRLJlqG1veEA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.10", + "@huggingface/tokenizers": "^0.2.0", + "onnxruntime-node": "1.30.0", + "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0", + "sharp": "^0.35.4" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@dudko.dev/pdf-to-md-core": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@dudko.dev/pdf-to-md-core/-/pdf-to-md-core-0.3.5.tgz", - "integrity": "sha512-m5NYUU+jznFOuMx8BUkqUWwlg7IV7VY3RP/BLp/xJpGVgL+V8khTc0ndES6PoHVpI3leskvUNC8FqiA8Lv/YLA==", - "license": "PolyForm-Noncommercial-1.0.0" + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", - "license": "MIT", + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=20" + "node": ">=20.9.0" }, - "peerDependencies": { - "hono": "^4" + "funding": { + "url": "https://opencollective.com/libvips" } }, + "node_modules/@mediapipe/tasks-text": { + "version": "0.10.35", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-text/-/tasks-text-0.10.35.tgz", + "integrity": "sha512-BlRWXZAHakqtAos8hNgSU0jy4mh60R9ewzwckS3q2nV7Q/7L65otCtoDrgslRBhlfbElfcHbHQXKRahlMRCf4Q==", + "license": "Apache-2.0" + }, "node_modules/@mlc-ai/web-llm": { "version": "0.2.85", "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.85.tgz", @@ -279,6 +1089,19 @@ } } }, + "node_modules/@openrouter/ai-sdk-provider": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-3.1.0.tgz", + "integrity": "sha512-BQy1TA9fKrh47s9ivsO6FY5QimN7GUF0i8Qon3pkxQclEgShso0dClpcWn8OIyPLEZZPFaFKgI8TW4S/O0p0Dw==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "ai": "^7.0.0", + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@oxc-project/types": { "version": "0.152.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", @@ -289,6 +1112,63 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", @@ -557,6 +1437,15 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -635,6 +1524,15 @@ "node": ">= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, "node_modules/ai": { "version": "7.0.127", "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.127.tgz", @@ -861,6 +1759,40 @@ } } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -874,7 +1806,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -945,6 +1876,18 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1098,6 +2041,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1177,6 +2126,37 @@ "node": ">= 0.4" } }, + "node_modules/global-agent": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", + "license": "BSD-3-Clause", + "dependencies": { + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1189,6 +2169,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1601,6 +2599,27 @@ "url": "https://tidelift.com/funding/github/npm/loglevel" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1715,6 +2734,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1736,6 +2764,49 @@ "wrappy": "1" } }, + "node_modules/onnxruntime-common": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.30.0.tgz", + "integrity": "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.30.0.tgz", + "integrity": "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.6.0", + "global-agent": "^4.1.3", + "onnxruntime-common": "1.30.0" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.31.0-dev.20260914-8d85527a0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.31.0-dev.20260914-8d85527a0.tgz", + "integrity": "sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.31.0-dev.20260911-2a43ec07e", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.31.0-dev.20260911-2a43ec07e", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.0-dev.20260911-2a43ec07e.tgz", + "integrity": "sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1793,6 +2864,12 @@ "node": ">=16.20.0" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", @@ -1822,6 +2899,29 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1971,6 +3071,18 @@ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -1997,6 +3109,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -2022,6 +3149,55 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2160,6 +3336,25 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -2214,6 +3409,12 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/demo/package.json b/demo/package.json index 2cea388..b22b955 100644 --- a/demo/package.json +++ b/demo/package.json @@ -11,13 +11,21 @@ }, "dependencies": { "@ai-sdk/anthropic": "^4.0.71", + "@ai-sdk/cerebras": "^3.0.63", "@ai-sdk/google": "^4.0.87", + "@ai-sdk/groq": "^4.0.55", + "@ai-sdk/mistral": "^4.0.57", + "@ai-sdk/moonshotai": "^3.0.63", "@ai-sdk/openai": "^4.0.83", + "@browser-ai/core": "^3.0.4", + "@browser-ai/transformers-js": "^3.0.4", "@browser-ai/web-llm": "^3.0.4", - "@dudko.dev/agent-web": "^0.0.21", + "@dudko.dev/agent-web": "^0.0.22", "@dudko.dev/pdf-to-md-core": "^0.3.5", + "@huggingface/transformers": "^4.3.0", "@mlc-ai/web-llm": "^0.2.85", "@modelcontextprotocol/sdk": "^1.32.0", + "@openrouter/ai-sdk-provider": "^3.1.0", "ai": "^7.0.127", "chess.js": "^1.4.0", "react": "^19.3.0", diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 4c9d985..ac6003c 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { Square } from 'chess.js' -import type { BrowserAgentConfig, ModelInput } from '@dudko.dev/agent-web' +import type { BrowserAgentConfig, ModelInput, ProviderType } from '@dudko.dev/agent-web' import { AgentChat, ChatHistoryStore, @@ -11,7 +11,7 @@ import { useAgent, useCredentials, useMcpServers, - useWebLLMModel, + useLocalModel, type AgentChatProps, type AgentToolSet, } from '@dudko.dev/agent-web-react' @@ -26,7 +26,8 @@ import { Settings } from './components/Settings' import { buildMockCatalog } from './mockCatalog' import { isLocal, MODELS } from './models' import { useNotesBoard } from './notes' -import { buildCloudModel, createLocalModel } from './providers' +import { buildCloudModel } from './cloud' +import { engineFor } from './local-engines' import { RU_LABELS } from './i18n' import { Markdown } from './markdown' import { @@ -75,29 +76,54 @@ const initialView = (): View => { return VIEWS.includes(hash) ? hash : 'notes' } +const CUSTOM_MODEL_KEY = 'agent-web-demo:openrouter-model' +const readCustomModelId = (fallback: string): string => { + try { + return localStorage.getItem(CUSTOM_MODEL_KEY) ?? fallback + } catch { + return fallback + } +} +const writeCustomModelId = (id: string) => { + try { + localStorage.setItem(CUSTOM_MODEL_KEY, id) + } catch { + /* private mode */ + } +} + export const App = () => { const [modelId, setModelId] = useState('google-flash') const model = MODELS.find((m) => m.id === modelId) ?? MODELS[0] const local = isLocal(model) + // OpenRouter's "any model": the id the user typed (remembered). + const [customModelId, setCustomModelId] = useState(() => + readCustomModelId(MODELS.find((m) => m.customModel)?.model ?? ''), + ) + const cloudModelId = model.customModel ? customModelId.trim() || model.model : model.model const credentials = useCredentials() - // Inject a statically-imported WebLLM factory so the weights actually bundle - // (the core's dynamic import gets stubbed to an empty module by Vite). - const webllm = useWebLLMModel(model.model, { create: createLocalModel }) + // One hook for every on-device runtime: WebLLM, the browser's built-in model, + // transformers.js (local-engines.ts). WebLLM models load with their own + // window, not WebLLM's 4096 default. + const localModel = useLocalModel(model.model, engineFor(model), { + contextWindowTokens: model.runtime === 'web-llm' ? model.contextWindow : undefined, + }) // Cloud models are built in-app from the vault-stored key and passed to the - // agent directly (see providers.ts). Rebuilds when the key or model changes. + // agent directly (see cloud.ts). Rebuilds when the key or model changes. const [cloudModel, setCloudModel] = useState(undefined) useEffect(() => { - if (local) { + if (local || !model.provider) { setCloudModel(undefined) return } let active = true credentials.store .getApiKey(model.credentialRef!) - .then((key) => { - if (active) setCloudModel(key ? buildCloudModel(model, key) : undefined) + .then(async (key) => { + const built = key ? await buildCloudModel(model.provider!, cloudModelId, key) : undefined + if (active) setCloudModel(built) }) .catch(() => { if (active) setCloudModel(undefined) @@ -105,12 +131,16 @@ export const App = () => { return () => { active = false } - }, [local, model, credentials.store, credentials.version]) - const resolvedModel = local ? webllm.model : cloudModel + }, [local, model, cloudModelId, credentials.store, credentials.version]) + const resolvedModel = local ? localModel.model : cloudModel // ── Agent settings shared by every tab ───────────────────────────────────── const { settings, update, reset: resetSettings } = useDemoSettings() - const { config: settingsConfig, rebuildKey } = useSettingsConfig(settings) + // The window runs must fit: a loaded local model reports its own. + const modelWindow = local + ? (localModel.contextWindow ?? model.contextWindow) + : model.contextWindow + const { config: settingsConfig, rebuildKey } = useSettingsConfig(settings, modelWindow) // The model's memory per conversation. With saved chats it lives in // IndexedDB too (keyed by the chat id), so a reopened chat continues with its // context; the chess game is not saved, so its memory isn't either. @@ -152,6 +182,8 @@ export const App = () => { skills: skillsByTab[tab], // The consent mode is applied live by useAgent — no rebuild on a switch. toolApproval: { mode: settings.approvalMode }, + // Images, where the core can't tell from the model (built-in, Gemma 4). + ...(model.vision ? { vision: true } : {}), // Stream the agent's internal phases to the console — handy for poking. logLevel: 'debug', }) @@ -210,8 +242,9 @@ export const App = () => { new Worker(new URL('./chess/analyst.worker.ts', import.meta.url), { type: 'module' }), workerConfig: { model: { - providerType: model.providerType, - model: model.model, + // The worker builds it with the same builder (cloud.ts). + providerType: model.provider as ProviderType, + model: cloudModelId, credentialRef: model.credentialRef, }, systemPrompt: ANALYST_PROMPT, @@ -241,7 +274,7 @@ export const App = () => { readOnly: true, }), } - }, [settings.analysts, resolvedModel, local, model, credentials.store, game.tools]) + }, [settings.analysts, resolvedModel, local, model, cloudModelId, credentials.store, game.tools]) const chessAgent = useAgent( { ...base('chess'), @@ -417,7 +450,12 @@ export const App = () => { selected={model} onSelect={setModelId} credentials={credentials} - webllm={webllm} + local={localModel} + customModelId={customModelId} + onCustomModelId={(id) => { + setCustomModelId(id) + writeCustomModelId(id) + }} onKeyChange={() => { notesAgent.reload() mcpAgent.reload() @@ -428,6 +466,7 @@ export const App = () => { diff --git a/demo/src/chess/analyst.worker.ts b/demo/src/chess/analyst.worker.ts index 0e29d1b..a74d39a 100644 --- a/demo/src/chess/analyst.worker.ts +++ b/demo/src/chess/analyst.worker.ts @@ -1,8 +1,6 @@ /// -import { createAnthropic } from '@ai-sdk/anthropic' -import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { createOpenAI } from '@ai-sdk/openai' import { serveSubagentWorker, type ProviderModelSpec } from '@dudko.dev/agent-web' +import { buildCloudModel, type CloudProvider } from '../cloud' import { engineTools } from './analyst-tools' /** @@ -12,24 +10,10 @@ import { engineTools } from './analyst-tools' * would otherwise stall the page, and answers with a verdict. * * The model is built here from the spec the parent posted (its key arrives with - * the task and is never stored). Provider factories are imported statically — - * a bundler cannot resolve the core's dynamic provider imports in a worker. + * the task and is never stored), with the same builder as the page — the spec's + * `providerType` carries the demo's cloud provider name. */ -const resolveModel = (spec: ProviderModelSpec) => { - const apiKey = spec.apiKey - switch (spec.providerType) { - case 'google': - return createGoogleGenerativeAI({ apiKey })(spec.model) - case 'anthropic': - return createAnthropic({ - apiKey, - headers: { 'anthropic-dangerous-direct-browser-access': 'true' }, - })(spec.model) - case 'openai': - return createOpenAI({ apiKey })(spec.model) - default: - throw new Error(`the analyst worker has no factory for "${spec.providerType}"`) - } -} +const resolveModel = (spec: ProviderModelSpec) => + buildCloudModel(spec.providerType as CloudProvider, spec.model, spec.apiKey ?? '') serveSubagentWorker({ resolveModel, tools: engineTools(3) }) diff --git a/demo/src/cloud.ts b/demo/src/cloud.ts new file mode 100644 index 0000000..d2b11ce --- /dev/null +++ b/demo/src/cloud.ts @@ -0,0 +1,66 @@ +import type { LanguageModel } from 'ai' + +/** The cloud providers the demo calls straight from the browser (BYOK). */ +export type CloudProvider = + 'google' | 'anthropic' | 'openai' | 'moonshotai' | 'groq' | 'cerebras' | 'mistral' | 'openrouter' + +/** + * Build a cloud provider's `LanguageModel` **in the app** — on the page and in + * the analyst worker alike — and hand it to the agent as a direct model. + * + * The core (`@dudko.dev/agent-web`) can also resolve a `{ providerType, model, + * credentialRef }` spec by dynamically importing the provider package — but + * that `import(pkg)` is `@vite-ignore`d and uses a bare specifier, which a + * browser bundle can't resolve at runtime. A literal `import('@ai-sdk/…')` + * here is one Vite can see: each provider becomes its own chunk, fetched the + * first time a model of it is used. + * + * Every one of these answers a browser directly (CORS, checked October 2026); + * Anthropic only with its opt-in header. + */ +export const buildCloudModel = async ( + provider: CloudProvider, + model: string, + apiKey: string, +): Promise => { + switch (provider) { + case 'google': { + const { createGoogleGenerativeAI } = await import('@ai-sdk/google') + return createGoogleGenerativeAI({ apiKey })(model) + } + case 'anthropic': { + const { createAnthropic } = await import('@ai-sdk/anthropic') + return createAnthropic({ + apiKey, + // Anthropic's API refuses direct browser calls without this opt-in header. + headers: { 'anthropic-dangerous-direct-browser-access': 'true' }, + })(model) + } + case 'openai': { + const { createOpenAI } = await import('@ai-sdk/openai') + return createOpenAI({ apiKey })(model) + } + case 'moonshotai': { + const { createMoonshotAI } = await import('@ai-sdk/moonshotai') + return createMoonshotAI({ apiKey })(model) + } + case 'groq': { + const { createGroq } = await import('@ai-sdk/groq') + return createGroq({ apiKey })(model) + } + case 'cerebras': { + const { createCerebras } = await import('@ai-sdk/cerebras') + return createCerebras({ apiKey })(model) + } + case 'mistral': { + const { createMistral } = await import('@ai-sdk/mistral') + return createMistral({ apiKey })(model) + } + case 'openrouter': { + const { createOpenRouter } = await import('@openrouter/ai-sdk-provider') + return createOpenRouter({ apiKey }).chat(model) as LanguageModel + } + default: + throw new Error(`unsupported cloud provider: ${provider as string}`) + } +} diff --git a/demo/src/components/AgentSettingsPanel.tsx b/demo/src/components/AgentSettingsPanel.tsx index 44fc11b..09cd993 100644 --- a/demo/src/components/AgentSettingsPanel.tsx +++ b/demo/src/components/AgentSettingsPanel.tsx @@ -6,6 +6,7 @@ import { } from '@dudko.dev/agent-web-react' import { allSkills, + DEFAULT_WINDOW, skillsOfView, type DemoSettings, type ThinkingChoice, @@ -17,14 +18,18 @@ export interface AgentSettingsPanelProps { settings: DemoSettings /** The open tab: its skills are listed, and the notes speak about its tools. */ view: View + /** The model's own window, when known ("auto" uses it). */ + modelWindow?: number update: (patch: Partial) => void onReset: () => void } const TOKEN_BUDGETS = [0, 20_000, 50_000, 100_000, 250_000] const TOOL_CALL_CAPS = [0, 5, 10, 25, 50] -const WINDOWS = [8_000, 32_000, 128_000, 1_000_000] -const k = (n: number) => (n >= 1_000_000 ? `${n / 1_000_000}M` : `${n / 1000}k`) +const WINDOWS = [4_096, 8_000, 32_000, 128_000, 1_000_000] +// 4096 → 4k, 32768 → 32k, 128000 → 128k. +const k = (n: number) => + n >= 1_000_000 ? `${n / 1_000_000}M` : `${n % 1024 === 0 ? n / 1024 : Math.round(n / 1000)}k` /** * Everything the agent loop exposes, as settings: tool consent (the autopilot @@ -34,6 +39,7 @@ const k = (n: number) => (n >= 1_000_000 ? `${n / 1_000_000}M` : `${n / 1000}k`) export const AgentSettingsPanel = ({ settings, view, + modelWindow, update, onReset, }: AgentSettingsPanelProps) => { @@ -183,9 +189,11 @@ export const AgentSettingsPanel = ({ value={settings.contextWindowTokens} onChange={(e) => update({ contextWindowTokens: Number(e.target.value) })} > + {WINDOWS.map((n) => ( ))} @@ -209,6 +217,14 @@ export const AgentSettingsPanel = ({ />{' '} Auto-compact the conversation +
diff --git a/demo/src/components/Settings.tsx b/demo/src/components/Settings.tsx index 0db8b7a..cd12147 100644 --- a/demo/src/components/Settings.tsx +++ b/demo/src/components/Settings.tsx @@ -3,7 +3,7 @@ import { ApiKeyForm, ModelLoadBar, type UseCredentialsReturn, - type UseWebLLMModelReturn, + type UseLocalModelReturn, } from '@dudko.dev/agent-web-react' import { isLocal, type ModelOption } from '../models' @@ -12,7 +12,11 @@ export interface SettingsProps { selected: ModelOption onSelect: (id: string) => void credentials: UseCredentialsReturn - webllm: UseWebLLMModelReturn + /** The on-device model loader (WebLLM, built-in, transformers.js). */ + local: UseLocalModelReturn + /** OpenRouter's "any model": the id typed by the user. */ + customModelId: string + onCustomModelId: (id: string) => void /** Rebuild the agent after a key changes so it picks up the new credential. */ onKeyChange: () => void /** A usable model is in hand (key stored / local model loaded): start collapsed. */ @@ -20,7 +24,7 @@ export interface SettingsProps { } /** - * Provider picker + BYOK key entry (cloud) or WebGPU model loader (local). + * Provider picker + BYOK key entry (cloud) or on-device model loader (local). * Open while there is nothing to talk to; once a key is stored (or a local * model loaded) it folds into one line, so the chat gets the height. */ @@ -29,7 +33,9 @@ export const Settings = ({ selected, onSelect, credentials, - webllm, + local: loader, + customModelId, + onCustomModelId, onKeyChange, ready, }: SettingsProps) => { @@ -40,8 +46,8 @@ export const Settings = ({ const open = userOpen ?? !ready // The local model in GPU memory, when it isn't the selected one. const held = - webllm.loadedModelId && webllm.loadedModelId !== selected.model - ? (models.find((m) => m.model === webllm.loadedModelId)?.label ?? webllm.loadedModelId) + loader.loadedModelId && loader.loadedModelId !== selected.model + ? (models.find((m) => m.model === loader.loadedModelId)?.label ?? loader.loadedModelId) : undefined const status = ready ? (local ? 'loaded' : 'key stored ✓') : local ? 'not loaded' : 'add a key' return ( @@ -83,18 +89,26 @@ export const Settings = ({ {local ? (
- {!webllm.supported && ( + {!loader.supported && (

- WebGPU isn’t available in this browser. Try Chrome or Edge on desktop. + {selected.runtime === 'built-in' + ? 'This browser has no built-in model — it’s Chrome’s Prompt API (Chrome 148+ on desktop).' + : 'WebGPU isn’t available in this browser. Try Chrome or Edge on desktop.'}

)} - {webllm.error &&

{webllm.error}

} - {webllm.loading ? ( - - ) : webllm.ready ? ( + {loader.error &&

{loader.error}

} + {loader.loading ? ( + + ) : loader.ready ? (
-

Model loaded — chat away, fully offline.

-
@@ -102,27 +116,43 @@ export const Settings = ({ <> {held && (

- {held} is in GPU memory — loading this one frees it first. + {held} is in memory — loading this one frees it first.

)} )}
) : ( - + <> + {selected.customModel && ( + + )} + + )} {!local && selected.keyUrl && ( diff --git a/demo/src/local-engines.ts b/demo/src/local-engines.ts new file mode 100644 index 0000000..8661bb5 --- /dev/null +++ b/demo/src/local-engines.ts @@ -0,0 +1,108 @@ +import { createWebLLMEngine, type LocalModelEngine } from '@dudko.dev/agent-web-react' +import type { ModelOption } from './models' +import { createLocalModel } from './providers' + +/** + * The on-device runtimes the demo can run, as engines for `useLocalModel`: + * WebLLM (WebGPU), the browser's built-in model, and transformers.js (ONNX on + * WebGPU). Each runtime's package is imported when a model of it is loaded, so + * nobody downloads a runtime they never pick. + */ + +/** WebLLM, with the statically-imported factory a bundler needs (providers.ts). */ +const webLLM = createWebLLMEngine({ create: createLocalModel }) + +const hasGpu = () => typeof navigator !== 'undefined' && 'gpu' in navigator + +/** + * Chrome's built-in model (Gemini Nano, the Prompt API) via `@browser-ai/core`. + * The page downloads nothing: Chrome fetches the model once for every site. + */ +const builtIn: LocalModelEngine = { + create: async () => { + const { browserAI } = await import('@browser-ai/core') + return browserAI('text', { expectedInputs: [{ type: 'text' }, { type: 'image' }] }) + }, + warmUp: async (model, { onProgress }) => { + const m = model as unknown as { + availability: () => Promise + createSessionWithProgress: (cb: (p: number) => void) => Promise + } + const availability = await m.availability() + if (availability === 'unavailable') { + throw new Error( + 'Chrome says its built-in model is unavailable here: it needs Chrome 148+ on desktop with ~22 GB free disk and a GPU with 4+ GB (or 16 GB RAM).', + ) + } + await m.createSessionWithProgress((p) => + onProgress({ + progress: p, + text: + availability === 'available' + ? 'Starting the built-in model' + : 'Chrome is downloading its model', + }), + ) + }, + supported: () => typeof globalThis !== 'undefined' && 'LanguageModel' in globalThis, + unsupportedMessage: + "This browser has no built-in model — it's Chrome's Prompt API (Chrome 148+ on desktop).", + contextWindowOf: (model) => + (model as unknown as { getContextWindow?: () => number | undefined }).getContextWindow?.(), +} + +/** Picks q4f16 weights where the GPU has f16 shaders, plain q4 elsewhere. */ +const pickDtype = async (): Promise<'q4f16' | 'q4'> => { + try { + const adapter = await ( + navigator as unknown as { + gpu: { requestAdapter: () => Promise<{ features: Set } | null> } + } + ).gpu.requestAdapter() + return adapter?.features.has('shader-f16') ? 'q4f16' : 'q4' + } catch { + return 'q4' + } +} + +/** transformers.js (`@browser-ai/transformers-js`): ONNX models on WebGPU. */ +const transformersFor = (vision: boolean): LocalModelEngine => ({ + create: async (id) => { + const [{ transformersJS }, dtype] = await Promise.all([ + import('@browser-ai/transformers-js'), + pickDtype(), + ]) + return transformersJS(id, { device: 'webgpu', dtype, isVisionModel: vision }) + }, + warmUp: async (model, { onProgress }) => { + await ( + model as unknown as { + createSessionWithProgress: (cb: (p: number) => void) => Promise + } + ).createSessionWithProgress((p) => + onProgress({ progress: p, text: `Downloading the weights — ${Math.round(p * 100)}%` }), + ) + }, + // The provider keeps the loaded model private; dispose() frees its GPU buffers. + unload: async (model) => { + const loaded = ( + model as unknown as { modelInstance?: [unknown, { dispose?: () => Promise }] } + ).modelInstance + await loaded?.[1]?.dispose?.() + }, + supported: hasGpu, + unsupportedMessage: 'WebGPU is not available in this browser.', +}) +const transformersVision = transformersFor(true) + +/** The engine that runs a local model option (cloud options never load). */ +export const engineFor = (option: ModelOption): LocalModelEngine => { + switch (option.runtime) { + case 'built-in': + return builtIn + case 'transformers-js': + return transformersVision + default: + return webLLM + } +} diff --git a/demo/src/models.ts b/demo/src/models.ts index 769ddb7..ac012d5 100644 --- a/demo/src/models.ts +++ b/demo/src/models.ts @@ -1,81 +1,140 @@ -import type { ProviderType } from '@dudko.dev/agent-web' +import type { CloudProvider } from './cloud' + +/** Where a model runs: a cloud API (your key) or this device. */ +export type Runtime = 'cloud' | 'web-llm' | 'built-in' | 'transformers-js' export interface ModelOption { id: string - /** Optgroup shown in the picker (provider family). */ + /** Optgroup shown in the picker (provider family / runtime). */ group: string /** Label within its group. */ label: string - /** 'web-llm' is handled specially (local, no key); the rest are cloud specs. */ - providerType: ProviderType + runtime: Runtime + /** For cloud models: the provider whose API is called. */ + provider?: CloudProvider + /** The model id as the API / runtime expects it. */ model: string + /** OpenRouter's "any model": the id is typed by the user (`model` is the placeholder). */ + customModel?: boolean /** For cloud providers: the vault ref the key is stored under. */ credentialRef?: string keyLabel?: string keyPlaceholder?: string keyUrl?: string - /** A short note about direct-browser BYOK reliability / CORS / download size. */ + /** What it is good at, what it costs (VRAM / download), and anything to know. */ note: string + /** + * The context window, in tokens. For WebLLM models it is the window the + * model is LOADED with (WebLLM's default is 4096; more costs KV-cache VRAM, + * not a download). The agent fits its runs into it; unset = 128k. + */ + contextWindow?: number + /** Takes images, where the core can't tell from the model (built-in, Gemma 4). */ + vision?: boolean } -// Shared per-provider key metadata (all tiers of a provider use one key). +// Shared per-provider key metadata (all models of a provider use one key). const GOOGLE = { + runtime: 'cloud', + provider: 'google', credentialRef: 'google', keyLabel: 'Google AI Studio key', keyPlaceholder: 'AIza…', keyUrl: 'https://aistudio.google.com/apikey', } as const const ANTHROPIC = { + runtime: 'cloud', + provider: 'anthropic', credentialRef: 'anthropic', keyLabel: 'Anthropic API key', keyPlaceholder: 'sk-ant-…', keyUrl: 'https://console.anthropic.com/settings/keys', } as const const OPENAI = { + runtime: 'cloud', + provider: 'openai', credentialRef: 'openai', keyLabel: 'OpenAI API key', keyPlaceholder: 'sk-…', keyUrl: 'https://platform.openai.com/api-keys', } as const +const KIMI = { + runtime: 'cloud', + provider: 'moonshotai', + credentialRef: 'moonshot', + keyLabel: 'Moonshot (Kimi) API key', + keyPlaceholder: 'sk-…', + keyUrl: 'https://platform.kimi.ai/', +} as const +const GROQ = { + runtime: 'cloud', + provider: 'groq', + credentialRef: 'groq', + keyLabel: 'Groq API key', + keyPlaceholder: 'gsk_…', + keyUrl: 'https://console.groq.com/keys', +} as const +const CEREBRAS = { + runtime: 'cloud', + provider: 'cerebras', + credentialRef: 'cerebras', + keyLabel: 'Cerebras API key', + keyPlaceholder: 'csk-…', + keyUrl: 'https://cloud.cerebras.ai/', +} as const +const MISTRAL = { + runtime: 'cloud', + provider: 'mistral', + credentialRef: 'mistral', + keyLabel: 'Mistral API key', + keyPlaceholder: 'your Mistral key', + keyUrl: 'https://console.mistral.ai/api-keys', +} as const +const OPENROUTER = { + runtime: 'cloud', + provider: 'openrouter', + credentialRef: 'openrouter', + keyLabel: 'OpenRouter API key', + keyPlaceholder: 'sk-or-…', + keyUrl: 'https://openrouter.ai/keys', +} as const +const WEBLLM = { group: 'Local · WebLLM (WebGPU, no key)', runtime: 'web-llm' } as const /** - * The models the demo can drive — three tiers (fast · balanced · smart) per - * cloud provider, plus a spread of local WebGPU models by parameter count. - * - * Cloud model ids are **concrete versions** (not rolling `*-latest` aliases), - * current as of mid-2026; the demo builds each provider model directly (see - * `providers.ts`). Google (Gemini) is the most reliable direct BYOK from a - * browser; Anthropic works with an injected opt-in header; OpenAI blocks direct - * browser calls (CORS) and needs a proxy. + * The models the demo can drive: cloud providers called straight from the + * page with the user's key (every one sends CORS headers — checked October + * 2026), and on-device models in three runtimes. Cloud ids are concrete + * versions, current as of October 2026; the demo builds each model itself + * (see `cloud.ts` and `local-engines.ts`). */ export const MODELS: ModelOption[] = [ - // ── Google (Gemini) ──────────────────────────────────────────────────────── + // ── Google (Gemini) — 1M window, images & PDFs ───────────────────────────── { id: 'google-flash-lite', group: 'Google · Gemini', - label: 'Gemini 3.1 Flash-Lite — fast', - providerType: 'google', - model: 'gemini-3.1-flash-lite-preview', + label: 'Gemini 3.5 Flash-Lite — fast', + model: 'gemini-3.5-flash-lite', ...GOOGLE, - note: 'Cheapest, fastest Gemini tier — great for high-throughput tool use. Reliable direct BYOK from the browser.', + contextWindow: 1_048_576, + note: 'Cheap and fast ($0.30 / $2.50 per 1M tokens), images and PDFs, a free tier.', }, { id: 'google-flash', group: 'Google · Gemini', - label: 'Gemini 3.5 Flash — balanced', - providerType: 'google', - model: 'gemini-3.5-flash', + label: 'Gemini 3.8 Flash — balanced', + model: 'gemini-3.8-flash', ...GOOGLE, - note: 'The balanced default — recommended for this demo. Reliable direct BYOK from the browser.', + contextWindow: 1_048_576, + note: 'The newest Flash and the demo’s default: strong tool use, images and PDFs, a free tier ($0.75 / $3.75 per 1M until the end of 2026).', }, { id: 'google-pro', group: 'Google · Gemini', label: 'Gemini 3.1 Pro — smart', - providerType: 'google', model: 'gemini-3.1-pro-preview', ...GOOGLE, - note: 'Most capable Gemini for complex reasoning. Reliable direct BYOK from the browser.', + contextWindow: 1_048_576, + note: 'The most capable Gemini (a preview; no free tier).', }, // ── Anthropic (Claude) ─────────────────────────────────────────────────────── @@ -83,116 +142,287 @@ export const MODELS: ModelOption[] = [ id: 'anthropic-haiku', group: 'Anthropic · Claude', label: 'Claude Haiku 4.5 — fast', - providerType: 'anthropic', model: 'claude-haiku-4-5', ...ANTHROPIC, - note: 'Fastest, cheapest Claude. Works directly from the browser (the required opt-in header is injected for you).', + contextWindow: 200_000, + note: 'The fastest, cheapest Claude (retires no sooner than 15 Oct 2026). Called from the page with the opt-in header Anthropic requires.', }, { id: 'anthropic-sonnet', group: 'Anthropic · Claude', - label: 'Claude Sonnet 5 — balanced', - providerType: 'anthropic', - model: 'claude-sonnet-5', + label: 'Claude Sonnet 5.5 — balanced', + model: 'claude-sonnet-5-5', ...ANTHROPIC, - note: 'Balanced Claude — strong coding & tool use. Works directly from the browser (opt-in header injected).', + contextWindow: 1_000_000, + note: 'Balanced Claude: strong coding and tool use, images and PDFs, a 1M window.', }, { id: 'anthropic-opus', group: 'Anthropic · Claude', - label: 'Claude Opus 4.8 — smart', - providerType: 'anthropic', - model: 'claude-opus-4-8', + label: 'Claude Opus 5.5 — smart', + model: 'claude-opus-5-5', ...ANTHROPIC, - note: 'Most capable Claude. Works directly from the browser (opt-in header injected).', + contextWindow: 1_000_000, + note: 'Anthropic’s recommended default for hard tasks, a 1M window.', + }, + { + id: 'anthropic-fable', + group: 'Anthropic · Claude', + label: 'Claude Fable 5.1 — top tier', + model: 'claude-fable-5-1', + ...ANTHROPIC, + contextWindow: 1_000_000, + note: 'The most capable Claude ($10 / $50 per 1M tokens).', }, - // ── OpenAI (GPT) ───────────────────────────────────────────────────────────── + // ── OpenAI (GPT-6) ─────────────────────────────────────────────────────────── { - id: 'openai-nano', + id: 'openai-luna', group: 'OpenAI · GPT', - label: 'GPT-5.4 nano — fast', - providerType: 'openai', - model: 'gpt-5.4-nano', + label: 'GPT-6 Luna — fast', + model: 'gpt-6-luna', ...OPENAI, - note: 'Smallest, fastest GPT-5. OpenAI blocks direct browser calls (CORS) — expect to need a proxy.', + contextWindow: 922_000, + note: 'The cheapest GPT-6 ($0.10 / $0.50 per 1M tokens), images.', }, { - id: 'openai-mini', + id: 'openai-sol', group: 'OpenAI · GPT', - label: 'GPT-5.4 mini — balanced', - providerType: 'openai', - model: 'gpt-5.4-mini', + label: 'GPT-6.1 Sol — balanced', + model: 'gpt-6.1-sol', ...OPENAI, - note: 'Balanced GPT-5. OpenAI blocks direct browser calls (CORS) — expect to need a proxy.', + contextWindow: 922_000, + note: 'Balanced GPT-6 (tools through the Responses API, which the AI SDK uses).', }, { - id: 'openai-gpt55', + id: 'openai-astra', group: 'OpenAI · GPT', - label: 'GPT-5.5 — smart', - providerType: 'openai', - model: 'gpt-5.5', + label: 'GPT-6 Astra — smart', + model: 'gpt-6-astra', ...OPENAI, - note: 'Flagship GPT-5.5 for complex reasoning & coding. OpenAI blocks direct browser calls (CORS) — expect to need a proxy.', + contextWindow: 922_000, + note: 'OpenAI’s flagship.', + }, + + // ── Moonshot (Kimi) ────────────────────────────────────────────────────────── + { + id: 'kimi-k2_6', + group: 'Moonshot · Kimi', + label: 'Kimi K2.6 — balanced', + model: 'kimi-k2.6', + ...KIMI, + contextWindow: 262_144, + note: 'Cheap ($0.95 / $4 per 1M tokens), images and video, thinking you can switch off. No PDFs (convert them first).', + }, + { + id: 'kimi-k3', + group: 'Moonshot · Kimi', + label: 'Kimi K3 — smart', + model: 'kimi-k3', + ...KIMI, + contextWindow: 1_048_576, + note: 'Kimi’s flagship (open weights): always thinks, images and video, a 1M window ($3 / $15 per 1M).', }, - // ── Local (WebGPU / WebLLM) — no key, fully private ────────────────────────── + // ── Fast inference (Groq, Cerebras) ────────────────────────────────────────── + { + id: 'groq-oss-20b', + group: 'Fast · Groq / Cerebras', + label: 'GPT-OSS 20B on Groq — fastest', + model: 'openai/gpt-oss-20b', + ...GROQ, + contextWindow: 131_072, + note: 'Answers in a blink: Groq runs it at ~1000 tokens/s. Text only.', + }, + { + id: 'groq-oss-120b', + group: 'Fast · Groq / Cerebras', + label: 'GPT-OSS 120B on Groq', + model: 'openai/gpt-oss-120b', + ...GROQ, + contextWindow: 131_072, + note: 'The bigger open GPT, still very fast. Text only.', + }, + { + id: 'groq-qwen', + group: 'Fast · Groq / Cerebras', + label: 'Qwen3.8 27B on Groq — images', + model: 'qwen/qwen3.8-27b', + ...GROQ, + contextWindow: 131_072, + note: 'Fast and multimodal (a preview on Groq): images, tools, reasoning.', + }, + { + id: 'cerebras-oss-120b', + group: 'Fast · Groq / Cerebras', + label: 'GPT-OSS 120B on Cerebras', + model: 'gpt-oss-120b', + ...CEREBRAS, + contextWindow: 65_536, + note: 'Among the fastest inference there is. Cerebras answers errors without CORS headers, so a wrong key shows as a network error.', + }, + + // ── Mistral ────────────────────────────────────────────────────────────────── + { + id: 'mistral-small', + group: 'Mistral', + label: 'Mistral Small 4 — fast', + model: 'mistral-small-2603', + ...MISTRAL, + contextWindow: 262_144, + note: 'Cheap ($0.15 / $0.60 per 1M tokens) and quick, images.', + }, + { + id: 'mistral-medium', + group: 'Mistral', + label: 'Mistral Medium 3.5 — balanced', + model: 'mistral-medium-2604', + ...MISTRAL, + contextWindow: 262_144, + note: 'Mistral’s balanced model, images.', + }, + + // ── OpenRouter: one key, any model ─────────────────────────────────────────── + { + id: 'openrouter-kimi', + group: 'OpenRouter (any model)', + label: 'Kimi K3 via OpenRouter', + model: 'moonshotai/kimi-k3', + ...OPENROUTER, + contextWindow: 1_048_576, + note: 'One OpenRouter key reaches every provider it lists.', + }, + { + id: 'openrouter-custom', + group: 'OpenRouter (any model)', + label: 'Any model — type its id', + model: 'deepseek/deepseek-v4-pro', + customModel: true, + ...OPENROUTER, + note: 'Type any OpenRouter model id (provider/model), e.g. deepseek/deepseek-v4-pro, zai-org/glm-5.3, qwen/qwen3.8-27b.', + }, + + // ── Local: WebLLM (WebGPU) — no key, nothing leaves the device ────────────── + { + id: 'local-gemma3-1b', + ...WEBLLM, + label: 'Gemma 3 1B — fastest · ~0.7 GB', + model: 'gemma3-1b-it-q4f16_1-MLC', + contextWindow: 8_192, + note: 'The quickest local model to download and to answer. 8k window (its maximum) — ~0.8 GB of VRAM.', + }, + { + id: 'local-llama-1b', + ...WEBLLM, + label: 'Llama 3.2 1B — fast · ~0.9 GB', + model: 'Llama-3.2-1B-Instruct-q4f16_1-MLC', + contextWindow: 16_384, + note: 'Small and quick. Loaded with a 16k window — ~1.3 GB of VRAM.', + }, { id: 'local-qwen-0_8b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Qwen3.5 0.8B — ~0.6 GB', - providerType: 'web-llm', model: 'Qwen3.5-0.8B-q4f16_1-MLC', - note: 'Tiny & quick to download. Runs entirely on your GPU via WebLLM — no key, fully private. Needs WebGPU (Chrome/Edge).', + contextWindow: 32_768, + note: 'Tiny, with a 32k window (WebLLM defaults to 4k) — ~2 GB of VRAM. It thinks by default: set Thinking to "none" for faster answers.', }, { id: 'local-qwen-2b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Qwen3.5 2B — ~1.5 GB', - providerType: 'web-llm', model: 'Qwen3.5-2B-q4f16_1-MLC', - note: 'Small & capable for tools. Runs entirely on your GPU via WebLLM — no key, fully private. Needs WebGPU (Chrome/Edge).', + contextWindow: 32_768, + note: 'Small and capable with tools. 32k window — ~2.6 GB of VRAM. Thinking "none" makes it faster.', + }, + { + id: 'local-ministral-3b', + ...WEBLLM, + label: 'Ministral 3 3B — ~2.5 GB', + model: 'Ministral-3-3B-Instruct-2512-BF16-q4f16_1-MLC', + contextWindow: 8_192, + note: 'Mistral’s small model (Dec 2025). 8k window — ~3.3 GB of VRAM.', }, { id: 'local-llama-3b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Llama 3.2 3B — ~2 GB', - providerType: 'web-llm', model: 'Llama-3.2-3B-Instruct-q4f16_1-MLC', - note: 'A well-rounded 3B. Runs entirely on your GPU via WebLLM — no key, fully private. Needs WebGPU (Chrome/Edge).', + contextWindow: 8_192, + note: 'A well-rounded 3B. 8k window — ~2.7 GB of VRAM.', + }, + { + id: 'local-phi4-mini', + ...WEBLLM, + label: 'Phi-4 mini 3.8B — ~2.5 GB', + model: 'Phi-4-mini-instruct-q4f16_1-MLC', + contextWindow: 8_192, + note: 'Microsoft’s small model, good at reasoning and code. 8k window — ~3.9 GB of VRAM.', }, { id: 'local-qwen-4b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Qwen3.5 4B — ~2.7 GB', - providerType: 'web-llm', model: 'Qwen3.5-4B-q4f16_1-MLC', - note: 'Stronger reasoning at a mid size. Runs entirely on your GPU via WebLLM — no key. Needs WebGPU + a few GB of VRAM.', + contextWindow: 16_384, + note: 'Stronger reasoning at a mid size. 16k window — ~4.3 GB of VRAM.', + }, + { + id: 'local-phi-vision', + ...WEBLLM, + label: 'Phi-3.5 Vision 4B — images · ~2.4 GB', + model: 'Phi-3.5-vision-instruct-q4f16_1-MLC', + contextWindow: 8_192, + note: 'A local multimodal model: paste, drop or attach an image and ask about it — it never leaves your device. 8k window (an image takes ~750 tokens) — ~5.5 GB of VRAM.', }, { id: 'local-llama-8b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Llama 3.1 8B — ~5 GB', - providerType: 'web-llm', model: 'Llama-3.1-8B-Instruct-q4f16_1-MLC', - note: 'Large local model. Runs on your GPU via WebLLM — no key. A ~5 GB one-time download; needs WebGPU + ~6 GB of VRAM.', + contextWindow: 8_192, + note: 'A large local model. 8k window — ~5.5 GB of VRAM.', }, { id: 'local-qwen-9b', - group: 'Local · WebGPU (no key)', + ...WEBLLM, label: 'Qwen3.5 9B — ~6 GB', - providerType: 'web-llm', model: 'Qwen3.5-9B-q4f16_1-MLC', - note: 'A strong ~9B local model. Runs on your GPU via WebLLM — no key. A ~6 GB download; needs WebGPU + ample VRAM.', + contextWindow: 16_384, + note: 'The strongest local model here. 16k window — ~6.8 GB of VRAM.', + }, + + // ── Local: the browser's own model ─────────────────────────────────────────── + { + id: 'built-in-nano', + group: 'Local · built into the browser', + label: 'Gemini Nano (Chrome) — no download', + runtime: 'built-in', + model: 'gemini-nano', + vision: true, + note: 'Chrome’s built-in model (the Prompt API, Chrome 148+ on desktop): the page downloads nothing, it takes images. Small window (a few thousand tokens) — keep chats short.', + }, + + // ── Local: transformers.js (ONNX on WebGPU) ───────────────────────────────── + { + id: 'tjs-smolvlm', + group: 'Local · transformers.js (WebGPU)', + label: 'SmolVLM 256M — images · ~0.2 GB', + runtime: 'transformers-js', + model: 'HuggingFaceTB/SmolVLM-256M-Instruct', + contextWindow: 8_192, + note: 'A tiny, fast vision model: describe or read an image. Too small to drive tools well — ask it about pictures.', }, { - id: 'local-llama-13b', - group: 'Local · WebGPU (no key)', - label: 'Llama 2 13B — ~7 GB', - providerType: 'web-llm', - model: 'Llama-2-13b-chat-hf-q4f16_1-MLC', - note: 'The largest local model here (13B). Runs on your GPU via WebLLM — no key. A ~7 GB download; needs WebGPU + a lot of VRAM (~10 GB).', + id: 'tjs-gemma4-e2b', + group: 'Local · transformers.js (WebGPU)', + label: 'Gemma 4 E2B — images · ~3.5 GB', + runtime: 'transformers-js', + model: 'onnx-community/gemma-4-E2B-it-ONNX', + contextWindow: 32_768, + vision: true, + note: 'Google’s on-device multimodal model (text and images; a 128k window, run here with 32k). Experimental in this demo: a large first download.', }, ] -export const isLocal = (m: ModelOption): boolean => m.providerType === 'web-llm' +export const isLocal = (m: ModelOption): boolean => m.runtime !== 'cloud' diff --git a/demo/src/providers.ts b/demo/src/providers.ts index f7fed78..4c95dc1 100644 --- a/demo/src/providers.ts +++ b/demo/src/providers.ts @@ -1,46 +1,28 @@ -import { createAnthropic } from '@ai-sdk/anthropic' -import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { createOpenAI } from '@ai-sdk/openai' import { webLLM } from '@browser-ai/web-llm' -import type { LanguageModel } from 'ai' +import { prebuiltAppConfig } from '@mlc-ai/web-llm' +import { withWebLLMContextWindow } from '@dudko.dev/agent-web' import type { WebLLMModelFactory } from '@dudko.dev/agent-web-react' -import type { ModelOption } from './models' - -/** - * Build a cloud provider's `LanguageModel` **in the app**, then hand it to the - * agent as a direct model. - * - * The core (`@dudko.dev/agent-web`) can also resolve a `{ providerType, model, - * credentialRef }` spec by dynamically importing the provider package — but - * that `import(pkg)` is `@vite-ignore`d and uses a bare specifier, which a - * browser bundle can't resolve at runtime. The core then reports - * `Provider package "@ai-sdk/…" is not installed`. Importing the factories - * statically here lets Vite bundle them, and passing the built model directly - * sidesteps the dynamic import entirely. - */ -export const buildCloudModel = (model: ModelOption, apiKey: string): LanguageModel => { - switch (model.providerType) { - case 'google': - return createGoogleGenerativeAI({ apiKey })(model.model) - case 'anthropic': - return createAnthropic({ - apiKey, - // Anthropic's SDK refuses direct browser calls without this opt-in header. - headers: { 'anthropic-dangerous-direct-browser-access': 'true' }, - })(model.model) - case 'openai': - return createOpenAI({ apiKey })(model.model) - default: - throw new Error(`unsupported cloud provider: ${model.providerType}`) - } -} /** * Build a local WebLLM model, statically importing `webLLM` so the bundler - * includes it. Passed to `useWebLLMModel({ create })` — same root cause as the - * cloud case: the core's `await import('@browser-ai/web-llm')` gets stubbed to - * an empty module by Vite, which surfaces at runtime as `webLLM is not a + * includes it (the WebLLM engine in local-engines.ts uses it). The core's own + * `await import('@browser-ai/web-llm')` is a bare, `@vite-ignore`d specifier + * that a bundle can't resolve — it surfaces at runtime as `webLLM is not a * function`. */ -export const createLocalModel: WebLLMModelFactory = (modelId, options) => - Promise.resolve(webLLM(modelId, options as never) as never) +export const createLocalModel: WebLLMModelFactory = (modelId, options) => { + // WebLLM loads its models with a 4096-token window; load with the model's + // own (see models.ts). It goes in engineConfig.appConfig — @browser-ai/web-llm + // ignores its top-level `appConfig` setting. + const { contextWindowTokens, ...rest } = options ?? {} + const settings = contextWindowTokens + ? { + ...rest, + engineConfig: { + ...(rest.engineConfig as object | undefined), + appConfig: withWebLLMContextWindow(prebuiltAppConfig, modelId, contextWindowTokens), + }, + } + : rest + return Promise.resolve(webLLM(modelId, settings as never) as never) +} diff --git a/demo/src/settings.ts b/demo/src/settings.ts index 64c1f4f..017243b 100644 --- a/demo/src/settings.ts +++ b/demo/src/settings.ts @@ -22,6 +22,12 @@ export interface DemoSettings { /** Tool-calling rounds inside one step. */ maxStepsPerTask: number autoCompact: boolean + /** + * Fewer model calls per turn: no replanning and no separate final answer — + * the step's own reply is the answer (2 calls instead of 3+). + */ + fastAnswers: boolean + /** The window compaction works with; 0 = the model's own (auto). */ contextWindowTokens: number /** Names of enabled skills (built-in and custom). */ enabledSkills: string[] @@ -48,7 +54,8 @@ export const DEFAULT_SETTINGS: DemoSettings = { maxIterations: 6, maxStepsPerTask: 4, autoCompact: true, - contextWindowTokens: 128_000, + fastAnswers: false, + contextWindowTokens: 0, enabledSkills: BUILTIN_SKILLS.map((s) => s.name), customSkills: [], analysts: 'off', @@ -102,7 +109,12 @@ export const enabledSkillsFor = (s: DemoSettings, view: View): Skill[] => * a setting that is baked in at createAgent changes (the consent mode is NOT * in it — the hook applies that one live, mid-run even). */ -export const useSettingsConfig = (s: DemoSettings) => { +/** The model's window when it is known (local models: the one they are loaded with). */ +export const DEFAULT_WINDOW = 128_000 + +export const useSettingsConfig = (s: DemoSettings, modelWindow: number | undefined) => { + // Auto = the model's window; the agent never goes above a local model's anyway. + const window = s.contextWindowTokens || modelWindow || DEFAULT_WINDOW const baked = { thinking: s.thinking, maxTotalTokens: s.maxTotalTokens, @@ -110,7 +122,8 @@ export const useSettingsConfig = (s: DemoSettings) => { maxIterations: s.maxIterations, maxStepsPerTask: s.maxStepsPerTask, autoCompact: s.autoCompact, - contextWindowTokens: s.contextWindowTokens, + fastAnswers: s.fastAnswers, + contextWindowTokens: window, } const rebuildKey = JSON.stringify(baked) const config = useMemo>( @@ -122,11 +135,14 @@ export const useSettingsConfig = (s: DemoSettings) => { maxStepsPerTask: s.maxStepsPerTask, compaction: { auto: s.autoCompact, - contextWindowTokens: s.contextWindowTokens, + contextWindowTokens: window, // Small on purpose so the demo shows compaction within a few turns. - thresholdTokens: Math.min(6_000, Math.floor(s.contextWindowTokens / 2)), + thresholdTokens: Math.min(6_000, Math.floor(window / 2)), keepRecentTurns: 4, }, + // Fast answers: the executor's reply is the answer — no synthesizer call, + // no replanner call (the planner and the step remain). + ...(s.fastAnswers ? { synthesize: false, replan: false } : {}), }), // eslint-disable-next-line react-hooks/exhaustive-deps [rebuildKey], diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 6923d35..d0ec39b 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -24,9 +24,16 @@ export default defineConfig({ // The chess analysts run as module Web Workers (`new Worker(new URL(…), { // type: 'module' })`) that import the agent core — they need ES output. worker: { format: 'es' }, - // WebLLM and the PDF converter ship their own wasm (found next to their JS - // via `new URL(…, import.meta.url)`) and must not be pre-bundled. + // WebLLM, transformers.js (onnxruntime-web) and the PDF converter ship their + // own wasm (found next to their JS via `new URL(…, import.meta.url)`) and + // must not be pre-bundled. optimizeDeps: { - exclude: ['@browser-ai/web-llm', '@mlc-ai/web-llm', '@dudko.dev/pdf-to-md-core'], + exclude: [ + '@browser-ai/web-llm', + '@mlc-ai/web-llm', + '@browser-ai/transformers-js', + '@huggingface/transformers', + '@dudko.dev/pdf-to-md-core', + ], }, }) diff --git a/e2e/agent.spec.ts b/e2e/agent.spec.ts index 4444ee0..a3410ed 100644 --- a/e2e/agent.spec.ts +++ b/e2e/agent.spec.ts @@ -254,6 +254,68 @@ test('a setting changed mid-run keeps the run going and applies to the next one' expect(calls.every((c) => c.thinkingLevel === 'high')).toBe(true) }) +test('cloud providers: Kimi and a typed OpenRouter model are called with the user’s key', async ({ + page, +}) => { + // Record what reaches each API; answer 401 — the wiring is what is tested. + const seen: { url: string; auth: string; model: string }[] = [] + for (const host of ['https://api.moonshot.ai/**', 'https://openrouter.ai/**']) { + await page.route(host, async (route) => { + const req = route.request() + if (req.method() === 'OPTIONS') return route.fulfill({ status: 204 }) + seen.push({ + url: req.url(), + auth: req.headers()['authorization'] ?? '', + model: (req.postDataJSON() as { model?: string } | null)?.model ?? '', + }) + await route.fulfill({ + status: 401, + headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*' }, + body: JSON.stringify({ + error: { message: 'Invalid API key', type: 'invalid_request_error' }, + }), + }) + }) + } + await page.goto('/') + const picker = page.locator('.settings select').first() + await picker.selectOption('kimi-k2_6') + await page.getByPlaceholder('sk-…').fill('sk-kimi-test') + await page.getByRole('button', { name: 'Save' }).click() + await send(page, 'hello') + await expect.poll(() => seen.length).toBeGreaterThan(0) + expect(seen[0].url).toContain('api.moonshot.ai/v1') + expect(seen[0].auth).toBe('Bearer sk-kimi-test') + expect(seen[0].model).toBe('kimi-k2.6') + await expect(page.locator('.awr-msg--error, .awr-msg__error').first()).toBeVisible() + + seen.length = 0 + await page.locator('.settings__summary').click() + await picker.selectOption('openrouter-custom') + await page.getByLabel('Model id').fill('zai-org/glm-5.3') + await page.getByPlaceholder('sk-or-…').fill('sk-or-test') + await page.getByRole('button', { name: 'Save' }).click() + await send(page, 'hello again') + await expect.poll(() => seen.length).toBeGreaterThan(0) + expect(seen[0].url).toContain('openrouter.ai/api/v1') + expect(seen[0].auth).toBe('Bearer sk-or-test') + expect(seen[0].model).toBe('zai-org/glm-5.3') +}) + +test('fast answers: the step’s reply is the answer — no synthesizer call', async ({ page }) => { + const calls = await mockGemini(page, (c) => + c.stage === 'planner' ? plan('Say hi') : { text: 'Hi from the step.' }, + ) + await openWithKey(page) + await page.locator('.agentset__title').click() + await page.getByLabel('Fast answers (fewer model calls)').check() + await send(page, 'hello') + await expect(page.locator('.awr-msg--assistant .awr-msg__bubble')).toContainText( + 'Hi from the step.', + ) + expect(calls.map((c) => c.stage)).toEqual(['planner', 'executor']) +}) + test('labels: every text of the chat can be swapped (Russian)', async ({ page }) => { await mockGemini(page, () => ({ text: 'ok' })) await openWithKey(page) diff --git a/package-lock.json b/package-lock.json index f66361b..02ffe8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dudko.dev/agent-web-react", - "version": "0.0.12", + "version": "0.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dudko.dev/agent-web-react", - "version": "0.0.12", + "version": "0.0.13", "funding": [ { "type": "individual", @@ -27,7 +27,7 @@ ], "license": "MIT", "devDependencies": { - "@dudko.dev/agent-web": "^0.0.21", + "@dudko.dev/agent-web": "^0.0.22", "@modelcontextprotocol/sdk": "^1.32.0", "@playwright/test": "^1.63.0", "@types/node": "^22.9.0", @@ -43,7 +43,7 @@ "node": ">=18" }, "peerDependencies": { - "@dudko.dev/agent-web": ">=0.0.20", + "@dudko.dev/agent-web": ">=0.0.22", "react": ">=18", "react-dom": ">=18" } @@ -100,9 +100,9 @@ } }, "node_modules/@dudko.dev/agent-web": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.21.tgz", - "integrity": "sha512-UDcJia0LXbZD3LIFwNICvdc/zdFMRN87YNZly/Ir4NAtUstK2xF9D7y/gwS9pd0jF3AyyB+EsZDzVO0BPF1j0w==", + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@dudko.dev/agent-web/-/agent-web-0.0.22.tgz", + "integrity": "sha512-Y8+oOnhZpHiAphwZj9CU8pQYo8QOshB/ULQp9Cq9a3OdHhOdvdJf/IDXrURe6AEL3sgCpfdJ1dhiprHjT/h46g==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 11c8ba5..7b34ff1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dudko.dev/agent-web-react", - "version": "0.0.12", + "version": "0.0.13", "description": "React bindings for @dudko.dev/agent-web: a headless useAgent hook, an AgentProvider context, and optional pre-styled components (chat panel, plan/step view, BYOK key form, WebLLM load bar) that connect the in-browser LLM agent to any React site. UI you can drop in — or a headless reducer you can build your own around.", "type": "module", "sideEffects": [ @@ -105,12 +105,12 @@ "node": ">=18" }, "peerDependencies": { - "@dudko.dev/agent-web": ">=0.0.20", + "@dudko.dev/agent-web": ">=0.0.22", "react": ">=18", "react-dom": ">=18" }, "devDependencies": { - "@dudko.dev/agent-web": "^0.0.21", + "@dudko.dev/agent-web": "^0.0.22", "@modelcontextprotocol/sdk": "^1.32.0", "@playwright/test": "^1.63.0", "@types/node": "^22.9.0", diff --git a/src/hooks/use-local-model.ts b/src/hooks/use-local-model.ts new file mode 100644 index 0000000..6b8cf6f --- /dev/null +++ b/src/hooks/use-local-model.ts @@ -0,0 +1,241 @@ +import { useCallback, useRef, useState } from 'react' +import type { createWebLLMModel } from '@dudko.dev/agent-web' +import { errMessage } from '../util.js' + +/** An AI SDK `LanguageModel` (the type, via the core — no `ai` import here). */ +type LanguageModel = Awaited> + +/** Download / initialisation progress of a local model. */ +export interface LocalModelProgress { + /** 0..1. */ + progress: number + /** Human-readable status from the runtime. */ + text: string +} + +/** What `create` and `warmUp` are given. */ +export interface LocalModelLoadContext { + /** Report progress; the hook shows it for the load in flight only. */ + onProgress: (report: LocalModelProgress) => void + /** The window to load the model with, when the host asked for one. */ + contextWindowTokens?: number +} + +/** + * How to run one kind of on-device model — WebLLM, the browser's built-in + * model (`@browser-ai/core`), transformers.js (`@browser-ai/transformers-js`), + * or your own. Only `create` is required. + */ +export interface LocalModelEngine { + /** Build the model (cheap: the download starts in `warmUp`). */ + create: (modelId: string, ctx: LocalModelLoadContext) => Promise + /** + * Download the weights and initialise now, so `ready` means "ready to chat" + * and the progress shows during the load (default: nothing — the runtime + * then loads on the first message). + */ + warmUp?: (model: M, ctx: LocalModelLoadContext) => Promise + /** Free the model's memory (default: nothing). */ + unload?: (model: M) => Promise + /** Whether this browser can run it at all (default true). */ + supported?: () => boolean + /** The error a load reports when `supported()` is false. */ + unsupportedMessage?: string + /** The model's context window once loaded, when the runtime says. */ + contextWindowOf?: (model: M) => number | undefined +} + +export interface UseLocalModelOptions { + /** + * The window to load the model with (WebLLM loads with 4096 unless told + * otherwise). A different window is a different load. + */ + contextWindowTokens?: number +} + +export interface UseLocalModelReturn { + /** + * The built model, once **this** `modelId` is loaded — pass to + * `createAgent({ model })`. Undefined right after the id changes, until the + * new one is loaded. + */ + model: M | undefined + /** + * Download and initialize the current `modelId`. Resolves with the model + * already loaded for it, if any. Loading another id first frees the previous + * model's memory (one model per hook); call again after an error to retry. + */ + load: () => Promise + /** Free the loaded model's memory (and drop a load in flight). */ + unload: () => Promise + /** The id of the model held in memory, whichever id is current. */ + loadedModelId: string | undefined + /** True while the current `modelId` is downloading / initializing. */ + loading: boolean + /** Load progress of the current `modelId`, 0..1. */ + progress: number + /** Human-readable progress text from the runtime. */ + text: string + /** Why the current `modelId` failed to load. */ + error: string | undefined + /** Whether this browser can run the engine. */ + supported: boolean + /** True once the current `modelId` is ready. */ + ready: boolean + /** The loaded model's context window, in tokens, when the runtime says. */ + contextWindow: number | undefined +} + +interface Loaded { + id: string + /** The id and the window it was loaded with: another window is another load. */ + key: string + model: M + /** Frees it with the engine that built it (the engine may change since). */ + free: () => Promise +} + +const keyOf = (id: string, tokens: number | undefined) => (tokens ? `${id}@${tokens}` : id) + +/** + * Load an on-device model and track its download — for any local runtime, + * described by an {@link LocalModelEngine}. Nothing downloads until you call + * `load()` (models are large), so you can gate it behind a user action. + * + * Everything it reports is about the **current** `modelId`: switch the id and + * `model` / `ready` / `progress` / `error` describe the new one (not loaded + * yet), while the previous model stays in memory — switching back is instant. + * Loading the new id frees the previous one first, so two models never hold + * memory at once; `unload()` frees it on demand. A load superseded by another + * frees its model; a second `load()` of the same id shares the download. + * + * ```tsx + * import { browserAI, doesBrowserSupportBrowserAI } from '@browser-ai/core' + * const nano: LocalModelEngine = { + * create: async () => browserAI('text', { expectedInputs: [{ type: 'image' }] }), + * warmUp: async (m, { onProgress }) => { + * await m.createSessionWithProgress((p) => onProgress({ progress: p, text: 'Downloading' })) + * }, + * supported: doesBrowserSupportBrowserAI, + * contextWindowOf: (m) => m.getContextWindow(), + * } + * const local = useLocalModel('gemini-nano', nano) + * ``` + * + * {@link useWebLLMModel} is this hook with the WebLLM engine. + */ +export const useLocalModel = ( + modelId: string, + engine: LocalModelEngine, + options?: UseLocalModelOptions, +): UseLocalModelReturn => { + const [loaded, setLoaded] = useState | undefined>(undefined) + const [pending, setPending] = useState<{ key: string; progress: number; text: string }>() + const [failure, setFailure] = useState<{ key: string; message: string }>() + // The engine is read at load time: a new object every render is fine. + const engineRef = useRef(engine) + engineRef.current = engine + // The source of truth for async code (state lags a render behind). + const loadedRef = useRef | undefined>(undefined) + const inflightRef = useRef<{ key: string; promise: Promise } | undefined>( + undefined, + ) + const windowTokens = options?.contextWindowTokens + const key = keyOf(modelId, windowTokens) + // Bumped by every load()/unload(): an older load that finishes late is stale. + const seqRef = useRef(0) + + const setHeld = (next: Loaded | undefined) => { + loadedRef.current = next + setLoaded(next) + } + const freeWith = (eng: LocalModelEngine, model: M) => async () => { + try { + await eng.unload?.(model) + } catch { + /* best-effort */ + } + } + + const load = useCallback((): Promise => { + const id = modelId + if (loadedRef.current?.key === key) return Promise.resolve(loadedRef.current.model) + if (inflightRef.current?.key === key) return inflightRef.current.promise + const eng = engineRef.current + if (eng.supported && !eng.supported()) { + setFailure({ + key, + message: eng.unsupportedMessage ?? 'This browser cannot run this model.', + }) + return Promise.resolve(undefined) + } + const seq = ++seqRef.current + const current = () => seq === seqRef.current + const ctx: LocalModelLoadContext = { + onProgress: (report) => { + if (current()) setPending({ key, progress: report.progress, text: report.text }) + }, + ...(windowTokens ? { contextWindowTokens: windowTokens } : {}), + } + const promise = (async (): Promise => { + setPending({ key, progress: 0, text: '' }) + setFailure(undefined) + let built: M | undefined + try { + // One model per hook: free the previous one before the next download. + const previous = loadedRef.current + if (previous) { + setHeld(undefined) + await previous.free() + } + built = await eng.create(id, ctx) + await eng.warmUp?.(built, ctx) + if (!current()) { + // Superseded by another load() or an unload(): don't leak its memory. + await freeWith(eng, built)() + return undefined + } + setHeld({ id, key, model: built, free: freeWith(eng, built) }) + return built + } catch (err) { + if (built) await freeWith(eng, built)() + if (current()) setFailure({ key, message: errMessage(err) }) + return undefined + } finally { + if (current()) { + inflightRef.current = undefined + setPending(undefined) + } + } + })() + inflightRef.current = { key, promise } + return promise + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [modelId, key]) + + const unload = useCallback(async (): Promise => { + seqRef.current++ // a load in flight becomes stale and frees itself + inflightRef.current = undefined + setPending(undefined) + const previous = loadedRef.current + setHeld(undefined) + if (previous) await previous.free() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const model = loaded?.key === key ? loaded.model : undefined + const progressing = pending?.key === key ? pending : undefined + return { + model, + load, + unload, + loadedModelId: loaded?.id, + loading: progressing !== undefined, + progress: progressing?.progress ?? 0, + text: progressing?.text ?? '', + error: failure?.key === key ? failure.message : undefined, + supported: engine.supported ? engine.supported() : true, + ready: model !== undefined, + contextWindow: model ? engine.contextWindowOf?.(model) : undefined, + } +} diff --git a/src/hooks/use-webllm-model.ts b/src/hooks/use-webllm-model.ts index feae798..27543e2 100644 --- a/src/hooks/use-webllm-model.ts +++ b/src/hooks/use-webllm-model.ts @@ -1,12 +1,16 @@ -import { useCallback, useRef, useState } from 'react' import { createWebLLMModel, isWebGPUAvailable, preloadWebLLMModel, unloadWebLLMModel, + webLLMContextWindow, type WebLLMModelOptions, } from '@dudko.dev/agent-web' -import { errMessage } from '../util.js' +import { + useLocalModel, + type LocalModelEngine, + type UseLocalModelReturn, +} from './use-local-model.js' /** The AI SDK `LanguageModel` a WebLLM build produces (resolved via agent-web). */ type WebLLMModel = Awaited> @@ -39,48 +43,16 @@ export interface UseWebLLMModelOptions extends WebLLMModelOptions { create?: WebLLMModelFactory } -export interface UseWebLLMModelReturn { - /** - * The built model, once **this** `modelId` is loaded — pass to - * `createAgent({ model })`. Undefined right after the id changes, until the - * new one is loaded. - */ - model: WebLLMModel | undefined - /** - * Download and initialize the current `modelId`. Resolves with the model - * already loaded for it, if any. Loading another id first frees the previous - * model's GPU memory (one engine per hook); call again after an error to retry. - */ - load: () => Promise - /** Free the loaded model's GPU memory (and drop a load in flight). */ - unload: () => Promise - /** The id of the model held in memory, whichever id is current. */ - loadedModelId: string | undefined - /** True while the current `modelId` is downloading / initializing. */ - loading: boolean - /** Load progress of the current `modelId`, 0..1. */ - progress: number - /** Human-readable progress text from WebLLM. */ - text: string - /** Why the current `modelId` failed to load. */ - error: string | undefined - /** Whether WebGPU is available (required for local models). */ - supported: boolean - /** True once the current `modelId` is ready. */ - ready: boolean -} - -interface Loaded { - id: string - model: WebLLMModel -} +/** What {@link useWebLLMModel} returns — {@link UseLocalModelReturn} for a WebLLM model. */ +export type UseWebLLMModelReturn = UseLocalModelReturn /** - * Load a local WebGPU model with WebLLM and track its download progress. - * Nothing downloads until you call `load()` (models are large), so you can - * gate it behind a user action. `load()` eagerly initializes the engine (via - * the core's `preloadWebLLMModel`), so `ready` means "ready to chat" and - * progress fills during the load rather than silently on the first message. + * Load a local WebGPU model with WebLLM and track its download progress — + * {@link useLocalModel} with the WebLLM engine. Nothing downloads until you + * call `load()` (models are large), so you can gate it behind a user action. + * `load()` eagerly initializes the engine (via the core's + * `preloadWebLLMModel`), so `ready` means "ready to chat" and progress fills + * during the load rather than silently on the first message. * * Everything it reports is about the **current** `modelId`: switch the id and * `model` / `ready` / `progress` / `error` describe the new one (not loaded @@ -88,6 +60,10 @@ interface Loaded { * Loading the new id frees the previous one first, so two models never hold * GPU memory at once; `unload()` frees it on demand. * + * WebLLM loads its models with a 4096-token window; pass `contextWindowTokens` + * to load with more (Qwen3, Llama 3.x take far more — it costs KV-cache VRAM, + * not a new download). A different window is a different load. + * * ```tsx * import { webLLM } from '@browser-ai/web-llm' // your app's optional peer * const create = (id: string, opts?: WebLLMModelOptions) => Promise.resolve(webLLM(id, opts)) @@ -100,106 +76,48 @@ interface Loaded { export const useWebLLMModel = ( modelId: string, options?: UseWebLLMModelOptions, -): UseWebLLMModelReturn => { - const [loaded, setLoaded] = useState(undefined) - const [pending, setPending] = useState<{ id: string; progress: number; text: string }>() - const [failure, setFailure] = useState<{ id: string; message: string }>() - const optionsRef = useRef(options) - optionsRef.current = options - // The source of truth for async code (state lags a render behind). - const loadedRef = useRef(undefined) - const inflightRef = useRef<{ id: string; promise: Promise } | undefined>( - undefined, - ) - // Bumped by every load()/unload(): an older load that finishes late is stale. - const seqRef = useRef(0) - - const setHeld = (next: Loaded | undefined) => { - loadedRef.current = next - setLoaded(next) - } - - const load = useCallback((): Promise => { - const id = modelId - if (loadedRef.current?.id === id) return Promise.resolve(loadedRef.current.model) - if (inflightRef.current?.id === id) return inflightRef.current.promise - if (!isWebGPUAvailable()) { - setFailure({ id, message: 'WebGPU is not available in this browser.' }) - return Promise.resolve(undefined) - } - const seq = ++seqRef.current - const current = () => seq === seqRef.current - const promise = (async (): Promise => { - setPending({ id, progress: 0, text: '' }) - setFailure(undefined) - let built: WebLLMModel | undefined - try { - // One engine per hook: free the previous model before the next download. - const previous = loadedRef.current - if (previous) { - setHeld(undefined) - await unloadWebLLMModel(previous.model) - } - const { create = createWebLLMModel, ...modelOptions } = optionsRef.current ?? {} - built = await create(id, { - ...modelOptions, - // Drive preload here (below) so download progress is reported the same - // way whether `create` is the core's `createWebLLMModel` or an injected - // factory (e.g. a statically-imported `webLLM`, needed under bundlers). - preload: false, - initProgressCallback: (report) => { - if (current()) setPending({ id, progress: report.progress, text: report.text }) - modelOptions.initProgressCallback?.(report) - }, - }) - // Download the weights + init the engine now via the core's helper (a - // 1-token warm-up). WebLLM builds are otherwise lazy — the ~GB download - // would only start on the first `run()`, long after we told the UI the - // model is "ready". Fast + idempotent once the weights are cached. - await preloadWebLLMModel(built) - if (!current()) { - // Superseded by another load() or an unload(): don't leak its engine. - await unloadWebLLMModel(built) - return undefined - } - setHeld({ id, model: built }) - return built - } catch (err) { - if (built) await unloadWebLLMModel(built) - if (current()) setFailure({ id, message: errMessage(err) }) - return undefined - } finally { - if (current()) { - inflightRef.current = undefined - setPending(undefined) - } - } - })() - inflightRef.current = { id, promise } - return promise - }, [modelId]) +): UseWebLLMModelReturn => + useLocalModel(modelId, createWebLLMEngine(options), { + contextWindowTokens: options?.contextWindowTokens, + }) - const unload = useCallback(async (): Promise => { - seqRef.current++ // a load in flight becomes stale and frees itself - inflightRef.current = undefined - setPending(undefined) - const previous = loadedRef.current - setHeld(undefined) - if (previous) await unloadWebLLMModel(previous.model) - }, []) - - const model = loaded?.id === modelId ? loaded.model : undefined - const progressing = pending?.id === modelId ? pending : undefined +/** + * The WebLLM {@link LocalModelEngine}: what {@link useWebLLMModel} runs, for + * hosts that switch between local runtimes with one {@link useLocalModel} + * (pass the window to that hook's `contextWindowTokens`). + */ +export const createWebLLMEngine = ( + options?: UseWebLLMModelOptions, +): LocalModelEngine => { + // The window comes per load (useLocalModel's option), not from here. + const { + create = createWebLLMModel, + contextWindowTokens: _perLoad, + ...modelOptions + } = options ?? {} return { - model, - load, - unload, - loadedModelId: loaded?.id, - loading: progressing !== undefined, - progress: progressing?.progress ?? 0, - text: progressing?.text ?? '', - error: failure?.id === modelId ? failure.message : undefined, - supported: isWebGPUAvailable(), - ready: model !== undefined, + create: (id, ctx) => + create(id, { + ...modelOptions, + ...(ctx.contextWindowTokens ? { contextWindowTokens: ctx.contextWindowTokens } : {}), + // Drive preload in warmUp (below) so download progress is reported the + // same way whether `create` is the core's `createWebLLMModel` or an + // injected factory (e.g. a statically-imported `webLLM`, needed under + // bundlers). + preload: false, + initProgressCallback: (report) => { + ctx.onProgress({ progress: report.progress, text: report.text }) + modelOptions.initProgressCallback?.(report) + }, + }), + // Download the weights + init the engine now via the core's helper (a + // 1-token warm-up). WebLLM builds are otherwise lazy — the ~GB download + // would only start on the first `run()`, long after we told the UI the + // model is "ready". Fast + idempotent once the weights are cached. + warmUp: (model) => preloadWebLLMModel(model), + unload: unloadWebLLMModel, + supported: isWebGPUAvailable, + unsupportedMessage: 'WebGPU is not available in this browser.', + contextWindowOf: webLLMContextWindow, } } diff --git a/src/index.ts b/src/index.ts index fa81edb..82f98b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,15 @@ export type { McpServerResult, McpModule, } from './mcp-types.js' -export { useWebLLMModel } from './hooks/use-webllm-model.js' +export { createWebLLMEngine, useWebLLMModel } from './hooks/use-webllm-model.js' +export { useLocalModel } from './hooks/use-local-model.js' +export type { + LocalModelEngine, + LocalModelLoadContext, + LocalModelProgress, + UseLocalModelOptions, + UseLocalModelReturn, +} from './hooks/use-local-model.js' export type { UseWebLLMModelReturn, UseWebLLMModelOptions,