One API key, three terminals: driving many models from the shell with llm, mods and aichat
Introduction
The fastest way to use an LLM is often not a chat window - it's a pipe. git diff | ... "write a commit message", ... "explain this error" < stderr.log, cat main.py | ... "find the bug". Three of the best command-line tools for this - llm, mods, and aichat - all accept a custom OpenAI-compatible endpoint, which means one key can back all three and give you Claude, GPT, Gemini and more from the shell.
This is the terminal setup, end to end. It's genuinely tool-agnostic: every config below works against any OpenAI-compatible endpoint. I use daoxe as the example endpoint (disclosure below); swap the base URL for your own.
Disclosure: I work on daoxe, an OpenAI-compatible gateway (it also speaks Anthropic Messages natively). Everything here is standard OpenAI-compatible config - point these tools at whatever endpoint you trust.
One convention across all three: the key lives in an environment variable, DAOXE_API_KEY, never on the command line (so it doesn't land in your shell history). And model ids are account-scoped - list yours with curl https://daoxe.com/v1/models -H "Authorization: Bearer $DAOXE_API_KEY".
1. llm (Simon Willison) - the extensible one
llm is the most extensible of the three: a pluggy-based plugin system, a local SQLite log of everything, templates, embeddings. There are two ways to add an OpenAI-compatible gateway.
Option A - zero-YAML via a plugin
There's a small plugin, llm-daoxe, that registers the gateway's models and adds a command to pull your account's real model list:
llm install llm-daoxe # (coming soon on PyPI; until then: clone + `pip install -e .`)
llm keys set daoxe # store the key (or export DAOXE_API_KEY=...)
llm daoxe models --refresh # pull your account's real models from GET /v1/models, and cache them
llm -m daoxe/claude-sonnet-4-6 "In one line, what's an OpenAI-compatible gateway?"
git diff | llm -m daoxe/gpt-5.5 -s "Write a concise commit message."
Model ids are namespaced as daoxe/<model> so they don't collide with other plugins' ids. The key is read only from llm's key store or DAOXE_API_KEY, and it's never printed.
Option B - no plugin, native config
Don't want a plugin? llm natively reads custom OpenAI-compatible models from extra-openai-models.yaml in its config dir:
- model_id: daoxe/claude-sonnet-4-6
model_name: claude-sonnet-4-6
api_base: https://daoxe.com/v1
api_key_name: daoxe # matches `llm keys set daoxe`
can_stream: true
- model_id: daoxe/gpt-5.5
model_name: gpt-5.5
api_base: https://daoxe.com/v1
api_key_name: daoxe
The plugin's advantage is just convenience: zero YAML plus llm daoxe models --refresh to auto-discover your account's models. Both paths use llm's built-in OpenAI transport, so streaming and tools behave the same as with official models.
2. mods (charmbracelet) - the pipe-native one
mods is built for Unix pipes and looks great doing it. Open its config with mods --settings and add a provider under apis::
default-api: daoxe # optional: make it the default so you can skip -a
apis:
daoxe:
base-url: https://daoxe.com/v1
api-key-env: DAOXE_API_KEY
models: # use real ids from GET /v1/models; max-input-chars is a client-side value
claude-sonnet-4-6:
aliases: ["daoxe-sonnet"]
max-input-chars: 600000
gpt-5.5:
aliases: ["daoxe-gpt"]
max-input-chars: 600000
export DAOXE_API_KEY=your-daoxe-key
mods -a daoxe -m claude-sonnet-4-6 "explain this diff" < patch.diff
mods --model daoxe-sonnet "summarize" < README.md # via alias
Gotcha: mods requires api-key-env to point at an environment variable that already exists, or it complains about a missing key. max-input-chars is a client-side truncation setting - tune it to your model, it's not a claim about the gateway.
3. aichat (sigoden) - the all-in-one one
aichat is a single binary that's a CLI, a REPL, and a local server. It uses an openai-compatible client type. Edit ~/.config/aichat/config.yaml:
clients:
- type: openai-compatible
name: daoxe
api_base: https://daoxe.com/v1
# omit api_key and aichat auto-reads DAOXE_API_KEY (client name โ {NAME}_API_KEY)
models: # real ids from GET /v1/models; tune capabilities per model
- name: claude-sonnet-4-6
max_input_tokens: 200000
supports_function_calling: true
- name: gpt-5.5
max_input_tokens: 272000
supports_function_calling: true
export DAOXE_API_KEY=your-daoxe-key
aichat -m daoxe:claude-sonnet-4-6 "explain this code" < main.py
aichat --list-models | grep daoxe
Nice touch: because the client is named daoxe, aichat automatically looks for DAOXE_API_KEY - you can omit the api_key field entirely. Model selection is client:model, e.g. daoxe:claude-sonnet-4-6.
Which one should you use?
| Tool | Best for | Superpower |
|---|---|---|
llm |
scripting, logging, plugins | SQLite log of every call; huge plugin ecosystem |
mods |
quick pipes | gorgeous TUI output, alias/fallback config |
aichat |
interactive + server | REPL, roles, and a built-in local API server |
They're not mutually exclusive - I keep all three configured against the same key. The same mental model applies to other CLIs too: anything that takes an OpenAI-compatible base_url + key + model id works the same way (point the base URL at the /v1 root, use a real id from /v1/models, keep the key in an env var). Verify each tool's current config fields before you rely on them - these tools move fast.
The reason one key matters here
CLI users tend to script things and forget them: a cron job that summarizes logs, a git hook that drafts commit messages, a shell function that explains errors. When each of those needs a different vendor key, it's a mess of secrets and bills. One OpenAI-compatible key across llm, mods, and aichat - and across every script that calls them - means one secret to rotate, one bill to read, and model choice as a string.
daoxe is the endpoint I point them at: https://daoxe.com/v1, one key across many models, plus native Anthropic Messages if a tool wants Claude's protocol. It's not available in mainland China. As always, none of the config above is daoxe-specific - and because it's a standard endpoint, you can benchmark and verify it rather than trust it.
TL;DR
llm,mods, andaichatall take a custom OpenAI-compatible endpoint - one key backs all three.llm: a plugin (llm-daoxe, coming soon) orextra-openai-models.yaml.mods: anapis:block;api-key-envmust point at an existing env var.aichat: anopenai-compatibleclient; name itdaoxeand it auto-readsDAOXE_API_KEY.- Keep the key in an env var, use exact ids from
/v1/models, and pipe away.
Comments
No comments yet. Start the discussion.