aichat with a Custom OpenAI-Compatible Endpoint

In short: Several programs share the name and they take different config files and field names, so the config path is worth confirming before editing anything. The Rust binary uses a clients list with a base URL, and with no key field it reads the client name uppercased plus a key suffix from the environment. Models are declared rather than discovered, and selection is client and model together.

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.

You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment

Which aichat do you have

ProgramConfig fileEndpoint fieldValue it expects
sigoden/aichat (Rust, single binary)~/.config/aichat/config.yamlapi_baseversion root, e.g. https://gw.example.com/v1
forks derived from the same lineage~/.config/aichat/config.yamlapi_urlfull operation path, ending /v1/chat/completions
ctxinf/terminal-aichat (different program)~/.config/terminal-aichat/config.jsoncproviders.<name>.baseURLversion 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.

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.

Per 1M input / output tokens and a 20-day month
ModelGateway rate
in / out per 1M tokens
One day20 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.

Across 221 catalogue models that publish both rates, the median output rate is 4.0× the input rate, and 69% of them (152 models) charge at least 4× more for output than input. The other 624 raw catalogue entries are dropped by cleaning (test entries, dated snapshots and non-chat variants) not for a missing rate. This is why an agent workload is decided on the output column, not the input one everyone quotes.

Verifying the route

  1. Run aichat --info and read the config path. Confirm you are editing the file it loads.
  2. Check whether the file has clients: or a flat api_url:. That decides which value your endpoint field takes.
  3. Name the client and export {NAME}_API_KEY rather than writing the key into the file.
  4. Declare each model with its real ID from the gateway's models route, plus honest capability flags.
  5. Select as client:model whenever more than one client is configured.
  6. Run aichat --list-models and 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

Get API access