Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Build with Mubit

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.

ProviderPython (patch or wrap)Node.js (wrap only)
OpenAIopenai; mubit.wrap_openai(client)mubit.wrapOpenAI(new OpenAI())
Anthropicanthropic; mubit.wrap_anthropic(client)mubit.wrapAnthropic(new Anthropic())
Google Geminigoogle-genai; mubit.wrap(client)mubit.wrap(client)
LiteLLMlitellm (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.

import mubit, openai
 
mubit.init(agent="my-agent")                   # instrument=True patches supported clients in place
llm = mubit.wrap_openai(openai.OpenAI())       # or wrap one client explicitly (works with instrument=False)
 
with mubit.run("review-diff"):
    resp = llm.chat.completions.create(
        model="gpt-5-mini",
        messages=[{"role": "user", "content": "Review this diff..."}],
    )

To patch only some libraries, pass a list: mubit.init(instrument=["openai"]). To patch nothing and wrap explicitly, pass instrument=False.

What a wrapped call captures

For every wrapped call inside a run:

  • Pre-call: a ContextBlock is 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 an injection_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.call event on the run, tagged with the injection_id so 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=dev

mubit 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)  # True

If 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

NeedDo this
No patching at allmubit.init(instrument=False) and wrap the clients you want
Stop patching after the factmubit.uninstrument() restores the original client classes
Some calls outside the loopCreate a second, unwrapped client for those calls
Whole SDK off for a processMUBIT_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, utilities

Capturing 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.create is wrapped; the deprecated completions.create is not.
  • Tool-use blocks are captured as part of the response.

Google Gemini

  • The google-genai client is patched on first import after init() (Python) or wrapped with mubit.wrap(client).
  • Multi-turn chat sessions inherit instrumentation from their parent client.

LiteLLM (Python only)

  • LiteLLM's unified completion() and acompletion() 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" }) with wrapLanguageModel() rather than wrapOpenAI(). 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.