@iicp/web-node 0.1.0
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.
- package/LICENSE +180 -0
- package/README.md +68 -0
- package/dist/browserNodeProvider.d.ts +73 -0
- package/dist/browserNodeProvider.js +455 -0
- package/dist/cxConfidentiality.d.ts +27 -0
- package/dist/cxConfidentiality.js +117 -0
- package/dist/iicpConsumer.d.ts +104 -0
- package/dist/iicpConsumer.js +201 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/webllmRuntime.d.ts +140 -0
- package/dist/webllmRuntime.js +259 -0
- package/package.json +60 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type CxPublicKey } from "./cxConfidentiality.js";
|
|
2
|
+
export declare const DEFAULT_DIRECTORY_URL = "https://iicp.network";
|
|
3
|
+
export interface ClientConfig {
|
|
4
|
+
/** Directory base URL. Default: https://iicp.network (CORS-enabled). */
|
|
5
|
+
directory_url?: string;
|
|
6
|
+
/** Per-request timeout (ms). Default 10000. */
|
|
7
|
+
timeout_ms?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DiscoverOptions {
|
|
10
|
+
region?: string;
|
|
11
|
+
min_reputation?: number;
|
|
12
|
+
/** Max nodes to return (directory caps at 50). */
|
|
13
|
+
limit?: number;
|
|
14
|
+
/** Browser pages should keep only HTTPS/loopback endpoints. Default: true. */
|
|
15
|
+
browser_usable_only?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** A discoverable provider node (public discovery view — no tokens/endpoints private). */
|
|
18
|
+
export interface Node {
|
|
19
|
+
node_id: string;
|
|
20
|
+
endpoint: string;
|
|
21
|
+
region?: string;
|
|
22
|
+
reputation_score?: number;
|
|
23
|
+
reputation_tier?: string;
|
|
24
|
+
models?: string[];
|
|
25
|
+
directory_observed_reachable?: boolean | null;
|
|
26
|
+
route_evidence?: string;
|
|
27
|
+
routing_hint?: string;
|
|
28
|
+
browser_usable?: boolean;
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatMessage {
|
|
32
|
+
role: "system" | "user" | "assistant";
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class IicpError extends Error {
|
|
36
|
+
readonly code: string;
|
|
37
|
+
readonly status?: number | undefined;
|
|
38
|
+
constructor(message: string, code: string, status?: number | undefined);
|
|
39
|
+
}
|
|
40
|
+
/** CIP consumer task envelope — extracted for testability (KAT, parity with @iicp/client). */
|
|
41
|
+
export interface TaskEnvelope {
|
|
42
|
+
task_id: string;
|
|
43
|
+
intent: string;
|
|
44
|
+
constraints: Record<string, unknown>;
|
|
45
|
+
payload: {
|
|
46
|
+
messages: ChatMessage[];
|
|
47
|
+
model?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the CIP consumer task envelope. Pure, deterministic (pass task_id for KAT).
|
|
52
|
+
* Wire-protocol parity with the Node SDK: `POST {endpoint}/v1/task` when the caller
|
|
53
|
+
* deliberately builds a plaintext test envelope. Production chat() below is fail-closed
|
|
54
|
+
* and sends an `iicp_conf` envelope only.
|
|
55
|
+
*/
|
|
56
|
+
export declare function cipConsumerEnvelope(messages: ChatMessage[], opts?: {
|
|
57
|
+
intent?: string;
|
|
58
|
+
model?: string;
|
|
59
|
+
task_id?: string;
|
|
60
|
+
}): TaskEnvelope;
|
|
61
|
+
/**
|
|
62
|
+
* Build the discover URL. Extracted so the query construction is unit-testable
|
|
63
|
+
* (same discipline as the federation event-log URL regression test).
|
|
64
|
+
*/
|
|
65
|
+
export declare function discoverUrl(directoryUrl: string, intent: string, opts?: DiscoverOptions): string;
|
|
66
|
+
/** Browser-native, consumer-only IICP client. */
|
|
67
|
+
export declare class IicpBrowserClient {
|
|
68
|
+
private readonly directory;
|
|
69
|
+
private readonly timeout;
|
|
70
|
+
constructor(cfg?: ClientConfig);
|
|
71
|
+
private getJson;
|
|
72
|
+
/** Discover nodes capable of an intent (GET /v1/discover — CORS-enabled on iicp.network). */
|
|
73
|
+
discover(intent: string, opts?: DiscoverOptions): Promise<Node[]>;
|
|
74
|
+
/** Directory mesh stats (GET /v1/stats), incl. mesh_health + active_nodes. */
|
|
75
|
+
stats(): Promise<Record<string, unknown>>;
|
|
76
|
+
/** Public node registry listing (GET /v1/registry/nodes). */
|
|
77
|
+
registry(): Promise<Record<string, unknown>>;
|
|
78
|
+
/**
|
|
79
|
+
* Route a chat to a node over the node's **HTTP transport**: `POST {endpoint}/v1/task`
|
|
80
|
+
* with the real task body `{ task_id, intent, payload, constraints }` (SDK-01/02 — same
|
|
81
|
+
* shape the @iicp/client Node SDK sends; the node replies `{ task_id, result, status,
|
|
82
|
+
* metrics }`). `payload` for llm:chat is `{ messages, model }`.
|
|
83
|
+
*
|
|
84
|
+
* Transport note: nodes also expose the **native IICP binary protocol on port 9484**
|
|
85
|
+
* (`transport_endpoint: iicp://…`) — more efficient, used by the full SDKs. A browser
|
|
86
|
+
* can't open raw TCP, so this client uses the HTTP transport (the discover `endpoint`).
|
|
87
|
+
*
|
|
88
|
+
* Privacy: IICP-CX is fail-closed in the browser too. A discovered node must
|
|
89
|
+
* advertise `cx_public_key` (or the temporary `public_key` alias) before this
|
|
90
|
+
* helper will send it a task.
|
|
91
|
+
*
|
|
92
|
+
* ⚠ Reachability: from an https:// page the browser reaches iicp.network (discover) but
|
|
93
|
+
* NOT http://localhost LLMs (mixed-content; Chrome 129+ flag only), nor a node without
|
|
94
|
+
* CORS, nor an IPv6-firewalled node (today's live nodes are `ipv6_direct_firewall_required`).
|
|
95
|
+
* Pass a reachable `endpoint`. In Node there is no CORS restriction. Returns the node JSON.
|
|
96
|
+
*/
|
|
97
|
+
chat(messages: ChatMessage[], opts: {
|
|
98
|
+
endpoint: string;
|
|
99
|
+
intent?: string;
|
|
100
|
+
model?: string;
|
|
101
|
+
cxPublicKey?: CxPublicKey | null;
|
|
102
|
+
}): Promise<Record<string, unknown>>;
|
|
103
|
+
}
|
|
104
|
+
export declare function maskTunnelUrl(url: string): string;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// iicpConsumer — the CONSUMER module of @iicp/web-node (discover + encrypted submit).
|
|
4
|
+
//
|
|
5
|
+
// A tiny (zero-runtime-dependency) TypeScript client for the IICP discovery mesh,
|
|
6
|
+
// built on the browser's native fetch / TextEncoder / SubtleCrypto so it embeds
|
|
7
|
+
// directly into a web page (and also runs in Node ≥18). It is the CONSUMER subset
|
|
8
|
+
// of the protocol only — it never registers as a provider and carries no TCP / NAT /
|
|
9
|
+
// relay machinery (that lives in the full @iicp/client Node SDK).
|
|
10
|
+
//
|
|
11
|
+
// Discovery (GET {directory}/v1/discover) works from any https:// page because
|
|
12
|
+
// iicp.network sends CORS headers. Task routing to a discovered node, or to a local
|
|
13
|
+
// LLM, is subject to browser CORS / mixed-content policy — see README "CORS reality".
|
|
14
|
+
//
|
|
15
|
+
// Epic: #446 · Dev: #447 · Research: research/wasm/WASM-1-feasibility.md (#292).
|
|
16
|
+
import { encryptPayload } from "./cxConfidentiality.js";
|
|
17
|
+
/** Intent URN shape — parity with @iicp/client (SDK-02). */
|
|
18
|
+
const INTENT_RE = /^urn:iicp:intent:[a-z0-9_:/-]+$/;
|
|
19
|
+
export const DEFAULT_DIRECTORY_URL = "https://iicp.network";
|
|
20
|
+
export class IicpError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
status;
|
|
23
|
+
constructor(message, code, status) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.name = "IicpError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateIntent(intent) {
|
|
31
|
+
if (!INTENT_RE.test(intent)) {
|
|
32
|
+
throw new IicpError(`invalid intent URN: ${intent}`, "invalid_intent");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the CIP consumer task envelope. Pure, deterministic (pass task_id for KAT).
|
|
37
|
+
* Wire-protocol parity with the Node SDK: `POST {endpoint}/v1/task` when the caller
|
|
38
|
+
* deliberately builds a plaintext test envelope. Production chat() below is fail-closed
|
|
39
|
+
* and sends an `iicp_conf` envelope only.
|
|
40
|
+
*/
|
|
41
|
+
export function cipConsumerEnvelope(messages, opts = {}) {
|
|
42
|
+
const intent = opts.intent ?? "urn:iicp:intent:llm:chat:v1";
|
|
43
|
+
validateIntent(intent);
|
|
44
|
+
const taskId = opts.task_id ??
|
|
45
|
+
(globalThis.crypto?.randomUUID?.() ?? `task-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
46
|
+
return {
|
|
47
|
+
task_id: taskId,
|
|
48
|
+
intent,
|
|
49
|
+
constraints: {},
|
|
50
|
+
payload: { messages, model: opts.model },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the discover URL. Extracted so the query construction is unit-testable
|
|
55
|
+
* (same discipline as the federation event-log URL regression test).
|
|
56
|
+
*/
|
|
57
|
+
export function discoverUrl(directoryUrl, intent, opts = {}) {
|
|
58
|
+
const params = new URLSearchParams({ intent });
|
|
59
|
+
if (opts.region)
|
|
60
|
+
params.set("region", opts.region);
|
|
61
|
+
if (opts.min_reputation != null)
|
|
62
|
+
params.set("min_reputation", String(opts.min_reputation));
|
|
63
|
+
if (opts.limit != null)
|
|
64
|
+
params.set("limit", String(opts.limit));
|
|
65
|
+
// /api/v1 — the directory API prefix (verified against prod iicp.network 2026-06-04:
|
|
66
|
+
// /api/v1/discover → JSON {nodes,count,query_ms}; a bare /v1/discover hits the website's
|
|
67
|
+
// static HTML fallback, not the directory). Same /api/v1 lesson as the federation work.
|
|
68
|
+
return `${directoryUrl.replace(/\/+$/, "")}/api/v1/discover?${params}`;
|
|
69
|
+
}
|
|
70
|
+
function isBrowserUsableEndpoint(endpoint) {
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(endpoint);
|
|
73
|
+
if (url.protocol === "https:")
|
|
74
|
+
return true;
|
|
75
|
+
if (url.protocol !== "http:")
|
|
76
|
+
return false;
|
|
77
|
+
return ["localhost", "127.0.0.1", "::1"].includes(url.hostname.toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Browser-native, consumer-only IICP client. */
|
|
84
|
+
export class IicpBrowserClient {
|
|
85
|
+
directory;
|
|
86
|
+
timeout;
|
|
87
|
+
constructor(cfg = {}) {
|
|
88
|
+
this.directory = (cfg.directory_url ?? DEFAULT_DIRECTORY_URL).replace(/\/+$/, "");
|
|
89
|
+
this.timeout = cfg.timeout_ms ?? 10_000;
|
|
90
|
+
}
|
|
91
|
+
async getJson(url) {
|
|
92
|
+
const ctrl = new AbortController();
|
|
93
|
+
const t = setTimeout(() => ctrl.abort(), this.timeout);
|
|
94
|
+
try {
|
|
95
|
+
const resp = await fetch(url, { signal: ctrl.signal, headers: { Accept: "application/json" } });
|
|
96
|
+
if (!resp.ok) {
|
|
97
|
+
throw new IicpError(`GET ${url} → ${resp.status}`, "http_error", resp.status);
|
|
98
|
+
}
|
|
99
|
+
return (await resp.json());
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(t);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Discover nodes capable of an intent (GET /v1/discover — CORS-enabled on iicp.network). */
|
|
106
|
+
async discover(intent, opts = {}) {
|
|
107
|
+
validateIntent(intent);
|
|
108
|
+
const body = await this.getJson(discoverUrl(this.directory, intent, opts));
|
|
109
|
+
const nodes = Array.isArray(body) ? body : (body.nodes ?? []);
|
|
110
|
+
if (opts.browser_usable_only === false)
|
|
111
|
+
return nodes;
|
|
112
|
+
return nodes.filter((node) => {
|
|
113
|
+
if (typeof node.browser_usable === "boolean")
|
|
114
|
+
return node.browser_usable;
|
|
115
|
+
return isBrowserUsableEndpoint(String(node.endpoint ?? ""));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Directory mesh stats (GET /v1/stats), incl. mesh_health + active_nodes. */
|
|
119
|
+
async stats() {
|
|
120
|
+
return this.getJson(`${this.directory}/api/v1/stats`);
|
|
121
|
+
}
|
|
122
|
+
/** Public node registry listing (GET /v1/registry/nodes). */
|
|
123
|
+
async registry() {
|
|
124
|
+
return this.getJson(`${this.directory}/api/v1/registry/nodes`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Route a chat to a node over the node's **HTTP transport**: `POST {endpoint}/v1/task`
|
|
128
|
+
* with the real task body `{ task_id, intent, payload, constraints }` (SDK-01/02 — same
|
|
129
|
+
* shape the @iicp/client Node SDK sends; the node replies `{ task_id, result, status,
|
|
130
|
+
* metrics }`). `payload` for llm:chat is `{ messages, model }`.
|
|
131
|
+
*
|
|
132
|
+
* Transport note: nodes also expose the **native IICP binary protocol on port 9484**
|
|
133
|
+
* (`transport_endpoint: iicp://…`) — more efficient, used by the full SDKs. A browser
|
|
134
|
+
* can't open raw TCP, so this client uses the HTTP transport (the discover `endpoint`).
|
|
135
|
+
*
|
|
136
|
+
* Privacy: IICP-CX is fail-closed in the browser too. A discovered node must
|
|
137
|
+
* advertise `cx_public_key` (or the temporary `public_key` alias) before this
|
|
138
|
+
* helper will send it a task.
|
|
139
|
+
*
|
|
140
|
+
* ⚠ Reachability: from an https:// page the browser reaches iicp.network (discover) but
|
|
141
|
+
* NOT http://localhost LLMs (mixed-content; Chrome 129+ flag only), nor a node without
|
|
142
|
+
* CORS, nor an IPv6-firewalled node (today's live nodes are `ipv6_direct_firewall_required`).
|
|
143
|
+
* Pass a reachable `endpoint`. In Node there is no CORS restriction. Returns the node JSON.
|
|
144
|
+
*/
|
|
145
|
+
async chat(messages, opts) {
|
|
146
|
+
const intent = opts.intent ?? "urn:iicp:intent:llm:chat:v1";
|
|
147
|
+
validateIntent(intent);
|
|
148
|
+
const taskId = globalThis.crypto?.randomUUID?.() ?? `task-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
149
|
+
const payload = { messages, model: opts.model };
|
|
150
|
+
// IICP-CX S.16: encryption is MANDATORY (privacy-first #360) — no opt-out.
|
|
151
|
+
// Refuse keyless nodes instead of silently regressing to plaintext.
|
|
152
|
+
if (!opts.cxPublicKey) {
|
|
153
|
+
throw new IicpError("IICP-CX confidentiality required: node advertises no cx_public_key/public_key", "cx_required");
|
|
154
|
+
}
|
|
155
|
+
const body = {
|
|
156
|
+
task_id: taskId,
|
|
157
|
+
intent,
|
|
158
|
+
constraints: {},
|
|
159
|
+
iicp_conf: await encryptPayload(payload, opts.cxPublicKey, taskId, intent),
|
|
160
|
+
};
|
|
161
|
+
const ctrl = new AbortController();
|
|
162
|
+
const t = setTimeout(() => ctrl.abort(), this.timeout);
|
|
163
|
+
try {
|
|
164
|
+
const resp = await fetch(`${opts.endpoint.replace(/\/+$/, "")}/v1/task`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
signal: ctrl.signal,
|
|
167
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
});
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
throw new IicpError(`task → ${resp.status}`, "node_error", resp.status);
|
|
172
|
+
}
|
|
173
|
+
return (await resp.json());
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
clearTimeout(t);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Mask the high-entropy random subdomain of an ephemeral tunnel endpoint for
|
|
182
|
+
* display (#privacy — maintainer 2026-06-12: keep tunnel DNS names private,
|
|
183
|
+
* like node UUIDs are shown only as prefixes). The random label of a
|
|
184
|
+
* *.trycloudflare.com (or other known tunnel-provider) host is a capability
|
|
185
|
+
* secret — anyone who learns it has a direct line to the operator's machine.
|
|
186
|
+
* Routing code keeps the real URL; only human-facing surfaces mask it.
|
|
187
|
+
* Operator-chosen public domains (e.g. iicp.shaal.dev) are left intact.
|
|
188
|
+
*/
|
|
189
|
+
const TUNNEL_SUFFIXES = [".trycloudflare.com", ".ngrok.io", ".ngrok-free.app", ".loca.lt"];
|
|
190
|
+
export function maskTunnelUrl(url) {
|
|
191
|
+
try {
|
|
192
|
+
const u = new URL(url);
|
|
193
|
+
const suffix = TUNNEL_SUFFIXES.find((s) => u.hostname.endsWith(s));
|
|
194
|
+
if (!suffix)
|
|
195
|
+
return url;
|
|
196
|
+
return `${u.protocol}//****${suffix}${u.pathname === "/" ? "" : u.pathname}`;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return url;
|
|
200
|
+
}
|
|
201
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @iicp/web-node — browser-native IICP node (consume AND serve), ESM, zero-config.
|
|
2
|
+
//
|
|
3
|
+
// The full browser node: a discovery-mesh CONSUMER (IicpBrowserClient) with MANDATORY
|
|
4
|
+
// end-to-end encryption (IICP-CX S.16 — WebCrypto X25519 + AES-256-GCM, no opt-out), a
|
|
5
|
+
// browser PROVIDER (BrowserNodeProvider — serve a model via WebLLM behind a relay), and
|
|
6
|
+
// the WebLLM runtime helpers. Discovery + consume work from any https:// page; serving
|
|
7
|
+
// needs a relay (see README). Parity with the Python/TS/Rust SDKs on the wire.
|
|
8
|
+
export * from "./iicpConsumer.js";
|
|
9
|
+
export * from "./cxConfidentiality.js";
|
|
10
|
+
export * from "./browserNodeProvider.js";
|
|
11
|
+
export * from "./webllmRuntime.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ChatMessage } from "./iicpConsumer.js";
|
|
2
|
+
export type { ChatMessage };
|
|
3
|
+
export interface WebLLMProgress {
|
|
4
|
+
/** Human-readable status description (e.g. "Loading model 23%"). */
|
|
5
|
+
text: string;
|
|
6
|
+
/** 0–1 fraction. 1 = fully loaded and ready. */
|
|
7
|
+
progress: number;
|
|
8
|
+
}
|
|
9
|
+
export type ProgressCallback = (p: WebLLMProgress) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Supported model IDs for UI display. Lineup (maintainer 2026-06-12): the two
|
|
12
|
+
* lightest variants run fine on ANY computer ("CPU" tier — integrated
|
|
13
|
+
* graphics, no dedicated card needed), plus one quality model that wants a
|
|
14
|
+
* dedicated GPU. tier drives the CPU/GPU badge on the picker.
|
|
15
|
+
* (Technically WebLLM always executes via WebGPU; the tier expresses the
|
|
16
|
+
* hardware a model needs to run WELL — tooltips carry that nuance.)
|
|
17
|
+
*/
|
|
18
|
+
export declare const WEBLLM_MODELS: {
|
|
19
|
+
readonly "Qwen2.5-0.5B-Instruct-q4f32_1-MLC": {
|
|
20
|
+
readonly label: "Qwen 2.5 0.5B";
|
|
21
|
+
readonly sizeMB: 350;
|
|
22
|
+
readonly tier: "cpu";
|
|
23
|
+
readonly description: "Lightest — runs on any computer, no graphics card needed";
|
|
24
|
+
};
|
|
25
|
+
readonly "Llama-3.2-1B-Instruct-q4f32_1-MLC": {
|
|
26
|
+
readonly label: "Llama 3.2 1B";
|
|
27
|
+
readonly sizeMB: 500;
|
|
28
|
+
readonly tier: "cpu";
|
|
29
|
+
readonly description: "Light and capable — fine on laptops with integrated graphics";
|
|
30
|
+
};
|
|
31
|
+
readonly "Llama-3.2-3B-Instruct-q4f32_1-MLC": {
|
|
32
|
+
readonly label: "Llama 3.2 3B";
|
|
33
|
+
readonly sizeMB: 1500;
|
|
34
|
+
readonly tier: "gpu";
|
|
35
|
+
readonly description: "Best quality — needs a dedicated graphics card (~4 GB VRAM)";
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type WebLLMModelId = keyof typeof WEBLLM_MODELS;
|
|
39
|
+
export declare const DEFAULT_MODEL: WebLLMModelId;
|
|
40
|
+
/**
|
|
41
|
+
* Coarse device-capability assessment for steering the model picker (#517 D4).
|
|
42
|
+
* Phones/tablets and low-memory machines frequently OOM loading even a 1B model,
|
|
43
|
+
* so the UI should nudge toward the lightest model or the WebGPU-free mesh path
|
|
44
|
+
* rather than letting a newcomer pick a model their device can't run.
|
|
45
|
+
*
|
|
46
|
+
* Signals: `navigator.deviceMemory` (GB, Chromium-only, coarse) + a UA mobile
|
|
47
|
+
* check. Best-effort — absence of a signal is treated as "ok".
|
|
48
|
+
*/
|
|
49
|
+
export type DeviceClass = "mobile" | "low" | "ok";
|
|
50
|
+
export interface DeviceAssessment {
|
|
51
|
+
deviceClass: DeviceClass;
|
|
52
|
+
deviceMemoryGB: number | null;
|
|
53
|
+
/** Whether the GPU-tier (largest) model is likely to fail on this device. */
|
|
54
|
+
gpuModelRisky: boolean;
|
|
55
|
+
/** Plain-language steer, empty when the device looks capable. */
|
|
56
|
+
note: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function assessDevice(): DeviceAssessment;
|
|
59
|
+
export interface WebGPUCheckResult {
|
|
60
|
+
supported: boolean;
|
|
61
|
+
/** Present when unsupported — human-readable reason. */
|
|
62
|
+
reason?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect WebGPU availability. Safe to call server-side (returns unsupported).
|
|
66
|
+
* Chrome 113+, Firefox nightly, Safari Technology Preview.
|
|
67
|
+
*/
|
|
68
|
+
export declare function detectWebGPU(): WebGPUCheckResult;
|
|
69
|
+
/**
|
|
70
|
+
* Async WebGPU probe — the REAL availability signal. `detectWebGPU()` only
|
|
71
|
+
* checks that `navigator.gpu` exists, which some browsers expose while still
|
|
72
|
+
* being unable to run a model (e.g. Firefox surfaces `navigator.gpu` but has no
|
|
73
|
+
* usable adapter unless `dom.webgpu.enabled` is set, producing a false-positive
|
|
74
|
+
* "WebGPU available" badge — #518). This calls `requestAdapter()` to confirm a
|
|
75
|
+
* usable GPU, so UI that gates on it reflects reality, not just API presence.
|
|
76
|
+
*/
|
|
77
|
+
export declare function probeWebGPU(): Promise<WebGPUCheckResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Classify a load-failure message as out-of-memory (#517 D4). OOM is the most
|
|
80
|
+
* common real-device failure (phones / integrated GPUs); detecting it lets the
|
|
81
|
+
* UI steer to a smaller model or the mesh instead of a generic error. Matches
|
|
82
|
+
* the strings WebGPU/WebLLM/browsers surface for memory exhaustion.
|
|
83
|
+
*/
|
|
84
|
+
export declare function isOutOfMemoryError(message: string): boolean;
|
|
85
|
+
export declare class WebLLMError extends Error {
|
|
86
|
+
readonly code: "webgpu_unavailable" | "not_loaded" | "load_failed" | "out_of_memory" | "inference_failed" | "bad_response";
|
|
87
|
+
constructor(message: string, code: "webgpu_unavailable" | "not_loaded" | "load_failed" | "out_of_memory" | "inference_failed" | "bad_response");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* In-browser LLM runtime wrapping @mlc-ai/web-llm.
|
|
91
|
+
*
|
|
92
|
+
* Lifecycle: construct → canRun() check → load() → chat() → unload()
|
|
93
|
+
*
|
|
94
|
+
* Model weights are downloaded once and cached in IndexedDB by the WebLLM
|
|
95
|
+
* engine (automatic, per-origin). Subsequent loads skip the download.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const rt = new WebLLMRuntime();
|
|
100
|
+
* if (!rt.canRun()) return; // WebGPU unavailable
|
|
101
|
+
* await rt.load('Llama-3.2-1B-Instruct-q4f32_1-MLC', p => setProgress(p));
|
|
102
|
+
* const reply = await rt.chat([{ role: 'user', content: 'Hello' }]);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare class WebLLMRuntime {
|
|
106
|
+
private _engine;
|
|
107
|
+
private _modelId;
|
|
108
|
+
/** True if WebGPU is available in this browser/environment. */
|
|
109
|
+
canRun(): boolean;
|
|
110
|
+
/** True if a model is currently loaded and ready for inference. */
|
|
111
|
+
isLoaded(): boolean;
|
|
112
|
+
/** Currently loaded model ID, or null if not loaded. */
|
|
113
|
+
get modelId(): WebLLMModelId | null;
|
|
114
|
+
/**
|
|
115
|
+
* Load a model. First call downloads weights (~300–1500 MB) and caches them
|
|
116
|
+
* in IndexedDB. Subsequent calls with the same model skip the download.
|
|
117
|
+
*
|
|
118
|
+
* @param model Model ID from WEBLLM_MODELS (default: Llama 3.2 1B q4)
|
|
119
|
+
* @param onProgress Optional progress callback (text, 0–1 fraction)
|
|
120
|
+
* @throws WebLLMError If WebGPU is unavailable or model load fails
|
|
121
|
+
*/
|
|
122
|
+
load(model?: WebLLMModelId, onProgress?: ProgressCallback): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* CIP-compatible chat inference. Returns the assistant reply text.
|
|
125
|
+
* Provider glue (#452) calls this to satisfy llm:chat intent tasks.
|
|
126
|
+
* The model is fixed at load() time — pass a different modelId to load() to switch.
|
|
127
|
+
*
|
|
128
|
+
* @param messages Chat history (system/user/assistant turns)
|
|
129
|
+
* @throws WebLLMError If no model is loaded or inference fails
|
|
130
|
+
*/
|
|
131
|
+
chat(messages: ChatMessage[], opts?: {
|
|
132
|
+
temperature?: number;
|
|
133
|
+
max_tokens?: number;
|
|
134
|
+
}): Promise<string>;
|
|
135
|
+
/**
|
|
136
|
+
* Unload the model and release GPU memory.
|
|
137
|
+
* Safe to call when not loaded (no-op).
|
|
138
|
+
*/
|
|
139
|
+
unload(): Promise<void>;
|
|
140
|
+
}
|