Rust bridge for Omni Code. Exposes HTTP and SSE APIs for the mobile client and connects to local coding agents such as codex, claude, and opencode.
| Method | Command |
|---|---|
| Homebrew (macOS / Linux) | brew install omni-stream-ai/omni-code-bridge/omni-code-bridge |
| Arch Linux (AUR) | yay -S omni-code-bridge-bin |
| cargo | cargo install omni-code-bridge |
| curl (macOS / Linux) | curl -fsSL https://raw.githubusercontent.com/omni-stream-ai/omni-code-bridge/main/scripts/install.sh | bash |
| PowerShell (Windows) | powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/omni-stream-ai/omni-code-bridge/main/scripts/install.ps1 -UseBasicParsing | iex" |
Arch Linux users: After installing, enable the systemd service:
systemctl --user enable --now omni-code-bridge.serviceThe bundled user service now runs
omni-code-bridge settings-validateasExecStartPre, so invalid bridge settings fail before the server starts.
cp .env.example .env
cargo runThe bridge listens on http://127.0.0.1:8787 by default.
cargo runrequires the Rust toolchain. If you installed via Homebrew, AUR, or the install script, just runomni-code-bridgedirectly.
- Agent CLIs:
codex,claude,opencode, orkiro-cli - A local checkout of omni-code if you want the built-in APK update manifest endpoints to serve a local Android build
Agent binary paths can be overridden with ECHO_MATE_CODEX_BIN and ECHO_MATE_OPENCODE_BIN.
| Endpoint | Description |
|---|---|
POST /client-auth/requests |
Request approval for a client |
POST /client/messages |
Push a message into a project/session flow |
POST /devices/register |
Register a client device for push notifications |
GET /files?path=... |
Return a file from a registered local project root |
GET /sessions/{id}/messages |
List session messages; supports limit, before_id, after_id |
GET /app-update/manifest |
APK update manifest |
GET /app-update/apk |
Download latest APK |
GET /speech, GET /speech/models |
Local speech model management |
POST /speech/models/downloads |
Download a speech model |
GET /speech/models/downloads/{task_id} |
Poll download status |
GET/PUT /speech/profiles/{profile}/model |
Bind model to profile |
GET /speech/realtime |
Realtime speech descriptor |
GET /speech/realtime/ws |
Websocket realtime/call-mode speech |
GET /v1/models |
OpenAI-compatible model list |
POST /v1/audio/transcriptions |
OpenAI-compatible ASR |
POST /v1/audio/speech |
OpenAI-compatible TTS |
GET /sessions/{id}/messages returns messages in stored order. Without a cursor it returns the
latest page (limit defaults to 50 and is clamped to 1..=200), so an in-progress assistant
reply remains visible on the default page. before_id returns messages before a message id;
after_id returns messages after a message id. before_id and after_id cannot be combined.
limit counts display messages: each user message counts as one, and each contiguous agent reply
segment counts as one even when it contains multiple assistant/system messages. The response is
data: { messages, has_more, next_cursor }; use next_cursor as the next
before_id when paging older history, or as the next after_id when polling newer messages.
Session creation (POST /sessions), session update (PATCH /sessions/{id}), and message send
(POST /sessions/{id}/messages) all accept provider_id.
- Omit
provider_idto skip bridge-side provider resolution entirely - Set
provider_idto"AUTO"to auto-select from project-level or global providers by priority - Set
provider_idto a concrete provider config id to force that provider
Examples:
// No bridge provider resolution
{
"provider_id": null
}// Explicit auto-selection
{
"provider_id": "AUTO"
}// Explicit provider
{
"provider_id": "openai-primary"
}For PATCH /sessions/{id}, omitting provider_id leaves the session unchanged, while
"provider_id": null clears the stored session-level selection.
Experimental ACP agent support is configured through acp_servers in ~/.omni-code/settings.json.
You can start from the checked-in example at
config/settings.acp.example.json and adapt it for your local
deployment.
The bridge currently supports two ACP profiles:
kiro: localstdio + JSON-RPCviakiro-cli acpgeneric_http: experimental HTTP/SSE ACP endpoint compatibility
Before using the kiro profile, make sure kiro-cli is installed and authenticated:
kiro-cli loginFor AgentKind::Acp, set provider_id to an acp_servers[].id, or use "AUTO" to select the
highest-priority enabled ACP server.
Example Kiro ACP configuration:
{
"ai_approval": {
"enabled": false,
"base_url": "https://api.openai.com/v1",
"api_key": "",
"model": "gpt-4.1-mini",
"max_risk": "low"
},
"model_providers": [],
"acp_servers": [
{
"id": "kiro-local",
"name": "Kiro Local ACP",
"profile": "kiro",
"command": "kiro-cli",
"args": ["acp"],
"default_model": "claude-sonnet-4",
"enabled": true,
"priority": 0,
"headers": [],
"env": []
}
]
}Example generic HTTP ACP configuration:
{
"ai_approval": {
"enabled": false,
"base_url": "https://api.openai.com/v1",
"api_key": "",
"model": "gpt-4.1-mini",
"max_risk": "low"
},
"model_providers": [],
"acp_servers": [
{
"id": "acp-http",
"name": "ACP HTTP Gateway",
"profile": "generic_http",
"endpoint": "https://acp.example.com",
"auth_token": "replace-me",
"enabled": true,
"priority": 10,
"headers": [
{ "key": "X-ACP-Client", "value": "omni-code-bridge" }
],
"env": []
}
]
}Example session creation using ACP:
{
"project_id": "my-project",
"title": "Try Kiro ACP",
"agent": "acp",
"provider_id": "kiro-local"
}For generic_http, the bridge currently probes and runs against these candidate turn URLs in order:
/turns, /turn, /sessions/{session_id}/turns, then the configured endpoint itself.
Requests are sent as JSON with fields like session_id, thread_id, conversation_id, cwd,
project_root, input, message, and prompt. The response may be either:
- JSON containing assistant text under fields such as
output_text,text,content, ormessage.text - SSE with
data: {...}events that emitdelta.text, approval events, and a terminal{ "type": "done" }
For approval round-trips, the bridge currently tries these reply URLs:
/approvals/{request_id}/reply, /approval/{request_id}/reply,
/permissions/{request_id}/reply, /permission/{request_id}/reply,
/approvals/{request_id}, /approval/{request_id}.
Cancellation is best-effort via /sessions/{session_ref}/cancel, /session/{session_ref}/cancel,
/turns/cancel, /turn/cancel, and /cancel.
GET /agents now reports both binary presence and structured readiness state. For Kiro-backed ACP,
readiness becomes attention_required when kiro-cli is installed but still needs
kiro-cli login, and readiness_message explains the next step. ACP entries also include
acp_diagnostic for the currently selected enabled server, including its server_id, name,
profile, command/args or endpoint, auth/model/header/env summary, and how many ACP servers
are enabled.
For health checks and scripting, GET /agents/acp/diagnostic returns the selected ACP server's
structured diagnostic together with installed, installed_path, readiness, and
readiness_message. The response is marked with source: "live_probe" and probed_at, and the
endpoint accepts ?refresh=true so callers can explicitly request a fresh probe contract. You can
also pass ?provider_id=<acp_server_id> to diagnose a specific configured ACP server directly,
including lower-priority or disabled entries. When omitted, the response fills in the resolved
provider_id for the selected server. For bulk inspection, ?all=true returns diagnostics for all
configured ACP servers in one response. For a minimal real runtime check, ?probe=true adds a
handshake_probe result. Today this performs a live initialize -> session/new -> session/prompt
probe turn for Kiro-backed ACP servers, and a live turn-creation POST probe for generic_http
endpoints. Probe results now include mode and stage so clients can distinguish a full stdio
probe turn from a best-effort HTTP turn probe. The generic_http probe now follows the same
candidate turn URLs as real runtime requests and can validate both JSON and SSE turn responses, but
it is still a lightweight healthcheck rather than a full end-to-end ACP conversation guarantee. When
provider_id is supplied but does not match any configured ACP server, the endpoint returns
400 Bad Request. Bulk results also include
is_default_selected so clients can tell which ACP server would currently be chosen by default.
For generic_http, diagnostics now also expose turn_url_candidates,
approval_reply_url_templates, and cancel_url_templates, so operators can see the exact bridge
URL patterns used during runtime without reading the source.
all=true and provider_id are mutually exclusive and also return 400 Bad Request when used
together.
Session creation (POST /sessions), session update (PATCH /sessions/{id}), and message send
(POST /sessions/{id}/messages) also accept optional reasoning_effort.
Allowed values are "low", "medium", "high", "xhigh", and "max". Message-level
reasoning_effort overrides the session default for that turn. For PATCH /sessions/{id},
omitting reasoning_effort leaves it unchanged, while "reasoning_effort": null clears the
session default.
Codex receives this as model_reasoning_effort, Claude Code receives it as --effort, and
OpenCode currently ignores the field because the headless prompt API used by the bridge does not
expose a matching option.
The bridge can run local ASR and TTS through sherpa-onnx and exposes two API layers:
- Model management endpoints under
/speech/*for the client to list, download, inspect, and select local speech models - OpenAI-compatible inference endpoints under
/v1/*so the client can reuse standard audio request flows
Current bundled model catalog includes:
- Batch ASR:
sensevoice-small-int8 - Realtime/call-mode ASR candidates:
streaming-paraformer-zh-en,funasr-streaming-paraformer-zh-yue-en - TTS:
vits-melo-tts-zh-en,kokoro-int8-multi-lang-v1_1 - VAD:
silero-vad
GET /speech/models returns installation state plus metadata the client can use for filtering, including:
kind,runtime,backendcapabilitiessuch asstreaming,batch_asr,realtime_asr,speech_synthesis,vadfeatures,languages,download_size_mb,memory_hint,notessample_rate_hz,default_voice,voicesfor client-side TTS settings Local TTS voice selection currently uses numeric speaker IDs.vits-melo-tts-zh-enexposes a single voice (0), whilekokoro-int8-multi-lang-v1_1exposes multiple speakers.supports_profiles,recommended_profiles,selected_by
- Call
GET /speech/models - Pick a compatible model and start download with
POST /speech/models/downloads - Poll
GET /speech/models/downloads/{task_id}untilcompleted - Bind the installed model to a profile with
PUT /speech/profiles/{profile}/model - Call
/v1/audio/transcriptionsor/v1/audio/speech
Speech profile bindings are persisted in ~/.omni-code/settings.json by default. Set
ECHO_MATE_SETTINGS_PATH to override the settings file location.
TTS voice selections are stored per model through GET/PUT /speech/models/{model_id}/voice, so
switching between single-speaker and multi-speaker TTS models does not reuse an incompatible voice.
GET /v1/modelsreturns installed local speech models that are usable by OpenAI-compatible audio endpointsPOST /v1/audio/transcriptionsaccepts standard multipart form fields such asfile,model,language,prompt,response_format, andtimestamp_granularities[]POST /v1/audio/speechaccepts OpenAI-style JSON withmodel,input,voice,response_format, andspeedmodelis optional on both/v1/audio/*endpoints. If omitted, the bridge falls back to the selected speech profile model
Current compatibility limits:
/v1/audio/transcriptionssupportsresponse_format=json|text|verbose_json/v1/audio/transcriptionsrejectsstream=true/v1/audio/transcriptionsonly supportstimestamp_granularities[]=segment;wordis not supported yet/v1/audio/speechcurrently supportsresponse_format=wavonly/v1/audio/speechdoes not supportinstructionsyet
Realtime speech is exposed separately from /v1/audio/* because it is aimed at
future call mode rather than batch inference compatibility.
GET /speech/realtimereturns the websocket descriptor, audio requirements, default profile bindings, command names, and event namesGET /speech/realtime/wsupgrades to a websocket session- Websocket auth is the same as the rest of the protected API:
Authorization: Bearer <token>andx-omni-code-client-id: <client_id>
- Client sends binary websocket frames as raw
pcm_s16le - Sample rate is fixed at
16000 - Channels can be
1or2; stereo is downmixed to mono on the server session.updatecan overrideasr_model,vad_model,channels,sample_rate_hz, andenable_vadinput_audio_buffer.commitflushes the current utteranceinput_audio_buffer.clearresets the current utterance state
session.createdsession.updatedinput_audio_buffer.committedinput_audio_buffer.clearedinput_audio_buffer.speech_startedinput_audio_buffer.speech_stoppedresponse.audio_transcript.deltaresponse.audio_transcript.completederror
- Use
GET /speech/models - Keep models where
installed == true - For realtime ASR, filter on
capabilities.realtime_asr == true - For VAD, filter on
capabilities.vad == true
Example session.update:
{
"type": "session.update",
"session": {
"asr_model": "funasr-streaming-paraformer-zh-yue-en",
"vad_model": "silero-vad",
"sample_rate_hz": 16000,
"channels": 1,
"enable_vad": true
}
}Clients register and get approved via CLI:
- Client sends
POST /client-auth/requestswith{ client_id, device_name } - Admin reviews pending requests and approves them:
# list pending requests
omni-code-bridge client-auth list --pending
# approve a specific request
omni-code-bridge client-auth approve --request-id <request-id>
# approve all pending requests at once
omni-code-bridge client-auth approve- Client polls
GET /client-auth/requests/{request_id}untilstatusbecomesapprovedand atokenis returned - Client uses the token as
Authorization: Bearer <token>together with thex-omni-code-client-idheader for all subsequent API calls
Approved records are stored in ~/.omni-code/client-auth.json.
GET /files?path=<file-path>
Returns files from registered local project directories. Requires client authorization headers:
Authorization: Bearer <token>x-omni-code-client-id: <client_id>
The response body is the raw file content. Content-Type is inferred from the file extension, so images, text, JSON, PDF, audio, and similar assets can be returned directly.
| Parameter | Required | Description |
|---|---|---|
path |
Yes | Absolute paths returned directly; relative paths require project_id or session_id |
project_id |
No | Restricts lookup to one project root |
session_id |
No | Restricts lookup to the local project root for that session |
project_idandsession_idcannot be used together.
# absolute path
curl -H "Authorization: Bearer <token>" \
-H "x-omni-code-client-id: <client_id>" \
"http://127.0.0.1:8787/files?path=/absolute/path/to/image.png"
# relative path with project_id
curl -H "Authorization: Bearer <token>" \
-H "x-omni-code-client-id: <client_id>" \
"http://127.0.0.1:8787/files?project_id=<project-id>&path=assets/logo.png"The bridge can serve the newest Android APK from a local checkout of omni-code:
GET /app-update/manifestGET /app-update/apk
By default it looks for APK build outputs and reads the version from that repository's pubspec.yaml. These endpoints are primarily useful for local development or self-hosted distribution. The client checks the official GitHub release manifest by default and only uses the bridge manifest when you explicitly configure a custom update URL.
cargo check
sh scripts/setup-git-hooks.shRun sh scripts/setup-git-hooks.sh once after cloning to enable the local commit-msg hook.
Validate a bridge settings file before starting the server:
cargo run -- settings-validate --path config/settings.acp.example.jsonIf --path is omitted, the command validates the resolved default settings path
(~/.omni-code/settings.json, or ECHO_MATE_SETTINGS_PATH when set).
The production server startup path now uses the same strict settings parsing and validation rules,
so invalid settings fail fast instead of silently falling back to defaults.
Inspect the last few agent command/response pairs from a local Codex or Claude transcript:
omni-code-bridge session-trace --session "<session-id-or-title>"
omni-code-bridge session-trace --session "<session-id-or-title>" --limit 10--session accepts a session id, an exact title, or a partial title. --limit is optional and
defaults to 5.
For local validation there is an end-to-end smoke test script:
scripts/speech-smoke.sh --keep-artifactsWhat it does:
- Checks
GET /health - Auto-provisions a local client auth token when
BRIDGE_CLIENT_IDandBRIDGE_TOKENare not set - Downloads missing ASR/TTS models through
/speech/models/downloads - Binds
asr.batchandtts.default - Synthesizes a wav file through
/v1/audio/speech - Transcribes that wav through
/v1/audio/transcriptionsusing both profile fallback and explicit model selection
Useful options:
| Option | Description |
|---|---|
--with-call-models |
Also install and bind asr.realtime and vad.default |
--with-realtime |
Also run the websocket realtime ASR smoke example |
--skip-download |
Fail if required models are missing |
--output-dir DIR |
Store generated artifacts in a fixed directory |
--no-auto-auth |
Require an existing BRIDGE_CLIENT_ID and BRIDGE_TOKEN |
The script requires curl and jq.
There is also a realtime websocket smoke example:
cargo run --example speech_realtime_smoke -- \
--bridge-url http://127.0.0.1:8787 \
--client-id "$BRIDGE_CLIENT_ID" \
--token "$BRIDGE_TOKEN" \
--wav /tmp/omni-code-bridge-speech-smoke-12345/tts.wavThe example expects a local wav file, resamples it to 16 kHz, streams it to
/speech/realtime/ws, and prints the observed realtime events and completed
transcript.
For ACP validation there is also a bridge-level smoke script:
scripts/acp-smoke.sh --keep-artifactsWhat it does:
- Checks
GET /health - Auto-provisions a local client auth token when
BRIDGE_CLIENT_IDandBRIDGE_TOKENare not set - Fetches
GET /agentsand verifies the ACP summary is present - Fetches
GET /agents/acp/diagnostic - By default, runs
?probe=true&refresh=trueso you get a live ACP probe result as part of the smoke check - Fails fast when ACP readiness is not
ready, or when the live probe does not succeed
Useful options:
| Option | Description |
|---|---|
--provider-id ID |
Diagnose a specific acp_servers[].id |
--all |
Fetch diagnostics for all configured ACP servers |
--no-probe |
Skip the live probe and only inspect static diagnostics |
--allow-attention-required |
Allow readiness=attention_required without failing the smoke run |
--output-dir DIR |
Store JSON outputs in a fixed directory |
--no-auto-auth |
Require an existing BRIDGE_CLIENT_ID and BRIDGE_TOKEN |
The script requires curl and jq. If auto-auth is enabled, it also uses cargo when no local
omni-code-bridge binary is already available for approving the temporary client auth request.
This repo validates commit messages with a shell-based commit-msg hook.
Use Conventional Commits. GitHub release notes are generated from these commit messages:
feat: add approval webhook fallbackfix(api): guard missing client id headerdocs: update bridge deployment notes
This repository publishes release binaries for Linux, macOS, and Windows via GitHub Actions. Release notes are generated from Conventional Commit messages.