-
Notifications
You must be signed in to change notification settings - Fork 14
how to contribute 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.
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/.
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.
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.
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.
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.
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.
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.
- UI work happens on the main thread; background work posts results back with
DispatchQueue.main.async. - Long-lived watchers (
AuthDirectoryMonitor) are debouncedDispatchSourcefile watchers; always pairstart()withstop()in lifecycle hooks anddeinit. -
OAuthUsageTrackeris@MainActorand usesTask/withTaskGroup; cancel the in-flightrefreshTaskbefore starting a new refresh. - Notifications use the shared
Notification.Nameconstants insrc/Sources/NotificationNames.swift(serverStatusChanged,authDirectoryChanged) plusdroidProxyThemeChangeddefined onAppDelegate.
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.
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 live in src/Tests/CLIProxyMenuBarTests/ and use XCTest. The existing suite covers ClaudeThinkingBlockSanitizer behavior. Run with cd src && swift test. See Testing.