@chatpanel/gateway 0.3.1 → 0.4.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/package.json +2 -2
- package/src/configstore.js +13 -1
- package/src/server.js +48 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@chatpanel/pii": "^0.
|
|
31
|
+
"@chatpanel/pii": "^0.2.1"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://chatpanel.net",
|
|
34
34
|
"repository": {
|
package/src/configstore.js
CHANGED
|
@@ -19,7 +19,7 @@ export function persistConfig(cfg, path = configPath()) {
|
|
|
19
19
|
host: cfg.host, port: cfg.port, backend: cfg.backend,
|
|
20
20
|
bridge: cfg.bridge, upstreams: cfg.upstreams, redaction: cfg.redaction,
|
|
21
21
|
ner: cfg.ner, allowedOrigins: cfg.allowedOrigins, maxBodyBytes: cfg.maxBodyBytes,
|
|
22
|
-
pro: cfg.pro, logRequests: cfg.logRequests,
|
|
22
|
+
pro: cfg.pro, logRequests: cfg.logRequests, tools: cfg.tools,
|
|
23
23
|
};
|
|
24
24
|
writeFileSync(path, JSON.stringify(out, null, 2));
|
|
25
25
|
}
|
|
@@ -44,6 +44,11 @@ export function publicConfig(cfg, { proUnlocked = false } = {}) {
|
|
|
44
44
|
allowedOrigins: Array.isArray(cfg.allowedOrigins) ? cfg.allowedOrigins : [],
|
|
45
45
|
pro: { unlocked: proUnlocked, hasToken: !!cfg.pro?.entitlementToken, free: cfg.pro?.free },
|
|
46
46
|
logRequests: !!cfg.logRequests,
|
|
47
|
+
tools: {
|
|
48
|
+
autoNarrow: cfg.tools?.autoNarrow !== false,
|
|
49
|
+
maxPerTurn: Number(cfg.tools?.maxPerTurn) > 0 ? Number(cfg.tools.maxPerTurn) : 8,
|
|
50
|
+
narrowAll: !!cfg.tools?.narrowAll,
|
|
51
|
+
},
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -96,5 +101,12 @@ export function applyConfigPatch(cfg, patch = {}) {
|
|
|
96
101
|
if (Number.isFinite(cap) && cap >= 0) cfg.pro.free.maxRequestsPerDay = cap;
|
|
97
102
|
}
|
|
98
103
|
if (typeof patch.logRequests === 'boolean') cfg.logRequests = patch.logRequests;
|
|
104
|
+
if (patch.tools && typeof patch.tools === 'object') {
|
|
105
|
+
cfg.tools = cfg.tools || {};
|
|
106
|
+
if ('autoNarrow' in patch.tools) cfg.tools.autoNarrow = !!patch.tools.autoNarrow;
|
|
107
|
+
if ('narrowAll' in patch.tools) cfg.tools.narrowAll = !!patch.tools.narrowAll;
|
|
108
|
+
const cap = Number(patch.tools.maxPerTurn);
|
|
109
|
+
if (Number.isFinite(cap) && cap >= 1) cfg.tools.maxPerTurn = Math.floor(cap);
|
|
110
|
+
}
|
|
99
111
|
return cfg;
|
|
100
112
|
}
|
package/src/server.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createServer } from 'node:http';
|
|
|
21
21
|
import { loadConfig } from './config.js';
|
|
22
22
|
import { redactSegments } from './redact.js';
|
|
23
23
|
import { pipeRestoredStream, pipeRestoredOpenAIStream, makeTokenRestorer } from './stream.js';
|
|
24
|
-
import { restoreText, effectiveTier, gatedDictionary } from '@chatpanel/pii';
|
|
24
|
+
import { restoreText, effectiveTier, gatedDictionary, narrowSpecs } from '@chatpanel/pii';
|
|
25
25
|
import { streamBridgeChat, readBridgeToken, openBridgeChat } from './bridge.js';
|
|
26
26
|
import { createRelaySession, getRelaySession, endRelaySession, pumpBridgeStream, deliverToolResult, toolsToSpecs, parseToolCallId } from './toolrelay.js';
|
|
27
27
|
import { shaperFor } from './shape.js';
|
|
@@ -33,10 +33,40 @@ import * as openai from './openai.js';
|
|
|
33
33
|
import * as responses from './responses.js';
|
|
34
34
|
import * as anthropic from './anthropic.js';
|
|
35
35
|
|
|
36
|
-
export const VERSION = '0.
|
|
36
|
+
export const VERSION = '0.4.0';
|
|
37
37
|
|
|
38
38
|
const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
|
|
39
39
|
|
|
40
|
+
// Auto-narrow: arm only the top-K most-relevant MCP tools per turn (speed). Mirrors
|
|
41
|
+
// the extension's AUTO mode via the SAME shared ranker. We narrow only tools whose
|
|
42
|
+
// name looks like an MCP tool (server-prefixed) so a client's CORE tools (bash,
|
|
43
|
+
// read, edit…) are never dropped — that would break agent clients like OpenCode.
|
|
44
|
+
const DEFAULT_GATEWAY_TOOL_CAP = 8;
|
|
45
|
+
const MCP_NAME_RE = /^mcp[_-]/i;
|
|
46
|
+
const toolName = (t) => (t && t.function && t.function.name) || (t && t.name) || '';
|
|
47
|
+
const toolDesc = (t) => (t && t.function && t.function.description) || (t && t.description) || '';
|
|
48
|
+
|
|
49
|
+
// Flatten message/content shapes to the latest user text — the query we rank tools against.
|
|
50
|
+
function textFromContent(content) {
|
|
51
|
+
if (typeof content === 'string') return content;
|
|
52
|
+
if (Array.isArray(content)) return content.map((p) => (typeof p === 'string' ? p : (p && (p.text || p.content)) || '')).join(' ');
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
function latestUserText(body, kind) {
|
|
56
|
+
if (!body) return '';
|
|
57
|
+
if (kind === 'responses') {
|
|
58
|
+
const inp = body.input;
|
|
59
|
+
if (typeof inp === 'string') return inp;
|
|
60
|
+
if (Array.isArray(inp)) return inp.map((x) => textFromContent(x && (x.content ?? x))).join(' ');
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
const msgs = Array.isArray(body.messages) ? body.messages : [];
|
|
64
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
65
|
+
if (msgs[i] && msgs[i].role === 'user') return textFromContent(msgs[i].content);
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
const HOP_BY_HOP = new Set([
|
|
41
71
|
'host', 'connection', 'content-length', 'transfer-encoding',
|
|
42
72
|
'accept-encoding', 'content-encoding', 'keep-alive',
|
|
@@ -345,10 +375,24 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
345
375
|
let body = null;
|
|
346
376
|
let outBody = raw;
|
|
347
377
|
let redactedCount = 0;
|
|
378
|
+
let narrowedTools = 0;
|
|
348
379
|
let isPro = true;
|
|
349
380
|
if (r.redactable && req.method === 'POST' && raw.length) {
|
|
350
381
|
try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
|
|
351
382
|
if (body) {
|
|
383
|
+
// Auto-narrow tools to the top-K most relevant for this turn (speed) —
|
|
384
|
+
// same shared ranker as the extension's AUTO mode. Only MCP-named tools
|
|
385
|
+
// are narrowed; the client's core tools (bash/read/edit…) are always kept,
|
|
386
|
+
// unless tools.narrowAll is set. Mutates body.tools BEFORE redaction so
|
|
387
|
+
// both the API forward and the bridge relay see the trimmed set.
|
|
388
|
+
const tcfg = cfg.tools || {};
|
|
389
|
+
if (tcfg.autoNarrow !== false && Array.isArray(body.tools) && body.tools.length) {
|
|
390
|
+
const cap = Number(tcfg.maxPerTurn) > 0 ? Number(tcfg.maxPerTurn) : DEFAULT_GATEWAY_TOOL_CAP;
|
|
391
|
+
const keep = tcfg.narrowAll ? null : (t) => !MCP_NAME_RE.test(toolName(t));
|
|
392
|
+
const before = body.tools.length;
|
|
393
|
+
body.tools = narrowSpecs(body.tools, latestUserText(body, r.kind), { cap, keep, name: toolName, description: toolDesc });
|
|
394
|
+
narrowedTools = before - body.tools.length;
|
|
395
|
+
}
|
|
352
396
|
// Free/Pro gate: meter the request and pick the effective tier.
|
|
353
397
|
isPro = await resolvePro(cfg.pro?.entitlementToken);
|
|
354
398
|
const allow = meter(cfg, isPro);
|
|
@@ -372,8 +416,8 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
372
416
|
// API we forward to). Falls back to the legacy backend when none configured.
|
|
373
417
|
const dest = resolveDestination(body?.model, cfg, r.kind);
|
|
374
418
|
if (cfg.logRequests && r.redactable) {
|
|
375
|
-
recordRequest({ t: Date.now(), model: body?.model || null, dest: dest ? dest.id : null, type: dest ? dest.type : null, redacted: redactedCount });
|
|
376
|
-
console.log(`[gateway] ${req.method} ${pathname} · model=${body?.model || '-'} → ${dest ? `${dest.id}(${dest.type})` : 'none'} · redacted ${redactedCount}`);
|
|
419
|
+
recordRequest({ t: Date.now(), model: body?.model || null, dest: dest ? dest.id : null, type: dest ? dest.type : null, redacted: redactedCount, narrowed: narrowedTools });
|
|
420
|
+
console.log(`[gateway] ${req.method} ${pathname} · model=${body?.model || '-'} → ${dest ? `${dest.id}(${dest.type})` : 'none'} · redacted ${redactedCount}${narrowedTools ? ` · narrowed -${narrowedTools} tools` : ''}`);
|
|
377
421
|
}
|
|
378
422
|
if (dest && dest.type === 'api') {
|
|
379
423
|
if (!dest.baseUrl) return sendJson(res, 502, { error: `destination "${dest.id}" has no baseUrl` });
|