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 + latencyNothing 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 aiThe 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,
): LanguageModelV1WrapModelOptions
| Field | Type | Default | Notes |
|---|---|---|---|
model | string | model.modelId | Override the model label stored in traces. |
provider | string | model.provider | Override 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 idOutside 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
(promptTokens → inputTokens, completionTokens → outputTokens). 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 completesThe 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.