npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@raindrop-ai/ai-sdk

v0.3.1

Published

Standalone Vercel AI SDK integration for Raindrop (events + OTLP/HTTP JSON traces, no OTEL runtime)

Readme

@raindrop-ai/ai-sdk

Standalone Vercel AI SDK integration for Raindrop:

  • Events: sends a track_partial payload to POST /v1/events/track_partial when the model finishes
  • Standalone traces: ships spans directly to POST /v1/traces as 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 TelemetryIntegration interface (no Proxy wrapping)

Install

pnpm add @raindrop-ai/ai-sdk

Usage

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.

Detached sub-agents

A detached sub-agent is different: it runs in another process (a queue worker, a container, another machine), does not block its caller, and reports as its own Raindrop event with its own lifecycle. Neither the trace nor the event id survives that boundary, so the link is carried explicitly — the caller stamps it on the span it dispatched from, the child stamps the reverse reference on every span it emits, and a carrier travels with the job.

Caller — allocate the child's event id and get the headers to send:

const rd = raindrop({ context: { userId: "user_123" } });
registerTelemetry(rd);

const launch_subagent = tool({
  description: "Hand a task to a background sub-agent",
  inputSchema: z.object({ task: z.string() }),
  execute: async ({ task }) => {
    const dispatch = rd.subagent({ name: "researcher", input: { task } });
    await queue.send({ task, headers: dispatch.headers });
    // Return a handle, not an answer — the answer is the child's to report.
    return { jobId: dispatch.childEventId, status: "accepted" };
  },
});

The dispatch span is your tool's span, so it carries your tool's name — the example above emits launch_subagent because that is what the tool is called here, not because the SDK imposes it. launch_subagent is only the fallback name, used when subagent() has to synthesize a dispatch span because no tool call was open; pass toolName to choose your own. The sub-agent's own name travels on raindrop.handoff.name either way, which is where readers group by it.

subagent() allocates childEventId before dispatching, so the caller's hand-off resolves while the job is still queued. It also opts the turn in to producing an event: a turn whose entire content is launches finishes with finishReason: "tool-calls" and no assistant text, and such turns are dropped as intermediate steps by default. If you launch from outside a turn this SDK instruments (the v4-v6 wrap() path), pass the opt-in yourself with eventMetadata({ toolEvents: "allow" }).

The caller never writes the child's status. It returned a job handle and moved on; status is derived from the child's own event.

Child — resume from the carrier and report under the id the caller allocated:

const run = rd.subagents.resume(dispatch.headers);

try {
  await generateText({
    model: anthropic("claude-sonnet-4-5"),
    prompt: job.task,
    experimental_telemetry: { isEnabled: true, metadata: eventMetadata(run.metadata) },
  });
} catch (err) {
  // Only needed for a failure OUTSIDE a generation — one that throws already
  // reports itself.
  await run.fail(err);
}

run.metadata carries the child's event id, identity, and the reverse reference, which the integration stamps on every span the run emits. A cancellation is the one outcome telemetry cannot show on its own (spans close, nothing errors, there is just no answer), so state it with run.cancel(reason); a generation cancelled via its AbortSignal is marked automatically.

A child that ends without reporting anything produces no event at all, which leaves its caller unable to tell a failed job from one that never started. The SDK therefore records an abort reason as the run's output whenever a detached child errors, aborts, or is cancelled.

Output alone is not enough for a failure, though. Status is derived from the child's telemetry — a run with final output and no errored span reads as finished — so run.fail() also marks the child's own spans as errored, and records one when the job died before it reached the model and left nothing to mark. run.cancel() deliberately does not: cancelled and failed are different states, and error spans are what separate them.

resume(headers) returns a usable run even when there is no carrier — a child invoked directly rather than dispatched is a legitimate state, not an error — so call sites stay unconditional.

The carrier wins over anything local. resume(headers, { convoId }) treats its options as fallbacks for the no-carrier case, not overrides, because every field the carrier supplies names something the caller already recorded: a child that keeps its own convo id lands in a different conversation from the supervisor that launched it, and one that keeps its own event id leaves the caller's reference pointing at nothing.

Trust boundary. A carrier names the event a child will be attributed to, so accepting one from an untrusted caller lets that caller write into another tenant's trace. Only read carriers that came from inside your own trust boundary.

fromHeaders also accepts LangSmith's langsmith-trace + langsmith-metadata headers, so a service already propagating those links with no call-site change.

Send every header dispatch.headers gives you, not just traceparent. There are three: the standard traceparent and baggage pair, which other readers understand, and x-raindrop-handoff, which carries the same fields under a name no propagator owns. The standard pair is not ours to rely on — an instrumented HTTP client rewrites traceparent from its own client span, and OTel's baggage propagator replaces baggage with the caller's own entries, so a caller propagating something ordinary like a tenant id would otherwise strip every hand-off identity and leave the child unlinked. The Raindrop header is read last and wins; the standard pair remains the fallback for a carrier written by a different sender. A carrier header that arrives repeated with differing values is refused entirely, leaving an honestly unlinked child rather than a guess about which value gets to name the event this process is attributed to.

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.ts

Running 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:min

Test 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.eventId for correlation, and omit ai.telemetry.metadata.raindrop.userId to prevent duplicate span→event creation server-side.