@chatpanel/gateway 0.5.1 → 0.5.2
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/anthropic.js +9 -0
- package/src/openai.js +10 -0
- package/src/responses.js +7 -0
- package/src/server.js +28 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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/anthropic.js
CHANGED
|
@@ -9,6 +9,15 @@ export function matches(pathname) {
|
|
|
9
9
|
return /\/messages$/.test(pathname);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Append a (non-redacted) instruction to the system prompt.
|
|
13
|
+
export function injectSystemNote(body, note) {
|
|
14
|
+
if (!note || !body) return body;
|
|
15
|
+
if (typeof body.system === 'string') body.system += `\n\n${note}`;
|
|
16
|
+
else if (Array.isArray(body.system)) body.system.push({ type: 'text', text: note });
|
|
17
|
+
else body.system = note;
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
// Push segments for a content field that may be a string or an array of blocks
|
|
13
22
|
// ({type:'text',text}, {type:'tool_result',content}, …).
|
|
14
23
|
function collectContent(content, segs) {
|
package/src/openai.js
CHANGED
|
@@ -11,6 +11,16 @@ export function matches(pathname) {
|
|
|
11
11
|
return /\/chat\/completions$/.test(pathname) || /\/completions$/.test(pathname);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Append a (non-redacted) instruction to the system prompt — e.g. the placeholder
|
|
15
|
+
// note that tells the model placeholders are auto-restored for tools.
|
|
16
|
+
export function injectSystemNote(body, note) {
|
|
17
|
+
if (!note || !body || !Array.isArray(body.messages)) return body;
|
|
18
|
+
const sys = body.messages.find((m) => m && m.role === 'system');
|
|
19
|
+
if (sys && typeof sys.content === 'string') sys.content += `\n\n${note}`;
|
|
20
|
+
else body.messages.unshift({ role: 'system', content: note });
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
// Collect segments from messages[].content (string or multimodal parts). System
|
|
15
25
|
// messages are included unless redactSystem is false.
|
|
16
26
|
export function collectSegments(body, redactionCfg) {
|
package/src/responses.js
CHANGED
|
@@ -9,6 +9,13 @@ export function matches(pathname) {
|
|
|
9
9
|
return /\/responses$/.test(pathname);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Append a (non-redacted) instruction to the Responses-API instructions field.
|
|
13
|
+
export function injectSystemNote(body, note) {
|
|
14
|
+
if (!note || !body) return body;
|
|
15
|
+
body.instructions = body.instructions ? `${body.instructions}\n\n${note}` : note;
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
// Redactable text in a Responses request lives in `instructions` (system) and
|
|
13
20
|
// `input` (a string, or an array of items whose content parts carry text).
|
|
14
21
|
function collectInputItem(item, segs) {
|
package/src/server.js
CHANGED
|
@@ -33,7 +33,7 @@ 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.5.
|
|
36
|
+
export const VERSION = '0.5.2';
|
|
37
37
|
|
|
38
38
|
const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
|
|
39
39
|
|
|
@@ -122,6 +122,18 @@ function pickAgent(model, cfg) {
|
|
|
122
122
|
return KNOWN_AGENTS.has(model) ? model : cfg.bridge.agent;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// A follow-up request carrying a tool result for a PARKED relay session. Such a
|
|
126
|
+
// request must NOT be redacted here: the relay owns redaction/restore through its
|
|
127
|
+
// OWN (round-1) vault, so re-redacting with a fresh vault would put the tool
|
|
128
|
+
// result's new tokens in the wrong vault and leave them unrestored in the reply.
|
|
129
|
+
function isRelayResume(body, kind) {
|
|
130
|
+
if (kind !== 'openai' || !body) return false;
|
|
131
|
+
const tr = openai.extractLatestToolResult(body);
|
|
132
|
+
if (!tr) return false;
|
|
133
|
+
const parsed = parseToolCallId(tr.tool_call_id);
|
|
134
|
+
return !!(parsed && getRelaySession(parsed.gwId));
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
// Guard against an api destination pointing back at THIS gateway (loopback host +
|
|
126
138
|
// our own port) — forwarding there would loop forever.
|
|
127
139
|
function isSelfUrl(baseUrl, cfg) {
|
|
@@ -208,14 +220,11 @@ async function startRelay(req, res, { kind, adapter, agent }, body, vault, cfg,
|
|
|
208
220
|
const redactOpts = { tier: effectiveTier({ tier: cfg.redaction.tier }, isPro), dictionary: gatedDictionary(cfg.redaction, isPro), entities: [] };
|
|
209
221
|
const s = createRelaySession({ vault, redactOpts, bridgeUrl: cfg.bridge.url, token, harness });
|
|
210
222
|
const ttl = setTimeout(() => endRelaySession(s.id), 135_000); // bridge tool-call timeout is 120s
|
|
211
|
-
//
|
|
212
|
-
// (
|
|
213
|
-
const sysWithNote = (vault && tools?.length)
|
|
214
|
-
? `${system || ''}\n\n${placeholderToolNote({ toolData: cfg.tools?.toolData })}`.trim()
|
|
215
|
-
: system;
|
|
223
|
+
// The placeholder note is already in `system` (injected into the body after
|
|
224
|
+
// redaction in the main handler), so toTurn() carried it here — nothing to add.
|
|
216
225
|
let resp;
|
|
217
226
|
try {
|
|
218
|
-
resp = await openBridgeChat({ bridgeUrl: cfg.bridge.url, agent, token, messages, system
|
|
227
|
+
resp = await openBridgeChat({ bridgeUrl: cfg.bridge.url, agent, token, messages, system, specs: toolsToSpecs(tools), options: {}, signal: undefined });
|
|
219
228
|
} catch (e) { clearTimeout(ttl); endRelaySession(s.id); return sendJson(res, 502, { error: { message: `bridge: ${e.message}`, type: 'bridge_error' } }); }
|
|
220
229
|
s.reader = resp.body.getReader();
|
|
221
230
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
@@ -434,7 +443,11 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
434
443
|
let isPro = true;
|
|
435
444
|
if (r.redactable && req.method === 'POST' && raw.length) {
|
|
436
445
|
try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
|
|
437
|
-
if (body) {
|
|
446
|
+
if (body && isRelayResume(body, r.kind)) {
|
|
447
|
+
// Relay tool-result follow-up: do NOT redact here — the parked session
|
|
448
|
+
// redacts the tool result + restores the reply with ITS vault. Pass raw.
|
|
449
|
+
outBody = raw;
|
|
450
|
+
} else if (body) {
|
|
438
451
|
// Auto-narrow tools to the top-K most relevant for this turn (speed) —
|
|
439
452
|
// same shared ranker as the extension's AUTO mode. Only MCP-named tools
|
|
440
453
|
// are narrowed; the client's core tools (bash/read/edit…) are always kept,
|
|
@@ -463,6 +476,13 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
463
476
|
const { vault: v, count } = await redactSegments(segs, cfg.redaction, { signal: ac.signal, isPro });
|
|
464
477
|
vault = v;
|
|
465
478
|
redactedCount = count;
|
|
479
|
+
// When tools are armed, tell the model placeholders are auto-restored for
|
|
480
|
+
// tools (so privacy-aware models USE them instead of refusing). Injected
|
|
481
|
+
// AFTER redaction so the note isn't itself redacted. Covers BOTH the API
|
|
482
|
+
// forward and the relay (which reads system from this same body).
|
|
483
|
+
if (Array.isArray(body.tools) && body.tools.length && typeof r.adapter.injectSystemNote === 'function') {
|
|
484
|
+
r.adapter.injectSystemNote(body, placeholderToolNote({ toolData: cfg.tools?.toolData }));
|
|
485
|
+
}
|
|
466
486
|
outBody = Buffer.from(JSON.stringify(body), 'utf8');
|
|
467
487
|
}
|
|
468
488
|
}
|