Drive Claude Code, OpenAI Codex, and Google Antigravity CLIs through one unified Python API and an OpenAI-compatible server โ using your existing CLI subscriptions, no API keys required.
Project description
unified-cli
One Python + CLI interface for Claude Code, OpenAI Codex, and Google
Antigravity (agy).
๐ฐ๐ท ํ๊ตญ์ด README ยท ๐ Detailed usage (EN) ยท ๐ ์์ธ ๊ฐ์ด๋ (ํ๊ตญ์ด)
Install
pip install unified-cli
For the OpenAI-compatible HTTP server, install the optional server extra:
pip install "unified-cli[server]"
Prerequisites โ this package installs and authenticates nothing.
unified-cliis a thin wrapper that shells out to the official agentic CLIs you already have. It ships no API keys and no credentials, and it stores or transmits no credentials of its own โ every call reuses the login already on your machine.Before using a provider you must have installed the corresponding CLI and signed in with your own subscription:
- Claude โ the
claudeCLI (Claude Code), logged in with Claude Pro/Max- Codex โ the
codexCLI, logged in with ChatGPT Plus/Pro- Gemini โ the
agyCLI (Google Antigravity), logged in with your Google Antigravity accountAny subset works โ you do not need all three. The wrapper simply uses whichever of
claude/codex/agyit finds on your$PATH.
Use all three AI coding CLIs โ each signed in with your personal subscription
(Claude Pro/Max, ChatGPT Plus/Pro, Google Antigravity) โ from a single unified
interface, both as a terminal CLI and as a Python library you can
import in your own code.
The provider key for the Google side is still
"gemini"(and-m gemini-3.5-flashetc. still route to it), but it now wraps the AntigravityagyCLI โ Google blocked the oldgeminiCLI for individual accounts in 2026. See the migration note below.
# CLI
$ unified-cli chat "hi" -m haiku
# or: unified-cli repl โ interactive mode with slash commands
# Python
from unified_cli import create, UnifiedConversation
resp = create("claude").chat("hi")
conv = UnifiedConversation()
conv.send("Hello", provider="claude")
conv.send("Continue", provider="gemini") # context auto-injected
Why this exists
Each of the three CLIs (claude, codex, agy) ships great subscription
auth but lives in its own world. Want to route "quick query" to the fastest
model regardless of provider? Want a single OpenAI-compatible /v1/chat/completions
endpoint backed by whatever CLI is cheapest/freshest? Want your Python app to
switch providers mid-conversation with automatic context handoff? That's what
this wrapper does โ as a CLI you can shell into, and as a Python package you
can import.
Features
- Dual mode: full-featured CLI (
unified-cli chat,repl,status, ...) AND clean Python API (from unified_cli import ...) โ same code, same state - Subscription-aware: uses your existing
claude/codex login/agyOAuth. Claude/Codex fall back automatically toANTHROPIC_API_KEY/OPENAI_API_KEYif OAuth expires (agy is OAuth-only) - Multi-turn history: CLI via
--continue/--resume, Python viasession_id=orUnifiedConversation - Cross-provider conversation: one
UnifiedConversationcan switch providers mid-chat; the last 8 turns auto-inject as context into the new provider's prompt - Unified streaming events:
kind="text" | "tool_use" | "tool_result" | "reasoning" | "usage" | "session" | "done" | "error"โ normalized across the three native JSONL schemas - Web search by default: Claude
WebSearch, Codexweb_search. Thegeminiprovider (now the AntigravityagyCLI) is agentic and decides when to web-search on its own โ always available. - Image input (multimodal, all 3 providers): pass
images=[paths]tochat()/stream()or--image foo.pngon the CLI. Each provider uses its native vision path:- Codex โ
-i, --image <FILE>flag (codex CLI 0.129+). - Gemini (
agy) โ@<path>reference embedded in the prompt +--dangerously-skip-permissionsso the agent can read the file. - Claude โ Routed through Claude Code's built-in
Readtool with--permission-mode bypassPermissions; the image path is prepended to the prompt. PNG / JPEG / GIF / WebP all supported.
- Codex โ
- Structured errors: every failure โ
UnifiedError(kind=...)from one of seven categories (auth_expired/rate_limit/model_not_allowed/not_found/network/config/internal) with Korean recovery hints - OpenAI-compatible server: drop-in
/v1/chat/completions+ auto-updating dashboard at/dashboard - Rich terminal UI:
doctorhealth table,status --watchlive dashboard,setupinteractive wizard, streaming spinner
Default models (lightweight, subscription-friendly)
| Provider | Default | Latest flagship (override with -m) |
|---|---|---|
| Claude | claude-haiku-4-5 |
claude-opus-4-7 (or alias opus) |
| Codex | gpt-5.4-mini |
gpt-5.4 (or gpt-5.5 if your codex CLI is up to date) |
Gemini (agy) |
gemini-3.5-flash |
gemini-3.1-pro |
Override via -m <name>. The wrapper passes any model ID straight through to
the underlying CLI; unified-cli models shows the available list as a starting
point. For the absolute fastest interactive feel use -m gpt-5.3-codex-spark.
Gemini โ Antigravity migration: As of 2026, Google blocked the old
geminiCLI for individual accounts (IneligibleTierError: ... migrate to the Antigravity suite). Thegeminiprovider now wraps the AntigravityagyCLI (~/.local/bin/agy).agyis fully agentic (web search, shell, file tools) and routes to several model families โ rununified-cli models gemini(which callsagy models) to see them, e.g.Gemini 3.5 Flash (Medium),Gemini 3.1 Pro (High),Claude Sonnet 4.6 (Thinking),GPT-OSS 120B (Medium). Both the display names and slugs likegemini-3.5-flashwork with-m. Unknown names silently fall back to the default. Note:agyheadless mode outputs plain text (no token-usage reporting).
Install from source (development)
git clone https://github.com/MinwooKim1990/unified_cli.git
cd unified_cli
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[server,dev]'
unified-cli setup # first-time onboarding wizard (see note below)
Requires Python 3.9+ and at least one of claude, codex, agy already
installed and logged in โ see Prerequisites above. The optional setup
wizard only suggests the official install commands for any missing CLI (e.g.
npm/brew for Claude/Codex; agy ships with the Antigravity suite โ
https://antigravity.google) and opens each provider's own browser login; it
never stores credentials and you can decline any step.
Usage at a glance
CLI
# Single turn
unified-cli chat "explain python list reversal in one line"
# Continue the last conversation
unified-cli chat "what about in-place?" --continue
# Resume a specific session
unified-cli chat "continue from earlier" --resume <session_id>
# Interactive REPL with slash commands (/provider, /model, /history, /save, ...)
unified-cli repl
# Stream + web-search (both defaults)
unified-cli chat "latest Python release?" --stream
# Cheapest fast query
unified-cli chat "quick q" -m gpt-5.3-codex-spark
# Image input (works with all 3 providers โ see Features above for details)
unified-cli chat "what's in this photo?" --image cat.png -m haiku
unified-cli chat "compare these two" --image a.jpg --image b.jpg -m gpt-5.4-mini
# Status & dashboard
unified-cli doctor # one-time health check
unified-cli status --watch # live terminal dashboard (5s refresh)
uvicorn unified_cli.server:app --port 8000 # + http://localhost:8000/dashboard
Interactive REPL โ unified-cli repl
[claude/haiku] > hello
[claude/haiku] > /provider codex # switch providers (context auto-injected)
[codex/gpt-5.4-mini] > /image photo.png # attach image for the next turn
[codex/gpt-5.4-mini] > describe this
[codex/gpt-5.4-mini] > /history # last 10 turns
[codex/gpt-5.4-mini] > /save # current session_id + resume hint
[codex/gpt-5.4-mini] > /exit # state saved โ `chat --continue` from here
Slash commands: /help /model /provider /new /save /history
/tokens /doctor /image /images /clear-images /exit.
Python
from unified_cli import create, UnifiedConversation, UnifiedError, load_last_session
# Pattern 1 โ single call
resp = create("claude").chat("hi")
# Pattern 2 โ external code manages history (typical for chatbots)
cli = create("codex")
sessions = {}
def reply(user_id: str, prompt: str) -> str:
r = cli.chat(prompt, session_id=sessions.get(user_id))
sessions[user_id] = r.session_id
return r.text
# Pattern 3 โ wrapper manages history + cross-provider
conv = UnifiedConversation()
conv.send("My name is Minwoo.", provider="claude")
conv.send("What's my name?", provider="gemini") # knows "Minwoo"
# Pattern 4 โ resume from CLI session
state = load_last_session() # reads ~/.unified-cli/state.json
if state:
resp = create(state.provider, model=state.model).chat(
"follow-up from REPL", session_id=state.session_id,
)
# Pattern 5 โ error-aware fallback
for p in ("claude", "codex", "gemini"):
try:
return create(p).chat("...")
except UnifiedError as e:
if e.kind in ("auth_expired", "rate_limit"):
continue
raise
# Pattern 6 โ image input (works on all 3 providers)
resp = create("claude").chat(
"What single color is this image?",
images=["/path/to/photo.png"],
)
print(resp.text)
# `images` accepts mixed inputs:
# - file path (str or pathlib.Path)
# - raw bytes
# - http(s) URL or "data:image/png;base64,..." (Anthropic Attachment)
images = [
"cat.png",
b"\\x89PNG...", # bytes
"https://example.com/dog.jpg", # URL
"data:image/png;base64,iVBOR...", # data URL
]
# CLI equivalent:
# unified-cli chat "describe" --image a.png --image b.jpg -m gpt-5.4-mini
See USAGE.md (English) or USAGE.ko.md (Korean) for the full cookbook โ 9 patterns including sync, async, streaming, tool events, error fallback, image input, CLIโPython state sharing, and advanced provider options.
OpenAI-compatible server
uvicorn unified_cli.server:app --port 8000
# Browse: http://localhost:8000/dashboard (live usage / sessions)
Drop-in for any OpenAI client โ model is auto-routed by name; the user
field acts as a conversation id (preserves history across calls):
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
# Plain text turn
client.chat.completions.create(
model="haiku", # โ claude
messages=[{"role":"user","content":"hi"}],
user="session-1",
)
# Image input (OpenAI multi-content schema, works for all 3 providers)
client.chat.completions.create(
model="gpt-5.4-mini", # โ codex
messages=[{"role":"user","content":[
{"type":"text","text":"describe"},
{"type":"image_url",
"image_url":{"url":"data:image/png;base64,iVBOR..."}}
]}],
)
# Continue in a different provider (cross-provider conversation)
client.chat.completions.create(
model="gemini-3.5-flash", # โ gemini (agy)
messages=[{"role":"user","content":"summarize what we discussed"}],
user="session-1", # last 8 turns auto-injected
)
Known limitations
Speed: every call spawns a fresh subprocess (claude -p / codex exec /
agy for the gemini provider) โ these CLIs don't support a long-lived
daemon. Measured latency:
| Stage | Claude | Codex | Gemini |
|---|---|---|---|
| Subprocess spawn | ~50 ms | ~60 ms | ~460 ms (Node bundle) |
| API round-trip (API round-trip) | 3โ6 s | 2โ3 s | 3โ4 s |
| Full chat turn | 5โ6 s | 2.7โ3 s | 3โ4 s |
For the absolute fastest interactive feel, use -m gpt-5.3-codex-spark. Even
then, expect 2โ3 seconds per turn. This is a structural limit of the
subprocess architecture โ not something the wrapper can fix without either
(a) losing subscription auth by calling provider APIs directly, or (b) using
experimental daemon modes (e.g. codex app-server) that aren't fully stable
yet.
Subscription ToS: each provider's terms forbid reselling/exposing your personal subscription as a third-party service. This wrapper is designed for personal local automation, not as a SaaS gateway. Don't ship a web service backed by your personal OAuth.
macOS-first: Claude's Desktop app bundle is auto-discovered on macOS. On
Linux/Windows the claude binary needs to be on $PATH. REPL's arrow-key
history needs readline (stdlib on macOS/Linux; Windows users may need
pyreadline3).
Gemini (agy) specifics: agy headless mode prints plain text (no JSON
event stream), so the wrapper can't surface per-token usage โ tokens in/out
shows as None. Session resume uses --conversation <UUID> / --continue;
the conversation id is recovered from the newest .db in
~/.gemini/antigravity-cli/conversations/. Because agy runs full agentic
loops (web/shell/file), a turn can take longer than a one-shot completion, so
this provider defaults to a larger timeout (300s).
No persistent usage tracking: UsageTracker keeps per-provider aggregates
and recent-call history in process memory only. Restart = counters reset. For
long-term usage analytics you'd need to log separately.
Comparison with similar projects
| Project | Language | CLI + Python import | 3-CLI subprocess | OpenAI server | Dashboard | REPL |
|---|---|---|---|---|---|---|
| unified-cli (this) | Python | โ | โ (direct) | โ | โ | โ |
| oauth-cli-coder | Python | โ | โ (via tmux) | โ | โ | โ |
| coding-cli-runtime | Python | library only | โ | โ | โ | โ |
| router-for-me/CLIProxyAPI | Go | โ (server only) | โ | โ | โ | โ |
| codeking-ai/cligate | TypeScript | โ (server only) | โ | โ | โ | โ |
| PleasePrompto/ductor | Python | โ (bot only) | โ | โ | โ | โ |
| simonw/llm + llm-claude-code | Python | โ | Claude only | โ | โ | โ |
| litellm | Python | โ | direct API | โ | โ | โ |
Closest neighbour: oauth-cli-coder โ same dual-mode idea, but uses tmux
sessions as the integration primitive (requires tmux on user's machine). This
project uses direct subprocess.Popen for a simpler deployment story
(stdlib-only core, no external process manager), adds the OpenAI-compatible
server + live dashboard + rich REPL + state-file sharing between CLI and
Python code.
Closest library-only alternative: coding-cli-runtime on PyPI โ pure
Python library that wraps multiple coding CLIs per its PyPI page (verify the
exact set yourself). No CLI entry point, no server, no REPL.
If your use case is just "spawn a CLI and get text back" โ coding-cli-runtime
is smaller. If you want dual-mode + richer infrastructure (state, server,
dashboard, REPL), this is the one.
Project structure
unified_cli/
โโโ src/unified_cli/
โ โโโ core.py # Message, Response, Usage, ModelInfo dataclasses
โ โโโ errors.py # UnifiedError + classify() per-provider matchers
โ โโโ discovery.py # find_{claude,codex,gemini}_bin()
โ โโโ base.py # BaseProvider ABC + retry/fallback
โ โโโ providers/ # claude.py, codex.py, gemini.py
โ โโโ conversation.py # UnifiedConversation (cross-provider context)
โ โโโ state.py # ~/.unified-cli/state.json read/write
โ โโโ usage.py # UsageTracker (per-process aggregates)
โ โโโ factory.py # create() + route()
โ โโโ cli.py # doctor / setup / status / chat / repl / models
โ โโโ repl.py # interactive REPL with slash commands
โ โโโ server.py # FastAPI OpenAI-compat server + /dashboard
โ โโโ ui.py # rich helpers (tables, panels)
โโโ tests/ # 46 unit tests, stdlib only
โโโ examples/ # 8 runnable scripts
License
MIT License ยท Copyright (c) 2026 Minwoo Kim โ see LICENSE.
Anyone is free to use, modify, and redistribute this software, provided the copyright notice and license text are preserved in the redistribution. Personal use of provider subscriptions (Claude Pro/Max, ChatGPT Plus/Pro, Google AI Pro) is your own responsibility under each provider's Terms of Service โ see "Known limitations" above.
Contributing
Issues and PRs welcome. Please run python tests/test_errors.py (and the
other tests/test_*.py) before opening a PR โ all 46 should stay green.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unified_cli-0.1.0.tar.gz.
File metadata
- Download URL: unified_cli-0.1.0.tar.gz
- Upload date:
- Size: 96.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ec5a78fbfcc3bf250e47efe205b1f6c6a512e50887eab9c61cf32ac1ab7b4f4
|
|
| MD5 |
c429a1d44dcf707ba59a5f3041cd71e1
|
|
| BLAKE2b-256 |
2518d53849a154c0dcec1f747c277bfc98ab135bd06e08ca92a4bf074d5b4b0a
|
Provenance
The following attestation bundles were made for unified_cli-0.1.0.tar.gz:
Publisher:
publish.yml on MinwooKim1990/unified_cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unified_cli-0.1.0.tar.gz -
Subject digest:
3ec5a78fbfcc3bf250e47efe205b1f6c6a512e50887eab9c61cf32ac1ab7b4f4 - Sigstore transparency entry: 1926870332
- Sigstore integration time:
-
Permalink:
MinwooKim1990/unified_cli@5a2106f28e5fb5080bdcb791c549595f8c7a9333 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/MinwooKim1990
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5a2106f28e5fb5080bdcb791c549595f8c7a9333 -
Trigger Event:
push
-
Statement type:
File details
Details for the file unified_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: unified_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 69.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8289e67b499a6227f810cac7d04b6afcc0d1ad0e423b91b6a3e10568b5ebda8f
|
|
| MD5 |
42b0ba4b050c5d069153dfe3ecca36ec
|
|
| BLAKE2b-256 |
84a47da0fd1c37ba90c6325f66486d38e56ac6d593f350e86b49f3842daeae2c
|
Provenance
The following attestation bundles were made for unified_cli-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on MinwooKim1990/unified_cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unified_cli-0.1.0-py3-none-any.whl -
Subject digest:
8289e67b499a6227f810cac7d04b6afcc0d1ad0e423b91b6a3e10568b5ebda8f - Sigstore transparency entry: 1926870490
- Sigstore integration time:
-
Permalink:
MinwooKim1990/unified_cli@5a2106f28e5fb5080bdcb791c549595f8c7a9333 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/MinwooKim1990
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5a2106f28e5fb5080bdcb791c549595f8c7a9333 -
Trigger Event:
push
-
Statement type: