@avanor/sdk-vercel-ai
v0.1.3
Published
Avanor SDK — Vercel AI SDK auto-instrumentation
Maintainers
Readme
@avanor/sdk-vercel-ai
Avanor SDK. Vercel AI SDK auto-instrumentation.
Wraps Vercel AI SDK's generateText, streamText, and generateObject with
llm.call spans carrying OTel GenAI semconv attributes, and reports AI
discovery for the call shapes where it can do so honestly.
Install
npm install @avanor/sdk @avanor/sdk-vercel-aiAdd ai to your dependencies if it isn't already.
Quickstart
import { Avanor } from '@avanor/sdk';
import { instrumentVercelAI } from '@avanor/sdk-vercel-ai';
Avanor.init({
apiKey: process.env.AVANOR_API_KEY!,
environment: process.env.NODE_ENV ?? 'dev',
service: 'broker-portal',
});
// Drop-in replacement for the bare `ai` functions:
const { generateText, streamText, generateObject } = instrumentVercelAI();
const result = await generateText({
model: 'anthropic/claude-haiku-4.5', // a gateway model id
prompt: 'Hi',
});What this adapter fills
Two different mechanisms, feeding different pages on different timetables. One does not imply the other.
| Mechanism | Which calls | Where it lands |
|---|---|---|
| the llm.call span this adapter opens around every wrapped call | all three of generateText, streamText, generateObject, in every model shape | an agent event, which graduates into the Agents roster on a 30-minute reconcile schedule, with governance off |
| a discovery report this adapter sends after the call completes | generateText and generateObject only, and only when model is a gateway-form string | Inventory, directly, and the same Agents roster on the same schedule |
So a streamText-only app will show up on Agents and stay absent from
Inventory. That is the code behaving as designed, not a broken install.
Neither mechanism fills Activity or the Audit Log. Those come from your
own allow() / guard() and attest() calls, which this adapter does not make
for you. Activity additionally needs the agent switched to governed before
allow() records anything. See
@avanor/sdk's README for the full
path per page.
Which model shape reports discovery
| model you pass | Discovery reported? |
|---|---|
| 'anthropic/claude-haiku-4.5' (gateway string) | Yes. The namespace before the slash is a value your own source code declared, so reporting it invents nothing. |
| 'gpt-4o' (bare string) | No. There is no namespace in the input, so naming a provider would mean the adapter supplying a fact from nowhere. |
| openai('gpt-4o') (provider instance) | No. The object carries the provider package's own model id, not a caller-declared namespace. |
If you pass provider instances, this adapter alone will not fill Inventory for
them. They are covered only if you also install the auto rail
(node --import @avanor/sdk/auto), whose network host matcher recognises
api.openai.com alongside eleven other provider host rules, several of them
wildcards covering more than one host. Installing this adapter does
not install that rail, and vice versa. See the coverage matrix in
@avanor/sdk's README for which
hosts are on the list and what each producer does and does not see.
What the reported provider is, and is not. It is the namespace your model
id declared. It is not a guaranteed identification of the vendor that served the
inference: 'meta/llama-3.3-70b' reports meta, the creator of the weights,
while the gateway may route that call to Bedrock, Groq, or anywhere else. The
serving provider is only reachable through an async gateway lookup that cannot
be made from inside your synchronous model call. Treat the field as "which model
family this feature asked for", not as "which vendor we have a data-processing
relationship with".
Why this exists
A customer calling a model gateway matches neither of the auto rail's automatic mechanisms: no vendor library is patched, and no allowlisted provider host is contacted. Their Inventory stays empty while their Agents page shows live governed agents. This adapter sits directly on the model call and already holds the provider namespace, the model, and the token counts, so it can report them.
One thing that is not automatic but does see gateway calls: if you have turned on
the AI SDK's own experimental_telemetry and you run the auto rail, those spans
land in Avanor's tracer provider and its span consumer picks the call up. That is
an opt-in you make in your own code. If it applies to you, read the next section
before installing this adapter as well.
Do not assume the rails deduplicate
If you run both node --import @avanor/sdk/auto and this adapter in the same
process, one AI feature can produce two Inventory rows. The two rails resolve
feature identity differently (different call-site precedence, this adapter sends
an empty route where the auto rail sends the real one, and the auto rail applies
a dev-mode call-site bypass this adapter does not). The rows stay
distinguishable on the wire via avanor.discovery.source (explicit here,
a detection-mechanism value there), but nothing merges them at write time. If you
need exactly one row per feature today, run one rail.
Multi-tenant: pass the client explicitly
instrumentVercelAI() with no argument resolves its client from the module-level
singleton that only Avanor.init() populates. If your process builds clients with
Avanor.createClient() per tenant instead, that singleton is never populated, and
the adapter silently returns the raw, uninstrumented ai functions: no spans, no
discovery, no error, in every tenant.
Pass the client:
const tenantClient = Avanor.createClient({ apiKey, environment, service });
const { generateText } = instrumentVercelAI({ client: tenantClient });There is no request-scoped mechanism today, so the client has to travel from where you construct it to where you make the model call, through your own plumbing. Build the wrapper at the narrowest scope that is guaranteed to be single-tenant, not at module scope. Do not work around this by registering one tenant's client as a process-wide default: that attributes every call to whichever tenant loaded first.
Attributes captured per call
| Attribute | Value |
|---|---|
| gen_ai.provider.name | 'vercel-ai' |
| gen_ai.operation.name | 'generateText', 'streamText', 'generateObject' |
| gen_ai.request.model | The model spec (model id when extractable) |
| gen_ai.usage.input_tokens | From the result's usage (non-stream paths) |
| gen_ai.usage.output_tokens | From the result's usage (non-stream paths) |
gen_ai.provider.name names the integration, not the model vendor, and it is
a shipped field on rows you already have. The discovery report carries the model
id's namespace separately. Both are correct; they answer different questions.
Streaming limitations (v0)
For streamText, the span closes when the wrapped function returns the
stream handle, not when the stream completes. The wrapped result is still the
exact Vercel AI SDK return value, so you can await result.text,
await result.usage, etc., as normal.
Token-usage attributes are not captured for streamText in v0. The span
carries provider/operation/model attributes only. Full streaming-completion
capture (token usage, finish reason, and so on) lands in v0.5.
streamText also reports no discovery. There is no completion hook, so the
only row it could emit would carry no token counts and an end time for a call
that had not ended. A fabricated row is worse than a missing one in a compliance
surface, so nothing is emitted until the v0.5 completion hook exists.
Other known gaps
Two build shapes still cannot load the optional ai peer, and where they cannot your model call throws
The most severe item on this page, and the only one that makes your own code fail
rather than merely reporting less. 0.1.3 repaired the loader for every shape it
can reach. Two shapes it cannot reach are below. Both were measured after the
fix, and both fail on every version of this package, including the one you are
reading this from.
- Emitting an ES module from your bundler (
output.module: truewithexperiments.outputModule), on an otherwise-working config. Internal cause:Dynamic require of "ai" is not supported. Both loader branches need arequireyour bundler has removed, so no branch is left that can succeed. - Relocating the emitted bundle outside the directory tree that holds
node_modules/ai. The same artifact that worked before the move fails after it. Internal cause:Cannot find module 'ai', because the load resolves relative to the OUTPUT bundle's location rather than to your source. Next.jsoutput: 'standalone'and its file tracing relocate output in exactly that way.
In both, instrumentVercelAI() logs that ai is not installed and returns
wrappers that throw on call:
@avanor/sdk-vercel-ai: `ai` package not installed. Run `npm install ai`
(or `pnpm add ai`) to use instrumentVercelAI().That message is wrong when you already have ai installed, and it sends the
debugging in the wrong direction. The real cause is the load path, not a missing
package.
Those two lines are the only thing you will see. The underlying loader error
is caught and discarded by loadAi(), which latches a flag and returns
undefined, so the internal causes named anywhere on this page
(Dynamic require of "ai" is not supported, Cannot find module 'ai') will not
appear in your logs. Verified: a consumer run against the built dist with ai
genuinely installed emitted neither string. Do not grep for them to decide
whether this applies to you. Grep for `ai` package not installed.
What you can do. Keep this package out of the bundle: add
@avanor/sdk-vercel-ai itself to your bundler's externals, or consume the
CommonJS entry. Neither shape above is something this package can observe or
resolve from inside a bundle, so externalizing it is the answer for both. Adding
ai to externals does nothing, because a dynamic require creates no
bundler dependency for that entry to match. Next.js externalizes node_modules
packages in its server build by default, which would put you in the externalized
case, but that is read off Next.js's documented default and has not been
confirmed against a real app.
Turbopack has been tested by nobody, in either direction, before 0.1.3 or
after.
0.1.3 also introduces one new build-time warning. A bundler that parses
dist/index.mjs now emits module.createRequire failed parsing argument. where
it previously emitted nothing. The peer still loads. The warning is the bundler
reporting that it could not statically analyse the call.
What 0.1.3 repaired, and what <= 0.1.2 still does
These tables are kept rather than deleted because they are not history for
everyone: if you are pinned to 0.1.2 or earlier, they describe the package
you have. Every "on <= 0.1.2" column below is a measurement of 0.1.0,
0.1.1 and 0.1.2, all of which carry the identical pre-fix loader. The gap has
been present since a2f8ef6d (2026-05-20); 0.1.3 is the first published
version that closes it.
On <= 0.1.2 this package loaded its optional ai peer through a bare
require() call that whole classes of ordinary install could not run. It was not
"ESM is broken": some ESM shapes worked and some CommonJS shapes did not. What
decided the outcome was which of this package's two built files your toolchain
parsed, dist/index.mjs or dist/index.cjs, and not which module system your
own app was written in.
Unbundled, plain Node:
| how you load the adapter | on <= 0.1.2 | on 0.1.3 |
|---|---|---|
| require('@avanor/sdk-vercel-ai'), which resolves dist/index.cjs | works. Verified end to end against the real ai package. | works |
| import '@avanor/sdk-vercel-ai', which resolves dist/index.mjs | fails. Internal cause: Dynamic require of "ai" is not supported. | works |
Bundled, measured on webpack 5 with target: 'node' across eight configurations:
| what the bundler parses | on <= 0.1.2 | on 0.1.3 |
|---|---|---|
| dist/index.mjs | works. | works |
| dist/index.cjs | fails. Internal cause: Cannot find module 'ai'. Unlike the runtime error, the accompanying Critical dependency: require function is used in a way in which dependencies cannot be statically extracted warning IS visible, at BUILD time. | works, and that Critical dependency warning is still emitted |
Note that on <= 0.1.2 bundling inverts the unbundled answer: unbundled it
is the CommonJS file that works, bundled it is the ESM file. Those eight
configurations were 2 files x 2 modes x 2 externals settings, and across them
the only variable that moved the outcome was which of the two files webpack
parsed: not the mode, and not whether ai was listed in externals. Two further
independences were observed separately rather than inside that matrix, so treat
them as weaker evidence: your own app's module system, and the webpack version.
0.1.3's loader tries the ambient require first and falls back to
createRequire when that throws, which covers both built files instead of one
each. Measured across twelve configurations against the real ai package, both
with the peer installed and with it absent: four in plain Node with no bundler
involved, and eight across four webpack configurations (webpack 5.109.2,
target: 'node'). Optionality is preserved. With ai genuinely absent,
every one of those shapes still behaves the way an optional peer must: the module
loads, and only calling the wrapper throws. The load failure stays caught, not
fatal at import.
What 0.1.3 does not repair is the two shapes at the top of this section.
They fail on <= 0.1.2 and on 0.1.3 alike.
Smaller gaps
- A model call that throws reports no discovery. The report is emitted only after a successful call, so a feature whose calls always fail never appears in Inventory.
- Object-form models report no discovery. The model object does expose a
providerfield, but its values are sub-provider-qualified ('openai.chat','anthropic.messages') and using them raw would fragment one vendor across several Inventory rows. Wiring it needs a canonicalisation decision first.
Zero false-positive guarantee
Scoped, deliberately, to the case where this package successfully loaded ai.
Where it did, the guarantee is unconditional: each wrapped function returns the
exact same value as the underlying ai function. No wrapping, no transformation,
no extra fields. The discovery report is fire-and-forget and fully isolated, so
nothing in it, including a customer-supplied client whose reportDiscovery
throws, can change what your generateText returns or throws. That isolation is
a property of the discovery report specifically, which sits behind its own
try/catch. It is not claimed for the span path: startActiveSpan,
setAttribute, setStatus and the end() in the finally are not individually
wrapped, so a throwing tracer would propagate.
Where it did NOT load ai, there is no wrapped ai function to return a value
from. instrumentVercelAI() returns wrappers that throw on call, so your model
call fails rather than returning anything. That is the peer-load gap documented
above, not a property of the wrapping, and it is the only way in which installing
this package changes the outcome of your own code. On 0.1.3 two build shapes
reach it; on <= 0.1.2 many more do. Establish which case you are in before
treating the guarantee as covering you.
Docs
There is no Vercel AI recipe page yet. This adapter is documented on the auto-discovery page, which covers which surfaces each install fills and which it does not: https://www.avanor.ai/docs/sdk/auto-discovery
The exported types for the whole SDK family are at https://www.avanor.ai/docs/sdk/api-reference, and the core install is at https://www.avanor.ai/docs/sdk/quickstart.
License
Avanor Source License v1 (BSL-modeled, 4-year Apache 2.0 sunset). See LICENSE.
