@signalvaultio/node 0.5.0 → 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/dist/index.d.ts +1 -1
- package/dist/index.js +114 -29
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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.
|
|
5
|
+
export declare const SDK_VERSION = "0.5.2";
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const tools_1 = require("./tools");
|
|
7
7
|
/** Kept in sync with package.json by a unit test. */
|
|
8
|
-
exports.SDK_VERSION = '0.5.
|
|
8
|
+
exports.SDK_VERSION = '0.5.2';
|
|
9
9
|
const DEFAULT_BASE_URL = 'https://api.signalvault.io';
|
|
10
10
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
11
11
|
/** Longest Retry-After the SDK will wait out before retrying a background event once. */
|
|
@@ -38,25 +38,67 @@ function assertNodeEnvironment() {
|
|
|
38
38
|
* `http://` URL would expose both before the server could redirect to HTTPS.
|
|
39
39
|
*/
|
|
40
40
|
function normalizeBaseUrl(raw) {
|
|
41
|
+
// Errors never quote `raw`: it may contain credentials.
|
|
41
42
|
let url;
|
|
42
43
|
try {
|
|
43
44
|
url = new URL(raw);
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
46
|
-
throw new Error(
|
|
47
|
+
throw new Error('[SignalVault] baseUrl is not a valid URL.');
|
|
48
|
+
}
|
|
49
|
+
if (url.username || url.password) {
|
|
50
|
+
throw new Error('[SignalVault] baseUrl must not contain credentials (user:password@). Pass the key as apiKey.');
|
|
47
51
|
}
|
|
48
52
|
if (url.protocol === 'http:' && !LOCAL_HOSTS.has(url.hostname)) {
|
|
49
53
|
throw new Error('[SignalVault] baseUrl must use https:// (plain http:// is only allowed for localhost). ' +
|
|
50
54
|
'Your API key and prompts would otherwise be sent unencrypted.');
|
|
51
55
|
}
|
|
52
56
|
if (url.search || url.hash) {
|
|
53
|
-
throw new Error(
|
|
57
|
+
throw new Error('[SignalVault] baseUrl must not contain a query or fragment.');
|
|
54
58
|
}
|
|
55
59
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
56
60
|
throw new Error(`[SignalVault] baseUrl must be an https:// URL, got ${url.protocol}`);
|
|
57
61
|
}
|
|
58
62
|
return raw.replace(/\/+$/, '');
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Strips surrounding whitespace and refuses keys that can't go in a header.
|
|
66
|
+
* A key read from a file or a secret often ends in a newline. Sent as is,
|
|
67
|
+
* every request fails before it leaves the process, so with failMode 'open'
|
|
68
|
+
* every call would go through unchecked. Errors never include the key.
|
|
69
|
+
*/
|
|
70
|
+
function normalizeApiKey(raw) {
|
|
71
|
+
if (typeof raw !== 'string') {
|
|
72
|
+
throw new TypeError(`[SignalVault] apiKey must be a string, got ${raw === null ? 'null' : typeof raw}.`);
|
|
73
|
+
}
|
|
74
|
+
const key = raw.trim();
|
|
75
|
+
if (!key) {
|
|
76
|
+
throw new Error('[SignalVault] apiKey is empty.');
|
|
77
|
+
}
|
|
78
|
+
if (/[^\x21-\x7e]/.test(key)) {
|
|
79
|
+
throw new Error('[SignalVault] apiKey contains whitespace, control or non-ASCII characters. Check how the key is loaded.');
|
|
80
|
+
}
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One line describing an error and its causes, for debug output. Never
|
|
85
|
+
* includes `secret`, and never throws.
|
|
86
|
+
*/
|
|
87
|
+
function describeError(error, secret) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let current = error;
|
|
90
|
+
for (let depth = 0; current !== undefined && current !== null && depth < 5; depth++) {
|
|
91
|
+
try {
|
|
92
|
+
parts.push(current instanceof Error ? `${current.name}: ${current.message}` : String(current));
|
|
93
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
parts.push('<unprintable error>');
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return parts.join(' <- ').split(secret).join('[redacted]');
|
|
101
|
+
}
|
|
60
102
|
/** Thrown when a guardrail rule blocks the request. */
|
|
61
103
|
class SignalVaultBlockedError extends Error {
|
|
62
104
|
constructor(requestId, violations, dashboardUrl) {
|
|
@@ -122,6 +164,39 @@ function anthropicOutputText(response) {
|
|
|
122
164
|
.map((b) => b.text)
|
|
123
165
|
.join('');
|
|
124
166
|
}
|
|
167
|
+
const ANTHROPIC_USAGE_KEYS = [
|
|
168
|
+
'input_tokens',
|
|
169
|
+
'cache_creation_input_tokens',
|
|
170
|
+
'cache_read_input_tokens',
|
|
171
|
+
'output_tokens',
|
|
172
|
+
];
|
|
173
|
+
/** `usage.cache_creation`: the cache writes by cache lifetime, priced differently. */
|
|
174
|
+
const CACHE_CREATION_KEYS = ['ephemeral_5m_input_tokens', 'ephemeral_1h_input_tokens'];
|
|
175
|
+
function copyCounts(target, source, keys) {
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
const n = source?.[key];
|
|
178
|
+
if (typeof n === 'number' && Number.isFinite(n) && n >= 0)
|
|
179
|
+
target[key] = n;
|
|
180
|
+
}
|
|
181
|
+
return target;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Anthropic usage under Anthropic's own names, as the proxy records it. The
|
|
185
|
+
* server adds the cache counts to `input_tokens`; renamed to `prompt_tokens`,
|
|
186
|
+
* which for OpenAI already includes cached tokens, they would be lost. Later
|
|
187
|
+
* sources override earlier ones, but only with a count.
|
|
188
|
+
*/
|
|
189
|
+
function anthropicUsage(...sources) {
|
|
190
|
+
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
191
|
+
let cacheCreation = {};
|
|
192
|
+
for (const source of sources) {
|
|
193
|
+
copyCounts(usage, source, ANTHROPIC_USAGE_KEYS);
|
|
194
|
+
cacheCreation = copyCounts(cacheCreation, source?.cache_creation, CACHE_CREATION_KEYS);
|
|
195
|
+
}
|
|
196
|
+
if (Object.keys(cacheCreation).length)
|
|
197
|
+
usage.cache_creation = cacheCreation;
|
|
198
|
+
return usage;
|
|
199
|
+
}
|
|
125
200
|
function parseRetryAfterMs(response) {
|
|
126
201
|
const raw = response?.headers?.get?.('retry-after');
|
|
127
202
|
if (!raw)
|
|
@@ -236,7 +311,7 @@ class SignalVaultClient {
|
|
|
236
311
|
if (config.failMode !== undefined && config.failMode !== 'open' && config.failMode !== 'closed') {
|
|
237
312
|
throw new Error("[SignalVault] failMode must be 'open' or 'closed'.");
|
|
238
313
|
}
|
|
239
|
-
this.svApiKey = config.apiKey;
|
|
314
|
+
this.svApiKey = normalizeApiKey(config.apiKey);
|
|
240
315
|
this.svBaseUrl = normalizeBaseUrl(config.baseUrl || DEFAULT_BASE_URL);
|
|
241
316
|
this.svEnvironment = config.environment || 'production';
|
|
242
317
|
this.debugMode = config.debug || false;
|
|
@@ -282,8 +357,9 @@ class SignalVaultClient {
|
|
|
282
357
|
track(promise) {
|
|
283
358
|
const tracked = promise
|
|
284
359
|
.catch((err) => {
|
|
285
|
-
if (this.debugMode)
|
|
286
|
-
console.error('[SignalVault] Background event failed:', err);
|
|
360
|
+
if (this.debugMode) {
|
|
361
|
+
console.error('[SignalVault] Background event failed:', describeError(err, this.svApiKey));
|
|
362
|
+
}
|
|
287
363
|
})
|
|
288
364
|
.finally(() => this.pending.delete(tracked));
|
|
289
365
|
this.pending.add(tracked);
|
|
@@ -462,10 +538,13 @@ class SignalVaultClient {
|
|
|
462
538
|
const model = params.model;
|
|
463
539
|
const messages = params.messages;
|
|
464
540
|
if (params.stream) {
|
|
465
|
-
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendAuditEvents(requestId, model, messages, output, promptTokens, completionTokens, metadata, 'openai'));
|
|
541
|
+
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendAuditEvents(requestId, model, messages, output, { prompt_tokens: promptTokens, completion_tokens: completionTokens }, metadata, 'openai'));
|
|
466
542
|
}
|
|
467
543
|
const completion = response;
|
|
468
|
-
this.track(this.sendAuditEvents(requestId, model, messages, completion.choices[0]?.message?.content || '',
|
|
544
|
+
this.track(this.sendAuditEvents(requestId, model, messages, completion.choices[0]?.message?.content || '', {
|
|
545
|
+
prompt_tokens: completion.usage?.prompt_tokens || 0,
|
|
546
|
+
completion_tokens: completion.usage?.completion_tokens || 0,
|
|
547
|
+
}, metadata, 'openai'));
|
|
469
548
|
return completion;
|
|
470
549
|
}
|
|
471
550
|
// ---------------------------------------------------------------------------
|
|
@@ -479,11 +558,14 @@ class SignalVaultClient {
|
|
|
479
558
|
const usage = withStreamUsage(params);
|
|
480
559
|
const response = await this.openai.chat.completions.create(usage.params);
|
|
481
560
|
if (params.stream) {
|
|
482
|
-
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendResponseEvent(requestId, model, output, promptTokens, completionTokens, metadata, 'openai', fallback));
|
|
561
|
+
return this.wrapStream(response, usage.injected, (output, promptTokens, completionTokens) => this.sendResponseEvent(requestId, model, output, { prompt_tokens: promptTokens, completion_tokens: completionTokens }, metadata, 'openai', fallback));
|
|
483
562
|
}
|
|
484
563
|
const completion = response;
|
|
485
564
|
// Background: the audit of the response must not delay returning it.
|
|
486
|
-
this.track(this.sendResponseEvent(requestId, model, completion.choices[0]?.message?.content || '',
|
|
565
|
+
this.track(this.sendResponseEvent(requestId, model, completion.choices[0]?.message?.content || '', {
|
|
566
|
+
prompt_tokens: completion.usage?.prompt_tokens || 0,
|
|
567
|
+
completion_tokens: completion.usage?.completion_tokens || 0,
|
|
568
|
+
}, metadata, 'openai', fallback));
|
|
487
569
|
return completion;
|
|
488
570
|
}
|
|
489
571
|
// ---------------------------------------------------------------------------
|
|
@@ -493,9 +575,9 @@ class SignalVaultClient {
|
|
|
493
575
|
const response = await this.anthropic.messages.create(params);
|
|
494
576
|
const messages = anthropicMessagesForAudit(params);
|
|
495
577
|
if (params.stream) {
|
|
496
|
-
return this.wrapAnthropicStream(response, (output,
|
|
578
|
+
return this.wrapAnthropicStream(response, (output, usage) => this.sendAuditEvents(requestId, params.model, messages, output, usage, metadata, 'anthropic'));
|
|
497
579
|
}
|
|
498
|
-
this.track(this.sendAuditEvents(requestId, params.model, messages, anthropicOutputText(response), response.usage
|
|
580
|
+
this.track(this.sendAuditEvents(requestId, params.model, messages, anthropicOutputText(response), anthropicUsage(response.usage), metadata, 'anthropic'));
|
|
499
581
|
return response;
|
|
500
582
|
}
|
|
501
583
|
// ---------------------------------------------------------------------------
|
|
@@ -507,9 +589,9 @@ class SignalVaultClient {
|
|
|
507
589
|
const fallback = this.fallbackRequest(decision, requestId, params.model, messages, metadata, 'anthropic');
|
|
508
590
|
const response = await this.anthropic.messages.create(params);
|
|
509
591
|
if (params.stream) {
|
|
510
|
-
return this.wrapAnthropicStream(response, (output,
|
|
592
|
+
return this.wrapAnthropicStream(response, (output, usage) => this.sendResponseEvent(requestId, params.model, output, usage, metadata, 'anthropic', fallback));
|
|
511
593
|
}
|
|
512
|
-
this.track(this.sendResponseEvent(requestId, params.model, anthropicOutputText(response), response.usage
|
|
594
|
+
this.track(this.sendResponseEvent(requestId, params.model, anthropicOutputText(response), anthropicUsage(response.usage), metadata, 'anthropic', fallback));
|
|
513
595
|
return response;
|
|
514
596
|
}
|
|
515
597
|
// ---------------------------------------------------------------------------
|
|
@@ -547,24 +629,24 @@ class SignalVaultClient {
|
|
|
547
629
|
}
|
|
548
630
|
async *wrapAnthropicStream(stream, onComplete) {
|
|
549
631
|
const chunks = [];
|
|
550
|
-
let
|
|
551
|
-
let outputTokens = 0;
|
|
632
|
+
let usage = anthropicUsage();
|
|
552
633
|
try {
|
|
553
634
|
for await (const event of stream) {
|
|
554
635
|
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
555
636
|
chunks.push(event.delta.text || '');
|
|
556
637
|
}
|
|
638
|
+
// message_delta may repeat the input and cache counts; its values win.
|
|
557
639
|
if (event.type === 'message_start' && event.message?.usage) {
|
|
558
|
-
|
|
640
|
+
usage = anthropicUsage(usage, event.message.usage);
|
|
559
641
|
}
|
|
560
642
|
if (event.type === 'message_delta' && event.usage) {
|
|
561
|
-
|
|
643
|
+
usage = anthropicUsage(usage, event.usage);
|
|
562
644
|
}
|
|
563
645
|
yield event;
|
|
564
646
|
}
|
|
565
647
|
}
|
|
566
648
|
finally {
|
|
567
|
-
this.track(onComplete(chunks.join(''),
|
|
649
|
+
this.track(onComplete(chunks.join(''), usage));
|
|
568
650
|
}
|
|
569
651
|
}
|
|
570
652
|
// ---------------------------------------------------------------------------
|
|
@@ -699,8 +781,9 @@ class SignalVaultClient {
|
|
|
699
781
|
}
|
|
700
782
|
}
|
|
701
783
|
catch (error) {
|
|
702
|
-
if (this.debugMode)
|
|
703
|
-
console.error('[SignalVault] Event send failed:', error);
|
|
784
|
+
if (this.debugMode) {
|
|
785
|
+
console.error('[SignalVault] Event send failed:', describeError(error, this.svApiKey));
|
|
786
|
+
}
|
|
704
787
|
if (!isTimeout(error))
|
|
705
788
|
retryDelayMs = 250 + Math.random() * 500;
|
|
706
789
|
}
|
|
@@ -712,8 +795,9 @@ class SignalVaultClient {
|
|
|
712
795
|
return result;
|
|
713
796
|
}
|
|
714
797
|
catch (error) {
|
|
715
|
-
if (this.debugMode)
|
|
716
|
-
console.error('[SignalVault] Event retry failed:', error);
|
|
798
|
+
if (this.debugMode) {
|
|
799
|
+
console.error('[SignalVault] Event retry failed:', describeError(error, this.svApiKey));
|
|
800
|
+
}
|
|
717
801
|
}
|
|
718
802
|
}
|
|
719
803
|
this.logUndelivered(result);
|
|
@@ -734,8 +818,9 @@ class SignalVaultClient {
|
|
|
734
818
|
}
|
|
735
819
|
catch (error) {
|
|
736
820
|
const timedOut = isTimeout(error);
|
|
737
|
-
if (this.debugMode)
|
|
738
|
-
console.error('[SignalVault] Pre-flight failed:', error);
|
|
821
|
+
if (this.debugMode) {
|
|
822
|
+
console.error('[SignalVault] Pre-flight failed:', describeError(error, this.svApiKey));
|
|
823
|
+
}
|
|
739
824
|
return this.unavailable(requestId, timedOut ? 'timeout' : 'network', timedOut
|
|
740
825
|
? `SignalVault pre-flight check timed out after ${this.preflightTimeout}ms`
|
|
741
826
|
: 'SignalVault API is unreachable');
|
|
@@ -766,7 +851,7 @@ class SignalVaultClient {
|
|
|
766
851
|
dashboard_url: typeof data.dashboard_url === 'string' ? data.dashboard_url : undefined,
|
|
767
852
|
};
|
|
768
853
|
}
|
|
769
|
-
async sendResponseEvent(requestId, model, output,
|
|
854
|
+
async sendResponseEvent(requestId, model, output, usage, metadata, provider, fallbackRequest) {
|
|
770
855
|
if (fallbackRequest === null)
|
|
771
856
|
return;
|
|
772
857
|
if (fallbackRequest) {
|
|
@@ -783,7 +868,7 @@ class SignalVaultClient {
|
|
|
783
868
|
metadata,
|
|
784
869
|
payload: {
|
|
785
870
|
output,
|
|
786
|
-
usage
|
|
871
|
+
usage,
|
|
787
872
|
},
|
|
788
873
|
});
|
|
789
874
|
}
|
|
@@ -792,7 +877,7 @@ class SignalVaultClient {
|
|
|
792
877
|
* purpose — the server rejects an ai.response whose ai.request it has not
|
|
793
878
|
* stored yet, so sending both at once can lose the response.
|
|
794
879
|
*/
|
|
795
|
-
async sendAuditEvents(requestId, model, messages, output,
|
|
880
|
+
async sendAuditEvents(requestId, model, messages, output, usage, metadata, provider) {
|
|
796
881
|
const base = {
|
|
797
882
|
request_id: requestId,
|
|
798
883
|
environment: this.svEnvironment,
|
|
@@ -810,7 +895,7 @@ class SignalVaultClient {
|
|
|
810
895
|
type: 'ai.response',
|
|
811
896
|
payload: {
|
|
812
897
|
output,
|
|
813
|
-
usage
|
|
898
|
+
usage,
|
|
814
899
|
monitor_mode: true,
|
|
815
900
|
},
|
|
816
901
|
});
|