@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.
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Base error class for all Songlines SDK errors.
3
+ *
4
+ * The SDK never throws — all errors are surfaced via the `onError` callback
5
+ * configured on the client. This class is provided for type-safe error handling
6
+ * in the callback.
7
+ */
8
+ declare class SonglinesError extends Error {
9
+ readonly code: SonglinesErrorCode;
10
+ readonly statusCode: number | undefined;
11
+ readonly retryable: boolean;
12
+ constructor(message: string, code: SonglinesErrorCode, options?: {
13
+ statusCode?: number;
14
+ retryable?: boolean;
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ declare class InvalidConfigError extends SonglinesError {
19
+ constructor(message: string);
20
+ }
21
+ declare class InvalidApiKeyError extends SonglinesError {
22
+ constructor();
23
+ }
24
+ declare class NetworkError extends SonglinesError {
25
+ constructor(message: string, cause?: unknown);
26
+ }
27
+ declare class TimeoutError extends SonglinesError {
28
+ constructor(timeoutMs: number);
29
+ }
30
+ declare class RateLimitedError extends SonglinesError {
31
+ readonly retryAfterMs: number | undefined;
32
+ constructor(retryAfterMs?: number);
33
+ }
34
+ declare class ServerError extends SonglinesError {
35
+ constructor(statusCode: number, body?: string);
36
+ }
37
+ declare class QueueOverflowError extends SonglinesError {
38
+ readonly droppedCount: number;
39
+ constructor(droppedCount: number);
40
+ }
41
+ declare class PartialFailureError extends SonglinesError {
42
+ readonly accepted: number;
43
+ readonly failed: number;
44
+ readonly failedIds: string[];
45
+ constructor(accepted: number, failed: number, failedIds: string[]);
46
+ }
47
+
48
+ /**
49
+ * @songlines/sdk — Core Types
50
+ *
51
+ * All public-facing TypeScript types exported from the SDK.
52
+ * Prompt and completion text are intentionally excluded — the SDK
53
+ * captures metadata only. No prompt content is ever transmitted.
54
+ */
55
+
56
+ type Environment = "production" | "staging" | "development" | "test";
57
+ type RequestStatus = "success" | "error" | "blocked" | "pending";
58
+ interface SonglinesClientConfig {
59
+ /**
60
+ * Your Songlines API key. Must start with `sk-sl-`.
61
+ * Always load from an environment variable — never hardcode.
62
+ * @example process.env.SONGLINES_API_KEY
63
+ */
64
+ apiKey: string;
65
+ /**
66
+ * Base URL of the Songlines Control ingest API.
67
+ * @default "https://api.songlinesai.com"
68
+ */
69
+ baseUrl?: string;
70
+ /**
71
+ * Deployment environment tag applied to all events from this client instance.
72
+ * @default "production"
73
+ */
74
+ environment?: Environment;
75
+ /**
76
+ * Maximum number of events to accumulate before flushing to the API.
77
+ * @default 10
78
+ */
79
+ batchSize?: number;
80
+ /**
81
+ * Maximum time in milliseconds to wait before flushing a partial batch.
82
+ * @default 5000
83
+ */
84
+ flushIntervalMs?: number;
85
+ /**
86
+ * HTTP request timeout in milliseconds.
87
+ * @default 10000
88
+ */
89
+ timeout?: number;
90
+ /**
91
+ * Number of times to retry a failed flush before dropping events.
92
+ * Uses exponential backoff with jitter.
93
+ * @default 3
94
+ */
95
+ retries?: number;
96
+ /**
97
+ * Called when events are dropped due to network failure or queue overflow.
98
+ * The SDK never throws — all errors are surfaced here.
99
+ */
100
+ onError?: (error: SonglinesError) => void;
101
+ /**
102
+ * When true, logs internal SDK activity to console.debug.
103
+ * @default false
104
+ */
105
+ debug?: boolean;
106
+ }
107
+ interface TrackAIRequestParams {
108
+ /**
109
+ * Caller-generated unique identifier for this request.
110
+ * Used for idempotency — duplicate IDs within a 24-hour window are ignored.
111
+ * UUID v4 recommended. If omitted, the SDK generates one automatically.
112
+ */
113
+ requestId?: string;
114
+ /**
115
+ * Model name exactly as called.
116
+ * @example "gpt-4o", "claude-3-5-sonnet-20241022", "mistral-large-latest"
117
+ */
118
+ model: string;
119
+ /**
120
+ * Provider name.
121
+ * @example "openai", "anthropic", "azure", "bedrock", "mistral", "groq"
122
+ */
123
+ provider?: string;
124
+ /**
125
+ * Logical workflow or application name. Used for cost attribution and grouping.
126
+ * @example "invoice-processor", "customer-support", "document-review"
127
+ */
128
+ workflow?: string;
129
+ /**
130
+ * Step within a multi-step workflow.
131
+ * @example "extract", "summarise", "classify", "generate"
132
+ */
133
+ step?: string;
134
+ /**
135
+ * Agent or service identifier for multi-agent architectures.
136
+ * @example "orchestrator", "researcher", "writer"
137
+ */
138
+ agentId?: string;
139
+ /**
140
+ * Opaque user identifier. Must not contain PII — use an internal ID or hash.
141
+ * @example "user_8f3a2b", "team_finance"
142
+ */
143
+ user?: string;
144
+ /**
145
+ * Number of input (prompt) tokens consumed.
146
+ */
147
+ inputTokens: number;
148
+ /**
149
+ * Number of output (completion) tokens generated.
150
+ */
151
+ outputTokens: number;
152
+ /**
153
+ * End-to-end wall time in milliseconds from request dispatch to response received.
154
+ */
155
+ latencyMs?: number;
156
+ /**
157
+ * Actual cost in USD. If omitted, the SDK estimates cost from built-in model rates.
158
+ */
159
+ cost?: number;
160
+ /**
161
+ * Request outcome.
162
+ * @default "success"
163
+ */
164
+ status?: RequestStatus;
165
+ /**
166
+ * Error message if status is "error".
167
+ */
168
+ errorMessage?: string;
169
+ /**
170
+ * Timestamp of when the request was made.
171
+ * @default new Date()
172
+ */
173
+ timestamp?: Date;
174
+ /**
175
+ * Additional key-value metadata stored alongside the event.
176
+ * Do not include PII or sensitive data — values are visible in the dashboard.
177
+ */
178
+ metadata?: Record<string, string | number | boolean>;
179
+ }
180
+ interface IngestEvent {
181
+ request_id: string;
182
+ model: string;
183
+ provider?: string;
184
+ workflow?: string;
185
+ step?: string;
186
+ agent_id?: string;
187
+ user?: string;
188
+ environment: Environment;
189
+ input_tokens: number;
190
+ output_tokens: number;
191
+ latency_ms?: number;
192
+ status: RequestStatus;
193
+ error_message?: string;
194
+ timestamp?: string;
195
+ }
196
+ interface IngestResult {
197
+ request_id: string;
198
+ cost: number;
199
+ }
200
+ interface IngestError {
201
+ request_id: string;
202
+ error: string;
203
+ }
204
+ interface IngestResponse {
205
+ accepted: number;
206
+ failed: number;
207
+ results: IngestResult[];
208
+ errors?: IngestError[];
209
+ }
210
+ type SonglinesErrorCode = "INVALID_API_KEY" | "NETWORK_ERROR" | "TIMEOUT" | "RATE_LIMITED" | "SERVER_ERROR" | "INVALID_CONFIG" | "QUEUE_OVERFLOW" | "PARTIAL_FAILURE";
211
+ interface WrapOptions {
212
+ /**
213
+ * Workflow name applied to all calls made through this wrapped client.
214
+ */
215
+ workflow?: string;
216
+ /**
217
+ * Step name applied to all calls made through this wrapped client.
218
+ */
219
+ step?: string;
220
+ /**
221
+ * Agent ID applied to all calls made through this wrapped client.
222
+ */
223
+ agentId?: string;
224
+ /**
225
+ * User identifier applied to all calls made through this wrapped client.
226
+ */
227
+ user?: string;
228
+ }
229
+
230
+ /**
231
+ * @songlines/sdk — Guardrail Evaluation (v0.2.0)
232
+ *
233
+ * Provides real-time policy evaluation via the Songlines Gateway API.
234
+ * Call evaluateGuardrail() before sending a prompt to an LLM to enforce
235
+ * policies inline — blocking, redacting, or alerting as configured.
236
+ *
237
+ * Architecture note: This feature uses the Songlines Gateway / Enforce path,
238
+ * which is distinct from the Control Ingest API used by trackAIRequest().
239
+ * The Gateway sits inline in the request path; the Control Ingest API
240
+ * receives telemetry after the model response.
241
+ */
242
+
243
+ /** The outcome of a guardrail evaluation. */
244
+ type GuardrailDecision = "allow" | "block" | "modify";
245
+ /** The action configured on the policy that triggered this violation. */
246
+ type PolicyAction = "block" | "alert" | "redact" | "log";
247
+ /** A single policy violation detected during evaluation. */
248
+ interface GuardrailViolation {
249
+ /** Internal policy identifier. */
250
+ policyId: string;
251
+ /** Human-readable policy name. */
252
+ policyName: string;
253
+ /** The action the policy is configured to take. */
254
+ action: PolicyAction;
255
+ /** Human-readable reason for the violation. */
256
+ reason: string;
257
+ /** The field that triggered the violation (e.g. "prompt", "model", "workflow"). */
258
+ field: string;
259
+ }
260
+ /** The result of a guardrail evaluation. */
261
+ interface GuardrailResult {
262
+ /** The overall decision: allow, block, or modify. */
263
+ decision: GuardrailDecision;
264
+ /** All violations detected across all evaluated policies. */
265
+ violations: GuardrailViolation[];
266
+ /**
267
+ * The modified prompt to send to the LLM.
268
+ * Only present when `decision === "modify"` (e.g. PII redaction).
269
+ * Use this instead of the original prompt when decision is "modify".
270
+ */
271
+ modifiedInput?: string;
272
+ /** Evaluation latency in milliseconds. */
273
+ latencyMs: number;
274
+ /** Unique identifier for this evaluation (for audit correlation). */
275
+ evaluationId: string;
276
+ }
277
+ /** Parameters for evaluateGuardrail(). */
278
+ interface EvaluateGuardrailParams {
279
+ /**
280
+ * The prompt text to evaluate. This is the only content sent to the
281
+ * Gateway — it is evaluated against policies and then discarded.
282
+ * It is never stored in the Songlines platform.
283
+ */
284
+ prompt: string;
285
+ /**
286
+ * The model identifier the prompt will be sent to.
287
+ * Used for model allowlist/denylist policy evaluation.
288
+ */
289
+ model: string;
290
+ /**
291
+ * The logical workflow or feature name.
292
+ * Used for workflow-scoped policy evaluation.
293
+ */
294
+ workflow?: string;
295
+ /**
296
+ * The provider name (e.g. "openai", "anthropic", "azure-openai").
297
+ * Used for provider-level policy enforcement.
298
+ */
299
+ provider?: string;
300
+ /**
301
+ * When true, throws a GuardrailBlockedError if the decision is "block".
302
+ * When false (default), returns the result and lets the caller decide.
303
+ * @default false
304
+ */
305
+ throwOnBlock?: boolean;
306
+ }
307
+ /**
308
+ * Thrown by evaluateGuardrail() when `throwOnBlock: true` and the
309
+ * guardrail returns `decision: "block"`.
310
+ *
311
+ * The full GuardrailResult is available on the `result` property.
312
+ */
313
+ declare class GuardrailBlockedError extends Error {
314
+ readonly result: GuardrailResult;
315
+ constructor(result: GuardrailResult);
316
+ }
317
+
318
+ /**
319
+ * Songlines Control SDK client.
320
+ *
321
+ * Instruments AI workloads with a single `trackAIRequest()` call per LLM
322
+ * invocation. All telemetry is batched and sent asynchronously — the SDK
323
+ * never blocks or slows down your AI calls.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * import { SonglinesClient } from "@songlines/sdk";
328
+ *
329
+ * const songlines = new SonglinesClient({
330
+ * apiKey: process.env.SONGLINES_API_KEY!,
331
+ * });
332
+ *
333
+ * // After every LLM call:
334
+ * await songlines.trackAIRequest({
335
+ * model: "gpt-4o",
336
+ * provider: "openai",
337
+ * workflow: "invoice-processor",
338
+ * inputTokens: response.usage.prompt_tokens,
339
+ * outputTokens: response.usage.completion_tokens,
340
+ * latencyMs: Date.now() - startTime,
341
+ * });
342
+ * ```
343
+ */
344
+ declare class SonglinesClient {
345
+ private readonly config;
346
+ private readonly queue;
347
+ constructor(config: SonglinesClientConfig);
348
+ /**
349
+ * Records an AI request event. The event is queued immediately and sent
350
+ * asynchronously — this method returns as soon as the event is enqueued.
351
+ *
352
+ * The returned Promise resolves when the event has been enqueued (not when
353
+ * it has been sent). Use `flush()` if you need confirmation of delivery.
354
+ */
355
+ trackAIRequest(params: TrackAIRequestParams): Promise<void>;
356
+ /**
357
+ * Forces an immediate flush of all queued events to the API.
358
+ * Resolves when the flush completes (successfully or not).
359
+ */
360
+ flush(): Promise<void>;
361
+ /**
362
+ * Flushes all remaining events and shuts down the background timer.
363
+ * Call this during graceful shutdown (e.g. in a `process.on("SIGTERM")` handler).
364
+ *
365
+ * @example
366
+ * ```typescript
367
+ * process.on("SIGTERM", async () => {
368
+ * await songlines.shutdown();
369
+ * process.exit(0);
370
+ * });
371
+ * ```
372
+ */
373
+ shutdown(): Promise<void>;
374
+ /**
375
+ * Evaluates a prompt against all active Songlines policies in real time.
376
+ *
377
+ * This method uses the **Songlines Gateway / Enforce** path, which is
378
+ * distinct from `trackAIRequest()`. Call it *before* sending the prompt
379
+ * to an LLM to enforce policies inline.
380
+ *
381
+ * @returns A `GuardrailResult` with `decision`, `violations`, and optionally
382
+ * a `modifiedInput` (when a redaction policy fires).
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * const result = await songlines.evaluateGuardrail({
387
+ * prompt: userMessage,
388
+ * model: "gpt-4o",
389
+ * workflow: "customer-support",
390
+ * });
391
+ *
392
+ * if (result.decision === "block") {
393
+ * return { error: "Request blocked by policy", violations: result.violations };
394
+ * }
395
+ *
396
+ * const promptToSend = result.modifiedInput ?? userMessage;
397
+ * const response = await openai.chat.completions.create({ ... });
398
+ * ```
399
+ */
400
+ evaluateGuardrail(params: EvaluateGuardrailParams): Promise<GuardrailResult>;
401
+ /**
402
+ * Converts TrackAIRequestParams into the wire format expected by /api/ingest.
403
+ */
404
+ private buildEvent;
405
+ /**
406
+ * Sends a batch of events to POST /api/ingest.
407
+ * Called by the BatchQueue — never called directly.
408
+ */
409
+ private sendBatch;
410
+ /**
411
+ * Performs a single HTTP POST with timeout and error classification.
412
+ */
413
+ private httpPost;
414
+ }
415
+
416
+ /**
417
+ * OpenAI SDK wrapper for Songlines Control.
418
+ *
419
+ * Wraps an OpenAI client instance so that every `chat.completions.create()`
420
+ * and `completions.create()` call is automatically instrumented with telemetry.
421
+ *
422
+ * Prompt and completion text are NEVER captured — only metadata
423
+ * (model, tokens, latency, cost) is sent to the Songlines ingest API.
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * import OpenAI from "openai";
428
+ * import { SonglinesClient } from "@songlines/sdk";
429
+ *
430
+ * const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
431
+ * const songlines = new SonglinesClient({ apiKey: process.env.SONGLINES_API_KEY! });
432
+ *
433
+ * const instrumentedOpenAI = songlines.wrapOpenAI(openai, {
434
+ * workflow: "invoice-processor",
435
+ * });
436
+ *
437
+ * // All calls through instrumentedOpenAI are automatically tracked
438
+ * const response = await instrumentedOpenAI.chat.completions.create({
439
+ * model: "gpt-4o",
440
+ * messages: [{ role: "user", content: "Hello" }],
441
+ * });
442
+ * ```
443
+ */
444
+
445
+ /**
446
+ * Wraps an OpenAI client to automatically track all LLM calls.
447
+ * Returns a Proxy that is type-compatible with the original client.
448
+ */
449
+ declare function wrapOpenAI<T extends object>(client: T, songlines: SonglinesClient, defaults?: WrapOptions): T;
450
+
451
+ /**
452
+ * Anthropic SDK wrapper for Songlines Control.
453
+ *
454
+ * Wraps an Anthropic client instance so that every `messages.create()` call
455
+ * is automatically instrumented with telemetry.
456
+ *
457
+ * Prompt and completion text are NEVER captured — only metadata
458
+ * (model, tokens, latency, cost) is sent to the Songlines ingest API.
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * import Anthropic from "@anthropic-ai/sdk";
463
+ * import { SonglinesClient } from "@songlines/sdk";
464
+ *
465
+ * const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
466
+ * const songlines = new SonglinesClient({ apiKey: process.env.SONGLINES_API_KEY! });
467
+ *
468
+ * const instrumentedAnthropic = songlines.wrapAnthropic(anthropic, {
469
+ * workflow: "document-review",
470
+ * });
471
+ *
472
+ * const response = await instrumentedAnthropic.messages.create({
473
+ * model: "claude-3-5-sonnet-20241022",
474
+ * max_tokens: 1024,
475
+ * messages: [{ role: "user", content: "Hello" }],
476
+ * });
477
+ * ```
478
+ */
479
+
480
+ /**
481
+ * Wraps an Anthropic client to automatically track all `messages.create()` calls.
482
+ */
483
+ declare function wrapAnthropic<T extends object>(client: T, songlines: SonglinesClient, defaults?: WrapOptions): T;
484
+
485
+ /**
486
+ * Client-side cost estimation.
487
+ *
488
+ * Provides approximate USD costs based on publicly available per-million-token
489
+ * pricing. Actual costs may differ due to batch discounts, enterprise agreements,
490
+ * or provider pricing changes.
491
+ *
492
+ * Rates are per 1,000,000 tokens (input / output) in USD.
493
+ * Last updated: June 2026.
494
+ */
495
+ interface ModelRates {
496
+ /** Cost per 1M input tokens in USD */
497
+ input: number;
498
+ /** Cost per 1M output tokens in USD */
499
+ output: number;
500
+ }
501
+ interface CostEstimateParams {
502
+ model: string;
503
+ inputTokens: number;
504
+ outputTokens: number;
505
+ }
506
+ /**
507
+ * Estimates the USD cost of an AI request based on public model pricing.
508
+ *
509
+ * @returns Estimated cost in USD, rounded to 8 decimal places.
510
+ * Returns 0 if token counts are 0.
511
+ */
512
+ declare function estimateCost({ model, inputTokens, outputTokens }: CostEstimateParams): number;
513
+ /**
514
+ * Returns the known rate table. Useful for displaying pricing information.
515
+ */
516
+ declare function getModelRates(): Readonly<Record<string, ModelRates>>;
517
+
518
+ export { type Environment, type EvaluateGuardrailParams, GuardrailBlockedError, type GuardrailDecision, type GuardrailResult, type GuardrailViolation, type IngestError, type IngestEvent, type IngestResponse, type IngestResult, InvalidApiKeyError, InvalidConfigError, NetworkError, PartialFailureError, type PolicyAction, QueueOverflowError, RateLimitedError, type RequestStatus, ServerError, SonglinesClient, type SonglinesClientConfig, SonglinesError, type SonglinesErrorCode, TimeoutError, type TrackAIRequestParams, type WrapOptions, estimateCost, getModelRates, wrapAnthropic, wrapOpenAI };