aichat with a Custom OpenAI-Compatible Endpoint
Search for how to point aichat at a custom endpoint and you will find at least three different answers, because there are at least three different programs called aichat. They take different config files, in different formats, with different field names.
The one most people mean is the Rust binary by sigoden: a single executable that is a CLI, a REPL, and a local server. Its config uses api_base, and it has an ergonomic trick that removes the key field entirely. The forks use api_url, which is a different value (the full operation path rather than the version root).
Figuring out which one you installed takes ten seconds and saves an hour.
https://aicomp.ai/v1).
Create one free →
Which aichat do you have
| Program | Config file | Endpoint field | Value it expects |
|---|---|---|---|
| sigoden/aichat (Rust, single binary) | ~/.config/aichat/config.yaml | api_base | version root, e.g. https://gw.example.com/v1 |
| forks derived from the same lineage | ~/.config/aichat/config.yaml | api_url | full operation path, ending /v1/chat/completions |
| ctxinf/terminal-aichat (different program) | ~/.config/terminal-aichat/config.jsonc | providers.<name>.baseURL | version root |
If your config file has a clients: list, you have the one this page is about. If it has a flat api_key: and api_url: at the top level, you have a fork, and the value belongs at the operation path.
aichat --info prints the config path it is actually reading. That is the fastest way to stop guessing.
The current config shape
# ~/.config/aichat/config.yaml
clients:
- type: openai-compatible
name: gw
api_base: https://gw.example.com/v1
# api_key omitted on purpose — see below
models:
- name: claude-sonnet-5
max_input_tokens: 200000
supports_function_calling: true
- name: deepseek-v4-pro
max_input_tokens: 128000
supports_function_calling: false
Two things are doing work here.
name: gw is not decoration. When api_key is absent, aichat looks for an environment variable named after the client: the client name uppercased, with _API_KEY appended. gw becomes GW_API_KEY.
export GW_API_KEY=sk-your-gateway-key
This is the reason to keep the key out of the file. The config is a file you copy between machines and commit to dotfiles repositories; the key stays in the shell environment.
models: is a declaration, not a discovery step. aichat does not enumerate your gateway. The name values must match the IDs the gateway serves, and max_input_tokens and supports_function_calling are claims you are making about each one. A wrong supports_function_calling: true produces a role that fails only once tools are involved.
aichat --list-models prints what the config resolved to, which is the quickest way to confirm the client registered at all.
Selecting a model means naming the client
With more than one client, a bare model name is ambiguous, so selection is client:model:
aichat -m gw:claude-sonnet-5 "explain this stack trace" < trace.log
Inside the REPL, .model switches. Because the client name is part of the identifier, a model that works from the command line and fails in the REPL is usually a selection problem rather than a configuration one.
The version segment
The api_base value is the version root. aichat appends the operation path itself.
https://gw.example.com/v1is correct.https://gw.example.com/v1/chat/completionsproduces a path with the operation twice.https://gw.example.com/v1/produces a doubled slash that some gateways route and others reject, which is worse than an outright failure because it works on your machine and not on a colleague's.
If you are on a fork that expects api_url, all three of those flip: the fork wants the full operation path and will break if you give it a version root. This is the concrete reason the two field names exist, and why copying a config between builds fails in a way that looks like a network problem.
Where the tokens go
aichat is a shell client, so its shape depends entirely on how you use it. Two habits dominate.
One-shot pipes, a diff into a commit message, a log into an error explanation. Each call is self-contained: the piped text plus a short instruction. Cheap per call, and the call count is however many times you reach for it.
REPL sessions (the conversation accumulates and every turn resends the thread plus its attachments). The first question is cheap and the twentieth is not, because the twentieth carries all nineteen prior turns.
A day on this page's workload is 180k input and 40k output tokens: 3.6M input and 0.8M output across 20 working days. The lever is session length, not the model.
See output vs input pricing for the ratio arithmetic and prompt caching savings for why a long REPL session with a stable system prompt is cheaper than the same traffic in fresh processes.
| Model | Gateway rate in / out per 1M tokens | One day | 20 days |
|---|---|---|---|
| gpt-5.6-terra | $1.00 / $6.00 | $0.42 | $8.40 |
| claude-sonnet-5 | $1.00 / $5.00 | $0.38 | $7.60 |
| deepseek-v4-pro | $0.66 / $1.98 | $0.20 | $3.96 |
| qwen3.8-max | $1.00 / $3.00 | $0.30 | $6.00 |
| glm-5.3 | $0.70 / $2.2 | $0.21 | $4.28 |
One day = 180k input + 40k output on this page's workload. 20 days = 3.6M input and 0.8M output. Rates checked 2026-10-14, gateway rates move with upstream promotions, so verify the current number in your dashboard before committing to a budget.
Verifying the route
- Run
aichat --infoand read the config path. Confirm you are editing the file it loads. - Check whether the file has
clients:or a flatapi_url:. That decides which value your endpoint field takes. - Name the client and export
{NAME}_API_KEYrather than writing the key into the file. - Declare each model with its real ID from the gateway's models route, plus honest capability flags.
- Select as
client:modelwhenever more than one client is configured. - Run
aichat --list-modelsand confirm the client and models resolved before debugging anything else.
FAQ
FAQ
Why is my config being ignored?
There are several programs named aichat with different config paths and formats. aichat --info prints the file in use. If that path is not the file you edited, you are editing the wrong one.
Is api_base or api_url correct?
The Rust binary by sigoden uses api_base and expects the version root. Forks using api_url expect the full path including /v1/chat/completions. The field name in your file tells you which you have.
Why does my key setting do nothing?
With api_key omitted, aichat reads {CLIENT_NAME}_API_KEY in uppercase. If the client is named gw, the variable is GW_API_KEY, and an unexported variable is indistinguishable from a wrong key.
Why does a model work from the shell but not in the REPL?
Model selection is client:model. A bare model name is ambiguous once two clients define models with similar names, and the REPL resolves it differently from the command line.
Does aichat discover models from the gateway?
No. The models: list is a declaration. Names must match the gateway's IDs exactly, and the capability flags are claims you supply.
Why does a command work here and fail on another machine?
Trailing slash differences in api_base are the usual cause. Some gateways tolerate the doubled slash and some do not, so a value that works locally can fail elsewhere with the same config.
Related
- OpenAI-compatible endpoints — the request shape all of these clients rely on
- OpenAI SDK with a custom endpoint — the same base URL rule in a library rather than a binary
- Base URL not working, the trailing slash and doubled path failures, in general form
- OpenAI-compatible error codes (what 401), 404 and 429 mean with a gateway in the middle
- Claude Code with a custom API — a terminal agent pointed at the same kind of endpoint
- Tools that take one endpoint — the full matrix