@chatpanel/gateway 0.6.25 → 0.6.26

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 CHANGED
@@ -54,6 +54,15 @@ chatpanel-gateway
54
54
  # backend : bridge (agent: codex, via http://127.0.0.1:4319)
55
55
  ```
56
56
 
57
+ > **Binary vs. npm — same features, very different local-AI speed.** Both run
58
+ > identical redaction/routing. But the standalone binary runs the local models
59
+ > (speech-to-text, diarization, NER) on the **WASM** runtime — **fp32-only,
60
+ > single-threaded, slow**. The **npm** install uses the **native** runtime with
61
+ > **quantized (q8)** models — in our tests **~10× faster** speech-to-text
62
+ > (real-time even on larger, more accurate models). If you'll use voice/meeting
63
+ > features, install via **npm**. Don't run both — they can shadow each other on
64
+ > `PATH`; check `GET /health` → `stt.runtime` (`native` vs `wasm`).
65
+
57
66
  Then point your front-end agent at it. **opencode** (`opencode.json`):
58
67
 
59
68
  ```jsonc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
4
4
  "description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ner-engine.js CHANGED
@@ -179,6 +179,7 @@ export async function ensureLib() {
179
179
  // (Re)load a specific model into _pipe. Downloads it first if missing (and allowed).
180
180
  // Reusable by init() and setModel(). Fail-open: on error, state='error', _pipe stays
181
181
  // whatever it was (so a failed SWITCH doesn't kill a working detector).
182
+ /** @param {string} modelId @param {{ log?: (m: string) => void, allowDownload?: boolean }} [opts] */
182
183
  async function loadModel(modelId, { log = () => {}, allowDownload = true } = {}) {
183
184
  const prevPipe = _pipe;
184
185
  const prevModel = _model;
@@ -211,18 +212,26 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
211
212
  if (!haveLocal) { _progress = { model: modelId, file: null, pct: 0 }; log(`[ner] downloading model ${modelId} (one-time${isCustom ? ', from Hugging Face' : ''})…`); }
212
213
 
213
214
  try {
214
- const pipe = await lib.pipeline('token-classification', modelId, {
215
- dtype: 'q8',
216
- progress_callback: (p) => {
217
- if (!p) return;
218
- const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
219
- if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
220
- _progress = { model: modelId, file: p.file || _progress?.file || null, pct };
221
- } else if (p.status === 'done' && p.file) {
222
- log(`[ner] fetched ${p.file}`);
223
- }
224
- },
225
- });
215
+ const progress_callback = (p) => {
216
+ if (!p) return;
217
+ const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
218
+ if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
219
+ _progress = { model: modelId, file: p.file || _progress?.file || null, pct };
220
+ } else if (p.status === 'done' && p.file) {
221
+ log(`[ner] fetched ${p.file}`);
222
+ }
223
+ };
224
+ let pipe;
225
+ try {
226
+ pipe = await lib.pipeline('token-classification', modelId, { dtype: 'q8', progress_callback });
227
+ } catch (e) {
228
+ // Mirror gap → fall back to Hugging Face (same as stt-engine).
229
+ if (haveLocal || isCustom || !allowDownload) throw e;
230
+ log(`[ner] mirror fetch failed (${String(e.message).slice(0, 80)}) — retrying from Hugging Face…`);
231
+ lib.env.remoteHost = 'https://huggingface.co/';
232
+ lib.env.allowRemoteModels = true;
233
+ pipe = await lib.pipeline('token-classification', modelId, { dtype: 'q8', progress_callback });
234
+ }
226
235
  // Swap in the new pipeline, dispose the old one (free its WASM/native session).
227
236
  _pipe = pipe; _model = modelId; _state = 'ready'; _err = null; _progress = null;
228
237
  if (prevPipe && prevPipe !== pipe) { try { await prevPipe.dispose?.(); } catch { /* ignore */ } }
package/src/server.js CHANGED
@@ -43,7 +43,7 @@ import * as openai from './openai.js';
43
43
  import * as responses from './responses.js';
44
44
  import * as anthropic from './anthropic.js';
45
45
 
46
- export const VERSION = '0.6.25';
46
+ export const VERSION = '0.6.26';
47
47
 
48
48
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
49
49
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
package/src/stt-engine.js CHANGED
@@ -119,18 +119,28 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
119
119
  try {
120
120
  // Per-model override wins (catalog `dtype`), else the runtime-appropriate default.
121
121
  const dtype = sttModelDtype(modelId) || runtimeDtype();
122
- const pipe = await lib.pipeline('automatic-speech-recognition', modelId, {
123
- dtype,
124
- progress_callback: (p) => {
125
- if (!p) return;
126
- const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
127
- if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
128
- _progress = { model: modelId, file: p.file || _progress?.file || null, pct };
129
- } else if (p.status === 'done' && p.file) {
130
- log(`[stt] fetched ${p.file}`);
131
- }
132
- },
133
- });
122
+ const progress_callback = (p) => {
123
+ if (!p) return;
124
+ const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
125
+ if (p.status === 'progress' || p.status === 'download' || p.status === 'initiate') {
126
+ _progress = { model: modelId, file: p.file || _progress?.file || null, pct };
127
+ } else if (p.status === 'done' && p.file) {
128
+ log(`[stt] fetched ${p.file}`);
129
+ }
130
+ };
131
+ let pipe;
132
+ try {
133
+ pipe = await lib.pipeline('automatic-speech-recognition', modelId, { dtype, progress_callback });
134
+ } catch (e) {
135
+ // The dl.chatpanel.net mirror only proxies an allowlist; a catalog model
136
+ // missing from it (or any mirror hiccup) 403s. Fall back to Hugging Face so
137
+ // a download is never blocked by a mirror gap. (Custom ids already use HF.)
138
+ if (haveLocal || isCustom || !allowDownload) throw e;
139
+ log(`[stt] mirror fetch failed (${String(e.message).slice(0, 80)}) — retrying from Hugging Face…`);
140
+ lib.env.remoteHost = 'https://huggingface.co/';
141
+ lib.env.allowRemoteModels = true;
142
+ pipe = await lib.pipeline('automatic-speech-recognition', modelId, { dtype, progress_callback });
143
+ }
134
144
  _pipe = pipe; _model = modelId; _state = 'ready'; _err = null; _progress = null; _dtype = dtype;
135
145
  if (prevPipe && prevPipe !== pipe) { try { await prevPipe.dispose?.(); } catch { /* ignore */ } }
136
146
  log(`[stt] ready — model ${modelId} @ ${dtype} (in-process, offline) — local dictation active`);