This document covers the detailed model-access configuration for zcode-cli. For installation and basic usage, see the main README.
On first launch, ZCode recursively creates the configuration directory and a
credential-free config.json when it is missing. Existing files are never
replaced. The location is ~/.zcode/cli/config.json on macOS and Linux, and
%USERPROFILE%\.zcode\cli\config.json on Windows. Newly created directories
and files use private permissions on POSIX; Windows keeps the current user's
inherited ACLs.
The generated file contains the complete non-secret configuration shape plus
valid Z.AI model metadata, but deliberately omits apiKey until one is
configured. This lets the official runtime and TUI start cleanly without
pretending that model access is already configured. Choose one of the
model-access paths below before sending a prompt.
When the TUI starts while model access has not been set up, a setup wizard
opens automatically. ZCode tracks this with a setup-pending marker file next
to config.json: it is written when the credential-free default config is
first created, survives non-interactive commands (zcode plugin list,
zcode -p …, app-server, …) so the wizard still appears on the first
interactive TUI start, and is cleared once setup is handled — finishing or
explicitly skipping the wizard, choosing the custom-provider help entry,
deferring the post-import sign-in, or configuring model access by any other
means (zcode login, a hand-edited config.json), in which case the wizard
does not appear at all. The marker is only kept when login or the desktop
import was attempted and failed, so an unconfigured user is guided again on
the next start. Press Esc to skip the wizard. It can be reopened anytime with
/setup, and it never appears for an existing configuration unless invoked
manually.
The desktop import copies the selected desktop provider family (Z.AI or
BigModel): provider name, baseURL, and the desktop model list (merged into
the existing models, desktop IDs first). model.main keeps the current
selection when the model ID exists on both sides (case-insensitively),
otherwise it falls back to the first available of glm-5.2/glm-5.3 (or the
desktop list's first model); model.lite falls back to glm-5-turbo and then
to the selected main model, so both selections always reference models that
exist after the import. A backup of the pre-import config.json is written
next to it as config.json.pre-migration.bak; if the backup cannot be
written, the import is aborted before any change is made.
Desktop credentials are never copied: the desktop app stores them encrypted
(enc:v1:) with a key held by the desktop process, and the CLI reads desktop
files only. After importing, sign in once via the offered login step (or
/login later) so a fresh Coding Plan API key lands in the CLI config. An
existing CLI-side apiKey for the same provider is always preserved.
Three model-access paths are supported:
- Z.AI OAuth on macOS: run
zcode loginwhen no provider is configured, orzcode login --oauthto force reauthorization; add--no-browserto print the authorization URL instead of opening a browser (useful over SSH); - Z.AI/BigModel Coding Plan API key: open
/loginin the TUI and choose the matching masked API-key option; - Direct API key with a custom provider: use the
config.example.jsontemplate — or the flat custom-provider file — and do not log in.
The custom-provider file serves the signed-out state. A plain zcode login
means the user wants to sign in, so it runs even when a custom provider is
already configured — the file keeps working after the next /logout. Only an
existing login short-circuits the command (use zcode login --oauth to force
a re-login). A login is either a stored OAuth token in the credential vault or
an API key in an official zai/bigmodel slot — a pasted Coding Plan key is a
sign-in too and shows the key identity.
zcode stats prints a usage report grouped by provider, aggregated from the
local SQLite database the runtime maintains at ~/.zcode/cli/db/db.sqlite
(%USERPROFILE%\.zcode\cli\db\db.sqlite on Windows). Each group shows the
provider ID with the masked API key, request/error counts, input tokens,
cache-hit tokens with hit rate, output tokens, and estimated credit
consumption split into input / cache / output buckets (official GLM Coding
Plan rates: credits = tokens × rate ÷ 10000; off-peak hours at 50%).
zcode stats --json emits the same data as JSON for scripting.
zcode-cli sends requests through the official ZCode runtime (requests carry
User-Agent: ZCode/<version>), so ZCode-targeted vendor promotions apply
here too. When the BigModel OAuth token stored by zcode login is available,
zcode stats queries the vendor monitor API (the same one the ZCode desktop
usage page calls) and appends a real server-side spend report: actual
deducted credits per model with input / cache / output buckets. A missing
token or a failed request silently omits the section.
Instead of hand-editing the nested config.json, keep model settings in a
single flat file — the way to use zcode without logging in to bigmodel.cn /
z.ai. Copy the commented template from the repository to
~/.zcode/cli/custom.env (same directory as config.json; override
the location with ZCODE_ENV_FILE) and fill in your values:
mkdir -p ~/.zcode/cli
cp custom.env.example ~/.zcode/cli/custom.env
chmod 600 ~/.zcode/cli/custom.envA legacy ~/.zcode/cli/custom-provider.env (or the even older .env) from
earlier versions is renamed to custom.env automatically on the first start;
when both the new name and a legacy file exist, the new name wins and the
legacy file is left in place for you to dispose of.
The minimum required content:
ZCODE_API_KEY=your-api-key
ZCODE_MAIN_MODEL=glm-5.2Optional entries (all documented inline in custom.env.example): the provider ID
(ZCODE_PROVIDER_ID, default zai — any lowercase ID works; the synced
config lives in its own env-<provider-id> slot, so it never collides with
the official zai/bigmodel slots owned by /login; zai and bigmodel
additionally carry built-in base-URL defaults), the display name, the wire
protocol (ZCODE_PROVIDER_KIND: anthropic, openai, or
openai-compatible), the endpoint root (ZCODE_BASE_URL), the lite model for
lightweight/subagent work (ZCODE_LITE_MODEL), and extra models for the picker
(ZCODE_EXTRA_MODELS, comma-separated id or id:Display Name entries).
While signed out, every start reads the file and syncs it into config.json
before the runtime boots: the file is the authority for its own
env-<provider-id> provider entry and the model block, and the identity
line shows "Not signed in" (zcode identity reports the provider with the
env- prefix stripped, e.g. bigmodel/glm-5.2). Other providers and every
unrelated config block are left untouched. Sign-in has two tiers: a stored
OAuth token in the credential vault shows the account name, and an API key in
an official zai/bigmodel slot (what the /login key options paste) shows
the key identity — the mapped name from ~/.zcode/cli/bigmodel-users.json
together with the masked key for BigModel (a mapped name is a user-chosen
alias two accounts can share, so the masked key keeps the display
distinguishable), otherwise the masked key alone. While signed in (either tier) the sync
refreshes only the provider entry — the login's official slot owns the model
selection — and after a /logout (which also clears the official-slot keys)
the file takes over again automatically, so it never has to be removed or
restored by hand. A file
without model settings is ignored, an absent file changes nothing, and invalid
values stop startup with a clear error pointing at the offending entry.
The file holds a live API key: keep it out of every repository and at mode 600.
Keys are configured one variable per key: the primary stays in
ZCODE_API_KEY, backups add numbered variables (tried in ascending order,
duplicates dropped):
ZCODE_API_KEY=key-aaaa
ZCODE_API_KEY_2=key-bbbb
ZCODE_API_KEY_3=key-cccc
ZCODE_MAIN_MODEL=glm-5.2With more than one key zcode starts a loopback proxy (bound to 127.0.0.1,
first free port from 7849) before the runtime boots and syncs the provider in
config.json to point at it with the placeholder key zcode-failover. The
runtime keeps talking to what it believes is the real endpoint; the proxy holds
the real keys in memory and forwards every request with the currently selected
key. When the upstream rejects that key — HTTP 401/403/429, any 5xx status, or
a connection failure — the same request is retried with the next key before
anything is sent back. Successful responses are streamed untouched, so SSE
model streams behave exactly as with a single key. The next request starts
from the key that last succeeded.
Failover events are appended to ~/.zcode/cli/key-failover.log (keys masked
to their first and last four characters, rotated at 1 MiB). If every key fails,
the last upstream answer is passed through so the runtime's own retry logic
and error reporting keep working.
Notes:
- The real keys never enter
config.jsonor any other file — only the custom-provider file and proxy memory hold them. - The endpoint stays the one declared by
ZCODE_BASE_URL/ the provider default; failover only rotates keys, all keys must belong to the same endpoint/account family. - A single key keeps the direct connection with no proxy involved. Removing the extra variables (or the custom-provider file) restores the previous behavior on the next start.
- Each variable holds exactly one key; comma-separated lists are not read.
Start the TUI and open its setup picker:
/login
Choose either Z.AI Coding Plan API Key or BigModel Coding Plan API Key,
then paste the key into the masked prompt. The raw key is sent only to the
official runtime's configureCodingPlanApiKey implementation. The local TUI
does not add it to editor history or the visible/session transcript, and error
messages are redacted before rendering.
The same picker includes a Custom provider entry that points to the configuration-template path below. Custom providers do not use OAuth.
Selecting Z.AI Coding Plan releases TUI raw mode and starts the registered
Desktop authorization-code flow. On macOS the CLI temporarily installs a
background-only callback receiver, verifies the returned state, restores the
previous zcode:// handler, and hands the callback to the official runtime.
The authorization code travels over stdin instead of command-line arguments or
environment variables. The runtime performs token exchange, encrypted
credential persistence, Coding Plan API-key resolution and config.json
updates. The TUI is then restored and the model configuration is re-read.
The callback receiver is removed after success, cancellation or timeout. A small recovery record lets the next login restore the previous handler after an unclean process exit. The BigModel option continues to use the official localhost-callback implementation inside the runtime.
BigModel logins land on an API key whose account name the runtime never
stores, so the identity display falls back to the masked key. To show a name
of your choosing instead — one that stays correct across account switches —
map the keys in ~/.zcode/cli/bigmodel-users.json (a login-channel file,
kept out of the custom-provider file):
{
"<api-key>": "Work account",
"<api-key>": "Personal account"
}The mapped value is a free-form label — the choice is entirely yours: a user
name, a key remark, an account name, or anything else you want to see next to
the key. It is purely local display text. A key with an entry is displayed as
its mapped name in the TUI banner, the status line and zcode identity;
unmapped keys keep the masked-key display. The TUI collects the name for
you: any /login that goes through a BigModel option (OAuth or pasted key)
first prompts for a user name — Esc cancels the login — and writes it into
this file for the key the login landed on, so the mapping stays maintained
without hand editing. A missing or malformed file is ignored, as are
non-string or blank entries.
Start zcode once to generate the full user configuration automatically. From
a source checkout, config.example.json contains the same initial structure
for reference. Then edit the generated file:
zcodeEdit these four areas in ~/.zcode/cli/config.json (or the Windows path shown
above):
provider.zai.kind: useanthropic,openai-compatible, oropenai;provider.zai.options.baseURL: use the provider's API root;provider.zai.options.apiKey: insert the direct API key;- replace the entries in
provider.zai.models, then point bothmodel.mainandmodel.liteat the desired model IDs.
The provider map key is deliberately zai. The upstream CLI 0.15.x TUI
considers a direct API key configured only when it is stored under provider ID
zai or bigmodel. An arbitrary provider ID is valid model configuration,
but as the only provider it still triggers the upstream login gate. The
display name, API format, endpoint, headers and models remain fully custom.
For an Anthropic-compatible endpoint:
{
"kind": "anthropic",
"options": {
"baseURL": "https://example.com/api/anthropic",
"apiKey": "YOUR_API_KEY",
"apiKeyRequired": true
}
}Use the API root, not a final /messages path. For an OpenAI-compatible
endpoint, set kind to openai-compatible and normally use a root ending in
/v1, not /chat/completions. For the official OpenAI API, use openai;
baseURL can be omitted.
The object keys form the runtime model reference:
provider.<provider-id>.models.<model-id>
-> <provider-id>/<model-id>
Set both roles to keep all work on the custom provider:
{
"model": {
"main": "zai/your-model-id",
"lite": "zai/your-model-id"
}
}main is the normal conversation model. lite is used for lightweight and
subagent work. Model IDs are case-sensitive and must match the endpoint.
The no-login TUI path currently requires a non-empty options.apiKey in the
local config; an environment-only API key does not satisfy the upstream login
gate. Never commit the populated file, and keep its mode at 600.
After saving the config, no login command is required. Start the client:
zcodeFrom a source checkout, use bun run dev instead (see
Development).
Use these commands inside the TUI:
/model # show the active and available models
/model zai/your-model-id # switch to the custom provider explicitly
/new # start a new session with the configured default
The status line should show zai/your-model-id. Setting both model.main and
model.lite in the config makes the custom provider the default for normal,
lightweight and subagent work. A resumed session may retain its previous model,
so use /new after changing the default.
Headless prompts use the same provider configuration:
zcode --prompt "Explain this repository"Project-level overrides are read from zcode.json or .zcode/config.json in
the working directory. Running /model does not call the provider, so it is a
safe configuration check before the first prompt.
Long-running Agent calls automatically detach from the foreground turn after
one second and remain available through /tasks. Short Agent calls stay inline
so the current response can use their result without a notification round trip.
Configure the threshold in milliseconds:
{
"subagents": {
"autoBackgroundMs": 1000
}
}Set the value to 0 to disable automatic backgrounding. Agent tool calls that
use run_in_background: true detach immediately regardless of this threshold.
The CLI leaves retry classification and execution to the official ZCode runtime. It supplies a default retry budget of five retries; override it when needed with the runtime's own environment variable:
ZCODE_MODEL_RETRY_MAX_RETRIES=3 zcodeNewly generated configs use a 60-second model-stream idle timeout:
{
"modelStream": {
"idleTimeoutMs": 60000
}
}Existing configs are never overwritten, so update this field manually if an
older generated file still contains 600000. Retryable timeouts, dropped
streams, rate limits and server/network errors are retried and shown in the
TUI. Authentication and invalid-request responses remain non-retryable.
The interactive TUI captures runtime stderr so background diagnostics cannot
overwrite terminal rendering. A non-zero runtime exit prints its status and the
diagnostic path after the TUI stops. The active log is capped at 2 MB and rotated
to .1 on the next launch; both files use owner-only permissions.
The default path is ~/.zcode/cli/tui-runtime.log. Override it when collecting
diagnostics in an isolated environment:
ZCODE_TUI_RUNTIME_LOG=/tmp/zcode-tui-runtime.log zcodeSet ui.theme to "auto" (terminal detection), "dark", or "light" in the
user config: ~/.zcode/cli/config.json on macOS/Linux or
%USERPROFILE%\.zcode\cli\config.json on Windows. An explicit dark/light value
takes priority over terminal probing. auto queries the terminal background
color and color scheme at startup and re-applies the matching palette.
Notifications are enabled by default and emitted after a normal agent turn
completes or fails while the terminal is unfocused. Following Codex's terminal
capability fallback, auto uses OSC 9 in Ghostty, iTerm2, Kitty, Warp and
WezTerm, and BEL in terminals such as Apple Terminal. Selecting OSC 9 in an
unsupported terminal also falls back to BEL instead of silently emitting an
ignored sequence.
The unfocused condition uses DEC focus reporting when the terminal provides
it. Until focus support is confirmed, ZCode sends the notification instead of
permanently suppressing it as focused. native is an explicit opt-in that uses
an existing system command: terminal-notifier on macOS, notify-send on
Linux, or SnoreToast on Windows. These tools are not bundled, keeping the
default terminal notification path dependency-free. If the selected command is
unavailable or delivery fails, ZCode falls back to BEL. On macOS, the detected
terminal application is used as both the sender and click target. Exact tab or
pane restoration remains terminal-dependent; use the default auto setting so
OSC-capable terminals can preserve their native session behavior.
Open the interactive settings picker inside the TUI (both commands are equivalent):
/config
/settings
Saving a value returns to the settings root so several options can be changed
in one visit. Esc returns from a setting to the root, then closes the root.
The picker updates the active session immediately and persists the selected
values under ui.notifications in the cross-platform user config.json:
{
"ui": {
"notifications": {
"method": "auto",
"condition": "unfocused"
}
}
}Environment variables override config.json on startup and are useful for a
temporary per-shell setting:
export ZCODE_TUI_NOTIFICATION_METHOD=auto # auto|osc9|bel|native|off
export ZCODE_TUI_NOTIFICATION_CONDITION=always # unfocused|always
zcode