Skip to content

Commit bdcf9fa

Browse files
committed
fix: load browser assets directly from Hugging Face
Ensure program IDs keep working even if the PAW site is unavailable by fetching browser metadata and assets from the Hugging Face CDN. Refresh the browser docs and examples around the email-triage-browser slug, and bump the package to 0.3.1.
1 parent eb041e4 commit bdcf9fa

10 files changed

Lines changed: 50 additions & 87 deletions

File tree

‎AGENTS.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,13 @@ npm install @programasweights/web
137137
```javascript
138138
import paw from '@programasweights/web';
139139

140-
const fn = await paw.function('programasweights/email-triage');
140+
const fn = await paw.function('email-triage-browser');
141141
const result = await fn('Urgent: server is down!');
142142
// result: "immediate"
143143
```
144144

145+
If you load by program ID instead of slug, browser inference depends only on Hugging Face-hosted assets at runtime.
146+
145147
## Authentication (optional)
146148

147149
Sign in for higher rate limits and program naming. Everything works without it.

‎README.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# @programasweights/web
22

3-
Run [PAW (Programs as Weights)](https://programasweights.com) neural programs directly in the browser. No server required.
3+
Run [PAW (Programs as Weights)](https://programasweights.com) neural programs directly in the browser. No custom server required for inference.
44

5-
PAW compiles natural language specifications into tiny neural programs. This SDK runs them client-side via WebAssembly, using a shared GPT-2 base model (105 MB, cached after first load) and per-program LoRA adapters (~5 MB each).
5+
PAW compiles natural language specifications into tiny neural programs. This SDK runs them client-side via WebAssembly, using a shared GPT-2 base model (134 MB, cached after first load) and per-program assets (~12 MB total: ~5 MB LoRA adapter + ~7 MB prefix cache).
66

77
## Quick Start
88

99
```html
1010
<script type="module">
1111
import paw from 'https://cdn.jsdelivr.net/npm/@programasweights/web';
1212
13-
const fn = await paw.function('programasweights/email-triage');
13+
const fn = await paw.function('email-triage-browser');
1414
const result = await fn('Urgent: server is down!');
1515
console.log(result); // "immediate"
1616
</script>
@@ -27,8 +27,8 @@ npm install @programasweights/web
2727
```typescript
2828
import paw from '@programasweights/web';
2929

30-
// Load by slug (resolves via API)
31-
const triage = await paw.function('programasweights/email-triage');
30+
// Load by slug (resolves via the PAW API)
31+
const triage = await paw.function('email-triage-browser');
3232

3333
// Load by program ID (direct, no API call needed)
3434
const fn = await paw.function('abc123def456');
@@ -40,7 +40,7 @@ const result = await triage('Check this urgent message');
4040
const short = await triage('Check this', 10);
4141

4242
// Show download progress
43-
const fn2 = await paw.function('programasweights/json-fixer', {
43+
const fn2 = await paw.function('email-triage-browser', {
4444
onProgress: ({ loaded, total, stage }) => {
4545
console.log(`${stage}: ${Math.round(loaded/total*100)}%`);
4646
},
@@ -52,12 +52,14 @@ await fn2.free();
5252

5353
## How It Works
5454

55-
1. **First call**: downloads the GPT-2 Q6_K base model (~105 MB) and caches it in IndexedDB
56-
2. **Per program**: downloads only the LoRA adapter (~5 MB) from HuggingFace CDN
55+
1. **First call**: downloads the GPT-2 Q8_0 base model (~134 MB) and caches it in IndexedDB
56+
2. **Per program**: downloads the program assets (~12 MB total: ~5 MB LoRA adapter + ~7 MB prefix cache) from Hugging Face CDN
5757
3. **Inference**: runs entirely in the browser via WebAssembly (llama.cpp compiled to WASM)
5858
4. **Subsequent visits**: base model loads from cache instantly
5959

60-
Multiple programs share one cached base model. Loading a second program is just a 5 MB download.
60+
Multiple programs share one cached base model. Loading a second program is just a ~12 MB download.
61+
62+
If you load a program by content-addressable ID, the browser runtime only depends on Hugging Face-hosted assets. Slugs still need the PAW API for the initial ID lookup.
6163

6264
## API Reference
6365

@@ -66,15 +68,15 @@ Multiple programs share one cached base model. Loading a second program is just
6668
Loads a PAW program and returns a callable function.
6769

6870
**Parameters:**
69-
- `slugOrId` — Program slug (e.g., `"programasweights/email-triage"`) or program ID hash
71+
- `slugOrId` — Program slug (for example `"email-triage-browser"`) or program ID hash
7072
- `options.onProgress` — Callback for download progress: `({ loaded, total, stage }) => void`
7173
- `options.maxTokens` — Default max output tokens (default: unlimited, runs until EOS or context limit)
7274
- `options.temperature` — Sampling temperature, 0 = greedy (default: 0)
7375

7476
**Returns:** `Promise<PawCallable>` — an async callable with per-call options
7577

7678
```typescript
77-
const fn = await paw.function('programasweights/email-triage');
79+
const fn = await paw.function('email-triage-browser');
7880

7981
// Default: generates until EOS or context limit
8082
const result = await fn('Urgent: server is down!');

‎__tests__/e2e/browser.spec.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
* Browser E2E tests for @programasweights/web SDK.
33
*
44
* Runs real browser inference via Playwright + COOP/COEP test server.
5-
* Downloads the 105MB GPT-2 base model on first run (cached after).
5+
* Downloads the 134MB GPT-2 base model on first run (cached after).
66
*/
77
import { test, expect, type Page } from '@playwright/test';
88

99
const TEST_URL = 'http://localhost:9876';
10-
const KNOWN_GPT2_HASH = '3b4e38680509be51f0ff';
10+
const KNOWN_GPT2_HASH = 'd34792fc9654c0a41483';
1111
const LOAD_TIMEOUT = 180_000;
1212

1313
async function loadProgram(
@@ -50,8 +50,8 @@ test.beforeEach(async ({ page }) => {
5050
// ── Loading programs ──
5151

5252
test.describe('Loading programs', () => {
53-
test.skip('load by slug', async ({ page }) => {
54-
await loadProgram(page, 'email-triage');
53+
test('load by slug', async ({ page }) => {
54+
await loadProgram(page, 'da03/verb-counter');
5555
const hasFn = await page.evaluate(() => typeof (window as any)._fn === 'function');
5656
expect(hasFn).toBe(true);
5757
});

‎__tests__/loader.test.ts‎

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -250,22 +250,11 @@ describe('hash detection', () => {
250250
// ── loadProgramAssets ──
251251

252252
describe('loadProgramAssets', () => {
253-
it('loads program detail, polls API assets, and returns runtime-aware URLs', async () => {
253+
it('loads HF metadata, polls HF assets, and returns runtime-aware URLs', async () => {
254254
mockFetch
255-
.mockResolvedValueOnce({
256-
ok: true,
257-
json: async () => ({
258-
id: 'abc123def456',
259-
spec: 'test spec',
260-
interpreter: 'gpt2',
261-
compiler_snapshot: 'paw-4b-gpt2-20260406',
262-
runtime_id: 'gpt2-q8_0',
263-
runtime_manifest_version: 1,
264-
created_at: '2026-01-01T00:00:00Z',
265-
}),
266-
})
255+
.mockResolvedValueOnce({ ok: true, status: 200 })
256+
.mockResolvedValueOnce({ ok: true, json: async () => GPT2_META })
267257
.mockResolvedValueOnce({ ok: true, json: async () => GPT2_RUNTIME });
268-
// HEAD adapter, HEAD prompt, HEAD cache, HEAD tokens, GET prompt
269258
[200, 200].forEach((status) => {
270259
mockFetch.mockResolvedValueOnce({ ok: status === 200, status });
271260
});
@@ -275,23 +264,20 @@ describe('loadProgramAssets', () => {
275264
expect(assets.meta.runtime_id).toBe('gpt2-q8_0');
276265
expect(assets.promptTemplate).toBe('Hello {INPUT_PLACEHOLDER}');
277266
expect(assets.baseModelUrl).toContain('GPT2-GGUF-Q8_0');
278-
expect(assets.adapterUrl).toBe('https://programasweights.com/api/v1/programs/abc123def456/asset/adapter.gguf');
279-
expect(assets.prefixCacheUrl).toBe('https://programasweights.com/api/v1/programs/abc123def456/asset/prefix_cache.bin');
280-
expect(assets.prefixTokensUrl).toBe('https://programasweights.com/api/v1/programs/abc123def456/asset/prefix_tokens.json');
267+
expect(assets.adapterUrl).toBe('https://huggingface.co/programasweights/paw-programs/resolve/main/abc123def456/adapter.gguf');
268+
expect(assets.prefixCacheUrl).toBe('https://huggingface.co/programasweights/paw-programs/resolve/main/abc123def456/prefix_cache.bin');
269+
expect(assets.prefixTokensUrl).toBe('https://huggingface.co/programasweights/paw-programs/resolve/main/abc123def456/prefix_tokens.json');
281270
});
282271

283272
it('rejects runtimes without browser support', async () => {
284273
mockFetch
274+
.mockResolvedValueOnce({ ok: true, status: 200 })
285275
.mockResolvedValueOnce({
286276
ok: true,
287277
json: async () => ({
288-
id: 'abc123',
289-
spec: 'test spec',
278+
...GPT2_META,
290279
interpreter: 'Qwen/Qwen3-0.6B',
291-
compiler_snapshot: 'paw-4b-qwen3-0.6b-20260407',
292280
runtime_id: 'qwen3-0.6b-q6_k',
293-
runtime_manifest_version: 1,
294-
created_at: '2026-01-01T00:00:00Z',
295281
}),
296282
})
297283
.mockResolvedValueOnce({
@@ -321,17 +307,10 @@ describe('loadProgramAssets', () => {
321307

322308
it('throws on prompt template fetch failure after assets are ready', async () => {
323309
mockFetch
310+
.mockResolvedValueOnce({ ok: true, status: 200 })
324311
.mockResolvedValueOnce({
325312
ok: true,
326-
json: async () => ({
327-
id: 'abc123',
328-
spec: 'test spec',
329-
interpreter: 'gpt2',
330-
compiler_snapshot: 'paw-4b-gpt2-20260406',
331-
runtime_id: 'gpt2-q8_0',
332-
runtime_manifest_version: 1,
333-
created_at: '2026-01-01T00:00:00Z',
334-
}),
313+
json: async () => GPT2_META,
335314
})
336315
.mockResolvedValueOnce({ ok: true, json: async () => GPT2_RUNTIME });
337316
[200, 200].forEach((status) => {

‎examples/basic.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
</head>
2626
<body>
2727
<h1>PAW Browser Inference</h1>
28-
<p class="subtitle">Run neural programs locally in your browser — no server needed.</p>
28+
<p class="subtitle">Run neural programs locally in your browser — no custom server needed for inference.</p>
2929

3030
<div class="card">
3131
<label for="program">Program</label>
32-
<input id="program" type="text" value="programasweights/email-triage"
32+
<input id="program" type="text" value="email-triage-browser"
3333
style="width:100%; padding:0.75rem; border:1px solid #ddd; border-radius:8px; font-size:0.9rem;"
3434
placeholder="Program slug or ID">
3535

@@ -68,7 +68,7 @@ <h1>PAW Browser Inference</h1>
6868

6969
btnRun.disabled = true;
7070
btnRun.textContent = 'Loading model...';
71-
elStatus.textContent = 'Downloading base model (first time only, ~105 MB)...';
71+
elStatus.textContent = 'Downloading base model (first time only, ~134 MB)...';
7272

7373
try {
7474
fn = await paw.function(slug, {

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@programasweights/web",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Run PAW (Programs as Weights) neural programs in the browser via WebAssembly",
55
"type": "module",
66
"main": "dist/index.js",

‎src/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function configure(config: PawConfig): void {
1313
/**
1414
* Load a PAW program for browser-side inference.
1515
*
16-
* Accepts a program ID (hash) or a slug (e.g., "email-triage").
16+
* Accepts a program ID (hash) or a slug (e.g., "email-triage-browser").
1717
* Downloads the base GPT-2 model (~134 MB, cached after first load) and the
1818
* program's browser assets (~12 MB total: ~5 MB adapter + ~7 MB prefix cache).
1919
* Returns a callable PawCallable.
@@ -22,7 +22,7 @@ export function configure(config: PawConfig): void {
2222
* ```ts
2323
* import paw from '@programasweights/web';
2424
*
25-
* const fn = await paw.function('email-triage');
25+
* const fn = await paw.function('email-triage-browser');
2626
* const result = await fn('Urgent: server is down!');
2727
* console.log(result); // "immediate"
2828
*

‎src/loader.ts‎

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ProgramAssets, ProgramMeta, ProgressCallback, RuntimeBaseModel, Ru
22

33
const DEFAULT_API_URL = 'https://programasweights.com/api/v1';
44
const HF_BASE_URL = 'https://huggingface.co';
5+
const HF_PROGRAMS_REPO = 'programasweights/paw-programs';
56
const ASSET_READY_TIMEOUT_MS = 60_000;
67
const ASSET_READY_POLL_MS = 2_000;
78

@@ -79,11 +80,11 @@ export function getBaseModelUrl(runtime: RuntimeManifest): string {
7980
}
8081

8182
function getProgramAssetUrl(programId: string, filename: string): string {
82-
return `${configuredApiUrl}/programs/${encodeURIComponent(programId)}/asset/${encodeURIComponent(filename)}`;
83+
return `${HF_BASE_URL}/${HF_PROGRAMS_REPO}/resolve/main/${programId}/${filename}`;
8384
}
8485

85-
function getProgramUrl(programId: string): string {
86-
return `${configuredApiUrl}/programs/${encodeURIComponent(programId)}`;
86+
function getMetaUrl(programId: string): string {
87+
return getProgramAssetUrl(programId, 'meta.json');
8788
}
8889

8990
function getPromptUrl(programId: string, promptFilename = 'prompt_template.txt'): string {
@@ -130,14 +131,6 @@ export async function resolveSlug(slug: string): Promise<string> {
130131
return data.program_id;
131132
}
132133

133-
async function getProgramDetail(programId: string): Promise<any> {
134-
const resp = await fetch(getProgramUrl(programId));
135-
if (!resp.ok) {
136-
throw new Error(`Failed to load program metadata for "${programId}": ${resp.status}`);
137-
}
138-
return resp.json();
139-
}
140-
141134
async function waitForAssetReady(url: string, label: string, optional = false): Promise<void> {
142135
const deadline = Date.now() + ASSET_READY_TIMEOUT_MS;
143136
let lastStatus = 0;
@@ -163,25 +156,12 @@ export async function loadProgramAssets(
163156
programId: string,
164157
_onProgress?: ProgressCallback
165158
): Promise<ProgramAssets> {
166-
const detail = await getProgramDetail(programId);
167-
const meta: ProgramMeta = {
168-
version: detail.runtime_manifest_version ?? 4,
169-
program_id: detail.id,
170-
spec: detail.spec,
171-
examples: detail.runtime?.examples ?? detail.examples,
172-
interpreter: detail.interpreter,
173-
compiler_snapshot: detail.compiler_snapshot,
174-
compiler_fingerprint: detail.compiler_fingerprint ?? '',
175-
compiler_kind: detail.compiler_kind,
176-
pseudo_program_strategy: detail.pseudo_program_strategy,
177-
runtime_id: detail.runtime_id,
178-
runtime_manifest_version: detail.runtime_manifest_version,
179-
runtime: detail.runtime,
180-
lora_rank: detail.runtime?.adapter?.lora_rank ?? 0,
181-
lora_alpha: detail.runtime?.adapter?.lora_alpha ?? 0,
182-
prefix_steps: detail.prefix_steps ?? 0,
183-
created_at: detail.created_at,
184-
};
159+
await waitForAssetReady(getMetaUrl(programId), 'program metadata');
160+
const metaResp = await fetch(getMetaUrl(programId));
161+
if (!metaResp.ok) {
162+
throw new Error(`Failed to load program metadata for "${programId}": ${metaResp.status}`);
163+
}
164+
const meta: ProgramMeta = await metaResp.json();
185165
const runtime = await resolveRuntimeManifest(meta);
186166

187167
if (!runtime.js_sdk.supported) {

‎test-standalone.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</head>
2727
<body>
2828
<h1>PAW Browser Inference Test</h1>
29-
<p class="sub">Loads GPT-2 base + LoRA adapter + KV cache. First load downloads ~117 MB; subsequent loads use cache.</p>
29+
<p class="sub">Loads GPT-2 base + LoRA adapter + KV cache. First load downloads ~146 MB; subsequent loads use cache.</p>
3030

3131
<div class="card">
3232
<label>Program ID (GPT-2)</label>
@@ -94,7 +94,7 @@ <h1>PAW Browser Inference Test</h1>
9494
wllama = new Wllama(WasmPaths, { suppressNativeLog: false });
9595

9696
// Step 1: Download base model
97-
statusEl.textContent = 'Downloading GPT-2 base model (~105 MB)...';
97+
statusEl.textContent = 'Downloading GPT-2 base model (~134 MB)...';
9898
const baseBlob = await fetchWithProgress(BASE_MODEL_URL, 'Base model');
9999
statusEl.textContent = 'Loading model into WASM...';
100100
await wllama.loadModel([baseBlob], { n_ctx: 2048 });

0 commit comments

Comments
 (0)