@tryhamster/gerbil 1.0.0-rc.1 → 1.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/browser/{index.d.mts → index.d.ts} +354 -3
  2. package/dist/browser/index.d.ts.map +1 -0
  3. package/dist/browser/{index.mjs → index.js} +117 -7
  4. package/dist/browser/index.js.map +1 -0
  5. package/dist/{chrome-backend-Y9F7W5VQ.mjs → chrome-backend-CORwaIyC.mjs} +1 -1
  6. package/dist/{chrome-backend-Y9F7W5VQ.mjs.map → chrome-backend-CORwaIyC.mjs.map} +1 -1
  7. package/dist/{chrome-backend-JEPeM2YE.mjs → chrome-backend-DIKYoWj-.mjs} +1 -1
  8. package/dist/cli.mjs +14 -15
  9. package/dist/cli.mjs.map +1 -1
  10. package/dist/frameworks/express.d.mts +1 -1
  11. package/dist/frameworks/express.mjs +3 -4
  12. package/dist/frameworks/express.mjs.map +1 -1
  13. package/dist/frameworks/fastify.d.mts +1 -1
  14. package/dist/frameworks/fastify.mjs +2 -3
  15. package/dist/frameworks/fastify.mjs.map +1 -1
  16. package/dist/frameworks/hono.d.mts +1 -1
  17. package/dist/frameworks/hono.mjs +2 -3
  18. package/dist/frameworks/hono.mjs.map +1 -1
  19. package/dist/frameworks/next.d.mts +2 -2
  20. package/dist/frameworks/next.mjs +2 -3
  21. package/dist/frameworks/next.mjs.map +1 -1
  22. package/dist/frameworks/react.d.mts +1 -1
  23. package/dist/frameworks/trpc.d.mts +1 -1
  24. package/dist/frameworks/trpc.mjs +2 -3
  25. package/dist/frameworks/trpc.mjs.map +1 -1
  26. package/dist/{gerbil-POAz8peb.d.mts → gerbil-CnncBh38.d.mts} +2 -2
  27. package/dist/{gerbil-POAz8peb.d.mts.map → gerbil-CnncBh38.d.mts.map} +1 -1
  28. package/dist/{gerbil-yoSpRHgv.mjs → gerbil-Dq039a6V.mjs} +187 -19
  29. package/dist/gerbil-Dq039a6V.mjs.map +1 -0
  30. package/dist/gerbil-DyTEWXLy.mjs +4 -0
  31. package/dist/index.d.mts +19 -3
  32. package/dist/index.d.mts.map +1 -1
  33. package/dist/index.mjs +6 -7
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/integrations/ai-sdk.d.mts +1 -1
  36. package/dist/integrations/ai-sdk.mjs +4 -5
  37. package/dist/integrations/ai-sdk.mjs.map +1 -1
  38. package/dist/integrations/langchain.d.mts +1 -1
  39. package/dist/integrations/langchain.mjs +2 -3
  40. package/dist/integrations/langchain.mjs.map +1 -1
  41. package/dist/integrations/llamaindex.d.mts +1 -1
  42. package/dist/integrations/llamaindex.mjs +2 -3
  43. package/dist/integrations/llamaindex.mjs.map +1 -1
  44. package/dist/integrations/mcp-client.mjs +2 -2
  45. package/dist/integrations/mcp.d.mts +2 -2
  46. package/dist/integrations/mcp.mjs +5 -6
  47. package/dist/{mcp-Bitg4sjX.mjs → mcp-DY57Whwj.mjs} +3 -3
  48. package/dist/{mcp-Bitg4sjX.mjs.map → mcp-DY57Whwj.mjs.map} +1 -1
  49. package/dist/{one-liner-B1rmFto6.mjs → one-liner-CgRVfe5K.mjs} +2 -2
  50. package/dist/{one-liner-B1rmFto6.mjs.map → one-liner-CgRVfe5K.mjs.map} +1 -1
  51. package/dist/repl-BEusmMZs.mjs +9 -0
  52. package/dist/skills/index.d.mts +2 -2
  53. package/dist/skills/index.d.mts.map +1 -1
  54. package/dist/skills/index.mjs +4 -5
  55. package/dist/{skills-5DxAV-rn.mjs → skills-BGS20rGK.mjs} +2 -2
  56. package/dist/{skills-5DxAV-rn.mjs.map → skills-BGS20rGK.mjs.map} +1 -1
  57. package/dist/stt-BT4Rt49f.mjs +3 -0
  58. package/dist/stt-BtklAjR2.js +439 -0
  59. package/dist/stt-BtklAjR2.js.map +1 -0
  60. package/dist/{stt-Bv_dum-R.mjs → stt-CkfJswka.mjs} +8 -2
  61. package/dist/stt-CkfJswka.mjs.map +1 -0
  62. package/dist/{tools-IYPrqoek.mjs → tools-Bi1P7Xoy.mjs} +2 -2
  63. package/dist/{tools-IYPrqoek.mjs.map → tools-Bi1P7Xoy.mjs.map} +1 -1
  64. package/dist/{tts-DG6denWG.mjs → tts-BFL984rV.mjs} +11 -3
  65. package/dist/tts-BFL984rV.mjs.map +1 -0
  66. package/dist/{tts-5yWeP_I0.mjs → tts-Cuu1TOkM.mjs} +1 -1
  67. package/dist/tts-DKIOWafo.js +731 -0
  68. package/dist/tts-DKIOWafo.js.map +1 -0
  69. package/dist/{types-s6Py2_DL.d.mts → types-DJhOZ6Ct.d.mts} +1 -1
  70. package/dist/{types-s6Py2_DL.d.mts.map → types-DJhOZ6Ct.d.mts.map} +1 -1
  71. package/dist/{utils-CkB4Roi6.mjs → utils-CZBZ8dgR.mjs} +1 -1
  72. package/dist/{utils-CkB4Roi6.mjs.map → utils-CZBZ8dgR.mjs.map} +1 -1
  73. package/package.json +1 -1
  74. package/dist/browser/index.d.mts.map +0 -1
  75. package/dist/browser/index.mjs.map +0 -1
  76. package/dist/gerbil-DeQlX_Mt.mjs +0 -5
  77. package/dist/gerbil-yoSpRHgv.mjs.map +0 -1
  78. package/dist/models-BAtL8qsA.mjs +0 -171
  79. package/dist/models-BAtL8qsA.mjs.map +0 -1
  80. package/dist/models-CE0fBq0U.d.mts +0 -22
  81. package/dist/models-CE0fBq0U.d.mts.map +0 -1
  82. package/dist/repl-D20JO260.mjs +0 -10
  83. package/dist/stt-Bv_dum-R.mjs.map +0 -1
  84. package/dist/stt-KzSoNvwI.mjs +0 -3
  85. package/dist/tts-DG6denWG.mjs.map +0 -1
  86. /package/dist/{auto-update-DsWBBnEk.mjs → auto-update-S9s5-g0C.mjs} +0 -0
  87. /package/dist/{chunk-Ct1HF2bE.mjs → chunk-CkXuGtQK.mjs} +0 -0
  88. /package/dist/{microphone-D-6y9aiE.mjs → microphone-DaMZFRuR.mjs} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"chrome-backend-Y9F7W5VQ.mjs","names":["files: { path: string; size?: number; lfs?: { size?: number } }[]","globalBrowser: Browser | null","globalBrowserPromise: Promise<Browser> | null","globalServer: Server | null","activeBackends: Set<ChromeGPUBackend>","pid: number | null","err: any","wsEndpoint: string | null","modelId: string | null","memory: { usedGB: number; totalGB: number } | null"],"sources":["../src/core/chrome-backend.ts"],"sourcesContent":["/**\n * Chrome DevTools Protocol Backend for WebGPU Inference\n *\n * Uses headless Chrome as a WebGPU accelerator for Node.js environments.\n * Provides the same performance as browser inference (~100+ tok/s with q4f16).\n */\n\nimport { execSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { createServer, type Server } from \"node:http\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport puppeteer, { type Browser, type CDPSession, type Page } from \"puppeteer-core\";\n\n// Persistent cache directory for Chrome profile (keeps model cache between runs)\nconst GERBIL_CACHE_DIR = join(homedir(), \".gerbil\", \"chrome-cache\");\nconst WS_ENDPOINT_FILE = join(GERBIL_CACHE_DIR, \"ws-endpoint.txt\");\nconst CACHED_MODELS_FILE = join(homedir(), \".gerbil\", \"cached-models.json\");\n\n// ============================================\n// Cached Models Tracking\n// ============================================\n\ntype CachedModelEntry = {\n modelId: string;\n downloadedAt: string;\n lastUsed: string;\n sizeBytes?: number;\n contextLength?: number;\n};\n\n/** Get list of models cached in Chrome's IndexedDB */\nexport function getChromeCachedModels(): CachedModelEntry[] {\n try {\n if (!existsSync(CACHED_MODELS_FILE)) {\n return [];\n }\n const data = JSON.parse(readFileSync(CACHED_MODELS_FILE, \"utf-8\"));\n return data.models || [];\n } catch {\n return [];\n }\n}\n\n/** Fetch model context length from HuggingFace (config.json preferred for actual limit) */\nasync function fetchContextLength(modelId: string): Promise<number | undefined> {\n // Priority 1: config.json → max_position_embeddings (actual architectural limit)\n try {\n const res = await fetch(`https://huggingface.co/${modelId}/raw/main/config.json`);\n if (res.ok) {\n const config = await res.json();\n // Check root level first, then nested text_config (for multimodal models like Ministral)\n const textConfig = config.text_config || {};\n const ctxLen =\n config.max_position_embeddings ||\n textConfig.max_position_embeddings ||\n config.sliding_window ||\n textConfig.sliding_window ||\n config.max_seq_len ||\n config.max_sequence_length ||\n config.n_ctx ||\n config.n_positions;\n if (ctxLen) {\n return ctxLen;\n }\n }\n } catch {\n // Continue to fallback\n }\n\n // Priority 2: tokenizer_config.json → model_max_length (fallback only)\n try {\n const tokRes = await fetch(`https://huggingface.co/${modelId}/raw/main/tokenizer_config.json`);\n if (tokRes.ok) {\n const tokConfig = await tokRes.json();\n // Only use if reasonable (< 1M tokens - anything larger is likely placeholder)\n if (tokConfig.model_max_length && tokConfig.model_max_length < 1e6) {\n return tokConfig.model_max_length;\n }\n }\n } catch {\n // Ignore\n }\n\n return;\n}\n\n/** Get file size from HuggingFace tree entry (handles both regular and LFS files) */\nfunction getFileSize(file: { size?: number; lfs?: { size?: number } }): number {\n // LFS files have size in lfs.size, regular files in size\n return file.lfs?.size || file.size || 0;\n}\n\n/** Fetch model size from HuggingFace API */\nasync function fetchModelSize(modelId: string): Promise<number | undefined> {\n try {\n // Try to get ONNX file size from tree API\n const treeRes = await fetch(`https://huggingface.co/api/models/${modelId}/tree/main/onnx`);\n if (treeRes.ok) {\n const files: { path: string; size?: number; lfs?: { size?: number } }[] =\n await treeRes.json();\n\n // Prefer q4f16 > q4 > fp16 > any .onnx file\n const q4f16 = files.find((f) => f.path.includes(\"q4f16\") && f.path.endsWith(\".onnx\"));\n const q4 = files.find(\n (f) => f.path.includes(\"q4\") && !f.path.includes(\"f16\") && f.path.endsWith(\".onnx\"),\n );\n const fp16 = files.find((f) => f.path.includes(\"fp16\") && f.path.endsWith(\".onnx\"));\n const anyOnnx = files.find((f) => f.path.endsWith(\".onnx\"));\n const bestFile = q4f16 || q4 || fp16 || anyOnnx;\n\n if (bestFile) {\n // Sum up the .onnx file AND all associated .onnx_data* files\n const baseName = bestFile.path.replace(\".onnx\", \"\");\n const relatedFiles = files.filter(\n (f) => f.path === bestFile.path || f.path.startsWith(`${baseName}.onnx_data`),\n );\n const totalSize = relatedFiles.reduce((sum, f) => sum + getFileSize(f), 0);\n if (totalSize > 0) {\n return totalSize;\n }\n }\n }\n\n // Fallback to model info API\n const res = await fetch(`https://huggingface.co/api/models/${modelId}`);\n if (res.ok) {\n const info = await res.json();\n return info.usedStorage;\n }\n } catch {\n // Ignore fetch errors\n }\n return;\n}\n\n/** Track a model as cached */\nexport function trackCachedModel(\n modelId: string,\n sizeBytes?: number,\n contextLength?: number,\n): void {\n try {\n const dir = join(homedir(), \".gerbil\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const models = getChromeCachedModels();\n const existing = models.find((m) => m.modelId === modelId);\n const now = new Date().toISOString();\n\n if (existing) {\n existing.lastUsed = now;\n if (sizeBytes) {\n existing.sizeBytes = sizeBytes;\n }\n if (contextLength) {\n existing.contextLength = contextLength;\n }\n } else {\n models.push({\n modelId,\n downloadedAt: now,\n lastUsed: now,\n sizeBytes,\n contextLength,\n });\n }\n\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n\n // Fetch missing metadata in background\n const needsSize = !(sizeBytes || existing?.sizeBytes);\n const needsContext = !(contextLength || existing?.contextLength);\n\n if (needsSize || needsContext) {\n Promise.all([\n needsSize ? fetchModelSize(modelId) : Promise.resolve(undefined),\n needsContext ? fetchContextLength(modelId) : Promise.resolve(undefined),\n ])\n .then(([size, context]) => {\n const updatedModels = getChromeCachedModels();\n const model = updatedModels.find((m) => m.modelId === modelId);\n if (model) {\n if (size) {\n model.sizeBytes = size;\n }\n if (context) {\n model.contextLength = context;\n }\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models: updatedModels }, null, 2));\n }\n })\n .catch(() => {});\n }\n } catch {\n // Ignore tracking errors\n }\n}\n\n/** Remove a model from cache tracking */\nexport function untrackCachedModel(modelId: string): void {\n try {\n const models = getChromeCachedModels().filter((m) => m.modelId !== modelId);\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n } catch {\n // Ignore tracking errors\n }\n}\n\n/** Refresh metadata (size, context length) for cached models that need it */\nexport async function refreshCachedModelSizes(): Promise<void> {\n try {\n const models = getChromeCachedModels();\n // Refresh if: no size, size < 1MB (wrong), or missing context length\n const MIN_EXPECTED_SIZE = 1_000_000; // 1MB - real models are always bigger\n const needsRefresh = models.filter(\n (m) => !m.sizeBytes || m.sizeBytes < MIN_EXPECTED_SIZE || !m.contextLength,\n );\n if (needsRefresh.length === 0) {\n return;\n }\n\n // Fetch metadata in parallel (max 3 at a time)\n const batchSize = 3;\n for (let i = 0; i < needsRefresh.length; i += batchSize) {\n const batch = needsRefresh.slice(i, i + batchSize);\n await Promise.all(\n batch.map(async (model) => {\n const [size, context] = await Promise.all([\n !model.sizeBytes || model.sizeBytes < MIN_EXPECTED_SIZE\n ? fetchModelSize(model.modelId)\n : Promise.resolve(undefined),\n model.contextLength ? Promise.resolve(undefined) : fetchContextLength(model.modelId),\n ]);\n if (size) {\n model.sizeBytes = size;\n }\n if (context) {\n model.contextLength = context;\n }\n }),\n );\n }\n\n // Save updated models\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n } catch {\n // Ignore refresh errors\n }\n}\n\n// Fixed port for local server - IndexedDB cache is origin-scoped, so using a\n// consistent port ensures the model cache persists between runs\n// Port 43724 = \"GERBI\" on phone keypad (GERBIL=437245 is too big for port range)\nconst GERBIL_LOCAL_PORT = 43_724;\n\n// Global singletons - multiple Gerbil instances share browser and server\nlet globalBrowser: Browser | null = null;\nlet globalBrowserPromise: Promise<Browser> | null = null;\nlet globalServer: Server | null = null;\nlet globalServerPort = 0;\nlet globalServerHtml = \"\"; // Current HTML content to serve\n\n// Page tracking for memory management\nlet activePagesCount = 0;\nconst MAX_CONCURRENT_PAGES = 5; // Limit to prevent dev mistakes\n\n// Track active backend instances for memory monitoring and cleanup\nconst activeBackends: Set<ChromeGPUBackend> = new Set();\n\n// ============================================\n// Types\n// ============================================\n\nexport type ChromeBackendOptions = {\n /** Custom Chrome executable path */\n chromePath?: string;\n /** Model ID to load */\n modelId?: string;\n /** Model context length (for KV cache management) */\n contextLength?: number;\n /** Whether this is a vision model (auto-detected if not specified) */\n isVision?: boolean;\n /** Progress callback */\n onProgress?: (info: { status: string; progress?: number; file?: string }) => void;\n /** Token callback for streaming */\n onToken?: (token: { text: string; state: string; numTokens: number; tps: number }) => void;\n};\n\nexport type GenerateOptions = {\n maxTokens?: number;\n temperature?: number;\n topP?: number;\n topK?: number;\n thinking?: boolean;\n system?: string;\n /** Images for vision models (URLs or data URIs) */\n images?: string[];\n onToken?: (token: { text: string; state: string; numTokens: number; tps: number }) => void;\n};\n\n// ============================================\n// Chrome Path Detection\n// ============================================\n\nconst CHROME_PATHS = {\n darwin: [\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\",\n \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\",\n ],\n linux: [\n \"google-chrome-stable\",\n \"google-chrome\",\n \"chromium-browser\",\n \"chromium\",\n \"microsoft-edge\",\n \"brave-browser\",\n ],\n win32: [\n \"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n \"C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n `${process.env.LOCALAPPDATA}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n \"C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\",\n \"C:\\\\Program Files\\\\BraveSoftware\\\\Brave-Browser\\\\Application\\\\brave.exe\",\n ],\n};\n\nfunction findChrome(): string {\n // Check env override first\n if (process.env.CHROME_PATH) {\n return process.env.CHROME_PATH;\n }\n\n const platform = process.platform as \"darwin\" | \"linux\" | \"win32\";\n const paths = CHROME_PATHS[platform] || [];\n\n for (const p of paths) {\n try {\n if (platform === \"linux\") {\n // For Linux, check if command exists in PATH\n execSync(`which ${p}`, { stdio: \"ignore\" });\n return p;\n }\n // For macOS/Windows, use fs.existsSync (portable)\n if (existsSync(p)) {\n return p;\n }\n } catch {}\n }\n\n throw new Error(\"Chrome not found. Install Chrome or set CHROME_PATH environment variable.\");\n}\n\n// ============================================\n// Chrome Launch Flags\n// ============================================\n\nfunction getChromeFlags(userDataDir: string, _debuggingPort: number): string[] {\n const flags = [\"--no-sandbox\", `--user-data-dir=${userDataDir}`];\n\n // Platform-specific WebGPU flags\n if (process.platform === \"linux\") {\n // Linux: use Vulkan backend for WebGPU\n flags.push(\n \"--enable-unsafe-webgpu\",\n \"--enable-features=Vulkan\",\n \"--use-angle=vulkan\",\n \"--disable-vulkan-surface\",\n );\n } else if (process.platform === \"darwin\") {\n // macOS: WebGPU uses Metal by default, minimal flags needed\n // Only add --enable-unsafe-webgpu if WebGPU is disabled (rare)\n // For now, try without it to avoid triggering GPU bugs\n } else {\n // Windows: use default DirectX/D3D12 backend\n flags.push(\"--enable-unsafe-webgpu\");\n }\n\n return flags;\n}\n\n// ============================================\n// Worker Page HTML\n// ============================================\n\nfunction getWorkerPageHTML(modelPath: string, contextLength = 32_768, isVision = false): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <title>Gerbil WebGPU Backend</title>\n <script type=\"module\">\n import {\n AutoTokenizer,\n AutoModelForCausalLM,\n AutoProcessor,\n AutoModelForImageTextToText,\n RawImage,\n TextStreamer,\n InterruptableStoppingCriteria,\n env,\n } from \"https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1\";\n\n // Enable IndexedDB caching (prevents re-downloading models)\n env.useBrowserCache = true;\n env.allowLocalModels = false;\n\n const IS_VISION = ${isVision};\n \n class ModelPipeline {\n static tokenizer = null;\n static processor = null;\n static model = null;\n static modelId = \"${modelPath}\";\n static isVision = IS_VISION;\n\n static async getInstance(progressCallback) {\n if (this.isVision) {\n // Vision model: use AutoProcessor + AutoModelForImageTextToText\n if (!this.processor) {\n this.processor = await AutoProcessor.from_pretrained(this.modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.model) {\n this.model = await AutoModelForImageTextToText.from_pretrained(this.modelId, {\n device: \"webgpu\",\n progress_callback: progressCallback,\n });\n }\n return { \n processor: this.processor, \n tokenizer: this.processor.tokenizer, \n model: this.model,\n isVision: true \n };\n } else {\n // Text model: use AutoTokenizer + AutoModelForCausalLM\n if (!this.tokenizer) {\n this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.model) {\n this.model = await AutoModelForCausalLM.from_pretrained(this.modelId, {\n dtype: \"q4f16\",\n device: \"webgpu\",\n progress_callback: progressCallback,\n });\n }\n return { \n tokenizer: this.tokenizer, \n model: this.model,\n isVision: false \n };\n }\n }\n }\n\n const stoppingCriteria = new InterruptableStoppingCriteria();\n let pastKeyValuesCache = null;\n let totalTokensInCache = 0;\n \n // Context length for auto-reset (passed from model config)\n const CONTEXT_LENGTH = ${contextLength};\n\n // Auto-load model on page init\n (async function() {\n console.log(JSON.stringify({ type: \"progress\", status: IS_VISION ? \"Loading vision model...\" : \"Loading model...\" }));\n \n try {\n const result = await ModelPipeline.getInstance((progress) => {\n if (progress.status === \"progress\" && progress.file) {\n console.log(JSON.stringify({\n type: \"progress\",\n status: \"progress\",\n file: progress.file,\n progress: Math.round(progress.progress || 0),\n }));\n }\n });\n\n console.log(JSON.stringify({ type: \"progress\", status: \"Compiling shaders...\" }));\n \n // Warmup generation to compile shaders and initialize model\n // Always do text warmup first\n const textWarmupInputs = result.tokenizer(\"hello\");\n await result.model.generate({ ...textWarmupInputs, max_new_tokens: 1 });\n \n // Vision models also need vision warmup\n if (result.isVision) {\n console.log(JSON.stringify({ type: \"progress\", status: \"Warming up vision encoder...\" }));\n try {\n // Create a tiny 8x8 red test image\n const canvas = new OffscreenCanvas(8, 8);\n const ctx = canvas.getContext('2d');\n ctx.fillStyle = 'red';\n ctx.fillRect(0, 0, 8, 8);\n const blob = await canvas.convertToBlob({ type: 'image/png' });\n const warmupImage = await RawImage.fromBlob(blob);\n \n // Process with vision pipeline\n const warmupContent = [{ type: \"image\" }, { type: \"text\", text: \"hi\" }];\n const warmupMessages = [{ role: \"user\", content: warmupContent }];\n const warmupPrompt = result.processor.apply_chat_template(warmupMessages);\n const warmupInputs = await result.processor(warmupImage, warmupPrompt, { add_special_tokens: false });\n \n // Run vision warmup generation\n await result.model.generate({\n ...warmupInputs,\n max_new_tokens: 1,\n do_sample: false,\n });\n } catch {\n // Vision warmup failed, text warmup was done so continue\n }\n }\n \n // Set page title to model ID for cross-process identification\n document.title = \"Gerbil: \" + ModelPipeline.modelId;\n \n console.log(JSON.stringify({ type: \"ready\", isVision: result.isVision }));\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n }\n })();\n\n // Text generation (for non-vision models or vision without images)\n window.gerbilGenerate = async function(messages, options = {}) {\n const { maxTokens = 256, temperature = 0.7, topP = 0.9, topK = 20, thinking = false, images = [] } = options;\n \n const result = await ModelPipeline.getInstance();\n \n // Route to vision generation if we have images and this is a vision model\n if (images.length > 0 && result.isVision) {\n return window.gerbilGenerateVision(messages, images, options);\n }\n \n // Auto-reset KV cache if it exceeds context length\n if (totalTokensInCache > CONTEXT_LENGTH) {\n console.log(JSON.stringify({ \n type: \"cache_reset\", \n reason: \"context_exceeded\",\n tokensInCache: totalTokensInCache,\n contextLength: CONTEXT_LENGTH\n }));\n pastKeyValuesCache = null;\n totalTokensInCache = 0;\n }\n\n try {\n const { tokenizer, model } = result;\n\n const inputs = tokenizer.apply_chat_template(messages, {\n add_generation_prompt: true,\n return_dict: true,\n enable_thinking: thinking,\n });\n\n let state = \"answering\";\n let prevState = \"answering\";\n const [START_THINKING_TOKEN_ID, END_THINKING_TOKEN_ID] = tokenizer.encode(\n \"<think></think>\",\n { add_special_tokens: false }\n );\n\n let startTime = null;\n let numTokens = 0;\n\n const tokenCallback = (tokens) => {\n startTime ??= performance.now();\n numTokens += 1;\n \n const tokenId = Number(tokens[0]);\n if (tokenId === START_THINKING_TOKEN_ID) {\n state = \"thinking\";\n } else if (tokenId === END_THINKING_TOKEN_ID) {\n state = \"answering\";\n }\n };\n\n const streamCallback = (text) => {\n const tps = startTime ? (numTokens / (performance.now() - startTime)) * 1000 : 0;\n \n let outputText = text;\n if (thinking) {\n if (state === \"thinking\" && prevState !== \"thinking\") {\n outputText = \"<think>\" + text;\n } else if (state === \"answering\" && prevState === \"thinking\") {\n outputText = \"</think>\" + text;\n }\n }\n prevState = state;\n \n console.log(JSON.stringify({ type: \"token\", text: outputText, state, numTokens, tps }));\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n token_callback_function: tokenCallback,\n });\n\n console.log(JSON.stringify({ type: \"start\" }));\n\n const { past_key_values, sequences } = await model.generate({\n ...inputs,\n past_key_values: pastKeyValuesCache,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n max_new_tokens: maxTokens,\n streamer,\n stopping_criteria: stoppingCriteria,\n return_dict_in_generate: true,\n });\n\n pastKeyValuesCache = past_key_values;\n \n const inputLength = inputs.input_ids.dims[1];\n totalTokensInCache += inputLength + numTokens;\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n \n const generatedTokens = sequences.slice(null, [inputLength, null]);\n const decoded = tokenizer.batch_decode(generatedTokens, { skip_special_tokens: true });\n\n console.log(JSON.stringify({\n type: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n tokensInCache: totalTokensInCache,\n }));\n\n return decoded[0] || \"\";\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n throw error;\n }\n };\n\n // Vision generation (for vision models with images)\n window.gerbilGenerateVision = async function(messages, imageUrls, options = {}) {\n const { maxTokens = 2048, temperature = 0.7, topP = 0.9, topK = 20 } = options;\n\n try {\n const { processor, tokenizer, model } = await ModelPipeline.getInstance();\n\n // Build message content with image placeholders for the user prompt\n const lastMessage = messages[messages.length - 1];\n const content = [];\n for (let i = 0; i < imageUrls.length; i += 1) {\n content.push({ type: \"image\" });\n }\n content.push({ type: \"text\", text: lastMessage.content });\n\n // For vision models, include a brief system instruction for concise responses\n const visionMessages = [\n { role: \"system\", content: \"You are a helpful assistant. Be concise and direct in your responses.\" },\n { role: \"user\", content }\n ];\n\n // Apply chat template with generation prompt\n const chatPrompt = processor.apply_chat_template(visionMessages, { \n add_generation_prompt: true \n });\n\n // Load images\n console.log(JSON.stringify({ type: \"progress\", status: \"Loading images...\" }));\n const loadedImages = await Promise.all(\n imageUrls.map(url => RawImage.fromURL(url))\n );\n\n // Process inputs\n const inputs = await processor(\n loadedImages.length === 1 ? loadedImages[0] : loadedImages,\n chatPrompt,\n { add_special_tokens: false }\n );\n\n let startTime = null;\n let numTokens = 0;\n\n const streamCallback = (text) => {\n startTime ??= performance.now();\n numTokens += 1;\n const tps = (numTokens / (performance.now() - startTime)) * 1000;\n console.log(JSON.stringify({ type: \"token\", text, state: \"answering\", numTokens, tps }));\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n });\n\n console.log(JSON.stringify({ type: \"start\" }));\n\n const outputs = await model.generate({\n ...inputs,\n max_new_tokens: maxTokens,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n streamer,\n stopping_criteria: stoppingCriteria,\n });\n\n // Decode output (skip prompt)\n const inputLength = inputs.input_ids.dims?.at(-1) || 0;\n const decoded = processor.batch_decode(\n outputs.slice(null, [inputLength, null]),\n { skip_special_tokens: true }\n );\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n\n console.log(JSON.stringify({\n type: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n }));\n\n return decoded[0] || \"\";\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n throw error;\n }\n };\n\n window.gerbilInterrupt = function() {\n stoppingCriteria.interrupt();\n };\n\n window.gerbilReset = function() {\n pastKeyValuesCache = null;\n totalTokensInCache = 0;\n stoppingCriteria.reset();\n console.log(JSON.stringify({ type: \"cache_reset\", reason: \"manual\" }));\n };\n\n // Signal that the page is ready for commands\n console.log(JSON.stringify({ type: \"init\" }));\n </script>\n</head>\n<body>\n <h1>Gerbil WebGPU Backend</h1>\n <p>This page provides WebGPU inference for the Gerbil CLI.</p>\n</body>\n</html>\n`;\n}\n\n// ============================================\n// Chrome GPU Backend\n// ============================================\n\nexport class ChromeGPUBackend {\n private browser: Browser | null = null;\n private page: Page | null = null;\n private cdp: CDPSession | null = null;\n private server: Server | null = null;\n private serverPort = 0;\n private userDataDir: string = GERBIL_CACHE_DIR; // Always use shared cache\n private readonly modelId: string;\n private isReady = false;\n private readonly isVisionModel: boolean = false; // Whether this is a vision model\n private readonly messageHandlers: Map<string, (data: any) => void> = new Map();\n private pendingRejects: Array<(err: Error) => void> = []; // Track pending waits for cleanup\n\n private constructor(modelId: string, isVision = false) {\n this.modelId = modelId;\n this.isVisionModel = isVision;\n }\n\n /**\n * Create and initialize a Chrome GPU backend\n */\n static async create(options: ChromeBackendOptions = {}): Promise<ChromeGPUBackend> {\n const modelId = options.modelId || \"onnx-community/Qwen3-0.6B-ONNX\";\n\n // Detect if this is a vision model (can be overridden via options)\n const isVision = options.isVision ?? ChromeGPUBackend.detectVisionModel(modelId);\n\n const backend = new ChromeGPUBackend(modelId, isVision);\n await backend.launch(options);\n return backend;\n }\n\n /**\n * Detect if a model is a vision model based on its ID\n */\n private static detectVisionModel(modelId: string): boolean {\n const visionModelPatterns = [\n /ministral/i,\n /pixtral/i,\n /llava/i,\n /vision/i,\n /vl/i,\n /image-text/i,\n /multimodal/i,\n ];\n return visionModelPatterns.some((pattern) => pattern.test(modelId));\n }\n\n /**\n * Check if this backend is for a vision model\n */\n isVision(): boolean {\n return this.isVisionModel;\n }\n\n /**\n * Clean up orphan Gerbil pages from previous sessions\n * These are pages that were left behind when process exited without proper cleanup\n */\n private async cleanupOrphanPages(\n browser: Browser,\n options: ChromeBackendOptions,\n ): Promise<number> {\n try {\n const pages = await browser.pages();\n const gerbilPages = pages.filter((p) => {\n const url = p.url();\n // Match our worker pages by URL pattern (127.0.0.1:PORT where PORT > 40000)\n return /127\\.0\\.0\\.1:4\\d{4}/.test(url);\n });\n\n // Only clean up if there are orphan pages not tracked by this process\n const orphanCount = gerbilPages.length - activeBackends.size;\n if (orphanCount > 0) {\n options.onProgress?.({ status: `Cleaning up ${orphanCount} orphan page(s)...` });\n\n for (const page of gerbilPages) {\n // Check if this page is owned by a current backend\n let isOwned = false;\n for (const backend of activeBackends) {\n if (backend.page === page) {\n isOwned = true;\n break;\n }\n }\n\n // Close unowned pages\n if (!isOwned) {\n try {\n await page.close();\n } catch {\n // Page may already be closed\n }\n }\n }\n }\n\n return orphanCount;\n } catch {\n return 0;\n }\n }\n\n /**\n * Get existing browser or launch a new one (singleton pattern)\n * Multiple Gerbil instances share the same browser process\n */\n private async getOrCreateBrowser(\n chromePath: string,\n options: ChromeBackendOptions,\n ): Promise<Browser> {\n // If we already have a global browser, reuse it\n if (globalBrowser?.connected) {\n options.onProgress?.({ status: \"Reusing existing Chrome...\" });\n // Clean up orphan pages from previous sessions\n await this.cleanupOrphanPages(globalBrowser, options);\n return globalBrowser;\n }\n\n // If another caller is launching, wait for them\n if (globalBrowserPromise) {\n options.onProgress?.({ status: \"Waiting for Chrome startup...\" });\n return globalBrowserPromise;\n }\n\n // Try to connect to existing browser via saved WebSocket endpoint\n if (existsSync(WS_ENDPOINT_FILE)) {\n try {\n const wsEndpoint = readFileSync(WS_ENDPOINT_FILE, \"utf-8\").trim();\n options.onProgress?.({ status: \"Connecting to existing Chrome...\" });\n globalBrowser = await puppeteer.connect({\n browserWSEndpoint: wsEndpoint,\n });\n // Clean up orphan pages from previous sessions\n await this.cleanupOrphanPages(globalBrowser, options);\n return globalBrowser;\n } catch {\n // Stale endpoint, remove it and launch fresh\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n }\n }\n\n // Launch new browser\n globalBrowserPromise = this.launchBrowser(chromePath, options);\n try {\n globalBrowser = await globalBrowserPromise;\n return globalBrowser;\n } finally {\n globalBrowserPromise = null;\n }\n }\n\n /**\n * Launch a new Chrome browser instance\n */\n private async launchBrowser(\n chromePath: string,\n _options: ChromeBackendOptions,\n ): Promise<Browser> {\n const debuggingPort = 9222 + Math.floor(Math.random() * 1000);\n\n // Clean up stale lock file if Chrome crashed\n const lockFile = join(this.userDataDir, \"SingletonLock\");\n if (existsSync(lockFile)) {\n try {\n unlinkSync(lockFile);\n await new Promise((r) => setTimeout(r, 200));\n } catch {}\n }\n\n // Use new headless mode - more compatible with WebGPU than old headless\n // Previous crashes were caused by killing our own server, not headless mode\n const browser = await puppeteer.launch({\n executablePath: chromePath,\n headless: true, // Standard headless mode - crashes before were from killing our own server\n args: [\n ...getChromeFlags(this.userDataDir, debuggingPort),\n \"--enable-gpu\",\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-background-timer-throttling\",\n \"--disable-renderer-backgrounding\",\n \"--disable-dev-shm-usage\",\n ],\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n });\n\n // Save WebSocket endpoint for reconnection\n writeFileSync(WS_ENDPOINT_FILE, browser.wsEndpoint());\n\n // Clean up endpoint file when browser closes\n browser.on(\"disconnected\", () => {\n globalBrowser = null;\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n });\n\n return browser;\n }\n\n /**\n * Launch Chrome and initialize the worker page\n */\n private async launch(options: ChromeBackendOptions): Promise<void> {\n // Check page limit to prevent memory issues during development\n if (activePagesCount >= MAX_CONCURRENT_PAGES) {\n throw new Error(\n `Maximum concurrent pages (${MAX_CONCURRENT_PAGES}) reached. ` +\n \"Call dispose() on old Gerbil instances to free resources. \" +\n `Currently active: ${activePagesCount}`,\n );\n }\n\n const chromePath = options.chromePath || findChrome();\n\n // Use persistent cache directory (keeps model downloads between runs)\n this.userDataDir = GERBIL_CACHE_DIR;\n if (!existsSync(this.userDataDir)) {\n mkdirSync(this.userDataDir, { recursive: true });\n }\n\n // Start tiny HTTP server to serve the worker page\n // (Required because file:// + ES modules + CDN imports doesn't work due to CORS)\n const contextLength = options.contextLength || 32_768; // Default to 32K if not specified\n const html = getWorkerPageHTML(this.modelId, contextLength, this.isVisionModel);\n await this.startServer(html);\n\n options.onProgress?.({ status: \"Starting Chrome...\" });\n\n // Get or create the shared browser instance\n this.browser = await this.getOrCreateBrowser(chromePath, options);\n\n // Create page and set up CDP session\n this.page = await this.browser.newPage();\n this.cdp = await this.page.createCDPSession();\n\n // Increment active page counter and register this backend\n activePagesCount += 1;\n activeBackends.add(this);\n options.onProgress?.({ status: `Active pages: ${activePagesCount}/${MAX_CONCURRENT_PAGES}` });\n\n // Listen for browser disconnect (OOM kill, crash, etc.)\n this.browser.on(\"disconnected\", () => {\n this.isReady = false;\n this.browser = null;\n this.page = null;\n this.cdp = null;\n // Fail fast: reject any pending waits so callers don't hang\n this.rejectPendingWaits(new Error(\"CHROME_DISCONNECTED\"));\n });\n\n // Enable console API events and exceptions\n await this.cdp.send(\"Runtime.enable\");\n await this.cdp.send(\"Runtime.setAsyncCallStackDepth\", { maxDepth: 32 });\n\n // Set up console message handler\n this.cdp.on(\"Runtime.consoleAPICalled\", (event) => {\n const text = event.args.map((a: any) => a.value || a.description || \"\").join(\" \");\n\n if (event.type === \"log\" && event.args[0]?.value) {\n try {\n const data = JSON.parse(event.args[0].value);\n this.handleMessage(data, options);\n } catch {\n // Not JSON - only log short messages, skip large data dumps (like KV cache)\n if (\n text.length < 500 &&\n !text.includes(\"Float32Array\") &&\n !text.includes(\"past_key_values\")\n ) {\n // Uncomment for debugging: console.log(\"[Chrome Log]\", text);\n }\n }\n } else if (event.type === \"error\" || event.type === \"warning\") {\n // Filter out noisy messages\n if (\n !(\n text.includes(\"onnxruntime\") ||\n text.includes(\"content-length\") ||\n text.includes(\"Float32Array\") ||\n text.includes(\"past_key_values\")\n ) &&\n text.length < 1000\n ) {\n }\n }\n });\n\n // Listen for exceptions\n this.cdp.on(\"Runtime.exceptionThrown\", (event) => {\n const errText =\n event.exceptionDetails?.text || event.exceptionDetails?.exception?.description || \"\";\n // Skip noisy tensor/KV cache dumps\n if (\n errText.includes(\"Float32Array\") ||\n errText.includes(\"past_key_values\") ||\n errText.length > 1000\n ) {\n return;\n }\n });\n\n // Navigate to our HTTP server - model loads automatically\n await this.page.goto(`http://127.0.0.1:${this.serverPort}/`, {\n waitUntil: \"domcontentloaded\",\n timeout: 30_000,\n });\n\n // Wait for model to be ready (loads automatically on page init)\n await this.waitForMessage(\"ready\", 300_000); // 5 min timeout for model download\n\n this.isReady = true;\n options.onProgress?.({ status: \"Ready (WebGPU)!\" });\n\n // Track this model as cached\n trackCachedModel(this.modelId);\n }\n\n /**\n * Handle incoming messages from the page\n */\n private handleMessage(data: any, options: ChromeBackendOptions): void {\n const { type, ...rest } = data;\n\n // Call registered handler\n const handler = this.messageHandlers.get(type);\n if (handler) {\n handler(rest);\n }\n\n // Also call option callbacks\n if (type === \"progress\") {\n options.onProgress?.(rest);\n } else if (type === \"token\") {\n options.onToken?.(rest);\n }\n }\n\n /**\n * Wait for a specific message type\n */\n private waitForMessage(type: string, timeout = 30_000): Promise<any> {\n return new Promise((resolve, reject) => {\n // Track this reject for cleanup on browser disconnect\n this.pendingRejects.push(reject);\n\n const cleanup = () => {\n clearTimeout(timer);\n this.messageHandlers.delete(type);\n const idx = this.pendingRejects.indexOf(reject);\n if (idx >= 0) {\n this.pendingRejects.splice(idx, 1);\n }\n };\n\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timeout waiting for ${type} message`));\n }, timeout);\n\n this.messageHandlers.set(type, (data) => {\n cleanup();\n resolve(data);\n });\n });\n }\n\n /**\n * Check if Chrome backend is still alive\n */\n isAlive(): boolean {\n return this.isReady && this.browser !== null && this.page !== null;\n }\n\n /**\n * Get Chrome backend status information\n */\n getStatus(): { pid: number | null; port: number; modelId: string; startedAt: Date | null } {\n // Try instance browser first, then global browser\n let pid: number | null = null;\n const browserProcess = this.browser?.process?.() || globalBrowser?.process?.();\n if (browserProcess?.pid) {\n pid = browserProcess.pid;\n }\n return {\n pid,\n port: this.serverPort || globalServerPort,\n modelId: this.modelId,\n startedAt: this.isReady ? new Date() : null,\n };\n }\n\n /**\n * Get Chrome memory usage via CDP Performance metrics\n * Returns memory in bytes or null if unavailable\n */\n async getMemoryUsage(): Promise<{ jsHeapUsed: number; jsHeapTotal: number } | null> {\n if (!(this.cdp && this.isReady)) {\n return null;\n }\n\n try {\n // Enable Performance domain if needed\n await this.cdp.send(\"Performance.enable\");\n\n const { metrics } = (await this.cdp.send(\"Performance.getMetrics\")) as {\n metrics: Array<{ name: string; value: number }>;\n };\n\n const jsHeapUsed = metrics.find((m) => m.name === \"JSHeapUsedSize\")?.value ?? 0;\n const jsHeapTotal = metrics.find((m) => m.name === \"JSHeapTotalSize\")?.value ?? 0;\n\n return { jsHeapUsed, jsHeapTotal };\n } catch {\n return null;\n }\n }\n\n /**\n * Check memory usage and auto-cleanup if threshold exceeded\n * @param thresholdGB Memory threshold in GB (default: 8)\n * @returns true if cleanup was performed\n */\n async checkMemoryAndCleanup(thresholdGB = 8): Promise<boolean> {\n const mem = await this.getMemoryUsage();\n if (!mem) {\n return false;\n }\n\n const usedGB = mem.jsHeapUsed / 1024 ** 3;\n\n if (usedGB > thresholdGB) {\n await this.reset();\n return true;\n }\n\n return false;\n }\n\n /**\n * Get memory usage in a human-readable format\n */\n async getMemoryStats(): Promise<{ usedGB: number; totalGB: number; usedPercent: number } | null> {\n const mem = await this.getMemoryUsage();\n if (!mem) {\n return null;\n }\n\n const usedGB = mem.jsHeapUsed / 1024 ** 3;\n const totalGB = mem.jsHeapTotal / 1024 ** 3;\n const usedPercent = (mem.jsHeapUsed / mem.jsHeapTotal) * 100;\n\n return { usedGB, totalGB, usedPercent };\n }\n\n /**\n * Generate text with streaming\n */\n async generate(prompt: string, options: GenerateOptions = {}): Promise<string> {\n if (!this.isAlive()) {\n throw new Error(\"CHROME_BACKEND_DEAD\");\n }\n\n const system = options.system || \"You are a helpful assistant.\";\n const messages = [\n { role: \"system\", content: system },\n { role: \"user\", content: prompt },\n ];\n\n const genOptions = {\n maxTokens: options.maxTokens ?? (this.isVisionModel ? 2048 : 256),\n temperature: options.temperature ?? 0.7,\n topP: options.topP ?? 0.9,\n topK: options.topK ?? 20,\n thinking: options.thinking ?? false,\n images: options.images ?? [], // Pass images for vision models\n };\n\n // Set up token handler if callback provided\n if (options.onToken) {\n this.messageHandlers.set(\"token\", options.onToken);\n }\n\n try {\n // Start generation\n const resultPromise = this.page?.evaluate(\n (msgs, opts) => (window as any).gerbilGenerate(msgs, opts),\n messages,\n genOptions,\n );\n\n // Wait for completion\n const completeData = await this.waitForMessage(\"complete\", 600_000); // 10 min timeout\n\n // Clean up token handler\n this.messageHandlers.delete(\"token\");\n\n await resultPromise; // Ensure evaluate completes\n\n return completeData.text || \"\";\n } catch (err: any) {\n // Check if Chrome died during generation\n if (!this.isAlive()) {\n throw new Error(\"CHROME_BACKEND_DEAD\");\n }\n throw err;\n }\n }\n\n /**\n * Interrupt current generation\n */\n async interrupt(): Promise<void> {\n if (this.page) {\n await this.page.evaluate(\"window.gerbilInterrupt()\");\n }\n }\n\n /**\n * Reset conversation cache\n */\n async reset(): Promise<void> {\n if (this.page) {\n await this.page.evaluate(\"window.gerbilReset()\");\n }\n }\n\n /**\n * Check if backend is ready\n */\n ready(): boolean {\n return this.isReady;\n }\n\n /**\n * Start or reuse the global HTTP server\n * Uses singleton pattern to prevent killing our own server\n * Updates HTML content for new model loads\n */\n private async startServer(html: string): Promise<void> {\n // Always update the HTML content for new model loads\n globalServerHtml = html;\n\n // If global server is already running, reuse it (with updated HTML)\n if (globalServer && globalServerPort) {\n this.server = globalServer;\n this.serverPort = globalServerPort;\n return;\n }\n\n return new Promise((resolve, reject) => {\n const server = createServer((_req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(globalServerHtml); // Serve current HTML (not captured at creation time)\n });\n\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n // Port in use - assume it's our server from a previous run\n // Just use that port (Chrome will connect to existing server)\n this.serverPort = GERBIL_LOCAL_PORT;\n globalServerPort = GERBIL_LOCAL_PORT;\n resolve();\n } else {\n reject(err);\n }\n });\n\n // Listen on fixed port for consistent IndexedDB origin (cache persistence)\n server.listen(GERBIL_LOCAL_PORT, \"127.0.0.1\", () => {\n this.server = server;\n this.serverPort = GERBIL_LOCAL_PORT;\n globalServer = server;\n globalServerPort = GERBIL_LOCAL_PORT;\n resolve();\n });\n });\n }\n\n /**\n * Dispose of the backend and clean up\n * Note: We keep the shared browser running for other backends\n * @param disconnect If true, also disconnect from shared browser (for clean script exit)\n */\n async dispose(disconnect = false): Promise<void> {\n // Mark as not ready first to prevent new operations\n this.isReady = false;\n\n // Clear pending waits silently (don't reject - just clear them)\n this.pendingRejects = [];\n this.messageHandlers.clear();\n\n // Detach CDP session first\n if (this.cdp) {\n try {\n await this.cdp.detach();\n } catch {\n // CDP may already be detached\n }\n this.cdp = null;\n }\n\n // Close our page (but NOT the shared browser - other backends may use it)\n if (this.page) {\n try {\n // Navigate away first to ensure page is \"dead\"\n await this.page.goto(\"about:blank\").catch(() => {});\n\n // Small delay for navigation\n await new Promise((r) => setTimeout(r, 50));\n\n // Now close\n await this.page.close({ runBeforeUnload: false });\n\n // Decrement active page counter\n activePagesCount = Math.max(0, activePagesCount - 1);\n } catch {\n // Ignore close errors\n }\n this.page = null;\n }\n\n // Unregister from active backends\n activeBackends.delete(this);\n\n // Clear our references first\n this.browser = null;\n this.server = null;\n\n // Small delay to ensure page close completes before disconnect\n if (disconnect) {\n await new Promise((r) => setTimeout(r, 100));\n }\n\n // If requested and we're the last backend, disconnect from browser\n // This allows clean script exit without killing browser for other processes\n if (disconnect && activeBackends.size === 0 && globalBrowser) {\n try {\n globalBrowser.disconnect();\n globalBrowser = null;\n globalBrowserPromise = null;\n } catch {\n // Browser may already be disconnected\n }\n }\n }\n\n /**\n * Reject all pending waits (called on browser disconnect or dispose)\n */\n private rejectPendingWaits(error: Error): void {\n for (const reject of this.pendingRejects) {\n reject(error);\n }\n this.pendingRejects = [];\n this.messageHandlers.clear();\n }\n\n /**\n * Clear the model cache (forces re-download on next start)\n */\n static clearCache(): void {\n if (existsSync(GERBIL_CACHE_DIR)) {\n rmSync(GERBIL_CACHE_DIR, { recursive: true, force: true });\n }\n }\n\n /**\n * Get the number of active Chrome pages\n */\n static getActivePageCount(): number {\n return activePagesCount;\n }\n\n /**\n * Get memory usage info for all active pages\n */\n static getMemoryInfo(): { activePagesCount: number; maxPages: number } {\n return {\n activePagesCount,\n maxPages: MAX_CONCURRENT_PAGES,\n };\n }\n\n /**\n * Get global browser status (even if no active backends)\n */\n static getGlobalBrowserStatus(): {\n running: boolean;\n pid: number | null;\n port: number;\n activePagesCount: number;\n maxPages: number;\n wsEndpoint: string | null;\n } {\n let pid: number | null = null;\n let wsEndpoint: string | null = null;\n\n if (globalBrowser?.connected) {\n const browserProcess = globalBrowser.process?.();\n if (browserProcess?.pid) {\n pid = browserProcess.pid;\n }\n wsEndpoint = globalBrowser.wsEndpoint();\n }\n\n return {\n running: globalBrowser?.connected ?? false,\n pid,\n port: globalServerPort,\n activePagesCount, // This process only\n maxPages: MAX_CONCURRENT_PAGES,\n wsEndpoint,\n };\n }\n\n /**\n * Get total page count from Chrome (all processes)\n */\n static async getTotalPageCount(): Promise<number> {\n if (!globalBrowser?.connected) {\n return 0;\n }\n\n try {\n const pages = await globalBrowser.pages();\n // Count only Gerbil pages\n return pages.filter((p) => {\n const url = p.url();\n return url.includes(`127.0.0.1:${globalServerPort}`);\n }).length;\n } catch {\n return 0;\n }\n }\n\n /**\n * Get all active backends with their memory usage (this process only)\n */\n static async getAllBackendsInfo(): Promise<\n Array<{\n modelId: string;\n isVision: boolean;\n isReady: boolean;\n memory: { usedGB: number; totalGB: number; usedPercent: number } | null;\n }>\n > {\n const results = [];\n\n for (const backend of activeBackends) {\n const mem = await backend.getMemoryStats();\n results.push({\n modelId: backend.modelId,\n isVision: backend.isVisionModel,\n isReady: backend.isReady,\n memory: mem,\n });\n }\n\n return results;\n }\n\n /**\n * Get ALL pages in Chrome browser (cross-process visibility)\n * This shows pages from ALL Gerbil processes sharing the browser\n */\n static async getAllChromePages(): Promise<\n Array<{\n url: string;\n title: string;\n isOurs: boolean; // true if this process owns it\n modelId: string | null; // extracted from URL/title if possible\n memory: { usedGB: number; totalGB: number } | null;\n }>\n > {\n if (!globalBrowser?.connected) {\n return [];\n }\n\n try {\n const pages = await globalBrowser.pages();\n\n const results = [];\n for (const page of pages) {\n const url = page.url();\n const title = await page.title().catch(() => \"\");\n\n // Skip about:blank and non-gerbil pages\n if (url === \"about:blank\" || !url.includes(`127.0.0.1:${globalServerPort}`)) {\n continue;\n }\n\n // Check if we own this page\n let modelId: string | null = null;\n let isOurs = false;\n let memory: { usedGB: number; totalGB: number } | null = null;\n\n for (const backend of activeBackends) {\n if (backend.page === page) {\n isOurs = true;\n modelId = backend.modelId;\n // Get memory from our backend\n const mem = await backend.getMemoryStats();\n if (mem) {\n memory = { usedGB: mem.usedGB, totalGB: mem.totalGB };\n }\n break;\n }\n }\n\n // For pages we don't own, extract model ID from title and try to get memory\n if (!isOurs) {\n if (title.startsWith(\"Gerbil: \")) {\n modelId = title.replace(\"Gerbil: \", \"\");\n }\n\n // Try to get memory for other sessions via CDP\n try {\n const cdp = await page.createCDPSession();\n await cdp.send(\"Performance.enable\");\n const { metrics } = (await cdp.send(\"Performance.getMetrics\")) as {\n metrics: Array<{ name: string; value: number }>;\n };\n const jsHeapUsed = metrics.find((m) => m.name === \"JSHeapUsedSize\")?.value ?? 0;\n const jsHeapTotal = metrics.find((m) => m.name === \"JSHeapTotalSize\")?.value ?? 0;\n memory = {\n usedGB: jsHeapUsed / 1024 ** 3,\n totalGB: jsHeapTotal / 1024 ** 3,\n };\n await cdp.detach();\n } catch {\n // Can't get memory for this page\n }\n }\n\n results.push({\n url,\n title: title || \"Gerbil WebGPU Backend\",\n isOurs,\n modelId,\n memory,\n });\n }\n\n return results;\n } catch {\n return [];\n }\n }\n\n /**\n * Kill a Chrome page by index (works cross-process)\n */\n static async killPageByIndex(index: number): Promise<boolean> {\n if (!globalBrowser?.connected) {\n return false;\n }\n\n try {\n const pages = await globalBrowser.pages();\n // Filter to only Gerbil pages\n const gerbilPages = pages.filter((p) => {\n const url = p.url();\n return url.includes(`127.0.0.1:${globalServerPort}`);\n });\n\n if (index < 0 || index >= gerbilPages.length) {\n return false;\n }\n\n const page = gerbilPages[index];\n\n // If it's our page, use dispose() for proper cleanup\n for (const backend of activeBackends) {\n if (backend.page === page) {\n await backend.dispose();\n return true;\n }\n }\n\n // Otherwise just close the page directly\n await page.close();\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Kill a specific backend by index (this process only)\n */\n static async killBackendByIndex(index: number): Promise<boolean> {\n const backends = [...activeBackends];\n if (index < 0 || index >= backends.length) {\n return false;\n }\n\n const backend = backends[index];\n try {\n await backend.dispose();\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Force kill all backends (for zombie cleanup)\n */\n static async killAllBackends(): Promise<{ pagesKilled: number; browserKilled: boolean }> {\n const count = activeBackends.size;\n\n // Dispose all active backends\n for (const backend of [...activeBackends]) {\n try {\n await backend.dispose();\n } catch {\n // Ignore errors during cleanup\n }\n }\n activeBackends.clear();\n\n // Close shared browser\n let browserKilled = false;\n if (globalBrowser) {\n try {\n await globalBrowser.close();\n browserKilled = true;\n } catch {\n // Browser may already be closed\n }\n globalBrowser = null;\n globalBrowserPromise = null;\n }\n\n // Close server\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n globalServerPort = 0;\n }\n\n activePagesCount = 0;\n\n // Clean up endpoint file\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n\n return { pagesKilled: count, browserKilled };\n }\n\n /**\n * Gracefully close the shared browser (call on process exit)\n */\n static async closeSharedBrowser(): Promise<void> {\n if (globalBrowser) {\n try {\n await globalBrowser.close();\n } catch {\n // Browser may already be closed\n }\n globalBrowser = null;\n globalBrowserPromise = null;\n }\n\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n globalServerPort = 0;\n }\n\n // Reset page counter\n activePagesCount = 0;\n\n // Clean up endpoint file\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n }\n}\n\n// Register cleanup on process exit to prevent mutex errors\nlet cleanupRegistered = false;\nfunction registerCleanup() {\n if (cleanupRegistered) {\n return;\n }\n cleanupRegistered = true;\n\n const cleanup = () => {\n // Synchronous close - can't await in exit handlers\n if (globalBrowser) {\n try {\n // Force kill the browser process\n const browserProcess = globalBrowser.process();\n if (browserProcess) {\n browserProcess.kill(\"SIGTERM\");\n }\n } catch {}\n globalBrowser = null;\n }\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n }\n };\n\n process.on(\"exit\", cleanup);\n process.on(\"SIGINT\", () => {\n cleanup();\n process.exit(0);\n });\n process.on(\"SIGTERM\", () => {\n cleanup();\n process.exit(0);\n });\n}\n\n// Auto-register when module loads\nregisterCleanup();\n\nexport default ChromeGPUBackend;\n"],"mappings":"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"}
1
+ {"version":3,"file":"chrome-backend-CORwaIyC.mjs","names":["files: { path: string; size?: number; lfs?: { size?: number } }[]","globalBrowser: Browser | null","globalBrowserPromise: Promise<Browser> | null","globalServer: Server | null","activeBackends: Set<ChromeGPUBackend>","pid: number | null","err: any","wsEndpoint: string | null","modelId: string | null","memory: { usedGB: number; totalGB: number } | null"],"sources":["../src/core/chrome-backend.ts"],"sourcesContent":["/**\n * Chrome DevTools Protocol Backend for WebGPU Inference\n *\n * Uses headless Chrome as a WebGPU accelerator for Node.js environments.\n * Provides the same performance as browser inference (~100+ tok/s with q4f16).\n */\n\nimport { execSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { createServer, type Server } from \"node:http\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport puppeteer, { type Browser, type CDPSession, type Page } from \"puppeteer-core\";\n\n// Persistent cache directory for Chrome profile (keeps model cache between runs)\nconst GERBIL_CACHE_DIR = join(homedir(), \".gerbil\", \"chrome-cache\");\nconst WS_ENDPOINT_FILE = join(GERBIL_CACHE_DIR, \"ws-endpoint.txt\");\nconst CACHED_MODELS_FILE = join(homedir(), \".gerbil\", \"cached-models.json\");\n\n// ============================================\n// Cached Models Tracking\n// ============================================\n\ntype CachedModelEntry = {\n modelId: string;\n downloadedAt: string;\n lastUsed: string;\n sizeBytes?: number;\n contextLength?: number;\n};\n\n/** Get list of models cached in Chrome's IndexedDB */\nexport function getChromeCachedModels(): CachedModelEntry[] {\n try {\n if (!existsSync(CACHED_MODELS_FILE)) {\n return [];\n }\n const data = JSON.parse(readFileSync(CACHED_MODELS_FILE, \"utf-8\"));\n return data.models || [];\n } catch {\n return [];\n }\n}\n\n/** Fetch model context length from HuggingFace (config.json preferred for actual limit) */\nasync function fetchContextLength(modelId: string): Promise<number | undefined> {\n // Priority 1: config.json → max_position_embeddings (actual architectural limit)\n try {\n const res = await fetch(`https://huggingface.co/${modelId}/raw/main/config.json`);\n if (res.ok) {\n const config = await res.json();\n // Check root level first, then nested text_config (for multimodal models like Ministral)\n const textConfig = config.text_config || {};\n const ctxLen =\n config.max_position_embeddings ||\n textConfig.max_position_embeddings ||\n config.sliding_window ||\n textConfig.sliding_window ||\n config.max_seq_len ||\n config.max_sequence_length ||\n config.n_ctx ||\n config.n_positions;\n if (ctxLen) {\n return ctxLen;\n }\n }\n } catch {\n // Continue to fallback\n }\n\n // Priority 2: tokenizer_config.json → model_max_length (fallback only)\n try {\n const tokRes = await fetch(`https://huggingface.co/${modelId}/raw/main/tokenizer_config.json`);\n if (tokRes.ok) {\n const tokConfig = await tokRes.json();\n // Only use if reasonable (< 1M tokens - anything larger is likely placeholder)\n if (tokConfig.model_max_length && tokConfig.model_max_length < 1e6) {\n return tokConfig.model_max_length;\n }\n }\n } catch {\n // Ignore\n }\n\n return;\n}\n\n/** Get file size from HuggingFace tree entry (handles both regular and LFS files) */\nfunction getFileSize(file: { size?: number; lfs?: { size?: number } }): number {\n // LFS files have size in lfs.size, regular files in size\n return file.lfs?.size || file.size || 0;\n}\n\n/** Fetch model size from HuggingFace API */\nasync function fetchModelSize(modelId: string): Promise<number | undefined> {\n try {\n // Try to get ONNX file size from tree API\n const treeRes = await fetch(`https://huggingface.co/api/models/${modelId}/tree/main/onnx`);\n if (treeRes.ok) {\n const files: { path: string; size?: number; lfs?: { size?: number } }[] =\n await treeRes.json();\n\n // Prefer q4f16 > q4 > fp16 > any .onnx file\n const q4f16 = files.find((f) => f.path.includes(\"q4f16\") && f.path.endsWith(\".onnx\"));\n const q4 = files.find(\n (f) => f.path.includes(\"q4\") && !f.path.includes(\"f16\") && f.path.endsWith(\".onnx\"),\n );\n const fp16 = files.find((f) => f.path.includes(\"fp16\") && f.path.endsWith(\".onnx\"));\n const anyOnnx = files.find((f) => f.path.endsWith(\".onnx\"));\n const bestFile = q4f16 || q4 || fp16 || anyOnnx;\n\n if (bestFile) {\n // Sum up the .onnx file AND all associated .onnx_data* files\n const baseName = bestFile.path.replace(\".onnx\", \"\");\n const relatedFiles = files.filter(\n (f) => f.path === bestFile.path || f.path.startsWith(`${baseName}.onnx_data`),\n );\n const totalSize = relatedFiles.reduce((sum, f) => sum + getFileSize(f), 0);\n if (totalSize > 0) {\n return totalSize;\n }\n }\n }\n\n // Fallback to model info API\n const res = await fetch(`https://huggingface.co/api/models/${modelId}`);\n if (res.ok) {\n const info = await res.json();\n return info.usedStorage;\n }\n } catch {\n // Ignore fetch errors\n }\n return;\n}\n\n/** Track a model as cached */\nexport function trackCachedModel(\n modelId: string,\n sizeBytes?: number,\n contextLength?: number,\n): void {\n try {\n const dir = join(homedir(), \".gerbil\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const models = getChromeCachedModels();\n const existing = models.find((m) => m.modelId === modelId);\n const now = new Date().toISOString();\n\n if (existing) {\n existing.lastUsed = now;\n if (sizeBytes) {\n existing.sizeBytes = sizeBytes;\n }\n if (contextLength) {\n existing.contextLength = contextLength;\n }\n } else {\n models.push({\n modelId,\n downloadedAt: now,\n lastUsed: now,\n sizeBytes,\n contextLength,\n });\n }\n\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n\n // Fetch missing metadata in background\n const needsSize = !(sizeBytes || existing?.sizeBytes);\n const needsContext = !(contextLength || existing?.contextLength);\n\n if (needsSize || needsContext) {\n Promise.all([\n needsSize ? fetchModelSize(modelId) : Promise.resolve(undefined),\n needsContext ? fetchContextLength(modelId) : Promise.resolve(undefined),\n ])\n .then(([size, context]) => {\n const updatedModels = getChromeCachedModels();\n const model = updatedModels.find((m) => m.modelId === modelId);\n if (model) {\n if (size) {\n model.sizeBytes = size;\n }\n if (context) {\n model.contextLength = context;\n }\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models: updatedModels }, null, 2));\n }\n })\n .catch(() => {});\n }\n } catch {\n // Ignore tracking errors\n }\n}\n\n/** Remove a model from cache tracking */\nexport function untrackCachedModel(modelId: string): void {\n try {\n const models = getChromeCachedModels().filter((m) => m.modelId !== modelId);\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n } catch {\n // Ignore tracking errors\n }\n}\n\n/** Refresh metadata (size, context length) for cached models that need it */\nexport async function refreshCachedModelSizes(): Promise<void> {\n try {\n const models = getChromeCachedModels();\n // Refresh if: no size, size < 1MB (wrong), or missing context length\n const MIN_EXPECTED_SIZE = 1_000_000; // 1MB - real models are always bigger\n const needsRefresh = models.filter(\n (m) => !m.sizeBytes || m.sizeBytes < MIN_EXPECTED_SIZE || !m.contextLength,\n );\n if (needsRefresh.length === 0) {\n return;\n }\n\n // Fetch metadata in parallel (max 3 at a time)\n const batchSize = 3;\n for (let i = 0; i < needsRefresh.length; i += batchSize) {\n const batch = needsRefresh.slice(i, i + batchSize);\n await Promise.all(\n batch.map(async (model) => {\n const [size, context] = await Promise.all([\n !model.sizeBytes || model.sizeBytes < MIN_EXPECTED_SIZE\n ? fetchModelSize(model.modelId)\n : Promise.resolve(undefined),\n model.contextLength ? Promise.resolve(undefined) : fetchContextLength(model.modelId),\n ]);\n if (size) {\n model.sizeBytes = size;\n }\n if (context) {\n model.contextLength = context;\n }\n }),\n );\n }\n\n // Save updated models\n writeFileSync(CACHED_MODELS_FILE, JSON.stringify({ models }, null, 2));\n } catch {\n // Ignore refresh errors\n }\n}\n\n// Fixed port for local server - IndexedDB cache is origin-scoped, so using a\n// consistent port ensures the model cache persists between runs\n// Port 43724 = \"GERBI\" on phone keypad (GERBIL=437245 is too big for port range)\nconst GERBIL_LOCAL_PORT = 43_724;\n\n// Global singletons - multiple Gerbil instances share browser and server\nlet globalBrowser: Browser | null = null;\nlet globalBrowserPromise: Promise<Browser> | null = null;\nlet globalServer: Server | null = null;\nlet globalServerPort = 0;\nlet globalServerHtml = \"\"; // Current HTML content to serve\n\n// Page tracking for memory management\nlet activePagesCount = 0;\nconst MAX_CONCURRENT_PAGES = 5; // Limit to prevent dev mistakes\n\n// Track active backend instances for memory monitoring and cleanup\nconst activeBackends: Set<ChromeGPUBackend> = new Set();\n\n// ============================================\n// Types\n// ============================================\n\nexport type ChromeBackendOptions = {\n /** Custom Chrome executable path */\n chromePath?: string;\n /** Model ID to load */\n modelId?: string;\n /** Model context length (for KV cache management) */\n contextLength?: number;\n /** Whether this is a vision model (auto-detected if not specified) */\n isVision?: boolean;\n /** Progress callback */\n onProgress?: (info: { status: string; progress?: number; file?: string }) => void;\n /** Token callback for streaming */\n onToken?: (token: { text: string; state: string; numTokens: number; tps: number }) => void;\n};\n\nexport type GenerateOptions = {\n maxTokens?: number;\n temperature?: number;\n topP?: number;\n topK?: number;\n thinking?: boolean;\n system?: string;\n /** Images for vision models (URLs or data URIs) */\n images?: string[];\n onToken?: (token: { text: string; state: string; numTokens: number; tps: number }) => void;\n};\n\n// ============================================\n// Chrome Path Detection\n// ============================================\n\nconst CHROME_PATHS = {\n darwin: [\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\",\n \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\",\n ],\n linux: [\n \"google-chrome-stable\",\n \"google-chrome\",\n \"chromium-browser\",\n \"chromium\",\n \"microsoft-edge\",\n \"brave-browser\",\n ],\n win32: [\n \"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n \"C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n `${process.env.LOCALAPPDATA}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n \"C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\",\n \"C:\\\\Program Files\\\\BraveSoftware\\\\Brave-Browser\\\\Application\\\\brave.exe\",\n ],\n};\n\nfunction findChrome(): string {\n // Check env override first\n if (process.env.CHROME_PATH) {\n return process.env.CHROME_PATH;\n }\n\n const platform = process.platform as \"darwin\" | \"linux\" | \"win32\";\n const paths = CHROME_PATHS[platform] || [];\n\n for (const p of paths) {\n try {\n if (platform === \"linux\") {\n // For Linux, check if command exists in PATH\n execSync(`which ${p}`, { stdio: \"ignore\" });\n return p;\n }\n // For macOS/Windows, use fs.existsSync (portable)\n if (existsSync(p)) {\n return p;\n }\n } catch {}\n }\n\n throw new Error(\"Chrome not found. Install Chrome or set CHROME_PATH environment variable.\");\n}\n\n// ============================================\n// Chrome Launch Flags\n// ============================================\n\nfunction getChromeFlags(userDataDir: string, _debuggingPort: number): string[] {\n const flags = [\"--no-sandbox\", `--user-data-dir=${userDataDir}`];\n\n // Platform-specific WebGPU flags\n if (process.platform === \"linux\") {\n // Linux: use Vulkan backend for WebGPU\n flags.push(\n \"--enable-unsafe-webgpu\",\n \"--enable-features=Vulkan\",\n \"--use-angle=vulkan\",\n \"--disable-vulkan-surface\",\n );\n } else if (process.platform === \"darwin\") {\n // macOS: WebGPU uses Metal by default, minimal flags needed\n // Only add --enable-unsafe-webgpu if WebGPU is disabled (rare)\n // For now, try without it to avoid triggering GPU bugs\n } else {\n // Windows: use default DirectX/D3D12 backend\n flags.push(\"--enable-unsafe-webgpu\");\n }\n\n return flags;\n}\n\n// ============================================\n// Worker Page HTML\n// ============================================\n\nfunction getWorkerPageHTML(modelPath: string, contextLength = 32_768, isVision = false): string {\n return `\n<!DOCTYPE html>\n<html>\n<head>\n <title>Gerbil WebGPU Backend</title>\n <script type=\"module\">\n import {\n AutoTokenizer,\n AutoModelForCausalLM,\n AutoProcessor,\n AutoModelForImageTextToText,\n RawImage,\n TextStreamer,\n InterruptableStoppingCriteria,\n env,\n } from \"https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1\";\n\n // Enable IndexedDB caching (prevents re-downloading models)\n env.useBrowserCache = true;\n env.allowLocalModels = false;\n\n const IS_VISION = ${isVision};\n \n class ModelPipeline {\n static tokenizer = null;\n static processor = null;\n static model = null;\n static modelId = \"${modelPath}\";\n static isVision = IS_VISION;\n\n static async getInstance(progressCallback) {\n if (this.isVision) {\n // Vision model: use AutoProcessor + AutoModelForImageTextToText\n if (!this.processor) {\n this.processor = await AutoProcessor.from_pretrained(this.modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.model) {\n this.model = await AutoModelForImageTextToText.from_pretrained(this.modelId, {\n device: \"webgpu\",\n progress_callback: progressCallback,\n });\n }\n return { \n processor: this.processor, \n tokenizer: this.processor.tokenizer, \n model: this.model,\n isVision: true \n };\n } else {\n // Text model: use AutoTokenizer + AutoModelForCausalLM\n if (!this.tokenizer) {\n this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId, {\n progress_callback: progressCallback,\n });\n }\n if (!this.model) {\n this.model = await AutoModelForCausalLM.from_pretrained(this.modelId, {\n dtype: \"q4f16\",\n device: \"webgpu\",\n progress_callback: progressCallback,\n });\n }\n return { \n tokenizer: this.tokenizer, \n model: this.model,\n isVision: false \n };\n }\n }\n }\n\n const stoppingCriteria = new InterruptableStoppingCriteria();\n let pastKeyValuesCache = null;\n let totalTokensInCache = 0;\n \n // Context length for auto-reset (passed from model config)\n const CONTEXT_LENGTH = ${contextLength};\n\n // Auto-load model on page init\n (async function() {\n console.log(JSON.stringify({ type: \"progress\", status: IS_VISION ? \"Loading vision model...\" : \"Loading model...\" }));\n \n try {\n const result = await ModelPipeline.getInstance((progress) => {\n if (progress.status === \"progress\" && progress.file) {\n console.log(JSON.stringify({\n type: \"progress\",\n status: \"progress\",\n file: progress.file,\n progress: Math.round(progress.progress || 0),\n }));\n }\n });\n\n console.log(JSON.stringify({ type: \"progress\", status: \"Compiling shaders...\" }));\n \n // Warmup generation to compile shaders and initialize model\n // Always do text warmup first\n const textWarmupInputs = result.tokenizer(\"hello\");\n await result.model.generate({ ...textWarmupInputs, max_new_tokens: 1 });\n \n // Vision models also need vision warmup\n if (result.isVision) {\n console.log(JSON.stringify({ type: \"progress\", status: \"Warming up vision encoder...\" }));\n try {\n // Create a tiny 8x8 red test image\n const canvas = new OffscreenCanvas(8, 8);\n const ctx = canvas.getContext('2d');\n ctx.fillStyle = 'red';\n ctx.fillRect(0, 0, 8, 8);\n const blob = await canvas.convertToBlob({ type: 'image/png' });\n const warmupImage = await RawImage.fromBlob(blob);\n \n // Process with vision pipeline\n const warmupContent = [{ type: \"image\" }, { type: \"text\", text: \"hi\" }];\n const warmupMessages = [{ role: \"user\", content: warmupContent }];\n const warmupPrompt = result.processor.apply_chat_template(warmupMessages);\n const warmupInputs = await result.processor(warmupImage, warmupPrompt, { add_special_tokens: false });\n \n // Run vision warmup generation\n await result.model.generate({\n ...warmupInputs,\n max_new_tokens: 1,\n do_sample: false,\n });\n } catch {\n // Vision warmup failed, text warmup was done so continue\n }\n }\n \n // Set page title to model ID for cross-process identification\n document.title = \"Gerbil: \" + ModelPipeline.modelId;\n \n console.log(JSON.stringify({ type: \"ready\", isVision: result.isVision }));\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n }\n })();\n\n // Text generation (for non-vision models or vision without images)\n window.gerbilGenerate = async function(messages, options = {}) {\n const { maxTokens = 256, temperature = 0.7, topP = 0.9, topK = 20, thinking = false, images = [] } = options;\n \n const result = await ModelPipeline.getInstance();\n \n // Route to vision generation if we have images and this is a vision model\n if (images.length > 0 && result.isVision) {\n return window.gerbilGenerateVision(messages, images, options);\n }\n \n // Auto-reset KV cache if it exceeds context length\n if (totalTokensInCache > CONTEXT_LENGTH) {\n console.log(JSON.stringify({ \n type: \"cache_reset\", \n reason: \"context_exceeded\",\n tokensInCache: totalTokensInCache,\n contextLength: CONTEXT_LENGTH\n }));\n pastKeyValuesCache = null;\n totalTokensInCache = 0;\n }\n\n try {\n const { tokenizer, model } = result;\n\n const inputs = tokenizer.apply_chat_template(messages, {\n add_generation_prompt: true,\n return_dict: true,\n enable_thinking: thinking,\n });\n\n let state = \"answering\";\n let prevState = \"answering\";\n const [START_THINKING_TOKEN_ID, END_THINKING_TOKEN_ID] = tokenizer.encode(\n \"<think></think>\",\n { add_special_tokens: false }\n );\n\n let startTime = null;\n let numTokens = 0;\n\n const tokenCallback = (tokens) => {\n startTime ??= performance.now();\n numTokens += 1;\n \n const tokenId = Number(tokens[0]);\n if (tokenId === START_THINKING_TOKEN_ID) {\n state = \"thinking\";\n } else if (tokenId === END_THINKING_TOKEN_ID) {\n state = \"answering\";\n }\n };\n\n const streamCallback = (text) => {\n const tps = startTime ? (numTokens / (performance.now() - startTime)) * 1000 : 0;\n \n let outputText = text;\n if (thinking) {\n if (state === \"thinking\" && prevState !== \"thinking\") {\n outputText = \"<think>\" + text;\n } else if (state === \"answering\" && prevState === \"thinking\") {\n outputText = \"</think>\" + text;\n }\n }\n prevState = state;\n \n console.log(JSON.stringify({ type: \"token\", text: outputText, state, numTokens, tps }));\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n token_callback_function: tokenCallback,\n });\n\n console.log(JSON.stringify({ type: \"start\" }));\n\n const { past_key_values, sequences } = await model.generate({\n ...inputs,\n past_key_values: pastKeyValuesCache,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n max_new_tokens: maxTokens,\n streamer,\n stopping_criteria: stoppingCriteria,\n return_dict_in_generate: true,\n });\n\n pastKeyValuesCache = past_key_values;\n \n const inputLength = inputs.input_ids.dims[1];\n totalTokensInCache += inputLength + numTokens;\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n \n const generatedTokens = sequences.slice(null, [inputLength, null]);\n const decoded = tokenizer.batch_decode(generatedTokens, { skip_special_tokens: true });\n\n console.log(JSON.stringify({\n type: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n tokensInCache: totalTokensInCache,\n }));\n\n return decoded[0] || \"\";\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n throw error;\n }\n };\n\n // Vision generation (for vision models with images)\n window.gerbilGenerateVision = async function(messages, imageUrls, options = {}) {\n const { maxTokens = 2048, temperature = 0.7, topP = 0.9, topK = 20 } = options;\n\n try {\n const { processor, tokenizer, model } = await ModelPipeline.getInstance();\n\n // Build message content with image placeholders for the user prompt\n const lastMessage = messages[messages.length - 1];\n const content = [];\n for (let i = 0; i < imageUrls.length; i += 1) {\n content.push({ type: \"image\" });\n }\n content.push({ type: \"text\", text: lastMessage.content });\n\n // For vision models, include a brief system instruction for concise responses\n const visionMessages = [\n { role: \"system\", content: \"You are a helpful assistant. Be concise and direct in your responses.\" },\n { role: \"user\", content }\n ];\n\n // Apply chat template with generation prompt\n const chatPrompt = processor.apply_chat_template(visionMessages, { \n add_generation_prompt: true \n });\n\n // Load images\n console.log(JSON.stringify({ type: \"progress\", status: \"Loading images...\" }));\n const loadedImages = await Promise.all(\n imageUrls.map(url => RawImage.fromURL(url))\n );\n\n // Process inputs\n const inputs = await processor(\n loadedImages.length === 1 ? loadedImages[0] : loadedImages,\n chatPrompt,\n { add_special_tokens: false }\n );\n\n let startTime = null;\n let numTokens = 0;\n\n const streamCallback = (text) => {\n startTime ??= performance.now();\n numTokens += 1;\n const tps = (numTokens / (performance.now() - startTime)) * 1000;\n console.log(JSON.stringify({ type: \"token\", text, state: \"answering\", numTokens, tps }));\n };\n\n const streamer = new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: streamCallback,\n });\n\n console.log(JSON.stringify({ type: \"start\" }));\n\n const outputs = await model.generate({\n ...inputs,\n max_new_tokens: maxTokens,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n top_p: topP,\n top_k: topK,\n streamer,\n stopping_criteria: stoppingCriteria,\n });\n\n // Decode output (skip prompt)\n const inputLength = inputs.input_ids.dims?.at(-1) || 0;\n const decoded = processor.batch_decode(\n outputs.slice(null, [inputLength, null]),\n { skip_special_tokens: true }\n );\n\n const endTime = performance.now();\n const totalTime = startTime ? endTime - startTime : 0;\n\n console.log(JSON.stringify({\n type: \"complete\",\n text: decoded[0] || \"\",\n numTokens,\n totalTime,\n tps: totalTime > 0 ? (numTokens / totalTime) * 1000 : 0,\n }));\n\n return decoded[0] || \"\";\n } catch (error) {\n console.log(JSON.stringify({ type: \"error\", error: error.message || String(error) }));\n throw error;\n }\n };\n\n window.gerbilInterrupt = function() {\n stoppingCriteria.interrupt();\n };\n\n window.gerbilReset = function() {\n pastKeyValuesCache = null;\n totalTokensInCache = 0;\n stoppingCriteria.reset();\n console.log(JSON.stringify({ type: \"cache_reset\", reason: \"manual\" }));\n };\n\n // Signal that the page is ready for commands\n console.log(JSON.stringify({ type: \"init\" }));\n </script>\n</head>\n<body>\n <h1>Gerbil WebGPU Backend</h1>\n <p>This page provides WebGPU inference for the Gerbil CLI.</p>\n</body>\n</html>\n`;\n}\n\n// ============================================\n// Chrome GPU Backend\n// ============================================\n\nexport class ChromeGPUBackend {\n private browser: Browser | null = null;\n private page: Page | null = null;\n private cdp: CDPSession | null = null;\n private server: Server | null = null;\n private serverPort = 0;\n private userDataDir: string = GERBIL_CACHE_DIR; // Always use shared cache\n private readonly modelId: string;\n private isReady = false;\n private readonly isVisionModel: boolean = false; // Whether this is a vision model\n private readonly messageHandlers: Map<string, (data: any) => void> = new Map();\n private pendingRejects: Array<(err: Error) => void> = []; // Track pending waits for cleanup\n\n private constructor(modelId: string, isVision = false) {\n this.modelId = modelId;\n this.isVisionModel = isVision;\n }\n\n /**\n * Create and initialize a Chrome GPU backend\n */\n static async create(options: ChromeBackendOptions = {}): Promise<ChromeGPUBackend> {\n const modelId = options.modelId || \"onnx-community/Qwen3-0.6B-ONNX\";\n\n // Detect if this is a vision model (can be overridden via options)\n const isVision = options.isVision ?? ChromeGPUBackend.detectVisionModel(modelId);\n\n const backend = new ChromeGPUBackend(modelId, isVision);\n await backend.launch(options);\n return backend;\n }\n\n /**\n * Detect if a model is a vision model based on its ID\n */\n private static detectVisionModel(modelId: string): boolean {\n const visionModelPatterns = [\n /ministral/i,\n /pixtral/i,\n /llava/i,\n /vision/i,\n /vl/i,\n /image-text/i,\n /multimodal/i,\n ];\n return visionModelPatterns.some((pattern) => pattern.test(modelId));\n }\n\n /**\n * Check if this backend is for a vision model\n */\n isVision(): boolean {\n return this.isVisionModel;\n }\n\n /**\n * Clean up orphan Gerbil pages from previous sessions\n * These are pages that were left behind when process exited without proper cleanup\n */\n private async cleanupOrphanPages(\n browser: Browser,\n options: ChromeBackendOptions,\n ): Promise<number> {\n try {\n const pages = await browser.pages();\n const gerbilPages = pages.filter((p) => {\n const url = p.url();\n // Match our worker pages by URL pattern (127.0.0.1:PORT where PORT > 40000)\n return /127\\.0\\.0\\.1:4\\d{4}/.test(url);\n });\n\n // Only clean up if there are orphan pages not tracked by this process\n const orphanCount = gerbilPages.length - activeBackends.size;\n if (orphanCount > 0) {\n options.onProgress?.({ status: `Cleaning up ${orphanCount} orphan page(s)...` });\n\n for (const page of gerbilPages) {\n // Check if this page is owned by a current backend\n let isOwned = false;\n for (const backend of activeBackends) {\n if (backend.page === page) {\n isOwned = true;\n break;\n }\n }\n\n // Close unowned pages\n if (!isOwned) {\n try {\n await page.close();\n } catch {\n // Page may already be closed\n }\n }\n }\n }\n\n return orphanCount;\n } catch {\n return 0;\n }\n }\n\n /**\n * Get existing browser or launch a new one (singleton pattern)\n * Multiple Gerbil instances share the same browser process\n */\n private async getOrCreateBrowser(\n chromePath: string,\n options: ChromeBackendOptions,\n ): Promise<Browser> {\n // If we already have a global browser, reuse it\n if (globalBrowser?.connected) {\n options.onProgress?.({ status: \"Reusing existing Chrome...\" });\n // Clean up orphan pages from previous sessions\n await this.cleanupOrphanPages(globalBrowser, options);\n return globalBrowser;\n }\n\n // If another caller is launching, wait for them\n if (globalBrowserPromise) {\n options.onProgress?.({ status: \"Waiting for Chrome startup...\" });\n return globalBrowserPromise;\n }\n\n // Try to connect to existing browser via saved WebSocket endpoint\n if (existsSync(WS_ENDPOINT_FILE)) {\n try {\n const wsEndpoint = readFileSync(WS_ENDPOINT_FILE, \"utf-8\").trim();\n options.onProgress?.({ status: \"Connecting to existing Chrome...\" });\n globalBrowser = await puppeteer.connect({\n browserWSEndpoint: wsEndpoint,\n });\n // Clean up orphan pages from previous sessions\n await this.cleanupOrphanPages(globalBrowser, options);\n return globalBrowser;\n } catch {\n // Stale endpoint, remove it and launch fresh\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n }\n }\n\n // Launch new browser\n globalBrowserPromise = this.launchBrowser(chromePath, options);\n try {\n globalBrowser = await globalBrowserPromise;\n return globalBrowser;\n } finally {\n globalBrowserPromise = null;\n }\n }\n\n /**\n * Launch a new Chrome browser instance\n */\n private async launchBrowser(\n chromePath: string,\n _options: ChromeBackendOptions,\n ): Promise<Browser> {\n const debuggingPort = 9222 + Math.floor(Math.random() * 1000);\n\n // Clean up stale lock file if Chrome crashed\n const lockFile = join(this.userDataDir, \"SingletonLock\");\n if (existsSync(lockFile)) {\n try {\n unlinkSync(lockFile);\n await new Promise((r) => setTimeout(r, 200));\n } catch {}\n }\n\n // Use new headless mode - more compatible with WebGPU than old headless\n // Previous crashes were caused by killing our own server, not headless mode\n const browser = await puppeteer.launch({\n executablePath: chromePath,\n headless: true, // Standard headless mode - crashes before were from killing our own server\n args: [\n ...getChromeFlags(this.userDataDir, debuggingPort),\n \"--enable-gpu\",\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-background-timer-throttling\",\n \"--disable-renderer-backgrounding\",\n \"--disable-dev-shm-usage\",\n ],\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n });\n\n // Save WebSocket endpoint for reconnection\n writeFileSync(WS_ENDPOINT_FILE, browser.wsEndpoint());\n\n // Clean up endpoint file when browser closes\n browser.on(\"disconnected\", () => {\n globalBrowser = null;\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n });\n\n return browser;\n }\n\n /**\n * Launch Chrome and initialize the worker page\n */\n private async launch(options: ChromeBackendOptions): Promise<void> {\n // Check page limit to prevent memory issues during development\n if (activePagesCount >= MAX_CONCURRENT_PAGES) {\n throw new Error(\n `Maximum concurrent pages (${MAX_CONCURRENT_PAGES}) reached. ` +\n \"Call dispose() on old Gerbil instances to free resources. \" +\n `Currently active: ${activePagesCount}`,\n );\n }\n\n const chromePath = options.chromePath || findChrome();\n\n // Use persistent cache directory (keeps model downloads between runs)\n this.userDataDir = GERBIL_CACHE_DIR;\n if (!existsSync(this.userDataDir)) {\n mkdirSync(this.userDataDir, { recursive: true });\n }\n\n // Start tiny HTTP server to serve the worker page\n // (Required because file:// + ES modules + CDN imports doesn't work due to CORS)\n const contextLength = options.contextLength || 32_768; // Default to 32K if not specified\n const html = getWorkerPageHTML(this.modelId, contextLength, this.isVisionModel);\n await this.startServer(html);\n\n options.onProgress?.({ status: \"Starting Chrome...\" });\n\n // Get or create the shared browser instance\n this.browser = await this.getOrCreateBrowser(chromePath, options);\n\n // Create page and set up CDP session\n this.page = await this.browser.newPage();\n this.cdp = await this.page.createCDPSession();\n\n // Increment active page counter and register this backend\n activePagesCount += 1;\n activeBackends.add(this);\n options.onProgress?.({ status: `Active pages: ${activePagesCount}/${MAX_CONCURRENT_PAGES}` });\n\n // Listen for browser disconnect (OOM kill, crash, etc.)\n this.browser.on(\"disconnected\", () => {\n this.isReady = false;\n this.browser = null;\n this.page = null;\n this.cdp = null;\n // Fail fast: reject any pending waits so callers don't hang\n this.rejectPendingWaits(new Error(\"CHROME_DISCONNECTED\"));\n });\n\n // Enable console API events and exceptions\n await this.cdp.send(\"Runtime.enable\");\n await this.cdp.send(\"Runtime.setAsyncCallStackDepth\", { maxDepth: 32 });\n\n // Set up console message handler\n this.cdp.on(\"Runtime.consoleAPICalled\", (event) => {\n const text = event.args.map((a: any) => a.value || a.description || \"\").join(\" \");\n\n if (event.type === \"log\" && event.args[0]?.value) {\n try {\n const data = JSON.parse(event.args[0].value);\n this.handleMessage(data, options);\n } catch {\n // Not JSON - only log short messages, skip large data dumps (like KV cache)\n if (\n text.length < 500 &&\n !text.includes(\"Float32Array\") &&\n !text.includes(\"past_key_values\")\n ) {\n // Uncomment for debugging: console.log(\"[Chrome Log]\", text);\n }\n }\n } else if (event.type === \"error\" || event.type === \"warning\") {\n // Filter out noisy messages\n if (\n !(\n text.includes(\"onnxruntime\") ||\n text.includes(\"content-length\") ||\n text.includes(\"Float32Array\") ||\n text.includes(\"past_key_values\")\n ) &&\n text.length < 1000\n ) {\n }\n }\n });\n\n // Listen for exceptions\n this.cdp.on(\"Runtime.exceptionThrown\", (event) => {\n const errText =\n event.exceptionDetails?.text || event.exceptionDetails?.exception?.description || \"\";\n // Skip noisy tensor/KV cache dumps\n if (\n errText.includes(\"Float32Array\") ||\n errText.includes(\"past_key_values\") ||\n errText.length > 1000\n ) {\n return;\n }\n });\n\n // Navigate to our HTTP server - model loads automatically\n await this.page.goto(`http://127.0.0.1:${this.serverPort}/`, {\n waitUntil: \"domcontentloaded\",\n timeout: 30_000,\n });\n\n // Wait for model to be ready (loads automatically on page init)\n await this.waitForMessage(\"ready\", 300_000); // 5 min timeout for model download\n\n this.isReady = true;\n options.onProgress?.({ status: \"Ready (WebGPU)!\" });\n\n // Track this model as cached\n trackCachedModel(this.modelId);\n }\n\n /**\n * Handle incoming messages from the page\n */\n private handleMessage(data: any, options: ChromeBackendOptions): void {\n const { type, ...rest } = data;\n\n // Call registered handler\n const handler = this.messageHandlers.get(type);\n if (handler) {\n handler(rest);\n }\n\n // Also call option callbacks\n if (type === \"progress\") {\n options.onProgress?.(rest);\n } else if (type === \"token\") {\n options.onToken?.(rest);\n }\n }\n\n /**\n * Wait for a specific message type\n */\n private waitForMessage(type: string, timeout = 30_000): Promise<any> {\n return new Promise((resolve, reject) => {\n // Track this reject for cleanup on browser disconnect\n this.pendingRejects.push(reject);\n\n const cleanup = () => {\n clearTimeout(timer);\n this.messageHandlers.delete(type);\n const idx = this.pendingRejects.indexOf(reject);\n if (idx >= 0) {\n this.pendingRejects.splice(idx, 1);\n }\n };\n\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timeout waiting for ${type} message`));\n }, timeout);\n\n this.messageHandlers.set(type, (data) => {\n cleanup();\n resolve(data);\n });\n });\n }\n\n /**\n * Check if Chrome backend is still alive\n */\n isAlive(): boolean {\n return this.isReady && this.browser !== null && this.page !== null;\n }\n\n /**\n * Get Chrome backend status information\n */\n getStatus(): { pid: number | null; port: number; modelId: string; startedAt: Date | null } {\n // Try instance browser first, then global browser\n let pid: number | null = null;\n const browserProcess = this.browser?.process?.() || globalBrowser?.process?.();\n if (browserProcess?.pid) {\n pid = browserProcess.pid;\n }\n return {\n pid,\n port: this.serverPort || globalServerPort,\n modelId: this.modelId,\n startedAt: this.isReady ? new Date() : null,\n };\n }\n\n /**\n * Get Chrome memory usage via CDP Performance metrics\n * Returns memory in bytes or null if unavailable\n */\n async getMemoryUsage(): Promise<{ jsHeapUsed: number; jsHeapTotal: number } | null> {\n if (!(this.cdp && this.isReady)) {\n return null;\n }\n\n try {\n // Enable Performance domain if needed\n await this.cdp.send(\"Performance.enable\");\n\n const { metrics } = (await this.cdp.send(\"Performance.getMetrics\")) as {\n metrics: Array<{ name: string; value: number }>;\n };\n\n const jsHeapUsed = metrics.find((m) => m.name === \"JSHeapUsedSize\")?.value ?? 0;\n const jsHeapTotal = metrics.find((m) => m.name === \"JSHeapTotalSize\")?.value ?? 0;\n\n return { jsHeapUsed, jsHeapTotal };\n } catch {\n return null;\n }\n }\n\n /**\n * Check memory usage and auto-cleanup if threshold exceeded\n * @param thresholdGB Memory threshold in GB (default: 8)\n * @returns true if cleanup was performed\n */\n async checkMemoryAndCleanup(thresholdGB = 8): Promise<boolean> {\n const mem = await this.getMemoryUsage();\n if (!mem) {\n return false;\n }\n\n const usedGB = mem.jsHeapUsed / 1024 ** 3;\n\n if (usedGB > thresholdGB) {\n await this.reset();\n return true;\n }\n\n return false;\n }\n\n /**\n * Get memory usage in a human-readable format\n */\n async getMemoryStats(): Promise<{ usedGB: number; totalGB: number; usedPercent: number } | null> {\n const mem = await this.getMemoryUsage();\n if (!mem) {\n return null;\n }\n\n const usedGB = mem.jsHeapUsed / 1024 ** 3;\n const totalGB = mem.jsHeapTotal / 1024 ** 3;\n const usedPercent = (mem.jsHeapUsed / mem.jsHeapTotal) * 100;\n\n return { usedGB, totalGB, usedPercent };\n }\n\n /**\n * Generate text with streaming\n */\n async generate(prompt: string, options: GenerateOptions = {}): Promise<string> {\n if (!this.isAlive()) {\n throw new Error(\"CHROME_BACKEND_DEAD\");\n }\n\n const system = options.system || \"You are a helpful assistant.\";\n const messages = [\n { role: \"system\", content: system },\n { role: \"user\", content: prompt },\n ];\n\n const genOptions = {\n maxTokens: options.maxTokens ?? (this.isVisionModel ? 2048 : 256),\n temperature: options.temperature ?? 0.7,\n topP: options.topP ?? 0.9,\n topK: options.topK ?? 20,\n thinking: options.thinking ?? false,\n images: options.images ?? [], // Pass images for vision models\n };\n\n // Set up token handler if callback provided\n if (options.onToken) {\n this.messageHandlers.set(\"token\", options.onToken);\n }\n\n try {\n // Start generation\n const resultPromise = this.page?.evaluate(\n (msgs, opts) => (window as any).gerbilGenerate(msgs, opts),\n messages,\n genOptions,\n );\n\n // Wait for completion\n const completeData = await this.waitForMessage(\"complete\", 600_000); // 10 min timeout\n\n // Clean up token handler\n this.messageHandlers.delete(\"token\");\n\n await resultPromise; // Ensure evaluate completes\n\n return completeData.text || \"\";\n } catch (err: any) {\n // Check if Chrome died during generation\n if (!this.isAlive()) {\n throw new Error(\"CHROME_BACKEND_DEAD\");\n }\n throw err;\n }\n }\n\n /**\n * Interrupt current generation\n */\n async interrupt(): Promise<void> {\n if (this.page) {\n await this.page.evaluate(\"window.gerbilInterrupt()\");\n }\n }\n\n /**\n * Reset conversation cache\n */\n async reset(): Promise<void> {\n if (this.page) {\n await this.page.evaluate(\"window.gerbilReset()\");\n }\n }\n\n /**\n * Check if backend is ready\n */\n ready(): boolean {\n return this.isReady;\n }\n\n /**\n * Start or reuse the global HTTP server\n * Uses singleton pattern to prevent killing our own server\n * Updates HTML content for new model loads\n */\n private async startServer(html: string): Promise<void> {\n // Always update the HTML content for new model loads\n globalServerHtml = html;\n\n // If global server is already running, reuse it (with updated HTML)\n if (globalServer && globalServerPort) {\n this.server = globalServer;\n this.serverPort = globalServerPort;\n return;\n }\n\n return new Promise((resolve, reject) => {\n const server = createServer((_req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(globalServerHtml); // Serve current HTML (not captured at creation time)\n });\n\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n // Port in use - assume it's our server from a previous run\n // Just use that port (Chrome will connect to existing server)\n this.serverPort = GERBIL_LOCAL_PORT;\n globalServerPort = GERBIL_LOCAL_PORT;\n resolve();\n } else {\n reject(err);\n }\n });\n\n // Listen on fixed port for consistent IndexedDB origin (cache persistence)\n server.listen(GERBIL_LOCAL_PORT, \"127.0.0.1\", () => {\n this.server = server;\n this.serverPort = GERBIL_LOCAL_PORT;\n globalServer = server;\n globalServerPort = GERBIL_LOCAL_PORT;\n resolve();\n });\n });\n }\n\n /**\n * Dispose of the backend and clean up\n * Note: We keep the shared browser running for other backends\n * @param disconnect If true, also disconnect from shared browser (for clean script exit)\n */\n async dispose(disconnect = false): Promise<void> {\n // Mark as not ready first to prevent new operations\n this.isReady = false;\n\n // Clear pending waits silently (don't reject - just clear them)\n this.pendingRejects = [];\n this.messageHandlers.clear();\n\n // Detach CDP session first\n if (this.cdp) {\n try {\n await this.cdp.detach();\n } catch {\n // CDP may already be detached\n }\n this.cdp = null;\n }\n\n // Close our page (but NOT the shared browser - other backends may use it)\n if (this.page) {\n try {\n // Navigate away first to ensure page is \"dead\"\n await this.page.goto(\"about:blank\").catch(() => {});\n\n // Small delay for navigation\n await new Promise((r) => setTimeout(r, 50));\n\n // Now close\n await this.page.close({ runBeforeUnload: false });\n\n // Decrement active page counter\n activePagesCount = Math.max(0, activePagesCount - 1);\n } catch {\n // Ignore close errors\n }\n this.page = null;\n }\n\n // Unregister from active backends\n activeBackends.delete(this);\n\n // Clear our references first\n this.browser = null;\n this.server = null;\n\n // Small delay to ensure page close completes before disconnect\n if (disconnect) {\n await new Promise((r) => setTimeout(r, 100));\n }\n\n // If requested and we're the last backend, disconnect from browser\n // This allows clean script exit without killing browser for other processes\n if (disconnect && activeBackends.size === 0 && globalBrowser) {\n try {\n globalBrowser.disconnect();\n globalBrowser = null;\n globalBrowserPromise = null;\n } catch {\n // Browser may already be disconnected\n }\n }\n }\n\n /**\n * Reject all pending waits (called on browser disconnect or dispose)\n */\n private rejectPendingWaits(error: Error): void {\n for (const reject of this.pendingRejects) {\n reject(error);\n }\n this.pendingRejects = [];\n this.messageHandlers.clear();\n }\n\n /**\n * Clear the model cache (forces re-download on next start)\n */\n static clearCache(): void {\n if (existsSync(GERBIL_CACHE_DIR)) {\n rmSync(GERBIL_CACHE_DIR, { recursive: true, force: true });\n }\n }\n\n /**\n * Get the number of active Chrome pages\n */\n static getActivePageCount(): number {\n return activePagesCount;\n }\n\n /**\n * Get memory usage info for all active pages\n */\n static getMemoryInfo(): { activePagesCount: number; maxPages: number } {\n return {\n activePagesCount,\n maxPages: MAX_CONCURRENT_PAGES,\n };\n }\n\n /**\n * Get global browser status (even if no active backends)\n */\n static getGlobalBrowserStatus(): {\n running: boolean;\n pid: number | null;\n port: number;\n activePagesCount: number;\n maxPages: number;\n wsEndpoint: string | null;\n } {\n let pid: number | null = null;\n let wsEndpoint: string | null = null;\n\n if (globalBrowser?.connected) {\n const browserProcess = globalBrowser.process?.();\n if (browserProcess?.pid) {\n pid = browserProcess.pid;\n }\n wsEndpoint = globalBrowser.wsEndpoint();\n }\n\n return {\n running: globalBrowser?.connected ?? false,\n pid,\n port: globalServerPort,\n activePagesCount, // This process only\n maxPages: MAX_CONCURRENT_PAGES,\n wsEndpoint,\n };\n }\n\n /**\n * Get total page count from Chrome (all processes)\n */\n static async getTotalPageCount(): Promise<number> {\n if (!globalBrowser?.connected) {\n return 0;\n }\n\n try {\n const pages = await globalBrowser.pages();\n // Count only Gerbil pages\n return pages.filter((p) => {\n const url = p.url();\n return url.includes(`127.0.0.1:${globalServerPort}`);\n }).length;\n } catch {\n return 0;\n }\n }\n\n /**\n * Get all active backends with their memory usage (this process only)\n */\n static async getAllBackendsInfo(): Promise<\n Array<{\n modelId: string;\n isVision: boolean;\n isReady: boolean;\n memory: { usedGB: number; totalGB: number; usedPercent: number } | null;\n }>\n > {\n const results = [];\n\n for (const backend of activeBackends) {\n const mem = await backend.getMemoryStats();\n results.push({\n modelId: backend.modelId,\n isVision: backend.isVisionModel,\n isReady: backend.isReady,\n memory: mem,\n });\n }\n\n return results;\n }\n\n /**\n * Get ALL pages in Chrome browser (cross-process visibility)\n * This shows pages from ALL Gerbil processes sharing the browser\n */\n static async getAllChromePages(): Promise<\n Array<{\n url: string;\n title: string;\n isOurs: boolean; // true if this process owns it\n modelId: string | null; // extracted from URL/title if possible\n memory: { usedGB: number; totalGB: number } | null;\n }>\n > {\n if (!globalBrowser?.connected) {\n return [];\n }\n\n try {\n const pages = await globalBrowser.pages();\n\n const results = [];\n for (const page of pages) {\n const url = page.url();\n const title = await page.title().catch(() => \"\");\n\n // Skip about:blank and non-gerbil pages\n if (url === \"about:blank\" || !url.includes(`127.0.0.1:${globalServerPort}`)) {\n continue;\n }\n\n // Check if we own this page\n let modelId: string | null = null;\n let isOurs = false;\n let memory: { usedGB: number; totalGB: number } | null = null;\n\n for (const backend of activeBackends) {\n if (backend.page === page) {\n isOurs = true;\n modelId = backend.modelId;\n // Get memory from our backend\n const mem = await backend.getMemoryStats();\n if (mem) {\n memory = { usedGB: mem.usedGB, totalGB: mem.totalGB };\n }\n break;\n }\n }\n\n // For pages we don't own, extract model ID from title and try to get memory\n if (!isOurs) {\n if (title.startsWith(\"Gerbil: \")) {\n modelId = title.replace(\"Gerbil: \", \"\");\n }\n\n // Try to get memory for other sessions via CDP\n try {\n const cdp = await page.createCDPSession();\n await cdp.send(\"Performance.enable\");\n const { metrics } = (await cdp.send(\"Performance.getMetrics\")) as {\n metrics: Array<{ name: string; value: number }>;\n };\n const jsHeapUsed = metrics.find((m) => m.name === \"JSHeapUsedSize\")?.value ?? 0;\n const jsHeapTotal = metrics.find((m) => m.name === \"JSHeapTotalSize\")?.value ?? 0;\n memory = {\n usedGB: jsHeapUsed / 1024 ** 3,\n totalGB: jsHeapTotal / 1024 ** 3,\n };\n await cdp.detach();\n } catch {\n // Can't get memory for this page\n }\n }\n\n results.push({\n url,\n title: title || \"Gerbil WebGPU Backend\",\n isOurs,\n modelId,\n memory,\n });\n }\n\n return results;\n } catch {\n return [];\n }\n }\n\n /**\n * Kill a Chrome page by index (works cross-process)\n */\n static async killPageByIndex(index: number): Promise<boolean> {\n if (!globalBrowser?.connected) {\n return false;\n }\n\n try {\n const pages = await globalBrowser.pages();\n // Filter to only Gerbil pages\n const gerbilPages = pages.filter((p) => {\n const url = p.url();\n return url.includes(`127.0.0.1:${globalServerPort}`);\n });\n\n if (index < 0 || index >= gerbilPages.length) {\n return false;\n }\n\n const page = gerbilPages[index];\n\n // If it's our page, use dispose() for proper cleanup\n for (const backend of activeBackends) {\n if (backend.page === page) {\n await backend.dispose();\n return true;\n }\n }\n\n // Otherwise just close the page directly\n await page.close();\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Kill a specific backend by index (this process only)\n */\n static async killBackendByIndex(index: number): Promise<boolean> {\n const backends = [...activeBackends];\n if (index < 0 || index >= backends.length) {\n return false;\n }\n\n const backend = backends[index];\n try {\n await backend.dispose();\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Force kill all backends (for zombie cleanup)\n */\n static async killAllBackends(): Promise<{ pagesKilled: number; browserKilled: boolean }> {\n const count = activeBackends.size;\n\n // Dispose all active backends\n for (const backend of [...activeBackends]) {\n try {\n await backend.dispose();\n } catch {\n // Ignore errors during cleanup\n }\n }\n activeBackends.clear();\n\n // Close shared browser\n let browserKilled = false;\n if (globalBrowser) {\n try {\n await globalBrowser.close();\n browserKilled = true;\n } catch {\n // Browser may already be closed\n }\n globalBrowser = null;\n globalBrowserPromise = null;\n }\n\n // Close server\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n globalServerPort = 0;\n }\n\n activePagesCount = 0;\n\n // Clean up endpoint file\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n\n return { pagesKilled: count, browserKilled };\n }\n\n /**\n * Gracefully close the shared browser (call on process exit)\n */\n static async closeSharedBrowser(): Promise<void> {\n if (globalBrowser) {\n try {\n await globalBrowser.close();\n } catch {\n // Browser may already be closed\n }\n globalBrowser = null;\n globalBrowserPromise = null;\n }\n\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n globalServerPort = 0;\n }\n\n // Reset page counter\n activePagesCount = 0;\n\n // Clean up endpoint file\n try {\n unlinkSync(WS_ENDPOINT_FILE);\n } catch {}\n }\n}\n\n// Register cleanup on process exit to prevent mutex errors\nlet cleanupRegistered = false;\nfunction registerCleanup() {\n if (cleanupRegistered) {\n return;\n }\n cleanupRegistered = true;\n\n const cleanup = () => {\n // Synchronous close - can't await in exit handlers\n if (globalBrowser) {\n try {\n // Force kill the browser process\n const browserProcess = globalBrowser.process();\n if (browserProcess) {\n browserProcess.kill(\"SIGTERM\");\n }\n } catch {}\n globalBrowser = null;\n }\n if (globalServer) {\n globalServer.close();\n globalServer = null;\n }\n };\n\n process.on(\"exit\", cleanup);\n process.on(\"SIGINT\", () => {\n cleanup();\n process.exit(0);\n });\n process.on(\"SIGTERM\", () => {\n cleanup();\n process.exit(0);\n });\n}\n\n// Auto-register when module loads\nregisterCleanup();\n\nexport default ChromeGPUBackend;\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { i as trackCachedModel, n as getChromeCachedModels, r as refreshCachedModelSizes, t as ChromeGPUBackend } from "./chrome-backend-Y9F7W5VQ.mjs";
1
+ import { i as trackCachedModel, n as getChromeCachedModels, r as refreshCachedModelSizes, t as ChromeGPUBackend } from "./chrome-backend-CORwaIyC.mjs";
2
2
 
3
3
  export { ChromeGPUBackend };
package/dist/cli.mjs CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as __require } from "./chunk-Ct1HF2bE.mjs";
3
- import { t as Gerbil } from "./gerbil-yoSpRHgv.mjs";
4
- import { n as getChromeCachedModels, r as refreshCachedModelSizes } from "./chrome-backend-Y9F7W5VQ.mjs";
5
- import { t as BUILTIN_MODELS } from "./models-BAtL8qsA.mjs";
6
- import "./utils-CkB4Roi6.mjs";
7
- import "./one-liner-B1rmFto6.mjs";
8
- import { D as listSkills, T as getSkillInfo, a as summarize, d as explain, k as useSkill, m as commit, s as review, v as loadProjectSkills } from "./skills-5DxAV-rn.mjs";
9
- import { r as startMCPServer } from "./mcp-Bitg4sjX.mjs";
10
- import { a as getToolDefinitions, c as setToolContext, i as getTool, n as executeToolCall, o as loadProjectTools, r as formatToolsForPrompt, s as parseToolCall } from "./tools-IYPrqoek.mjs";
2
+ import { t as __require } from "./chunk-CkXuGtQK.mjs";
3
+ import { n as BUILTIN_MODELS, t as Gerbil } from "./gerbil-Dq039a6V.mjs";
4
+ import { n as getChromeCachedModels, r as refreshCachedModelSizes } from "./chrome-backend-CORwaIyC.mjs";
5
+ import "./utils-CZBZ8dgR.mjs";
6
+ import "./one-liner-CgRVfe5K.mjs";
7
+ import { D as listSkills, T as getSkillInfo, a as summarize, d as explain, k as useSkill, m as commit, s as review, v as loadProjectSkills } from "./skills-BGS20rGK.mjs";
8
+ import { r as startMCPServer } from "./mcp-DY57Whwj.mjs";
9
+ import { a as getToolDefinitions, c as setToolContext, i as getTool, n as executeToolCall, o as loadProjectTools, r as formatToolsForPrompt, s as parseToolCall } from "./tools-Bi1P7Xoy.mjs";
11
10
  import { exec, spawn, spawnSync } from "node:child_process";
12
11
  import fs, { existsSync, readFileSync, unlinkSync } from "node:fs";
13
12
  import http from "node:http";
@@ -25,7 +24,7 @@ import SelectInput from "ink-select-input";
25
24
  import TextInput from "ink-text-input";
26
25
 
27
26
  //#region package.json
28
- var version = "1.0.0-rc.1";
27
+ var version = "1.0.0-rc.3";
29
28
 
30
29
  //#endregion
31
30
  //#region src/cli/repl/auto-update.ts
@@ -8182,7 +8181,7 @@ function App({ initialView = "menu", forcedDevice } = {}) {
8182
8181
  return () => {
8183
8182
  mounted = false;
8184
8183
  if (gerbilRef.current) {
8185
- import("./repl-D20JO260.mjs").then(({ setCleanupPromise: setCleanupPromise$1 }) => {
8184
+ import("./repl-BEusmMZs.mjs").then(({ setCleanupPromise: setCleanupPromise$1 }) => {
8186
8185
  setCleanupPromise$1(gerbilRef.current?.dispose(true) ?? Promise.resolve());
8187
8186
  });
8188
8187
  gerbilRef.current = null;
@@ -9509,7 +9508,7 @@ program.command("serve").description("Start Gerbil server (use --mcp or --http f
9509
9508
  }
9510
9509
  if (opts.mcp) await startMCPServer({ model: opts.model });
9511
9510
  else {
9512
- const { Gerbil: Gerbil$1 } = await import("./gerbil-DeQlX_Mt.mjs");
9511
+ const { Gerbil: Gerbil$1 } = await import("./gerbil-DyTEWXLy.mjs");
9513
9512
  const g = new Gerbil$1();
9514
9513
  const spinner = ora("Loading model...").start();
9515
9514
  await g.loadModel(opts.model);
@@ -9653,7 +9652,7 @@ program.command("cache").description("Show or manage model cache").option("--cle
9653
9652
  });
9654
9653
  const formatSize = formatBytes;
9655
9654
  program.command("cleanup").description("Clean up zombie Chrome pages and free memory").option("--kill-browser", "Also kill the shared Chrome browser (forces restart on next use)").option("--force", "Force kill all Gerbil Chrome processes (use if --kill-browser fails)").action(async (opts) => {
9656
- const { ChromeGPUBackend } = await import("./chrome-backend-JEPeM2YE.mjs");
9655
+ const { ChromeGPUBackend } = await import("./chrome-backend-DIKYoWj-.mjs");
9657
9656
  const { execSync: execSync$1 } = await import("node:child_process");
9658
9657
  console.log(chalk.cyan("\nChecking Chrome backend status...\n"));
9659
9658
  let orphanPids = [];
@@ -9753,7 +9752,7 @@ program.command("bench").description("Benchmark model performance").option("-m,
9753
9752
  }
9754
9753
  });
9755
9754
  program.command("update").description("Update Gerbil to the latest version").action(async () => {
9756
- const { checkForUpdate: checkForUpdate$1, installUpdate: installUpdate$1, CURRENT_VERSION: CURRENT_VERSION$1 } = await import("./auto-update-DsWBBnEk.mjs");
9755
+ const { checkForUpdate: checkForUpdate$1, installUpdate: installUpdate$1, CURRENT_VERSION: CURRENT_VERSION$1 } = await import("./auto-update-S9s5-g0C.mjs");
9757
9756
  const spinner = ora("Checking for updates...").start();
9758
9757
  try {
9759
9758
  const check = await checkForUpdate$1();
@@ -9781,7 +9780,7 @@ program.parse = (...args) => {
9781
9780
  return result;
9782
9781
  };
9783
9782
  async function checkForUpdateCLI() {
9784
- const { checkForUpdate: checkForUpdate$1, CURRENT_VERSION: CURRENT_VERSION$1 } = await import("./auto-update-DsWBBnEk.mjs");
9783
+ const { checkForUpdate: checkForUpdate$1, CURRENT_VERSION: CURRENT_VERSION$1 } = await import("./auto-update-S9s5-g0C.mjs");
9785
9784
  if ((await checkForUpdate$1()).updateAvailable) {}
9786
9785
  }
9787
9786
  program.parse();