@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/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