-
Notifications
You must be signed in to change notification settings - Fork 12
overview architecture
DroidProxy is a menu bar app (LSUIElement, no dock icon) that supervises two local servers and writes configuration into the user's home directory. This page explains the two-server design, the request lifecycle, and how the Swift components connect.
DroidProxy runs two servers on localhost:
-
ThinkingProxy on
localhost:8317— a raw TCP HTTP proxy written from scratch on top ofNetwork.framework. This is the endpoint Droid CLI connects to. Implemented insrc/Sources/ThinkingProxy.swift. -
CLIProxyAPI on
127.0.0.1:8318— a bundled third-party binary (src/Sources/Resources/cli-proxy-api) that holds the OAuth logic for every provider. DroidProxy starts, stops, and configures it as a child process viasrc/Sources/ServerManager.swift.
The proxy sits in front of the backend so DroidProxy can inspect and lightly rewrite requests (headers, fast-mode tier, path rewrites) without forking the backend. The user always connects to 8317; 8318 is an implementation detail bound to loopback.
graph TD
subgraph Mac[Your Mac]
Droid[Droid CLI] -->|POST :8317| TP[ThinkingProxy<br/>Network.framework]
TP -->|POST :8318| BE[CLIProxyAPI<br/>child process]
BE -->|reads| AuthDir[(~/.cli-proxy-api/<br/>OAuth tokens)]
App[Menu bar app<br/>AppDelegate] -->|supervises| TP
App -->|supervises| BE
Settings[SettingsView] -->|OAuth login| BE
Settings -->|writes models| FactoryCfg[(~/.factory/settings.json)]
end
BE -->|HTTPS + OAuth| Claude[Anthropic]
BE -->|HTTPS + OAuth| Codex[OpenAI]
BE -->|HTTPS + OAuth| Gemini[Google / Antigravity]
BE -->|HTTPS + OAuth| Kimi[Moonshot]
TP -. Cursor beta .-> Cursor[cursor-api.standardagents.ai]
AppDelegate.applicationDidFinishLaunching (src/Sources/AppDelegate.swift) wires the app up in a deliberate order:
- Build the menu bar status item and menu.
- Instantiate
ServerManagerandThinkingProxy. - Call
startServer(), which starts the ThinkingProxy first, then polls up to 60 times (50 ms apart) forthinkingProxy.isRunningbefore starting the backend. - Once the proxy is ready,
ServerManager.startlaunchescli-proxy-apiwith the merged config. If the backend fails, the proxy is stopped again to keep state consistent.
Stopping reverses the order: the proxy stops accepting new connections first, then the backend is terminated (SIGTERM, then SIGKILL after a 2 s grace period).
A typical POST from Droid CLI flows through ThinkingProxy.processRequest:
sequenceDiagram
participant D as Droid CLI
participant P as ThinkingProxy :8317
participant B as CLIProxyAPI :8318
participant U as Upstream provider
D->>P: POST /v1/messages (or /v1/responses, etc.)
P->>P: Accumulate body until Content-Length satisfied
P->>P: Inspect JSON (model, thinking, service_tier)
P->>P: Rewrite model aliases / fast-mode / path / Anthropic-Beta
P->>P: Sanitize stale Claude thinking blocks
P->>B: Forward rewritten request (Connection: close)
B->>U: Authenticated upstream call (OAuth token)
U-->>B: Streamed response (SSE)
B-->>P: Streamed response
P-->>D: Streamed response (pass-through)
The proxy is intentionally thin. It does not inject reasoning effort — that is owned by Droid CLI. What it does do is documented in Thinking proxy: Anthropic-Beta header rewriting for visible thinking, service_tier=priority injection for fast mode, Gemini path rewriting, model-alias rewriting, Claude thinking-block sanitization, and Cursor routing.
| Component | File | Responsibility |
|---|---|---|
| App entry point | src/Sources/main.swift |
Creates NSApplication + AppDelegate
|
| App shell | src/Sources/AppDelegate.swift |
Menu bar, lifecycle, Sparkle updater, server startup ordering |
| Request proxy | src/Sources/ThinkingProxy.swift |
TCP HTTP proxy on :8317, request/response rewriting |
| Backend supervisor | src/Sources/ServerManager.swift |
Starts/stops cli-proxy-api, merges config |
| Settings UI | src/Sources/SettingsView.swift |
SwiftUI window: providers, fast mode, themes, remote access |
| Model catalog | src/Sources/DroidProxyModelCatalog.swift |
Authoritative list of exposed Factory models |
| Auth manager | src/Sources/AuthStatus.swift |
Parses credential files, enable/disable/delete accounts |
| Usage tracker | src/Sources/OAuthUsageTracker.swift |
Fetches Claude/Codex quota windows |
| Preferences | src/Sources/AppPreferences.swift |
UserDefaults-backed settings |
| Auth watcher | src/Sources/AuthDirectoryMonitor.swift |
Debounced DispatchSource on ~/.cli-proxy-api
|
| Thinking sanitizer | src/Sources/ClaudeThinkingBlockSanitizer.swift |
Strips stale Claude thinking blocks |
-
~/.cli-proxy-api/— OAuth credential JSON files (one per account), plus the generatedmerged-config.yamland optionallogs/. See AuthPaths. -
~/.factory/settings.json— Factory's config, where DroidProxy writes itscustomModelsentries (with a timestamped backup). See Reasoning and models. -
/tmp/droidproxy-debug.log— per-request reasoning log written by the proxy. - Bundled
config.yaml(src/Sources/Resources/config.yaml) — the template the backend config is merged from. See Configuration.
- The menu bar UI and SwiftUI settings run on the main thread.
-
ThinkingProxyruns its listener and per-connection handlers on a globaluserInitiatedqueue, postingisRunningupdates back to main. -
ServerManagerruns process termination on a dedicated serial queue and postsserverStatusChangednotifications on main. -
OAuthUsageTrackeris@MainActorand uses structured concurrency (withTaskGroup) for parallel quota fetches.
For coding conventions that hold across these components, see Patterns and conventions.