calado
v0.4.1
Published
Official calado SDK — wrap your Anthropic or OpenAI client to capture conversations and agent definitions for behavioral analytics.
Maintainers
Readme
calado
Official SDK for calado — behavioral analytics for AI agents. Wrap your Anthropic or OpenAI client and calado captures conversations, system prompts, and tool schemas for analysis.
Install
npm install calado
# or
pnpm add calado
# or
yarn add caladoQuickstart
import Anthropic from "@anthropic-ai/sdk";
import { calado } from "calado";
calado.init(process.env.CALADO_API_KEY!);
const anthropic = calado.wrap(new Anthropic());
// Use the client normally — calado captures in the background.
await anthropic.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system: "You are a helpful assistant.",
messages: [{ role: "user", content: "hello" }],
});OpenAI works the same way:
import OpenAI from "openai";
import { calado } from "calado";
calado.init(process.env.CALADO_API_KEY!);
const openai = calado.wrap(new OpenAI());Get your API key from your agent's settings in the calado dashboard.
Configuration
calado.init(apiKey, {
baseUrl: "https://app.calado.ai", // override for self-hosted
batchSize: 10, // flush when queue reaches this
flushInterval: 5_000, // flush every N ms (0 disables timer)
maxQueueEvents: 10_000, // drop-oldest overflow threshold
maxQueueBytes: 10 * 1024 * 1024, // byte-based overflow threshold
debug: false, // structured console logs
mask: undefined, // (event) => event | null. See "Redacting sensitive data".
});Redacting sensitive data
mask runs synchronously on every event before transport. Return a redacted copy, or return null / undefined to drop the event entirely. The callback runs inside your process, so raw values never leave the machine.
const EMAIL = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
const PHONE = /\+?\d[\d\s().-]{7,}\d/g;
const scrub = (s: string) => s.replace(EMAIL, "[EMAIL]").replace(PHONE, "[PHONE]");
// content fields may be a string or an array of content blocks (Anthropic
// blocks, OpenAI multimodal parts, tool results). Handle both.
function scrubContent(value: unknown): unknown {
if (typeof value === "string") return scrub(value);
if (Array.isArray(value)) {
for (const block of value) {
if (block && typeof block === "object") {
const b = block as { text?: unknown; content?: unknown };
if (typeof b.text === "string") b.text = scrub(b.text);
if (b.content !== undefined) b.content = scrubContent(b.content);
}
}
}
return value;
}
calado.init(process.env.CALADO_API_KEY!, {
mask: (event) => {
if (event.kind === "definition") {
event.payload.content = scrub(event.payload.content);
return event;
}
if (event.kind !== "conversation") return event;
const req = event.payload.request as {
messages?: Array<{ content?: unknown }>;
};
for (const m of req.messages ?? []) m.content = scrubContent(m.content);
// Response shape differs per provider; switch on event.format.
if (event.format === "anthropic") {
const res = event.payload.response as { content?: unknown };
res.content = scrubContent(res.content);
} else if (event.format === "openai_chat") {
const res = event.payload.response as {
choices?: Array<{ message?: { content?: unknown } }>;
};
for (const c of res.choices ?? []) {
if (c.message) c.message.content = scrubContent(c.message.content);
}
}
return event;
},
});For stable per-conversation placeholders ([EMAIL_1], [EMAIL_2]), use the exported createPlaceholderTracker() helper. The full guide, including Presidio pairing, allowlist patterns, and CI guards, is at docs.calado.ai/ingestion/redaction.
Serverless (important)
calado batches in memory and flushes on a timer. Serverless runtimes freeze between invocations, so you must flush explicitly before your handler returns.
Next.js App Router
import { after } from "next/server";
export async function POST() {
// ...
after(() => calado.flush());
return Response.json({ ok: true });
}Vercel Edge
export default async function handler(req: Request, ctx: { waitUntil: (p: Promise<unknown>) => void }) {
// ...
ctx.waitUntil(calado.flush());
return new Response("ok");
}AWS Lambda / any serverless
export async function handler(event) {
try {
// ...
return { statusCode: 200 };
} finally {
await calado.flush();
}
}Conversation context (Node.js)
In long-running Node processes, propagate a conversationId across async calls so calado groups multi-turn sessions correctly:
await calado.withConversation("conv_123", async () => {
await anthropic.messages.create({ /* ... */ });
await anthropic.messages.create({ /* ... */ }); // same session
});withContext(convId, userId, fn) is the longer form. Both use AsyncLocalStorage and require Node.js — in Vercel Edge Runtime, use calado.conversationId = "..." with manual reset.
API
| Method | Purpose |
|---|---|
| calado.init(apiKey, options?) | Initialize. Throws on bad config. |
| calado.wrap<T>(client) | Return a Proxy over your provider client. Unknown clients pass through unchanged. |
| calado.flush() | Send queued data now. |
| calado.shutdown() | Flush + clear timers. |
| calado.status() | Return { enabled, queued, lastIngestAt, lastError, consecutive401s, maskConfigured, maskFailures, consecutiveMaskFailures, maskedEvents, definitionsDropped }. |
| calado.withContext(convId, userId, fn) | Run fn with conversation context. |
| calado.withConversation(id, fn) | Shorthand for withContext(id, undefined, fn). |
| calado.conversationId | Get/set the active conversation id (scripts only). |
| Configuration: mask | (event: IngestionEvent) => IngestionEvent \| null \| undefined. Synchronous redaction callback applied to every event before transport. See Redacting sensitive data. |
| Configuration: createPlaceholderTracker() | Helper export. Returns a per-scope counter so the same raw value gets the same [CATEGORY_N] label across multi-turn conversations. |
What gets captured
- Full request (model, messages, temperature, etc.) with
systemandtoolsstripped — those are sent separately as agent definitions. - Full response body (content blocks, tool calls, usage, stop reason).
- Streaming: reconstructed final response. Aborted streams are marked
metadata.partial = true.
Error philosophy
- Init time: throws on bad config (invalid
cl_key, etc.). - Runtime: never throws. Your LLM calls always succeed even if calado's transport fails.
- 401 responses: log a warning every time. After 3 consecutive 401s the transport auto-disables. Fix your API key and call
initagain. - Mask auto-disable: after 100 consecutive
maskfailures the transport auto-disables.status().lastErroris set to'mask_disabled_after_100_failures: call calado.init() to recover'. Fix your mask and callinitagain. - Retry: 5xx / network errors only. Two retries with jittered backoff. 4xx responses drop immediately.
Supported runtimes
- Node.js 18+ (primary target)
- Bun
- Deno (via npm: specifier)
- Vercel Edge Runtime — pass-through works;
withContextrequires Node
License
MIT
