Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@requal/cli

Keeps the OpenTelemetry traces of an AI app in Requal.

npx -y @requal/cli@1.1.0 init

One 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.

Commands

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.

init

  1. 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.

  2. 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-XXXX on the page matches the one in the terminal.

  3. After Allow, it gets the ingest key and writes: a line in the .gitignore beside 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 from templates/, 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.

  4. 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 later yarn 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, and esbuild, sharp and Next.js's SWC work without their scripts. A package that does need its script, such as sqlite3 for its binding, or your own postinstall, then fails at your app's start. To run them: npm rebuild; yarn rebuild; on Yarn 1 yarn install --force; a package's with pnpm approve-builds <package>, then pnpm rebuild, or bun pm trust <package> (one Bun already trusts, as sqlite3: delete node_modules and run bun install); your own with pnpm run postinstall or bun run postinstall.

  5. 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.

wait

Polls for the first trace with a model call: DONE <trace link>. The status token lasts a day.

disconnect

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.

How it finds the app

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.

What it writes

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.

What each Node library gives

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/genai and @google/generative-ai. OpenInference has nothing for JavaScript, and Traceloop's instrumentation patches only a require of @google/genai 1.x, never an import or 2.x. Alone it stops; beside a library that is traced, init connects for that one, and says what will not show. @ai-sdk/google under 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's jsr:@openai/openai on 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.

Python libraries

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.

What stays out of a trace

  • 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's openai.request, Langfuse's generation) do not reach Requal, and Requal's traces are not joined to theirs. Where Logfire's instrument_* takes a model library from Requal's instrumentation, so that Requal gets nothing, one line for logfire.configure is 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.Span groups them. A library in recipes.json with 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, and wsl.version on 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, or other). 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_core 1.3.0a2 or dspy 3.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.

What stops it

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

--agent lines

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

Local files

.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.

Where it runs

Tested on macOS and on Linux. Windows and WSL are not tested yet.

Development

npm ci
npm run typecheck
npm test
npm run build
node scripts/pack-smoke.mjs

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages