Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b26d299
WebGPU + ONNX provider (Qwen 3 0.6B, in-browser, no server needed)
esokullu May 22, 2026
c7ed019
chore: bump version 7.3.1 → 7.4.0
esokullu May 22, 2026
351971f
Merge branch 'main' into webgpu
esokullu May 22, 2026
ac615d0
Update offscreen.html
esokullu May 22, 2026
479fe8d
WebGPU: download progress indicator
esokullu May 22, 2026
3ab4a97
Vendor @huggingface/transformers 4.2.0 (WebGPU + ONNX runtime)
esokullu May 22, 2026
007488c
version up
esokullu May 22, 2026
71bc9c9
WebGPU: fix bare-specifier import (vendor ort.webgpu.bundle, patch
esokullu May 22, 2026
60802f5
WebGPU: vendor UNMINIFIED builds (Web Store policy + readability)
esokullu May 22, 2026
2ec3c04
WebGPU: vendor onnxruntime-common, patch second bare specifier
esokullu May 22, 2026
2596d1d
WebGPU: vendor asyncify WASM variant for CPU fallback ops
esokullu May 22, 2026
08d1cdb
WebGPU: switch default dtype from q4 to q4f16
esokullu May 22, 2026
446a3ed
WebGPU: document fp16 fallback when q4f16 kernel overflows
esokullu May 22, 2026
e46db35
WebGPU: surface fallback-adapter / no-GPU case in Test Connection
esokullu May 22, 2026
7e23df5
WebGPU: force .jsep wasm variant so WebGPU EP actually engages
esokullu May 22, 2026
258dbbe
WebGPU: enable cross-origin isolation for SharedArrayBuffer
esokullu May 22, 2026
32fbe65
WebGPU: address PR #66 codex review (tool-call streaming + cache key)
esokullu May 22, 2026
5d66564
WebGPU: disable wasm-cache for chrome-extension scheme
esokullu May 22, 2026
e84f36a
WebGPU: move inference to a dedicated Worker + upgrade to Qwen 3.5 0.8B
esokullu May 22, 2026
9db2be7
title change
esokullu May 22, 2026
efc9a9d
WebGPU: keep outputs on GPU to avoid mapAsync OOM
esokullu May 22, 2026
117f7ff
Merge branch 'main' into webgpu
esokullu May 26, 2026
ffecb66
9.0.0
esokullu May 26, 2026
51c4128
9.0.2
esokullu May 26, 2026
69c7f5d
Fix WebGPU CPU tensor access error in worker pipeline
esokullu May 26, 2026
f991fc3
Handle WebGPU OrtRun buffer download/CPU data failures with retry mode
esokullu May 26, 2026
1aac015
Add robust fallback for WebGPU buffer map failures
esokullu May 26, 2026
9b46bc7
Improve WebGPU fallback errors and switch default ONNX model to Gemma
esokullu May 26, 2026
05461e1
Reset WebGPU mode when WASM kernel init fails
esokullu May 26, 2026
9c6d130
Retry WebGPU unaligned-access failures with fp16 dtype
esokullu May 26, 2026
c4209b9
Skip WASM fallback for quantized WebGPU models
esokullu May 26, 2026
901fef1
Retry quantized WebGPU map failures with fp16 before aborting
esokullu May 26, 2026
28fd949
improvements
esokullu May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
node_modules/
test/anonymous/.test-profile/

# Vendored @huggingface/transformers build artifacts. See
# src/{chrome,firefox}/vendor/transformers/README.md — the build is
# dropped in locally and too big to commit. The READMEs themselves
# stay tracked.
src/chrome/vendor/transformers/*.js
src/chrome/vendor/transformers/*.wasm
src/chrome/vendor/transformers/*.mjs
src/firefox/vendor/transformers/*.js
src/firefox/vendor/transformers/*.wasm
src/firefox/vendor/transformers/*.mjs

# ---- SocialMediaDownloader test suite ----
# Cookies / session state if someone ever points the profile here
smd-test-profile/
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WebBrain",
"version": "7.3.1",
"version": "7.4.0",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"sidePanel",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webbrain",
"version": "7.3.1",
"version": "7.4.0",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebBrain Chrome Extension — Architecture

> Version 7.3.1 · Manifest V3 · Service Worker background
> Version 7.4.0 · Manifest V3 · Service Worker background

## High-Level Overview

Expand Down
2 changes: 1 addition & 1 deletion src/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WebBrain",
"version": "7.3.1",
"version": "7.4.0",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"sidePanel",
Expand Down
9 changes: 8 additions & 1 deletion src/chrome/src/offscreen/offscreen.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
<!DOCTYPE html>
<html><head><script src="offscreen.js"></script></head></html>
<!--
Offscreen document for WebBrain.
- Local-network fetch proxy (PNA workaround).
- WebGPU + ONNX LLM runtime via vendored @huggingface/transformers.
type="module" is required so offscreen.js can dynamically import the
vendored library.
-->
<html><head><script type="module" src="offscreen.js"></script></head></html>
213 changes: 207 additions & 6 deletions src/chrome/src/offscreen/offscreen.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
/**
* Offscreen document — fetch proxy for local network LLM servers.
* Offscreen document — host for tasks the MV3 service worker can't do
* itself.
*
* Chrome MV3 service workers can't always reach HTTP servers on the local
* network (Private Network Access + CORS restrictions). This offscreen
* document receives fetch requests from the service worker, makes them
* from a regular page context (which has different networking rules),
* and sends the response back.
* 1. `offscreen-fetch` — fetch() proxy for local network LLM servers.
* The SW can't always reach 192.168.* / 127.* directly due to PNA +
* CORS; this page context can. See providers/fetch-with-fallback.js.
*
* 2. `webgpu-chat` / `webgpu-probe` — runs the WebGPU + ONNX local
* LLM (default Qwen 3 0.6B). The SW has no WebGPU; this document
* does. Pipeline is loaded lazily on first chat call and cached for
* the document's lifetime. Library is vendored under
* src/chrome/vendor/transformers/ — see the README there for how
* to drop the build in.
*/

// ─── Local-network fetch proxy ────────────────────────────────────────────

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type !== 'offscreen-fetch') return false;

Expand Down Expand Up @@ -47,3 +55,196 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {

return true; // keep sendResponse channel open for async
});

// ─── WebGPU LLM inference ─────────────────────────────────────────────────

let _libPromise = null; // Promise resolving to the transformers module
let _libraryVersion = null; // for diagnostics
let _activeModelId = null; // which model the cached pipeline is for
let _activePipeline = null; // text-generation pipeline instance

/**
* Lazily import the vendored @huggingface/transformers ESM build. The
* library is large (~5MB JS + ~30MB WASM blob) so we keep it out of the
* extension's normal load path — it only loads when the user actually
* picks the WebGPU provider. Missing-vendor case returns a clear error
* pointing at the vendor README.
*/
async function loadLibrary() {
if (_libPromise) return _libPromise;
_libPromise = (async () => {
let lib;
try {
// Dynamic import so a missing file fails at runtime (with a clear
// message) instead of at offscreen-doc parse time.
lib = await import('../../vendor/transformers/transformers.min.js');
} catch (e) {
_libPromise = null; // allow retry after the user vendors the file
throw new Error(
'transformers.js library not vendored. See ' +
'src/chrome/vendor/transformers/README.md for how to drop the ' +
'build in. Underlying error: ' + (e?.message || String(e))
);
}
_libraryVersion = lib.env?.version || lib.VERSION || 'unknown';
if (lib.env) {
// Force HF Hub for model loads (extensions can't run a side-server
// for local model files). IndexedDB cache is on by default — that
// gives us the "big first download, instant subsequent runs" UX.
lib.env.allowLocalModels = false;
lib.env.allowRemoteModels = true;
}
return lib;
})();
return _libPromise;
}

async function getPipeline(modelId, dtype, device) {
if (_activePipeline && _activeModelId === modelId) return _activePipeline;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild cached pipeline when dtype or device changes

The pipeline cache key only checks modelId, but users can edit dtype and device in provider settings. After one successful load, changing quantization/backend (for example q4q8 or webgpuwasm) will silently keep using the old pipeline, so configuration changes do not take effect until the offscreen document is recreated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

const lib = await loadLibrary();
const { pipeline } = lib;
// Free the previous pipeline before loading a new one — two 500MB
// models pinned in GPU memory is a recipe for OOM on integrated GPUs.
if (_activePipeline && _activePipeline.dispose) {
try { await _activePipeline.dispose(); } catch {}
}
_activePipeline = await pipeline('text-generation', modelId, {
device: device || 'webgpu',
dtype: dtype || 'q4',
});
_activeModelId = modelId;
return _activePipeline;
}

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type !== 'webgpu-probe') return false;
(async () => {
try {
// Just check the library imports and WebGPU exists — don't load
// model weights (that's the expensive step we defer until the
// user actually runs a chat).
await loadLibrary();
const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator;
sendResponse({
ok: true,
libraryVersion: _libraryVersion,
device: hasWebGPU ? 'webgpu' : 'wasm',
hasWebGPU,
});
} catch (e) {
sendResponse({ ok: false, error: e.message });
}
})();
return true;
});

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type !== 'webgpu-chat') return false;

(async () => {
try {
const pipe = await getPipeline(msg.model, msg.dtype, msg.device);
const messages = msg.messages || [];
const opts = msg.options || {};

// transformers.js text-generation pipelines accept a messages array
// for ChatML-style templating. Qwen 3 ships a chat template that
// knows about tools — passing `tools` in here makes the system
// prompt include them, and the model emits <tool_call>{...}</tool_call>
// blocks in its reply which we extract below.
const generateArgs = {
max_new_tokens: opts.maxTokens || 1024,
temperature: opts.temperature ?? 0.7,
do_sample: (opts.temperature ?? 0.7) > 0,
return_full_text: false,
};
if (opts.tools && opts.tools.length > 0) {
// OpenAI sends {type:'function', function:{name,description,parameters}}
// Qwen's template wants just the inner function object.
generateArgs.tools = opts.tools.map(t => t.function || t);
}

const output = await pipe(messages, generateArgs);
const text = extractGeneratedText(output);
const toolCalls = extractToolCalls(text);

sendResponse({
ok: true,
content: toolCalls ? '' : text,
toolCalls: toolCalls || null,
usage: null, // transformers.js doesn't surface token counts uniformly
raw: { rawText: text },
});
} catch (e) {
sendResponse({ ok: false, error: e.message });
}
})();

return true;
});

/**
* Pull the generated text out of transformers.js's output. The pipeline
* returns one of a few shapes depending on options — handle the common
* ones, fall back to JSON-stringifying so we never silently drop content.
*/
function extractGeneratedText(output) {
if (!output) return '';
if (typeof output === 'string') return output;
if (Array.isArray(output)) {
const first = output[0];
if (!first) return '';
if (typeof first === 'string') return first;
if (first.generated_text) {
if (typeof first.generated_text === 'string') return first.generated_text;
// ChatML mode: generated_text is the full messages array. Pick the
// last assistant message.
if (Array.isArray(first.generated_text)) {
for (let i = first.generated_text.length - 1; i >= 0; i--) {
const m = first.generated_text[i];
if (m?.role === 'assistant' && typeof m.content === 'string') return m.content;
}
}
}
}
return JSON.stringify(output);
}

/**
* Parse Qwen-style `<tool_call>{...}</tool_call>` blocks out of the
* model's text output and convert them to OpenAI-format tool_calls.
* Returns null if no tool calls are found.
*
* Qwen 3's chat template emits one block per tool call with `name` +
* `arguments` fields inside. The agent expects OpenAI's
* {id, type:'function', function:{name, arguments:JSON-stringified}}
* shape — we map between them here so the rest of the pipeline
* (loop detector, tool dispatch) treats WebGPU exactly like any other
* provider.
*/
function extractToolCalls(text) {
if (!text || typeof text !== 'string') return null;
if (!text.includes('<tool_call>')) return null;
const calls = [];
const re = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
let match;
let idx = 0;
while ((match = re.exec(text)) !== null) {
try {
const obj = JSON.parse(match[1]);
if (obj && obj.name) {
calls.push({
id: `webgpu_call_${Date.now()}_${idx++}`,
type: 'function',
function: {
name: obj.name,
arguments: typeof obj.arguments === 'string'
? obj.arguments
: JSON.stringify(obj.arguments || {}),
},
});
}
} catch { /* malformed tool-call block — ignore, treat as text */ }
}
return calls.length > 0 ? calls : null;
}
21 changes: 21 additions & 0 deletions src/chrome/src/providers/manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LlamaCppProvider } from './llamacpp.js';
import { OpenAICompatibleProvider } from './openai.js';
import { AnthropicProvider, AnthropicOAuthProvider } from './anthropic.js';
import { WebGPUProvider } from './webgpu.js';

/**
* Manages LLM provider instances and persists configuration.
Expand Down Expand Up @@ -77,6 +78,24 @@ export class ProviderManager {
supportsVision: false,
enabled: true,
},
// Browser-native local: WebGPU + ONNX runtime via @huggingface/transformers.
// No separate server needed — model weights download from HF Hub on
// first use (~500MB for q4 Qwen 3 0.6B), cached in IndexedDB by the
// library, inference runs in the offscreen document on the user's
// GPU. Default disabled because the first-run download is substantial
// and the library has to be vendored — see
// src/chrome/vendor/transformers/README.md.
webgpu_qwen3: {
type: 'webgpu',
category: 'local',
label: 'Qwen 3 0.6B (WebGPU, in-browser)',
model: 'onnx-community/Qwen3-0.6B-ONNX',
dtype: 'q4',
device: 'webgpu',
supportsVision: false,
useCompactPrompt: true,
enabled: false,
},
ollama: {
type: 'openai',
category: 'local',
Expand Down Expand Up @@ -241,6 +260,8 @@ export class ProviderManager {
return new AnthropicProvider(config);
case 'anthropic_oauth':
return new AnthropicOAuthProvider(config);
case 'webgpu':
return new WebGPUProvider(config);
default:
throw new Error(`Unknown provider type: ${config.type}`);
}
Expand Down
Loading