@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/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
- * SignalVault Client — wraps OpenAI with guardrails and audit logging.
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
- * // Streaming
24
- * const stream = await client.chat.completions.create({ model: 'gpt-4', messages: [...], stream: true });
25
- * for await (const chunk of stream) { ... }
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
- this.signalvaultApiKey = config.apiKey;
31
- this.signalvaultBaseUrl = (config.baseUrl || 'http://localhost:4000').replace(/\/+$/, '');
32
- this.signalvaultEnvironment = config.environment || 'production';
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.openai = new openai_1.default({
36
- apiKey: config.openaiApiKey,
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
- * Chat completions with SignalVault guardrails.
41
- * Supports both streaming and non-streaming.
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
- create: async (params) => {
223
+ async create(params, options) {
47
224
  const requestId = (0, uuid_1.v4)();
48
- if (this.debugMode) {
49
- console.log('[SignalVault] Processing request:', requestId, this.mirrorMode ? '(MIRROR MODE)' : '', params.stream ? '(STREAMING)' : '');
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
- // Mirror mode: call OpenAI first, then audit
52
- if (this.mirrorMode) {
53
- return this.handleMirrorMode(requestId, params);
229
+ if (self.mirrorMode) {
230
+ return self.handleOpenAIMirror(requestId, params, metadata);
54
231
  }
55
- // Normal mode: pre-flight checks first
56
- return this.handleNormalMode(requestId, params);
232
+ return self.handleOpenAINormal(requestId, params, metadata);
57
233
  },
58
234
  },
59
235
  };
60
236
  }
61
237
  // ---------------------------------------------------------------------------
62
- // Mirror mode — no latency added, audit asynchronously
238
+ // Anthropic: messages
63
239
  // ---------------------------------------------------------------------------
64
- async handleMirrorMode(requestId, params) {
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
- // Wrap the stream to collect chunks for auditing
69
- return this.wrapStreamForAudit(requestId, params, response);
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.sendAudit(requestId, params, completion).catch((err) => {
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
- // Normal mode — pre-flight guardrails, then OpenAI, then post-flight
284
+ // OpenAI — normal mode
89
285
  // ---------------------------------------------------------------------------
90
- async handleNormalMode(requestId, params) {
286
+ async handleOpenAINormal(requestId, params, metadata) {
91
287
  try {
92
- // Pre-flight check
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
- // Wrap stream to collect output for post-flight logging
103
- return this.wrapStreamForPostFlight(requestId, params, response);
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.sendResponse(requestId, completion, params.model);
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
- // Stream wrappers — collect chunks and send audit/post-flight events
336
+ // Anthropic — normal mode
119
337
  // ---------------------------------------------------------------------------
120
- async *wrapStreamForAudit(requestId, params, stream) {
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
- // Send audit after stream completes
136
- const fullOutput = chunks.join('');
137
- this.sendStreamAudit(requestId, params, fullOutput, promptTokens, completionTokens).catch((err) => {
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
- async *wrapStreamForPostFlight(requestId, params, stream) {
385
+ // ---------------------------------------------------------------------------
386
+ // Anthropic stream wrapper
387
+ // ---------------------------------------------------------------------------
388
+ async *wrapAnthropicStream(requestId, model, messages, stream, onComplete) {
144
389
  const chunks = [];
145
- let promptTokens = 0;
146
- let completionTokens = 0;
147
- for await (const chunk of stream) {
148
- const content = chunk.choices[0]?.delta?.content || '';
149
- if (content)
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
- yield chunk;
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
- async sendRequest(requestId, params) {
172
- const response = await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
173
- method: 'POST',
174
- headers: {
175
- 'Content-Type': 'application/json',
176
- Authorization: `Bearer ${this.signalvaultApiKey}`,
177
- },
178
- body: JSON.stringify({
179
- type: 'ai.request',
180
- request_id: requestId,
181
- environment: this.signalvaultEnvironment,
182
- provider: 'openai',
183
- model: params.model,
184
- metadata: {},
185
- payload: { messages: params.messages },
186
- }),
187
- });
188
- if (!response.ok) {
189
- if (this.debugMode) {
190
- console.error('[SignalVault] API error:', response.status);
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 sendResponse(requestId, response, model) {
447
+ async sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, provider) {
197
448
  try {
198
- const output = response.choices[0]?.message?.content || '';
199
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
449
+ await fetch(`${this.svBaseUrl}/v1/events`, {
200
450
  method: 'POST',
201
- headers: {
202
- 'Content-Type': 'application/json',
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.signalvaultEnvironment,
209
- provider: 'openai',
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 sendStreamResponse(requestId, model, output, promptTokens, completionTokens) {
229
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
230
- method: 'POST',
231
- headers: {
232
- 'Content-Type': 'application/json',
233
- Authorization: `Bearer ${this.signalvaultApiKey}`,
234
- },
235
- body: JSON.stringify({
236
- type: 'ai.response',
237
- request_id: requestId,
238
- environment: this.signalvaultEnvironment,
239
- provider: 'openai',
240
- model,
241
- metadata: {},
242
- payload: {
243
- output,
244
- usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens },
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
- // Send ai.response event
269
- await fetch(`${this.signalvaultBaseUrl}/v1/events`, {
270
- method: 'POST',
271
- headers: {
272
- 'Content-Type': 'application/json',
273
- Authorization: `Bearer ${this.signalvaultApiKey}`,
274
- },
275
- body: JSON.stringify({
276
- type: 'ai.response',
277
- request_id: requestId,
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
- monitor_mode: true,
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;