from omni import Inference, EventPythonThe original API. The wheel also installs silicon-omni, its short alias so, and omnid; the first operation starts the daemon if it is not already listening.
Every call omni exposes. Flip to internal at any point to see the flag, the JSON-RPC method or the file write it actually becomes.
The Python wheel has no Python dependencies and contains the daemon plus both terminal names. Rust uses the same product name for its library. You bring the provider CLIs; omni offers only the ones installed and signed in.
pip install silicon-omni
# Rust library, daemon, and terminal client
cargo add silicon-omni@0.7.0
cargo install omni-daemon@0.7.0 silicon-omni-cli@0.7.0from omni import Inference, EventPythonThe original API. The wheel also installs silicon-omni, its short alias so, and omnid; the first operation starts the daemon if it is not already listening.
silicon-omni · soterminalTwo names for the same native socket client. The experimental omni command name was retired in 0.5.
omniddaemonThe persistent Rust process that owns sessions and providers. The Python wheel bundles it; a standalone Rust installation gets it from omni-daemon.
Listens on a private Unix socket under ~/.omni. Python, Rust and terminal clients all speak the same newline-delimited protocol to it, so a conversation is not owned by whichever client opened it.
claude-code-cliClaude CodeServes Anthropic models. Checked with claude auth status.
Runs as one long-lived claude -p --output-format stream-json --input-format stream-json --verbose --dangerously-skip-permissions --disable-slash-commands, with --model and --effort taken from the registry's answer. Memory files are switched off through the environment on every run, so a session means the same thing on any machine.
codex-app-serverCodexServes OpenAI models. Checked with an account/read over the app server.
codex app-server --stdio, spoken to as JSON-RPC over newline-delimited JSON. CODEX_HOME is redirected to ~/.omni/jails/codex, which holds a symlink to your real auth.json and a near-empty config. No MCP server, hook or AGENTS.md is ever loaded.
antigravity-cliAntigravityServes Gemini models, plus the Anthropic and OpenAI models agy resells. Checked with agy models.
agy --output-format stream-json --input-format stream-json --disable-slash-commands --print-timeout 24h --dangerously-skip-permissions --print "". The empty --print goes last on purpose: it takes a value, and anywhere else it swallows the flag that follows it.
All three public surfaces attach to the same daemon and use the same vocabulary. Rust gets a synchronous, multiplexed client; the terminal gets the same client behind two command names.
use silicon_omni::{Event, Inference};
fn main() -> silicon_omni::Result<()> {
let inference = Inference::connect()?;
let mut chat = inference.load_or_create_session("nightly-triage", None);
chat.model("code")?.start()?;
chat.send("what changed today?")?;
for event in chat.events() {
let event = event?;
if event.event_type == Event::TEXT { println!("{}", event.text); }
if event.event_type == Event::END { break; }
}
chat.detach()?;
Ok(())
}Inference::connect()→ InferenceConnects to the daemon for this OMNI_HOME, starting omnid if needed. The Rust client currently uses Unix-domain sockets, so it supports macOS and Linux.
One reader thread multiplexes replies and session streams over one connection. Independent requests may finish out of order; requests for one session keep their wire order.
inference.load_or_create_session(id, providers)→ ChatNames a durable session and returns a lazy Chat handle. chat.model('code')?.start()? applies queued settings and attaches; start_since(-1) asks for only new Events.
The subscription is installed before the open request is sent, so replay or an early live frame cannot slip between the reply and the listener.
chat.send(text) · .events() · .detach() · .stop()Sends, reads ordered Events, leaves the shared session warm, or ends it. detach and stop deliberately mean different things.
chat.history() · .history_since(since) · .refresh()Reads persisted Events or refreshes the daemon-owned Chat snapshot.
silicon_omni::raw→ advanced transport APIKeeps Client, OpenOptions, Session, Frame, and Request available for integrations that need the wire itself; application code begins with Inference.
silicon-omni chat · so send · so logsStarts an interactive Chat, sends and streams one settled turn, or follows the complete live Event log. Use --key code, or --intelligence 7 with an optional --bench, or --model with --provider, --effort and --fast. Saying two of the three is refused rather than resolved by precedence. --since gives the first sequence to replay.
so history · sessions · providers · dial · account · daemonReads persisted Events once, or inspects live sessions, available logins, routing, quotas, and the daemon itself. --json emits one Event per line; --frames opts into transport envelopes and snapshots.
0.6 spent one release asking for three weights and a board. It is gone: weighting three axes produced answers that were defensible and wrong. --intelligence <0-10> means a dial position again, as it did in 0.5, and sits beside --key and --model. 0.7 removed the 0.4 aliases --level, --from, events and attach rather than carrying them a fourth release.
The front door, and the only object you import besides the events.
from omni import Inference, Event
PROVIDERS = Inference.get_available_providers()
chat = Inference.load_or_create_session("nightly-triage", PROVIDERS)Inference.get_available_providers(limit_to=None)→ list[str]Lists the providers whose CLI is installed and logged in, in a stable order. Pass limit_to to narrow what omni may use.
The daemon checks for the binary, then asks the provider's own account protocol. A yes is remembered for 10 minutes, because every probe means running a CLI. A no is trusted for 10 seconds, so a repaired login returns quickly.
Inference.load_or_create_session(id, providers=None)→ ChatNames a session, creating it on start if this is the first time. Several programs may attach to the same id; every one hears the ordered events, and any one may send.
Constructs a lightweight Python Chat. start() opens it over the daemon socket; the daemon registry owns the single live engine for that id and adds this connection as another listener. SessionBusy remains importable for compatibility but is no longer raised.
Inference.dial(providers=None)→ dictReturns what the registry answers — by key, by number, or for a model you name — for this provider set. The same call omni routes by.
Inference.sessions() · Inference.daemon()→ list · dictLists the sessions the daemon is holding open, or reports the daemon's version, pid and home.
Inference.claude_code_cli · .codex_app_server · .antigravity_cli→ AccountGives the three account handles: installed, auth_status, available, start_auth(), finish_auth(), limits.
Each property is a typed socket request. The daemon owns account probes and their cache, so every client sees the same answer.
Once attached, settings are durably written when you call them and applied at the next turn boundary. Nothing changes mid-turn. The one exception is send, which is injected where you sent it.
chat.start()→ ChatAttaches, applies settings queued before the open, and starts hearing events. since=0 replays the whole log; since=-1 hears only new events.
A warm open adds a listener and replays the requested range. A cold open waits for durable settings and one eager provider launch before the daemon replies, so the next send is never trapped behind startup.
chat.send(text)Opens a turn, or lands inside the one already running. Waits for durable daemon acceptance, not for model output or END. Safe from any thread, including an event handler. Raises if the chat is stopped.
Crosses the Unix socket and enters a transactionally rewritten metadata FIFO before the reply. A correlated START or INJECTED event proves delivery and removes it. claude gets a {"type":"user"} line; codex gets turn/steer or turn/start; agy gets {"event":"user"}.
chat.stop()Ends the shared session and shuts its providers down. Safe to call from inside a handler.
The daemon acknowledges the stop only after it has committed the lifecycle change. An open turn gets one durable END; if the provider emits none, omni writes an interrupted, stopped one.
chat.detach()Stops this client listening without ending the conversation. Reopen the id during the default 15-minute grace period and its active provider is still warm.
The daemon removes only this connection's listener. Process exit has the same ownership effect; an idle session with no listeners is reaped after OMNI_IDLE_SECS, 900 seconds by default.
chat.history(since=0) · chat.refresh()→ list[Event] · dictReads persisted events from a sequence number, or refreshes the local status snapshot from the daemon.
chat.status→ strReports idle before start, then busy / waiting, then stopped.
chat.idle→ boolTrue when the chat is waiting and nothing is queued. This is the one a polling loop should check.
chat.model(key) · (intelligence=n, bench=…) · (model=…, provider=…, effort=…, fast=…)a word, a number, or a modelSays what should answer. A key is a shortlist chosen by hand; a number is the dial; a model is passed to the CLI verbatim. May change model, effort and vendor at once.
Records the wish and returns. At the next turn end the wanted (provider, model, effort, config) is compared with what the runner was built with. If only model or effort differ on the same provider, the runner is re-tuned in place; anything else is a restart.
chat.active_inference_providers([...])Narrows which providers omni may route to.
Changes which dial is asked for, since there is one dial per set of providers.
chat.system_prompt(text)Replaces the provider's own session prompt.
claude --system-prompt; codex baseInstructions on thread start. agy has no equivalent, so it is folded into the front of the first message and a config event records that it was approximated.
chat.system_prompt_file(path)Reads the same from a file.
chat.append_system_prompt(text)Keeps the provider's prompt and adds to it.
claude --append-system-prompt; codex developerInstructions.
chat.append_system_prompt_file(path)Reads the same from a file.
chat.enable_subagents()Lets the provider spawn its own. Off by default: a chat starts quiet and you opt in, never out.
Quiet means claude gets --disallowedTools "Agent(*)" plus CLAUDE_CODE_DISABLE_WORKFLOWS=1, and codex gets --disable apps --disable plugins -c agents.enabled=false; because skills live outside CODEX_HOME they are additionally switched off one at a time over skills/config/write. -c project_doc_max_bytes=0 is sent whichever way this is set, so opting in cannot bring an AGENTS.md along with it. agy gets --disable-slash-commands, which switches its skills off but not its subagents, and it has no way at all to stop a GEMINI.md or AGENTS.md in the working directory being read — so a quiet chat is quieter on the other two.
chat.enable_mcp()Lets the provider load its MCP servers and connectors. Off by default.
Quiet means claude gets --strict-mcp-config --setting-sources "". codex is isolated by its jail either way. agy is not: omni passes it no environment today, so it loads whatever MCP servers and plugins the machine has. agy does honour HOME, so the same jail trick works on it — it is a gap in omni, not in the CLI.
chat.disable_subagents()States the default out loud.
chat.disable_mcp()Likewise.
chat.disable_autoremoving_unauthenticated_providers()Stops dropping a provider that loses its login mid-run. The dropping is on by default; see Auth.
chat.enable_autoremoving_unauthenticated_providers()Restores the default auth-failover behaviour.
chat.cwd(path)Sets where the provider runs its tools. Pinned to the session.
Resolved through symlinks first. Claude names its session file after the working directory, and on macOS /var and /tmp are links, so an unresolved path files the session somewhere omni would never look again.
@chat.on_eventDecorator. Delivers every event as it happens, on one thread, in order.
A handler that raises is reported as an ERROR of kind handler and stepped over. It cannot take the run down.
@chat.logsDecorator. Delivers the same complete daemon Event stream as on_event, plus a local ERROR/handler if one of this Python client's callbacks raises.
Callbacks run in order on this Python session's dispatcher. A failed event handler becomes a local handler error for log subscribers; a failed log handler is printed and cannot affect the daemon.
One versioned type for everything. The same ordered daemon Events go to every attached Python, Rust, and terminal client and onto disk, so the session file is the event log and there is never a second schema.
Event.STARTtextThe message that opened this turn.
Event.TEXTtextOne finished assistant message.
Event.THINKINGThe model is reasoning. Never what it thought.
Reasoning is signed or encrypted per vendor and cannot be replayed anywhere else, so omni records that it happened and drops the content, including out of your logs. There is no flag to keep it.
Event.TOOL.CALLtool, args, idA tool was invoked.
Event.TOOL.RESULTtool, id, result, okIt came back.
Event.ENDThe turn is over.
Event.INJECTEDtextA message that landed inside a running turn.
Event.ERRORerror, kindauth · limit · unavailable · crash from the model or its CLI, plus stderr for CLI chatter, omni when the engine failed, and handler when your own callback raised.
Every provider words its failures differently. omni sorts them into the only four answers that change what you do: log in, wait, retry, or read a stack trace.
Event.SWITCH_PROVIDERprovider, extra.fromThe conversation moved.
Event.NEW_SESSIONextra.nativeA provider opened one of its own.
Event.CONFIGtextA setting changed, or omni did something worth writing down: launch · retune · reseed · provider_removed · unsupported · approximated · stop.
event.seq→ intIts position in the session log. It only goes up, and it never repeats, across every provider the conversation has passed through.
This is how omni knows what a provider still has to be told. The meta file records the last seq each one saw, so coming back replays exactly what was recorded since, and nothing twice.
Fields: v, type, session, provider, model, text, tool, id, args, result, ok, kind, error, at, seq, turn, native, extra. Python exposes event.type; Rust exposes event.event_type because type is reserved but serializes it as type. kind only classifies an error. v is the on-disk schema version; turn groups cross-provider activity; native retains provider ids without pretending they are portable history. Anything at its default is left out rather than written as null.
The session file is the source of truth and it outlives every provider and client. The daemon owns live state; everything else on disk is durable bookkeeping.
~/.omni/sessions/{id}.jsonlThe conversation, as an append-only, data-synced event log. Never rewritten.
A live session keeps one validated appender open. Every record still crosses sync_data before publication; a torn tail is repaired, while complete corruption is preserved and refuses the session.
~/.omni/sessions/{id}.meta.jsonDurable settings, accepted messages awaiting delivery, and each provider's own session id and contiguous log watermark.
pending is the FIFO behind send acceptance. synced advances only across contiguous history that provider has seen. Identical metadata is not rewritten.
~/.omni/omnid.sock · omnid.pid · omnid.logThe private Unix socket, daemon identity, and daemon diagnostics shared by every client.
~/.omni/jails/codexCodex's stripped, shared CODEX_HOME.
~/.omni/cache/choose.jsonThe answer, keyed by what was asked and which providers are signed in, kept an hour.
Consecutive turns on one provider keep one live runner and native conversation. A switch may pay activation cost. Arriving somewhere new gets only what it missed; coming back resumes its own session and catches up. Codex can catch up in place, Antigravity carries missed history in its next message, and stale Claude resumes through a replacement process.
The 0.5 performance contract is steady-state warmth: consecutive turns on Claude, Codex or Antigravity reuse one runner and one native conversation. Switching providers may cost an activation.
live workloadTeach Claude five facts over five messages, then Codex five, then Antigravity five, then ask Claude for the exact compact fifteen-fact array. Five fresh, alternating trials per version.
correctness · 0.3 and 0.5→ 5/5 · 80/80Both versions passed every session and turn with exact ACK teaching replies and exact final recall. PID and native identity stayed fixed within every five-turn provider group.
whole workflow · median→ 49.543s → 44.826s0.5 was 9.5% faster than 0.3 across the complete sixteen-turn workflow; the mean improved 6.6%.
0.5 · first turn → hot turns 2–5Claude 2.663s → 1.368s (1.95×); Codex 3.288s → 1.184s (2.78×); Antigravity 13.727s → 1.940s (7.08×).
Stable PID and native identity are the decisive heat signal. The remaining 1.2–1.9 seconds for a tiny ACK is provider inference, not a process boot.
hot 0.3 → hot 0.5Claude changed 1.251s → 1.368s, Codex 1.208s → 1.184s, and Antigravity 1.808s → 1.940s. Mixed directions mean 0.5 is not claiming a blanket cloud-inference speedup.
Python send() acceptance→ 0.033ms → 10.801msThe contracts differ: 0.3 enqueued in-process; 0.5 crosses IPC and returns after durable journaling. That roughly 11ms local commit is small beside a model turn.
The timed client was Python. Rust and the CLI use the same daemon engine but were not scored as separate inference benchmarks. These are five descriptive live trials with frozen CLI/model versions; method and results live at benchmarks/results/2026-08-25-v0.3.0-v0.5.0.md in the silicon-omni repository.
omni drives each CLI's own login, so you never have to open the CLI yourself.
Inference.claude_code_cli.auth_status
print(Inference.claude_code_cli.start_auth()) # the url to open
Inference.claude_code_cli.finish_auth("code-or-redirect-url")start_auth()→ strBegins a login and hands back the URL to open.
Spawns the CLI's own login, watches its output for a URL and returns it. If nothing usable appears, whatever it printed comes back verbatim, along with the command to run by hand.
finish_auth(code)→ strTakes the code or redirect URL back, then re-checks.
Codex runs its own browser callback, so there this waits for an account/login/completed notification instead of typing anything, and the code you pass is ignored.
a login that dies mid-runThe provider is dropped from that Chat, the error is reported, and the same intelligence value is resolved again over whoever is left. The conversation carries on somewhere else, at an intelligence value that now means something slightly different.
An ERROR/auth closes the turn, emits CONFIG/provider_removed, then relaunches, which reports as a SWITCH_PROVIDER. The failed turn is not replayed: it is in the log for the next provider to read, but re-driving it could re-run a tool that has already run. Switch the whole behaviour off with chat.disable_autoremoving_unauthenticated_providers() and the error ends the turn and nothing else.
Asking costs no tokens on any of the three. used comes back a fraction and reset an RFC3339 UTC string, whatever each one answers natively: one of them counts seconds since the epoch, and you never have to know which.
Inference.codex_app_server.limits
# {'5h': {'used': 0.0, 'reset': '2026-08-21T14:31:07.000Z'},
# '7d': {'used': 0.16, 'reset': '2026-08-21T10:53:25.000Z'}}claude-code-cliSome enterprise plans expose no windows at all. used is then None, and not zero — the difference matters.
A get_usage control request over a throwaway stream-json process. Costs nothing and needs no credentials of omni's own.
codex-app-serverRead by window duration, never by position.
account/rateLimits/read. primary is not always the 5-hour window; on some plans it is the weekly one. So omni branches on windowDurationMins, where 300 is the 5h and 10080 the 7d.
antigravity-cliReports remaining per model group. omni reports used, worst group first.
agy -p /usage, which the CLI answers itself, so no model is called and no quota is spent.
A provider that needs no CLI, no login and no quota, and answers the same way every time. It is not registered until you ask for it, so it cannot turn up in a real run by accident.
from omni import Inference
from omni.providers import test
test.install()
chat = Inference.load_or_create_session("t", ["test"])
chat.start()
chat.send("hello") # -> TEXT 'echo: hello'test.install()→ list[str]Registers the provider and pins a fixed answer for it, however it is asked for.
The Python call crosses the real socket and registers the Rust test provider inside omnid, then pins a dial for it. Routing, persistence and events run through the shipped engine without reaching the network.
[tool:NAME]Runs NAME, and emits a TOOL.CALL and a matching TOOL.RESULT.
[recall]Replies with everything it was told before this message.
Including history it was only ever seeded with, never sent, which is what makes it useful for testing a provider switch without switching provider.
anything elseReplies echo: <what you sent>.
The library's own live suite runs against the real ~/.omni rather than a sandbox, because a test that uses different paths from a real run is not testing a real run. scripts/cleanup.py in the repo takes its sessions, jails and working directories back out afterwards.
omni contains the name of no model and does none of this choosing. It sends a key, a number, or a model it was given, plus which CLIs are signed in, and hands the strings that come back to a CLI without reading anything else out of them.
GET https://omni.teamofsilicons.com/choose.json?key=code&providers=claude-code-cli+antigravity-cliOMNI_REGISTRYPoints omni at your own registry. The contract is one GET with a providers query.
The answer is cached under ~/.omni/cache for an hour, keyed by the set of providers. A dial fetched once is reused even after it has expired, so a machine that has run before keeps working offline.
NoDialRaised when the registry has never been reached and nothing is cached. No model list ships in the wheel as a fallback, so this is a hard stop and not a degraded mode.
A model list baked into a release is a list going stale, and quietly recommending last quarter's best buy is worse than refusing to answer.