LLM Provider Support
Which LLM client libraries the SDK wraps or patches, and what each wrapped call captures.
Python: mubit.init() can patch openai, anthropic, litellm and google-genai in place (instrument=True, the default) or you can wrap one client explicitly with mubit.wrap_openai(client). Node: the SDK never patches classes; wrap each client: const llm = mubit.wrapOpenAI(new OpenAI()). A plain new OpenAI() created after init() is not instrumented.
| Provider | Python (patch or wrap) | Node.js (wrap only) |
|---|---|---|
| OpenAI | openai; mubit.wrap_openai(client) | mubit.wrapOpenAI(new OpenAI()) |
| Anthropic | anthropic; mubit.wrap_anthropic(client) | mubit.wrapAnthropic(new Anthropic()) |
| Google Gemini | google-genai; mubit.wrap(client) | mubit.wrap(client) |
| LiteLLM | litellm (patch only) | โ |
| Vercel AI SDK | โ | ai via mubit.middleware() with wrapLanguageModel() |
mubit.wrap(client) detects the client type and applies the matching wrapper. Rust has no wrappers: read run.context(), build the prompt, and record the outcome from your code.
What a wrapped call captures
For every wrapped call inside a run:
- Pre-call: a
ContextBlockis fetched for the current run and injected into the system message (or as a synthetic system message if none exists). The injection is recorded as a receipt with aninjection_id. - The call itself: sent to the provider unchanged.
- Post-call: the request, response, model name, latency and token usage are sent as a
model.callevent on the run, tagged with theinjection_idso an outcome can be attributed to the units that were injected.
When the run ends (with mubit.run() exits, await mubit.run(fn) resolves, or run.end() is called), the server distils lessons from the run's events under the effective policy. Nothing is extracted client-side.
Verifying instrumentation
Run with MUBIT_LOG=info. On the first injection in a process the SDK prints one line:
INFO mubit: injected 1 unit (inj_591700e9) from env=devmubit status --agent <id> shows events_received, last_injection and last_outcome for the agent, so you can confirm the call was captured after the process exits.
In Python, a patched client class carries _mubit_learn_wrapped = True:
import anthropic, mubit
mubit.init(agent="my-agent")
client = anthropic.Anthropic()
print(client._mubit_learn_wrapped) # TrueIf it is False, the patch did not take; usually because mubit.init() ran after the client was constructed. Patch first, instantiate second, or wrap the instance with mubit.wrap_anthropic(client).
Turning instrumentation off
| Need | Do this |
|---|---|
| No patching at all | mubit.init(instrument=False) and wrap the clients you want |
| Stop patching after the fact | mubit.uninstrument() restores the original client classes |
| Some calls outside the loop | Create a second, unwrapped client for those calls |
| Whole SDK off for a process | MUBIT_DISABLED=1 or mubit.init(disabled=True): every helper returns its typed empty value with reason="disabled", no network |
import mubit, openai
mubit.init(agent="my-agent", instrument=False)
llm = mubit.wrap_openai(openai.OpenAI()) # captured
plain = openai.OpenAI() # not captured: use for cost estimates, evals, utilitiesCapturing a call the SDK cannot wrap
For raw HTTP or an unsupported library, send the model.call event yourself on the current run. The body fields are the ones the wrappers send.
import mubit
with mubit.run("custom-model") as run:
resp_text = call_my_model("estimate cost") # your code
mubit.get_context().client.runs.emit(run.run_id, [{
"kind": "model.call",
"body": {
"model": "my-model",
"messages": [{"role": "user", "content": "estimate cost"}],
"response_text": resp_text,
},
}])Provider-specific notes
OpenAI
- Both sync (
OpenAI) and async (AsyncOpenAI) clients are supported. - Streaming responses are captured incrementally; the full assembled output is recorded.
- Tool/function call traces include the tool name and arguments.
Anthropic
- Both sync (
Anthropic) and async (AsyncAnthropic) clients are supported. messages.createis wrapped; the deprecatedcompletions.createis not.- Tool-use blocks are captured as part of the response.
Google Gemini
- The
google-genaiclient is patched on first import afterinit()(Python) or wrapped withmubit.wrap(client). - Multi-turn chat sessions inherit instrumentation from their parent client.
LiteLLM (Python only)
- LiteLLM's unified
completion()andacompletion()calls are patched. - The provider-specific client behind LiteLLM is not double-wrapped.
Vercel AI SDK (Node only)
- Use
mubit.middleware({ mode: "context" | "query" | "full" })withwrapLanguageModel()rather thanwrapOpenAI(). The wrappers instrument the lower-level provider SDKs, not the AI SDK abstraction. - Full example: see Framework integrations โ Vercel AI SDK.
Adding a provider
Open an issue or a PR against mubit-sdk with the client class name and the call surface that should be wrapped (constructor plus the methods that hit the network). The existing per-provider wrappers are the reference pattern to follow.
Upgrading from mubit.learn.init()
mubit.learn.init(), mubit.auto.instrument() and learn.wrap*() are accepted in 0.14 with a deprecation warning and removed in 1.0. mubit.learn.init(api_key=..., agent_id=...) becomes mubit.init(api_key=..., agent=...); learn.feedback({good}) becomes mubit.outcome({good}). See Migration.