Skip to content

how to contribute patterns and conventions

Nik edited this page May 30, 2026 · 2 revisions

Patterns and conventions

Conventions that hold across the DroidProxy codebase. Read this before changing code so your edits match the existing style and don't break cache-sensitive or security-sensitive paths.

Source of truth

The compiled app code lives under src/. Treat src/Sources/**, src/Info.plist, and create-app-bundle.sh (repo root) as the source of truth. There is no longer a mirrored top-level resources/ tree — older notes referencing one are stale. Bundled resources live in src/Sources/Resources/.

Logging

Use NSLog, not print or os_log. Menu-bar and server lifecycle logs use bracketed prefixes such as [ThinkingProxy], [ServerManager], [AuthStatus], [SettingsView]. The proxy additionally writes a per-request file log via ThinkingProxy.fileLog(_:) to /tmp/droidproxy-debug.log.

Surgical JSON editing (do not re-serialize)

The single most important convention: ThinkingProxy edits request JSON by string insertion at located ranges, never by JSONSerialization.data(...) round-trips. JSONSerialization reorders keys alphabetically, which breaks Anthropic's prompt-cache matching and bloats latency. The helpers findObjectFieldLocations, consumeJSONValue, parseJSONStringToken, and friends locate a field's byte range so a value can be replaced or a sibling inserted in place.

ClaudeThinkingBlockSanitizer (src/Sources/ClaudeThinkingBlockSanitizer.swift) follows the same rule — it computes index ranges of blocks to delete and rebuilds the string by copying spans, preserving key order.

If you need to read a field for a routing decision, scan only the small set of top-level keys you need (routingInspectionKeys = model, service_tier, thinking) so the scan can early-exit before traversing the (potentially huge) messages array. Debug-only keys are scanned separately (reasoningLogInspectionKeys) and only when logging.

The proxy forwards reasoning unchanged

Reasoning effort is owned by Droid CLI, not the proxy. Do not add injection of thinking, reasoning, reasoning_effort, output_config, budget_tokens, or generationConfig.thinkingConfig. The proxy's allowed mutations are a closed set: Anthropic-Beta header rewrite, service_tier=priority for fast mode, Gemini path rewrite, model-alias rewrite, Claude thinking-block sanitization, and Cursor routing. See Thinking proxy.

Preferences

User preferences are UserDefaults-backed and centralized in AppPreferences (src/Sources/AppPreferences.swift) as a namespace of keys, defaults, and computed accessors. SwiftUI reads them with @AppStorage(AppPreferences.<key>). User-controlled values that feed generated config (e.g. bindAddress) are validated in AppPreferences before use — trim, reject empty/multi-line input, and fall back to the default.

Config generation

ServerManager.getConfigPath() produces ~/.cli-proxy-api/merged-config.yaml by string-replacing anchors in the bundled config.yaml (e.g. host: 127.0.0.1, allow-remote: false). When replacing the bind host, replace only the first anchor and log a warning if the anchor is missing, so silent config drift is visible. The merged file is written atomically with 0o600 permissions.

File permissions on secrets

Files containing credentials or secret keys (the merged config, cursor.json) are written with 0o600 via FileManager.setAttributes([.posixPermissions: 0o600], ...). Preserve this when adding new files that hold tokens.

Concurrency

  • UI work happens on the main thread; background work posts results back with DispatchQueue.main.async.
  • Long-lived watchers (AuthDirectoryMonitor) are debounced DispatchSource file watchers; always pair start() with stop() in lifecycle hooks and deinit.
  • OAuthUsageTracker is @MainActor and uses Task / withTaskGroup; cancel the in-flight refreshTask before starting a new refresh.
  • Notifications use the shared Notification.Name constants in src/Sources/NotificationNames.swift (serverStatusChanged, authDirectoryChanged) plus droidProxyThemeChanged defined on AppDelegate.

Networking style

The proxy is hand-rolled on Network.framework (NWListener/NWConnection). It reads requests iteratively (async scheduling, not recursion) to avoid stack buildup on large payloads, honors Content-Length to know when a body is complete, and always sends Connection: close — there is no keep-alive or pipelining support. Local backend traffic stays on 127.0.0.1:8318.

UI theming

SettingsView (src/Sources/SettingsView.swift) supports a Liquid Glass look (macOS 26+, via droidGlassCard / droidGlassCapsule / droidGlassProminent view helpers with availability fallbacks) and an OLED-black theme. Theme/opacity changes post droidProxyThemeChanged so the window alpha updates live. Icons come through the cached IconCatalog (src/Sources/IconCatalog.swift) — don't re-decode images per access.

Tests

Tests live in src/Tests/CLIProxyMenuBarTests/ and use XCTest. The existing suite covers ClaudeThinkingBlockSanitizer behavior. Run with cd src && swift test. See Testing.

Clone this wiki locally