Metadata-Version: 2.4
Name: mubit-sdk
Version: 1.0.6
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.6.

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 at https://app.mubit.ai.

| 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". |

Your code needs the key for the job it does. To send model calls through Mubit, it needs an inference key. To manage Mubit from code, it needs a management key. Code that does both needs both.

Only an admin of your organization can create keys. If you aren't an admin, ask an admin of your organization to create the key for you. The console shows a new key once, so copy it when it appears. If you lose a key, create a new one.

### Create an inference key

1. Sign in at https://app.mubit.ai.
2. Open the project the key is for. In the sidebar, select **Mubit keys**.
3. Select **Create key**.
4. Enter a **Name**, such as the name of your app. Leave **Permission** on **Inference: calls this project**.
5. Select **Create key**, and copy the key.

The key works for that project only. For another project, create a key in that project.

You can also get an inference key when you create a project: in **New project**, keep **Create an inference key** selected. The **Connect** page has a **Create key** button too.

### Create a management key

1. Sign in at https://app.mubit.ai.
2. Open **All projects**. In the sidebar, under **Workspace**, select **Management keys**.
3. Select **Create management key**.
4. Enter a **Key name**, and choose a **Role**. The table below shows what each role can do.
5. Optional: choose a **Project scope** to limit the key to one project. The default is **All projects (organization-wide)**.
6. Select **Create management key** again. Select **Copy key**, save the key, and select **Done**.

| Role | What the key can do |
| --- | --- |
| `read` | Read everything except the list of management keys |
| `member` | Also rate calls, review training examples and import past calls |
| `admin` | Everything |

### Set the keys

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

### Neon AI Gateway

Each Neon branch has its own AI Gateway. If a project has a Neon AI Gateway key, Mubit sends the project's OpenAI, Anthropic and Gemini calls to that gateway whenever the project has no key of its own for the provider. Your code stays as it is: it keeps using the base URLs above.

Today, Neon AI Gateway keys are available on the EU shared workspace at https://app.mubit.ai only. Elsewhere the console does not list them.

Add the key in the console. Only an admin of your organization can.

1. Sign in at https://app.mubit.ai.
2. Open **All projects**. In the sidebar, under **Workspace**, select **Provider keys**.
3. Select **Add key**, and choose **Neon AI Gateway** as the **Provider**.
4. In **Gateway URL**, enter your branch's AI Gateway URL: `https://` and the branch's host, with no path, port, user or query after it, such as `https://br-example-123456-api.ai.c-2.us-east-2.aws.neon.tech`.
5. In **Token**, enter a Neon token that starts `nt_live_…` and has the `ai_gateway:invoke` scope.
6. Select **Check and save**. Mubit checks the token with your branch's gateway before it saves anything.

For a call on the OpenAI, Anthropic or Gemini base URL, Mubit uses the first of these that exists:

1. the project's own key for that provider;
2. the project's Neon AI Gateway key;
3. Mubit's starter-credit key, if the project has the starter credit.

The OpenAI-compatible base URLs (`https://api.mubit.ai/compat/<provider>/v1`) never go to Neon. Mubit records a call that goes to Neon under the provider `neon`, and sends the token only to the gateway URL saved with it.

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