@signalvaultio/node 0.1.0 → 0.3.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/README.md +164 -19
- package/dist/__tests__/index.test.js +89 -39
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +243 -0
- package/dist/index.d.ts +114 -25
- package/dist/index.js +384 -210
- package/dist/tools.d.ts +69 -0
- package/dist/tools.js +207 -0
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -6,91 +6,286 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.SignalVaultClient = void 0;
|
|
7
7
|
const openai_1 = __importDefault(require("openai"));
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
|
+
const tools_1 = require("./tools");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Environment guard
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
14
|
+
* Hard-fails if the SDK is loaded in a browser context. The SDK relies on
|
|
15
|
+
* `async_hooks` (Node-only) and is intended for server-side use; bundling it
|
|
16
|
+
* for the browser would expose `apiKey`, `openaiApiKey`, and `anthropicApiKey`
|
|
17
|
+
* to end users — which is exactly the kind of leak this product is meant to
|
|
18
|
+
* prevent.
|
|
19
|
+
*/
|
|
20
|
+
function assertNodeEnvironment() {
|
|
21
|
+
// `process` exists on Node and not in plain browsers. `window` is the
|
|
22
|
+
// canonical browser global. We refuse to run if we look browser-like.
|
|
23
|
+
const hasProcess = typeof process !== 'undefined' && process?.versions?.node;
|
|
24
|
+
const hasWindow = typeof globalThis.window !== 'undefined';
|
|
25
|
+
if (!hasProcess || hasWindow) {
|
|
26
|
+
throw new Error('[SignalVault] This SDK is server-side only. Do not bundle it for browser use — ' +
|
|
27
|
+
'it will leak your API keys to end users. Call SignalVault from your backend instead.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// SignalVaultClient
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* SignalVault Client — wraps OpenAI and/or Anthropic with guardrails and audit logging.
|
|
11
35
|
*
|
|
12
|
-
* @example
|
|
36
|
+
* @example OpenAI
|
|
13
37
|
* ```typescript
|
|
14
38
|
* const client = new SignalVaultClient({
|
|
15
39
|
* apiKey: 'sk_live_...',
|
|
16
40
|
* openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
17
41
|
* baseUrl: 'https://api.signalvault.io',
|
|
42
|
+
* metadata: { user_id: '123' },
|
|
18
43
|
* });
|
|
19
|
-
*
|
|
20
|
-
* // Non-streaming
|
|
21
44
|
* const res = await client.chat.completions.create({ model: 'gpt-4', messages: [...] });
|
|
45
|
+
* ```
|
|
22
46
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
47
|
+
* @example Anthropic
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const client = new SignalVaultClient({
|
|
50
|
+
* apiKey: 'sk_live_...',
|
|
51
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
|
|
52
|
+
* baseUrl: 'https://api.signalvault.io',
|
|
53
|
+
* });
|
|
54
|
+
* const res = await client.messages.create({ model: 'claude-3-5-sonnet-20241022', messages: [...], max_tokens: 1024 });
|
|
26
55
|
* ```
|
|
27
56
|
*/
|
|
28
57
|
class SignalVaultClient {
|
|
29
58
|
constructor(config) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
assertNodeEnvironment();
|
|
60
|
+
if (!config.openaiApiKey && !config.anthropicApiKey) {
|
|
61
|
+
throw new Error('[SignalVault] At least one of openaiApiKey or anthropicApiKey is required.');
|
|
62
|
+
}
|
|
63
|
+
this.svApiKey = config.apiKey;
|
|
64
|
+
this.svBaseUrl = (config.baseUrl || 'http://localhost:4000').replace(/\/+$/, '');
|
|
65
|
+
this.svEnvironment = config.environment || 'production';
|
|
33
66
|
this.debugMode = config.debug || false;
|
|
34
67
|
this.mirrorMode = config.mirrorMode || false;
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
}
|
|
68
|
+
this.preflightTimeout = config.preflightTimeout ?? 2000;
|
|
69
|
+
this.bgTimeout = config.timeout ?? 10000;
|
|
70
|
+
this.defaultMetadata = config.metadata ?? {};
|
|
71
|
+
this.openai = config.openaiApiKey
|
|
72
|
+
? new openai_1.default({ apiKey: config.openaiApiKey })
|
|
73
|
+
: null;
|
|
74
|
+
if (config.anthropicApiKey) {
|
|
75
|
+
try {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
77
|
+
const mod = require('@anthropic-ai/sdk');
|
|
78
|
+
const Anthropic = mod.default ?? mod;
|
|
79
|
+
this.anthropic = new Anthropic({ apiKey: config.anthropicApiKey });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error('[SignalVault] @anthropic-ai/sdk is not installed. Run: npm install @anthropic-ai/sdk');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.anthropic = null;
|
|
87
|
+
}
|
|
38
88
|
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Agent tool-use capture
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
39
92
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
93
|
+
* Wraps a function so every call records an `agent.tool_call` event with
|
|
94
|
+
* timing, input, output, and any error. The wrapper auto-times, runs the
|
|
95
|
+
* inner function, fires the audit event in the background, and returns the
|
|
96
|
+
* inner result (or rethrows the inner error).
|
|
97
|
+
*
|
|
98
|
+
* Tool calls invoked inside a `withContext({ requestId })` block auto-pick
|
|
99
|
+
* up the requestId for correlation with the parent ai.request.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const fetchWeather = signalvault.tool('fetch_weather', async (city: string) => {
|
|
104
|
+
* const res = await fetch(`https://api.example.com/weather?city=${city}`);
|
|
105
|
+
* return res.json();
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* const weather = await fetchWeather('London');
|
|
109
|
+
* ```
|
|
42
110
|
*/
|
|
111
|
+
tool(name, fn, options) {
|
|
112
|
+
return (0, tools_1.wrapTool)({
|
|
113
|
+
recordFn: (opts) => this.sendToolCallEvent(opts),
|
|
114
|
+
debug: this.debugMode,
|
|
115
|
+
}, name, fn, options);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Manual tool recording API. Use when the wrapper isn't a fit (streaming
|
|
119
|
+
* tools, custom timing, post-hoc capture from logs).
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* await signalvault.tools.record({
|
|
124
|
+
* toolName: 'fetch_weather',
|
|
125
|
+
* toolInput: { city: 'London' },
|
|
126
|
+
* toolOutput: { temp: 12.3 },
|
|
127
|
+
* durationMs: 142,
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
get tools() {
|
|
132
|
+
return {
|
|
133
|
+
record: (opts) => this.sendToolCallEvent(opts),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Runs the given async function inside a context where tool calls are
|
|
138
|
+
* automatically correlated to the provided `requestId`. Useful for grouping
|
|
139
|
+
* a multi-step agent loop under one logical turn.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* await signalvault.withContext({ requestId: 'agent-turn-abc' }, async () => {
|
|
144
|
+
* const llm = await signalvault.chat.completions.create({...});
|
|
145
|
+
* await fetchWeather('London'); // tool call auto-linked to 'agent-turn-abc'
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
withContext(ctx, fn) {
|
|
150
|
+
return (0, tools_1.runWithContext)(ctx, fn);
|
|
151
|
+
}
|
|
152
|
+
async sendToolCallEvent(opts) {
|
|
153
|
+
const metadata = { ...this.defaultMetadata, ...(opts.metadata ?? {}) };
|
|
154
|
+
// Validate + sanitize before constructing the body. validateToolName throws
|
|
155
|
+
// on missing/empty name; surfacing that to the caller is intentional —
|
|
156
|
+
// the manual API should refuse malformed input rather than silently fail.
|
|
157
|
+
const safeName = (0, tools_1.validateToolName)(opts.toolName);
|
|
158
|
+
const payload = {
|
|
159
|
+
tool_name: safeName,
|
|
160
|
+
tool_input: (0, tools_1.sanitizePayload)(opts.toolInput),
|
|
161
|
+
tool_output: (0, tools_1.sanitizePayload)(opts.toolOutput),
|
|
162
|
+
duration_ms: opts.durationMs,
|
|
163
|
+
};
|
|
164
|
+
const safeError = (0, tools_1.sanitizeError)(opts.error);
|
|
165
|
+
if (safeError !== undefined)
|
|
166
|
+
payload.error = safeError;
|
|
167
|
+
if (opts.startedAt)
|
|
168
|
+
payload.started_at = opts.startedAt;
|
|
169
|
+
const body = {
|
|
170
|
+
type: 'agent.tool_call',
|
|
171
|
+
environment: this.svEnvironment,
|
|
172
|
+
metadata,
|
|
173
|
+
payload,
|
|
174
|
+
};
|
|
175
|
+
if (opts.requestId)
|
|
176
|
+
body.request_id = opts.requestId;
|
|
177
|
+
let serialized;
|
|
178
|
+
try {
|
|
179
|
+
serialized = JSON.stringify(body);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// sanitizePayload should make this impossible, but guard the outer
|
|
183
|
+
// serialize too — we'd rather drop the audit than crash the app.
|
|
184
|
+
console.warn('[SignalVault] tool_call serialization failed, event dropped:', error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(`${this.svBaseUrl}/v1/events`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: this.authHeaders(),
|
|
191
|
+
signal: AbortSignal.timeout(this.bgTimeout),
|
|
192
|
+
body: serialized,
|
|
193
|
+
});
|
|
194
|
+
// Surface 4xx unconditionally — those are usually misconfigurations
|
|
195
|
+
// (bad key, malformed payload) the user must know about. 5xx are noisy
|
|
196
|
+
// and only logged in debug mode.
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
if (response.status >= 400 && response.status < 500) {
|
|
199
|
+
console.warn(`[SignalVault] tool_call rejected with ${response.status}. ` +
|
|
200
|
+
`Check your apiKey and event payload.`);
|
|
201
|
+
}
|
|
202
|
+
else if (this.debugMode) {
|
|
203
|
+
console.error('[SignalVault] tool_call event failed:', response.status);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (this.debugMode) {
|
|
209
|
+
console.error('[SignalVault] tool_call event error:', error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// OpenAI: chat.completions
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
43
216
|
get chat() {
|
|
217
|
+
if (!this.openai) {
|
|
218
|
+
throw new Error('[SignalVault] openaiApiKey was not provided.');
|
|
219
|
+
}
|
|
220
|
+
const self = this;
|
|
44
221
|
return {
|
|
45
222
|
completions: {
|
|
46
|
-
|
|
223
|
+
async create(params, options) {
|
|
47
224
|
const requestId = (0, uuid_1.v4)();
|
|
48
|
-
|
|
49
|
-
|
|
225
|
+
const metadata = { ...self.defaultMetadata, ...(options?.metadata ?? {}) };
|
|
226
|
+
if (self.debugMode) {
|
|
227
|
+
console.log('[SignalVault] Processing request:', requestId, self.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
|
|
50
228
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return this.handleMirrorMode(requestId, params);
|
|
229
|
+
if (self.mirrorMode) {
|
|
230
|
+
return self.handleOpenAIMirror(requestId, params, metadata);
|
|
54
231
|
}
|
|
55
|
-
|
|
56
|
-
return this.handleNormalMode(requestId, params);
|
|
232
|
+
return self.handleOpenAINormal(requestId, params, metadata);
|
|
57
233
|
},
|
|
58
234
|
},
|
|
59
235
|
};
|
|
60
236
|
}
|
|
61
237
|
// ---------------------------------------------------------------------------
|
|
62
|
-
//
|
|
238
|
+
// Anthropic: messages
|
|
63
239
|
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
240
|
+
get messages() {
|
|
241
|
+
if (!this.anthropic) {
|
|
242
|
+
throw new Error('[SignalVault] anthropicApiKey not provided or @anthropic-ai/sdk not installed.');
|
|
243
|
+
}
|
|
244
|
+
const self = this;
|
|
245
|
+
return {
|
|
246
|
+
async create(params, options) {
|
|
247
|
+
const requestId = (0, uuid_1.v4)();
|
|
248
|
+
const metadata = { ...self.defaultMetadata, ...(options?.metadata ?? {}) };
|
|
249
|
+
if (self.debugMode) {
|
|
250
|
+
console.log('[SignalVault] Anthropic request:', requestId, self.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
|
|
251
|
+
}
|
|
252
|
+
if (self.mirrorMode) {
|
|
253
|
+
return self.handleAnthropicMirror(requestId, params, metadata);
|
|
254
|
+
}
|
|
255
|
+
return self.handleAnthropicNormal(requestId, params, metadata);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// OpenAI — mirror mode
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
async handleOpenAIMirror(requestId, params, metadata) {
|
|
65
263
|
try {
|
|
66
264
|
const response = await this.openai.chat.completions.create(params);
|
|
67
265
|
if (params.stream) {
|
|
68
|
-
|
|
69
|
-
|
|
266
|
+
return this.wrapStream(requestId, params.model, params.messages, response, async (output, promptTokens, completionTokens) => {
|
|
267
|
+
await this.sendAuditEvents(requestId, params.model, params.messages, output, promptTokens, completionTokens, metadata, 'openai');
|
|
268
|
+
});
|
|
70
269
|
}
|
|
71
|
-
// Non-streaming: audit in background
|
|
72
270
|
const completion = response;
|
|
73
|
-
this.
|
|
74
|
-
if (this.debugMode)
|
|
271
|
+
this.sendAuditEvents(requestId, params.model, params.messages, completion.choices[0]?.message?.content || '', completion.usage?.prompt_tokens || 0, completion.usage?.completion_tokens || 0, metadata, 'openai').catch((err) => {
|
|
272
|
+
if (this.debugMode)
|
|
75
273
|
console.error('[SignalVault] Audit failed (non-blocking):', err);
|
|
76
|
-
}
|
|
77
274
|
});
|
|
78
275
|
return completion;
|
|
79
276
|
}
|
|
80
277
|
catch (error) {
|
|
81
|
-
if (this.debugMode)
|
|
278
|
+
if (this.debugMode)
|
|
82
279
|
console.error('[SignalVault] OpenAI error:', error);
|
|
83
|
-
}
|
|
84
280
|
throw error;
|
|
85
281
|
}
|
|
86
282
|
}
|
|
87
283
|
// ---------------------------------------------------------------------------
|
|
88
|
-
//
|
|
284
|
+
// OpenAI — normal mode
|
|
89
285
|
// ---------------------------------------------------------------------------
|
|
90
|
-
async
|
|
286
|
+
async handleOpenAINormal(requestId, params, metadata) {
|
|
91
287
|
try {
|
|
92
|
-
|
|
93
|
-
const decision = await this.sendRequest(requestId, params);
|
|
288
|
+
const decision = await this.sendRequest(requestId, params.model, params.messages, metadata, 'openai');
|
|
94
289
|
if (decision.decision === 'block') {
|
|
95
290
|
throw new Error(`[SignalVault] Request blocked: ${JSON.stringify(decision.violations)}`);
|
|
96
291
|
}
|
|
@@ -99,25 +294,76 @@ class SignalVaultClient {
|
|
|
99
294
|
}
|
|
100
295
|
const response = await this.openai.chat.completions.create(params);
|
|
101
296
|
if (params.stream) {
|
|
102
|
-
|
|
103
|
-
|
|
297
|
+
return this.wrapStream(requestId, params.model, params.messages, response, async (output, promptTokens, completionTokens) => {
|
|
298
|
+
await this.sendResponseEvent(requestId, params.model, output, promptTokens, completionTokens, metadata, 'openai');
|
|
299
|
+
});
|
|
104
300
|
}
|
|
105
|
-
// Non-streaming: post-flight logging
|
|
106
301
|
const completion = response;
|
|
107
|
-
await this.
|
|
302
|
+
await this.sendResponseEvent(requestId, params.model, completion.choices[0]?.message?.content || '', completion.usage?.prompt_tokens || 0, completion.usage?.completion_tokens || 0, metadata, 'openai');
|
|
108
303
|
return completion;
|
|
109
304
|
}
|
|
110
305
|
catch (error) {
|
|
111
|
-
if (this.debugMode)
|
|
306
|
+
if (this.debugMode)
|
|
112
307
|
console.error('[SignalVault] Error:', error);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Anthropic — mirror mode
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
async handleAnthropicMirror(requestId, params, metadata) {
|
|
315
|
+
try {
|
|
316
|
+
const response = await this.anthropic.messages.create(params);
|
|
317
|
+
if (params.stream) {
|
|
318
|
+
return this.wrapAnthropicStream(requestId, params.model, params.messages, response, async (output, inputTokens, outputTokens) => {
|
|
319
|
+
await this.sendAuditEvents(requestId, params.model, params.messages, output, inputTokens, outputTokens, metadata, 'anthropic');
|
|
320
|
+
});
|
|
113
321
|
}
|
|
322
|
+
const output = response.content?.[0]?.text || '';
|
|
323
|
+
this.sendAuditEvents(requestId, params.model, params.messages, output, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, metadata, 'anthropic').catch((err) => {
|
|
324
|
+
if (this.debugMode)
|
|
325
|
+
console.error('[SignalVault] Anthropic audit failed (non-blocking):', err);
|
|
326
|
+
});
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (this.debugMode)
|
|
331
|
+
console.error('[SignalVault] Anthropic error:', error);
|
|
114
332
|
throw error;
|
|
115
333
|
}
|
|
116
334
|
}
|
|
117
335
|
// ---------------------------------------------------------------------------
|
|
118
|
-
//
|
|
336
|
+
// Anthropic — normal mode
|
|
119
337
|
// ---------------------------------------------------------------------------
|
|
120
|
-
async
|
|
338
|
+
async handleAnthropicNormal(requestId, params, metadata) {
|
|
339
|
+
try {
|
|
340
|
+
const decision = await this.sendRequest(requestId, params.model, params.messages, metadata, 'anthropic');
|
|
341
|
+
if (decision.decision === 'block') {
|
|
342
|
+
throw new Error(`[SignalVault] Request blocked: ${JSON.stringify(decision.violations)}`);
|
|
343
|
+
}
|
|
344
|
+
if (decision.decision === 'warn' && this.debugMode) {
|
|
345
|
+
console.warn('[SignalVault] Warnings:', decision.violations);
|
|
346
|
+
}
|
|
347
|
+
const response = await this.anthropic.messages.create(params);
|
|
348
|
+
if (params.stream) {
|
|
349
|
+
return this.wrapAnthropicStream(requestId, params.model, params.messages, response, async (output, inputTokens, outputTokens) => {
|
|
350
|
+
await this.sendResponseEvent(requestId, params.model, output, inputTokens, outputTokens, metadata, 'anthropic');
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const output = response.content?.[0]?.text || '';
|
|
354
|
+
await this.sendResponseEvent(requestId, params.model, output, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, metadata, 'anthropic');
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (this.debugMode)
|
|
359
|
+
console.error('[SignalVault] Anthropic error:', error);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Unified OpenAI stream wrapper
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
async *wrapStream(requestId, model, messages, stream, onComplete) {
|
|
121
367
|
const chunks = [];
|
|
122
368
|
let promptTokens = 0;
|
|
123
369
|
let completionTokens = 0;
|
|
@@ -125,210 +371,138 @@ class SignalVaultClient {
|
|
|
125
371
|
const content = chunk.choices[0]?.delta?.content || '';
|
|
126
372
|
if (content)
|
|
127
373
|
chunks.push(content);
|
|
128
|
-
// Collect usage if provided in final chunk
|
|
129
374
|
if (chunk.usage) {
|
|
130
375
|
promptTokens = chunk.usage.prompt_tokens || 0;
|
|
131
376
|
completionTokens = chunk.usage.completion_tokens || 0;
|
|
132
377
|
}
|
|
133
378
|
yield chunk;
|
|
134
379
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (this.debugMode) {
|
|
139
|
-
console.error('[SignalVault] Stream audit failed:', err);
|
|
140
|
-
}
|
|
380
|
+
onComplete(chunks.join(''), promptTokens, completionTokens).catch((err) => {
|
|
381
|
+
if (this.debugMode)
|
|
382
|
+
console.error('[SignalVault] Post-stream callback failed:', err);
|
|
141
383
|
});
|
|
142
384
|
}
|
|
143
|
-
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Anthropic stream wrapper
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
async *wrapAnthropicStream(requestId, model, messages, stream, onComplete) {
|
|
144
389
|
const chunks = [];
|
|
145
|
-
let
|
|
146
|
-
let
|
|
147
|
-
for await (const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
chunks.push(content);
|
|
151
|
-
if (chunk.usage) {
|
|
152
|
-
promptTokens = chunk.usage.prompt_tokens || 0;
|
|
153
|
-
completionTokens = chunk.usage.completion_tokens || 0;
|
|
390
|
+
let inputTokens = 0;
|
|
391
|
+
let outputTokens = 0;
|
|
392
|
+
for await (const event of stream) {
|
|
393
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
394
|
+
chunks.push(event.delta.text || '');
|
|
154
395
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Post-flight logging
|
|
158
|
-
const fullOutput = chunks.join('');
|
|
159
|
-
try {
|
|
160
|
-
await this.sendStreamResponse(requestId, params.model, fullOutput, promptTokens, completionTokens);
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
if (this.debugMode) {
|
|
164
|
-
console.error('[SignalVault] Post-flight logging failed:', err);
|
|
396
|
+
if (event.type === 'message_start' && event.message?.usage) {
|
|
397
|
+
inputTokens = event.message.usage.input_tokens || 0;
|
|
165
398
|
}
|
|
399
|
+
if (event.type === 'message_delta' && event.usage) {
|
|
400
|
+
outputTokens = event.usage.output_tokens || 0;
|
|
401
|
+
}
|
|
402
|
+
yield event;
|
|
166
403
|
}
|
|
404
|
+
onComplete(chunks.join(''), inputTokens, outputTokens).catch((err) => {
|
|
405
|
+
if (this.debugMode)
|
|
406
|
+
console.error('[SignalVault] Anthropic post-stream callback failed:', err);
|
|
407
|
+
});
|
|
167
408
|
}
|
|
168
409
|
// ---------------------------------------------------------------------------
|
|
169
410
|
// API communication
|
|
170
411
|
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
412
|
+
authHeaders() {
|
|
413
|
+
return {
|
|
414
|
+
'Content-Type': 'application/json',
|
|
415
|
+
Authorization: `Bearer ${this.svApiKey}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async sendRequest(requestId, model, messages, metadata, provider) {
|
|
419
|
+
try {
|
|
420
|
+
const response = await fetch(`${this.svBaseUrl}/v1/events`, {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
headers: this.authHeaders(),
|
|
423
|
+
signal: AbortSignal.timeout(this.preflightTimeout),
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
type: 'ai.request',
|
|
426
|
+
request_id: requestId,
|
|
427
|
+
environment: this.svEnvironment,
|
|
428
|
+
provider,
|
|
429
|
+
model,
|
|
430
|
+
metadata,
|
|
431
|
+
payload: { messages },
|
|
432
|
+
}),
|
|
433
|
+
});
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
if (this.debugMode)
|
|
436
|
+
console.error('[SignalVault] API error:', response.status);
|
|
437
|
+
return { decision: 'allow', violations: [], redactions: [] };
|
|
191
438
|
}
|
|
439
|
+
return (await response.json());
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
if (this.debugMode)
|
|
443
|
+
console.error('[SignalVault] Pre-flight failed (fail-open):', error);
|
|
192
444
|
return { decision: 'allow', violations: [], redactions: [] };
|
|
193
445
|
}
|
|
194
|
-
return (await response.json());
|
|
195
446
|
}
|
|
196
|
-
async
|
|
447
|
+
async sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, provider) {
|
|
197
448
|
try {
|
|
198
|
-
|
|
199
|
-
await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
|
|
449
|
+
await fetch(`${this.svBaseUrl}/v1/events`, {
|
|
200
450
|
method: 'POST',
|
|
201
|
-
headers:
|
|
202
|
-
|
|
203
|
-
Authorization: `Bearer ${this.signalvaultApiKey}`,
|
|
204
|
-
},
|
|
451
|
+
headers: this.authHeaders(),
|
|
452
|
+
signal: AbortSignal.timeout(this.bgTimeout),
|
|
205
453
|
body: JSON.stringify({
|
|
206
454
|
type: 'ai.response',
|
|
207
455
|
request_id: requestId,
|
|
208
|
-
environment: this.
|
|
209
|
-
provider
|
|
456
|
+
environment: this.svEnvironment,
|
|
457
|
+
provider,
|
|
210
458
|
model,
|
|
211
|
-
metadata
|
|
459
|
+
metadata,
|
|
212
460
|
payload: {
|
|
213
461
|
output,
|
|
214
|
-
usage: {
|
|
215
|
-
prompt_tokens: response.usage?.prompt_tokens || 0,
|
|
216
|
-
completion_tokens: response.usage?.completion_tokens || 0,
|
|
217
|
-
},
|
|
462
|
+
usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
|
|
218
463
|
},
|
|
219
464
|
}),
|
|
220
465
|
});
|
|
221
466
|
}
|
|
222
467
|
catch (error) {
|
|
223
|
-
if (this.debugMode)
|
|
224
|
-
console.error('[SignalVault] Failed to send response:', error);
|
|
225
|
-
}
|
|
468
|
+
if (this.debugMode)
|
|
469
|
+
console.error('[SignalVault] Failed to send response event:', error);
|
|
226
470
|
}
|
|
227
471
|
}
|
|
228
|
-
async
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
},
|
|
246
|
-
}),
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
async sendAudit(requestId, params, response) {
|
|
250
|
-
const output = response.choices[0]?.message?.content || '';
|
|
251
|
-
// Send ai.request event
|
|
252
|
-
await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
headers: {
|
|
255
|
-
'Content-Type': 'application/json',
|
|
256
|
-
Authorization: `Bearer ${this.signalvaultApiKey}`,
|
|
257
|
-
},
|
|
258
|
-
body: JSON.stringify({
|
|
259
|
-
type: 'ai.request',
|
|
260
|
-
request_id: requestId,
|
|
261
|
-
environment: this.signalvaultEnvironment,
|
|
262
|
-
provider: 'openai',
|
|
263
|
-
model: params.model,
|
|
264
|
-
metadata: {},
|
|
265
|
-
payload: { messages: params.messages, monitor_mode: true },
|
|
472
|
+
async sendAuditEvents(requestId, model, messages, output, promptTokens, completionTokens, metadata, provider) {
|
|
473
|
+
const base = {
|
|
474
|
+
request_id: requestId,
|
|
475
|
+
environment: this.svEnvironment,
|
|
476
|
+
provider,
|
|
477
|
+
model,
|
|
478
|
+
metadata,
|
|
479
|
+
};
|
|
480
|
+
await Promise.all([
|
|
481
|
+
fetch(`${this.svBaseUrl}/v1/events`, {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: this.authHeaders(),
|
|
484
|
+
signal: AbortSignal.timeout(this.bgTimeout),
|
|
485
|
+
body: JSON.stringify({
|
|
486
|
+
...base,
|
|
487
|
+
type: 'ai.request',
|
|
488
|
+
payload: { messages, monitor_mode: true },
|
|
489
|
+
}),
|
|
266
490
|
}),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
environment: this.signalvaultEnvironment,
|
|
279
|
-
provider: 'openai',
|
|
280
|
-
model: params.model,
|
|
281
|
-
metadata: {},
|
|
282
|
-
payload: {
|
|
283
|
-
output,
|
|
284
|
-
usage: {
|
|
285
|
-
prompt_tokens: response.usage?.prompt_tokens || 0,
|
|
286
|
-
completion_tokens: response.usage?.completion_tokens || 0,
|
|
491
|
+
fetch(`${this.svBaseUrl}/v1/events`, {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: this.authHeaders(),
|
|
494
|
+
signal: AbortSignal.timeout(this.bgTimeout),
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
...base,
|
|
497
|
+
type: 'ai.response',
|
|
498
|
+
payload: {
|
|
499
|
+
output,
|
|
500
|
+
usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
|
|
501
|
+
monitor_mode: true,
|
|
287
502
|
},
|
|
288
|
-
|
|
289
|
-
},
|
|
290
|
-
}),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
async sendStreamAudit(requestId, params, output, promptTokens, completionTokens) {
|
|
294
|
-
// Send ai.request event
|
|
295
|
-
await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
|
|
296
|
-
method: 'POST',
|
|
297
|
-
headers: {
|
|
298
|
-
'Content-Type': 'application/json',
|
|
299
|
-
Authorization: `Bearer ${this.signalvaultApiKey}`,
|
|
300
|
-
},
|
|
301
|
-
body: JSON.stringify({
|
|
302
|
-
type: 'ai.request',
|
|
303
|
-
request_id: requestId,
|
|
304
|
-
environment: this.signalvaultEnvironment,
|
|
305
|
-
provider: 'openai',
|
|
306
|
-
model: params.model,
|
|
307
|
-
metadata: {},
|
|
308
|
-
payload: { messages: params.messages, monitor_mode: true },
|
|
309
|
-
}),
|
|
310
|
-
});
|
|
311
|
-
// Send ai.response event
|
|
312
|
-
await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
|
|
313
|
-
method: 'POST',
|
|
314
|
-
headers: {
|
|
315
|
-
'Content-Type': 'application/json',
|
|
316
|
-
Authorization: `Bearer ${this.signalvaultApiKey}`,
|
|
317
|
-
},
|
|
318
|
-
body: JSON.stringify({
|
|
319
|
-
type: 'ai.response',
|
|
320
|
-
request_id: requestId,
|
|
321
|
-
environment: this.signalvaultEnvironment,
|
|
322
|
-
provider: 'openai',
|
|
323
|
-
model: params.model,
|
|
324
|
-
metadata: {},
|
|
325
|
-
payload: {
|
|
326
|
-
output,
|
|
327
|
-
usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
|
|
328
|
-
monitor_mode: true,
|
|
329
|
-
},
|
|
503
|
+
}),
|
|
330
504
|
}),
|
|
331
|
-
|
|
505
|
+
]);
|
|
332
506
|
}
|
|
333
507
|
}
|
|
334
508
|
exports.SignalVaultClient = SignalVaultClient;
|