> ## Documentation Index
> Fetch the complete documentation index at: https://raindrop.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool catalog capture (ai.prompt.tools)

> Every Raindrop SDK records the list of tools a model was given on each model-call span, JSON Schema included, as `ai.prompt.tools`. This page defines the attribute, what each integration can see, and how to override it.

Knowing which tools a model *could* have called is as important as knowing which ones it did call: it is what lets you tell "the agent never had a `refund` tool" from "the agent had it and chose not to use it". Raindrop records that list on every model-call span under one attribute, `ai.prompt.tools`, with the same shape across every SDK and integration.

## The attribute

`ai.prompt.tools` is an OpenTelemetry **string array**. Each element is one JSON document describing one tool the model was given for that call:

```json theme={null}
{
  "type": "function",
  "name": "get_weather",
  "description": "Look up the current weather for a city",
  "inputSchema": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "City name" },
      "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
    },
    "required": ["city"]
  }
}
```

* **Function tools** carry `name`, `description` and the JSON Schema the model saw as `inputSchema` — properties, types, enums, `required` and per-field descriptions are recorded as given, never trimmed or invented. Framework schemas (Zod, TypeBox, Pydantic, ...) are converted to JSON Schema the same way the framework converts them for the provider.
* **Provider-defined tools** (web search, code execution, MCP toolsets, computer use, ...) are recorded as `{"type": "provider-defined", "id": ..., "name": ..., "args": {...}}`. The provider owns their schema, so none is invented.

Three states are distinguishable downstream:

| Attribute value | Meaning |
| - | - |
| absent | The integration could not see the tool list for this call ("unknown"). |
| `[]` | The model was given no tools. |
| one or more documents | The tools the model was given for this call. |

### Exact capture vs. catalogs

Most integrations sit on the request path and record the **exact** list the model received on that call. A few only see a registry or agent definition that *may* overstate what one particular request carried. Those record the list they can see and add a second attribute, `ai.prompt.tools.source`, naming where it came from (for example `"opencode.tool.list"`, `"mastra.agent.tools"`, `"agent.config"`, `"agno.agent.tools"`, `"crewai.agent.tools"`), so a catalog is never mistaken for exact capture. An explicit override (below) never carries `source`.

### Content gating

Tool definitions are prompt content. They follow the same switch as `ai.prompt.messages` in every SDK: `recordInputs: false` / `captureContent: false` in the TypeScript integrations that expose one, `TRACELOOP_TRACE_CONTENT=false` in Python. With content capture off, the attribute is never recorded — it is absent, not `[]`. The managed-agents packages (`@raindrop-ai/claude-managed-agents`, `@raindrop-ai/openai-managed-agents`) have no prompt-content switch of their own: they record the agent catalog whenever tracing is enabled, and the only way to withhold it is a `tools: []` override.

### Size

The full catalog is recorded on every model span, uncapped, because a truncated tool list defeats the purpose. Agents with very large MCP tool sets add a few tens of KB per model call; turn content capture off, or pass a smaller `tools` override, if that matters for you.

## Overriding the list

Every integration accepts a `tools` option that **replaces** whatever it would have inferred (there is no merging). Use it when the integration cannot see the tool list, or when you want to record a different one; `tools: []` records an explicitly empty catalog. Declarations may be passed in any shape the SDK recognizes — the canonical document above, OpenAI `{"type": "function", "function": {...}}`, Anthropic `{"name", "description", "input_schema"}`, plain `{name, description, parameters}`, or the framework's own tool objects where the integration accepts them.

<CodeGroup>
  ```typescript TypeScript (core SDK) theme={null}
  import { Raindrop } from "raindrop-ai";

  const raindrop = new Raindrop({ writeKey: process.env.RAINDROP_WRITE_KEY! });

  // Every model span started inside this interaction records these tools.
  const interaction = raindrop.begin({
    userId: "user_123",
    event: "support_chat",
    input: "Where is my order?",
    tools: [
      {
        type: "function",
        name: "lookup_order",
        description: "Fetch an order by id",
        inputSchema: {
          type: "object",
          properties: { orderId: { type: "string" } },
          required: ["orderId"],
        },
      },
    ],
  });
  ```

  ```typescript TypeScript (integration option) theme={null}
  import { createRaindropAISDK } from "@raindrop-ai/ai-sdk";

  // Same `tools` option on every integration's create*/wrap()/handler options.
  const raindrop = createRaindropAISDK({
    writeKey: process.env.RAINDROP_WRITE_KEY!,
    tools: [
      { type: "function", name: "lookup_order", inputSchema: { type: "object", properties: {} } },
    ],
  });
  ```

  ```python Python theme={null}
  import os

  import raindrop.analytics as raindrop

  raindrop.init(os.environ["RAINDROP_WRITE_KEY"], tracing_enabled=True)

  # Interaction-scoped: every model span inside the interaction gets this list.
  interaction = raindrop.begin(
      user_id="user_123",
      event="support_chat",
      input="Where is my order?",
      tools=[{
          "type": "function",
          "name": "lookup_order",
          "description": "Fetch an order by id",
          "inputSchema": {
              "type": "object",
              "properties": {"orderId": {"type": "string"}},
              "required": ["orderId"],
          },
      }],
  )
  interaction.set_tools([...])   # replace the list on a live interaction

  # Block-scoped, independent of interactions:
  with raindrop.prompt_tools([]):   # "this call had no tools"
      client.chat.completions.create(...)
  ```
</CodeGroup>

The override is stamped only on **model** spans (spans that carry `llm.request.type`, `gen_ai.request.model`, `llm.request.model` or `ai.model.id`), never on tool-call, task or interaction spans.

## What each integration captures

"Exact" means the integration sees the request the model received on every call. "Catalog" means it records a registry or agent definition and marks the span with `ai.prompt.tools.source`. "Override only" means the integration never sees the model request, so the attribute is absent unless you pass `tools`.

### TypeScript

| Integration | Capture | Where the list comes from | Override |
| - | - | - | - |
| [Vercel AI SDK](/docs/integrations/vercel-ai-sdk) | Exact | The `tools` of each `generateText` / `streamText` / `generateObject` / `streamObject` call (Zod converted via the AI SDK's `asSchema`); provider-defined tools keep `id` / `args`. Both the `wrap()` and the v7 native telemetry path. | `tools` on `createRaindropAISDK` / `wrap()` |
| [TanStack AI](/docs/integrations/tanstack-ai) | Exact | The `tools` the engine hands the adapter on each call | `tools` on the middleware options |
| [LangChain](/docs/integrations/langchain) | Exact | The callback's `invocation_params.tools` (or legacy `functions`), i.e. whatever `bindTools()` bound on ChatOpenAI, ChatAnthropic and similar | `tools` on `createRaindropLangChain` |
| [Deep Agents](/docs/integrations/deepagents) | Exact | Same callback data as LangChain | `tools` on `createRaindropDeepAgents` |
| [Pi Agent](/docs/integrations/pi-agent) | Exact | The agent's `context.tools` on each model call, TypeBox schemas intact (no longer subject to the `ai.prompt` text budget) | `tools` on `createRaindropPiAgent` or `subscribe(agent, { tools })` |
| [OpenRouter Agent](/docs/integrations/openrouter-agent) | Exact | `request.tools` of each `callModel` (`tool()` schemas converted to JSON Schema, `serverTool()`s as provider-defined) | `tools` on the create options |
| [Azure OpenAI](/docs/integrations/azure-openai) | Exact | The request's `tools` (or legacy `functions`) on every `chat.completions.create` | `tools` on `createRaindropAzureOpenAI` |
| [Amazon Bedrock](/docs/integrations/bedrock) | Exact | Converse `toolConfig.tools[].toolSpec`; Anthropic `InvokeModel` bodies' `tools` | `tools` on `createRaindropBedrock` |
| [Vertex AI](/docs/integrations/vertex-ai) | Exact | `config.tools` of each `generateContent` call: `functionDeclarations` as function tools, `googleSearch` / `codeExecution` / ... as provider-defined | `tools` on `createRaindropVertexAI` |
| [OpenAI Agents](/docs/integrations/openai-agents) | Exact (Responses API) | The tool list the Responses API echoes on each response; Chat Completions `generation` spans carry no list | `tools` on `createRaindropOpenAIAgents` |
| [Strands](/docs/integrations/strands) | Exact | The agent's registered tool specs, the list the SDK hands the model on each call | `tools` on `createRaindropStrands` |
| [Mastra](/docs/integrations/mastra) | Catalog (`mastra.agent.tools`) | The agent's registered tools plus the call's `toolsets` / `clientTools`; Mastra does not expose the per-request list | `tools` on `createRaindropMastra` |
| [OpenCode](/docs/integrations/opencode) | Catalog (`opencode.tool.list`) | The host's tool catalog for the step's model (`client.tool.list`, cached per session and model) | `tools` in `raindrop.json` |
| [Claude Managed Agents](/docs/integrations/claude-managed-agents) | Catalog (`agent.config`) | The resolved agent definition (`session.agent.tools`); model calls run inside Anthropic's runtime | `tools` on the create / `wrap()` options |
| [OpenAI Managed Agents](/docs/integrations/openai-managed-agents) | Catalog (`agent.config`) | The resolved agent definition; model calls run inside OpenAI's runtime | `tools` on the create / `wrap()` options |
| [Claude Agent SDK](/docs/integrations/claude-agent-sdk) | Override only | The message stream only lists tool *names* (`system/init`), no schemas | `tools` on `wrap(sdk, { tools })` |
| [Cursor Agent SDK](/docs/integrations/cursor-agent-sdk) | Override only | The SDK streams messages, never the model request | `tools` on the create / `wrap()` options |
| [Claude Code](/docs/integrations/claude-code) | Override only | Hooks and transcripts never expose the model request | `tools` in `~/.config/raindrop/config.json` or `RAINDROP_TOOLS` |
| [Cursor](/docs/integrations/cursor) | Override only | Hooks never expose the model request | `tools` in `~/.config/raindrop/config.json` or `RAINDROP_TOOLS` |
| Core SDK (`raindrop-ai`) | Exact (mirrored) | OpenLLMetry-style instrumentations record `llm.request.functions.{i}.*` on model spans; the SDK's span processor rebuilds `ai.prompt.tools` from them at export | `tools` on `begin()` |

### Python

| Integration | Capture | Where the list comes from | Override |
| - | - | - | - |
| Core SDK (`raindrop-ai` ≥ 0.0.70) | Exact (mirrored) | The OpenLLMetry instrumentations for OpenAI, Anthropic, LangChain, OpenAI Agents, Cohere and Ollama record `llm.request.functions.{i}.name` / `.description` / `.parameters`; the SDK rebuilds `ai.prompt.tools` from them at export. OpenLLMetry ≥ 0.54 (Anthropic) / 0.55 (OpenAI, LangChain) writes `gen_ai.tool.definitions` instead, one JSON string; 0.0.72 rebuilds from that too, preferring the legacy keys when both exist, and keeps OpenAI built-ins and Anthropic server tools as `provider-defined` entries. The wrappers below that sit on those instrumentations bind the list themselves and do not depend on it. The Bedrock, Vertex AI and Google GenAI instrumentations record no tool definitions, so the wrappers below read the request themselves. | `tools=` on `begin()` / `raindrop.interaction()`, `interaction.set_tools()`, `raindrop.prompt_tools()` (`source=` from 0.0.71) |
| [Amazon Bedrock](/docs/integrations/bedrock) (`raindrop-bedrock` ≥ 0.0.11) | Exact | Converse `toolConfig.tools[].toolSpec`; Anthropic `invoke_model` bodies' `tools`; sync, async and streaming | `tools=` on `RaindropBedrock` / `create_raindrop_bedrock` |
| [Vertex AI](/docs/integrations/vertex-ai) (`raindrop-vertex-ai` ≥ 0.0.12) | Exact | `config.tools` of each `generate_content` call: `function_declarations` as function tools, `google_search` / `code_execution` / ... as provider-defined | `tools=` on `RaindropVertexAI` / `create_raindrop_vertex_ai` |
| [Strands](/docs/integrations/strands) (`raindrop-strands` ≥ 0.0.13) | Exact | The agent's registered tool specs, bound around each model call from the before/after-model hooks | `tools=` on `RaindropStrands` / `create_raindrop_strands` |
| [Google ADK](/docs/integrations/google-adk) (`raindrop-google-adk` ≥ 0.0.17) | Exact | `LlmRequest.config.tools` from a `before_model_callback` plugin on the wrapped `Runner` (not `run_live()`) | `tools=` on `RaindropGoogleADK` / `setup_google_adk` / `create_raindrop_google_adk` |
| [Agno](/docs/integrations/agno) (`raindrop-agno` ≥ 0.0.11, core ≥ 0.0.71) | Catalog (`agno.agent.tools`) | `agent.tools` (toolkits expanded) plus team members' tools; Agno adds its own tools per run | `tools=` on `RaindropAgno` / `create_raindrop_agno`, per target on `wrap()` |
| [CrewAI](/docs/integrations/crewai) (`raindrop-crewai` ≥ 0.0.10) | Catalog (`crewai.agent.tools`) | Union of the crew's agent and task tools via CrewAI's own schema conversion; model calls are per agent | `tools=` on `RaindropCrewAI` / `setup_crewai` / `create_raindrop_crewai`, per crew on `wrap()` |
| [LangChain](/docs/integrations/langchain) (`raindrop-langchain` ≥ 0.0.13) | Exact | The callback's `invocation_params["tools"]` (or legacy `functions`), the list `bind_tools()` put on the request, bound from `on_chat_model_start` until `on_llm_end`; the OpenLLMetry `langchain` instrumentation writes the same list to `gen_ai.tool.definitions`, which the core rebuilds from since 0.0.72; the wrapper binds the list itself so it works on 0.0.70 too | `tools=` on `RaindropLangchain` / `RaindropCallbackHandler` / `create_raindrop_langchain`, per call `config={"metadata": {"raindrop_tools": [...]}}` |
| [Deep Agents](/docs/integrations/deepagents) (`raindrop-deep-agents` ≥ 0.0.10) | Exact | Same callback data as LangChain: your tools plus the Deep Agents built-ins the model was given on that call (`write_todos`, `read_file`, `task`, ...) | `tools=` on `RaindropDeepAgents` / `RaindropDeepAgentsHandler` / `create_raindrop_deep_agents`, per call `config={"metadata": {"raindrop_tools": [...]}}` |
| [OpenAI Agents](/docs/integrations/openai-agents) (`raindrop-openai-agents` ≥ 0.0.12) | Exact through the core (`raindrop-ai` ≥ 0.0.72), else override only | The Agents SDK trace API shows the processor tool *names* at agent-span start and the full list only when the response span ends, after the model span started, so the wrapper infers nothing; the OpenLLMetry `openai` instrumentation (`disable_auto_instrument=False`) writes the request's tools to `gen_ai.tool.definitions` and the core rebuilds `ai.prompt.tools` from it | `tools=` on `RaindropOpenAIAgents` / `create_raindrop_openai_agents` (through `begin(tools=...)`), per run `RunConfig(trace_metadata={"raindrop_tools": [...]})` |
| [Azure OpenAI](/docs/integrations/azure-openai) (`raindrop-azure-openai` ≥ 0.0.12) | Exact | The `tools=` (or legacy `functions=`) kwarg of each wrapped `chat.completions.create()`, sync, async and streaming | `tools=` on `RaindropAzureOpenAI` / `create_raindrop_azure_openai`, per client on `wrap(client, tools=[...])` |
| [Pydantic AI](/docs/integrations/pydantic-ai) (`raindrop-pydantic-ai` ≥ 0.0.12) | Exact | `ModelRequestParameters.function_tools` and `output_tools` of each `Model.request()` / `request_stream()`, plus `native_tools` (`WebSearchTool`, ...) as provider-defined | `tools=` on `RaindropPydanticAI` / `create_raindrop_pydantic_ai`, per agent on `wrap(agent, tools=[...])` |
| [DSPy](/docs/integrations/dspy) (`raindrop-dspy` ≥ 0.0.11) | Exact | The LM call's `kwargs["tools"]` seen by a DSPy `on_lm_start` callback installed for the wrapped `forward()`, the native tool list `ChatAdapter(use_native_function_calling=True)` built | `tools=` on `RaindropDSPy` / `create_raindrop_dspy`, per module on `wrap(module, tools=[...])` |

### Sending OTLP directly

If you export spans to Raindrop yourself, set `ai.prompt.tools` on your model spans as a string array of the documents above (one JSON string per tool). Add `ai.prompt.tools.source` when the list is a catalog rather than the exact per-request set. See [OpenTelemetry](/docs/sdk/opentelemetry).

## Reading it back

The attribute is stored on the span as sent. It is available on every model span in the trace view and through the [Query API](/docs/api-reference/overview) trace endpoints, and agent replays use it to reconstruct the tools an agent had available. Replay reads the list as recorded and does not consult `ai.prompt.tools.source`, so for catalog-based integrations it may offer tools the model never received on that particular call.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.