Terminal UI for switching local AI tool providers without hand-editing config files every time you move between DeepSeek, OpenRouter, a team gateway, or a local endpoint.
ezswitch keeps provider profiles under ~/.config/ezswitch/, lets you edit
them in a Textual interface, and asks each app plugin to write the selected
provider into the target tool's native config.
Built-in app plugins:
codexupdates~/.codex/config.tomland~/.codex/auth.json.claudeupdates~/.claude/settings.json.
The UI disables Switch when the selected app does not appear to be installed.
Plugins declare this with requires_cli and requires_path, so custom plugins
can adapt the same check for local tools, wrappers, or gateway CLIs.
uv tool install .This installs the ezswitch launcher into uv's tool bin directory, usually
~/.local/bin. Make sure that directory is on your PATH.
For development:
uv run ezswitch
uv run pytestezswitchOn startup, ezswitch creates:
~/.config/ezswitch/
~/.config/ezswitch/apps/
~/.config/ezswitch/codex.providers.json
~/.config/ezswitch/claude.providers.json
Existing provider files are never overwritten during initialization.
Provider files are JSON objects named {app}.providers.json.
{
"name": "codex",
"active": "8b3f6d7e-4c17-4f7c-8f7a-9bb2d83c9a3d",
"providers": {
"8b3f6d7e-4c17-4f7c-8f7a-9bb2d83c9a3d": {
"name": "openrouter",
"base_url": "https://openrouter.ai/api/",
"api_key": "...",
"model": "anthropic/claude-sonnet-4.5",
"model_list": [
"anthropic/claude-sonnet-4.5",
"openai/gpt-5"
],
"note": "shared fallback provider"
}
}
}Sync tries the OpenAI-compatible models endpoint and stores data[*].id into
model_list. For base URLs like https://openrouter.ai/api/, it tries:
https://openrouter.ai/v1/models
https://openrouter.ai/api/v1/models
Selecting a model writes it back to the provider's model field. Clear
empties the cached model list.
User plugins live in ~/.config/ezswitch/apps/{name}.py and override built-ins
with the same filename.
from ezswitch.core.plugins import AppPlugin
from ezswitch.core.editors import JsonEditor
class MyTool(AppPlugin):
display_name = "My Tool"
description = "Patch My Tool provider settings"
requires_cli = ("my-tool",)
requires_path = ("~/.my-tool",)
def switch(self, provider):
config = JsonEditor("~/.my-tool/settings.json")
config.update("base_url", provider.base_url)
config.update("api_key", provider.api_key)
config.update("model", provider.model)If requires_cli or requires_path is not satisfied, the app remains visible
but switching is disabled with a toast message.
The README image lives at docs/ezswitch.png. Regenerate it from a clean
temporary config whenever the TUI layout changes.
