DEV Community

Cover image for One API key, three terminals: driving many models from the shell with llm, mods and aichat
Seven
Seven

Posted on

One API key, three terminals: driving many models from the shell with llm, mods and aichat

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."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
export DAOXE_API_KEY=your-daoxe-key
aichat -m daoxe:claude-sonnet-4-6 "explain this code" < main.py
aichat --list-models | grep daoxe
Enter fullscreen mode Exit fullscreen mode

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, and aichat all take a custom OpenAI-compatible endpoint — one key backs all three.
  • llm: a plugin (llm-daoxe, coming soon) or extra-openai-models.yaml.
  • mods: an apis: block; api-key-env must point at an existing env var.
  • aichat: an openai-compatible client; name it daoxe and it auto-reads DAOXE_API_KEY.
  • Keep the key in an env var, use exact ids from /v1/models, and pipe away.

Top comments (0)