Skip to main content

mubit-sdk

The Mubit client for Python. Version 1.0.1.

Mubit records the model calls your agents make and trains smaller models from them. You send the calls through the Mubit relay with an inference key, using the stock OpenAI or Anthropic client. Mubit groups the calls into tasks, collects and rates training examples, fine-tunes a smaller model for each task and checks it against held-out examples. A model that passes serves its task on a canary or shadow share of the traffic and can be rolled back; Mubit also forms lessons from the traffic.

The console is https://app.mubit.ai. The management API (/api/v1) lets code and agents do what the console does. This package gives you the relay clients (client.calls), the management API (client.tasks, client.versions and the rest), the memory API of earlier releases (client.memory) and the mubit command.

Install

pip install mubit-sdk openai        # and anthropic for calls.anthropic()

openai and anthropic are not dependencies of this package: install the one you call. Python 3.10 or newer.

Keys and base URL

Key Prefix Environment variable Used for
Inference key mw_ or mbt_ MUBIT_API_KEY model calls through the relay, and the memory API
Management key mwm_ MUBIT_MANAGEMENT_KEY the management API (/api/v1)

The base URL is https://api.mubit.ai (EU, the default) or https://api-us.mubit.ai (US); use the one of your organization's region:

export MUBIT_API_KEY=mw_...
export MUBIT_BASE_URL=https://api-us.mubit.ai     # US organizations only

Mubit(api_key=..., management_key=..., base_url=...) takes the same values as keyword arguments; an argument wins over the environment, and MUBIT_ENDPOINT is still read when MUBIT_BASE_URL is unset. An inference key sent to the management API answers 403 wrong_lane.

Model calls through the relay

from mubit import Mubit

client = Mubit()                              # MUBIT_API_KEY and MUBIT_BASE_URL from the environment
openai = client.calls.openai()                # a stock openai.OpenAI for <base URL>/openai/v1 and the inference key

reply = openai.chat.completions.create(
    model="gpt-5-mini",
    messages=[{"role": "user", "content": "Classify this ticket: the invoice total is wrong."}],
)
print(reply.choices[0].message.content)

client.calls.anthropic() returns a stock anthropic.Anthropic for <base URL>/anthropic in the same way, and AsyncMubit().calls returns openai.AsyncOpenAI and anthropic.AsyncAnthropic. Both take the provider client's own keyword arguments (default_headers, timeout, max_retries and so on) and pass them through unchanged; api_key and base_url replace the defaults. To configure a client yourself, give it the base URL https://api.mubit.ai/openai/v1 (OpenAI) or https://api.mubit.ai/anthropic (Anthropic) and the inference key as its API key.

The relay records each call and forwards it to the provider with the provider key stored for your project. The calls, the tasks they form and the models trained from them show in the console and through the management API.

Management

from mubit import Mubit

client = Mubit()                              # MUBIT_MANAGEMENT_KEY and MUBIT_BASE_URL from the environment

for task in client.tasks.list():                                   # follows next_cursor across pages
    print(task["id"], task["name"], task["stage"], task["active_version"])

task_id = "task_triage12"
calls = client.calls.list(task=task_id, limit=20)                  # one page: calls.data, calls.next_cursor
client.calls.feedback(calls.data[0]["id"], rating="down", comment="Missed the sales tax line.")

train = client.tasks.train(task_id)
print(train["job"]["id"], train["job"]["status"])                  # a queued training job

versions = client.tasks.versions(task_id)
version = versions.data[0]
client.versions.rollout(version["id"], canary_percent=10, shadow_percent=0)
client.versions.promote(version["id"])                             # serve it for all of the task's traffic
back = client.versions.rollback(version["id"])                     # demote it; back["restored_version_id"] serves again
print(back["status"])

The namespaces follow the /api/v1 resources: client.org.get(), client.projects.list() / get(id), client.calls.list() / get(id) / feedback(id, rating=...), client.tasks.list() / get(id) / examples(id) / review_example(id, example_id, action=...) / settings(id) / train(id) / pause(id) / resume(id) / versions(id), client.versions.get(id) / promote(id) / rollback(id) / rollout(id, canary_percent=..., shadow_percent=...), client.lessons.list() / get(id), client.imports.list() / create(...), client.datasets.list(), client.evals.list() / get(id), client.usage.get(), client.billing.get(), client.activity.list(), client.jobs.get(id) / wait(id, timeout=...), client.webhooks.list(), client.management_keys.list() / create(name=..., role=...) / rotate(id) / revoke(id) and client.keys.list() (the inference keys). Answers are the API's JSON as dicts, with its snake_case field names.

A list call returns its first page (page.data, page.next_cursor), and iterating the page follows next_cursor across every page. Each POST carries an Idempotency-Key header (generated unless you pass idempotency_key=), so a retried POST is not applied twice. A 429 or 5xx answer is retried up to max_retries times (default 2), after Retry-After when the server sends one. AsyncMubit has the same namespaces for asyncio: page = await client.tasks.list(), or async for task in client.tasks.list():.

Availability in 1.0

  • Model calls through the relay with an inference key work today.
  • The management API is being turned on organization by organization. Until it is on for yours, every /api/v1 call answers 404 with code not_found (the client raises mubit.NotFoundError). Management keys (mwm_) are not issued yet.
  • Where the management API is on, the server answers the read routes (list and get). The write routes (call feedback, example review, train, pause, resume, promote, rollback, rollout, management keys and imports) answer 501 not_implemented until they ship; pause and resume come in 1.1.

Errors

A call that fails raises a subclass of mubit.APIStatusError with status_code, code (the API's error code, for example not_found, wrong_lane, rate_limited, not_implemented), request_id, retry_after and body:

MubitError
├── ConfigurationError                  no key, no base URL, bad option
├── APIConnectionError                  DNS, refused, TLS
│   └── APITimeoutError
├── APIStatusError(status_code, code, request_id, body, retry_after)
│   ├── BadRequestError (400)  AuthenticationError (401)  PermissionDeniedError (403)  NotFoundError (404)
│   ├── ConflictError (409)    GoneError (410)            PayloadTooLargeError (413)   RateLimitError (429)
│   └── InternalServerError (500 and other 5xx, 501 included)                          UnavailableError (503)
└── UnsupportedFeatureError
import mubit

client = mubit.Mubit()
try:
    client.tasks.get("task_triage12")
except mubit.NotFoundError as exc:            # no such task, or the management API is off
    print(exc.code, exc.request_id)           # "not_found"

Every class in the tree is on the package root. A failed connection to the management API raises MubitError (Connection error: ...). Errors of a call through the relay are the provider client's own (openai.APIError and its subclasses, for example). The memory API adds LessonRejected, ControlError, PolicyError, ControlAuthenticationError, PolicyAuthenticationError and EntitlementError, also on the root.

Memory (the API of earlier releases)

The memory API (/v2) stays for existing users, on the same inference key.

from mubit import Mubit, Scope

client = Mubit()                                                    # MUBIT_API_KEY and MUBIT_BASE_URL from the environment
support = client.with_options(scope=Scope(project="support", env="dev", run_id="ticket-42"))

support.memory.remember("Customer dates are day-first (DD/MM/YYYY): 03/04/2025 means 2025-04-03.",
                        kind="lesson", visibility="global")
answer = support.memory.recall("How should I read the date 03/04/2025?")
print(answer["final_answer"])

client.memory also has context, forget, checkpoint, reflect and the other memory operations, and the loop helpers of earlier releases (mubit.init, mubit.run, run.outcome, mubit.wrap_openai, mubit.wrap_anthropic and the rest) are still on the package root. mubit.Client is the memory client of earlier releases with its resource namespaces (client.memory, client.runs, client.outcomes, client.lessons, client.policy, client.kill, client.jobs, client.proposals, client.snapshots, client.agents, client.projects, client.audit, client.admin, client.keys, client.raw.invoke(op)); on Mubit, lessons, projects, jobs and keys are the management namespaces instead. mubit.Client().keys.delete(key_id) (alias revoke) is DELETE /v2/core/acl/keys/<key_id>. The mubit command (mubit doctor, mubit runs, mubit lessons, mubit import and the rest) works on the memory API. The full memory reference is at https://docs.mubit.ai.

Step routing (tuned vs frontier)

Route individual workflow steps to a tuned model behind an OpenAI-compatible endpoint, with automatic escalation back to your own frontier client. Routing decisions (rollout percentage, escalation triggers, recall) come from the instance's step manifest (GET /v2/adapters/step_manifest) — shifting traffic is a manifest write, not a code change.

import openai
from mubit import Client
from mubit.steps import StepRouter

router = StepRouter(
    Client(api_key="mbt_..."),
    tuned={"base_url": "https://gw.example.com/v1", "api_key": "gw-key"},
    frontier=openai.OpenAI(),  # or {"base_url": ..., "api_key": ..., "model": ...}
    frontier_model="gpt-4o",
)

result = router.chat("step_extract", messages)
result.routed_to       # "tuned" | "frontier"
result.escalated       # True when a trigger sent the call to the frontier
result.correlation_id  # sent as X-Mubit-Step-Correlation to the tuned endpoint

Escalation triggers, per the route's manifest knobs: output-schema failure (a top-level required-key check on JSON output — not full JSON-Schema validation), timeout_ms, tuned-endpoint HTTP and transport errors, and low confidence. When the tuned endpoint returns no logprobs the configured on_missing_logprobs policy fires (default escalate): an absent score is not a passing score, and treating it as one leaves the gate globally inert. Escalations buffer locally; router.flush_events() records them as step outcomes.

Pass user_id= (or session_id=) on every call. It scopes recall to that end user AND pins the rollout bucket, so one conversation stays on one side of the split instead of flipping between the adapter and the frontier between steps — a canary bucketed on the per-call correlation id measures a mixture, not a cohort. Without a scope, recall is skipped and counted: the router never falls back to a shared one, which would put one end user's memories in another end user's prompt.

LangChain / LangGraph recipe

Wrap inside the node body — the place a step actually constructs or receives its model client — never at graph-construction time. One line per step:

def summarize_node(state):
    step_llm = router.wrap_openai("step_summarize")
    resp = step_llm.chat.completions.create(
        model="step_summarize",
        messages=state["messages"],
        user_id=state["end_user_id"],  # scopes recall AND pins the rollout bucket
    )
    return {"summary": resp.choices[0].message.content}

router.chat("step_summarize", state["messages"], user_id=...) is the same call without the OpenAI-shaped shim, and returns result.routed_to / result.escalation_reason for your traces. A step that must keep a ChatOpenAI instance should build it inside the node from the wrapper, so the model client is chosen per call rather than pinned at import time.

Never point a chat client at the tuned endpoint. ChatOpenAI(base_url=<gateway>), openai.OpenAI(base_url=<gateway>) or any other static pointer sends every call to the adapter directly. That client has no fallback and no kill switch: no manifest gating (a route disabled in the control plane keeps serving), no rollout percentage, no escalation back to your frontier model when the adapter times out or returns an unusable answer, no MUBIT_STEPS_DISABLED and no router.disable(), so rolling a bad adapter back needs a redeploy. It also emits no step outcomes and carries no correlation id, so the escalation rate is uncomputable and the savings report stays empty. Route through the router.

Metadata

Release files for mubit-sdk 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mubit-sdk 1.0.1
File Size Uploaded Provenance Actions
mubit_sdk-1.0.1.tar.gz 365.6 kB Without provenance Details for mubit_sdk-1.0.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mubit-sdk 1.0.1
File Interpreter ABI Platform Actions
mubit_sdk-1.0.1-py3-none-any.whl Python 3 none any Details for mubit_sdk-1.0.1-py3-none-any.whl

Total release size: 751.3 kB

Release files / mubit_sdk-1.0.1.tar.gz

Download URL mubit_sdk-1.0.1.tar.gz
Size 365.6 kB
Tags Source
SHA-256 checksum
How to use checksums
3cbb2eae808191ba2f0efae0dfed18d80ade4f79f4608a17d569d1a45e5d4709
BLAKE2b-256 checksum
How to use checksums
be37c450580ea38425a848db16b8bb677e85e5010bfaf94562bcb4a655690f38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / mubit_sdk-1.0.1-py3-none-any.whl

Download URL mubit_sdk-1.0.1-py3-none-any.whl
Size 385.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ac54cb8c4a0cba73862bd9fd3a38f8af60463258089af8f7d5d5ae860f02d206
BLAKE2b-256 checksum
How to use checksums
03cfd21606f682761a898db4ad1e2cdb5201ca1d2e804cd4f2d64975c226eea8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for mubit-sdk 1.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page