@raindrop-ai/ai-sdk
v0.1.1
Published
Standalone Vercel AI SDK integration for Raindrop (events + OTLP/HTTP JSON traces, no OTEL runtime)
Keywords
Readme
@raindrop-ai/ai-sdk
Standalone Vercel AI SDK integration for Raindrop:
- Events: sends a
track_partialpayload toPOST /v1/events/track_partialwhen the model finishes - Standalone traces: ships spans directly to
POST /v1/tracesas OTLP/HTTP JSON - No OpenTelemetry SDK init: avoids global OTEL registration conflicts
- Native v7 telemetry: opt-in callback-based integration via AI SDK v7's
TelemetryIntegrationinterface (no Proxy wrapping)
Install
pnpm add @raindrop-ai/ai-sdkUsage
import * as ai from "ai";
import { createRaindropAISDK, eventMetadata } from "@raindrop-ai/ai-sdk";
const raindrop = createRaindropAISDK({
writeKey: process.env.RAINDROP_WRITE_KEY!,
});
const { generateText } = raindrop.wrap(ai, {
// userId is optional here (but recommended). If omitted here, you can still provide it per-call via eventMetadata(). Otherwise events will be skipped.
context: { convoId: "convo_456", eventName: "chat_message" },
// optional: full control over event input/output, metadata, and attachments
buildEvent: (messages) => {
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
return {
input: typeof lastUser?.content === "string" ? lastUser.content : undefined,
output: typeof lastAssistant?.content === "string" ? lastAssistant.content : undefined,
};
},
});
const result = await generateText({
model: /* your AI SDK model */,
prompt: "Hello!",
experimental_telemetry: {
isEnabled: true,
metadata: eventMetadata({ userId: "user_123" }),
},
});
// Identify a user (optional)
await raindrop.users.identify({
userId: "user_123",
traits: { plan: "pro" },
});
await raindrop.flush();Projects
If your org has multiple projects, route events to a specific one by passing its
slug as projectId:
const raindrop = createRaindropAISDK({
writeKey: process.env.RAINDROP_WRITE_KEY!,
projectId: "support-prod",
});This sets the X-Raindrop-Project-Id header on every event and trace. Omit it
(or pass "default") to use your org's default Production project — the
existing behavior. Single-project orgs need nothing new.
Payload size limits
Text fields (event input/output, tool/LLM span payloads such as ai.prompt,
ai.toolCall.args, ai.toolCall.result) are capped at 1,000,000 characters
per field by default and truncated with a ...[truncated by raindrop]
marker. The cap is enforced before (or during) serialization, so a multi-MB
tool result costs the cap — not the payload — on your event loop, and
oversized events land truncated instead of being dropped at the ingest size
limit. Tune it via:
const raindrop = createRaindropAISDK({
writeKey: "...",
maxTextFieldChars: 250_000,
});A stricter OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT env var is honored. The
bounded helpers are exported (capText, boundedStringify,
TRUNCATION_MARKER, DEFAULT_MAX_TEXT_FIELD_CHARS) for use in custom
transformSpan hooks and buildEvent builders.
Manual Traces
Create trace spans manually alongside, or instead of, auto-instrumented ones.
Use createSpan when the timing is already known:
const eventId = "evt_123";
raindrop.traces.createSpan({
name: "SET theme=dark",
eventId,
operationId: "ai.toolCall",
input: "SET theme=dark",
output: "OK",
durationMs: 12,
});Use startSpan / endSpan when you want to time an operation in real time:
const span = raindrop.traces.startSpan({
name: "database_query",
eventId: "evt_123",
operationId: "ai.toolCall",
});
try {
await db.query("SELECT ...");
raindrop.traces.endSpan(span);
} catch (err) {
raindrop.traces.endSpan(span, { error: err instanceof Error ? err : String(err) });
}Pass a span as parent to build nested trace trees:
const eventId = "evt_456";
const agentTurn = raindrop.traces.startSpan({
name: "agent_turn",
eventId,
});
raindrop.traces.createSpan({
name: "grep_search",
eventId,
parent: agentTurn,
operationId: "ai.toolCall",
input: { pattern: "execute" },
output: { matches: 12 },
durationMs: 120,
});
raindrop.traces.endSpan(agentTurn);
await raindrop.flush();AI SDK v7 quick start: registerTelemetry(raindrop())
On AI SDK v7, the simplest setup is the raindrop() factory, which returns a
ready-to-register telemetry integration. The writeKey defaults to the
RAINDROP_WRITE_KEY environment variable.
import { generateText, registerTelemetry } from "ai";
import { raindrop } from "@raindrop-ai/ai-sdk";
registerTelemetry(raindrop());
const result = await generateText({
model: anthropic("claude-sonnet-4-5"),
prompt: "what is the weather in Tokyo?",
telemetry: { functionId: "weather-agent" }, // no `isEnabled` needed in v7
});raindrop() is self-contained — no @ai-sdk/otel or OpenTelemetry setup is
required. Pass context and subagentWrapping to customise behaviour, e.g.
raindrop({ context: { userId: "user_123" } }).
For short-lived scripts that exit before the background flush timer fires, keep the reference and drain it before exiting:
const rd = raindrop();
registerTelemetry(rd);
// ... run your generations ...
await rd.flush(); // or rd.shutdown()Sub-agents (a generateText/streamText invoked from inside a tool's
execute) nest automatically: the inner generation's spans are parented under
the tool span so parent and child spans stay correctly nested.
AI SDK v7+ native telemetry (advanced)
For finer control you can build the integration from a createRaindropAISDK
client instead. The native Telemetry callback interface (formerly
TelemetryIntegration before beta.111) avoids Proxy overhead and works with all
AI SDK entry points (including ToolLoopAgent).
// Option A: wrap() with nativeTelemetry flag
const { generateText } = raindrop.wrap(ai, {
context: { userId: "user_123" },
nativeTelemetry: true,
});
// Option B: direct registration (no wrap needed)
// - registerTelemetry on AI SDK v7 beta.111+
// - registerTelemetryIntegration on older v7 betas
import { registerTelemetry } from "ai";
registerTelemetry(
raindrop.createTelemetryIntegration({ userId: "user_123" })
);Standalone self-diagnostics tool
When registering the native telemetry integration directly, create the self-diagnostics tool from the same Raindrop client:
import { createRaindropAISDK } from "@raindrop-ai/ai-sdk";
import { generateText, registerTelemetry } from "ai";
const raindrop = createRaindropAISDK({
writeKey: process.env.RAINDROP_WRITE_KEY!,
});
registerTelemetry(raindrop.createTelemetryIntegration({ userId: "user_123" }));
const diagnostics = raindrop.createSelfDiagnosticsTool();
await generateText({
model,
prompt,
tools: {
[diagnostics.name]: diagnostics,
},
experimental_telemetry: { isEnabled: true },
});With native telemetry and tracing enabled, the tool automatically attaches its
signal to the active Raindrop event. When there is no active trace context,
provide eventId or getEventId:
const diagnostics = raindrop.createSelfDiagnosticsTool({
getEventId: () => currentEventId,
});Setting nativeTelemetry: true on pre-v7 throws a clear error. The Proxy path remains the default and supports features not yet available on the native path (buildEvent, output attachment extraction).
Per-call routing on AI SDK v7 beta.94+
eventMetadata() keeps working on every published v7 beta:
const result = await generateText({
model,
prompt,
experimental_telemetry: {
isEnabled: true,
metadata: eventMetadata({
userId: "u_42",
eventName: "chat-turn",
eventId: "evt_abc",
convoId: "conv_xyz",
}),
},
});AI SDK v7 beta.94 (vercel/ai #14503) removed the metadata field from TelemetryOptions, and integration callbacks no longer receive it via event.metadata. Going through wrap({ nativeTelemetry: true }) still routes per-call userId / eventName / eventId / convoId / properties correctly: the wrapper extracts them from the call's experimental_telemetry.metadata (or telemetry.metadata) before delegating, and exposes them to the integration via an AsyncLocalStorage slot.
If you are bypassing wrap() and registering an integration manually, use the helpers exported from this package (runWithRaindropCallMetadata, readRaindropCallMetadataFromArgs, getCurrentRaindropCallMetadata) to plumb per-call metadata through your own call sites.
If userId is missing from both wrap() context and eventMetadata(), the SDK logs a warning (once) and skips sending events.
Runtime support
Browsers and edge runtimes
Use the browser entrypoint:
import { createRaindropAISDK } from "@raindrop-ai/ai-sdk/browser";The SDK works without async_hooks shims. When AsyncLocalStorage is not available,
Raindrop falls back to synchronous context scoping: nested work in the same call stack
still inherits context, but automatic propagation across arbitrary async boundaries is
not guaranteed.
Node.js
Use the default import:
import { createRaindropAISDK } from "@raindrop-ai/ai-sdk";In Node, the default entrypoint wires up AsyncLocalStorage automatically.
Cloudflare Workers
Cloudflare Workers can provide AsyncLocalStorage via node:async_hooks when nodejs_compat is enabled (docs).
If nodejs_compat is enabled, use the Workers entrypoint:
import { createRaindropAISDK } from "@raindrop-ai/ai-sdk/workers";This enables real AsyncLocalStorage propagation in Workers.
If nodejs_compat is not enabled, use the browser entrypoint instead. The SDK still works,
but it uses the same synchronous fallback described above rather than real
AsyncLocalStorage.
Supported AI SDK Versions
This package is tested against multiple Vercel AI SDK versions:
| Version | Status | Integration |
|---------|--------|-------------|
| v4.x | ✅ Supported | Proxy |
| v5.x | ✅ Supported | Proxy |
| v6.x | ✅ Supported | Proxy |
| v7.x (beta/canary) | ✅ Supported | Proxy (default) or native Telemetry (opt-in, formerly TelemetryIntegration pre-beta.111). Verified against beta.116 and canary.171. |
Version Differences Handled
| Feature | v4 | v5 | v6 |
|---------|----|----|-----|
| finishReason | String ("stop") | String ("stop") | Object ({ unified: "stop" }) |
| usage tokens | promptTokens/completionTokens | inputTokens/outputTokens | inputTokens/outputTokens |
| Output.object().responseFormat | N/A | Plain object | Promise |
Testing
Tests are organized to verify compatibility across AI SDK versions:
packages/ai-sdk/
├── tests/
│ ├── v4/ # AI SDK v4 (pins ai@^4.1.17)
│ │ ├── ai-sdk.v4.test.ts
│ │ ├── wrapper.test.ts
│ │ └── http-payloads.test.ts
│ ├── v5/ # AI SDK v5 (pins ai@^5.0.0)
│ │ ├── ai-sdk.v5.test.ts
│ │ ├── wrapper.test.ts
│ │ └── http-payloads.test.ts
│ ├── v6/ # AI SDK v6 (pins ai@^6.0.0)
│ │ ├── ai-sdk.v6.test.ts
│ │ ├── wrapper.test.ts
│ │ └── http-payloads.test.ts
│ └── v7/ # AI SDK v7 beta (native telemetry + proxy)
│ ├── telemetry-integration.test.ts # Unit tests for all callbacks
│ ├── e2e-native-telemetry.test.ts # E2E with real AI SDK + MSW
│ ├── e2e-subagent-nesting.test.ts # Subagent span hierarchy
│ └── wrapper.test.tsRunning Tests
# Run all version tests (requires OPENAI_API_KEY and RAINDROP_WRITE_KEY in .env)
pnpm test
# Run specific version
pnpm test:v4
pnpm test:v5
pnpm test:v6
pnpm test:v7
# Quick smoke test (real LLM calls, single version)
pnpm smoke:minTest Coverage
Each version runs:
- Wrapper tests - API shape, wrapper creation, tools passthrough
- HTTP payload tests - MSW-based payload validation for each spec version
- Version-specific tests - API differences (finishReason format, usage naming)
v7 additionally runs:
- Telemetry integration tests - All callback lifecycles with mock shippers
- E2E native telemetry - Real AI SDK v7 with MSW-intercepted payloads
- Subagent nesting - Span hierarchy for nested generateText inside tool execution
Notes
- Spans include
ai.telemetry.metadata.raindrop.eventIdfor correlation, and omitai.telemetry.metadata.raindrop.userIdto prevent duplicate span→event creation server-side.
