@nevescloud/pip 3.4.1 → 3.5.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/README.md +11 -5
- package/bundle/local.esm.js +15 -6
- package/package.json +1 -1
- package/pip-core.esm.js +11 -27
- package/providers/local.esm.js +58 -24
- package/runtime.esm.js +8 -15
package/README.md
CHANGED
|
@@ -38,8 +38,14 @@ import { createPip, createRuntime, openai } from 'https://cdn.jsdelivr.net/npm/@
|
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
```js
|
|
41
|
-
// Local — in-browser inference via transformers.js + WebGPU
|
|
42
|
-
import { createPip,
|
|
41
|
+
// Local — in-browser inference via transformers.js + WebGPU
|
|
42
|
+
import { createPip, createRuntime, local } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/bundle/local.esm.js';
|
|
43
|
+
|
|
44
|
+
const rt = createRuntime({ provider: local({ model: 'LiquidAI/LFM2.5-350M-ONNX' }) });
|
|
45
|
+
const pip = createPip({ onSubmit: rt.onSubmit, onSlash: rt.onSlash, slashSource: rt.slashSource });
|
|
46
|
+
// `createTransformersRenderer` is also exported for hosts that drive
|
|
47
|
+
// a one-shot paint themselves (e.g. an offline fallback that bypasses
|
|
48
|
+
// the turn loop entirely).
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
On jsdelivr the `.esm.js` suffix is required — jsdelivr serves files by raw path, not via `package.json` exports. npm-installed consumers can use the shorter `@nevescloud/pip/bundle/anthropic` (Node ESM resolver honors the exports map). `pip/bundle.esm.js` (or `pip/bundle` via npm) is an alias for `bundle/anthropic` — the default when you haven't picked a brain. Bundles are sugar over the layered files; hosts with a different brain shape (UI only, custom provider, in-browser model) import the granular files directly. See [CONSUMERS.md](../../CONSUMERS.md) for the full entry-point list.
|
|
@@ -50,7 +56,7 @@ On jsdelivr the `.esm.js` suffix is required — jsdelivr serves files by raw pa
|
|
|
50
56
|
|---|---|---|
|
|
51
57
|
| `onSubmit(text, ctx)` | — | Required. Returns the reply string (or a promise of one). |
|
|
52
58
|
| `onSlash(text)` | `null` | Legacy fallback intercept for `/`-prefixed input. Runs *after* registered commands and built-ins miss. New code should call `pip.registerSlash()` instead. |
|
|
53
|
-
| `slashSource()` | built-in | Override for the autocomplete dropdown's source. By default, pip enumerates `pip.registerSlash()` registrations + built-in `/
|
|
59
|
+
| `slashSource()` | built-in | Override for the autocomplete dropdown's source. By default, pip enumerates `pip.registerSlash()` registrations + built-in `/clear`. ↑/↓ to cycle, Enter to run, Tab to accept without submitting (use when adding args), Esc to close; `complete(partial)` per-keystroke after the space supplies arg suggestions. |
|
|
54
60
|
| `introText` | `""` | Muted message shown before first turn; auto-dismisses on submit. |
|
|
55
61
|
| `introDismissMs` | `7000` | How long the intro stays before fading. `0` to keep until first message. |
|
|
56
62
|
| `placeholder` | `"Ask Pip…"` | Input placeholder. |
|
|
@@ -70,7 +76,7 @@ On jsdelivr the `.esm.js` suffix is required — jsdelivr serves files by raw pa
|
|
|
70
76
|
|
|
71
77
|
## Slash commands
|
|
72
78
|
|
|
73
|
-
`pip.registerSlash({ name, handler, description?, complete? })` adds a command. Registry-first dispatch: registered commands win over built-ins so a host can override `/
|
|
79
|
+
`pip.registerSlash({ name, handler, description?, complete? })` adds a command. Registry-first dispatch: registered commands win over built-ins so a host can override `/clear` by registering with the same name.
|
|
74
80
|
|
|
75
81
|
```js
|
|
76
82
|
pip.registerSlash({
|
|
@@ -86,7 +92,7 @@ pip.registerSlash({
|
|
|
86
92
|
pip.unregisterSlash("model");
|
|
87
93
|
```
|
|
88
94
|
|
|
89
|
-
Handler returns `{ reply?, clearedUI?, openCompletions?, passThrough? } | null`. `null` falls through to `onSlash` (if provided) and then to the LLM.
|
|
95
|
+
Handler returns `{ reply?, clearedUI?, openCompletions?, passThrough? } | null`. `null` falls through to `onSlash` (if provided) and then to the LLM. The autocomplete dropdown is the help surface — every registered + built-in command appears with its description as the user types `/`. Built-in `/clear` wipes pip's local history + DOM.
|
|
90
96
|
|
|
91
97
|
- `reply` — render as an assistant turn in chat history.
|
|
92
98
|
- `clearedUI` — the handler already painted its own UI (via `startTurn`, `askInChat`, etc.); pip-core skips creating a turn.
|
package/bundle/local.esm.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// Quickstart entry — pip wired to an in-browser model in one import.
|
|
2
|
-
// Re-exports
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// Re-exports two shapes:
|
|
3
|
+
//
|
|
4
|
+
// * `createRuntime` + `local()` — runtime-compatible provider so local
|
|
5
|
+
// participates in `/model` alongside anthropic and openai. Same wiring
|
|
6
|
+
// pattern as bundle/anthropic and bundle/openai.
|
|
7
|
+
//
|
|
8
|
+
// * `createTransformersRenderer` + `splitThinking` — direct renderer for
|
|
9
|
+
// hosts that paint a one-shot reply themselves (e.g. an offline
|
|
10
|
+
// fallback that bypasses the turn loop entirely).
|
|
11
|
+
//
|
|
12
|
+
// Consumers shouldn't need to know which underlying library powers either
|
|
13
|
+
// path — the file name signals the category (local), the export name on
|
|
14
|
+
// the renderer signals the library (transformers.js).
|
|
7
15
|
|
|
8
16
|
export { createPip } from '../pip-core.esm.js';
|
|
9
|
-
export {
|
|
17
|
+
export { createRuntime } from '../runtime.esm.js';
|
|
18
|
+
export { local, createTransformersRenderer, splitThinking } from '../providers/local.esm.js';
|
package/package.json
CHANGED
package/pip-core.esm.js
CHANGED
|
@@ -1559,7 +1559,7 @@ export function createPip(opts = {}) {
|
|
|
1559
1559
|
|
|
1560
1560
|
// Built-in slash-command registry. Hosts call pip.registerSlash({name,
|
|
1561
1561
|
// description, handler, complete?}) — dispatched in submit() before
|
|
1562
|
-
// built-
|
|
1562
|
+
// built-in /clear and the legacy onSlash callback. Keeps the
|
|
1563
1563
|
// single mental model: one place to declare "what / commands does this
|
|
1564
1564
|
// app expose?", and the autocomplete picks them up automatically.
|
|
1565
1565
|
const _slashCommands = new Map();
|
|
@@ -1652,22 +1652,10 @@ export function createPip(opts = {}) {
|
|
|
1652
1652
|
function defaultSlashSource() {
|
|
1653
1653
|
const out = [];
|
|
1654
1654
|
for (const s of _slashCommands.values()) out.push({ name: s.name, description: s.description, complete: s.complete });
|
|
1655
|
-
if (!_slashCommands.has("help")) out.push({ name: "help", description: "list commands" });
|
|
1656
1655
|
if (!_slashCommands.has("clear")) out.push({ name: "clear", description: "clear chat history" });
|
|
1657
1656
|
return out;
|
|
1658
1657
|
}
|
|
1659
1658
|
function dispatchBuiltinSlash(cmd) {
|
|
1660
|
-
if (cmd === "help" || cmd === "?") {
|
|
1661
|
-
// The slash autocomplete dropdown already lists every command with
|
|
1662
|
-
// its description — /help duplicating that as a chat turn was visual
|
|
1663
|
-
// noise and history clutter. Reopen the dropdown in cmd-name mode
|
|
1664
|
-
// instead, same surface a user gets by typing `/`.
|
|
1665
|
-
input.value = "/";
|
|
1666
|
-
input.setSelectionRange(1, 1);
|
|
1667
|
-
input.focus();
|
|
1668
|
-
updateSlashSuggest();
|
|
1669
|
-
return { clearedUI: true };
|
|
1670
|
-
}
|
|
1671
1659
|
if (cmd === "clear") {
|
|
1672
1660
|
history.length = 0;
|
|
1673
1661
|
turns.innerHTML = "";
|
|
@@ -2091,7 +2079,7 @@ export function createPip(opts = {}) {
|
|
|
2091
2079
|
|
|
2092
2080
|
function updateSlashSuggest() {
|
|
2093
2081
|
// No early-return on missing slashSource — the built-in source ensures
|
|
2094
|
-
// even hosts that didn't wire anything still see /
|
|
2082
|
+
// even hosts that didn't wire anything still see /clear.
|
|
2095
2083
|
const value = input.value;
|
|
2096
2084
|
if (!value.startsWith("/")) return closeSlashSuggest();
|
|
2097
2085
|
const slice = value.slice(1);
|
|
@@ -2169,19 +2157,15 @@ export function createPip(opts = {}) {
|
|
|
2169
2157
|
e.preventDefault();
|
|
2170
2158
|
closeSlashSuggest();
|
|
2171
2159
|
} else if (e.key === "Enter") {
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2175
|
-
//
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
const value = input.value;
|
|
2182
|
-
const inCmdMode = value.startsWith("/") && !value.includes(" ");
|
|
2183
|
-
if (inCmdMode) { e.preventDefault(); acceptSlashSuggest(); }
|
|
2184
|
-
else acceptSlashSuggest();
|
|
2160
|
+
// Accept the highlighted entry and submit in one keystroke — CLI
|
|
2161
|
+
// command-palette convention (VS Code, shell tab+enter). Tab is the
|
|
2162
|
+
// path for "accept and keep editing" — e.g. pick the command, then
|
|
2163
|
+
// type args before running. The early-return at the top of this
|
|
2164
|
+
// handler guarantees a visible dropdown with at least one item, so
|
|
2165
|
+
// accept always has a target.
|
|
2166
|
+
e.preventDefault();
|
|
2167
|
+
acceptSlashSuggest();
|
|
2168
|
+
form.requestSubmit();
|
|
2185
2169
|
}
|
|
2186
2170
|
});
|
|
2187
2171
|
|
package/providers/local.esm.js
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
// In-browser model
|
|
2
|
-
// (not part of pip-runtime's turn loop) — hands the host a tokenizer +
|
|
3
|
-
// model loaded over WebGPU and a streaming generate() that paints into
|
|
4
|
-
// a Pip turnEl. Includes the download-progress UI and a `<think>` pill
|
|
5
|
-
// for reasoning models, so consumers don't reinvent either.
|
|
1
|
+
// In-browser model via transformers.js + WebGPU. Two shapes ship:
|
|
6
2
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// 1. `local({ model, dtype, maxTokens, genParams })` — runtime-compatible
|
|
4
|
+
// provider, slots into `createRuntime({ models: [{ provider: local(...) }] })`
|
|
5
|
+
// next to anthropic() and openai(). Wraps the renderer below and adapts
|
|
6
|
+
// its setReplyText-callback paint into the runtime's async-generator
|
|
7
|
+
// event protocol. Use this when local should participate in `/model`.
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// genParams: { temperature: 0.1, top_k: 50, repetition_penalty: 1.05 },
|
|
17
|
-
// });
|
|
18
|
-
// const text = await renderer.generate({
|
|
19
|
-
// messages, // chat messages array
|
|
20
|
-
// turnEl, // pip turnEl (download progress + <think> pill render here)
|
|
21
|
-
// setReplyText, // pip's setReplyText
|
|
22
|
-
// signal, // optional AbortSignal for mid-stream cancel
|
|
23
|
-
// });
|
|
9
|
+
// 2. `createTransformersRenderer()` — direct renderer for hosts that build
|
|
10
|
+
// their own one-shot paint loop (e.g. neves.cloud's offline fallback,
|
|
11
|
+
// where local isn't a /model peer but a recovery path when the relay is
|
|
12
|
+
// unreachable). Exposes `setModel` + `generate({ messages, turnEl,
|
|
13
|
+
// setReplyText, signal })` returning a Promise<text>.
|
|
24
14
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// model, repeat generate() calls reuse the loaded artifacts.
|
|
15
|
+
// Both share the same underlying model load (download-progress UI +
|
|
16
|
+
// <think> pill rendering); the provider just adapts the call shape.
|
|
28
17
|
|
|
29
18
|
import { showLoading, hideLoading } from '../pip-core.esm.js';
|
|
30
19
|
|
|
@@ -270,4 +259,49 @@ export function createTransformersRenderer() {
|
|
|
270
259
|
};
|
|
271
260
|
}
|
|
272
261
|
|
|
262
|
+
// Runtime-compatible provider. The renderer streams reply text via the
|
|
263
|
+
// setReplyText callback (cumulative buffer per call); we proxy that
|
|
264
|
+
// callback, diff each call into a `text_delta` event, and yield events
|
|
265
|
+
// the runtime's turn loop already consumes. The <think> pill mounts onto
|
|
266
|
+
// turnEl directly inside the renderer — unaffected, still works.
|
|
267
|
+
//
|
|
268
|
+
// One pitfall handled: the renderer occasionally re-paints the same
|
|
269
|
+
// buffer (no new tokens emitted between calls), so the diff guards
|
|
270
|
+
// against zero-length deltas. AbortSignal flows through naturally —
|
|
271
|
+
// the underlying TextStreamer throws AbortError, which we surface.
|
|
272
|
+
export function local({ model, dtype = 'q4', maxTokens = 256, genParams } = {}) {
|
|
273
|
+
const renderer = createTransformersRenderer();
|
|
274
|
+
if (model) renderer.setModel({ id: model, dtype, maxTokens, genParams });
|
|
275
|
+
|
|
276
|
+
return ({ messages, signal, turnEl, setReplyText }) => (async function* () {
|
|
277
|
+
let lastFull = '';
|
|
278
|
+
const queue = [];
|
|
279
|
+
let wake = null;
|
|
280
|
+
let done = false;
|
|
281
|
+
let error = null;
|
|
282
|
+
|
|
283
|
+
const proxySetReplyText = (_el, fullText) => {
|
|
284
|
+
const delta = fullText.slice(lastFull.length);
|
|
285
|
+
lastFull = fullText;
|
|
286
|
+
if (delta) {
|
|
287
|
+
queue.push({ type: 'text_delta', text: delta });
|
|
288
|
+
wake?.();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
renderer.generate({ messages, turnEl, setReplyText: proxySetReplyText, signal })
|
|
293
|
+
.then(() => { done = true; wake?.(); })
|
|
294
|
+
.catch((e) => { error = e; done = true; wake?.(); });
|
|
295
|
+
|
|
296
|
+
while (true) {
|
|
297
|
+
if (queue.length) { yield queue.shift(); continue; }
|
|
298
|
+
if (done) break;
|
|
299
|
+
await new Promise((r) => { wake = r; });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (error) throw error;
|
|
303
|
+
yield { type: 'turn_end', stopReason: 'end_turn' };
|
|
304
|
+
})();
|
|
305
|
+
}
|
|
306
|
+
|
|
273
307
|
export { splitThinking };
|
package/runtime.esm.js
CHANGED
|
@@ -121,6 +121,12 @@ export function createRuntime({
|
|
|
121
121
|
tools: buildToolList(),
|
|
122
122
|
system: systemFn(),
|
|
123
123
|
signal: abortCtrl.signal,
|
|
124
|
+
// turnEl + setReplyText let renderer-shaped providers (e.g. the
|
|
125
|
+
// local transformers wrapper) paint progress bars and <think>
|
|
126
|
+
// pills directly onto the active turn. Wire-shape providers
|
|
127
|
+
// (Anthropic, OpenAI) ignore unknown keys.
|
|
128
|
+
turnEl,
|
|
129
|
+
setReplyText,
|
|
124
130
|
});
|
|
125
131
|
|
|
126
132
|
const assistantContent = [];
|
|
@@ -210,8 +216,7 @@ export function createRuntime({
|
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
// Registry-first dispatch. Host-registered commands win over built-ins
|
|
213
|
-
// so `/
|
|
214
|
-
// Built-in `/help` enumerates both layers so users see one combined list.
|
|
219
|
+
// so `/clear` (etc.) can be overridden if a host wants different behavior.
|
|
215
220
|
function onSlash(text) {
|
|
216
221
|
const slice = text.slice(1);
|
|
217
222
|
const sp = slice.indexOf(' ');
|
|
@@ -255,17 +260,6 @@ export function createRuntime({
|
|
|
255
260
|
const suffix = target.label ? ` (${target.label})` : '';
|
|
256
261
|
return { reply: `Switched to \`${target.name}\`${suffix}.` };
|
|
257
262
|
}
|
|
258
|
-
if (cmd === 'help' || cmd === '?') {
|
|
259
|
-
const lines = ['**Commands:**'];
|
|
260
|
-
for (const s of registeredSlash.values()) {
|
|
261
|
-
lines.push(`- \`/${s.name}\` — ${s.description || ''}`);
|
|
262
|
-
}
|
|
263
|
-
lines.push('- `/clear` — reset conversation');
|
|
264
|
-
lines.push('- `/tools` — list registered tools');
|
|
265
|
-
if (models.length) lines.push('- `/model` — list or switch active model');
|
|
266
|
-
lines.push('- `/help` — this list');
|
|
267
|
-
return { reply: lines.join('\n') };
|
|
268
|
-
}
|
|
269
263
|
return null;
|
|
270
264
|
}
|
|
271
265
|
|
|
@@ -281,7 +275,7 @@ export function createRuntime({
|
|
|
281
275
|
}
|
|
282
276
|
|
|
283
277
|
// Slash commands. Same shape as registerTool: name + handler are required;
|
|
284
|
-
// description and complete() are surfaced by autocomplete
|
|
278
|
+
// description and complete() are surfaced by the autocomplete dropdown.
|
|
285
279
|
// handler(argsString) returns { reply?, clearedUI?, passThrough? } | null.
|
|
286
280
|
function registerSlash(s) {
|
|
287
281
|
if (!s || !s.name || typeof s.handler !== 'function') {
|
|
@@ -311,7 +305,6 @@ export function createRuntime({
|
|
|
311
305
|
.filter((n) => n.toLowerCase().startsWith(partial.toLowerCase())),
|
|
312
306
|
});
|
|
313
307
|
}
|
|
314
|
-
out.push({ name: 'help', description: 'list commands' });
|
|
315
308
|
return out;
|
|
316
309
|
}
|
|
317
310
|
function slashSource() {
|