Skip to content

overview architecture

Nik edited this page May 30, 2026 · 2 revisions

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.

The two-server design

DroidProxy runs two servers on localhost:

  1. ThinkingProxy on localhost:8317 — a raw TCP HTTP proxy written from scratch on top of Network.framework. This is the endpoint Droid CLI connects to. Implemented in src/Sources/ThinkingProxy.swift.
  2. 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 via src/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]
Loading

Startup ordering

AppDelegate.applicationDidFinishLaunching (src/Sources/AppDelegate.swift) wires the app up in a deliberate order:

  1. Build the menu bar status item and menu.
  2. Instantiate ServerManager and ThinkingProxy.
  3. Call startServer(), which starts the ThinkingProxy first, then polls up to 60 times (50 ms apart) for thinkingProxy.isRunning before starting the backend.
  4. Once the proxy is ready, ServerManager.start launches cli-proxy-api with 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).

Request lifecycle

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)
Loading

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 map

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

Data and config locations

  • ~/.cli-proxy-api/ — OAuth credential JSON files (one per account), plus the generated merged-config.yaml and optional logs/. See AuthPaths.
  • ~/.factory/settings.json — Factory's config, where DroidProxy writes its customModels entries (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.

Threading model

  • The menu bar UI and SwiftUI settings run on the main thread.
  • ThinkingProxy runs its listener and per-connection handlers on a global userInitiated queue, posting isRunning updates back to main.
  • ServerManager runs process termination on a dedicated serial queue and posts serverStatusChanged notifications on main.
  • OAuthUsageTracker is @MainActor and uses structured concurrency (withTaskGroup) for parallel quota fetches.

For coding conventions that hold across these components, see Patterns and conventions.

Clone this wiki locally