Metadata-Version: 2.4
Name: mubit-sdk
Version: 1.0.2
Summary: Mubit client for Python: model calls through the Mubit relay, the management API, the memory API and the mubit command
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31
Requires-Dist: grpcio>=1.62
Requires-Dist: protobuf>=7.35.1
Requires-Dist: httpx>=0.25
Provides-Extra: import
Requires-Dist: pyarrow>=14; extra == "import"

# mubit-sdk

The Python client for Mubit. Version 1.0.2.

Mubit learns from the model calls your agents already make. Send your OpenAI, Anthropic or Gemini calls through Mubit, and it:

- records each call and groups the calls into tasks;
- collects and rates training examples for each task;
- fine-tunes a smaller model for a task and tests it on examples it never trained on;
- serves a model that passes on a share of the task's traffic first, then on all of it, and rolls it back whenever you ask;
- forms lessons from the traffic.

You see and control all of this in the console at https://app.mubit.ai. This package lets your code and your agents do the same.

## Install

```bash
pip install mubit-sdk openai          # add anthropic if you call Claude
```

`openai` and `anthropic` aren't dependencies of this package: install the ones you use. Python 3.10 or newer.

## Keys

Mubit uses two kinds of key. You create both in the console.

| Key | Starts with | Environment variable | What it's for |
| --- | --- | --- | --- |
| Inference key | `mw_` | `MUBIT_API_KEY` | Sending model calls through Mubit. Each key belongs to one project. |
| Management key | `mwm_` | `MUBIT_MANAGEMENT_KEY` | Managing your organization from code: everything under "Manage Mubit from code". |

```bash
export MUBIT_API_KEY=mw_...
export MUBIT_MANAGEMENT_KEY=mwm_...
```

`Mubit()` reads both keys from the environment. You can also pass them in: `Mubit(api_key="mw_...", management_key="mwm_...")`. The base URL is `https://api.mubit.ai`.

## Send model calls through Mubit

Your code keeps using the provider's own client. Mubit hands you one that already points at Mubit and carries your inference key. Mubit records each call and forwards it to the provider with the provider key stored for your project. Your calls, the tasks they form and the models trained from them then show in the console.

### OpenAI

```python
from mubit import Mubit

client = Mubit()
openai = client.calls.openai()        # a stock openai.OpenAI client

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)
```

### Anthropic

`client.calls.anthropic()` gives you a stock `anthropic.Anthropic` client. You call it exactly as you would call Anthropic directly:

```python
from mubit import Mubit

client = Mubit()
claude = client.calls.anthropic()     # a stock anthropic.Anthropic client

reply = claude.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "Classify this ticket: the invoice total is wrong."}],
)
print(reply.content[0].text)
```

### Async

`AsyncMubit` returns the async clients, `openai.AsyncOpenAI` and `anthropic.AsyncAnthropic`:

```python
from mubit import AsyncMubit

client = AsyncMubit()
claude = client.calls.anthropic()     # anthropic.AsyncAnthropic

reply = await claude.messages.create(model="claude-sonnet-5-5", max_tokens=512, messages=[...])
```

### Client options

`calls.openai()` and `calls.anthropic()` take the provider client's own options, such as `timeout`, `max_retries` and `default_headers`, and pass them through unchanged:

```python
claude = client.calls.anthropic(timeout=30, max_retries=3)
```

### Other providers, or your own client setup

Any provider client works when you point it at Mubit's base URL for that provider and give it your inference key as its API key:

| Provider | Base URL |
| --- | --- |
| OpenAI | `https://api.mubit.ai/openai/v1` |
| Anthropic | `https://api.mubit.ai/anthropic` |
| Google Gemini | `https://api.mubit.ai/gemini` |
| OpenRouter, Groq, Together, Mistral (OpenAI-compatible) | `https://api.mubit.ai/compat/<provider>/v1`, for example `https://api.mubit.ai/compat/groq/v1` |

```python
import os
import anthropic

claude = anthropic.Anthropic(base_url="https://api.mubit.ai/anthropic", api_key=os.environ["MUBIT_API_KEY"])
```

## Manage Mubit from code

With a management key, your code can do everything you do in the console. The one exception is organization members, which you manage in the console.

```python
from mubit import Mubit

client = Mubit()

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

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

client.tasks.train(task_id)                      # train a new version now

version = client.tasks.versions(task_id).data[0]
client.versions.rollout(version["id"], canary_percent=10)   # serve it on 10% of the task's calls
client.versions.promote(version["id"])           # then on all of them
client.versions.rollback(version["id"])          # or go back to the previous version
```

What you can do, by area:

### Organization and projects

| You can | Python |
| --- | --- |
| See your organization | `client.org.get()` |
| List and open projects | `client.projects.list()`, `client.projects.get(id)` |
| Create, rename and delete a project | `client.projects.create(name=...)`, `client.projects.rename(id, name=...)`, `client.projects.delete(id)` |

### Keys

| You can | Python |
| --- | --- |
| List, create, rotate and revoke a project's inference keys | `client.keys.list(project=...)`, `client.keys.create(project=..., name=...)`, `client.keys.rotate(id)`, `client.keys.revoke(id)` |
| List, add, rotate and delete the provider keys Mubit calls providers with | `client.providers.list()`, `client.providers.add(...)`, `client.providers.rotate(id, ...)`, `client.providers.delete(id)` |
| List, create, rotate and revoke management keys | `client.management_keys.list()`, `client.management_keys.create(name=..., role=...)`, `client.management_keys.rotate(id)`, `client.management_keys.revoke(id)` |

### Calls

| You can | Python |
| --- | --- |
| List calls, filtered by project, task, model, status and time | `client.calls.list(...)` |
| Open a call | `client.calls.get(id)` |
| Rate a reply up or down, with a comment or a corrected answer | `client.calls.feedback(id, rating="up", comment=..., correction=...)` |
| Delete everything stored for one session | `client.calls.forget_session(session_id)` |

### Tasks

| You can | Python |
| --- | --- |
| List tasks, and open one: its stage, what it waits for and why | `client.tasks.list()`, `client.tasks.get(id)` |
| Read a task's timeline | `client.tasks.timeline(id)` |
| Find out why a task isn't training or serving yet | `client.tasks.diagnose(id)` |
| Review its training examples: keep or drop each one | `client.tasks.examples(id)`, `client.tasks.review_example(id, example_id, action=...)` |
| Train a new version now | `client.tasks.train(id)` |
| Pause and resume a task's learning | `client.tasks.pause(id)`, `client.tasks.resume(id)` |
| Turn automatic routing to the task's trained models off or on | `client.tasks.set_serving(id, on=False)` |
| Stop all of a task's trained models at once | `client.tasks.stop(id, reason=...)` |

### Trained models (versions)

| You can | Python |
| --- | --- |
| List a task's versions, and open one with its test results | `client.tasks.versions(id)`, `client.versions.get(id)` |
| Run a version silently beside the current model, or serve it on a share of the calls | `client.versions.rollout(id, shadow_percent=..., canary_percent=...)` |
| Promote, demote or roll back a version | `client.versions.promote(id)`, `client.versions.demote(id)`, `client.versions.rollback(id)` |
| Read a task's promotion history | `client.tasks.history(id)` |

### Settings

Your plan sets the outer limits. Within them, you can make a setting stricter or cheaper, but not looser.

| You can | Python |
| --- | --- |
| Read a project's settings, and who set each value | `client.settings.get(project=...)` |
| Change the project settings you see in the console: approval mode, silent test, partial rollout, live checks, grading, when new versions train, the longest example, how long calls are kept and the pass bar | `client.settings.update(project=..., ...)` |
| Fine-tuning, per task: base model, retrain schedule, rows needed to train, the daily rating cap and the rating rubric | `client.tasks.update_settings(id, ...)` |
| Routing, per task: when a trained model takes traffic, the silent-test and partial-rollout shares, the audit rate, and the quality bar it must hold | `client.tasks.update_settings(id, ...)` |
| Lessons, per project: lesson injection on or off, its token budget, and the daily cap on reflections | `client.settings.update(project=..., ...)` |
| Spend: a daily cap on audit spend | `client.settings.update(project=..., ...)` |
| Read the audit of every settings change | `client.settings.audit(project=...)` |
| Read a task's settings | `client.tasks.settings(id)` |

### Lessons, imports, usage and activity

| You can | Python |
| --- | --- |
| List and open what Mubit learned | `client.lessons.list()`, `client.lessons.get(id)` |
| Import past calls from Langfuse, Braintrust or Logfire, follow the import, or cancel it | `client.imports.create(...)`, `client.imports.list()`, `client.imports.cancel(id)` |
| See usage: calls and tokens | `client.usage.get()` |
| See your balance and credit | `client.billing.get()` |
| Read a project's activity | `client.activity.list()` |
| Follow a long-running job, such as a training run or an import | `client.jobs.get(id)`, `client.jobs.wait(id, timeout=...)` |

Answers are the API's JSON as Python dicts.

## Pages, retries and safe repeats

- **Lists come in pages.** A list call returns its first page, as `page.data` and `page.next_cursor`. Loop over the result and the client fetches the next pages for you.
- **Busy or failing calls are retried.** On a 429 or 5xx answer, the client waits (honouring the server's `Retry-After` when it sends one) and retries, up to `max_retries` times (2 by default).
- **A retried write isn't applied twice.** Every POST carries an `Idempotency-Key` header. The client makes one for you, or you can pass your own with `idempotency_key=...`.
- **Async works the same way.** `AsyncMubit` has the same namespaces: `page = await client.tasks.list()`, or `async for task in client.tasks.list():`.

## Errors

Every error the client raises is a `mubit.MubitError`, so catching that one class catches everything. To handle a specific case, catch its subclass:

| What went wrong | Error |
| --- | --- |
| No key, or a bad option | `mubit.ConfigurationError` |
| Mubit couldn't be reached (DNS, connection refused, TLS) | `mubit.APIConnectionError` |
| The call took too long | `mubit.APITimeoutError` |
| The request was malformed (400) | `mubit.BadRequestError` |
| The key is missing or wrong (401) | `mubit.AuthenticationError` |
| The key isn't allowed to do this (403) | `mubit.PermissionDeniedError` |
| The thing doesn't exist (404) | `mubit.NotFoundError` |
| It conflicts with the current state (409), or is gone (410) | `mubit.ConflictError`, `mubit.GoneError` |
| The request is too large (413) | `mubit.PayloadTooLargeError` |
| Too many requests (429), even after the retries | `mubit.RateLimitError` |
| A server error (500 and other 5xx), or Mubit is unavailable (503) | `mubit.InternalServerError`, `mubit.UnavailableError` |

Errors from the API carry `status_code`, `code` (a short reason, such as `not_found`), `request_id` (quote it when you contact support), `retry_after` and `body`:

```python
import mubit

client = mubit.Mubit()
try:
    client.tasks.get("task_triage12")
except mubit.NotFoundError as err:
    print(err.code, err.request_id)
```

Errors from model calls you send through Mubit are the provider client's own, for example `openai.APIError`.

## Related

- **Console:** https://app.mubit.ai
- **JavaScript client:** `npm install @mubit-ai/sdk`
