Server mode
ai serve exposes the router as an OpenAI-compatible API. It uses the same logins, keys, accounts, cooldowns and fallbacks as the CLI.
ai serve # http://127.0.0.1:8080/v1
ai serve --host 0.0.0.0 --port 9000 --max-concurrent 4 --timeout 600
ai serve keys [list | add NAME | remove NAME]
Administration API
Off by default. Configures a running server without shell access.
ai serve admin-keys add ops # its own key, shown once
ai serve --enable-admin
curl -H "Authorization: Bearer omni-admin-..." http://127.0.0.1:8080/admin/health
| Route | Purpose |
|---|---|
GET/PUT /admin/config |
Models, fallback order (fallback_order) and routing |
GET/POST /admin/keys, DELETE /admin/keys/{id} |
Provider keys. The id is openrouter or openrouter.work |
GET /admin/accounts |
Accounts, login and cooldown |
GET/POST /admin/api-keys, DELETE /admin/api-keys/{name} |
Keys for /v1 clients |
GET /admin/health |
Server health |
POST /admin/accounts/{account}/login |
Starts a subscription login: returns the link and the attempt id |
POST /admin/accounts/{account}/login/complete |
Finishes the login |
DELETE /admin/accounts/{account}/login |
Cancels, with the id in X-Omni-Login-Attempt |
- Separate keys: a
/v1key never opens/admin, and vice versa. Without an admin key,--enable-admindoes not start. - Network: from outside this machine, it requires HTTPS through a reverse proxy, which must set
X-Forwarded-Proto.--admin-allow-httpturns this check off. It only prevents accidental exposure: whoever reaches the port directly can forge the header, so keep the port open only to the proxy. In Docker, the published port counts as external access. - Secrets: responses show only the end of provider keys. The exception is a newly created client key, shown once.
- Protected fields: permission mode, binary paths and
config_dirreturn 403. - Careful: this key can turn on paid models (
openrouter_paid) and replace provider keys. In Docker, useSMART_AI_ADMIN=1.
Login through the API. {account} is primary and the other Claude accounts, codex, cursor, grok, or codex.NAME for an extra account. An attempt lasts 10 minutes and each account takes one at a time (409).
| Tool | How to finish |
|---|---|
| Claude Code | Open the link and send {"attempt_id": "...", "code": "..."} with the code you get |
| Codex, Grok | Open the link, type the user_code and send {"attempt_id": "..."}. The answer is 202 until it finishes |
| Cursor | Open the link and send {"attempt_id": "..."}. The answer is 202 until it finishes |
Antigravity and key-only tools return 400. Codes and tokens do not appear in responses or logs. The exception is user_code, which has to be typed in the browser.
Parallel writes
In write modes, requests run in parallel, up to --max-concurrent, without touching the original folder.
| Folder with git | Folder without git | |
|---|---|---|
| Where it works | Worktree on an omni/<id> branch, from the latest commit |
Copy of the folder, up to 1 GB |
| Result | Commit on the branch, named in X-Omni-Branch. Merging is up to you |
Copy in ~/.local/share/smart-ai/runs/, named in X-Omni-Copy |
| Continue | Send X-Omni-Branch on the next request |
Send X-Omni-Copy on the next request |
| Cleanup | The worktree goes at the end. The branch stays until you delete it | The copy stays until you delete it |
- No changes: a new branch or copy is discarded and the header is absent. When streaming, the header goes out first and may point at something discarded.
- Failure or timeout: what was done is kept, and the header comes with the error response. After a server crash, changes are saved on the next start.
- Concurrency: two requests on the same branch or copy get 409. Only
omni/branches can be continued. - Queue: over the limit, a request waits up to
--queue-waitseconds (60), with up to--queue-sizein line (16). Only then does it get 429. - Limitations: uncommitted changes and files ignored by git, such as
node_modules, are not in the worktree. Isolation covers files: ports, databases and other services used by commands can still collide, and a command can write outside the folder. Memory counts too: each request is one CLI process. The server's own git commands do not run the repository's hooks orcore.fsmonitor. Git filters, such as Git LFS, still run: only serve repositories you trust.
ai serve work --workdir FOLDER # lists
ai serve work remove ID --workdir FOLDER # shows; with --yes, deletes
ai serve work prune --older-than 30 --workdir FOLDER # also --merged; with --yes, deletes
Without --yes, nothing is deleted. The command only reaches omni/ branches and Omni's copies. An item in use, or a copy changed less than 15 minutes ago, is kept.
As a service
ai serve --install-service [--host H] [--port P] [--mode M] [--workdir FOLDER]
ai serve --service-status
ai serve --uninstall-service
Starts by itself and restarts on failure, with launchd on macOS and systemd user services on Linux. On Windows, in containers and on WSL without systemd, the command says so and installs nothing; run ai serve manually.
- Secrets: the service file holds no keys. With no saved key, installing creates one and shows it once. Outside
127.0.0.1, a saved key is required first. - Environment: only
PATHand the Omni folders reach the service. Keys that exist only in your shell do not: save them withai --key. Installing lists them, along with any CLIs it could not find. - Folder and mode: the default is
plan, in~/.local/share/smart-ai/serve.--mode yoloand overwriting a file Omni did not generate require--yes. - Logs:
~/.cache/smart-ai/serve.log, rotated, andserve.out.log, cleared on each install. - Linux: to start at boot with no open session, run
loginctl enable-linger $USER. Installing warns about it but does not do it for you. - Updates:
ai --updaterestarts the service. On installs without git, reinstall the service afterwards.
Using the API
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="omni-...")
client.chat.completions.create(model="omni-auto", messages=[{"role": "user", "content": "Olá"}])
On first start, the server creates a key and shows it once. Only its hash is stored. In a container, you can also set SMART_AI_SERVER_KEY. There is no unauthenticated mode.
| Route | Purpose |
|---|---|
POST /v1/chat/completions |
Full response or streaming (SSE) |
GET /v1/models |
omni-auto, which uses the router, and omni/<tool> for each configured tool |
| Response header | Meaning |
|---|---|
X-Omni-Provider |
Tool that answered |
X-Omni-Usage |
reported (by the provider) or estimated. Not present when streaming |
X-Omni-Ignored-Params |
Parameters received and ignored |
Security:
- By default, tools run in
planmode, in a temporary folder, whateverSMART_AI_MODEsays. - To create, edit and run commands:
ai serve --mode edits|auto|yolo --workdir FOLDER. The folder is required and nothing asks for approval. Each request works in isolation. Whoever has the key can change the folder and, with commands, reach the account logins. - Read-only does not prevent reading files. Anyone with the key can ask a tool to read files on this machine. Configured MCPs may also be available. Treat the key as access to your account.
- The default is
127.0.0.1. To expose it on a network, use--hostand HTTPS through a reverse proxy. - Above the connection limit (128 or more, depending on the system), the answer is 503. The start of each request must arrive within 30 seconds. On public exposure, also use the proxy's per-origin limits.
- Sharing an instance means sharing the same accounts and subscriptions. Check each provider's terms.
Limitations:
- When the server's mode refuses an action and the tool ends with an error, as Grok does, the response is
403 action_not_allowed. temperatureonly applies to OpenRouter, DeepSeek and Grok over the API. CLIs ignore it, and it shows up inX-Omni-Ignored-Params.max_tokensis approximate on CLIs (about 4 characters per token).usageis estimated when the provider does not report it.- Each request can use a lot of quota, because agent CLIs load their own context. On Codex, a simple "OK" used about 19 thousand input tokens.
- On CLIs, streaming delivers the answer at once, at the end. Only OpenRouter sends pieces during generation, except when personal data was masked.
- Fallback is transparent until the first piece of the answer. If the provider fails after that, the client gets an error.
tools/function calling only on API providers (OpenRouter, DeepSeek and Grok with a key). The server never runs the tools: the call goes back to your code. Other models return 400, because agent CLIs cannot hand the call back, unless--tool-emulationis on. Withomni-auto, the order is OpenRouter, DeepSeek and Grok. On OpenRouter, withoutopenrouter_paid, only free models. When streaming, the call arrives in one piece at the end. Arguments outside the schema make the server try the next provider; only the core of JSON Schema is checked (type,required,enum,properties,items,additionalProperties).functions(old format) returns 400.- Experimental:
ai serve --tool-emulation(SMART_AI_TOOL_EMULATION=1in Docker) emulatestoolson Claude Code and the Grok CLI, with their own tools switched off. The model writes the call as JSON, and the server checks it and retries once, which spends more quota. It only applies when no API provider is available. Codex and Cursor are left out, because their tools cannot be switched off. The model may get it wrong more often than in native mode, personal data is not masked, and client content can imitate the format. - Images return a 400 error.
- The prompt reaches the tools as a command-line argument, so it is visible in the machine's process list. The limit is 100 KB per request.
- Legacy Claude profiles, which swap the credentials file, limit the server to one request at a time.
This text comes from the project README. See the original on GitHub