Contents
Project
- Name: Henry Model Manager
- Author: Henry
- Version:
3.3- Stack: SwiftUI, AppKit, Foundation, launchctl, IOHID, LM Studio API
- Nature: personal macOS tool; source is currently private
- Article: Henry
I regularly use four model entry points:
- Codex for code and multi-step tasks
- dsh (DeepSeek Harness) for DeepSeek in the browser
- LM Studio for local Qwen and DeepSeek models
- A cloud gateway connected through cc-switch
Choosing a model sounds simple. In practice, every switch touches ~/.codex/config.toml, ~/.dsh/settings.yaml, LM Studio’s loaded models, the dsh service process, and each model’s context and reasoning requirements.
When one part is missed, Codex points at a provider that no longer exists, dsh still uses an old model, LM Studio loads duplicate multi-gigabyte instances, a cloud model is active while a local model still holds memory, or launchd fights with a manually killed process over the same port.
Henry Model Manager is therefore not another chat client. It is a model orchestration layer.
1. A menu-bar switch became state management
The first version only switched Codex between local Qwen, local DeepSeek R1, and a cloud DeepSeek model. It edited one config, preloaded a 64K context for local models, and unloaded local models before switching to the cloud.
It soon failed for obvious reasons: it ignored dsh, duplicated LM Studio instances, and had no idea who was using a local model.
2. Models as referenced resources
Local models are expensive memory resources, not just configuration values. The manager records which entry point references each loaded model. Codex and dsh may share a model, use different models, or release their references independently. LM Studio unloads a model only when the last reference disappears.
This reference-counting model is more accurate and far more memory-efficient than unloading everything on every switch.
3. Three entry points, three protocols
Qwen3-Coder locally
Codex uses the lmstudio-local provider with a 64K context and reasoning effort set to none. LM Studio’s Responses endpoint does not accept every reasoning or function-call input that Codex emits, so the protocol boundary has to be explicit.
DeepSeek R1 locally
Codex and dsh read different configuration files and have different expectations for model name, base URL, vision support, and context length.
DeepSeek V4 Flash in the cloud
Codex uses a custom provider, dsh uses the cc-switch proxy, and all local models are unloaded. The manager enforces this instead of relying on the user to remember.
4. Switching is a small transaction
A switch performs several steps:
- Back up Codex and dsh configuration.
- Write provider, model, and reasoning fields.
- Ensure the required provider block exists.
- Synchronize loaded models by reference.
- Restart dsh through launchd when needed.
- Save history.
- Update the interface and notification state.
Backups live beside the original files. Switch history is stored under:
~/Library/Application Support/HenryModelManager/history.json
That makes it possible to recover quickly after choosing the wrong model.
5. dsh must be managed by launchd
The dsh web service is managed by a launch agent with KeepAlive enabled. Killing the process only makes launchd start it again. Bad restart logic can also create duplicate bind attempts.
The app uses launchd lifecycle commands:
launchctl kickstart gui/$UID/com.henry.dsh.web
launchctl kickstart -k gui/$UID/com.henry.dsh.web
launchctl bootout gui/$UID/com.henry.dsh.web
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.henry.dsh.web.plist
“Start” and “stop” are not merely PID operations. They distinguish whether a service is loaded from whether a process is running.
6. Temperature sensors and Mach ports
The app also displays SoC and battery temperature through an IOHID C interface. The difficult part is resource ownership. Repeatedly creating an event-system client without releasing its Mach resources leaks ports over time.
The client is cached, Mach objects are released, and failed reads become an unavailable temperature state rather than a crash.
7. Keychain can interrupt startup
The first balance monitor read the API key from Keychain during launch. macOS displayed a permission prompt over the main window, which looked like a blank app.
The fix was not to remove Keychain. It was to request access only when the balance screen is opened.
8. Three equal cards
The Codex, dsh, and LM Studio cards originally had unequal heights and one card became gray because it used .disabled(true) only to suppress actions. Fixed-height layout, Spacer, and plain views for cards without menus made the interface intentional.
9. What it includes now
- Live Codex, dsh, and LM Studio status
- Local and cloud model switching
- Apply to one tool or both
- Reference-counted local memory
- launchd-based service control
- Model, memory, process, temperature, and balance monitoring
- Keychain-stored DeepSeek keys
- Reversible switch history
- Startup and notification preferences
The app is about 2,600 lines of Swift. Its value is not the amount of code but the state synchronization it removes from daily use.
10. Closing thought
Switching a model looked like a dropdown. It turned out to involve configuration formats, provider protocols, model lifecycles, launchd, Keychain, Mach resources, SwiftUI state, and failure recovery.
The best personal tools grow from repeatedly solving a problem you actually have.
Project: Henry Model Manager. Author of both the tool and this article: Henry. Version 3.3; source currently private.
Comments 0
Leave a thought or add to the conversation. Comments appear after moderation.
Loading comments…
Be the first to leave a thought.