Skip to main content

silicon omni

One Python interface for Claude Code, Codex and Antigravity — driven by the subscriptions you already pay for, not API keys.

from omni import Inference, Event

chat = Inference.load_or_create_session("my-session")
chat.intelligence(7)

@chat.on_event
def handle(event):
    if event.type == Event.TEXT:
        print(event.text)

chat.start()
chat.send("what changed in this repo today?")

The interesting part is not that it wraps three CLIs. It is that a conversation can move between them mid-flight and carry on where it left off.


Why

Each vendor ships a good agentic CLI and a subscription that makes it cheap to run. None of them talk to each other. Pick one and you are stuck with its models, its limits and its outage.

omni owns the conversation instead. Providers become interchangeable: raise the intelligence dial and the same chat may finish on a different vendor's model, with everything that came before already in its head.


Install

pip install silicon-omni

Zero runtime dependencies. You bring the CLIs:

provider CLI check
claude Claude Code claude auth status
openai Codex codex app-server
google Antigravity agy models
Inference.get_available_providers()   # ['claude', 'google', 'openai']

Installed and logged in. Anything else is not offered.


The dial

There is no model picker. There is one number.

chat.intelligence(0)    # cheapest thing worth using
chat.intelligence(10)   # best thing you have

Behind it is a graph, and omni is not the one drawing it. Every model the three CLIs can run is plotted by its GDPval-AA v2 Elo — Artificial Analysis' blind pairwise scoring of real economically valuable work, anchored so that a human expert is 1000 — against the dollars they measured it cost to earn that score.

Only the left edge of that graph becomes a dial: a model earns a level if nothing else is both better and cheaper. Level 10 is the top of the edge, and the dial walks down-left from there, so every step down is a real saving and never a sideways move.

lvl    Elo   $/task   model
 10  1844.7  6.7660   claude-opus-5 max
  9  1813.8  4.9630   claude-opus-5 xhigh
  8  1732.8  3.0271   claude-opus-5 high
  7  1678.9  2.1141   gpt-5.6-sol xhigh
  6  1621.2  1.3708   gpt-5.6-sol high
  5  1619.6  1.3641   claude-opus-5 medium
  4  1578.3  0.1022   gpt-5.6-luna max
  3  1525.7  0.0667   gpt-5.6-luna xhigh
  2  1465.8  0.0412   gpt-5.6-luna high
  1  1274.5  0.0133   gpt-5.6-luna medium
  0  1155.6  0.0072   gpt-5.6-luna low

Level 4 is worth staring at: GPT-5.6 Luna at max effort scores within 15% of the top of the board for 66× less money, which is why everything between it and Opus 5 falls off the edge.

There is one dial per set of providers, because losing a vendor puts models back on the dial that another vendor's were shadowing. With fewer providers the edge is shorter and levels start sharing a rung — that is the dial telling you there is nothing in between worth picking.

omni does none of this arithmetic, and knows the name of no model. It asks omni.teamofsilicons.com/intelligence.json for the finished map matching the providers it has, and keeps it in ~/.omni/cache for an hour. model and effort go to the CLI verbatim, so a model released tomorrow needs no release of this package — only a commit to models-gdpval.json in the registry repo. Point somewhere else with OMNI_REGISTRY.

Nothing ships in the wheel as a fallback. A model list baked into a release is a model list that goes quietly stale, and a wrong recommendation is worse than an honest refusal — so a machine that has never reached the registry raises NoDial rather than guessing. One that has run before keeps working from its cache, expired or not.

Events

Everything omni has to say arrives as one Event.

@chat.on_event
def handle(event):
    event.type == Event.THINKING
type carries
Event.START text — the message that opened this turn
Event.TEXT text — one completed assistant message
Event.THINKING nothing; the model is reasoning
Event.TOOL.CALL tool, args, id
Event.TOOL.RESULT tool, id, result, ok
Event.END the turn is over
Event.INJECTED text — a message that landed mid-turn
Event.ERROR error, kind — auth / limit / unavailable / crash from the model or its CLI, plus stderr (CLI chatter), omni (the engine itself) and handler (your callback raised)
Event.SWITCH_PROVIDER provider, extra['from']
Event.NEW_SESSION extra['native'] — the provider's own session id
Event.CONFIG text — a setting changed

Reasoning is deliberately contentless. A THINKING event says the model thought; it never says what. Provider reasoning is signed or encrypted, cannot be replayed anywhere else, and has no business sitting in your logs.

Handlers run on one thread, in the order things actually happened. A handler that raises is reported and stepped over — it cannot take the run down.


Sending

chat.send("...")

Sends a message. If nothing is running it opens a turn. If a turn is already in flight it is injected: the provider picks it up at the next safe point, once the tool it is running has finished. Either way send returns immediately.

How you keep the process alive is your business. A loop:

while chat.status in ("busy", "waiting"):
    message = fetch_new_messages()
    if message:
        chat.send(message)
    else:
        time.sleep(0.2)
    if should_stop() and last_event == Event.END:
        chat.stop()

…or a subscription:

import nats  # the example's dependency, not omni's

stop_event = asyncio.Event()


async def main():
    nc = await nats.connect("nats://localhost:4222")
    chat.start()

    async def on_msg(m):
        chat.send(m.data.decode())      # opens a turn, or lands mid-flight
        await m.ack()

    await nc.subscribe("agent.msgs", cb=on_msg)
    await stop_event.wait()


asyncio.run(main())

chat.send is thread-safe and never blocks, so omni does not care which one you pick. More in examples/.

status is idle before start, then busy / waiting, then stopped. chat.idle is the one a polling loop wants: waiting, with nothing left to process. Sending to a stopped chat raises rather than dropping the message on the floor.


Nothing changes mid-turn

This is the rule the whole design hangs off.

chat.intelligence(9)              # noted now
chat.active_inference_providers(["claude", "openai"])
chat.system_prompt("...")
chat.enable_subagents()

Every one of those is recorded when you call it and applied at the next turn boundary — after the running tool finishes and the turn ends. A model never changes underneath itself.

Calling any of them again overwrites the last value. Same for load_or_create_session: that is how a new session is started.

Prompts and isolation

chat.system_prompt("...")            # replace the provider's own prompt
chat.system_prompt_file("p.txt")
chat.append_system_prompt("...")     # or keep theirs and add

A chat starts quiet: no subagents, no MCP servers, no memory files. A provider that brings its own help makes the same run mean different things on different machines, so you opt back in rather than out.

chat.enable_subagents()              # let the provider spawn its own
chat.enable_mcp()                    # let it load MCP servers and connectors

Memory files are not a switch. CLAUDE.md, auto memory, org memory and AGENTS.md never load, whichever way the other two are set.


Sessions, and how switching works

~/.omni/sessions/{id}.jsonl is the source of truth. It is the event log — the same objects your handlers see, appended in order. It outlives any single provider.

chat = Inference.load_or_create_session("nightly-triage")

One live chat per session id. A second attempt raises SessionBusy; a lock whose owner died is reclaimed, so a crash never wedges a session shut.

Alongside it, {id}.meta.json remembers each provider's own session and how far up the omni log it has already seen:

{"providers": {"claude": {"id": "3cb0…", "synced": 19},
               "google": {"id": "1dbc…", "synced": 26}}}

So when a conversation moves:

  • Continuing on the same provider uses its native resume. omni's log is not read at all.
  • Arriving somewhere new replays only the part that provider missed.
  • Coming back resumes its own session and tops it up with what happened while away.

Which is exactly the scenario worth naming: start on Gemini, raise the dial to Opus mid-way, chat, drop back to Gemini. Gemini picks up its own conversation and is told what Claude did. Nothing is re-read that does not need to be.

What crosses, and what it looks like

Providers do not share tools, so a tool the destination does not have is rendered as text that reads as what happened:

[GoogleSearch: "kite festivals"]
[GoogleSearch result: 12 results …]

The omni log keeps the structured original, so this form only ever exists inside the seed handed to somebody else. Going back to Gemini replays Gemini's own session and the brackets never happened. Preserved, not lossy.

Nothing is trimmed on the way in — not the oldest turns, not a forty-thousand character tool result. A provider arriving late gets the whole conversation.


Auth

Inference.claude.auth_status          # 'authenticated' | 'unauthenticated'
print(Inference.claude.start_auth())  # the URL to open
Inference.claude.finish_auth("code-or-redirect-url")

omni drives each CLI's own login rather than making you use the CLI: it starts the flow, hands you the URL, and types the code back if one is wanted. Codex runs its own browser callback, so there finish_auth waits rather than types and the code is ignored. If a CLI does something unexpected, whatever it printed is handed back verbatim — a confusing message you can read beats a silent failure.

One account per provider.

When a login dies mid-run

An unauthenticated CLI cannot finish the turn it is in. By default omni takes that provider off the chat, resolves the same intelligence level again over whoever is left, and carries on there — you get an ERROR/auth, a CONFIG/provider_removed and a SWITCH_PROVIDER, and the conversation continues on another vendor's model.

chat.disable_autoremoving_unauthenticated_providers()

Turn it off and the auth error is reported and the turn simply ends. Either way the failed turn is not replayed: it is in the log, so the next provider reads it, but nothing re-runs a tool that may already have run.

Limits

Inference.openai.limits
# {'5h': {'used': 0.0,  'reset': '2026-08-21T14:31:07.000Z'},
#  '7d': {'used': 0.16, 'reset': '2026-08-21T10:53:25.000Z'}}

used is a fraction, 0.16 being 16%. reset is an RFC3339 UTC string from every provider — one of them answers in epoch seconds, and you never have to know which. 'unauthenticated' if you are not signed in. Every provider is asked in a way that costs no tokens:

provider how note
claude get_usage control request free; some enterprise plans report no windows, and used is then None
openai account/rateLimits/read read by window duration, never by position — primary is not always the 5h one
google agy -p /usage reports remaining per model group; omni reports used, worst group first

Logging

@chat.logs
def log(event):
    write_somewhere(event.to_dict())

Everything on_event sees, plus omni's own bookkeeping: every launch, model change, provider switch, new session, message in, tool call, error, stop. All of it is the same Event type, so it is parsable without a second schema, and it is the same thing that is already on disk in the session file.


Per-provider notes

Claude Code — flags do the work. Subagents and MCP are off by the command line unless a chat opts in; slash commands and memory files (CLAUDE.md, auto memory, org memory) are off unconditionally, so a run means the same thing on anyone's machine. Seeding is a file write into ~/.claude/projects/<slug>/<uuid>.jsonl; resume then treats it as real history. Model and effort change over the control channel between turns, so re-tuning does not restart anything or re-read the conversation.

Codex — the app server, not exec. It always runs against a CODEX_HOME of its own under ~/.omni/jails/, holding exactly two things: a symlink to your real auth.json (linked, not copied, so a token refresh is not lost) and a near-empty config.toml. That folder is the isolation — codex has nothing left to auto-load, so MCP servers, hooks and AGENTS.md never appear whether or not you asked. Skills live outside CODEX_HOME entirely, so they are switched off one at a time over the protocol. project_doc_max_bytes=0 goes on every launch, so opting back into subagents cannot smuggle somebody's AGENTS.md in with them. History is seeded with thread/inject_items; model and effort are per-turn parameters, so re-tuning is free. Antigravity — the most restricted. There is no flag for MCP, no flag for subagents, and no way to seed history, so omni lets it load what it wants and folds prior conversation into the front of the next message — one turn, not two. Neither isolation switch can be honoured here; omni logs a CONFIG event saying so rather than pretending. An injected message runs as its own turn instead of joining the one in flight, and there is no way to interrupt agy at all. Its cold start is ~10s per launch. An unrecognised conversation id makes agy silently start a new one, so omni checks the id it gets back and re-seeds from the top if it was not the one it asked for.


What omni will not do

  • Reasoning is never carried. It is signed or encrypted per vendor and cannot be replayed anywhere else. omni records that thinking happened and moves on.
  • Tools are observed, not defined. omni does not install tools into a provider or rename theirs. Whatever the CLI does, omni reports.
  • One account per provider. No multi-account support.
  • A switch is a real restart of the provider process, so it costs the destination a context read. Model changes within a provider do not.

Contributing

omni/ is small on purpose and split the way the problem is:

omni/
  events.py        the vocabulary — one type for everything
  chat.py          the engine: one conductor thread, turn boundaries, switching
  inference.py     the front door
  translate.py     history → something a foreign provider can read
  session/         the log, the provider map, the one-owner lock
  intelligence/    the 0-10 dial and its ladder
  providers/       claude/ · openai/ · google/, plus the contract they share
  shared/          paths, jsonl, clock, callback bus, subprocess plumbing

The dial, the landing page and the reference live in teamofsilicons/omnipotent. Which models exist is that repo's problem; running them is this one's.

Adding a provider means an Account and a Runner — see omni/providers/base.py, then providers.register(...). docs/ARCHITECTURE.md explains why the pieces are shaped this way.

pytest                        # fast, no CLI needed
pytest -m live                # drives the real CLIs; needs auth, spends a little quota
python3 scripts/cleanup.py    # afterwards: takes the live sessions back out of ~/.omni

Live tests deliberately run against your real ~/.omni, because a test that uses different paths from a real run is not testing a real run. Everything else gets a home of its own and never touches the network.

For your own tests there is a provider that needs no CLI, no login and no quota, and answers the same way every time:

from omni import Inference
from omni.providers import test

test.install()                                    # registers it, pins a whole 0-10 dial
chat = Inference.load_or_create_session("t", ["test"])
chat.start()
chat.send("hello")            # -> TEXT  'echo: hello'
chat.send("[tool:ls]")        # -> TOOL.CALL + TOOL.RESULT
chat.send("[recall]")         # -> everything it has been told, seeded history included

It is not registered until you call install(), so it can never appear in get_available_providers() by accident.

MIT.

Release files for silicon-omni 0.2.0

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

Source distribution (sdist)

Source distribution for silicon-omni 0.2.0
File Size Uploaded
silicon_omni-0.2.0.tar.gz 76.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for silicon-omni 0.2.0
File Interpreter ABI Platform
silicon_omni-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 137.1 kB

Release files / silicon_omni-0.2.0.tar.gz

Download URL silicon_omni-0.2.0.tar.gz
Size 76.3 kB
Tags Source
SHA-256 checksum
How to use checksums
02e4056dfc60c67655e63ed593a5e7d609b677f846e1f6bd80e1d5e59eb22952
BLAKE2b-256 checksum
How to use checksums
1655d1b35133144512c54e558acc59f451e6a97d485c4afcbd5673e0b135cbc6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / silicon_omni-0.2.0-py3-none-any.whl

Download URL silicon_omni-0.2.0-py3-none-any.whl
Size 60.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8a3ff9610218a78cbdcabe864d4a6907a93254b479691e2474c267541127af3
BLAKE2b-256 checksum
How to use checksums
d52068d538503c55295d6f3d9773ac3ef80be0920448bbd71350e5ce692c59c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

0.9.1

5 release files

0.9.0

5 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

1 release file

0.7.0

5 release files

0.4.0

5 release files

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

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