core

How rishi registers its backends with Urai, plus the few helpers that belong to rishi itself.

source

split_model_path

def split_model_path(
    model, model_path:NoneType=None
):

Split a model name, minus any runtime prefix, into (model_id, model_path): a Hub repo id or a local file path.


source

RishiToolLoop

def RishiToolLoop(
    *args, **kwargs
):

The base class for rishi backends that drive Urai’s tool loop, with the usage, tool-reminder and sliding-window callbacks on by default.

The shared layer

Backend modules import the shared types, such as Chat, ChatOpts and Resp, from urai directly. The tests below check that importing rishi.core registers the runtimes without copying Urai’s namespace.

RishiToolLoop is the base class for the backends that drive Urai’s tool loop: llama.cpp, MLX, Ollama, the hosted API, Claude Code and Copilot. LiteRT-LM is the exception, because its engine runs tools itself. The class switches on three stock callbacks, which count token usage, remind the model which tools it has, and slide old messages out when the context window fills. split_model_path turns a model name into either a Hugging Face repo id or a local file path, which is the first thing each local backend needs to know.

import importlib, importlib.util
import rishi, rishi.core, urai

test_eq(rishi.Chat is urai.Chat, True)
test_eq(hasattr(rishi.core, 'Chat'), False)
for mod in ('llama', 'mlx', 'remote', 'claude', 'copilot', 'ollama'):
    if mod == 'mlx' and not importlib.util.find_spec('mlx'): continue   # Apple Silicon only
    public = importlib.import_module(f'rishi.{mod}').__all__
    assert 'Chat' not in public and 'UsageStats' not in public
test_eq(split_model_path(None), (None, None))
test_eq(split_model_path('mlx-community/Qwen3-4B-4bit'), ('mlx-community/Qwen3-4B-4bit', None))
test_eq(split_model_path('mlx/mlx-community/Qwen3-4B-4bit'), ('mlx-community/Qwen3-4B-4bit', None))
test_eq(split_model_path('model.gguf'), (None, 'model.gguf'))
test_eq(split_model_path('repo/id', 'given/path.gguf'), ('repo/id', 'given/path.gguf'))

A SafePyRun patch

Urai can run the Python code a model writes, through SafePyRun. SafePyRun up to version 0.2.6 runs each block of code with separate global and local namespaces. A function defined in the block then cannot see a module imported in the same block: import math followed by a function that calls math.sqrt fails with a NameError. rishi replaces SafePyRun’s runner with one that uses a single namespace, until SafePyRun fixes this upstream.

Model capabilities

Before sending a picture to a model, it helps to know whether the model can see. Urai answers that with a Caps record listing the kinds of input and output a model takes. For hosted models it reads fastllm’s model table. Local models have no such table, so each local runtime supplies a function that inspects what it has. litert_caps reports text, image and audio for every LiteRT-LM model, because LiteRT-LM builds the vision and audio encoders whenever multimodal input is on. ollama_caps_ asks the Ollama daemon, which holds the weights and knows.


source

ollama_caps_

def ollama_caps_(
    model:NoneType=None, model_path:NoneType=None
):

Ask the Ollama daemon what model accepts, or return None when the Ollama backend cannot be imported.


source

litert_caps

def litert_caps(
    model:NoneType=None, model_path:NoneType=None
):

Report text, image and audio input for any LiteRT-LM model, since LiteRT-LM builds both encoders whenever multimodal input is on.

test_eq(litert_caps('anything').inp, ('text', 'image', 'audio'))
test_eq(litert_caps('anything').source, 'runtime')

The seven backends

BACKENDS holds one Runtime record per backend: its name, the class that implements it (imported only when first used), a capabilities function, and the name patterns that identify its models. When Chat gets a model name and no runtime, Urai tries the patterns in this order and takes the first match. The order matters in one place. Ollama comes before llama.cpp, because hf.co/unsloth/Qwen3-4B-GGUF contains gguf but is an Ollama id, not a file for llama.cpp. LiteRT-LM is the default when no model is named.

Claude Code and Copilot have no patterns, because their model names look like hosted ones. Choose them with a claude/ or copilot/ prefix, or with runtime='claude' or runtime='copilot'. The tests below show which names go where. A name that matches nothing raises an error instead of guessing.

test_eq(list(RUNTIMES), ['litert', 'ollama', 'llama', 'mlx', 'remote', 'claude', 'copilot'])
test_eq(infer_runtime('gemma-3n-E4B.litertlm'), 'litert')
test_eq(infer_runtime('hf.co/unsloth/Qwen3-4B-GGUF'), 'ollama')   # asked before llama
test_eq(infer_runtime('qwen3-4b-Q4_K_M.gguf'), 'llama')
test_eq(infer_runtime('mlx-community/Qwen3-4B-4bit'), 'mlx')
test_eq(infer_runtime('gpt-5.1'), 'remote')
test_eq(resolve_runtime('claude-sonnet-4.6'), ('remote', 'claude-sonnet-4.6'))
test_eq(resolve_runtime('claude/claude-sonnet-4.6'), ('claude', 'claude-sonnet-4.6'))
test_eq(resolve_runtime()[0], 'litert')                  # nothing named: the default runtime
test_fail(lambda: resolve_runtime('some-unknown-name'), contains="Can't tell which backend")
test_eq(model_caps('gemma-3n-E4B.litertlm').source, 'runtime')
test_eq(model_caps('gpt-5.1').inp, ('text', 'image'))
test_eq(resolve('gpt-5.1').local, False)
test_eq(resolve('anything.litertlm').local, True)

Compatibility names

Some functions moved from rishi to Urai and were renamed on the way. The old names still work as aliases, such as mk_oai_msg for mk_msg and browser_approval for http_approval, so older code keeps running. New code should use Urai’s names, as rishi’s own backends do.


source

use_system_certs_

def use_system_certs_(
    force:bool=False
):

Deprecated alias for urai.use_system_certs.

test_eq(mk_oai_msg('hi'), {'role': 'user', 'content': 'hi'})
test_eq(mk_oai_content('hi'), {'type': 'text', 'text': 'hi'})
test_eq(runtimes is RUNTIMES, True)
test_eq(_is_path('x.gguf'), True)

Installing the skill

The wheel ships a skill.md that tells a coding agent how to use rishi. mv_skill_md copies it into .agents/skills/rishi/ and .claude/skills/rishi/ at the root of the current git repository, where agents look for skills. By default it only prints what it would do; pass dry_run=False to copy. repo_root finds that root, and returns None outside a repository.


source

mv_skill_md

def mv_skill_md(
    dry_run:bool=True, dir:NoneType=None
):

Copy the bundled skill.md into the repository’s .agents and .claude skill directories, or only print the targets while dry_run is True.


source

repo_root

def repo_root()->pathlib.Path:

Root of the current git repository, or None if not in one.

assert repo_root() is None or (repo_root()/'.git').exists()