Skip to main content

rishi

rishi lets you use a language model from Python with the same calls, whether the model runs on your machine or on a remote server. Each way of running a model is a backend. Four backends run models locally: LiteRT-LM, llama.cpp, MLX and Ollama. Three backends send the conversation to another program or server: a hosted API such as OpenAI or Anthropic, your Claude Code installation, and GitHub Copilot. Each of these seven has its own message format and its own way to pass images and tool calls.

rishi uses one conversation format for all of them. The conversation is called the history. The history is a Python list of message dicts in the OpenAI format, such as {'role': 'user', 'content': 'hi'}. It is stored in chat.hist. Every backend reads and writes that list. You can start a conversation on a small local model, give the list to a hosted model, and the hosted model continues the conversation.

The parts that all backends share come from Urai, which is published on PyPI as uraiyadal. These are the Chat class, tool calling, streaming, options and recorded replies. rishi adds the seven backends and registers them with Urai. from rishi import * gives you the names from Urai and from rishi.

Install

The base package includes LiteRT-LM and the three backends that send the conversation to another program or server. The other local backends need optional extras. Install only the extras you use:

pip install rishi                # LiteRT-LM, hosted APIs, Claude Code and GitHub Copilot
pip install 'rishi[llama]'       # llama.cpp, for GGUF model files
pip install 'rishi[mlx]'         # MLX, on Apple Silicon Macs
pip install 'rishi[mlx-vlm]'     # MLX vision and audio models
pip install 'rishi[ollama]'      # Ollama; on Linux before Python 3.14 this also installs zstandard
pip install 'rishi[all]'         # every extra above

You can combine extras, as in rishi[llama,mlx]. rishi imports a backend module only when a chat first needs it. A missing extra causes no error until you ask for that backend. The hosted backends use their usual credentials. Hosted APIs need an API key in the environment. Claude Code needs a signed-in claude command. Copilot needs a GitHub sign-in.

Chat

Chat(model) opens one conversation. The model name selects the backend. rishi recognizes a .litertlm build, a -GGUF repo, an mlx-community repo, an Ollama hf.co/ id, and a hosted name such as gpt-5.1. A prefix such as claude/ or copilot/ selects the backend directly. With no model, Chat uses LiteRT-LM and its default model, Gemma 4 E2B. The core page lists the exact rules.

Call the chat with a prompt to send one turn. The call returns the reply, a dict that Urai calls a Resp. Each call appends the prompt and the reply to chat.hist. The second question below is therefore asked with the first exchange in the history. resp_text returns the answer text from a reply. close() frees the model memory.

chat = Chat(gemma4_e2b)
print(resp_text(chat('Give one fact about lobsters.')))
print(resp_text(chat('Give one more.')))
chat.close()
Lobsters are crustaceans, which means they have a hard exoskeleton and eight legs.
Lobsters are known for their ability to change their color and texture to blend in with their surroundings, a behavior called camouflage.

Streaming

Pass stream=True to receive a reply as it is generated. The call then returns an iterator of markdown strings instead of one reply. Urai renders the events of every backend into the same markdown. Thinking and tool calls look the same on every backend. Pass stream='raw' to receive the chunk dicts that the markdown is built from.

chat = Chat(gemma4_e2b)
for markdown in chat('Write a two-line poem about rain.', stream=True):
    print(markdown, end='', flush=True)
chat.close()
Soft drops tap on the pane,
Washing the world clean again.

Images

To ask about a picture, pass a list instead of a string. The list holds the prompt text and the image. The image can be a Path, raw bytes or a PIL.Image. Every backend that accepts images accepts the same list. The model must accept image input. Gemma 4 on LiteRT-LM does. The page of each backend names other models.

chat = Chat(gemma4_e2b)
image = repo_root()/'nbs/images.jpeg'
display(Image.open(image))
print(resp_text(chat(['Describe this image in one sentence.', image])))
chat.close()

A medium-sized, reddish-brown German Shepherd is looking attentively off to the side with its mouth slightly open.

Audio

Pass audio in the same list as the prompt, as a file path or as bytes. The model needs an audio encoder. Gemma 4 on LiteRT-LM has one. Some llama.cpp and MLX models have one. Claude Code, Ollama and text-only models raise an error. They do not drop the clip.

chat = Chat(gemma4_e2b)
audio = repo_root()/'nbs/speech.wav'
print(resp_text(chat(['Transcribe this clip.', audio])))
chat.close()
Dancing in the masquerade, idol truth in plain sight jaded, pop, roll, click, dot, who will I be today or not? But such a tide as moving seems asleep, too full for sound and foam, when that drew from out the boundless deep turns again home, twilight and evening bell and after that.

One conversation across seven backends

Every backend uses the same history format. A conversation can move between backends. The example below has seven legs. Each leg opens a new Chat with messages=history, sends one prompt, and passes the longer history to the next leg. The first leg uses Gemma on LiteRT-LM. Its prompt gives the code word “amber”. The next six legs use Qwen3 on Ollama, Qwen3 on MLX, Qwen3 on llama.cpp, GPT-4.1 through the hosted API, Claude Haiku through Claude Code, and GPT-4.1 through Copilot. Each of them is asked for the code word and answers “amber”. It can know this only from the history.

The example downloads four local models. The last three legs need their credentials.

legs = [
    ('LiteRT', gemma4_e2b, {}),
    ('Ollama', f'ollama/{ollama_qwen}', {'think': True}),
    ('MLX', mlx_qwen, {}),
    ('llama.cpp', llama_qwen, {'n_ctx': 4096}),
    ('cloud', 'gpt-4.1', {}),
    ('Claude', 'claude/haiku', {}),
    ('Copilot', 'copilot/gpt-4.1', {}),
]

history = []
for i, (name, model, options) in enumerate(legs):
    chat = Chat(model, messages=history, **options)
    prompt = (
        'The code word is amber. Start a travel log with one short sentence.'
        if i == 0 else
        f'What is the code word? Add one short sentence saying {name} received this conversation.'
    )
    reply = chat(prompt)
    print(f'{name}: {resp_text(reply)}')
    history = list(chat.hist)
    chat.close()

print([m['role'] for m in history])
LiteRT: Amber awaits.
Ollama: The code word is **amber**. Ollama received this conversation.

Fetching 9 files:   0%|                                                                                                                 | 0/9 [00:00<?, ?it/s]Fetching 9 files: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 9/9 [00:00<00:00, 1912.59it/s]

MLX: The code word is **amber**. MLX received this conversation.

llama_context: n_ctx_seq (4096) < n_ctx_train (40960) -- the full capacity of the model will not be utilized

llama.cpp: The code word is **amber**. llama.cpp received this conversation.
cloud: The code word is amber. Cloud received this conversation.
Claude: The code word is amber. Claude received this conversation.
Copilot: The code word is amber. Copilot received this conversation.
['user', 'assistant', 'user', 'assistant', 'user', 'assistant', 'user', 'assistant', 'user', 'assistant', 'user', 'assistant', 'user', 'assistant']

Backend guides

Each backend has a page for the details that apply only to it.

notebook what it covers
00_core.ipynb how backends are registered and chosen, old names kept for compatibility, installing the agent skill
01_llama.ipynb choosing a GGUF file, GPU offload, KV cache reuse, image and audio input
02_litert.ipynb .litertlm builds, GPU or CPU, the engine’s own tool loop, bench()
03_mlx.ipynb MLX text and vision models, the prompt cache, speculative decoding
04_remote.ipynb hosted providers, generated images and provider-side tools
06_claude.ipynb Claude Code sessions, transcripts, tools on locked-down machines
07_copilot.ipynb signing in, Copilot’s tokens and headers, listing models
08_ollama.ipynb installing and running the daemon, thinking levels, model capabilities

Metadata

Release files for rishi 0.1.60

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

Source distribution (sdist)

Source distribution for rishi 0.1.60
File Size Uploaded Provenance Actions
rishi-0.1.60.tar.gz 93.2 kB Without provenance Details for rishi-0.1.60.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for rishi 0.1.60
File Interpreter ABI Platform Actions
rishi-0.1.60-py3-none-any.whl Python 3 none any Details for rishi-0.1.60-py3-none-any.whl

Total release size: 195.3 kB

Release files / rishi-0.1.60.tar.gz

Download URL rishi-0.1.60.tar.gz
Size 93.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8b19206f70303e53f769fbcbba596e3df118b450df1e9371696b965473776b35
BLAKE2b-256 checksum
How to use checksums
d9d0183c74e6898eec20e9207e79e0a70fed23e555919c3afd32f01ebc9b12f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / rishi-0.1.60-py3-none-any.whl

Download URL rishi-0.1.60-py3-none-any.whl
Size 102.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f0514fab1be39119b9f208c7097efd66f8d5080db25ca857e9b0f012e70783fe
BLAKE2b-256 checksum
How to use checksums
773729c0364fde0688cd9db9562ac4ca22112e0f8523e52330485e6c8ad9b1cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Security for rishi 0.1.60

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.

Release history Release notifications | RSS feed

This release

0.1.60 This release

2 release files No provenance

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