@signalvaultio/node 0.4.0 → 0.5.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 +25 -3
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +86 -28
- package/dist/index.mjs +16 -0
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -20,6 +20,18 @@ npm install @signalvaultio/node @anthropic-ai/sdk
|
|
|
20
20
|
npm install @signalvaultio/node openai @anthropic-ai/sdk
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Supported provider SDKs: `openai` 4.4 through 7.x (openai 7 needs Node.js 22+) and `@anthropic-ai/sdk` 0.20 or later. Install only the one you use; the other is optional. TypeScript projects without `openai` need `skipLibCheck: true` (the default in most setups), because the SDK's type declarations refer to openai's types.
|
|
24
|
+
|
|
25
|
+
The package works with both `import` and `require()`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import SignalVaultClient from '@signalvaultio/node'; // ESM, TypeScript
|
|
29
|
+
import { SignalVaultClient } from '@signalvaultio/node'; // also works
|
|
30
|
+
const { SignalVaultClient } = require('@signalvaultio/node'); // CommonJS
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Server-side bundlers (webpack, esbuild) are supported. esbuild's ESM output needs its usual `createRequire` banner for CommonJS dependencies: `--banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);"`.
|
|
34
|
+
|
|
23
35
|
## Quick Start — OpenAI
|
|
24
36
|
|
|
25
37
|
```typescript
|
|
@@ -28,8 +40,6 @@ import SignalVaultClient from '@signalvaultio/node';
|
|
|
28
40
|
const client = new SignalVaultClient({
|
|
29
41
|
apiKey: 'sk_live_your_signalvault_key',
|
|
30
42
|
openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
31
|
-
baseUrl: 'https://api.signalvault.io',
|
|
32
|
-
environment: 'production',
|
|
33
43
|
});
|
|
34
44
|
|
|
35
45
|
// Use exactly like OpenAI SDK
|
|
@@ -91,7 +101,7 @@ for await (const event of stream) {
|
|
|
91
101
|
}
|
|
92
102
|
```
|
|
93
103
|
|
|
94
|
-
For
|
|
104
|
+
For OpenAI streams, the SDK sets `stream_options: { include_usage: true }` so the response is logged with its token counts. OpenAI then sends one extra final chunk that carries only the usage and no choices; the SDK consumes that chunk rather than passing it to you, so the stream you receive looks the same as without usage reporting. If you set `stream_options.include_usage` yourself, your setting is kept: with `true` you receive the usage chunk as usual, with `false` the response is logged without token counts. If your OpenAI-compatible server rejects `stream_options`, pass `stream_options: null` and the SDK sends none.
|
|
95
105
|
|
|
96
106
|
## Agent Tool-Use Capture
|
|
97
107
|
|
|
@@ -251,8 +261,20 @@ const client = new SignalVaultClient({
|
|
|
251
261
|
});
|
|
252
262
|
```
|
|
253
263
|
|
|
264
|
+
`environment` does not choose which rules apply: SignalVault uses the environment of the API key you pass as `apiKey`. Use a key created for the environment you want.
|
|
265
|
+
|
|
254
266
|
## Error Handling
|
|
255
267
|
|
|
268
|
+
When a guardrail rule blocks a request, `create()` throws `SignalVaultBlockedError` and the provider is never called. It carries:
|
|
269
|
+
|
|
270
|
+
- `violations` — the rules that matched, each with `rule_id`, `type` (for example `contains_secret`), `severity`, `action` and `details`
|
|
271
|
+
- `requestId` — the ID the SDK sent to SignalVault as `request_id` for this request
|
|
272
|
+
- `dashboardUrl` — a link to the blocked request, when the API provides one
|
|
273
|
+
|
|
274
|
+
The message lists only the violation types, never the matched content.
|
|
275
|
+
|
|
276
|
+
`SignalVaultUnavailableError` (with `requestId` and the HTTP `status`, if any) is thrown only with `failMode: 'closed'`, when no decision could be obtained.
|
|
277
|
+
|
|
256
278
|
```typescript
|
|
257
279
|
import { SignalVaultBlockedError, SignalVaultUnavailableError } from '@signalvaultio/node';
|
|
258
280
|
|
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
2
|
import { ToolsAPI } from './tools';
|
|
3
3
|
export type { ToolRecordOptions, ToolsAPI } from './tools';
|
|
4
4
|
/** Kept in sync with package.json by a unit test. */
|
|
5
|
-
export declare const SDK_VERSION = "0.
|
|
5
|
+
export declare const SDK_VERSION = "0.5.0";
|
|
6
6
|
/**
|
|
7
7
|
* Normalizes the base URL and refuses plaintext HTTP to anything but the local
|
|
8
8
|
* machine. The API key and full prompts travel in every request, so an
|
|
@@ -209,7 +209,9 @@ export declare class SignalVaultClient {
|
|
|
209
209
|
/**
|
|
210
210
|
* Yields chunks through unchanged and records the response when the stream
|
|
211
211
|
* ends — including when the consumer breaks out early or the stream throws,
|
|
212
|
-
* in which case the partial output is recorded.
|
|
212
|
+
* in which case the partial output is recorded. When the SDK added
|
|
213
|
+
* `include_usage` itself (`hideUsageChunk`), the usage-only final chunk is
|
|
214
|
+
* consumed here rather than passed on.
|
|
213
215
|
*/
|
|
214
216
|
private wrapStream;
|
|
215
217
|
private wrapAnthropicStream;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.SignalVaultClient = exports.SignalVaultUnavailableError = exports.SignalVaultBlockedError = exports.SDK_VERSION = void 0;
|
|
7
4
|
exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
8
5
|
const crypto_1 = require("crypto");
|
|
9
|
-
const openai_1 = __importDefault(require("openai"));
|
|
10
6
|
const tools_1 = require("./tools");
|
|
11
7
|
/** Kept in sync with package.json by a unit test. */
|
|
12
|
-
exports.SDK_VERSION = '0.
|
|
8
|
+
exports.SDK_VERSION = '0.5.0';
|
|
13
9
|
const DEFAULT_BASE_URL = 'https://api.signalvault.io';
|
|
14
10
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
15
11
|
/** Longest Retry-After the SDK will wait out before retrying a background event once. */
|
|
@@ -134,6 +130,76 @@ function parseRetryAfterMs(response) {
|
|
|
134
130
|
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
|
|
135
131
|
}
|
|
136
132
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
|
+
/**
|
|
134
|
+
* The provider SDKs are optional peers, loaded only when their API key is
|
|
135
|
+
* configured, so installing just the one you use is enough. The requires are
|
|
136
|
+
* literal strings inside try blocks on purpose: bundlers can only follow a
|
|
137
|
+
* literal require, and treat one inside a try as optional.
|
|
138
|
+
*/
|
|
139
|
+
function loadOpenAI(apiKey) {
|
|
140
|
+
let mod;
|
|
141
|
+
try {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
143
|
+
mod = require('openai');
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw notInstalled(error, 'openai');
|
|
147
|
+
}
|
|
148
|
+
return construct(mod, apiKey);
|
|
149
|
+
}
|
|
150
|
+
function loadAnthropic(apiKey) {
|
|
151
|
+
let mod;
|
|
152
|
+
try {
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
154
|
+
mod = require('@anthropic-ai/sdk');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw notInstalled(error, '@anthropic-ai/sdk');
|
|
158
|
+
}
|
|
159
|
+
return construct(mod, apiKey);
|
|
160
|
+
}
|
|
161
|
+
function construct(mod, apiKey) {
|
|
162
|
+
const Client = mod.default ?? mod;
|
|
163
|
+
return new Client({ apiKey });
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Only a missing package is reported as "not installed"; anything else (a
|
|
167
|
+
* broken install, a missing transitive dependency) is returned unchanged.
|
|
168
|
+
*/
|
|
169
|
+
function notInstalled(error, pkg) {
|
|
170
|
+
// Read fields directly: `instanceof Error` fails across realms (e.g. under Jest).
|
|
171
|
+
const { code, message } = (error ?? {});
|
|
172
|
+
if (code === 'MODULE_NOT_FOUND' && String(message).includes(`'${pkg}'`)) {
|
|
173
|
+
return new Error(`[SignalVault] ${pkg} is not installed. Run: npm install ${pkg}`);
|
|
174
|
+
}
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Asks OpenAI to report token usage on a stream, unless the caller already
|
|
179
|
+
* decided either way. OpenAI then sends one extra final chunk with an empty
|
|
180
|
+
* `choices` array; `injected` tells the stream wrapper to consume that chunk
|
|
181
|
+
* instead of passing it to a caller who never asked for it.
|
|
182
|
+
*
|
|
183
|
+
* `stream_options: null` opts out entirely: the key is removed, for
|
|
184
|
+
* OpenAI-compatible servers that reject `stream_options`. (The OpenAI API
|
|
185
|
+
* treats null and absent the same.)
|
|
186
|
+
*/
|
|
187
|
+
function withStreamUsage(params) {
|
|
188
|
+
if (!params.stream)
|
|
189
|
+
return { params, injected: false };
|
|
190
|
+
const options = params.stream_options;
|
|
191
|
+
if (options === null) {
|
|
192
|
+
const { stream_options: _omitted, ...rest } = params;
|
|
193
|
+
return { params: rest, injected: false };
|
|
194
|
+
}
|
|
195
|
+
if (options && options.include_usage !== undefined) {
|
|
196
|
+
return { params, injected: false };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
params: { ...params, stream_options: { ...(options ?? {}), include_usage: true } },
|
|
200
|
+
injected: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
137
203
|
// ---------------------------------------------------------------------------
|
|
138
204
|
// SignalVaultClient
|
|
139
205
|
// ---------------------------------------------------------------------------
|
|
@@ -179,23 +245,8 @@ class SignalVaultClient {
|
|
|
179
245
|
this.preflightTimeout = config.preflightTimeout ?? 2000;
|
|
180
246
|
this.bgTimeout = config.timeout ?? 10000;
|
|
181
247
|
this.defaultMetadata = config.metadata ?? {};
|
|
182
|
-
this.openai = config.openaiApiKey
|
|
183
|
-
|
|
184
|
-
: null;
|
|
185
|
-
if (config.anthropicApiKey) {
|
|
186
|
-
try {
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
188
|
-
const mod = require('@anthropic-ai/sdk');
|
|
189
|
-
const Anthropic = mod.default ?? mod;
|
|
190
|
-
this.anthropic = new Anthropic({ apiKey: config.anthropicApiKey });
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
throw new Error('[SignalVault] @anthropic-ai/sdk is not installed. Run: npm install @anthropic-ai/sdk');
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
this.anthropic = null;
|
|
198
|
-
}
|
|
248
|
+
this.openai = config.openaiApiKey ? loadOpenAI(config.openaiApiKey) : null;
|
|
249
|
+
this.anthropic = config.anthropicApiKey ? loadAnthropic(config.anthropicApiKey) : null;
|
|
199
250
|
}
|
|
200
251
|
// ---------------------------------------------------------------------------
|
|
201
252
|
// Lifecycle
|
|
@@ -406,11 +457,12 @@ class SignalVaultClient {
|
|
|
406
457
|
// OpenAI — mirror mode
|
|
407
458
|
// ---------------------------------------------------------------------------
|
|
408
459
|
async handleOpenAIMirror(requestId, params, metadata) {
|
|
409
|
-
const
|
|
460
|
+
const usage = withStreamUsage(params);
|
|
461
|
+
const response = await this.openai.chat.completions.create(usage.params);
|
|
410
462
|
const model = params.model;
|
|
411
463
|
const messages = params.messages;
|
|
412
464
|
if (params.stream) {
|
|
413
|
-
return this.wrapStream(response, (output, promptTokens, completionTokens) => this.sendAuditEvents(requestId, model, messages, output, promptTokens, completionTokens, metadata, 'openai'));
|
|
465
|
+
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendAuditEvents(requestId, model, messages, output, promptTokens, completionTokens, metadata, 'openai'));
|
|
414
466
|
}
|
|
415
467
|
const completion = response;
|
|
416
468
|
this.track(this.sendAuditEvents(requestId, model, messages, completion.choices[0]?.message?.content || '', completion.usage?.prompt_tokens || 0, completion.usage?.completion_tokens || 0, metadata, 'openai'));
|
|
@@ -424,9 +476,10 @@ class SignalVaultClient {
|
|
|
424
476
|
const messages = params.messages;
|
|
425
477
|
const decision = await this.enforce(requestId, model, messages, metadata, 'openai');
|
|
426
478
|
const fallback = this.fallbackRequest(decision, requestId, model, messages, metadata, 'openai');
|
|
427
|
-
const
|
|
479
|
+
const usage = withStreamUsage(params);
|
|
480
|
+
const response = await this.openai.chat.completions.create(usage.params);
|
|
428
481
|
if (params.stream) {
|
|
429
|
-
return this.wrapStream(response, (output, promptTokens, completionTokens) => this.sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, 'openai', fallback));
|
|
482
|
+
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, 'openai', fallback));
|
|
430
483
|
}
|
|
431
484
|
const completion = response;
|
|
432
485
|
// Background: the audit of the response must not delay returning it.
|
|
@@ -465,9 +518,11 @@ class SignalVaultClient {
|
|
|
465
518
|
/**
|
|
466
519
|
* Yields chunks through unchanged and records the response when the stream
|
|
467
520
|
* ends — including when the consumer breaks out early or the stream throws,
|
|
468
|
-
* in which case the partial output is recorded.
|
|
521
|
+
* in which case the partial output is recorded. When the SDK added
|
|
522
|
+
* `include_usage` itself (`hideUsageChunk`), the usage-only final chunk is
|
|
523
|
+
* consumed here rather than passed on.
|
|
469
524
|
*/
|
|
470
|
-
async *wrapStream(stream, onComplete) {
|
|
525
|
+
async *wrapStream(stream, hideUsageChunk, onComplete) {
|
|
471
526
|
const chunks = [];
|
|
472
527
|
let promptTokens = 0;
|
|
473
528
|
let completionTokens = 0;
|
|
@@ -479,6 +534,9 @@ class SignalVaultClient {
|
|
|
479
534
|
if (chunk.usage) {
|
|
480
535
|
promptTokens = chunk.usage.prompt_tokens || 0;
|
|
481
536
|
completionTokens = chunk.usage.completion_tokens || 0;
|
|
537
|
+
// Missing or empty `choices`: the usage-only chunk the caller didn't ask for.
|
|
538
|
+
if (hideUsageChunk && !chunk.choices?.length)
|
|
539
|
+
continue;
|
|
482
540
|
}
|
|
483
541
|
yield chunk;
|
|
484
542
|
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ESM entry point. Re-exports the CommonJS build rather than shipping a
|
|
2
|
+
// second copy of the SDK: two copies would each hold their own module state
|
|
3
|
+
// (the AsyncLocalStorage behind withContext), so context set through `import`
|
|
4
|
+
// would be invisible to code loaded through `require`. Named imports from
|
|
5
|
+
// CommonJS keep default-export interop out of the picture for Node and
|
|
6
|
+
// bundlers alike. Keep this list in sync with src/index.ts — the package test
|
|
7
|
+
// fails if they differ.
|
|
8
|
+
export {
|
|
9
|
+
SDK_VERSION,
|
|
10
|
+
SignalVaultBlockedError,
|
|
11
|
+
SignalVaultClient,
|
|
12
|
+
SignalVaultUnavailableError,
|
|
13
|
+
normalizeBaseUrl,
|
|
14
|
+
} from './index.js';
|
|
15
|
+
import { SignalVaultClient } from './index.js';
|
|
16
|
+
export default SignalVaultClient;
|
package/package.json
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalvaultio/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SignalVault SDK - AI audit logs and guardrails for safe production AI",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
7
21
|
"files": [
|
|
8
22
|
"dist",
|
|
9
23
|
"!dist/__tests__",
|
|
10
24
|
"README.md"
|
|
11
25
|
],
|
|
12
26
|
"scripts": {
|
|
13
|
-
"build": "
|
|
27
|
+
"build": "node scripts/build.mjs",
|
|
14
28
|
"prepublishOnly": "npm run build",
|
|
15
29
|
"test": "jest --passWithNoTests"
|
|
16
30
|
},
|
|
@@ -41,7 +55,7 @@
|
|
|
41
55
|
"homepage": "https://signalvault.io/docs",
|
|
42
56
|
"peerDependencies": {
|
|
43
57
|
"@anthropic-ai/sdk": ">=0.20.0",
|
|
44
|
-
"openai": "^4.0.0"
|
|
58
|
+
"openai": "^4.4.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
45
59
|
},
|
|
46
60
|
"peerDependenciesMeta": {
|
|
47
61
|
"openai": {
|