multichat-sdk (Python)
Change one import. Your code keeps calling OpenAI or Anthropic directly, with your own key, and multiChat gives it a durable conversation store, failover to a backup model, and the planner's savings shown before anything is changed.
from multichat import OpenAI # was: from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY as always; MULTICHAT_API_KEY for the store
r = client.chat.completions.create(model="gpt-5.1", messages=[{"role": "user", "content": "Hello"}])
multichat.OpenAI, AsyncOpenAI, Anthropic and AsyncAnthropic are subclasses of the official
clients: every argument, method and return type is theirs. Around each chat.completions.create
(OpenAI) or messages.create / messages.stream (Anthropic) call the SDK:
- finds the call's conversation (one store session per conversation) by matching the message
list against what it recorded before, or by the
conversation=handle you pass; - sends only the new messages to the store as SYNC lines, in the background (the provider call is never delayed or broken by the store; lines wait and go with the next turn if it is down);
- records the reply and its usage (
input,output,cache_read,cache_write,thinking, time, and who answered); - asks the planner what it would change (
optimise="preview", the default) and shows it inclient.multichat.last.changesand on themultichatlogger, without changing what you send; - with Anthropic prompt caching and
keepalive=on, schedules the cache keep-alive on the queue and cancels it when your next turn starts.
Nothing is routed through multiChat: the request goes from your process to the provider.
Install
pip install "multichat-sdk[openai]" # or [anthropic], or [all]
Python 3.9+. The import name is multichat. (An unrelated PyPI package called multichat
installs the same import name; do not install both.)
Sign in
MULTICHAT_API_KEY: an API key from the portal (for servers and CI), ormultichat login(orpython -m multichat login): signs in through the portal in your browser and saves a token to~/.multichat/credentials.json(mode 0600).
multichat_api_key= on the client wins over both. multiChat never breaks your app: without a
credential, or when the multiChat account is invalid or out of credit (the store answers 401, 402
or 403), the client calls the provider directly and records nothing, with one warning on the
multichat logger. record=False does the same on purpose (failover still works). Only the
explicit multiChat calls (advice, spend, files, forks) raise NotLoggedInError. Service addresses: MULTICHAT_STORE_URL, MULTICHAT_QUEUE_URL, MULTICHAT_PLANNER_URL.
Conversations
Calls are grouped by their message lists: a call whose messages start with what a conversation recorded (or, for a trimmed history, contain its last reply) continues it; anything else starts a new session. To be explicit:
conv = client.multichat.conversation() # or conversation("<session ID>") to continue one
client.chat.completions.create(model=..., messages=msgs, conversation=conv)
print(conv.session_id, conv.turn)
client.multichat.last is the last turn's TurnInfo: session_id, turn, model,
answered_by, usage, ms, sent (the body that went to the provider), changes, x, y.
client.multichat.flush() waits until the store has everything; close() and with do it too.
Optimise
optimise="preview"(default): the planner's proposal (model on the flip ladder, compaction, cache markers, with the X vs Y cost) is fetched after each turn and shown; nothing changes.optimise=True: the turn is stored first and the assembled request from the store is sent instead of yours (TurnInfo.sentandchangesshow exactly what and why). If the store is unreachable, your own request goes.optimise=False: no planner call.
Set it on the client or per call (create(..., optimise=True)).
Advice and guardrails
from multichat import OpenAI, Guardrails, GuardrailError
client = OpenAI(guardrails=Guardrails(max_session_usd=2.0, max_turn_usd=0.25, max_context_tokens="session"))
advice = client.multichat.advice() # {spend, findings: [{title, cost_usd, saving_usd, fixes}]}
for fix in advice["findings"][0]["fixes"]:
client.multichat.apply_fix(fix) # an SDK option, or a fork with better settings
A limit passed raises GuardrailError before the provider is called (on_limit="warn" sends
anyway). stable_tools=True sorts tools by name so an order change does not break the prompt
cache. Details: docs/v2/advice.md.
Failover
from multichat import OpenAI, Anthropic, Backup
client = OpenAI(failover=Backup(client=Anthropic(), model="claude-sonnet-4-5", after_ms=3000))
r = client.chat.completions.create(model="gpt-5.1", messages=msgs) # always a ChatCompletion
- The primary starts at once. If it has sent no token after
after_ms, the backup starts too; the first to answer is used. A rate limit, overload (429, 529, 503...), timeout or connection failure starts the backup at once; other errors (a bad request, a bad key) are raised as they are. - The request is converted locally (OpenAI and Anthropic messages, images, documents, tool calls
and tool results;
convert="service"uses the planner's/v2/convert), and the answer comes back in the primary's shape: aChatCompletionfor OpenAI, aMessagefor Anthropic, or their stream events when you streamed (the backup's answer then arrives whole as a short stream). - Per call:
create(..., failover=Backup(...)), orfailover=Noneto turn it off. Backup(params={"max_tokens": 1024})adds fields for the backup only. Sampling settings (temperature,top_p) are not carried to Anthropic, whose current models do not take them.- Queue mode:
Backup(..., mode="queue", key_mode="held")asks the multiChat queue to send the backup atafter_mswith your backup key (held encrypted, deleted once fired), orkey_mode="webhook", webhook="https://..."to have your endpoint send it. The primary's first token cancels it. If the queue cannot be reached the in-process timer is used.
Keep-alive (Anthropic)
client = Anthropic(keepalive=KeepAlive()) # or keepalive=True
When a request carries cache_control, the queue sends the exact request again (max_tokens=1)
at send time + TTL - 30 s (TTL 5 minutes, or 1 hour), and again a TTL later while the session's
enhanced cache extensions last (the queue keeps the count, so a chain never goes past what the
session allows), so the cache is warm for your next turn; that turn cancels the chain. A client
pointed at another deployment (base_url) has its keep-alives and queue-mode backups sent there. The queue needs your key: it is held
encrypted until the task fires (key_mode="held"), or use key_mode="webhook". Off by default.
Keeping the official client
import multichat, openai
client = openai.OpenAI()
messages = multichat.messages(client=client) # a list the official client takes as is
messages.add_prompt("Describe this", images=["https://example.com/cat.jpg"])
r = client.chat.completions.create(model="gpt-5.1", messages=messages)
messages.add_response(r) # appended and recorded
Session(client, model=..., optimise=..., session_id=...): one conversation in the store.Messages(session): the session's messages in the client's format (Anthropic'ssystemkept inmessages.system);add_system,add_prompt(text, images=, documents=),add_response(r),add_tool_result(call_id, output),render("anthropic")for the other provider.Model(name, session): the name to send; withoptimise=Truethe planner's model for the turn.turn(session, failover=...):
async with multichat.turn(session, failover=multichat.Backup(client=claude, model="claude-sonnet-4-5")) as t:
t.messages.add_prompt("And in French?")
r = await t.call(client.chat.completions.create) # calls with t.model and t.messages, records the reply
t.add_response(r) records a reply you got yourself. A turn that closes without a recorded
reply warns (RuntimeWarning).
Workspace (run_code)
Give the model a private workspace per conversation: Python or bash, a /work folder kept for 24
hours, the account's files in (files_in) and every file the run makes saved to the account's
files. Answer its run_code calls with Workspace.handle:
from multichat import OpenAI, Workspace, workspace
client = OpenAI()
ws = Workspace() # MULTICHAT_WORKSPACE_URL overrides the address
reply = client.chat.completions.create(model="gpt-5.1", messages=msgs, tools=[workspace.openai_tool()])
for call in reply.choices[0].message.tool_calls or []:
if call.function.name == workspace.TOOL_NAME:
result = ws.handle(session_id, call.function.arguments) # the tool result text
workspace.anthropic_tool() for Anthropic. The call and its result are recorded with the next
turn, like any tool call. Details: docs/v2/workspace.md.
Files
Your files live in your account's regional bucket; conversations hold a reference (mcfile:<id>),
and the store gives providers a short-lived signed link, or the bytes inline for a provider that
does not fetch links.
f = client.multichat.files.upload("chart.png", session=client.multichat.last.session_id)
url = client.multichat.files.link(f["id"], ttl=600)["url"] # at most 7 days
client.multichat.files.save_to_project(f["id"], "launch") # kept when the chat is deleted
Inline images, audio and documents in your requests still go to the provider exactly as written;
the store records the uploaded file's reference instead of the bytes (record_files=False keeps the
bytes). A file tied to a conversation is deleted with it unless saved to a project.
Conversion
multichat.convert_request(req, "openai.chat", "anthropic.messages", model) and
multichat.convert_response(resp, from_format, to_format) are the converters failover uses.
Tests
uv run pytest
Unit tests only: the services are an in-memory transport and the providers are fake servers behind the official clients' own mock transport. Nothing reaches a network or a provider.
Licence
Apache License 2.0: see LICENSE, with the attribution notices in NOTICE.
Copyright 2026 Digital Deep Ltd.
Metadata
Release files for multichat-sdk 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| multichat_sdk-0.2.0.tar.gz | 73.5 kB | With provenance | Details for multichat_sdk-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| multichat_sdk-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for multichat_sdk-0.2.0-py3-none-any.whl |
Total release size: 138.7 kB
Release files / multichat_sdk-0.2.0.tar.gz
| Download URL | multichat_sdk-0.2.0.tar.gz |
|---|---|
| Size | 73.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7788477b9a2a728352161f5b93c27d227cfa0d230bee8900e3dfd61ab84cb6d4
|
|
BLAKE2b-256 checksum How to use checksums |
c130f510815508ad5340f997d77d4b7ef473067ab3de61b83cb46d64dcc0e02f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / multichat_sdk-0.2.0-py3-none-any.whl
| Download URL | multichat_sdk-0.2.0-py3-none-any.whl |
|---|---|
| Size | 65.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cf3a53b534a3984176cabeff5150c48680135e294054aad1284358573ea06e71
|
|
BLAKE2b-256 checksum How to use checksums |
945327476ca6c9501c1e676f07dca0169cb99b369da8822d0fe61cc619e3fbb8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for multichat-sdk 0.2.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for multichat_sdk-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA 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.