Auto-Discovery
Add one flag to your start command and the LLM call sites Avanor can detect show up in your Inventory. No code changes, no instrumentation to write. If your app calls a model gateway, read what this rail cannot see first.
What it does
The auto-discovery rail registers itself before your application code loads, patches the vendor SDKs you already import (openai and @anthropic-ai/sdk), watches outbound network calls to known provider hosts, and reports each LLM call site it sees to Avanor. Those call sites populate your Inventory, the discover step of the discover, govern, prove loop.
This rail writes discovery reports and nothing else. Inventory fills directly. What it discovers also graduates into the Agents roster on a 30-minute reconcile schedule, so that page is not immediate and new rows arrive with governance switched off. The rail writes no justification records and no audit-log entries, so it does not fill Activity or the Audit Log, and it cannot enforce anything on its own.
The enforcement half comes from the explicit calls in the Quickstart, and they do not feed the same page each. track() and withSpan() graduate into the Agents roster on the same 30-minute schedule. allow() feeds Activity, but only once the agent is in the roster and you have switched governance on for it. Before it reaches the roster, allow() returns allow with reason no_policy_for_action; once it is in the roster but still ungoverned, the reason is governance_off. Neither of those two windows records anything on Activity, by design. attest()writes a hash-chained audit-log entry that is included in the "CSV (hash chain)" export up to that export's row cap, but the on-screen ledger list does not show that action type today, so verify it through that export rather than by watching the list.
What this rail cannot see
Both automaticdetection mechanisms need something to recognise: a patched vendor library, or a request to a provider host on Avanor's allowlist. A call routed through a model gateway (a model id like 'anthropic/claude-haiku-4.5') has neither, so neither of them reports it.
There is a third mechanism, and it is not automatic. If your own code already emits AI spans (for the Vercel AI SDK, that means experimental_telemetry: { isEnabled: true }), those spans land in Avanor's tracer provider and the span consumer picks the call up, gateway or not. So if you have that turned on, gateway calls are not invisible to this rail. If you have not, they are.
For the case where they are, install the framework adapter, which sits directly on the model call and already holds the provider namespace and the model. Today that is @avanor/sdk-vercel-ai. Its README carries the full coverage matrix, including the shapes it does not report either (streaming calls, and models passed as a provider instance rather than a string).
Check how your build loads that adapter before you rely on it. It loads its optional ai peer through a require() call, and there are real shapes where that call cannot succeed. Where it cannot, the adapter logs that ai is not installed and returns wrappers that throw when called, so your model call fails with a message telling you to install a package you already have. That is present in every published version of the adapterand is not fixed in any of them. What decides your outcome is which of the adapter's two built files your toolchain parses, not which module system your own app is written in. A fix exists on an unreleased development branch; what it repairs and what it leaves alone is at the end of this section.
The error strings named below are the internal cause and will not appear in your logs: the adapter catches and discards the underlying loader error. The only runtime message you will see is the "aipackage not installed" one. The "Critical dependency" warning is the exception and is visible, at build time.
- No bundler, loaded with
require(), which resolves the CommonJS file: works. Verified end to end against the realaipackage. - No bundler, loaded with
import, which resolves the ESM file: fails. Internal causeDynamic require of "ai" is not supported. - Bundled and the bundler parses the ESM file: works. Bundled and the bundler parses the CommonJS file: fails. Internal cause
Cannot find module 'ai', plus a "Critical dependency" build warning. Note that bundling inverts the unbundled answer. Measured on webpack across eight configurations, which were two files by two modes by twoexternalssettings. Across them the only variable that moved the outcome was which file webpack parsed, so the matrix establishes independence from the mode and fromexternals. Independence from your own app's module system and from the webpack version was observed separately, outside that matrix, and is weaker evidence. - Two further things break even the working bundled case. Emitting an ES module from your bundler (
output.module: true) fails, and moving the emitted bundle out of the directory tree that holdsnode_modules/aifails, because therequire('ai')resolves relative to the output bundle rather than to your source. Next.jsoutput: 'standalone'and its file tracing relocate output in exactly that way. - Turbopack has not been tested, in either direction.
The workaround available today is to keep the adapter out of the bundle entirely: add @avanor/sdk-vercel-aiitself to your bundler's externals, or consume the CommonJS entry. Adding ai to externals does nothing, because a dynamic require creates no bundler dependency for that entry to match.
What the unreleased fix changes. None of this is in a published package, so the list above is still what you have. The loader now 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 with target: 'node'.
- The two failures in the first three items above become works: unbundled
import, which resolves the ESM file, and a bundler parsing the CommonJS file. The two shapes that already worked still work. The fourth and fifth items are NOT repaired and are unchanged, and the bullets below restate them so that is not missed. - Optionality holds. With
aigenuinely absent, each of those four loading shapes still loads the module and throws only when you call the wrapper, which is what an optional peer has to do. - Two shapes are not repaired and still fail, both measured after the fix: emitting an ES module from your bundler (
output.module: true), and relocating the emitted bundle out of the directory tree that holdsnode_modules/ai, which is what Next.jsoutput: 'standalone'does. Externalizing the adapter remains the answer for both. - A bundler that parses the ESM file now emits a build warning,
module.createRequire failed parsing argument.The peer still loads; the warning says the bundler could not statically analyse the call. - Turbopack remains untested, before the fix and after.
Running both this rail and an adapter in one process is supported, but be aware that the two resolve feature identity differently, so one AI feature can produce two Inventory rows. Nothing merges them at write time. This applies just as much when the third mechanism above is what is seeing your gateway calls: turning on your framework's own telemetry and installing the adapter gives you two producers for the same feature. If you need exactly one row per feature today, run one of the two.
1. Install the SDK
One package. You do not import it in your code for auto-discovery; you load it with a Node flag (next step).
npm install @avanor/sdk2. Load it with one flag
Node evaluates the --import module to completion before it loads your app, so the vendor SDK patches are in place before your first import openai resolves. There is nothing to add to your source.
# Add one flag to however you already start your app.
node --import @avanor/sdk/auto your-app.jsIf you cannot change the start command, set it on the environment instead. Both forms are equivalent.
# Or, when you cannot edit the start command (Docker, PM2,
# a process manager), set it on the environment instead.
NODE_OPTIONS='--import @avanor/sdk/auto' node your-app.js3. Set your API key
AVANOR_API_KEY binds every discovered call site to your tenant, resolved server-side. Without it the rail loads but emits nothing and logs a warning at startup. Mint a key at /dashboard/settings/api-keys.
# Required: binds discovered call sites to your tenant, server-side.
export AVANOR_API_KEY=ak_live_...
# Optional: a human-readable environment label on every record.
export AVANOR_ENVIRONMENT=production # defaults to NODE_ENV, then 'development'
# Optional: name the service the call sites belong to.
export AVANOR_SERVICE=broker-portal4. See your AI footprint
Start your app and exercise a code path that calls an LLM. Within a few seconds your Inventory lists each discovered call site: the provider, the model, and where in your code it runs. From there you can author a policy to govern it.
Privacy defaults
Auto-discovery reports the shape of your AI usage, not its contents. Prompt and completion text is never captured unless you explicitly opt in with both AVANOR_CAPTURE_CONTENT and OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. The defaults are safe for production.
AVANOR_DRY_RUN: emit only the periodic self-report audit, no per-call detail. Useful for a first dry run in a sensitive environment.AVANOR_SDK_DISABLE: force the rail to no-op at process start, even when the flag is loaded.
Requirements
Node 18.19 or newer (18.19+, 20.6+, or 22+) for the ESM loader hook. On older Node the rail falls back to a network-level catch-net, so discovery still works; the ESM patch path is best-effort on top of it. CommonJS apps are supported through the require fallback.
Next
- Quickstart: the explicit
allow()andattest()path for enforcing policy and writing audit-grade evidence. - API Reference: every public type and method.