* Keep what a voice call said on its messages, and read Open WebUI's realtime call settings Open WebUI 0.12 stores a realtime call's spoken transcripts in each message's meta.voice and replays them to the chat model. A message Conduit wrote again from its own state carried no meta, and the server replaces each message whole, so the next push would have erased them. Rows rewritten locally now keep the meta stored on them, the voice record is read into the message's metadata, and the echo writes it back where Open WebUI reads it. Another device's chat:message:voice refreshes the chat. Direct chats replay what a call spoke the way Open WebUI does: a reply the voice gave by itself becomes its transcript, labeled as historical voice speech, and what was spoken over a chat answer follows that answer. The config's audio.realtime block and the chat.call permission are read, ready for gating realtime calls; unreadable permissions count as unknown. * Speak and listen through each chat's own server, including Hermes and Direct The Server engines for speech to text and text to speech only worked with an Open WebUI server, and in a Hermes or Direct chat they quietly used whichever Open WebUI server was signed in. Server speech now follows the selected model's backend: an Open WebUI model uses its server, a Hermes chat on a Desktop Gateway uses the gateway's audio relay, and a Direct or Apple chat uses a new Voice provider. A chat whose backend has none uses the device instead; audio never goes to a server the chat does not belong to. Hermes speech goes through /api/audio/transcribe and /api/audio/speak for the connection's profile, with the providers and keys configured on the gateway; the voice-config route, which hands out provider keys, is never called. The Voice provider is one Direct connection that speaks the OpenAI API, chosen in Audio settings with its transcription model, speech model and voice. Requests go to its audio/transcriptions and audio/speech with the connection's own key, headers, api-version and certificates. Picking OpenAI fills in the models Open WebUI defaults to. The page is also in the native Settings sheet on iOS. The server voice picker shows only for Open WebUI, which lists voices; Hermes and the Voice provider speak with their own. * Talk to a realtime voice in calls, ready for Open WebUI and Direct Open WebUI 0.12 can run calls through an OpenAI realtime voice that answers small talk itself and hands real requests to the chat's model. This adds the call side of that, shared by Open WebUI and Direct chats; the audio engine it needs comes next, so calls stay as they are until then. The engine follows Open WebUI's web client: one request to the voice at a time, made only while nobody is speaking; the chat's latest state sent before each; speaking over the voice cancels its reply and tells it how much was heard; a request the voice delegates runs as an ordinary chat turn, and its outcome goes back to be spoken. Open WebUI's server bridge carries a call for its chats. A Direct or Apple chat talks to the Voice provider's Realtime API instead, with the bridge's rules checked on the device, so one engine runs both; the device's bridge is tested against the server's own rules, and the engine's tests hold every command to them. A delegated request is sent like a typed message, with the user's words marked as spoken. A reply the voice gives itself is stored through the same send as a finished answer under the voice model, and no model runs; Open WebUI replays it to the chat model as past speech. What the voice says about a delegated answer is merged into that answer's meta.voice. Words nothing answered stay in the call's captions. The call controller runs either engine: a call with a realtime voice keeps CallKit, the audio session, background work, mute, speaker and the call panel as before. Whether a call has one is decided when it starts, and it falls back to a Standard call whenever anything is unknown: Standard chosen in settings, a temporary chat, a server without realtime calls, an account that may not call or whose permissions could not be read, a sign-in with an API key, or a Voice provider without a realtime model, voice and transcription model. Direct connections now share their key, header and certificate setup between HTTP and WebSocket, and Open WebUI's socket headers are built in one place, as is the WebSocket that never follows redirects. * Translate the Voice provider settings into Polish Polish carries every English key, so the Voice provider's strings need their Polish copies; without them polish_localization_test fails. * Talk with a realtime voice in calls on iOS and Android Calls in Direct and Apple chats with a Voice provider, and in Open WebUI chats on a server with realtime calls, now talk with a realtime voice: both sides can speak at once, and speaking over the voice stops it. The voice needs the microphone and the speaker on one echo-cancelled stream, so it does not hear itself. On iOS one audio engine runs with voice processing on: the microphone is converted to 24 kHz frames, and the voice plays from a queue that works like Open WebUI's web worklet, starting once 80 ms are buffered and reporting how much of each part was played when the user cuts in. It restarts after a route change. On Android the microphone records as voice communication with the platform's echo canceller, noise suppressor and gain control, and the voice plays on the same audio session; what was heard comes from the track's play position. A microphone that only offers 48 kHz is brought down to 24 kHz. If the audio stops mid-call, the call ends and says so. Audio settings gain Calls: Realtime, the default, or Standard, which keeps every call turn by turn. The Voice provider takes a realtime model, voice and optional instructions, prefilled for OpenAI. Both are in the native Settings sheet on iOS too. * Tell the user what a realtime call could not do, and mark what its voice said A realtime call's problems that do not end it now show as messages in the user's language: words that could not be made out, a reply cut short, and a part of the call that could not be saved to the chat. A reply the voice gave by itself shows its voice model with a waveform and "spoken", even when the answer above came from the same model, and offers no Regenerate: there is no chat model answer to run again. Opening another chat, or none, ends a realtime call: its voice talks about the chat it started in, and a request it handed over is answered there. A call started without a chat follows the chat its first turn creates, and that chat's server id. * Talk with Hermes's GPT-Live voice in calls on a Desktop Gateway A Hermes profile that runs voice calls through GPT-Live, and has an OpenAI key for it, now gets realtime calls in Conduit too. The gateway keeps the key: Conduit makes a WebRTC offer, Hermes opens the session with OpenAI and returns the answer, and the voice's audio and events then flow between the phone and OpenAI. Profiles in the chained voice mode, a gateway that cannot say, and Responses API connections keep Standard calls. The call follows Hermes's own desktop client. GPT-Live takes its own turns; a request it delegates carries no text, so Conduit sends the user's latest words as an ordinary Hermes turn, marked as coming from voice-live, with the spoken conversation of the last five minutes as context only the model reads. While Hermes works the voice is told so quietly; an approval waiting in the chat is mentioned once; the answer is handed back in sentence-sized pieces for the voice to say. A newer request stops the one still running, mute reaches the session, and ending a call closes the session before the connection. The WebRTC call comes from flutter_webrtc, behind the same port as the other call audio, so the call controller, CallKit, background work and the call panel are shared with every other call. * Translate realtime calls and the Voice provider into every language The Calls setting, the Voice provider page, the call's notices and the "spoken" label were in English and Polish only, and the Localization check requires every key in every language. They are now in all of them. Every language also still said Server speech always goes to Open WebUI. It now follows the chat's own server, so the descriptions name Open WebUI, Hermes, or the Voice provider for Direct chats, as the English does. * Keep the Voice provider page and the iOS call audio within the app's rules The Voice provider page imported the Material and Cupertino libraries directly, which the platform UI check allows only behind the shared facade; it now takes its icons from the shared vocabulary. The package boundary check also lists the new iOS call audio bridge, which reaches its host only through ConduitBridgeHost. * Fix what review found in realtime calls - A call writes only to its own chat. What it said could land in the chat the user opened as the call ended, and a call started without a chat adopted any chat opened next. It now follows only the chat its own send creates and that chat's server id, writes nothing elsewhere, and merges a delegated answer's transcript into the call's chat even after a switch. - A call that starts a new chat no longer seeds GPT-Live with the previous chat's messages, or names that chat to Open WebUI. - Hanging up while a call is starting leaves no microphone, WebRTC peer or socket open. A WebSocket handshake that times out is stopped, and a socket that opens after it is closed. - GPT-Live's microphone goes quiet before the session is asked to close, and an answer handed over as the call ends goes on in the chat unfollowed. - Muting while speaking no longer holds a finished answer back until the user speaks again. - Small talk held behind a delegated answer is saved after that answer when the call ends, so the answer stays last while it runs and can still be stopped. If it is still running a few seconds later, the small talk stays in the captions. - A delegated answer whose send failed after it was placed ends as failed instead of working forever. Hermes approvals and questions now count as waiting for the user, so the voice says to answer them in the chat. - GPT-Live's request is the user's latest words from the last five minutes, as in Hermes's own client; words from long before no longer join it. - Read-aloud and dictation keep the server they started with, so text and recordings never go to another chat's server when the model changes. - A voice reply's "spoken" label appears when its transcript arrives after the message, the native Voice provider choice is kept while connections reload, the call mode is saved on its own key, a Direct voice turn releases its database lease if saving it fails, and ending the call controller ends its realtime voice. * Fix what review found in the realtime call audio on iOS and Android - Android: a piece of the voice's speech taken just before the user cut in could still play, and count as heard. It is now taken with its clear generation, so a clear right after drops it. A stopped call's capture and playback threads stop with it, and an old thread sends nothing to the next call; the recorder is stopped before its thread is awaited, so a blocked read returns first. - iOS: after a Bluetooth or headset route change, the microphone tap and converter are rebuilt for the new route's format. The playback queue no longer builds a string on the audio thread for every render. * Fix what the second review found in realtime calls - The first request in a new chat no longer ends the call: the call's own send counts as creating its chat until it is done, as the chat can open only after the answer is placed. - A chat opened while the call connects ends it, and the voice never reads a chat other than the call's own. - A reply cut off by hanging up waits behind a delegated answer still running, like the small talk held before it. - A GPT-Live request replaced while the one before it was stopping is dropped instead of being sent to the chat. - Resetting settings clears the Voice provider for good; it came back on the next launch. - An earlier version of a voice reply is not labeled as spoken: versions keep no transcript to tell. - The call's chat host closes even when the voice's ending fails, and chunkForCommentary takes only a positive limit. * Follow only the chat a realtime call's own send created A call started without a chat followed any chat that opened while its first send was running, so a chat the user opened during that wait became the call's: the voice read it, and later requests went there. The call now remembers the messages its sends placed and follows a new chat only if it holds them, which every send path's new chat does when it opens. * Hand GPT-Live's newest request to the chat only once older answers stop A request replaced while the one before it was still stopping skipped the wait: the newest went to the chat while the first answer still ran, and the chat put it off as busy. Stopping replaced requests now runs as one chain, and a new request waits for all of it, including stops added while it waited. * Leave answers handed over as a realtime call ends running in the chat - A request handed to the chat as the call ended was stopped by the bridge engine, though the chat should go on answering it. Both engines now stop only following such an answer, through a new DelegatedTurn.close that leaves the answer running, and its listener no longer outlives the call. - A GPT-Live request still being handed to the chat when a newer one came is now stopped once the chat has it, before the newer request goes, so the chat never takes two at once. * Name Apple chats where Server speech says it uses the Voice provider The Voice provider page says Direct and Apple chats use it, but the Server speech descriptions named only Direct chats. Both now say Direct and Apple chats, in every language, worded as each language's Voice provider description already words it. * Keep realtime calls working alongside enforced temporary chats Open WebUI 0.12 support (#843) made a new chat start temporary when the server enforces it, and reading that now reaches the signed-in account and its permissions. Three things follow for realtime calls: - A call that starts a new chat stays Standard when the server will keep that chat temporary, as it already did for a temporary chat that is open. - A realtime route that cannot be read keeps the call Standard instead of failing it, like anything else the route cannot tell. - The Hermes route tests say the server enforces nothing, as they never sign in to Open WebUI. * End a realtime call at once when it is hung up while connecting - Hanging up while a realtime voice was still connecting waited for the start queued ahead of the stop, so the microphone and connection could stay up until the connection was made or timed out. The stop now ends a connecting voice right away, and the start it cuts short counts as cancelled, not failed. - A voice reply's "spoken" label kept its old language after the app's language changed: the cached header now keys on the label as shown. * Let every end of a realtime call wait for its one ending A hang-up while the voice connects ends it at once, and the teardown that follows ends it again. The second end returned as soon as the voice was marked ended, so teardown could turn off the audio session before the voice had stopped, and an error stopping it went unreported. Both engines now keep their ending: every end waits for the same one, and teardown reports what it throws. * End a connecting realtime call at once when the system call UI hangs up Hanging up from the CallKit screen queued the stop without the immediate end a hang-up in the app gets, so a realtime voice still connecting kept its microphone and connection until it connected or timed out, and could connect after the call had ended. Both hang-ups now share that immediate end, and the system's own end of the call is still not repeated. * Fix what Greptile's full review found in realtime calls - A chat opened while a call was still setting up kept the call going with the voice made for the chat the user left. The call now notes its chat when it starts, and a different chat open by the time it connects cancels it. - Stopping a request the call replaces now waits for the chat's stop to settle, so the stop's late effects cannot reach the new request. - An answer whose send failed after it was placed is marked failed in the chat, the way a typed send's is, instead of staying shown as running and keeping the chat busy. - Small talk waits while the chat is answering, whoever asked; it used to wait only for answers the call had delegated, so a typed answer could stop being the chat's last message and could no longer be stopped. - Pause holds the voice's answers until the call resumes, instead of only muting the microphone while finished answers went on being spoken. - Speech to text set to Server uses the device in a chat whose backend cannot transcribe, as the Voice provider promised, instead of hiding voice input. - Choosing the native Voice provider's connection, or editing its fields, now redraws that page, which iOS redraws only when it is the page the update is for. * Hold a realtime call to the chat it was asked for, and unpause on unmute - The chat a call is for is now noted when the call is asked for, before the setup that waits for the microphone and the route. A chat opened during that wait cancels the call instead of becoming its chat. - Unmuting a paused realtime call, from the app or CallKit, now resumes it too; the voice stayed paused and kept its answers back while the call showed as live. * Keep held small talk from silencing requests, and pause notices honest - Small talk said while a typed answer runs waited on the call's own writes, which a request then waited behind for minutes, so the caller heard nothing instead of being told the chat was busy. It is now held apart: a request meanwhile hears at once that the chat is busy, and one made after the answer comes after the held words. - A GPT-Live request that waited for approval while the call was paused, then finished, no longer has the voice say on resume that it still waits; its answer is still said. - The native Voice provider page is patched alone and at once when it opens, rather than after the speech voices it does not show have loaded, so the patch cannot land over text the user is typing. * Leave GPT-Live answers alone as a call closes, and drop replaced notices - A request the voice handed over while the call was closing replaced the answer still running, which stopped it although an ended call leaves its answer to go on in the chat. Such a request now replaces nothing. - A request replaced while the call was paused had its held approval notice said on resume, as if it still waited. Replacing a request now drops what was held about it. |
||
|---|---|---|
| .github | ||
| android | ||
| assets | ||
| docs | ||
| hermes-src@939e45c91d | ||
| ios | ||
| lib | ||
| openwebui-src@15818d88fd | ||
| packages | ||
| pigeons | ||
| scripts | ||
| test | ||
| third_party | ||
| tool | ||
| .gitignore | ||
| .gitmodules | ||
| .metadata | ||
| analysis_options.yaml | ||
| cargokit_options.yaml | ||
| devtools_options.yaml | ||
| l10n.yaml | ||
| LICENSE | ||
| paseo.json | ||
| PRIVACY_POLICY.md | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
| THIRD_PARTY_NOTICES.md | ||
Conduit
A native mobile client for Open WebUI, your own model endpoints, and self-hosted agents.
Connect · Screenshots · Features · Getting Started · Privacy · Build from Source
Open WebUI is excellent on the desktop. On mobile it breaks down at the edges: authentication behind a reverse proxy, streaming that drops when the app backgrounds, getting a screenshot into a prompt, starting a chat from the home screen. Conduit is a real Flutter app built to close that gap, and as of 4.0 it works with or without an Open WebUI server at all.
Your chats live on your device first. Nothing routes through a backend the maintainer operates.
Ways to connect
On first launch Conduit asks how you want to connect. Pick one, add the others later, and skip any sign-in you don't need.
| Open WebUI | Your self-hosted server | Full feature set: chats, folders, notes, channels, workspace, tools, web search, image generation |
| Direct | OpenAI-compatible, Ollama, OpenRouter | Talk straight to a provider or a model on your own machine. No Open WebUI account required |
| Apple On-Device | Apple Intelligence | Run Apple's local model offline on eligible iOS 26 devices without an API key |
| Apple PCC | Apple Private Cloud Compute | Use Apple's private cloud model on eligible iOS 27 devices without an API key |
| Hermes | Your self-hosted agent | An agent that runs tools, asks before sensitive steps, and works on a schedule |
Direct connections cover OpenAI-compatible endpoints (Chat Completions or Responses), LM Studio, Azure-style API versions, native Ollama, and first-party OpenRouter. Bring an API key, or skip it for a local endpoint that doesn't want one. Direct connections you already configured in Open WebUI come along automatically. Keys and custom headers stay in platform secure storage.
Apple On-Device is an iOS-only Direct provider backed by Apple's local SystemLanguageModel. It requires iOS 26 and Apple Intelligence, works offline, and supports streaming, sampling controls, and JSON-schema responses with a 4K context window. Image input, reasoning controls, and tool calling are not available.
Apple Private Cloud Compute is an iOS-only Direct provider. It uses Apple's Foundation Models framework, streams responses through the same chat path as other Direct models, and requires iOS 27, Apple Intelligence availability, and Apple's managed PCC entitlement. It supports image input, reasoning levels, sampling and output limits, JSON-schema responses, live quota/context status, and an optional on-device fallback for PCC network failures. Tool calling is not enabled.
Hermes Agent connects to your own Hermes server. You watch its tools work live, approve sensitive steps before they run, and let scheduled agents run while you sleep. Conversations and schedules get their own tab, and Conduit only exposes the capabilities your server actually reports.
Screenshots
| Chat | Connect | Chats | Voice |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
What you get
Chat that survives mobile
Token-by-token streaming over WebSocket. The transcript holds its place while a response grows, pinned prompts stay put, and long conversations load without stalling the UI. Search across conversations, organize with folders, pin what matters, or start a temporary chat that leaves nothing behind.
Rendering that holds up on a phone
Native Flutter surfaces, not a web view wrapped in a shell:
- syntax-highlighted code blocks with copy and preview
- Mermaid diagrams rendered natively
- LaTeX and math
- expandable reasoning, tool-call, and code-execution sections
- inline citations, source cards, and follow-up suggestions
- Chart.js embeds
A real Workspace
Models, knowledge, prompts, tools, and skills as native screens with unified settings navigation. Sections you don't have permission for simply don't appear.
Everything else
| Area | What's included |
|---|---|
| Files and media | Uploads, re-attaching previously uploaded server files, multimodal prompts, clipboard image paste, audio attachments |
| Notes | Autosave, pinning, AI-generated titles, AI enhancement, audio recording, all available offline |
| Channels | Threads and reactions, when your server enables them |
| Voice | Voice input with on-device or server speech recognition, plus a full voice-call mode |
| Home screen | Widgets on iOS and Android for new chat, mic, camera, photos, and clipboard; app quick actions; iOS App Intents and Shortcuts |
| Sharing | Share-sheet ingestion from other apps straight into a prompt |
| Terminal | Interactive sessions over WebSocket with a file browser, shown only when your server exposes it |
| Personalization | Light, dark, and system themes; five accent palettes; adaptive Material and Cupertino UI; haptics |
| Languages | 14 locales: English, German, Spanish, French, Italian, Japanese, Korean, Dutch, Russian, Simplified and Traditional Chinese, Czech, Slovak, Polish |
Server-dependent features (channels, notes, web search, image generation, toggle filters, terminal) appear only when your deployment exposes them.
Built for self-hosted reality
Most mobile clients assume a plain login form. Real deployments rarely look like that.
- Every auth path Open WebUI offers: username and password, LDAP, manual JWT entry, SSO and OAuth providers.
- Reverse proxies actually work:
oauth2-proxy, Authelia, Authentik, Pangolin, and Cloudflare Tunnel, by capturing the right cookies and session state on-device. - Custom headers during setup for environments that require
X-API-Key,Authorization, or organization routing headers. - Credentials in Keychain or Keystore, never plain-text local storage.
- Tracks upstream: Conduit supports Open WebUI 0.11.
Getting started
Install from the App Store or Google Play, then pick how you want to connect.
Open WebUI
- Launch Conduit and choose Open WebUI.
- Enter your instance's base URL.
- Add any required custom headers.
- Sign in with username and password, LDAP, JWT, SSO, or proxy auth.
- Pick a model and start chatting.
Direct connection
- Launch Conduit and choose Direct connection.
- Add an OpenAI-compatible or Ollama profile with its base URL and any API key or custom headers.
- Test the connection, enable it, and select a discovered or manually entered model.
- Choose whether new direct chats use Open WebUI history when one is signed in, or stay only on this device. Existing chats keep their current location.
- Start chatting. No Open WebUI account required.
Apple On-Device
- On an eligible iOS 26 device, launch Conduit and choose Apple On-Device.
- Conduit checks Apple Intelligence and local-model availability. No API key or network connection is required.
- Start chatting. New Apple-only installations keep Direct history on the device unless you later enable Open WebUI history.
Apple Private Cloud Compute
- On an eligible iOS 27 device, launch Conduit and choose Apple Private Cloud Compute.
- Conduit checks Apple Intelligence and PCC availability. No API key is required.
- Start chatting. New PCC-only installations keep Direct history on the device unless you later enable Open WebUI history.
Hermes Agent
- Launch Conduit and choose Hermes Agent.
- Enter your Hermes server URL and
API_SERVER_KEY. - Optionally set a memory key to scope the agent's long-term memory to you. One is generated automatically on first chat if you leave it blank.
- Open the Hermes tab for conversations and scheduled agents.
Privacy
- Chats, notes, and drafts are stored on your device. Notes and drafts stay available without a connection.
- Credentials use platform secure storage: Keychain on iOS, Keystore on Android.
- No third-party analytics or advertising SDKs.
- Diagnostic logging is local and transient.
- No developer-operated backend relays your data. Traffic goes from your device to the server or provider you configured, and nowhere else.
- Signing out lets you choose what stays behind. Clearing everything clears on-device chats too, and fails safely rather than half-deleting.
Full details in PRIVACY_POLICY.md.
Build from source
See docs/BUILDING.md for requirements, submodules, codegen, verification, project layout, and troubleshooting.
git clone --recursive https://github.com/cogwheel0/conduit.git
cd conduit
flutter pub get
dart run build_runner build
flutter run -d ios # or: flutter run -d android
Clone recursively and run build_runner. The Mermaid renderer is a submodule,
and generated Dart files are git-ignored.
Contributing
Conduit is actively developed and feedback is welcome.
- Bugs → GitHub Issues
- Features, deployment notes, questions → GitHub Discussions
Unsolicited pull requests are not the primary contribution path right now. Open an issue or discussion first so changes line up with the roadmap.
Enterprise and white-label
For private distribution, internal deployment support, or a custom enterprise/white-label build, open a discussion or reach the maintainer at cogwheel@cogwheel.app.
Support
If Conduit is useful to you, you can support development through GitHub Sponsors or Buy Me a Coffee.
Acknowledgements
- Supported by the Vercel OSS Program.
- Tested with BrowserStack.
- Code review provided by
.
License
Released under the GPL-3.0 License.
Conduit is an independent client and is not affiliated with Open WebUI.



