npm package: @raindrop-ai/ai-sdk
Description: Standalone Vercel AI SDK integration for Raindrop (events + OTLP/HTTP JSON traces, no OTEL runtime)
Latest version: 0.0.25

# `@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

```bash
pnpm add @raindrop-ai/ai-sdk
```

## Usage

```ts
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();
```

## Manual Traces

Create trace spans manually alongside, or instead of, auto-instrumented ones.

Use `createSpan` when the timing is already known:

```ts
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:

```ts
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:

```ts
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+ native telemetry (opt-in)

On AI SDK v7+, you can use the native `TelemetryIntegration` callback interface instead of Proxy wrapping. This avoids Proxy overhead and works with all AI SDK entry points (including `ToolLoopAgent`).

```ts
// Option A: wrap() with nativeTelemetry flag
const { generateText } = raindrop.wrap(ai, {
  context: { userId: "user_123" },
  nativeTelemetry: true,
});

// Option B: direct registration (no wrap needed)
import { registerTelemetryIntegration } from "ai";

registerTelemetryIntegration(
  raindrop.createTelemetryIntegration({ userId: "user_123" })
);
```

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

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:

```ts
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:

```ts
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](https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/)).

If `nodejs_compat` is enabled, use the Workers entrypoint:

```ts
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) | ✅ Supported | Proxy (default) or native `TelemetryIntegration` (opt-in) |

### 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

```bash
# 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.