Built-in commands
gko ships its built-in commands under eight names: four groups, backend (the runtime
lifecycle), config (inspection), model (discover) and mcp (serve, see
MCP server), plus doctor, describe, update and help. They are not AI commands,
and these eight names are reserved: a command file whose first path segment is one of them is
rejected at load time, naming the file. Any other name, status or logs included, is yours.
gko --help lists your commands under Commands: and the built-ins under Built-ins:, help
included: gko help backend serve is gko backend serve --help.
On a terminal, help, reports and diagnostics are coloured. Through a pipe — or with NO_COLOR
set — every byte is the same as without colours: the escape sequences are stripped on the way out,
never written to a stream that is not a terminal.
A command's answer, on a terminal, is framed: a blank line, a ● header naming the model that
actually answered (the fallback, when it took over), the answer, and a blank line. A pipe or a
file receives the answer alone, byte for byte.
A free-text answer (format = "text" without max_lines) is streamed to a terminal. gko
asks the backend for stream: true, and each token is printed as it arrives. Nothing is printed
until the first non-blank token. Until then the fallback can still take over, which covers a
stopped container or a prompt the NPU refuses. Once a token is on screen, the answer belongs to
that model: a failure mid-way exits 3 after a partial answer, with no fallback. A JSON answer,
or one bounded by max_lines, can only be validated whole, so it is never streamed. Neither is
anything written to a pipe or a file.
-
gko doctor(alsogko config check) gko config testgko config modelsgko config schemagko backend servegko backend stopgko backend statusgko backend logsgko backend tunegko model discovergko describegko --versiongko update- Verbosity
- Execution statistics
- Degraded mode
gko doctor
Validates the runtime environment and reports on stdout. Its report is its result.
gko config check is the same command under its grouped name.
$ gko doctor
✓ configuration loaded
✗ backend "ovms" reachable: TCP connection to "127.0.0.1:8000" failed: Connection refused (os error 111)
✓ container runtime available
✓ model "qwen-fast"
✓ command "classify": model
✓ command "commit-message": model
✓ command "translate": model
✓ command "classify": output schemaWhat it checks, in order:
- Configuration loads — every scope parses and merges.
- Each backend is reachable.
-
The container runtime answers —
docker infosucceeds. This check appears only if at least one backend declares a Docker runtime; Docker is an optional prerequisite, and a machine that never asked for a container is never penalized for not having one. -
Each runtime command is runnable — one check per distinct
commanddeclared by a process runtime, named, and again only if at least one backend declares one. - Each model names a backend that exists and an operation that backend exposes.
- Each command names a model that exists.
- Each declared output schema exists, is readable, is valid JSON and compiles as a schema.
Step 7 covers every schema in every scope. Running a command compiles only that command's
schema, so a broken schema elsewhere does not stop your other commands; doctor is where every
schema gets checked.
Exit codes
| Code | Meaning |
|---|---|
0 |
every check passed |
2 |
at least one configuration check failed (1, 5, 6, 7) |
3 |
only reachability failed (2, 3, 4) |
Configuration takes priority: an unreachable backend and a broken configuration gives 2, so
a calling program can tell fix your files from start your runtime. Code 3 here means the
same thing it means everywhere else in the CLI — a backend problem.
What doctor does not check
doctor does not report whether an NPU is present: gko does not observe the inference hardware.
The reachability probe opens a TCP connection and closes it — it never sends an HTTP request, so it never invokes a model. The label therefore says reachable, not available: a socket accepted, and that is all that was established.
gko config models
Lists configured models, sorted by name, with column widths computed from the content.
FALLBACK is the model retried once when this one fails with a backend error, or - when none is
declared — see fallback.
$ gko config models
NAME BACKEND OPERATION FALLBACK
qwen2.5-coder-3b-instruct ovms chat -
qwen3-8b ovms chat qwen3-8b-gpu
qwen3-8b-gpu ovms-gpu chat -gko config schema
Prints the JSON Schema (draft-07) of one configuration format: backend, model, command
(the frontmatter of a command file) or test (a test case). Each schema lists
exactly the keys gko accepts and rejects any other one, like gko itself.
$ gko config schema backend > .gekko/backend.schema.jsonA schema checks structure: the keys, their types, and the few literal values that are fixed
(type = "openai-compatible", method = "POST", port = "auto"). It does not check the rules
that link keys together, such as values being allowed only with type = "enum", or a model
naming a backend that exists. gko config check stays authoritative.
The schemas do not depend on your configuration, so the command also works when that configuration fails to load.
To have an editor using Taplo (Even Better TOML in VS Code)
validate a backend, model or test case file as you type, start the file with a #:schema
directive. A relative path is resolved from the TOML file's own directory:
#:schema ../backend.schema.json
id = "ovms"A command's frontmatter sits inside a Markdown file, which Taplo does not read. Its schema is meant for tools and for checking the documentation.
gko backend serve
Starts the runtime of the backend a model points at, and prints what that runtime family calls what it started — its result, and the only thing it writes to stdout. For a Docker backend that is the container identifier; for a process one, the pid.
$ gko backend serve qwen-fast
2ac5416d2aae6769b9c2674ee2e284eaab4be049fa7d02d38146852989c35e35$ gko backend serve qwen-fast
1002664The argument is a model, not a backend: a model already names exactly one backend
(backend = "ovms"), so there is nothing to disambiguate, and several served backends coexist
without ceremony. What gets run comes entirely from that backend's [runtime] table: image,
options, command and arguments are configuration.
The Docker family
The command built is:
docker run -d --name gko-<backend-id> <options…> <image> <args…>
-d and --name are imposed by gko. Detached, because an attached container would write the
server's logs onto stdout, where only the result belongs. Named after the backend, because
that is what owns the ports: starting the same backend twice then fails on an explicit name
conflict instead of silently running a second container fighting for port 8000. That second
attempt exits 3, with Docker's own Conflict. The container name "/gko-ovms" is already in use
on stderr and nothing on stdout.
The process family
The declared command is spawned directly, with the declared arguments, its two streams
redirected into a log file beside the state record gko writes, and [runtime.env] layered over
the environment gko itself runs in. Serving the same backend twice is refused before anything is
spawned, naming the backend and the pid already holding it.
Then, unlike the Docker family, gko backend serve waits: it polls the backend's base_url until
something answers, the server exits, or startup_timeout_secs runs out. The poll is a TCP
connection and nothing more — no byte is sent, no protocol is spoken — so a serve that printed a
pid means something accepted a connection on that address, which a server still loading its
model already does. Use gko doctor or the runtime's own readiness endpoint for anything
stronger.
Every failure after the spawn terminates the child and deletes the record, but keeps the log — the only place the server explained itself. The message that reaches stderr names the backend, the executable that was run, what happened — it exited during startup with its status, or it did not answer within its budget — and the path of that log. stdout stays empty, as it does on every failure path of every command here.
Exit codes
| Code | Meaning |
|---|---|
0 |
the runtime started; its container identifier or pid is on stdout |
2 |
unknown model, or its backend declares no [runtime] table |
3 |
the runtime could not be brought up |
Code 3 covers every way a start fails, whichever family: docker missing, its daemon down or
docker run failing; and for a process, a command this machine does not have, a port already
taken, a spawn the OS refused, a server that exited during startup or one that never answered
within its budget. The port pre-check only exists for a backend that declares a port key: one
spelling its number directly in base_url and in arguments has nothing for gko to check, and
a port already held then surfaces as the server exiting during startup, with the reason in its
log. It is the same 3 as everywhere else in this CLI — a backend problem. To a
calling program, the runtime could not be brought up and the backend is unreachable call for
the same reaction.
What gko backend serve does not do
For a Docker backend it does not wait for the server to be ready: docker run -d returns as soon
as the container is created, long before a model is loaded. Use gko doctor, gko backend status, or the
runtime's own readiness endpoint, to know when it can answer. (A process backend does wait — see
above.)
It does not detach a spawned process into its own session either, so a terminal hang-up takes it down along with everything else in that session.
The rest of the lifecycle lives in its own commands: gko backend stop,
gko backend status and gko backend logs.
gko backend stop
Ends what gko backend serve started for that model's backend.
$ gko backend stop qwen-fast
gko-ovms$ gko backend stop qwen-fast
llamacppFor a Docker backend it removes the container rather than merely stopping it, and prints its
name: a stopped container still owns that name, so gko backend serve would then fail on a conflict and
the lifecycle would be a one-way trip.
For a process backend it prints the backend identifier, not the pid serve returned: by the
time stop answers, that pid names nothing. The pid, while it exists, is
gko backend status's INSTANCE column. Termination escalates:
SIGTERM, a bounded wait, then SIGKILL.
Stopping a backend that was never started is not an error in either family — the command prints
the same name and exits 0, so a script can call it without checking first. Neither is a record
whose process is already gone, or whose pid has since been recycled: that record is forgotten,
never signalled, because the pid it holds may belong to anybody by now.
Exit codes are gko backend serve's: 2 for an unknown model or a backend without a [runtime] table,
3 when the runtime itself refuses — including a process that survived both signals, in which
case the record is kept, so gko can still find that server.
gko backend status
Reports the state of every backend that declares a runtime, sorted by backend, one line each. Its report is its result.
$ gko backend status
BACKEND RUNTIME INSTANCE URL STATE
ovms docker gko-ovms http://127.0.0.1:8000 Up 3 hours
ovms-gpu docker gko-ovms-gpu http://127.0.0.1:32768 Up 3 hours$ gko backend status
BACKEND RUNTIME INSTANCE URL STATE
llamacpp process 1002664 http://127.0.0.1:18432 runningRUNTIME is the family that manages the backend — docker or process. INSTANCE is what that
family calls the thing it started: the container name for Docker, the pid for a process, - when
there is nothing running.
URL is the backend's resolved base_url — the only place a
port = "auto" shows up, since Docker allocates that number and
nothing else in the CLI would reveal it. A backend whose port cannot be read back (not started,
Docker unusable) shows - there. A served process backend shows the URL its own record
holds — the address it was actually started on, even if port was edited since.
STATE is whatever the runtime says. For Docker that is Docker's own wording (Up 3 minutes,
Exited (0) 2 minutes ago); for a process it is one of running (the pid is ours and something
answers on that URL), unreachable (ours, but nothing answers — starting up, wedged, or listening
elsewhere), exited (the pid is gone), stale state (the pid was recycled and now belongs to
somebody else) or foreign state (a live runtime recorded by another backend file — two projects
sharing an identifier in a machine-global state directory). Every family prints not started when
nothing was started.
status reports without repairing: a stale record is named as such and left alone. serve and
stop are what clear it.
A backend the runtime cannot even be asked about — a Docker daemon that will not answer, one
unreadable state file — gets that failure as its own state and costs nothing but its own row.
status exits 0 as long as the configuration loads, and prints its header even when no backend
declares a runtime at all.
gko backend logs
Streams what the served runtime wrote.
$ gko backend logs qwen-fast
[2026-09-21 17:26:44.688][1][serving][info][server.cpp:115] OpenVINO Model Server 2026.4.0.869b2186a
[2026-09-21 17:26:44.688][1][serving][info][server.cpp:116] OpenVINO backend 2026.4.0-22959-99c81491cc3-releases/2026/4--follow (-f) keeps streaming as new lines arrive, until you interrupt it.
For a Docker backend the container's two streams are passed through untouched — its stdout on
gko's stdout, its stderr on gko's stderr, in the order the runtime wrote them.
For a process backend both streams were already redirected, at serve time, into a single log
file beside the state record — interleaved there in the order the server wrote them — and this
command hands that file back byte for byte. It therefore still works after the
server has exited, which is exactly when its last lines matter; --follow is a read to end of
file and then a poll, so it also works on a server that has not written anything yet. A backend
gko never served has no such file: that is exit 3, naming the backend and the path that was
looked for, with nothing on stdout.
The logs are this command's result. Exit codes are gko backend serve's.
Cargo feature: hardware-tooling
gko backend tune and gko model discover (below) help prepare a configuration for
Intel/OpenVINO and Hugging Face. They never run a command themselves, and they come with the
hardware-tooling Cargo feature, on by default.
Building with --no-default-features drops both commands, and the model group along with
discover (the group would otherwise be empty), from the CLI — backend's own description drops
tune too:
$ gko --help
Usage: gko [OPTIONS] [COMMAND]
Commands:
bank-classify Classify the transactions of a bank statement
chat Everyday question, answer streamed on a terminal
classify Classify an input document
code Code assistant
commit-message Generate a conventional commit message
mr-description Generate a merge request title and description from a diff
rewrite Rewrite a text (email, document) in a given tone
sort-files Propose a folder for each file of a listing
synthese Synthétise un ou plusieurs fichiers en Markdown
translate Translate input text
Built-ins:
backend Manage the runtime of a model's backend: serve, stop, status, logs
config Inspect the configuration: check, models, test, schema
mcp Expose configured commands to MCP clients
doctor Check the runtime environment: configuration, backend reachability, declared output schemas
describe Describe a command, built-in or configured, as JSON; with none given, list every command
update Download and install the latest gko release from GitHub
help Print this message or the help of the given command
Options:
-v, --verbose <LEVEL> Diagnostic verbosity on stderr; stdout always carries the result only [default: warn] [possible values: error, warn, info]
--error-format <FORMAT> Format errors on stderr as plain text or a one-line JSON envelope [default: text] [possible values: text, json]
--config-dir <DIR> Use this directory as the project scope instead of walking up from the current one [env: GKO_CONFIG_DIR]
-h, --help Print help
-V, --version Print versionmodel stays a reserved name either way, so a command file still cannot be placed at
commands/model/*.md.
gko backend tune
Sizes the context and memory of every model compiled for the NPU or the GPU, from the model and the
host, and writes the result. An OpenVINO NPU graph is compiled for a fixed prompt length plus a
fixed answer length: a longer prompt is refused, and a longer answer is cut. A GPU graph allocates
its KV cache on demand, and on unified memory nothing bounds it. tune sizes both instead of
leaving OVMS's defaults: 1024 + 128 tokens on NPU, and an unbounded cache on GPU.
$ gko backend tune --help
Size the context and memory of every NPU- and GPU-compiled model from the model and the host's RAM, and write it
Usage: gko backend tune [OPTIONS]
Options:
--npu Tune the NPU models only [default: NPU and GPU, same limits]
-v, --verbose <LEVEL> Diagnostic verbosity on stderr; stdout always carries the result only [default: warn] [possible values: error, warn, info]
--gpu Tune the GPU models only
--max-models <N|all> How many of the tuned models run at the same time [default: all]
--max-memory <PERCENT> Share of the total RAM those models get together, in percent [default: 50]
--models-dir <DIR> Directory holding the exports [default: $HOME/models]
--dry-run Print the plan without writing anything
--kv-u8 Store GPU KV caches as u8: about twice the context per GB
-h, --help Print helpA model is tuned when <models-dir>/<model>/graph.pbtxt declares device: "NPU" or
device: "GPU". Without --npu or --gpu, both devices are tuned together with the same limits.
With --npu or --gpu, only that device is tuned, so each device can get its own limits:
gko backend tune --npu --max-memory 35
gko backend tune --gpu --max-memory 15 --max-models 1 --kv-u8Each model's share is:
-
--max-memorypercent of the total RAM, - minus the weights of the
--max-modelsheaviest tuned models, - divided by
--max-models.
A GPU twin loads its own copy of the weights, so it counts like any other model. The plan is the command's result:
$ gko backend tune --dry-run
RAM 65.4 GB x 50% - weights 23.0 GB = 1.6 GB per model (6 of 6 NPU or GPU models at once)
model device model max KV/token prompt answer est. memory
qwen2.5-coder-7b-instruct NPU 32768 56KB 1536 512 5.8GB
qwen2.5-coder-7b-instruct-gpu GPU 32768 56KB 13824 4608 5.5GB
qwen3-4b-instruct NPU 262144 144KB 1536 512 3.7GB
qwen3-4b-instruct-gpu GPU 262144 144KB 5376 1792 3.3GB
qwen3-8b NPU 40960 144KB 512 512 5.9GB
qwen3-8b-gpu GPU 40960 144KB 5376 1792 5.9GB
calibration: activation_tenths=62 activation_tenths_long=90 long_context=24576 step=1024--dry-run additionally prints the calibration constants the NPU column above was computed
from: activation_tenths and activation_tenths_long are the tenths of
hidden_size x layers bytes of static buffers charged per token below and above
long_context, and step is the token granularity the context is rounded to.
What each device gets:
| Device | Context | graph.pbtxt |
|---|---|---|
| NPU | Grows by 1024 tokens until the share or the model's max_position_embeddings is reached. A quarter goes to the answer. The memory estimate counts the fp16 KV cache and the graph's static buffers, calibrated on a Meteor Lake NPU so that it never under-estimates. |
MAX_PROMPT_LEN and MIN_RESPONSE_LEN at the root of plugin_config. NPUW_LLM_ENABLE_PREFIX_CACHING under DEVICE_PROPERTIES.NPU, which reuses a repeated prompt prefix (the NPU ignores the top-level enable_prefix_caching). |
| GPU | The share, in whole GiB, becomes cache_size. The context is what that cache holds, capped by the model. A quarter goes to the answer. |
cache_size. max_num_seqs: 4, since one user never needs 256 sequences in flight. KV_CACHE_PRECISION: "u8" in plugin_config with --kv-u8, which holds about twice the tokens per GiB; it is removed without the flag. |
Each model file gets [generation].max_tokens set to the answer length. Nothing is written until
every file has been computed. A file is replaced, not rewritten, so a graph.pbtxt created by a
container running as another user can still be updated. The next gko backend serve applies the
new graph.
Re-run it after every export, re-export or --configure, since they reset graph.pbtxt to the
defaults. Also re-run it after adding a model, since each model's share then shrinks. It fails
with a configuration error (2) when no export for the selected devices is found, naming the
directory. It also fails with 2 when a config.json, openvino_model.bin or graph.pbtxt is
missing or malformed, naming the file.
gko model discover
Searches Hugging Face for the models this host can run, whatever runs them: CPU, GPU or NPU. It
needs no configuration, so like doctor it works when yours fails to load.
$ gko model discover --help
Search Hugging Face for models this host can run, judged by llmfit when it is on PATH; --backend or --npu narrow the list
Usage: gko model discover [OPTIONS] [QUERY]...
Arguments:
[QUERY]... Words to search for (e.g. "qwen coder"); none lists the most downloaded
Options:
--task <TASK> Hugging Face task the model must serve [default: text-generation]
-v, --verbose <LEVEL> Diagnostic verbosity on stderr; stdout always carries the result only [default: warn] [possible values: error, warn, info]
-n, --limit <N> Most models listed [default: 20]
--candidates <N> Hugging Face results examined before filtering [default: 100]
--max-memory <PERCENT> Share of the total RAM one model's INT4 weights may take, in percent, for the models llmfit does not size [default: 50]
--min-score <SCORE> Lowest llmfit score kept, out of 100 [default: 60]
--sort <COLUMN[:asc|desc],...> Order by one or more columns (model, type, params, mem, license, downloads, score, fit, on); numbers default to desc, text to asc [default: downloads]
--backend <ENGINE|ID> Only models packaged for this engine (openvino, llamacpp, mlx), or for the engine a configured backend runs
--npu --backend openvino, on a host that has an Intel NPU
-h, --help Print helpBy default, the only filter is that the model runs well on this machine. This is decided by
llmfit, when its CLI is on PATH
(uv tool install llmfit) and it knows the model: a fit of Perfect or Good, and a score of at
least --min-score. For a model llmfit does not know, or without llmfit, the check is an INT4
estimate (half a byte per parameter, plus 20 %) against --max-memory percent of the RAM. A
repository that cannot be sized that way, such as a GGUF-only one llmfit does not know, is left
out. The search also asks for --task (Hugging Face pipeline_tag, text-generation by
default), and leaves out anything under 100M parameters: tokenizer fixtures and toys.
--backend narrows the list to what one inference engine serves. It accepts an engine name, or
the identifier of a configured backend: its engine is read from what its runtime starts (the
Docker image and arguments, or the process command and arguments). openvino/model_server or
ovms means openvino, and llama-server means llamacpp. A backend that starts neither is a
configuration error (2) naming it, and so is a name that is neither an engine nor a backend.
--backend |
Keeps only |
|---|---|
openvino |
An architecture optimum-intel exports for --task, read at run time from its own registry (model_configs.py on main). The original weights, not an already-quantized repository (AWQ, GPTQ, FP8, etc.): optimum-cli export --weight-format int4 starts from those. Open weights, unless HF_TOKEN is set (it is then sent to the Hub). |
llamacpp |
GGUF repositories, searched by their Hub tag and sized from the parameter count in their GGUF header. A GGUF repository llmfit lists among a model's gguf_sources gets that model's score and fit. |
mlx |
MLX repositories, searched by their Hub tag. They are already quantized: the size is read from the bit width in the name (-4bit, -8bit), 4 bits when it gives none. |
--npu is --backend openvino on a host with an Intel NPU. With no /dev/accel/accel*, it fails
before any request with exit 3. It cannot be combined with another --backend.
A candidate that fails a filter is left out, never listed with a caveat. The report is the
result, on stdout, sorted by downloads unless --sort says otherwise. --sort takes one or more
comma-separated column[:asc|desc] keys, applied in order, each breaking the ties of the previous
one. A number column (params, mem, downloads, score) defaults to desc, and a text column
to asc. fit ranks Perfect before Good. A value shown as - always sorts last, and rows
equal on every key keep the downloads order. The sort runs over every model that passed the
filters, before -n keeps the first ones:
gko model discover qwen3 --sort score # best score first
gko model discover qwen3 --sort fit,params:asc # Perfect fits, smallest firstThe mem GB column is llmfit's figure when it sized the model, and ~ marks the INT4 estimate otherwise. The score, fit, on and use case columns
appear only with llmfit. On a terminal the report is coloured: model ids stand out, a ~ estimate
and a non-permissive licence are yellow, and a Perfect fit and a score of 75 or more are green.
Through a pipe the table is plain.
$ gko model discover qwen3 instruct -n 4
model type params mem GB license downloads score fit on use case
unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF - - 15.6 apache-2.0 12.4M 80.8 Perfect GPU Code generation and completion
Qwen/Qwen3-4B-Instruct-2507 qwen3 4.0B 5.8 apache-2.0 3.9M 72.3 Perfect GPU Instruction following, chat
Qwen/Qwen3-Coder-30B-A3B-Instruct-FP8 qwen3_moe 30.5B 15.6 apache-2.0 1.1M 80.8 Perfect GPU Code generation and completion
QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ qwen3_vl_moe 31.1B ~18.6 apache-2.0 1.1M - - - -A Hub or registry that cannot be reached fails with exit 3, naming the URL. This list answers
"worth trying". gko-export's CPU check answers "actually works", and
gko backend tune sizes the context once the model is exported.
gko describe
Prints a JSON description of a command — a configured one or a built-in — useful for humans, and
for programs driving the CLI. The path is given as words, like the command itself
(gko describe git review); git/review is accepted too. Built-ins are looked up first.
$ gko describe translate
{"name":"translate","kind":"command","description":"Translate input text","model":"qwen3-8b","backend":"ovms","fallback":"qwen3-8b-gpu","source":{"file":"/home/…/gekko/.gekko/commands/translate.md","scope":"/home/…/gekko/.gekko"},"input":"stdin_or_file","args":{"language":{"short":"l","required":true,"description":"Target language"}},"output":{"format":"text","schema":null,"max_lines":null}}Pipe it through jq to read it:
$ gko describe commit-message | jq .
{
"name": "commit-message",
"kind": "command",
"description": "Generate a conventional commit message",
"model": "qwen3-8b",
"backend": "ovms",
"fallback": "qwen3-8b-gpu",
"source": {
"file": "/home/…/gekko/.gekko/commands/commit-message.md",
"scope": "/home/…/gekko/.gekko"
},
"input": "stdin",
"args": {},
"output": {
"format": "text",
"schema": null,
"max_lines": 1
}
}For a configured command, backend and fallback are resolved from its model (null when the
model is not configured, which describe reports rather than fails on), and source names the
file that won and the scope it came from — which is how a shadowed command is told apart from its
winner.
A built-in is described from the command tree itself, so this works even with a broken
configuration; degraded_mode says whether the built-in does too:
$ gko describe backend serve | jq .
{
"name": "backend/serve",
"kind": "builtin",
"description": "Start the runtime of the backend a model points at",
"args": {
"MODEL": {
"short": null,
"required": true,
"description": "Identifier of the model to serve (e.g. \"qwen-fast\")"
}
},
"subcommands": [],
"degraded_mode": false
}An unknown path is a configuration error listing the configured commands:
$ gko describe nexistepas
configuration error: unknown command: "nexistepas" (available commands: classify, code, commit-message, synthese, translate)The index
With no argument, gko describe lists every describable path instead — built-in and business,
each with its own one-line description — rather than failing on a missing required argument:
$ gko describe | jq .
[
{
"about": "Stream the logs of the runtime started for a model's backend",
"kind": "builtin",
"path": "backend/logs"
},
...
{
"about": "",
"kind": "business",
"path": "classify"
},
...
]--json
gko doctor (and its alias config check), gko backend status and gko config models also
accept --json: the same report, serialized instead of formatted for a terminal.
$ gko doctor --json | jq .
[
{
"kind": "config",
"label": "configuration loaded",
"status": "ok"
},
{
"kind": "reachability",
"label": "backend \"ovms\" reachable",
"status": "failed",
"message": "TCP connection to \"127.0.0.1:8000\" failed: Connection refused (os error 111)"
}
]kind ("config" or "reachability") and status ("ok", or "failed" with a message)
are the machine contract: a calling agent reads those, never label's text, to tell a
configuration failure (exit 2) apart from a reachability one (exit 3).
--error-format
--error-format <text|json> (default text) changes how errors are rendered on stderr — both
usage errors and command failures. The latter include kind, message, exit_code, and the
available family-specific fields (for example backend, status, or file). Usage errors honour
it too, wherever the flag sits on the command line. --help and --version are never affected:
they print their usual text on stdout and exit 0, whatever this flag says.
$ gko does-not-exist --error-format json{"kind":"usage","message":"error: unrecognized subcommand 'does-not-exist'"}error-format is consequently a reserved argument name: a command declaring
[args."error-format"] is rejected at load time, naming the file.
--config-dir
--config-dir <DIR> (or $GKO_CONFIG_DIR, the flag winning when both are set) names the project
scope directly, skipping the walk-up search entirely. See
Scopes and precedence for the walk-up itself.
$ gko --config-dir /path/to/.gekko config modelsconfig-dir is consequently a reserved argument name: a command declaring [args."config-dir"]
is rejected at load time, naming the file.
Shell completions
gko supports dynamic shell completions, with no completions subcommand: setting
COMPLETE=<shell> makes the binary print the shell's registration script instead of running as
usual.
$ eval "$(COMPLETE=bash gko)" # bash, once per shell session (or in ~/.bashrc)
$ eval "$(COMPLETE=zsh gko)" # zsh
$ COMPLETE=fish gko | source # fishOnce registered, <TAB> completes business commands, built-in group names (backend, config,
...) and the global flags — but not the top-level built-ins doctor, describe and update.
Completions are computed on every request from the configuration in effect, so they reflect the
current .gekko/ — including --config-dir/GKO_CONFIG_DIR.
gko --version
Prints the program name and its version:
$ gko --version
gko 0.8.0It does not load or require a valid AI configuration.
gko update
Checks the latest GitHub Release and installs it over the currently running executable:
$ gko update
updated gko from 0.7.0 to 0.7.1When no newer release exists, it reports that fact and leaves the executable untouched:
$ gko update
gko 0.8.0 is already up to dateThe release publishes a gko-update.json manifest. It maps every supported platform to a raw
binary and its SHA-256 checksum. The command downloads that manifest through GitHub's stable
releases/latest URL, compares semantic versions, selects the current platform (one Linux build
per architecture, for every glibc-based distribution), verifies the downloaded bytes, then replaces the executable
at the same path. It exits 1 without replacing anything when the manifest, download, checksum,
platform detection, or replacement fails. The executable's directory must therefore be writable
by the current user.
The manifest and the binaries come from the same GitHub release: the SHA-256 check protects against a corrupted download, not against a compromised release.
Like --version, update does not depend on the AI configuration and remains available in degraded
mode. After a successful update, the new binary is asked whether it accepts your configuration;
if it does not, a warning on stderr points to the changelog and the documentation — the update
itself has succeeded and exits 0.
Verbosity
--verbose <LEVEL> (-v) sets what reaches stderr. Three levels, accepted after any
subcommand since the argument is global:
| Level | Prints |
|---|---|
error |
nothing but the failure itself, printed by the process regardless |
warn |
(default) what the engine had to work around — an unloadable scope, for instance |
info |
a trace: scopes read, command resolved, input size, request sent, response received |
$ echo "texte" | gko classify --verbose info > /dev/null
gko: info: scopes: /home/…/gekko/.gekko, /home/…/.config/gekko
gko: info: command "classify" -> model "qwen-fast" (backend "ovms", operation "chat") from /home/…/gekko/.gekko/commands/classify.md
gko: info: input: 6 characters read from stdin
gko: info: prompt rendered: 125 characters
gko: info: POST http://127.0.0.1:8000/v3/chat/completions (model "OpenVINO/Qwen3-8B-int4-ov", timeout 30 s)No level ever changes stdout. Verbosity moves a threshold on the diagnostic stream; the result
of a command is not a diagnostic. A failure message is printed by the process itself, once, at
every level — --verbose error silences the engine's commentary, never the error you need.
--dry-run
Every business command leaf accepts --dry-run: it builds the exact request gko would send —
url, headers, body — exactly as the real call would, and prints it as JSON
instead of sending it. Nothing is written or read but the terminal: the runtime is never resolved
(a port = "auto" backend keeps its {{ backend.port }} placeholder verbatim, since resolving it
means asking Docker), only the primary model is shown (never the fallback), and header VALUES are
redacted — only their names appear.
$ echo "texte" | gko classify --dry-run
{"body":{"messages":[{"content":"Classify: texte\n","role":"user"}],"model":"OpenVINO/Qwen3-8B-int4-ov"},"headers":{},"url":"http://127.0.0.1:8000/v3/chat/completions"}--dry-run exists only on business command leaves, never on a built-in: gko doctor --dry-run
is a usage error (exit 2, empty stdout), like any other unrecognized flag. dry-run is consequently a reserved argument name: a command declaring
[args."dry-run"] is rejected at load time, naming the file.
--model
Every business command leaf also accepts --model <ID>, to use a model other than the command
file's own for this one call. The override applies before the model is resolved and before the
input is read: an unknown id fails exactly like an unknown model in the command file would (a
configuration error, exit 2, the id named in the message), and nothing is sent to the network.
$ echo "texte" | gko classify --model qwen-fast --dry-run
{"body":{"messages":[{"content":"Classify: texte\n","role":"user"}],"model":"OpenVINO/Qwen3-8B-int4-ov"},"headers":{},"url":"http://127.0.0.1:8000/v3/chat/completions"}model is consequently a reserved argument name, for the same reason as dry-run: a command
declaring [args.model] is rejected at load time, naming the file.
--no-wait
A business command leaf also accepts --no-wait. When the command's backend declares
max_concurrent = 1 and another gko process is
using it, the command does not wait for its turn: the busy backend is a backend failure, so the
model's fallback answers if it declares one, and otherwise the command fails at once (exit 3,
empty stdout) naming the backend. On a backend without a limit it changes nothing.
no-wait is consequently a reserved argument name, for the same reason as dry-run.
Progress indicators
On a terminal, gko draws a spinner on stderr while it waits for a model (relabelled when the
fallback takes over) or for a process backend to start, and a progress bar while gko update
downloads. They are drawn only when stderr is a terminal and the level is above error:
through a pipe — how a program driving gko sees it — stderr receives no escape sequence and no
carriage return, and --verbose error means silence. Indicators never touch stdout.
The name verbose and the short letter -v are consequently reserved: a command declaring
[args.verbose] or short = "v" is rejected at load time, naming the argument.
Execution statistics
With GKO_STATS_FILE set to a path, every run of a configured command appends one JSON line to
that file, created if needed: from the CLI, from gko config test (one line per
run of a case) and from gko mcp serve (one line per tool call). A --dry-run sends no
request and writes nothing.
{"timestamp_ms":1790000000000,"command":"classify","model_requested":"qwen-npu","model_answered":"qwen-gpu","fallback_used":true,"backend":"ovms-gpu","duration_ms":1840,"prompt_tokens":212,"completion_tokens":14,"finish_reason":"stop","exit_code":0}| Field | Meaning |
|---|---|
timestamp_ms |
when the run started, in milliseconds since the Unix epoch |
command |
the command path, segments joined by /
|
case |
the config test case name; absent outside config test
|
model_requested |
the model the command (or --model) named |
model_answered |
the model that answered: the fallback, when it took over |
fallback_used |
whether the fallback answered |
backend |
the backend that answered, or the requested model's backend when none did |
duration_ms |
time spent reaching the backend and waiting for the answer, fallback included, and the wait for a busy max_concurrent backend |
prompt_tokens, completion_tokens
|
the backend's usage, when it reports one |
finish_reason |
the backend's finish_reason, when it reports one |
exit_code |
the invocation's exit code |
A field the run never got to know is null: a run that failed before reaching the backend has no
backend, one whose backend failed has no model_answered. A record never holds the prompt, the
answer or a header value.
The line is appended after the answer is written, in a single write, so concurrent runs sharing a file do not interleave. A file that cannot be written is a warning on stderr: the exit code and stdout stay what they would have been.
Degraded mode
A broken configuration does not leave you without the tools to diagnose it. When loading fails, the built-ins stay available and your commands are left out.
$ gko --help
Usage: gko [OPTIONS]
Commands:
none: the configuration failed to load; run "gko doctor"
Built-ins:
backend Manage the runtime of a model's backend: serve, stop, status, logs, tune
config Inspect the configuration: check, models, test, schema
mcp Expose configured commands to MCP clients
model Find models for this host: discover
doctor Check the runtime environment: configuration, backend reachability, declared output schemas
describe Describe a command, built-in or configured, as JSON; with none given, list every command
update Download and install the latest gko release from GitHub
help Print this message or the help of the given command
Options:
-v, --verbose <LEVEL> Diagnostic verbosity on stderr; stdout always carries the result only [default: warn] [possible values: error, warn, info]
--error-format <FORMAT> Format errors on stderr as plain text or a one-line JSON envelope [default: text] [possible values: text, json]
--config-dir <DIR> Use this directory as the project scope instead of walking up from the current one [env: GKO_CONFIG_DIR]
-h, --help Print help
-V, --version Print versionExit code 0, and on stderr:
gko: warn: invalid configuration (configuration error: invalid TOML in
/tmp/…/.gekko/backends/k.toml: TOML parse error at line 1, column 2
|
1 | x{[
| ^
key with no value, expected `=`
); run "gko doctor" for details on the failed checks
From there:
| Command | Behaviour with a broken configuration |
|---|---|
gko --help |
exit 0, built-ins listed, warning on stderr |
gko doctor, gko config check
|
exit 2, report on stdout naming the offending file and line |
gko --version, update
|
run normally; they do not depend on the configuration |
gko config schema |
runs normally (the warning still goes to stderr) |
gko describe <built-in> |
runs normally; gko describe <command> exits 2
|
gko backend serve, stop, status, logs, gko config models
|
exit 2, stdout empty — they need the configuration that could not load |
| anything else | exit 2, stdout empty, error on stderr |