@chatpanel/gateway 0.6.26 → 0.6.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
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/server.js CHANGED
@@ -35,7 +35,7 @@ import * as nerEngine from './ner-engine.js';
35
35
  import * as sttEngine from './stt-engine.js';
36
36
  import * as diarizeEngine from './diarize-engine.js';
37
37
  import { MODEL_CATALOG, isKnownModel, isValidCustomModelId } from './models.js';
38
- import { STT_MODEL_CATALOG, isKnownSttModel, isValidCustomSttId, DEFAULT_STT_MODEL } from './stt-models.js';
38
+ import { STT_MODEL_CATALOG, isKnownSttModel, isValidCustomSttId, DEFAULT_STT_MODEL, STT_DTYPES, isValidDtype } from './stt-models.js';
39
39
  import { resolvePro, checkQuota, consume, usage } from './freegate.js';
40
40
  import { publicConfig, applyConfigPatch, persistConfig, configPath } from './configstore.js';
41
41
  import { resolveDestination, aggregateModelsAsync } from './router.js';
@@ -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.26';
46
+ export const VERSION = '0.6.27';
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.
@@ -686,6 +686,12 @@ export function createGateway(cfg = loadConfig()) {
686
686
  state: sttEngine.state(),
687
687
  progress: sttEngine.progress(),
688
688
  available,
689
+ // Precision (quantization) picker: current choice + selectable options +
690
+ // the dtype actually loaded. On WASM only fp32 loads (see runtimeDtype).
691
+ dtype: cfg.stt?.dtype || 'auto',
692
+ loadedDtype: sttEngine.health().dtype,
693
+ runtime: sttEngine.health().runtime,
694
+ dtypes: STT_DTYPES,
689
695
  });
690
696
  }
691
697
  if (req.method === 'POST') {
@@ -694,10 +700,13 @@ export function createGateway(cfg = loadConfig()) {
694
700
  const id = body && typeof body.id === 'string' ? body.id.trim() : null;
695
701
  // Curated catalog OR a strictly-validated custom whisper id (Advanced).
696
702
  if (!id || !(isKnownSttModel(id) || isValidCustomSttId(id))) return sendJson(res, 400, { error: { message: 'unknown or invalid model id', type: 'bad_model' } });
703
+ // Optional precision override (q8/q4/fp16/…); 'auto' clears it.
704
+ const dtype = typeof body.dtype === 'string' && isValidDtype(body.dtype) ? body.dtype : undefined;
697
705
  if (cfg.stt) cfg.stt.model = id; else cfg.stt = { enabled: true, model: id, allowDownload: true };
706
+ if (dtype) cfg.stt.dtype = dtype === 'auto' ? null : dtype;
698
707
  try { persistConfig(cfg, configPath()); } catch { /* best effort */ }
699
- sttEngine.setModel(id, { onLog: (m) => console.log(m) });
700
- return sendJson(res, 202, { accepted: true, active: id, state: sttEngine.state(), progress: sttEngine.progress() });
708
+ sttEngine.setModel(id, { onLog: (m) => console.log(m), dtype: dtype || cfg.stt.dtype || 'auto' });
709
+ return sendJson(res, 202, { accepted: true, active: id, dtype: cfg.stt.dtype || 'auto', state: sttEngine.state(), progress: sttEngine.progress() });
701
710
  }
702
711
  }
703
712
  // Speaker (diarization) model manager — the "who said what" x-vector model.
@@ -725,7 +734,7 @@ export function createGateway(cfg = loadConfig()) {
725
734
  // Kick the model load on first use (single-flight; downloads once). The
726
735
  // client follows progress on the session's SSE stream.
727
736
  if (!sttEngine.isReady()) {
728
- sttEngine.init({ model: cfg.stt?.model || DEFAULT_STT_MODEL, allowDownload: cfg.stt?.allowDownload !== false, onLog: (m) => console.log(m) });
737
+ sttEngine.init({ model: cfg.stt?.model || DEFAULT_STT_MODEL, allowDownload: cfg.stt?.allowDownload !== false, dtype: cfg.stt?.dtype || undefined, onLog: (m) => console.log(m) });
729
738
  }
730
739
  // Diarization is another OPTIONAL stage: load its model only when a session
731
740
  // asks for it (never on the dictation path).
package/src/stt-engine.js CHANGED
@@ -85,8 +85,8 @@ export function health() {
85
85
 
86
86
  // (Re)load a model into _pipe. Same contract as ner-engine.loadModel: fail-open,
87
87
  // and a failed SWITCH keeps the previous working pipeline.
88
- /** @param {string} modelId @param {{ log?: (m: string) => void, allowDownload?: boolean }} [opts] */
89
- async function loadModel(modelId, { log = () => {}, allowDownload = true } = {}) {
88
+ /** @param {string} modelId @param {{ log?: (m: string) => void, allowDownload?: boolean, dtype?: string }} [opts] */
89
+ async function loadModel(modelId, { log = () => {}, allowDownload = true, dtype: dtypeOverride = null } = {}) {
90
90
  const prevPipe = _pipe;
91
91
  const prevModel = _model;
92
92
  let lib;
@@ -98,7 +98,13 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
98
98
  return false;
99
99
  }
100
100
 
101
- const haveLocal = modelOnDisk(modelId);
101
+ // Resolve the precision up front so the on-disk check targets the RIGHT files —
102
+ // else switching precision (e.g. q8→fp16) would see the q8 files as "present" and
103
+ // try to load fp16 offline, which fails.
104
+ const chosen = dtypeOverride && dtypeOverride !== 'auto' ? dtypeOverride : null;
105
+ const dtype = chosen || sttModelDtype(modelId) || runtimeDtype();
106
+
107
+ const haveLocal = modelOnDisk(modelId, dtype);
102
108
  lib.env.allowRemoteModels = haveLocal ? false : !!allowDownload;
103
109
  if (!haveLocal && !allowDownload) {
104
110
  _state = 'error'; _err = 'model not on disk and downloads disabled';
@@ -117,8 +123,6 @@ async function loadModel(modelId, { log = () => {}, allowDownload = true } = {})
117
123
  if (!haveLocal) { _progress = { model: modelId, file: null, pct: 0 }; log(`[stt] downloading model ${modelId} (one-time${isCustom ? ', from Hugging Face' : ''})…`); }
118
124
 
119
125
  try {
120
- // Per-model override wins (catalog `dtype`), else the runtime-appropriate default.
121
- const dtype = sttModelDtype(modelId) || runtimeDtype();
122
126
  const progress_callback = (p) => {
123
127
  if (!p) return;
124
128
  const pct = typeof p.progress === 'number' ? Math.round(p.progress) : (_progress?.pct ?? 0);
@@ -163,15 +167,17 @@ export function init(cfg = {}) {
163
167
  const log = typeof cfg.onLog === 'function' ? cfg.onLog : () => {};
164
168
  _model = cfg.model || DEFAULT_STT_MODEL;
165
169
  _state = 'loading';
166
- _initPromise = loadModel(_model, { log, allowDownload: cfg.allowDownload !== false });
170
+ _initPromise = loadModel(_model, { log, allowDownload: cfg.allowDownload !== false, dtype: cfg.dtype });
167
171
  return _initPromise;
168
172
  }
169
173
 
170
174
  export async function setModel(modelId, opts = {}) {
171
175
  const log = typeof opts.onLog === 'function' ? opts.onLog : () => {};
172
176
  if (!modelId) return false;
173
- if (modelId === _model && isReady()) return true;
174
- return loadModel(modelId, { log, allowDownload: opts.allowDownload !== false });
177
+ // Re-load if the model OR the requested precision changed.
178
+ const wantDtype = opts.dtype && opts.dtype !== 'auto' ? opts.dtype : (sttModelDtype(modelId) || runtimeDtype());
179
+ if (modelId === _model && isReady() && _dtype === wantDtype) return true;
180
+ return loadModel(modelId, { log, allowDownload: opts.allowDownload !== false, dtype: opts.dtype });
175
181
  }
176
182
 
177
183
  // ── Streaming sessions ──────────────────────────────────────────────────────────
package/src/stt-models.js CHANGED
@@ -85,3 +85,19 @@ export function isValidCustomSttId(id) {
85
85
  const s = String(id || '');
86
86
  return /^[A-Za-z0-9][\w.-]*\/[\w.-]+$/.test(s) && !s.includes('..');
87
87
  }
88
+
89
+ // Selectable quantizations (precision). 'auto' = let the runtime decide (q8 on
90
+ // native, fp32 on WASM). Each is a transformers.js dtype; a model must actually
91
+ // ship that ONNX variant (the load fails open otherwise). Ordered fastest→best.
92
+ export const STT_DTYPES = [
93
+ { id: 'auto', label: 'Auto (recommended)', note: 'q8 on the native gateway, fp32 on the WASM binary.' },
94
+ { id: 'q8', label: 'q8 — fast, balanced', note: 'Int8. Best speed/quality trade-off (native default).' },
95
+ { id: 'q4', label: 'q4 — smallest & fastest', note: '4-bit. Least memory; slightly lower accuracy.' },
96
+ { id: 'int8', label: 'int8', note: 'Alternative int8 export.' },
97
+ { id: 'fp16', label: 'fp16 — more accurate', note: 'Half precision. Larger, a bit slower.' },
98
+ { id: 'fp32', label: 'fp32 — most accurate (slow)', note: 'Full precision. Largest + slowest; the only one that loads on the WASM binary.' },
99
+ ];
100
+
101
+ export function isValidDtype(d) {
102
+ return STT_DTYPES.some((x) => x.id === d);
103
+ }