LlamaIndex

Query engines, chat engines, and indexing are recorded. Separating retrieval cost from generation cost, and why indexing needs its own agent.

Recorded with no integration. LlamaIndex's OpenAI and Anthropic wrappers construct the provider clients the SDK patches, and OpenAIEmbedding uses the patched embeddings surface.

pip install llama-index capsera

Query and chat engines

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
import capsera

capsera.init(api_key=os.environ["CAPSERA_API_KEY"])


@capsera.agent("docs-indexer", team="knowledge", task_type="indexing")
def build_index(path: str) -> VectorStoreIndex:
    documents = SimpleDirectoryReader(path).load_data()
    return VectorStoreIndex.from_documents(documents)


@capsera.agent("docs-qa", team="knowledge", task_type="retrieval")
def answer(index: VectorStoreIndex, question: str) -> str:
    return str(index.as_query_engine().query(question))

Coverage

CallRecorded
index.as_query_engine().query(...)Yes
await engine.aquery(...)Yes
index.as_chat_engine().chat(...)Yes
await engine.achat(...)Yes
VectorStoreIndex.from_documents(...) embeddingsYes

Give indexing its own agent

Note that build_index above has a separate agent, which is the recommendation rather than incidental style.

Indexing is usually the largest LLM cost in a RAG system. Embedding a corpus is thousands of calls in a burst, while answering a question is one or two. Combined into one agent, a re-index is indistinguishable from a busy day of queries, and re-indexing on every deploy multiplies the bill invisibly.

Separate agents make the two comparable, and task_type="indexing" against task_type="retrieval" makes them filterable.

Retrieval and generation

A query engine makes embeddings calls to find documents, then a completion to answer. Under one decorator both are attributed to the same agent, which is usually correct.

To split them:

with capsera.tag("retriever", task_type="embedding"):
    nodes = retriever.retrieve(question)

with capsera.tag("synthesizer", task_type="generation"):
    answer = synthesizer.synthesize(question, nodes)

Embeddings bypass routing and budget enforcement, so a blocking budget will not stop an indexing run. To cap indexing, count documents before starting or guard the call yourself.

Global Settings

LlamaIndex configures models globally:

from llama_index.core import Settings
from llama_index.llms.openai import OpenAI

Settings.llm = OpenAI(model="gpt-4o-mini")

This is recorded like any other client. Settings.llm can be assigned before init() runs and calls through it are still recorded, because the patch is on the class the object was built from.

Streaming

streaming=True on a query engine produces a streaming response, and coverage follows the underlying provider. OpenAI and Anthropic streaming is recorded. The synchronous-only providers are not. A stream abandoned before completion has no usage to record. See Troubleshooting.

Which provider is recorded

The wrapper you used. llama_index.llms.openai records openai and llama_index.llms.anthropic records anthropic. An OpenAI wrapper pointed at a compatible vendor records that vendor by base URL rather than the wrapper's name.