@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 +1 -1
- package/src/server.js +14 -5
- package/src/stt-engine.js +14 -8
- package/src/stt-models.js +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
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
|
+
}
|