@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 ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@songlines/sdk` are documented here.
4
+
5
+ This project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ---
8
+
9
+ ## [0.2.0] — Unreleased
10
+
11
+ ### Added
12
+ - `evaluateGuardrail(params)` — real-time policy evaluation via the Songlines Gateway API
13
+ - `SonglinesClient.evaluateGuardrail()` — synchronous guardrail check before sending to an LLM
14
+ - `GuardrailResult` type with `decision`, `violations`, `modifiedInput`, and `latencyMs`
15
+ - `GuardrailViolation` type with `policyId`, `policyName`, `action`, `reason`, and `field`
16
+ - `GuardrailBlockedError` — thrown when a guardrail returns `decision: "block"` and `throwOnBlock: true`
17
+ - Tests for allow, block, modify, and multiple-violation scenarios
18
+
19
+ ---
20
+
21
+ ## [0.1.0] — 2026-06-23
22
+
23
+ ### Added
24
+ - `SonglinesClient` with `trackAIRequest()`, `flush()`, and `shutdown()`
25
+ - `wrapOpenAI()` — transparent proxy for the OpenAI SDK
26
+ - `wrapAnthropic()` — transparent proxy for the Anthropic SDK
27
+ - `BatchQueue` — async batching with configurable `batchSize` and `flushIntervalMs`
28
+ - Exponential backoff retry with jitter (`withRetry()`)
29
+ - Built-in cost estimation for 14+ models (`estimateCost()`, `getModelRates()`)
30
+ - Zero-dependency UUID v4 generator
31
+ - 9 typed error classes surfaced via `onError` callback
32
+ - Dual ESM + CJS build with TypeScript declarations
33
+ - 76 unit tests across 5 test files
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cetus AI Pty Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,409 @@
1
+ # @songlines/sdk
2
+
3
+ Official TypeScript/JavaScript SDK for **Songlines Control** — AI observability, cost attribution, and governance for enterprise workloads.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@songlines/sdk)](https://www.npmjs.com/package/@songlines/sdk)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **Zero runtime dependencies** in the core ingest path — no supply chain risk, no version conflicts
14
+ - **Fire-and-forget** — `trackAIRequest()` never blocks or slows down your AI calls
15
+ - **Automatic batching** — events are queued and flushed in configurable batches
16
+ - **Exponential backoff** — failed flushes are retried automatically with jitter
17
+ - **OpenAI & Anthropic wrappers** — one-line instrumentation with `wrapOpenAI()` / `wrapAnthropic()`
18
+ - **Prompt-safe by design** — prompt content is never captured or transmitted
19
+ - **Dual ESM + CJS** — works in Node.js, edge runtimes, and bundlers
20
+ - **Full TypeScript types** — end-to-end type safety with declaration maps
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @songlines/sdk
28
+ # or
29
+ pnpm add @songlines/sdk
30
+ # or
31
+ yarn add @songlines/sdk
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ### Manual tracking
39
+
40
+ ```typescript
41
+ import { SonglinesClient } from "@songlines/sdk";
42
+
43
+ const songlines = new SonglinesClient({
44
+ apiKey: process.env.SONGLINES_API_KEY!,
45
+ });
46
+
47
+ // After your AI call completes:
48
+ await songlines.trackAIRequest({
49
+ model: "gpt-4o",
50
+ provider: "openai",
51
+ workflow: "invoice-processor",
52
+ inputTokens: 1200,
53
+ outputTokens: 400,
54
+ latencyMs: 1840,
55
+ status: "success",
56
+ });
57
+
58
+ // Flush on graceful shutdown
59
+ process.on("SIGTERM", async () => {
60
+ await songlines.shutdown();
61
+ process.exit(0);
62
+ });
63
+ ```
64
+
65
+ ### OpenAI wrapper (recommended)
66
+
67
+ ```typescript
68
+ import OpenAI from "openai";
69
+ import { SonglinesClient, wrapOpenAI } from "@songlines/sdk";
70
+
71
+ const openai = wrapOpenAI(new OpenAI(), new SonglinesClient({
72
+ apiKey: process.env.SONGLINES_API_KEY!,
73
+ }), {
74
+ workflow: "customer-support",
75
+ environment: "production",
76
+ });
77
+
78
+ // All calls are automatically tracked — no code changes needed
79
+ const response = await openai.chat.completions.create({
80
+ model: "gpt-4o",
81
+ messages: [{ role: "user", content: "Hello" }],
82
+ });
83
+ ```
84
+
85
+ ### Anthropic wrapper
86
+
87
+ ```typescript
88
+ import Anthropic from "@anthropic-ai/sdk";
89
+ import { SonglinesClient, wrapAnthropic } from "@songlines/sdk";
90
+
91
+ const anthropic = wrapAnthropic(new Anthropic(), new SonglinesClient({
92
+ apiKey: process.env.SONGLINES_API_KEY!,
93
+ }), {
94
+ workflow: "document-review",
95
+ });
96
+
97
+ const message = await anthropic.messages.create({
98
+ model: "claude-3-5-sonnet-20241022",
99
+ max_tokens: 1024,
100
+ messages: [{ role: "user", content: "Summarise this document." }],
101
+ });
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Configuration
107
+
108
+ ```typescript
109
+ const songlines = new SonglinesClient({
110
+ // Required
111
+ apiKey: process.env.SONGLINES_API_KEY!,
112
+
113
+ // Optional — defaults shown
114
+ baseUrl: "https://api.songlinesai.com", // Override for on-premises deployments
115
+ environment: "production", // "production" | "staging" | "development" | "test"
116
+ batchSize: 10, // Flush after N events (1–100)
117
+ flushIntervalMs: 5000, // Flush every N ms (100–60000)
118
+ timeout: 10000, // HTTP request timeout in ms
119
+ retries: 3, // Retry attempts on network failure (0–10)
120
+ debug: false, // Log SDK internals to console.debug
121
+
122
+ // Error callback — called when events are dropped after all retries
123
+ onError: (error) => {
124
+ console.error("[Songlines SDK]", error.code, error.message);
125
+ },
126
+ });
127
+ ```
128
+
129
+ ---
130
+
131
+ ## API Reference
132
+
133
+ ### `SonglinesClient`
134
+
135
+ #### `trackAIRequest(params)`
136
+
137
+ Records an AI request event. Returns immediately — the event is queued and sent asynchronously.
138
+
139
+ | Parameter | Type | Required | Description |
140
+ |---|---|---|---|
141
+ | `model` | `string` | Yes | Model identifier (e.g. `"gpt-4o"`, `"claude-3-5-sonnet-20241022"`) |
142
+ | `provider` | `string` | No | Provider name (e.g. `"openai"`, `"anthropic"`, `"azure"`) |
143
+ | `workflow` | `string` | No | Logical workflow or feature name for cost attribution |
144
+ | `step` | `string` | No | Step within a multi-step workflow |
145
+ | `agentId` | `string` | No | Agent identifier for multi-agent systems |
146
+ | `user` | `string` | No | End-user identifier (hashed/anonymised) |
147
+ | `inputTokens` | `number` | No | Prompt token count |
148
+ | `outputTokens` | `number` | No | Completion token count |
149
+ | `latencyMs` | `number` | No | End-to-end latency in milliseconds |
150
+ | `cost` | `number` | No | Actual cost in USD (auto-estimated if omitted) |
151
+ | `status` | `RequestStatus` | No | `"success"` \| `"error"` \| `"blocked"` \| `"pending"` (default: `"success"`) |
152
+ | `requestId` | `string` | No | Idempotency key (UUID auto-generated if omitted) |
153
+ | `timestamp` | `Date` | No | Event timestamp (current time if omitted) |
154
+
155
+ #### `flush()`
156
+
157
+ Forces immediate delivery of all queued events. Resolves when the flush completes.
158
+
159
+ ```typescript
160
+ await songlines.flush();
161
+ ```
162
+
163
+ #### `shutdown()`
164
+
165
+ Flushes remaining events and stops the background timer. Call during graceful shutdown.
166
+
167
+ ```typescript
168
+ await songlines.shutdown();
169
+ ```
170
+
171
+ ---
172
+
173
+ ### `wrapOpenAI(client, songlinesClient, options?)`
174
+
175
+ Returns a transparent proxy of the OpenAI client that automatically calls `trackAIRequest()` after every `chat.completions.create()` call.
176
+
177
+ **Options** (`WrapOptions`):
178
+
179
+ | Option | Type | Description |
180
+ |---|---|---|
181
+ | `workflow` | `string` | Workflow tag applied to all calls via this wrapper |
182
+ | `step` | `string` | Step tag applied to all calls |
183
+ | `agentId` | `string` | Agent identifier |
184
+ | `user` | `string` | End-user identifier |
185
+
186
+ **Behaviour:**
187
+ - Token counts are read from `response.usage` automatically
188
+ - Latency is measured from call start to response received
189
+ - On API error, `status: "error"` is recorded and the error is re-thrown
190
+ - Streaming calls (`stream: true`) are tracked with `inputTokens: 0, outputTokens: 0` — token counts are not available from the stream
191
+ - Prompt content is never captured
192
+
193
+ ---
194
+
195
+ ### `wrapAnthropic(client, songlinesClient, options?)`
196
+
197
+ Returns a transparent proxy of the Anthropic client that automatically calls `trackAIRequest()` after every `messages.create()` call.
198
+
199
+ Behaviour is identical to `wrapOpenAI()` — token counts from `response.usage.input_tokens` / `response.usage.output_tokens`, latency measured end-to-end, errors recorded and re-thrown.
200
+
201
+ ---
202
+
203
+ ### Cost Estimation
204
+
205
+ If `cost` is not provided to `trackAIRequest()`, the SDK estimates it from the model name and token counts using a built-in rate table. The table is updated with each SDK release.
206
+
207
+ ```typescript
208
+ import { estimateCost, getModelRates } from "@songlines/sdk";
209
+
210
+ // Estimate cost for a specific call
211
+ const cost = estimateCost({
212
+ model: "gpt-4o",
213
+ inputTokens: 1000,
214
+ outputTokens: 500,
215
+ });
216
+ // → 0.00750 (USD)
217
+
218
+ // Inspect the full rate table
219
+ const rates = getModelRates();
220
+ // → { "gpt-4o": { input: 2.50, output: 10.00 }, ... }
221
+ ```
222
+
223
+ **Supported models (built-in rates):**
224
+
225
+ | Model | Input ($/M tokens) | Output ($/M tokens) |
226
+ |---|---|---|
227
+ | gpt-4o | $2.50 | $10.00 |
228
+ | gpt-4o-mini | $0.15 | $0.60 |
229
+ | gpt-4-turbo | $10.00 | $30.00 |
230
+ | gpt-3.5-turbo | $0.50 | $1.50 |
231
+ | o1 | $15.00 | $60.00 |
232
+ | o1-mini | $3.00 | $12.00 |
233
+ | o3-mini | $1.10 | $4.40 |
234
+ | claude-3-5-sonnet | $3.00 | $15.00 |
235
+ | claude-3-5-haiku | $0.80 | $4.00 |
236
+ | claude-3-opus | $15.00 | $75.00 |
237
+ | gemini-1.5-pro | $1.25 | $5.00 |
238
+ | gemini-1.5-flash | $0.075 | $0.30 |
239
+ | Unknown models | $1.00 | $3.00 |
240
+
241
+ ---
242
+
243
+ ## Error Handling
244
+
245
+ The SDK never throws. All errors are surfaced via the `onError` callback.
246
+
247
+ ```typescript
248
+ import {
249
+ InvalidApiKeyError,
250
+ NetworkError,
251
+ RateLimitedError,
252
+ ServerError,
253
+ QueueOverflowError,
254
+ PartialFailureError,
255
+ } from "@songlines/sdk";
256
+
257
+ const songlines = new SonglinesClient({
258
+ apiKey: process.env.SONGLINES_API_KEY!,
259
+ onError: (error) => {
260
+ if (error instanceof InvalidApiKeyError) {
261
+ // API key is invalid — alert immediately
262
+ alertOps("Invalid Songlines API key");
263
+ } else if (error instanceof RateLimitedError) {
264
+ // Back off — error.retryAfterMs is available if the server sent Retry-After
265
+ console.warn(`Rate limited. Retry after ${error.retryAfterMs}ms`);
266
+ } else if (error instanceof QueueOverflowError) {
267
+ // Events were dropped — queue is full
268
+ metrics.increment("songlines.events.dropped", error.droppedCount);
269
+ } else if (error instanceof NetworkError) {
270
+ // Transient network failure after all retries exhausted
271
+ metrics.increment("songlines.flush.failed");
272
+ }
273
+ },
274
+ });
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Framework Examples
280
+
281
+ ### Express.js middleware
282
+
283
+ ```typescript
284
+ import express from "express";
285
+ import OpenAI from "openai";
286
+ import { SonglinesClient, wrapOpenAI } from "@songlines/sdk";
287
+
288
+ const songlines = new SonglinesClient({ apiKey: process.env.SONGLINES_API_KEY! });
289
+ const openai = wrapOpenAI(new OpenAI(), songlines);
290
+
291
+ const app = express();
292
+
293
+ app.post("/chat", async (req, res) => {
294
+ const { message, userId } = req.body;
295
+
296
+ const response = await openai.chat.completions.create({
297
+ model: "gpt-4o",
298
+ messages: [{ role: "user", content: message }],
299
+ // Songlines metadata passed via options at wrap time
300
+ });
301
+
302
+ res.json({ reply: response.choices[0]?.message.content });
303
+ });
304
+
305
+ process.on("SIGTERM", async () => {
306
+ await songlines.shutdown();
307
+ process.exit(0);
308
+ });
309
+ ```
310
+
311
+ ### Next.js API route
312
+
313
+ ```typescript
314
+ // app/api/chat/route.ts
315
+ import { SonglinesClient, wrapOpenAI } from "@songlines/sdk";
316
+ import OpenAI from "openai";
317
+
318
+ // Instantiate once per cold start
319
+ const songlines = new SonglinesClient({ apiKey: process.env.SONGLINES_API_KEY! });
320
+ const openai = wrapOpenAI(new OpenAI(), songlines, { workflow: "chat" });
321
+
322
+ export async function POST(req: Request) {
323
+ const { messages } = await req.json();
324
+ const response = await openai.chat.completions.create({ model: "gpt-4o", messages });
325
+ return Response.json(response);
326
+ }
327
+ ```
328
+
329
+ ### LangChain callback
330
+
331
+ ```typescript
332
+ import { SonglinesClient } from "@songlines/sdk";
333
+ import { ChatOpenAI } from "@langchain/openai";
334
+
335
+ const songlines = new SonglinesClient({ apiKey: process.env.SONGLINES_API_KEY! });
336
+
337
+ const llm = new ChatOpenAI({ model: "gpt-4o" });
338
+
339
+ // After each LangChain call, track manually:
340
+ const result = await llm.invoke("Hello");
341
+ await songlines.trackAIRequest({
342
+ model: "gpt-4o",
343
+ provider: "openai",
344
+ workflow: "langchain-agent",
345
+ inputTokens: result.usage_metadata?.input_tokens,
346
+ outputTokens: result.usage_metadata?.output_tokens,
347
+ });
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Privacy & Security
353
+
354
+ **Prompt content is never captured.** The `wrapOpenAI()` and `wrapAnthropic()` proxies read only:
355
+ - `response.usage` (token counts)
356
+ - `response.model` (model name)
357
+ - Request start/end timestamps (latency)
358
+
359
+ The message content, system prompts, and completion text are never accessed, stored, or transmitted by the SDK. This is a hard architectural boundary, not a configuration option.
360
+
361
+ **API key security:**
362
+ - Always load from environment variables — never hardcode
363
+ - The API key is sent only in the `Authorization: Bearer` header over HTTPS
364
+ - The SDK does not log the API key, even in `debug` mode
365
+
366
+ ---
367
+
368
+ ## Batching & Performance
369
+
370
+ The SDK uses an in-memory queue with automatic batching to minimise API calls:
371
+
372
+ ```
373
+ trackAIRequest() → queue.push() → [returns immediately]
374
+
375
+ [batch reaches batchSize]
376
+ [OR flushIntervalMs elapses]
377
+
378
+ POST /api/ingest (batch)
379
+ [retry with backoff on failure]
380
+ ```
381
+
382
+ **Default behaviour:**
383
+ - Events are batched up to 10 at a time
384
+ - A partial batch is flushed every 5 seconds
385
+ - Failed flushes are retried 3 times with exponential backoff (500ms base, 2× multiplier, ±20% jitter)
386
+ - Events that fail all retries are dropped and reported via `onError`
387
+ - The queue holds up to 1,000 events; overflow events are dropped and reported
388
+
389
+ **Memory impact:** Each event is approximately 200–400 bytes. At the default queue cap of 1,000 events, the maximum memory footprint is approximately 400 KB.
390
+
391
+ ---
392
+
393
+ ## Changelog
394
+
395
+ ### 0.1.0 (2026-06-23)
396
+ - Initial release
397
+ - `SonglinesClient` with `trackAIRequest()`, `flush()`, `shutdown()`
398
+ - `wrapOpenAI()` proxy wrapper
399
+ - `wrapAnthropic()` proxy wrapper
400
+ - Built-in cost estimation for 14+ models
401
+ - Exponential backoff retry with jitter
402
+ - Dual ESM + CJS build
403
+ - Full TypeScript declarations
404
+
405
+ ---
406
+
407
+ ## License
408
+
409
+ MIT © Cetus AI Pty Ltd