Keeps the OpenTelemetry traces of an AI app in Requal.
npx -y @requal/cli@1.1.0 initOne Allow in the browser and one use of the app: it prints DONE <trace link>.
Or give a coding agent (Claude Code, Codex, Cursor) this prompt:
Set up Requal tracing in this repository.
Run: npx -y @requal/cli@1.1.0 init --agent
Without Node, run: uvx requal@1.1.0 init --agent
It needs network access. Do what each "next" line says. When it prints a link for me, ask me to open it, and wait.
Stop when it prints DONE with a trace link, and show me the link.
Change only what it asks for, and show me each diff first. Never print, log or commit the ingest key.
requal init [--agent] [--dir PATH] [--api URL] [--timeout SECONDS] [--no-browser] [--no-start] [--proxy | --claude-code]
requal wait [--agent] [--timeout SECONDS]
requal disconnect [--agent]
requal --version | requal version | requal help
| Flag | |
|---|---|
--agent |
JSON lines on stdout. Never installs or starts anything |
--dir |
The app, inside this folder. Without it: this folder if it has an AI library or is a Next.js app, else the only app with an AI library two levels down or in a workspace the repository declares |
--api |
REQUAL_API_URL, else https://app.requal.ai. https only; http only on this machine |
--timeout |
How long each wait lasts, 1 to 600 seconds, with up to 30 seconds more for a request the server has not answered. Default 600; under --agent, init 60 and wait 90 |
--no-browser |
Prints the link and opens nothing |
--no-start |
Leaves starting the app to you |
--proxy |
A LiteLLM proxy, for an app in any language: needs no manifest. It sees every model call, and the app's calls stop if it is down |
--claude-code |
Claude Code itself, started with claude --settings <path> on every start: prompts, model names, tokens, tool names, a session id and a random user id, listed under "What stays out of a trace". Needs no manifest |
Exit codes: 0 done, or handed on with a next; 2 stopped; 1 an unexpected error.
-
Detects the app and plans. Anything that stops it stops here, before any network call, except what can only be known after Allow: git's last word on the env file, or a file that appeared in the meantime.
-
Asks Requal for a link. It sends the CLI's version, the stack, the repository's name and the plan's paths and packages, never code or file contents. The code
XXXX-XXXXon the page matches the one in the terminal. -
After Allow, it gets the ingest key and writes: a line in the
.gitignorebeside the env file first, so git ignores it before the key is there (.requal/ignores itself), then the env file, one new file, at most one change to your code, and the AI SDK 5 or 6 change. A second change it needs, or one to a file it cannot write, is handed to you or your agent, and so is moving a route off the Edge runtime: traces are sent from the Node.js runtime only. In .NET, Go, Ruby and Rust it writes the new file fromtemplates/, and hands on only the packages and the lines to add; a file it cannot place safely is handed on whole. Spring AI's class is handed on whole, since the class is the app's to keep. Mastra's exporter, which goes in your own code, is handed on the same way. -
Installs with your package manager, starts the dev script, and waits for the first trace with a model call. If a change was left for you, it stops before starting the app: make the change, start the app and run
wait. Under pnpm this install takes--ignore-scripts: pnpm 12 stops on any package's build it has not approved, and Requal's packages need none.With nothing installed, two things ask for an install before anything else, before any plan or link: AI SDK 5 or 6, whose change needs your own
typescript, and an AI SDK no file gives a version for (latest, a tag,workspace:*), whose major decides the plan. That install runs nothing of the repository's or of a package's before you have seen the plan:npm install --ignore-scripts,pnpm install --ignore-scripts,yarn install --ignore-scripts, on Yarn 2 and lateryarn install --mode=skip-build,bun install --ignore-scripts. Run on npm, pnpm, Bun and Yarn 1 and 4: the change is made from that install, andesbuild,sharpand Next.js's SWC work without their scripts. A package that does need its script, such assqlite3for its binding, or your ownpostinstall, then fails at your app's start. To run them:npm rebuild;yarn rebuild; on Yarn 1yarn install --force; a package's withpnpm approve-builds <package>, thenpnpm rebuild, orbun pm trust <package>(one Bun already trusts, assqlite3: deletenode_modulesand runbun install); your own withpnpm run postinstallorbun run postinstall. -
Prints
DONE <trace link>and keeps the app in the foreground until Ctrl-C.
Under --agent the first run ends at the link. Run it again after Allow; it writes what it can, always including the env file, so the agent never sees the key. Then it hands on the install, any change it could not place, starting the app, using it once, and wait --agent. A link that ran out, or a plan that changed since, is replaced with a new one.
Polls for the first trace with a model call: DONE <trace link>. The status token lasts a day.
Removes the env block or .env.requal, the one change if it is still what init wrote, the new file if init placed it, it is unchanged and nothing still loads it (a line you added from a hand-off counts, and in .NET, Go, Ruby and Rust a line in your source that names it), what init put in .requal/ whatever it holds, and .requal/. A file of that folder that git tracks stays. Leaves the AI SDK change, the packages, the line in .gitignore and a change you made from a hand-on, and names them, with what each stack needs from you: unset in a shell that loaded the Java agent's variables, your app's base URL after the proxy, the flag after Claude Code. Calls no server: revoke the key on the AI system's page.
It reads the manifests in this folder, two levels down, and wherever a workspace declaration reaches (npm's and yarn's workspaces, pnpm-workspace.yaml, a uv workspace, Poetry's path dependencies, Cargo's [workspace], go.work, Maven's modules, Gradle's settings, a .sln): package.json, pyproject.toml, requirements*.txt, go.mod, pom.xml, build.gradle and build.gradle.kts (with a version catalog), *.csproj and *.fsproj (with Directory.Packages.props), Gemfile, Cargo.toml, deno.json. One app with an AI library is chosen by itself; several list their --dir commands. A package beside an app that starts, and an example beside an app that is not one, are not choices.
It only reads, runs nothing of the repository's, and sends nothing. It skips what is built, vendored, hidden or a test's, follows no link while looking, stops after 5,000 folders, and reads only plain files, never a device or a pipe behind a link. Outside JavaScript and Python, a manifest must also be UTF-8, at most 1 MiB, and inside this folder once links are followed. The libraries it looks for, in each language, are in recipes.json.
| Stack | Found by | Env file | New file | One change | Installs |
|---|---|---|---|---|---|
| Next.js, AI SDK 7 | next 13.4+, ai 7 |
block in .env.local |
instrumentation.ts, or instrumentation.requal.ts beside yours |
With yours, the first line of register(); below Next.js 15, instrumentationHook |
@ai-sdk/otel in step with your ai, OpenTelemetry's SDK and OTLP exporter |
| Next.js, AI SDK 5 or 6 | next, ai 5 or 6 |
the same | the same | With yours, the last line of register(), handed to you when a path can return first; below Next.js 15, instrumentationHook; plus experimental_telemetry: { isEnabled: true } on each generateText, streamText, generateObject and streamObject call and each new ToolLoopAgent or new Experimental_Agent, with your own typescript |
OpenTelemetry's SDK and OTLP exporter |
| Next.js, another library | next, a library of the Node row below, alone or beside ai when your code imports it |
the same | the same, with its instrumentation | The library in serverExternalPackages (next.config.*), LangChain as @langchain/core |
The same, and its instrumentation |
| Next.js, Mastra | next, @mastra/core |
the same | None | @mastra/* in serverExternalPackages. Its exporter is handed on: see Mastra below |
The two Mastra packages |
| Node | package.json, with ai 5+, openai 5+, @anthropic-ai/sdk, @aws-sdk/client-bedrock-runtime, LangChain (@langchain/core 0.3+, by that name, langchain or any @langchain/*), @openai/agents 0.3.8+, or llamaindex 0.12+ with @llamaindex/openai |
.env.requal |
requal-otel.mjs |
--import ./requal-otel.mjs on node or tsx in the dev script, after any preload flag of the app's own. With nodemon, ts-node, nest or another launcher, the same through NODE_OPTIONS, handed to you. A start script is handed to you: node stops at boot if an image lacks that file. Beside a front that runs its own way (Vite, webpack), or under npm run of another script, the script that starts Node.js is handed to you. On ai 5 or 6, also experimental_telemetry on each call, as in Next.js |
The same; @ai-sdk/otel on ai 7; OpenTelemetry's context manager on ai 5 or 6 |
| Node, Mastra | package.json, @mastra/core 1.16+ |
block in .env |
None | None: its exporter goes in your new Mastra(), and is handed on |
@mastra/otel-exporter, @mastra/observability (under pnpm, with --ignore-scripts) |
| Bun | A dev or start script that runs a file with bun: bun src/app.ts, bun run src/app.ts, bun --watch src/app.ts |
.env.requal |
requal-otel.mjs |
--preload ./requal-otel.mjs right after bun, past any preload of the app's own, in the dev script. A start script is handed to you |
As Node. With bun as the manager only, and node in the script, it is the Node row |
| Deno | deno.json or deno.jsonc with no package.json, and an npm: import of a library above, under the package's own name: "openai": "npm:openai@7" |
.env.requal |
requal-otel.mjs |
None: --preload ./requal-otel.mjs on deno run in your task is handed to you, with the permissions the file needs if the task does not give --allow-all |
deno add --save-exact npm:.... On ai 5 or 6 the calls that need experimental_telemetry are handed to you: there is no TypeScript to load |
| Python | pyproject.toml or requirements*.txt, naming a library in "Python libraries" below |
.env.requal |
requal_otel.py |
import requal_otel, in a try so the app starts without it, first in the module your server loads |
opentelemetry-sdk, opentelemetry-exporter-otlp-proto-http, OpenInference's instrumentor for each library that has one; with pip, also in requirements.txt, or handed to you for a pyproject.toml, which pip does not write to |
| Java, Spring AI | pom.xml or build.gradle, org.springframework.ai:* |
.env.requal, which Spring Boot reads by a setting |
None: RequalTracing.java is handed on whole, in your package, beside the packages and three settings |
Handed on | Nothing: no jar and no OTEL_* |
| Java, other | LangChain4j, OpenAI Java, Amazon Bedrock | .env.requal, which you load when you start the JVM, also with JAVA_TOOL_OPTIONS, added to any you have, and OTEL_JAVAAGENT_CONFIGURATION_FILE |
.requal/otel.properties, mode 0600, holding the key, which only the agent reads |
LangChain4j: RequalChatModelListener.java and .listeners(...) on each model builder, handed on. The others: none |
After Allow, the OpenTelemetry Java agent to .requal/, its SHA-256 checked before anything names it |
| .NET | *.csproj, Semantic Kernel, Microsoft.Extensions.AI or OpenAI |
.env.requal |
RequalOtel.cs beside the project; with raw OpenAI, the larger one with a handler |
RequalOtel.Start(); first, and by library UseOpenTelemetry(...) on the builder or Transport = RequalOtel.Transport() on the client's options, handed on |
Nothing: OpenTelemetry's two packages are handed on |
| Go | go.mod, openai-go, go-openai, langchaingo or anthropic-sdk-go |
.env.requal |
requalotel/requalotel.go beside go.mod |
The import, defer requalotel.Shutdown() and requalotel.HTTPClient() on the client, handed on |
Nothing: OpenTelemetry's packages are handed on, by your go line, 1.22 up |
| Ruby | Gemfile, ruby-openai, openai or ruby_llm 1.8+ (not 2.0) |
.env.requal |
requal_otel.rb beside the Gemfile, or in Rails config/initializers/requal_otel.rb |
Outside Rails, require_relative "requal_otel" first in the file the app starts from, handed on |
Nothing: the gems are handed on, by your Ruby, 3.1 up |
| Rust | Cargo.toml, async-openai 0.37+ or rig |
.env.requal |
src/requal_otel.rs |
mod requal_otel;, requal_otel::start(), and with_http_service(...) on each client, or for rig record_content_telemetry(true) and what fits your subscriber, handed on |
Nothing: the crates are handed on |
| LiteLLM proxy | --proxy |
.env.requal, also with OTEL_EXPORTER, OTEL_ENDPOINT, OTEL_HEADERS and a generated LITELLM_MASTER_KEY, which the proxy reads by uvx --env-file |
.requal/litellm.yaml, with no secret in it |
The base URL, and export lines that load your app's API key from that file without showing it, handed on |
Nothing: the exact uvx command is handed on, at a pinned version |
| Claude Code | --claude-code |
.env.requal |
.requal/claude-code.json, mode 0600, holding the key |
None: start Claude Code with claude --settings <its path> |
Nothing |
Variables: REQUAL_OTLP_ENDPOINT and REQUAL_INGEST_KEY, at mode 0600. Never OTEL_* where your app reads them, which would redirect your own exporters, and never NEXT_PUBLIC_*: the Java agent, the proxy and Claude Code each read theirs alone. Each new file does nothing while either is missing (in Go, .NET, Rust and Spring AI, one line on stderr says so), does not stop the app when it fails (one line on stderr, each library in its own try; only --import naming a file that is gone can, and so does JAVA_TOOL_OPTIONS naming a jar that is gone, so .env.requal stays off images and other machines), exports over OTLP/HTTP protobuf through a batch processor, with the key in the authorization header, and sets service.name to the repository's name (in Ruby the folder's, in Rust the crate's). Its first line ends with a hash of the rest of the file, leaving out what a formatter changes: while that holds, any later init, on any machine or version, may replace it and disconnect may remove it.
Go, .NET and Rust look for .env.requal from the folder the app runs in up to the repository's top, or two folders up with no repository, and Spring Boot in the folder the app runs in, so start the app from its own folder. Ruby, Python and Node look from their own file.
Its provider is its own, never the global one, except on AI SDK 5 and 6, which read only that. It samples everything: your app's own sampling does not decide what Requal sees. A span whose parent is another provider's, such as your app's own span, says in its OTLP flags that the parent is remote, because that parent never reaches Requal, so the trace closes when its top span ends. Requal's flag comes after your own preload flags, and under @sentry/nextjs its line comes last in register(): first, your tracer's hooks would come after the SDK has loaded, and lose their own model span.
Beside a tracer of your own, what it gets stays as it was. openai, @anthropic-ai/sdk, LlamaIndex and AI SDK 7 make their span the active one while they call out, and a span of your tracer's below it would go under a parent your tracer never gets: Sentry's transaction would lose its http.client child. So the file changes trace.setSpan in the copies of the OpenTelemetry API these load, for Requal's own spans only: those go in a context key of their own, your active span stays the same object, and Requal's own tree is what it was. Run with and without Requal on Sentry 8, 9, 10 and 11 and on NodeSDK, under npm and pnpm: the same tree on your side. One mark stays: a tracer that instruments outgoing HTTP and does not go by OpenTelemetry's suppression, as Sentry 11 does, shows Requal's exports as http.client POST .../v1/traces spans.
A batch leaves within 5 seconds, and when the app ends by itself. On Node.js and Bun, process.exit, an uncaught error and SIGTERM lose nothing either: what is left goes by a short-lived process, run with the app's own process.execPath, that they wait for two seconds at most, and only when spans are left. Nothing after process.exit runs, and your exit code stays. SIGTERM's listener takes itself off first, so your own handlers, and a library counting them, see what they would without it and decide; with none, the process dies of the signal, once the spans are sent. While it is there, a process stuck in a synchronous loop needs SIGKILL, as under any handler. Not covered, at most the last 5 seconds' spans lost: any other signal, such as Ctrl+C's SIGINT or nodemon's SIGUSR2, SIGKILL, an export already on its way, Mastra's own exporter, Next.js's file, whose server has its own way down, and Deno, which would have to ask for --allow-run: its plan says so.
Packages are exact pins, each checked against the others, with the day each came out and the day it was read (recipes.json, which also holds the Java agent's SHA-256), the Node libraries' with the versions they were run on. The Python bootstrap is templates/requal_otel.py; the PyPI package writes the same file from the same template. They need Python 3.10 to 3.14 (3.13 with crewai: its releases and its instrumentor's say so, and pip keeps to that, though uv installs and runs them on 3.14); AI SDK 7 itself needs Node 22+. With Poetry the pins carry a Python constraint in pyproject.toml, so a default range such as ^3.11 still resolves.
Each was run for real through this CLI's plan, against a local fake model, with spans to a local sink: no real model. The apps were small ones written for this, not real repositories.
| Library | Model calls | Tool calls | Prompts | How the file reaches it |
|---|---|---|---|---|
openai |
One span per call: chat, responses, streaming | The model's request, on its span. No span of their own: your code runs the tool | Yes | OpenInference. Each build of the instrumentation patches the same build of the SDK, with its hook off |
@anthropic-ai/sdk |
One per call, streaming too | The same | Yes | OpenInference, by a patch of its import and its hook for require |
@aws-sdk/client-bedrock-runtime |
One per call: Converse, InvokeModel, each streaming | The same | Yes | OpenInference, one patch with its hook off: import and require reach one module |
| LangChain | One per call, with chain spans around it | A span each | Yes | OpenInference, on @langchain/core's callback manager. Under pnpm, package.json must list @langchain/core, or it is handed to you |
@openai/agents |
One per call, in agent and turn spans | A span each | Yes | OpenInference's processor, added beside the SDK's own: your OpenAI dashboard traces stay. OPENAI_AGENTS_DISABLE_TRACING, setTracingDisabled(true) and NODE_ENV=test turn it off, as they do the SDK's |
ai 7 |
One per call, in invoke_agent and step spans |
A span each | Yes | registerTelemetry, with this provider's tracer |
ai 5, 6 |
One per call, in ai.generateText and the like |
A span each | Yes | The global tracer provider, registered through the API copy ai resolves, and experimental_telemetry on each call |
| Mastra | One per call, in agent and step spans. It also sends model_generation, model_chunk and processor_run spans |
A span each | Yes | Its own exporter, in your code |
llamaindex |
One per call through @llamaindex/openai |
The model's request, on its span. No agent or tool span | Yes | Traceloop's instrumentation, as OpenInference's stopped in 2024. LlamaIndex.TS itself is deprecated, and its repository was archived on 2026-04-30 |
Mastra. No preload reaches new Mastra(), so the exporter is a change handed to you or your agent: two imports and one observability option, which adds nothing while the variables are missing. If your Mastra has observability already, add only the OtelExporter to the exporters of the config it has: Mastra refuses a second config with no configSelector. signals.logs stays false: a traces-only receiver has no /v1/logs. mastra dev reads .env; anywhere else the process must have the two variables itself, such as with node --env-file=.env. Beside a library the preload traces, the variables are in .env.requal, and the preload hands them on.
A framework and its raw SDK. LangChain, OpenAI Agents and LlamaIndex call openai, @anthropic-ai/sdk and the Bedrock client themselves, and their spans are never the current span, so with both patched one call would be two spans in two traces. So a raw SDK listed beside one of them, or beside ai, is patched only where your own code imports it. Where it does, the file drops the SDK's span whose caller is one of the framework's files, and keeps a call your own code makes, a tool's too. In Next.js this adds the packages in those frames to serverExternalPackages. What stays double counted: a model class of your own over the raw SDK, used inside the framework: the caller is your file, so such a call shows twice. Run on @langchain/openai, @langchain/anthropic and OpenAI Agents; @langchain/aws over Bedrock, and LlamaIndex with the raw SDK patched too, were not.
Not traced.
@google/genaiand@google/generative-ai. OpenInference has nothing for JavaScript, and Traceloop's instrumentation patches only arequireof@google/genai1.x, never an import or 2.x. Alone it stops; beside a library that is traced,initconnects for that one, and says what will not show.@ai-sdk/googleunder the AI SDK is traced as the AI SDK.- MCP (
@modelcontextprotocol/sdk). An MCP client or server makes no model call of its own, and OpenInference's instrumentation for it only carries trace context, with no span of its own. So it is not a library Requal looks for: alone it stops; beside a model library, the model's calls are traced, and each process's in a trace of its own. @aws-sdk/client-bedrock-agent-runtime, other@llamaindex/*providers, and JSR'sjsr:@openai/openaion Deno: no recipe.- Mastra on Deno: no recipe.
- An app that bundles its SDKs into one file (esbuild, or Next.js with the SDK left in the bundle): a preload patches the package, not a bundled copy.
Proven on Node.js 22.23, npm 10.9, pnpm 12.14, Bun 1.3.11, Deno 2.9.6, and Next.js 16.4 (Turbopack, next dev, and next build with next start) and 14.2 (webpack). Yarn 1 and 4 were run only for their lockfiles, and other versions not at all. With pnpm 12 strict about build scripts, an install of your own that stops on one is yours to settle first: @google/genai and llamaindex bring such scripts.
Each row was run for real: the library, the pin and the bootstrap, against a fake model, from the lowest version named to the newest. A version comes from what is installed, then a lockfile, then an exact pin; a range alone counts as the newest it allows.
| Library | Names in a manifest | From | Traced by | What is missing |
|---|---|---|---|---|
| OpenAI SDK | openai |
1.69 | Instrumentor | No tool span: the app runs the tool, so the call is in the model span's messages. The same for the next seven rows |
| Anthropic SDK | anthropic |
0.84 | Instrumentor | |
| google-genai | google-genai |
1.8 | Instrumentor | |
| Vertex AI | google-cloud-aiplatform, vertexai |
1.63 | Instrumentor | Run over REST only; gRPC, its default, is not proven |
| Bedrock | boto3, with "bedrock-runtime" in the app's own Python |
1.34 | Instrumentor | Below 1.34 the instrumentor would stop the app when it makes a client, so it stops here instead. aioboto3 is not traced |
| Mistral | mistralai |
1.0 | Instrumentor | |
| Groq | groq |
0.9 | Instrumentor | |
| LiteLLM | litellm |
1.65 | Instrumentor | litellm 1.106.6 fails on tools= unless mcp is installed, with or without Requal. An async stream's span has no output or tokens |
| LangChain | langchain, langchain-* |
core 0.2.2 | Instrumentor | A tool has a span only when it runs through LangChain |
| LangGraph | langgraph, langgraph-* |
core 0.2.2 | Instrumentor | |
| LlamaIndex | llama-index, llama-index-* |
core 0.12.37 | Instrumentor | Each tool-calling turn adds an LLM span that calls nothing (_prepare_chat_with_tools). A stream has no tokens |
| CrewAI | crewai, crewai-tools |
1.10.1 | Instrumentor, with OpenAI's beside it for its model calls | Its Anthropic and LiteLLM providers were not run |
| OpenAI Agents SDK | openai-agents |
0.2.6 | Instrumentor, keeping the SDK's own processors | |
| DSPy | dspy, dspy-ai |
2.6.19 | Instrumentor, with LiteLLM's beside it for tokens | A tool span has its name only in the span's name |
| Haystack | haystack-ai |
2.18 | Instrumentor, with OpenAI's beside it | An Agent's tools have no span. Each Agent turn is its own trace unless the Agent is in a Pipeline. Under run_async only the OpenAI SDK's span comes, so a generator on another provider has none there |
| Google ADK | google-adk |
1.2.1 | Instrumentor | It takes ADK's own spans from any exporter of yours: with one, run it without Requal or accept that. The pin for 2.13 was one day old when read, because no older one sees a model call there |
| AutoGen AgentChat | autogen-agentchat, autogen-core, autogen-ext, pyautogen from 0.10 |
0.5.1 | Instrumentor | |
| AG2 below 1.0 | ag2 from 0.14, autogen, pyautogen below 0.10 |
0.5 | Instrumentor, with OpenAI's beside it for its model calls | Another provider's model calls show only if its SDK is in the manifest |
| AG2 1.x | ag2 from 1.0 |
1.0 | Its own middleware, added to each Agent |
|
| smolagents | smolagents |
1.24 | Instrumentor | |
| Agno | agno |
2.0 | Instrumentor | |
| MCP | mcp below 2, beside another library here |
1.6 | Instrumentor, which only carries trace context across a call | It makes no spans, so mcp alone is not traced. mcp 2 carries the context itself and gets nothing from Requal; its own spans go to the global provider and do not reach Requal. fastmcp was not run |
| Claude Agent SDK | claude-agent-sdk |
0.1.45 | Instrumentor | No model span can exist: the calls happen in the claude process. The agent span holds the model, tokens and cost, and only the prompt and the answer of each query |
| Pydantic AI | pydantic-ai, pydantic-ai-slim |
0.2 | Its own Agent.instrument_all |
A later instrument_all of yours, logfire.instrument_pydantic_ai(), or instrument= on an Agent replaces Requal's for those agents |
| Strands | strands-agents, strands-agents-* |
1.0 | Its own telemetry, reached by wrapping its one Tracer |
Set a provider of your own before your first Agent: one set after it gets no Strands spans |
| Microsoft Agent Framework | agent-framework, agent-framework-* |
1.0 | Its own telemetry, reached by replacing observability.get_tracer |
With a provider of your own, prompts reach Requal only if you set ENABLE_SENSITIVE_DATA |
Older versions take older pins. Three of those pins pass wrapt 2 a keyword it removed, so wrapt 1.17.3 goes beside them: for google-genai below 1.55, mistralai below 2, and agno below 2.5.
One OpenTelemetry version goes in for the whole plan: 1.45.1, or the lowest any library here needs. google-adk from 1.15, crewai below 1.15.21, mistralai 1.10 to 1.12.1 and 2.0 to 2.9.1, Pydantic AI below 1.32 and autogen-agentchat 0.6.2 and 0.6.3 cap it, each by its version, and recipes.json holds what each takes. Two libraries with no version in common stop before Allow.
An opentelemetry-* package the app declares stops too, before Allow, when it cannot sit with that version: a bound on the sdk or api that leaves the version out, an instrumentation or semantic-conventions pin of another train (1.45.1 takes only 0.66b1), a retired one such as opentelemetry-instrumentation-elasticsearch, or one locked or installed on another train, which the install would replace. What a lock or a venv holds only for a library's own needs moves with the install. A pip constraints file named by -c or --constraint counts the same way, for what the app asks for and what the plan installs. traceloop-sdk stops too when what you declare cannot resolve beside that version, as do the other 49 distributions in otel_capped in recipes.json, each shown not to by the resolver and by real pip, with its version: the stop says what to move it to, or to take it out, or to run init --proxy, which adds no package. For these a lock or a venv is no pin: a version in it that fits stays, one that does not moves inside your range at the next install, so only a range whose newest cannot resolve stops. Anything outside otel_own and otel_capped is not judged: if it caps OpenTelemetry, your next pip install -r requirements.txt fails with ResolutionImpossible, and uv add or poetry add fails after the key is spent. disconnect takes Requal's lines back.
The bootstrap never touches the global tracer provider. With a provider of your own, your spans stay yours. Strands' and Agent Framework's spans reach both: Requal's processor goes on your provider, so your sampler decides which of them Requal sees, and it sees your other spans too; a trace with no AI span is deleted at close. Everywhere else the bootstrap's own provider samples everything, whatever your sampling says. It does two things to what it sends:
- An SDK's span below LangChain's, LangGraph's, LlamaIndex's or LiteLLM's model call is dropped, so one call is one model span, with the framework's span holding it. A raw SDK call beside them keeps its span.
- A span whose parent is not one of its own, such as your request's span from your own provider, says its parent is remote. That parent never reaches Requal, so the trace closes 30 seconds after its top span, not 10 minutes after its last.
When any of this cannot run (an import fails, a library has moved), the app runs on, with one line on stderr.
Run with uvicorn --workers and with gunicorn, with and without --preload. The bootstrap starts no thread at import, because on macOS a worker forked from a process that ever had a thread can die at its first model call, in the system's proxy lookup. That can still happen there with LlamaIndex and the OpenAI Agents SDK, whose instrumentors bring it on at import, with CrewAI and Vertex AI, which start a thread themselves with or without Requal, and in an app that calls a model before it forks. no_proxy=* in the environment stops it: run on the LlamaIndex, OpenAI Agents and model-call cases. Not run on Linux, which has no such lookup.
Not run: any real model, and everything named above as not run. Run on Python 3.10, 3.12 and 3.14 for all but older versions, and resolved on 3.11 and 3.13.
- Beside a tracer of your own: Requal has a tracer provider of its own beside Sentry, Logfire, Langfuse and Datadog, and each keeps receiving what it did. In Python, a span of your own OpenTelemetry provider's or Logfire's below a model call goes under OpenInference's span, which yours never receives, so your tree loses that link; Sentry's SDK for Python is not affected. In Node this does not happen (above). Their own spans (Sentry's
gen_ai.chat, Datadog'sopenai.request, Langfuse's generation) do not reach Requal, and Requal's traces are not joined to theirs. Where Logfire'sinstrument_*takes a model library from Requal's instrumentation, so that Requal gets nothing, one line forlogfire.configureis handed on:later, until your code shows such a call. - Java: plain OpenAI Java and Amazon Bedrock have model spans with tokens and no prompts: the agent writes those only as OTLP logs, which Requal does not read. OpenAI Java's Responses API has no model span. The agent also sends the JVM's other spans, such as HTTP calls with their URLs and database queries, and Spring Boot's go beside Spring AI's: a trace with no model call is deleted when it closes, and a kept trace keeps every span it has. Gradle's daemon carries the agent too, and its download spans arrive and are deleted at close.
- Go, Ruby, async-openai: each model call is its own trace, unless Go's
requalotel.Spangroups them. A library inrecipes.jsonwith no form, client or parts stops, naming--proxy: nothing would make a model span for it. - Claude Code: a trace has each prompt (
user_prompt), model names, tokens, timings, tool names, Claude Code's version (service.version), the session (session.id), a random user id (user.id), the terminal (terminal.type), the operating system (os.type,os.version, andwsl.versionon WSL), the machine's architecture (host.arch), and for a Bash command the kind of command (bash_command_class) and its first program when that is on Claude Code's fixed list (bash_argv0, orother). It has no answers, tool results, or the rest of a tool's arguments. When you are signed in, your e-mail address (user.email), organization id (organization.id) and account id (user.account_uuid,user.account_id) go with the spans. A session started without the flag sends nothing, and the Desktop app and IDE extensions have no way to pass it that was run. - The proxy: a refused request shows as a failed model call, since LiteLLM marks its span that way.
- A pre-release: an OpenInference instrumentor declines a library's pre-release, such as
langchain_core1.3.0a2 ordspy3.0.0b3, with no error. Its own spans do not come; a model call still shows where an SDK's or LiteLLM's instrumentor is beside it. - A notebook: no recipe. A repository of notebooks gets the plain stop, or the hand-off to put the import in a module, which a notebook has no place for.
stop code |
When |
|---|---|
existing_provider |
On AI SDK 5 or 6 in Next.js, @sentry/nextjs 8+: it holds the global tracer provider those need, and from 11 turns their telemetry off on each call. AI SDK 7 sits beside it. On AI SDK 5 or 6 on any stack here, Sentry's other SDKs for a server from 8: @sentry/node, @sentry/nestjs, @sentry/aws-serverless, @sentry/google-cloud-serverless, @sentry/bun, @sentry/deno and the rest of the family in recipes.json, by name, or because the lockfile of npm, pnpm, yarn or bun says a dependency of the app's brings one in. 8 to 10 hold the provider: with Sentry first Requal sends nothing, and with Requal first Sentry's tracing stops, with no message. From 11 most turn the SDK's telemetry off; @sentry/aws-serverless, @sentry/google-cloud-serverless, @sentry/bun and @sentry/elysia do not, and go on. Also on AI SDK 5 or 6, which use the global tracer provider, a provider already set up in instrumentation.ts or a file it imports, or by a preload a dev or start script names, or a package that sets one up as a dependency: @opentelemetry/sdk-node, sdk-trace-node, sdk-trace-base, @opentelemetry/auto-instrumentations-node, @vercel/otel, @traceloop/node-server-sdk, @honeycombio/opentelemetry-node, @azure/monitor-opentelemetry, applicationinsights, @arizeai/phoenix-otel, @pydantic/logfire-node, @elastic/opentelemetry-node, @splunk/otel, @aws/aws-distro-opentelemetry-node-autoinstrumentation, @hyperdx/node-opentelemetry, @lumigo/opentelemetry, @dash0/opentelemetry, @uptrace/node, @highlight-run/node, @launchdarkly/observability-node, @instana/collector, @baselime/node-opentelemetry, @middleware.io/node-apm, langwatch, openlit, mlflow-tracing, @mlflow/core. A package only in devDependencies does not count, nor what only a sibling app in a monorepo's lockfile brings in; a tool of Sentry's in dependencies that brings @sentry/node in does. Elsewhere Requal has a provider of its own and sits beside yours. On AI SDK 5 or 6, Requal's line goes last in register(): a provider it cannot see keeps its place only if it registers before that line runs. One set up later, such as in a module a route imports, loses the place to Requal and stops receiving spans, with no message. Also a pinned OpenTelemetry package of yours on another major, which installing would replace, and a file with Requal's first line whose hash no longer holds, because someone edited it |
env_file_tracked, env_file_not_ignored |
Git would commit the env file: it is tracked (found before asking anything), or a .gitignore undoes the rule (found after Allow, and that key is not saved). The same for a tracked file in .requal/ that a plan writes, as the key in .requal/otel.properties, and for a tracked .requal/.gitignore, which the CLI replaces whole from its first save |
env_file_unusable |
The env file is a link or hard linked (the key would go into the other name too), leads out of this folder, is a folder, unreadable, or not UTF-8 |
git_failed |
A repository is here and git cannot answer for it within 30 seconds, so the CLI cannot tell what would be committed |
cannot_write |
It cannot write what it would need to, a file or folder is in the way, a file a plan must read is a pipe or a device or past 64 MiB, or a file appeared in the wait for Allow |
state_tracked |
Git tracks .requal/connect.json: it may have come with the repository, and nothing in it is used |
state_unusable |
disconnect with a .requal/connect.json it refuses (cut short, a link, wider modes than the CLI gives it, or an edit that does more than add Requal's words): it is the only record of what init changed, so nothing is changed. init moves such a file aside to connect.json.refused, then .refused.1 and so on, which stay until you delete .requal, and starts again; with twenty there already, it stops instead of losing one |
unsupported_version |
ai below 5, next below 13.4, JS openai below 5, @aws-sdk/client-bedrock-runtime below 3, @langchain/core below 0.3, @openai/agents below 0.3.8 (a CommonJS app would send nothing), llamaindex below 0.12, @mastra/core below 1.16, a deno.json that imports ai with no version, a Python library below the first version Requal traces ("From" in "Python libraries"); a declared lower bound alone does not count, only a range that cannot reach the floor. The OpenAI SDK below 1.69 beside CrewAI or AG2, whose model calls show only through it. Two Python libraries with no OpenTelemetry version in common, naming both. A go line below 1.22, Ruby below 3.1, ruby_llm below 1.8 or on 2.0, async-openai below 0.37. @opentelemetry/api declared from 1.10. A different == pin of a package Requal needs. An opentelemetry-* package the app declares that cannot sit with the OpenTelemetry version the plan installs ("Python libraries" says which). Python below 3.10, or from 3.15 (3.14 with crewai), in a virtual environment, .python-version, runtime.txt, .tool-versions, a Dockerfile's FROM python, requires-python, or what pip names. No pip, or a virtual environment with none |
unsupported_stack |
No manifest, or no AI library it has a recipe for, with why for @google/genai and @modelcontextprotocol/sdk, which are not traced; boto3 with no "bedrock-runtime" in the app's own Python is not one, nor is mcp alone, which says so; or only one nothing makes a model span for, in Java, .NET, Go, Ruby or Rust, which names --proxy; an F# project; a Rust crate with both async-openai and rig; a library where a preload does not reach the app's code and no script starts Node.js or a file on Bun (Vite, SvelteKit, Nuxt, Astro, React Router, Remix, Wrangler, bunx, deno in a package.json script, Create React App, Vue CLI, Expo and the like), said by a script, one level of npm run under it, or the tool's config file; llamaindex without @llamaindex/openai; a deno.json with no library under an npm: import of its own name, or Mastra alone on Deno; a pageExtensions under which Next.js would not load its file; or a virtual environment's python that git tracks |
multiple_apps |
Several apps with an AI library, in any language, or with none anywhere, several Next.js apps: it lists the --dir commands, twenty at most |
not_connected, connection_expired |
wait with no connection here, or a status token past its day |
refused, unreachable |
Requal said no (server_code has its code), or did not answer. A 404 on the device route says Requal does not take this version yet. Behind a proxy, HTTPS_PROXY is used from Node 22.21 and 24; a value that is not an http or https URL is named and left unused |
Each line is one object, {"v": 1, "type": ...}. Diagnostics go to stderr. No line ever holds the key, the device code or the status token.
type |
Fields |
|---|---|
found |
stack, dir, repository, package_manager, ai |
plan |
env_file, env, files, edits, codemod (each with its unified diff), packages, install |
link |
url, code, expires_at |
connected |
organisation, ai_system, ai_system_id, environment |
wrote |
path, kind: env, file, edit or codemod; the Java agent's jar is a file, after its SHA-256 is checked |
removed |
path |
waiting |
for: allow or first_trace; spans_received |
next |
do: ask_person (say, then), run (run, background), edit (path, line, where) or change (language, packages, code, where): the packages and lines to add in a language the CLI does not edit, or a whole new file first where it could not place one |
done |
say, trace_url, model_calls, has_prompts, fixes |
stop |
code, say, owner, server_code |
.requal/connect.json, mode 0600 in a 0700 folder, naming this folder's path: the link until it is used, then the status token, and what init wrote so disconnect can undo it. Never the key. It is saved before anything is written, and anything wider is not trusted. One written for another folder means this one was moved or copied: disconnect undoes what it finds here, init keeps only its record of what changed and makes a new key, and its address and tokens are never used. A state can come with an archive, so what it holds is checked: an edit may only add Requal's words, no path reaches .git, even through a link, and the code a change holds is asked of the recipe on each run, never read from the file.
The CLI itself loads no tracked code of a repository before Allow: pip is looked for in a virtual environment's folders and asked with no PYTHONPATH, a TypeScript that git tracks, or in a folder no repository holds, is not loaded (one that git does not track, in a repository, is loaded: a folder you were handed with a .git in it, or that you ran git init in, has its node_modules/typescript run before Allow), git is told to ignore a repository's core.fsmonitor, and node_modules/.bin, any relative folder and any empty entry are taken off the PATH of its git and pip calls and of the browser it opens. It reads only plain files, never a device or a pipe behind a link, and none past 64 MiB. But npx runs first, and it takes a repository's node_modules/.bin, its .npmrc and its own node_modules/requal. Run this only in a repository you trust.
Tested on macOS and on Linux. Windows and WSL are not tested yet.
npm ci
npm run typecheck
npm test
npm run build
node scripts/pack-smoke.mjs