@cetusai/sdk 0.2.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +409 -0
- package/dist/index.cjs +910 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +518 -0
- package/dist/index.d.ts +518 -0
- package/dist/index.mjs +880 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var init_esm_shims = __esm({
|
|
15
|
+
"node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/errors.ts
|
|
21
|
+
var errors_exports = {};
|
|
22
|
+
__export(errors_exports, {
|
|
23
|
+
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
24
|
+
InvalidConfigError: () => InvalidConfigError,
|
|
25
|
+
NetworkError: () => NetworkError,
|
|
26
|
+
PartialFailureError: () => PartialFailureError,
|
|
27
|
+
QueueOverflowError: () => QueueOverflowError,
|
|
28
|
+
RateLimitedError: () => RateLimitedError,
|
|
29
|
+
ServerError: () => ServerError,
|
|
30
|
+
SonglinesError: () => SonglinesError,
|
|
31
|
+
TimeoutError: () => TimeoutError
|
|
32
|
+
});
|
|
33
|
+
var SonglinesError, InvalidConfigError, InvalidApiKeyError, NetworkError, TimeoutError, RateLimitedError, ServerError, QueueOverflowError, PartialFailureError;
|
|
34
|
+
var init_errors = __esm({
|
|
35
|
+
"src/errors.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
init_esm_shims();
|
|
38
|
+
SonglinesError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
statusCode;
|
|
41
|
+
retryable;
|
|
42
|
+
constructor(message, code, options) {
|
|
43
|
+
super(message, { cause: options?.cause });
|
|
44
|
+
this.name = "SonglinesError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.statusCode = options?.statusCode !== void 0 ? options.statusCode : void 0;
|
|
47
|
+
this.retryable = options?.retryable ?? false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
InvalidConfigError = class extends SonglinesError {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message, "INVALID_CONFIG", { retryable: false });
|
|
53
|
+
this.name = "InvalidConfigError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
InvalidApiKeyError = class extends SonglinesError {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(
|
|
59
|
+
"Invalid or revoked API key. Check your SONGLINES_API_KEY environment variable.",
|
|
60
|
+
"INVALID_API_KEY",
|
|
61
|
+
{ statusCode: 401, retryable: false }
|
|
62
|
+
);
|
|
63
|
+
this.name = "InvalidApiKeyError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
NetworkError = class extends SonglinesError {
|
|
67
|
+
constructor(message, cause) {
|
|
68
|
+
super(message, "NETWORK_ERROR", { retryable: true, cause });
|
|
69
|
+
this.name = "NetworkError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
TimeoutError = class extends SonglinesError {
|
|
73
|
+
constructor(timeoutMs) {
|
|
74
|
+
super(
|
|
75
|
+
`Request timed out after ${timeoutMs}ms`,
|
|
76
|
+
"TIMEOUT",
|
|
77
|
+
{ retryable: true }
|
|
78
|
+
);
|
|
79
|
+
this.name = "TimeoutError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
RateLimitedError = class extends SonglinesError {
|
|
83
|
+
retryAfterMs;
|
|
84
|
+
constructor(retryAfterMs) {
|
|
85
|
+
super(
|
|
86
|
+
retryAfterMs ? `Rate limited. Retry after ${retryAfterMs}ms.` : "Rate limited by the Songlines API.",
|
|
87
|
+
"RATE_LIMITED",
|
|
88
|
+
{ statusCode: 429, retryable: true }
|
|
89
|
+
);
|
|
90
|
+
this.name = "RateLimitedError";
|
|
91
|
+
this.retryAfterMs = retryAfterMs !== void 0 ? retryAfterMs : void 0;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
ServerError = class extends SonglinesError {
|
|
95
|
+
constructor(statusCode, body) {
|
|
96
|
+
super(
|
|
97
|
+
`Songlines API returned ${statusCode}${body ? `: ${body}` : ""}`,
|
|
98
|
+
"SERVER_ERROR",
|
|
99
|
+
{ statusCode, retryable: statusCode >= 500 }
|
|
100
|
+
);
|
|
101
|
+
this.name = "ServerError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
QueueOverflowError = class extends SonglinesError {
|
|
105
|
+
droppedCount;
|
|
106
|
+
constructor(droppedCount) {
|
|
107
|
+
super(
|
|
108
|
+
`Event queue is full. Dropped ${droppedCount} event(s). Check network connectivity.`,
|
|
109
|
+
"QUEUE_OVERFLOW",
|
|
110
|
+
{ retryable: false }
|
|
111
|
+
);
|
|
112
|
+
this.name = "QueueOverflowError";
|
|
113
|
+
this.droppedCount = droppedCount;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
PartialFailureError = class extends SonglinesError {
|
|
117
|
+
accepted;
|
|
118
|
+
failed;
|
|
119
|
+
failedIds;
|
|
120
|
+
constructor(accepted, failed, failedIds) {
|
|
121
|
+
super(
|
|
122
|
+
`Partial ingest failure: ${accepted} accepted, ${failed} failed.`,
|
|
123
|
+
"PARTIAL_FAILURE",
|
|
124
|
+
{ retryable: false }
|
|
125
|
+
);
|
|
126
|
+
this.name = "PartialFailureError";
|
|
127
|
+
this.accepted = accepted;
|
|
128
|
+
this.failed = failed;
|
|
129
|
+
this.failedIds = failedIds;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/index.ts
|
|
136
|
+
init_esm_shims();
|
|
137
|
+
|
|
138
|
+
// src/client.ts
|
|
139
|
+
init_esm_shims();
|
|
140
|
+
init_errors();
|
|
141
|
+
|
|
142
|
+
// src/utils/cost.ts
|
|
143
|
+
init_esm_shims();
|
|
144
|
+
var MODEL_RATES = {
|
|
145
|
+
// OpenAI
|
|
146
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
147
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
148
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
149
|
+
"gpt-4": { input: 30, output: 60 },
|
|
150
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
151
|
+
"o1-mini": { input: 3, output: 12 },
|
|
152
|
+
"o1": { input: 15, output: 60 },
|
|
153
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
154
|
+
"o3": { input: 10, output: 40 },
|
|
155
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
156
|
+
// Anthropic
|
|
157
|
+
"claude-3-5-haiku": { input: 0.8, output: 4 },
|
|
158
|
+
"claude-3-5-sonnet": { input: 3, output: 15 },
|
|
159
|
+
"claude-3-7-sonnet": { input: 3, output: 15 },
|
|
160
|
+
"claude-3-opus": { input: 15, output: 75 },
|
|
161
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
162
|
+
"claude-3-sonnet": { input: 3, output: 15 },
|
|
163
|
+
// Google
|
|
164
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
165
|
+
"gemini-2.5-flash": { input: 0.15, output: 0.6 },
|
|
166
|
+
"gemini-2.5-pro": { input: 1.25, output: 10 },
|
|
167
|
+
"gemini-1.5-flash": { input: 0.075, output: 0.3 },
|
|
168
|
+
"gemini-1.5-pro": { input: 1.25, output: 5 },
|
|
169
|
+
// Mistral
|
|
170
|
+
"mistral-large": { input: 2, output: 6 },
|
|
171
|
+
"mistral-small": { input: 0.2, output: 0.6 },
|
|
172
|
+
"mistral-nemo": { input: 0.15, output: 0.15 },
|
|
173
|
+
"codestral": { input: 0.2, output: 0.6 },
|
|
174
|
+
// Meta (via Groq / Together / Bedrock)
|
|
175
|
+
"llama-3.3-70b": { input: 0.59, output: 0.79 },
|
|
176
|
+
"llama-3.1-405b": { input: 2.7, output: 2.7 },
|
|
177
|
+
"llama-3.1-70b": { input: 0.59, output: 0.79 },
|
|
178
|
+
"llama-3.1-8b": { input: 0.05, output: 0.08 },
|
|
179
|
+
// AWS Bedrock (Nova)
|
|
180
|
+
"amazon.nova-pro": { input: 0.8, output: 3.2 },
|
|
181
|
+
"amazon.nova-lite": { input: 0.06, output: 0.24 },
|
|
182
|
+
"amazon.nova-micro": { input: 0.035, output: 0.14 },
|
|
183
|
+
// Cohere
|
|
184
|
+
"command-r-plus": { input: 2.5, output: 10 },
|
|
185
|
+
"command-r": { input: 0.15, output: 0.6 },
|
|
186
|
+
// DeepSeek
|
|
187
|
+
"deepseek-chat": { input: 0.27, output: 1.1 },
|
|
188
|
+
"deepseek-reasoner": { input: 0.55, output: 2.19 }
|
|
189
|
+
};
|
|
190
|
+
var FALLBACK_RATES = { input: 1, output: 3 };
|
|
191
|
+
function estimateCost({ model, inputTokens, outputTokens }) {
|
|
192
|
+
if (inputTokens === 0 && outputTokens === 0) return 0;
|
|
193
|
+
const normalised = model.toLowerCase().trim();
|
|
194
|
+
const rates = findRates(normalised);
|
|
195
|
+
const inputCost = inputTokens / 1e6 * rates.input;
|
|
196
|
+
const outputCost = outputTokens / 1e6 * rates.output;
|
|
197
|
+
return Math.round((inputCost + outputCost) * 1e8) / 1e8;
|
|
198
|
+
}
|
|
199
|
+
function findRates(normalisedModel) {
|
|
200
|
+
if (normalisedModel in MODEL_RATES) {
|
|
201
|
+
return MODEL_RATES[normalisedModel];
|
|
202
|
+
}
|
|
203
|
+
let bestMatch = "";
|
|
204
|
+
for (const key of Object.keys(MODEL_RATES)) {
|
|
205
|
+
if (normalisedModel.startsWith(key) && key.length > bestMatch.length) {
|
|
206
|
+
bestMatch = key;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return bestMatch ? MODEL_RATES[bestMatch] : FALLBACK_RATES;
|
|
210
|
+
}
|
|
211
|
+
function getModelRates() {
|
|
212
|
+
return MODEL_RATES;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/utils/id.ts
|
|
216
|
+
init_esm_shims();
|
|
217
|
+
function generateId() {
|
|
218
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
219
|
+
return crypto.randomUUID();
|
|
220
|
+
}
|
|
221
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
222
|
+
const r = Math.random() * 16 | 0;
|
|
223
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
224
|
+
return v.toString(16);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/retry.ts
|
|
229
|
+
init_esm_shims();
|
|
230
|
+
init_errors();
|
|
231
|
+
async function withRetry(fn, options = {}) {
|
|
232
|
+
const {
|
|
233
|
+
maxAttempts = 3,
|
|
234
|
+
baseDelayMs = 500,
|
|
235
|
+
maxDelayMs = 3e4,
|
|
236
|
+
jitter = 0.2,
|
|
237
|
+
onRetry
|
|
238
|
+
} = options;
|
|
239
|
+
let lastError;
|
|
240
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
241
|
+
try {
|
|
242
|
+
return await fn();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
lastError = err;
|
|
245
|
+
if (err instanceof SonglinesError && !err.retryable) {
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
if (attempt === maxAttempts) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, jitter);
|
|
252
|
+
onRetry?.(attempt, delay, err);
|
|
253
|
+
await sleep(delay);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw lastError;
|
|
257
|
+
}
|
|
258
|
+
function computeDelay(attempt, baseDelayMs, maxDelayMs, jitter) {
|
|
259
|
+
const exponential = baseDelayMs * Math.pow(2, attempt - 1);
|
|
260
|
+
const capped = Math.min(exponential, maxDelayMs);
|
|
261
|
+
const jitterRange = capped * jitter;
|
|
262
|
+
const jitterOffset = (Math.random() * 2 - 1) * jitterRange;
|
|
263
|
+
return Math.max(0, Math.round(capped + jitterOffset));
|
|
264
|
+
}
|
|
265
|
+
function sleep(ms) {
|
|
266
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/utils/queue.ts
|
|
270
|
+
init_esm_shims();
|
|
271
|
+
init_errors();
|
|
272
|
+
var BatchQueue = class {
|
|
273
|
+
queue = [];
|
|
274
|
+
flushTimer = null;
|
|
275
|
+
flushing = false;
|
|
276
|
+
opts;
|
|
277
|
+
constructor(opts) {
|
|
278
|
+
this.opts = opts;
|
|
279
|
+
this.scheduleFlush();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Pushes an event onto the queue. Triggers an immediate flush if the
|
|
283
|
+
* batch size threshold is reached.
|
|
284
|
+
*/
|
|
285
|
+
push(event) {
|
|
286
|
+
if (this.queue.length >= this.opts.maxQueueSize) {
|
|
287
|
+
this.opts.onError(new QueueOverflowError(1));
|
|
288
|
+
this.log(`Queue overflow \u2014 dropped event ${event.request_id}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.queue.push(event);
|
|
292
|
+
this.log(`Queued event ${event.request_id} (queue size: ${this.queue.length})`);
|
|
293
|
+
if (this.queue.length >= this.opts.batchSize) {
|
|
294
|
+
this.log(`Batch size reached (${this.opts.batchSize}) \u2014 flushing immediately`);
|
|
295
|
+
void this.flushNow();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Flushes all queued events immediately.
|
|
300
|
+
* Safe to call multiple times concurrently — subsequent calls wait for the
|
|
301
|
+
* in-flight flush to complete before starting their own.
|
|
302
|
+
*/
|
|
303
|
+
async flushNow() {
|
|
304
|
+
if (this.queue.length === 0) return;
|
|
305
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
306
|
+
this.log(`Flushing batch of ${batch.length} event(s)`);
|
|
307
|
+
try {
|
|
308
|
+
await this.opts.flush(batch);
|
|
309
|
+
this.log(`Flush successful \u2014 ${batch.length} event(s) accepted`);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.log(`Flush failed \u2014 ${batch.length} event(s) dropped: ${String(err)}`);
|
|
312
|
+
if (err instanceof SonglinesError) {
|
|
313
|
+
this.opts.onError(err);
|
|
314
|
+
} else {
|
|
315
|
+
this.opts.onError(new NetworkError(String(err), err));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Cancels the periodic timer, flushes remaining events, and marks the queue
|
|
321
|
+
* as shut down. No further events should be pushed after calling this.
|
|
322
|
+
*/
|
|
323
|
+
async shutdown() {
|
|
324
|
+
this.cancelTimer();
|
|
325
|
+
await this.flushNow();
|
|
326
|
+
this.log("Queue shut down");
|
|
327
|
+
}
|
|
328
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
329
|
+
scheduleFlush() {
|
|
330
|
+
this.cancelTimer();
|
|
331
|
+
this.flushTimer = setTimeout(() => {
|
|
332
|
+
void this.flushNow().finally(() => this.scheduleFlush());
|
|
333
|
+
}, this.opts.flushIntervalMs);
|
|
334
|
+
if (this.flushTimer.unref) {
|
|
335
|
+
this.flushTimer.unref();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
cancelTimer() {
|
|
339
|
+
if (this.flushTimer !== null) {
|
|
340
|
+
clearTimeout(this.flushTimer);
|
|
341
|
+
this.flushTimer = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
log(message) {
|
|
345
|
+
if (this.opts.debug) {
|
|
346
|
+
console.debug(`[Songlines SDK] ${message}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Exposed for testing only. */
|
|
350
|
+
get _queueLength() {
|
|
351
|
+
return this.queue.length;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/guardrail.ts
|
|
356
|
+
init_esm_shims();
|
|
357
|
+
var GuardrailBlockedError = class extends Error {
|
|
358
|
+
result;
|
|
359
|
+
constructor(result) {
|
|
360
|
+
const reasons = result.violations.map((v) => v.reason).join("; ");
|
|
361
|
+
super(`Guardrail blocked request: ${reasons}`);
|
|
362
|
+
this.name = "GuardrailBlockedError";
|
|
363
|
+
this.result = result;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
var evaluateGuardrailImpl = async (params, config) => {
|
|
367
|
+
const { NetworkError: NetworkError2, ServerError: ServerError2, InvalidApiKeyError: InvalidApiKeyError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
368
|
+
const url = `${config.baseUrl}/api/guardrail/evaluate`;
|
|
369
|
+
const controller = new AbortController();
|
|
370
|
+
const timer = setTimeout(() => controller.abort(), config.timeout);
|
|
371
|
+
let response;
|
|
372
|
+
try {
|
|
373
|
+
response = await fetch(url, {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: {
|
|
376
|
+
"Content-Type": "application/json",
|
|
377
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
378
|
+
"User-Agent": "@songlines/sdk/0.2.0"
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
prompt: params.prompt,
|
|
382
|
+
model: params.model,
|
|
383
|
+
workflow: params.workflow,
|
|
384
|
+
provider: params.provider
|
|
385
|
+
}),
|
|
386
|
+
signal: controller.signal
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
391
|
+
const { TimeoutError: TimeoutError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
392
|
+
throw new TimeoutError2(config.timeout);
|
|
393
|
+
}
|
|
394
|
+
throw new NetworkError2(`Failed to reach ${url}: ${String(err)}`, err);
|
|
395
|
+
} finally {
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
}
|
|
398
|
+
if (response.status === 401) {
|
|
399
|
+
throw new InvalidApiKeyError2();
|
|
400
|
+
}
|
|
401
|
+
if (response.status >= 500) {
|
|
402
|
+
const text = await response.text().catch(() => "");
|
|
403
|
+
throw new ServerError2(response.status, text);
|
|
404
|
+
}
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const text = await response.text().catch(() => "");
|
|
407
|
+
throw new ServerError2(response.status, text);
|
|
408
|
+
}
|
|
409
|
+
const data = await response.json();
|
|
410
|
+
const result = {
|
|
411
|
+
decision: data.decision,
|
|
412
|
+
violations: data.violations.map((v) => ({
|
|
413
|
+
policyId: v.policy_id,
|
|
414
|
+
policyName: v.policy_name,
|
|
415
|
+
action: v.action,
|
|
416
|
+
reason: v.reason,
|
|
417
|
+
field: v.field
|
|
418
|
+
})),
|
|
419
|
+
latencyMs: data.latency_ms,
|
|
420
|
+
evaluationId: data.evaluation_id
|
|
421
|
+
};
|
|
422
|
+
if (data.modified_input !== void 0) {
|
|
423
|
+
result.modifiedInput = data.modified_input;
|
|
424
|
+
}
|
|
425
|
+
if (params.throwOnBlock && result.decision === "block") {
|
|
426
|
+
throw new GuardrailBlockedError(result);
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/client.ts
|
|
432
|
+
var DEFAULT_BASE_URL = "https://api.songlinesai.com";
|
|
433
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
434
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
435
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
436
|
+
var DEFAULT_RETRIES = 3;
|
|
437
|
+
var MAX_QUEUE_SIZE = 1e3;
|
|
438
|
+
var SonglinesClient = class {
|
|
439
|
+
config;
|
|
440
|
+
queue;
|
|
441
|
+
constructor(config) {
|
|
442
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
443
|
+
throw new InvalidConfigError(
|
|
444
|
+
"apiKey is required. Set SONGLINES_API_KEY in your environment and pass it as apiKey."
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (config.apiKey.trim().length === 0) {
|
|
448
|
+
throw new InvalidConfigError("apiKey must not be empty.");
|
|
449
|
+
}
|
|
450
|
+
if (config.batchSize !== void 0 && (config.batchSize < 1 || config.batchSize > 100)) {
|
|
451
|
+
throw new InvalidConfigError("batchSize must be between 1 and 100.");
|
|
452
|
+
}
|
|
453
|
+
if (config.flushIntervalMs !== void 0 && config.flushIntervalMs < 100) {
|
|
454
|
+
throw new InvalidConfigError("flushIntervalMs must be at least 100ms.");
|
|
455
|
+
}
|
|
456
|
+
if (config.retries !== void 0 && (config.retries < 0 || config.retries > 10)) {
|
|
457
|
+
throw new InvalidConfigError("retries must be between 0 and 10.");
|
|
458
|
+
}
|
|
459
|
+
this.config = {
|
|
460
|
+
apiKey: config.apiKey.trim(),
|
|
461
|
+
baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
462
|
+
environment: config.environment ?? "production",
|
|
463
|
+
batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
464
|
+
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
465
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
466
|
+
retries: config.retries ?? DEFAULT_RETRIES,
|
|
467
|
+
onError: config.onError ?? (() => {
|
|
468
|
+
}),
|
|
469
|
+
debug: config.debug ?? false
|
|
470
|
+
};
|
|
471
|
+
this.queue = new BatchQueue({
|
|
472
|
+
batchSize: this.config.batchSize,
|
|
473
|
+
flushIntervalMs: this.config.flushIntervalMs,
|
|
474
|
+
maxQueueSize: MAX_QUEUE_SIZE,
|
|
475
|
+
onError: this.config.onError,
|
|
476
|
+
flush: (events) => this.sendBatch(events),
|
|
477
|
+
debug: this.config.debug
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
481
|
+
/**
|
|
482
|
+
* Records an AI request event. The event is queued immediately and sent
|
|
483
|
+
* asynchronously — this method returns as soon as the event is enqueued.
|
|
484
|
+
*
|
|
485
|
+
* The returned Promise resolves when the event has been enqueued (not when
|
|
486
|
+
* it has been sent). Use `flush()` if you need confirmation of delivery.
|
|
487
|
+
*/
|
|
488
|
+
async trackAIRequest(params) {
|
|
489
|
+
const event = this.buildEvent(params);
|
|
490
|
+
this.queue.push(event);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Forces an immediate flush of all queued events to the API.
|
|
494
|
+
* Resolves when the flush completes (successfully or not).
|
|
495
|
+
*/
|
|
496
|
+
async flush() {
|
|
497
|
+
await this.queue.flushNow();
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Flushes all remaining events and shuts down the background timer.
|
|
501
|
+
* Call this during graceful shutdown (e.g. in a `process.on("SIGTERM")` handler).
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```typescript
|
|
505
|
+
* process.on("SIGTERM", async () => {
|
|
506
|
+
* await songlines.shutdown();
|
|
507
|
+
* process.exit(0);
|
|
508
|
+
* });
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
async shutdown() {
|
|
512
|
+
await this.queue.shutdown();
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Evaluates a prompt against all active Songlines policies in real time.
|
|
516
|
+
*
|
|
517
|
+
* This method uses the **Songlines Gateway / Enforce** path, which is
|
|
518
|
+
* distinct from `trackAIRequest()`. Call it *before* sending the prompt
|
|
519
|
+
* to an LLM to enforce policies inline.
|
|
520
|
+
*
|
|
521
|
+
* @returns A `GuardrailResult` with `decision`, `violations`, and optionally
|
|
522
|
+
* a `modifiedInput` (when a redaction policy fires).
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```typescript
|
|
526
|
+
* const result = await songlines.evaluateGuardrail({
|
|
527
|
+
* prompt: userMessage,
|
|
528
|
+
* model: "gpt-4o",
|
|
529
|
+
* workflow: "customer-support",
|
|
530
|
+
* });
|
|
531
|
+
*
|
|
532
|
+
* if (result.decision === "block") {
|
|
533
|
+
* return { error: "Request blocked by policy", violations: result.violations };
|
|
534
|
+
* }
|
|
535
|
+
*
|
|
536
|
+
* const promptToSend = result.modifiedInput ?? userMessage;
|
|
537
|
+
* const response = await openai.chat.completions.create({ ... });
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
async evaluateGuardrail(params) {
|
|
541
|
+
return evaluateGuardrailImpl(params, {
|
|
542
|
+
baseUrl: this.config.baseUrl,
|
|
543
|
+
apiKey: this.config.apiKey,
|
|
544
|
+
timeout: this.config.timeout,
|
|
545
|
+
onError: this.config.onError
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
// ── Internal ─────────────────────────────────────────────────────────────────
|
|
549
|
+
/**
|
|
550
|
+
* Converts TrackAIRequestParams into the wire format expected by /api/ingest.
|
|
551
|
+
*/
|
|
552
|
+
buildEvent(params) {
|
|
553
|
+
const requestId = params.requestId ?? generateId();
|
|
554
|
+
const cost = params.cost ?? estimateCost({
|
|
555
|
+
model: params.model,
|
|
556
|
+
inputTokens: params.inputTokens,
|
|
557
|
+
outputTokens: params.outputTokens
|
|
558
|
+
});
|
|
559
|
+
const event = {
|
|
560
|
+
request_id: requestId,
|
|
561
|
+
model: params.model,
|
|
562
|
+
input_tokens: params.inputTokens,
|
|
563
|
+
output_tokens: params.outputTokens,
|
|
564
|
+
environment: this.config.environment,
|
|
565
|
+
status: params.status ?? "success"
|
|
566
|
+
};
|
|
567
|
+
if (params.provider !== void 0) event.provider = params.provider;
|
|
568
|
+
if (params.workflow !== void 0) event.workflow = params.workflow;
|
|
569
|
+
if (params.step !== void 0) event.step = params.step;
|
|
570
|
+
if (params.agentId !== void 0) event.agent_id = params.agentId;
|
|
571
|
+
if (params.user !== void 0) event.user = params.user;
|
|
572
|
+
if (params.latencyMs !== void 0) event.latency_ms = params.latencyMs;
|
|
573
|
+
if (params.errorMessage !== void 0) event.error_message = params.errorMessage;
|
|
574
|
+
if (params.timestamp !== void 0) {
|
|
575
|
+
event.timestamp = params.timestamp.toISOString();
|
|
576
|
+
}
|
|
577
|
+
return event;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Sends a batch of events to POST /api/ingest.
|
|
581
|
+
* Called by the BatchQueue — never called directly.
|
|
582
|
+
*/
|
|
583
|
+
async sendBatch(events) {
|
|
584
|
+
const url = `${this.config.baseUrl}/api/ingest`;
|
|
585
|
+
const body = events.length === 1 ? events[0] : { events };
|
|
586
|
+
await withRetry(
|
|
587
|
+
() => this.httpPost(url, body),
|
|
588
|
+
{
|
|
589
|
+
maxAttempts: this.config.retries + 1,
|
|
590
|
+
onRetry: (attempt, delayMs, error) => {
|
|
591
|
+
if (this.config.debug) {
|
|
592
|
+
console.debug(
|
|
593
|
+
`[Songlines SDK] Retry ${attempt} in ${delayMs}ms after: ${String(error)}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Performs a single HTTP POST with timeout and error classification.
|
|
602
|
+
*/
|
|
603
|
+
async httpPost(url, body) {
|
|
604
|
+
const controller = new AbortController();
|
|
605
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeout);
|
|
606
|
+
let response;
|
|
607
|
+
try {
|
|
608
|
+
response = await fetch(url, {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: {
|
|
611
|
+
"Content-Type": "application/json",
|
|
612
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
613
|
+
"User-Agent": "@songlines/sdk/0.1.0"
|
|
614
|
+
},
|
|
615
|
+
body: JSON.stringify(body),
|
|
616
|
+
signal: controller.signal
|
|
617
|
+
});
|
|
618
|
+
} catch (err) {
|
|
619
|
+
clearTimeout(timer);
|
|
620
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
621
|
+
throw new TimeoutError(this.config.timeout);
|
|
622
|
+
}
|
|
623
|
+
throw new NetworkError(`Failed to reach ${url}: ${String(err)}`, err);
|
|
624
|
+
} finally {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
}
|
|
627
|
+
if (response.status === 401) {
|
|
628
|
+
throw new InvalidApiKeyError();
|
|
629
|
+
}
|
|
630
|
+
if (response.status === 429) {
|
|
631
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
632
|
+
const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : void 0;
|
|
633
|
+
throw new RateLimitedError(retryAfterMs);
|
|
634
|
+
}
|
|
635
|
+
if (response.status >= 500) {
|
|
636
|
+
const text = await response.text().catch(() => "");
|
|
637
|
+
throw new ServerError(response.status, text);
|
|
638
|
+
}
|
|
639
|
+
if (!response.ok && response.status !== 207) {
|
|
640
|
+
const text = await response.text().catch(() => "");
|
|
641
|
+
throw new ServerError(response.status, text);
|
|
642
|
+
}
|
|
643
|
+
const data = await response.json();
|
|
644
|
+
if (data.failed > 0 && data.errors) {
|
|
645
|
+
const failedIds = data.errors.map((e) => e.request_id);
|
|
646
|
+
this.config.onError(new PartialFailureError(data.accepted, data.failed, failedIds));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// src/index.ts
|
|
652
|
+
init_errors();
|
|
653
|
+
|
|
654
|
+
// src/middleware/openai.ts
|
|
655
|
+
init_esm_shims();
|
|
656
|
+
function wrapOpenAI(client, songlines, defaults = {}) {
|
|
657
|
+
return new Proxy(client, {
|
|
658
|
+
get(target, prop, receiver) {
|
|
659
|
+
const value = Reflect.get(target, prop, receiver);
|
|
660
|
+
if (prop === "chat" && typeof value === "object" && value !== null) {
|
|
661
|
+
return wrapChat(value, songlines, defaults);
|
|
662
|
+
}
|
|
663
|
+
if (prop === "completions" && typeof value === "object" && value !== null) {
|
|
664
|
+
return wrapLegacyCompletions(value, songlines, defaults);
|
|
665
|
+
}
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
function wrapChat(chat, songlines, defaults) {
|
|
671
|
+
return new Proxy(chat, {
|
|
672
|
+
get(target, prop, receiver) {
|
|
673
|
+
const value = Reflect.get(target, prop, receiver);
|
|
674
|
+
if (prop === "completions" && typeof value === "object" && value !== null) {
|
|
675
|
+
return wrapChatCompletions(value, songlines, defaults);
|
|
676
|
+
}
|
|
677
|
+
return value;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function wrapChatCompletions(completions, songlines, defaults) {
|
|
682
|
+
return new Proxy(completions, {
|
|
683
|
+
get(target, prop, receiver) {
|
|
684
|
+
const value = Reflect.get(target, prop, receiver);
|
|
685
|
+
if (prop === "create") {
|
|
686
|
+
return async (params, options) => {
|
|
687
|
+
if (params.stream === true) {
|
|
688
|
+
const requestId2 = generateId();
|
|
689
|
+
const start2 = Date.now();
|
|
690
|
+
const stream = await value.call(target, params, options);
|
|
691
|
+
void songlines.trackAIRequest({
|
|
692
|
+
requestId: requestId2,
|
|
693
|
+
model: params.model,
|
|
694
|
+
provider: "openai",
|
|
695
|
+
inputTokens: 0,
|
|
696
|
+
outputTokens: 0,
|
|
697
|
+
latencyMs: Date.now() - start2,
|
|
698
|
+
status: "success",
|
|
699
|
+
metadata: { streaming: true },
|
|
700
|
+
...defaults
|
|
701
|
+
});
|
|
702
|
+
return stream;
|
|
703
|
+
}
|
|
704
|
+
const requestId = generateId();
|
|
705
|
+
const start = Date.now();
|
|
706
|
+
let response;
|
|
707
|
+
let status = "success";
|
|
708
|
+
let errorMessage;
|
|
709
|
+
try {
|
|
710
|
+
response = await value.call(target, params, options);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
status = "error";
|
|
713
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
714
|
+
void songlines.trackAIRequest({
|
|
715
|
+
requestId,
|
|
716
|
+
model: params.model,
|
|
717
|
+
provider: "openai",
|
|
718
|
+
inputTokens: 0,
|
|
719
|
+
outputTokens: 0,
|
|
720
|
+
latencyMs: Date.now() - start,
|
|
721
|
+
status: "error",
|
|
722
|
+
errorMessage,
|
|
723
|
+
...defaults
|
|
724
|
+
});
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
const latencyMs = Date.now() - start;
|
|
728
|
+
void songlines.trackAIRequest({
|
|
729
|
+
requestId,
|
|
730
|
+
model: params.model,
|
|
731
|
+
provider: "openai",
|
|
732
|
+
inputTokens: response.usage?.prompt_tokens ?? 0,
|
|
733
|
+
outputTokens: response.usage?.completion_tokens ?? 0,
|
|
734
|
+
latencyMs,
|
|
735
|
+
status,
|
|
736
|
+
...defaults
|
|
737
|
+
});
|
|
738
|
+
return response;
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function wrapLegacyCompletions(completions, songlines, defaults) {
|
|
746
|
+
return new Proxy(completions, {
|
|
747
|
+
get(target, prop, receiver) {
|
|
748
|
+
const value = Reflect.get(target, prop, receiver);
|
|
749
|
+
if (prop === "create") {
|
|
750
|
+
return async (params, options) => {
|
|
751
|
+
if (params.stream === true) {
|
|
752
|
+
return value.call(target, params, options);
|
|
753
|
+
}
|
|
754
|
+
const requestId = generateId();
|
|
755
|
+
const start = Date.now();
|
|
756
|
+
let response;
|
|
757
|
+
try {
|
|
758
|
+
response = await value.call(target, params, options);
|
|
759
|
+
} catch (err) {
|
|
760
|
+
void songlines.trackAIRequest({
|
|
761
|
+
requestId,
|
|
762
|
+
model: params.model,
|
|
763
|
+
provider: "openai",
|
|
764
|
+
inputTokens: 0,
|
|
765
|
+
outputTokens: 0,
|
|
766
|
+
latencyMs: Date.now() - start,
|
|
767
|
+
status: "error",
|
|
768
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
769
|
+
...defaults
|
|
770
|
+
});
|
|
771
|
+
throw err;
|
|
772
|
+
}
|
|
773
|
+
void songlines.trackAIRequest({
|
|
774
|
+
requestId,
|
|
775
|
+
model: params.model,
|
|
776
|
+
provider: "openai",
|
|
777
|
+
inputTokens: response.usage?.prompt_tokens ?? 0,
|
|
778
|
+
outputTokens: response.usage?.completion_tokens ?? 0,
|
|
779
|
+
latencyMs: Date.now() - start,
|
|
780
|
+
status: "success",
|
|
781
|
+
...defaults
|
|
782
|
+
});
|
|
783
|
+
return response;
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return value;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/middleware/anthropic.ts
|
|
792
|
+
init_esm_shims();
|
|
793
|
+
function wrapAnthropic(client, songlines, defaults = {}) {
|
|
794
|
+
return new Proxy(client, {
|
|
795
|
+
get(target, prop, receiver) {
|
|
796
|
+
const value = Reflect.get(target, prop, receiver);
|
|
797
|
+
if (prop === "messages" && typeof value === "object" && value !== null) {
|
|
798
|
+
return wrapMessages(value, songlines, defaults);
|
|
799
|
+
}
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function wrapMessages(messages, songlines, defaults) {
|
|
805
|
+
return new Proxy(messages, {
|
|
806
|
+
get(target, prop, receiver) {
|
|
807
|
+
const value = Reflect.get(target, prop, receiver);
|
|
808
|
+
if (prop === "create") {
|
|
809
|
+
return async (params, options) => {
|
|
810
|
+
if (params.stream === true) {
|
|
811
|
+
const requestId2 = generateId();
|
|
812
|
+
const start2 = Date.now();
|
|
813
|
+
const stream = await value.call(target, params, options);
|
|
814
|
+
void songlines.trackAIRequest({
|
|
815
|
+
requestId: requestId2,
|
|
816
|
+
model: params.model,
|
|
817
|
+
provider: "anthropic",
|
|
818
|
+
inputTokens: 0,
|
|
819
|
+
outputTokens: 0,
|
|
820
|
+
latencyMs: Date.now() - start2,
|
|
821
|
+
status: "success",
|
|
822
|
+
metadata: { streaming: true },
|
|
823
|
+
...defaults
|
|
824
|
+
});
|
|
825
|
+
return stream;
|
|
826
|
+
}
|
|
827
|
+
const requestId = generateId();
|
|
828
|
+
const start = Date.now();
|
|
829
|
+
let response;
|
|
830
|
+
try {
|
|
831
|
+
response = await value.call(target, params, options);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
void songlines.trackAIRequest({
|
|
834
|
+
requestId,
|
|
835
|
+
model: params.model,
|
|
836
|
+
provider: "anthropic",
|
|
837
|
+
inputTokens: 0,
|
|
838
|
+
outputTokens: 0,
|
|
839
|
+
latencyMs: Date.now() - start,
|
|
840
|
+
status: "error",
|
|
841
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
842
|
+
...defaults
|
|
843
|
+
});
|
|
844
|
+
throw err;
|
|
845
|
+
}
|
|
846
|
+
void songlines.trackAIRequest({
|
|
847
|
+
requestId,
|
|
848
|
+
model: params.model,
|
|
849
|
+
provider: "anthropic",
|
|
850
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
851
|
+
outputTokens: response.usage?.output_tokens ?? 0,
|
|
852
|
+
latencyMs: Date.now() - start,
|
|
853
|
+
status: "success",
|
|
854
|
+
...defaults
|
|
855
|
+
});
|
|
856
|
+
return response;
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return value;
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
export {
|
|
864
|
+
GuardrailBlockedError,
|
|
865
|
+
InvalidApiKeyError,
|
|
866
|
+
InvalidConfigError,
|
|
867
|
+
NetworkError,
|
|
868
|
+
PartialFailureError,
|
|
869
|
+
QueueOverflowError,
|
|
870
|
+
RateLimitedError,
|
|
871
|
+
ServerError,
|
|
872
|
+
SonglinesClient,
|
|
873
|
+
SonglinesError,
|
|
874
|
+
TimeoutError,
|
|
875
|
+
estimateCost,
|
|
876
|
+
getModelRates,
|
|
877
|
+
wrapAnthropic,
|
|
878
|
+
wrapOpenAI
|
|
879
|
+
};
|
|
880
|
+
//# sourceMappingURL=index.mjs.map
|