Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ gaia llm QUERY [OPTIONS]

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--model` | string | Client default | Specify the model to use |
| `--model` | string | Client default | Specify the model to use. Overrides `default_model` from config (see [Configuration](#configuration)) |
| `--max-tokens` | integer | 512 | Maximum tokens to generate |
| `--no-stream` | flag | false | Disable streaming response |

Expand Down Expand Up @@ -666,7 +666,7 @@ gaia chat [MESSAGE] [OPTIONS]
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--query, -q` | string | - | Single query to execute |
| `--model` | string | auto-selected by agent | Override the model used by `ChatAgent` (`None` means let the agent pick; see `ChatAgentConfig.model_id`) |
| `--model` | string | auto-selected by agent | Override the model used by `ChatAgent` (`None` means let the agent pick; see `ChatAgentConfig.model_id`). Falls back to `default_model` from config — see [Configuration](#configuration) |
| `--index, -i` | path(s) | - | PDF document(s) to index for RAG |
| `--watch, -w` | path(s) | - | Directories to monitor for new documents |
| `--chunk-size` | integer | 500 | Document chunk size for RAG |
Expand Down Expand Up @@ -799,7 +799,7 @@ gaia prompt "MESSAGE" [OPTIONS]

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--model` | string | auto | Model ID to use (default: auto-selected by each agent) |
| `--model` | string | auto | Model ID to use (default: auto-selected by each agent). Falls back to `default_model` from config — see [Configuration](#configuration) |
| `--max-tokens` | integer | 512 | Maximum tokens to generate |
| `--stats` | flag | false | Show performance statistics |

Expand Down Expand Up @@ -1482,6 +1482,64 @@ gaia connectors grants grant google builtin:chat --scopes gmail.readonly

---

## Configuration

GAIA keeps a small persistent config at `~/.gaia/config.json` (override the
location with the `GAIA_CONFIG_DIR` / `GAIA_CONFIG_FILE` environment variables).
Use it to set a **default model** once instead of passing `--model` on every
command.

```bash
gaia config show # print current config + file path
gaia config get default_model # read one value
gaia config set default_model Qwen3.5-35B-A3B-GGUF # persist a default model
```

| Key | Default | Purpose |
|-----|---------|---------|
| `default_model` | _(unset)_ | Default model ID for `gaia chat` / `gaia llm` / `gaia prompt` |
| `default_device` | `gpu` | Default inference device (`cpu` / `gpu` / `npu`) |
| `profile` | `chat` | Last `gaia init` profile |

### Default model precedence

For `gaia chat`, `gaia llm`, and `gaia prompt`, the model is resolved highest-wins:

1. An explicit `--model <id>` flag
2. `default_model` from `~/.gaia/config.json`
3. The command's built-in default (`DEFAULT_MODEL_NAME`)

So `gaia config set default_model <id>` lets you skip `--model` entirely, while
`--model` still overrides it for a single run. For `gaia chat`, passing an
explicit `--device` selects a device-specific model and takes precedence over
the config default.

<Note>
A **missing** config file is fine — GAIA falls back to built-in defaults. A
**corrupt** config file (invalid JSON) fails loudly with the file path and how
to recover, rather than silently reverting to defaults.
</Note>

### Using a custom config file

Point GAIA at a config file anywhere on disk — handy for per-project configs or
keeping work and personal defaults separate. Two equivalent ways:

```bash
# 1. The --config flag (on `gaia config`, `gaia chat`, `gaia llm`, `gaia prompt`)
gaia config set default_model Qwen3.5-35B-A3B-GGUF --config ./project.gaia.json
gaia llm "hello" --config ./project.gaia.json # uses that file's default_model

# 2. The GAIA_CONFIG_FILE env var (or GAIA_CONFIG_DIR for just the directory)
GAIA_CONFIG_FILE=./project.gaia.json gaia config set default_model Qwen3.5-35B-A3B-GGUF
GAIA_CONFIG_FILE=./project.gaia.json gaia llm "hello"
```

When both are given, `--config` wins over `GAIA_CONFIG_FILE`, which wins over the
default `~/.gaia/config.json`.

---

## Model Management

### Download Command
Expand Down
134 changes: 131 additions & 3 deletions src/gaia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,17 @@ def build_parser():
default="INFO",
help="Set the logging level (default: INFO)",
)
# Shared --config flag. Attached only to commands that read the persistent
# config (chat/llm/prompt + the `gaia config` subcommands) — NOT to
# parent_parser, since `gaia summarize` already defines its own --config.
config_path_parser = argparse.ArgumentParser(add_help=False)
config_path_parser.add_argument(
"--config",
default=None,
metavar="PATH",
help="Path to a GAIA config file (default: ~/.gaia/config.json or "
"$GAIA_CONFIG_FILE). Used to resolve default_model.",
)

# Generic LLM backend options (available to all agents)
parent_parser.add_argument(
Expand Down Expand Up @@ -1312,7 +1323,9 @@ def build_parser():

# Add prompt-specific options
prompt_parser = subparsers.add_parser(
"prompt", help="Send a single prompt to Gaia", parents=[parent_parser]
"prompt",
help="Send a single prompt to Gaia",
parents=[parent_parser, config_path_parser],
)
prompt_parser.add_argument(
"message",
Expand All @@ -1334,7 +1347,7 @@ def build_parser():
chat_parser = subparsers.add_parser(
"chat",
help="Interactive chat with RAG, file search, and shell execution",
parents=[parent_parser],
parents=[parent_parser, config_path_parser],
)
chat_parser.add_argument(
"--query",
Expand Down Expand Up @@ -2115,7 +2128,7 @@ def build_parser():
llm_parser = subparsers.add_parser(
"llm",
help="Run simple LLM queries using LLMClient wrapper",
parents=[parent_parser],
parents=[parent_parser, config_path_parser],
)
llm_parser.add_argument("query", help="The query/prompt to send to the LLM")
llm_parser.add_argument(
Expand Down Expand Up @@ -2804,6 +2817,35 @@ def build_parser():

connectors_cli.add_subparser(subparsers)

# Persistent CLI config (~/.gaia/config.json) — e.g. a default model so
# users don't have to pass --model on every chat/llm/prompt (issue #98).
config_parser = subparsers.add_parser(
"config",
help="Manage persistent GAIA config (~/.gaia/config.json)",
)
config_subparsers = config_parser.add_subparsers(
dest="config_action", help="Config action"
)
config_subparsers.add_parser(
"show",
help="Show current config and the config file path",
parents=[config_path_parser],
)
config_get_parser = config_subparsers.add_parser(
"get",
help="Get a config value, e.g. `gaia config get default_model`",
parents=[config_path_parser],
)
config_get_parser.add_argument("key", help="Config key to read")
config_set_parser = config_subparsers.add_parser(
"set",
help="Set a config value, e.g. `gaia config set default_model Qwen3.5-35B-A3B-GGUF`",
parents=[config_path_parser],
)
config_set_parser.add_argument("key", help="Config key to set")
config_set_parser.add_argument("value", help="Value to assign")
config_parser.set_defaults(action="config")

# Init command (one-stop GAIA setup)
# Note: Does not use parent_parser to avoid showing irrelevant global options
init_parser = subparsers.add_parser(
Expand Down Expand Up @@ -2934,6 +2976,29 @@ def main():
if hasattr(args, "logging_level"):
log_manager.set_level("gaia", getattr(logging, args.logging_level))

# Apply the persistent default_model (issue #98) for model-bearing commands.
# Precedence: explicit --model flag > config default_model > built-in default.
# An explicit `chat --device` requests a device-specific model, so it keeps
# precedence over the config default there.
if (
args.action in ("prompt", "chat", "llm")
and getattr(args, "model", None) is None
):
from gaia.config import GaiaConfig, GaiaConfigError

try:
_cfg = GaiaConfig.load(getattr(args, "config", None))
except GaiaConfigError as e:
print(f"❌ {e}", file=sys.stderr)
sys.exit(1)
# builtin_default=None: leave args.model unset when no config default so
# each command keeps applying its own built-in default downstream.
if not (args.action == "chat" and getattr(args, "device", None)):
resolved = _cfg.resolve_model(args.model, None)
if resolved:
args.model = resolved
log.debug("Using default_model from config: %s", resolved)

# Handle chat --ui: launch Agent UI server (backward compat)
if args.action == "chat" and getattr(args, "ui", False):
max_files = getattr(args, "max_indexed_files", 0)
Expand Down Expand Up @@ -3048,6 +3113,9 @@ def main():
kwargs = {
k: v for k, v in vars(args).items() if v is not None and k != "action"
}
# --config is only an input to model resolution (already applied to
# args.model above); it's not a runtime parameter for the agents.
kwargs.pop("config", None)
log.debug(f"Executing {args.action} with parameters: {kwargs}")
try:
result = run_cli(args.action, **kwargs)
Expand Down Expand Up @@ -4216,6 +4284,11 @@ def main():
handle_mcp_command(args)
return

# Handle Config command
if args.action == "config":
handle_config_command(args)
return

# Handle Cache command
if args.action == "cache":
handle_cache_command(args)
Expand Down Expand Up @@ -5216,6 +5289,61 @@ def handle_knowledge_command(args):
client.close()


def handle_config_command(args):
"""Handle `gaia config show|get|set` (persistent ~/.gaia/config.json)."""
from gaia.config import GaiaConfig, GaiaConfigError

path = getattr(args, "config", None)
config_file = GaiaConfig.config_path(path)

action = getattr(args, "config_action", None)
if not action:
print(
"No config action specified. Use: gaia config show|get|set",
file=sys.stderr,
)
sys.exit(1)

try:
cfg = GaiaConfig.load(path)
except GaiaConfigError as e:
print(f"❌ {e}", file=sys.stderr)
sys.exit(1)

if action == "show":
exists = config_file.exists()
print(f"Config file: {config_file}")
print(
" (file exists)"
if exists
else " (file not created yet — showing built-in defaults)"
)
for key in cfg.field_names():
value = cfg.get(key)
print(f" {key} = {value if value is not None else '(unset)'}")
return

if action == "get":
try:
value = cfg.get(args.key)
except GaiaConfigError as e:
print(f"❌ {e}", file=sys.stderr)
sys.exit(1)
print(value if value is not None else "")
return

if action == "set":
try:
cfg.set(args.key, args.value)
except GaiaConfigError as e:
print(f"❌ {e}", file=sys.stderr)
sys.exit(1)
cfg.save(path)
print(f"✅ Set {args.key} = {args.value}")
print(f" Saved to {config_file}")
return


def handle_cache_command(args):
"""Handle the cache management command.

Expand Down
Loading
Loading