How interception works
The SDK patches provider client classes at init() time. Why import order does not matter, how framework calls are captured, and where the limits are.
The patch is applied to classes
capsera.init() imports each provider library that is present and replaces methods on
the class:
anthropic.resources.messages.Messages.create
anthropic.resources.messages.AsyncMessages.create
openai.resources.chat.completions.Completions.create
openai.resources.embeddings.Embeddings.create
Because the patch lives on the class, import order does not matter:
client = anthropic.Anthropic() # constructed first
capsera.init(api_key="cap-...") # patched second
client.messages.create(...) # recorded
client resolves create on its class at call time, and by then the class holds the
wrapper. Instrumenting an application therefore does not require finding every place a
client is constructed.
The limit follows from the same mechanism. A call that executed before init() ran
went through the original method and is not recorded.
What the wrapper does
Around each call, in order:
- Resolve the calling location by walking the stack to the first frame that is not Capsera, a provider library, or a known framework.
- Apply routing, if enabled, which may change the model.
- Run the pre-call budget check, if enabled, which may raise or downgrade.
- Call the provider method.
- Read token counts from the response, compute cost, build an event, and queue it.
- Return the provider's response object unchanged.
Step 6 is a guarantee. For non-streaming calls the SDK does not wrap, proxy, or copy
the response, so code that reads response.usage or indexes response.content
behaves identically with and without instrumentation.
Framework calls need no integration
LangChain, LangGraph, CrewAI, LlamaIndex, AutoGen, Haystack, DSPy, and LiteLLM all call the provider clients listed above. Since the patch is on those classes, framework calls are recorded without any additional setup.
What frameworks affect is step 1. Without help, the first non-Capsera, non-provider
frame is inside the framework, so events would report a location like
langchain/…/base.py instead of your code. The SDK keeps a list of framework paths to
skip so the walk continues to the function you wrote.
A wrapper library that is not on that list can produce a caller_file pointing into a
dependency. The call is still recorded correctly; only the reported location is
unhelpful.
Streaming is handled differently
A streamed call has no usage data when it returns. Token counts arrive with the final chunk or after the stream is drained, so the SDK returns a thin wrapper for streaming calls and records the event once the stream finishes.
This makes streaming the one case where how you consume the response affects whether
the call is recorded. See
Troubleshooting for the specific shapes that
are not recorded. For OpenAI, the SDK also injects
stream_options={"include_usage": True} so the final chunk carries token counts.
Failures are contained
Every step above is wrapped so a failure inside Capsera cannot reach your call path. If a response shape cannot be read, if a pricing lookup fails, or if the queue is full, the call still returns the provider's response. See Reliability.
init() is idempotent
init() can be called more than once. The patch is applied once per class and guarded,
so a second call does not wrap twice and cannot record a call twice. Re-initialising
flushes the previous emitter, so queued events are delivered rather than lost.