keelwave
SDKsTypeScript

Vercel AI adapter

Wrap a Vercel AI SDK model so every generateText and streamText call emits a keelwave trace.

wrapModel wraps a Vercel AI SDK LanguageModelV1 so every call it makes records a model trace — model, provider, token counts, latency, status — and links it to the active run when there is one.

import { openai } from '@ai-sdk/openai'
import { generateText } from 'ai'
import { Keelwave } from 'keelwave'
import { wrapModel } from 'keelwave/vercel-ai'

const client = new Keelwave({
  apiKey: process.env.KEELWAVE_API_KEY ?? '',
  endpoint: process.env.KEELWAVE_ENDPOINT ?? 'http://localhost:8080',
})

const model = wrapModel(client, openai('gpt-4o'))

const { text } = await generateText({ model, prompt: 'hello' })
// keelwave receives a model trace with tokens + latency

Nothing else changes: wrapModel returns a LanguageModelV1, so it drops into generateText, streamText, and anywhere else the AI SDK takes a model.

Install

The adapter lives at the keelwave/vercel-ai subpath so that ai stays an optional peer dependency — core tracing never imports it. Install ai only if you use this adapter (you already have it if you use the Vercel AI SDK).

npm install keelwave ai

The declared peer range is ai@^4.3.19, which is the LanguageModelV1 generation of the AI SDK.

Import the adapter from keelwave/vercel-ai, not keelwave. The root entry point deliberately does not re-export it.

wrapModel()

wrapModel(
  keelwave: Keelwave,
  model: LanguageModelV1,
  opts?: WrapModelOptions,
): LanguageModelV1

WrapModelOptions

FieldTypeDefaultNotes
modelstringmodel.modelIdOverride the model label stored in traces.
providerstringmodel.providerOverride the provider label.
const model = wrapModel(client, openai('gpt-4o'), {
  model: 'gpt-4o',
  provider: 'openai',
})

Linking traces to a run

When the call happens inside a keelwave run — opened by client.run() or client.agent() — the adapter reads the ambient run and sets agentRunId on the trace. No extra wiring:

import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { Keelwave } from 'keelwave'
import { wrapModel } from 'keelwave/vercel-ai'

const client = new Keelwave({ apiKey: process.env.KEELWAVE_API_KEY ?? '' })
const model = wrapModel(client, openai('gpt-4o'))

const summarise = client.agent({ name: 'summariser' })(async (doc: string) => {
  const { text } = await generateText({
    model,
    prompt: `Summarise:\n\n${doc}`,
  })
  return text
})

await summarise('…long document…')
// the model trace carries agent_run_id = this run's id

Outside a run the trace is still recorded, just without a run link.

What is captured

Both call paths are instrumented via AI SDK middleware.

Non-streaming (wrapGenerate, used by generateText): after the call settles the adapter emits one trace with model, provider, status, latencyMs, and token counts read from the result's usage (promptTokensinputTokens, completionTokensoutputTokens). If the call throws, status is 'error', errorMessage carries the message, and the original error is rethrown.

Streaming (wrapStream, used by streamText): chunks pass through untouched. The adapter reads usage from the finish chunk and marks status as 'error' if an error chunk appears; the trace is emitted when the stream flushes. If no finish chunk arrives, no trace is emitted.

import { streamText } from 'ai'

const result = streamText({ model, prompt: 'write a haiku' })
for await (const chunk of result.textStream) {
  process.stdout.write(chunk)
}
// trace emitted once the stream completes

The adapter's emit is fire-and-forget and swallows its own failures, so a keelwave outage can never slow down or break a model call. This bypasses raiseOnError — adapter emits never throw.

Token and cost fields

The adapter sets inputTokens and outputTokens from the provider's usage data. It does not compute costUsd — if you want cost on these traces, call client.ingestAi() yourself with a costUsd value, or attach cost to run steps via StepOptions.

On this page