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.
Split a model name, minus any runtime prefix, into (model_id, model_path): a Hub repo id or a local file path.
The base class for rishi backends that drive Urai’s tool loop, with the usage, tool-reminder and sliding-window callbacks on by default.
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 publictest_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'))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.
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.
Ask the Ollama daemon what model accepts, or return None when the Ollama backend cannot be imported.
Report text, image and audio input for any LiteRT-LM model, since LiteRT-LM builds both encoders whenever multimodal input is on.
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")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.
Deprecated alias for urai.use_system_certs.
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.
Copy the bundled skill.md into the repository’s .agents and .claude skill directories, or only print the targets while dry_run is True.
Root of the current git repository, or None if not in one.