diff options
Diffstat (limited to 'src/config.cpp')
| -rw-r--r-- | src/config.cpp | 732 |
1 files changed, 732 insertions, 0 deletions
diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..4f3d00a --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,732 @@ +#include "config.h" + +#include <cjson/cJSON.h> +#include <ctype.h> +#include <errno.h> +#include <getopt.h> +#include <limits> +#include <sys/stat.h> +#include <math.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "compact.h" +#include "dotagents.h" +#include "tools.h" + +#define DEFAULT_COMPACT_THRESHOLD 0.75 +#define DEFAULT_CLEAR_THRESHOLD 0.5 +#define DEFAULT_MAX_RETRIES 3UL +#define DEFAULT_SKILL_INDEX_CAP 50UL +#define DEFAULT_SHELL_TIMEOUT_SECS 120UL +#define DEFAULT_MAX_OUTPUT_CHARS 16000UL +#define DEFAULT_MAX_FILE_BYTES 2000000UL + +/* The default system preamble. The "Available tools" section is generated + * from the tool registry (tools.c) so it can't drift from the actual + * tool set. */ +static const char *PREAMBLE_HEAD = + "You are an agent operating inside a terminal. You accomplish tasks by " + "calling tools.\n" + "\n"; +static const char *PREAMBLE_TAIL = + "\n" + "Guidelines:\n" + "- Paths may be relative to the working directory or absolute.\n" + "- Inspect before you modify: use read_file, or run_shell with ls / " + "rg / grep, to understand existing state.\n" + "- Prefer small, verifiable steps. After making changes, verify them " + "(e.g. run the build or tests with run_shell).\n" + "- Keep run_shell commands safe and scoped to the task; avoid " + "destructive commands unless explicitly asked.\n" + "- When a task is complete, stop calling tools and reply with a short " + "summary of what you did."; + +char *default_preamble(void) { + xbuf b; + xbuf_init(&b); + xbuf_put(&b, PREAMBLE_HEAD); + char *tools = tool_preamble_list(); + xbuf_put(&b, tools); + free(tools); + xbuf_put(&b, PREAMBLE_TAIL); + char *out = xbuf_steal(&b); + xbuf_free(&b); + return out; +} + +/* ------------------------------------------------------------------ */ +/* Providers */ +/* ------------------------------------------------------------------ */ + +provider_kind provider_parse(const char *raw, char **err) { + xbuf l; + xbuf_init(&l); + for (const char *p = raw; *p; p++) + xbuf_push(&l, (char)tolower((unsigned char)*p)); + l.data[l.len] = '\0'; + const char *s = l.data; + if (strcmp(s, "openai") == 0 || strcmp(s, "openai-compatible") == 0 || + strcmp(s, "openai_compatible") == 0) { + xbuf_free(&l); + return PROV_OPENAI; + } + if (strcmp(s, "anthropic") == 0 || strcmp(s, "claude") == 0) { + xbuf_free(&l); + return PROV_ANTHROPIC; + } + if (strcmp(s, "ollama") == 0) { + xbuf_free(&l); + return PROV_OLLAMA; + } + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, "unknown provider `%s` (expected openai, anthropic, or " + "ollama)", + raw); + xbuf_free(&l); + char *r = xbuf_steal(&b); + if (err) + *err = r; + else + free(r); + return PROV_OPENAI; +} + +const char *provider_name(provider_kind k) { + switch (k) { + case PROV_OPENAI: + return "openai"; + case PROV_ANTHROPIC: + return "anthropic"; + case PROV_OLLAMA: + return "ollama"; + } + return "openai"; +} + +static const char *provider_debug(provider_kind k) { + switch (k) { + case PROV_OPENAI: + return "OpenAI"; + case PROV_ANTHROPIC: + return "Anthropic"; + case PROV_OLLAMA: + return "Ollama"; + } + return "OpenAI"; +} + +const char *provider_default_model(provider_kind k) { + switch (k) { + case PROV_OPENAI: + return "gpt-5.2"; + case PROV_ANTHROPIC: + return "claude-sonnet-4-6"; + case PROV_OLLAMA: + return "llama3.2"; + } + return "gpt-5.2"; +} + +/* ------------------------------------------------------------------ */ +/* CLI parsing */ +/* ------------------------------------------------------------------ */ + +static void print_usage(FILE *f) { + fprintf( + f, + "llsh %s\n" + "llsh — a terminal LLM agent harness built on rig\n" + "\n" + "Usage: llsh [OPTIONS] [PROMPT]\n" + "\n" + "Arguments:\n" + " [PROMPT] One-shot prompt. If omitted (and stdin is a TTY), " + "launches the interactive REPL.\n" + "\n" + "Options:\n" + " --model <MODEL> Model name, e.g. `gpt-5.2`, " + "`claude-sonnet-4-6`, `llama3.2`; or a `.agents/models.json` " + "preset name (which also brings that preset's " + "provider/base_url/api_key). Overrides the preset's `model`; " + "defaults per provider when unset\n" + " --provider <PROVIDER> Model provider: `openai`, " + "`anthropic`, or `ollama`. `openai` also works against any " + "OpenAI-compatible endpoint via `--base-url`\n" + " --base-url <BASE_URL> Base URL override. For `openai` " + "this targets any OpenAI-compatible endpoint (LM Studio, vLLM, " + "Ollama's /v1, llama.cpp, ...); for `ollama` this is the Ollama " + "server address (default http://localhost:11434)\n" + " --api-key <API_KEY> API key. Overrides the preset's " + "`api_key`\n" + " -C, --cwd <CWD> Working directory the agent's file and shell " + "tools operate in\n" + " --preamble <PREAMBLE> System preamble for the agent\n" + " --max-turns <MAX_TURNS> Maximum model calls (including " + "tool-call continuations) per user turn. `0` (the default) is " + "unbounded\n" + " --max-retries <MAX_RETRIES> Number of times a transient " + "provider failure (timeout, 429, 5xx) is retried with exponential " + "backoff before the turn fails. `0` disables retry\n" + " --context-limit <CONTEXT_LIMIT> Model context window in " + "tokens. `0` (the default) disables context compaction; a window " + "reported by the provider's endpoint is adopted when available, " + "otherwise this value is used\n" + " --compact-threshold <COMPACT_THRESHOLD> Fraction of " + "`context_limit` at which earlier context is summarized. `0` " + "disables the tier; otherwise the value must be in `(0, 1]`\n" + " --clear-threshold <CLEAR_THRESHOLD> Fraction of " + "`context_limit` at which stale tool results are cleared. `0` " + "disables the tier; otherwise the value must be in `(0, 1]`\n" + " --no-rubric-compaction Disable the SelfCompact rubric " + "probe that compacts the trajectory at a judged-safe point " + "(independent of the token thresholds above)\n" + " --rubric-max-summaries <N> Cap on rubric-triggered " + "compactions per session. The default is `1` (the paper's cap); " + "`0` means unlimited\n" + " --verbose Verbose, structured logging to stderr (one JSON " + "object per event: turn lifecycle, tool calls, compaction, " + "retries, and errors)\n" + " --debug Write the whole conversation history to " + "`debug.json` (in the working directory) after every turn and " + "again on exit\n" + " --skill-index-cap <SKILL_INDEX_CAP> Maximum number of " + "skills listed in the preamble's skills index\n" + " --shell-timeout-secs <SHELL_TIMEOUT_SECS> `run_shell` " + "timeout in seconds (the whole process tree is killed on expiry)\n" + " --max-output-chars <MAX_OUTPUT_CHARS> Cap on tool/shell " + "output text, in characters (longer is truncated)\n" + " --max-file-bytes <MAX_FILE_BYTES> Cap on the size of a " + "file `read_file` will read, in bytes\n" + " --resume <PATH> Resume a session saved by `/save`: restore " + "its conversation, model, preset, sub-agent, and working " + "directory. The saved model/config values are authoritative, so " + "`--model` / `--provider` / `--base-url` / `--api-key` are " + "ignored when this is set\n" + " --allow-mcp Trust and start MCP servers from .agents/mcp.json\n" + " --shell-allow <COMMAND> Allow this run_shell command (repeatable)\n" + " --shell-deny <COMMAND> Deny this run_shell command (repeatable)\n" + " --confirm-destructive Prompt before recognized destructive shell commands\n" + " --no-confirm-destructive Disable the destructive-command prompt\n" + " -h, --help Print help\n" + " -V, --version Print version\n", + LLSH_VERSION); +} + +static int parse_ulong(const char *s, unsigned long *out, char **err, + const char *flag) { + /* strtoull silently accepts a leading '-' and wraps on overflow; reject + * both explicitly so `--max-turns -1` does not become ULONG_MAX. */ + errno = 0; + char *endp = NULL; + unsigned long v = strtoull(s, &endp, 10); + if (s[0] == '-' || endp == s || *endp != '\0' || errno == ERANGE) { + if (err) { + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, "invalid value for %s: `%s`", flag, s); + *err = xbuf_steal(&b); + } + return -1; + } + *out = v; + return 0; +} + +static int parse_fraction_arg(const char *s, double *out, char **err, + const char *flag) { + errno = 0; + char *endp = NULL; + double value = strtod(s, &endp); + if (endp == s || *endp != '\0' || errno == ERANGE || !isfinite(value) || + value < 0.0 || value > 1.0) { + if (err) + *err = xstrfmt("invalid value for %s: `%s` (expected 0 or a finite value in (0, 1])", + flag, s); + return -1; + } + *out = value; + return 0; +} + +int cli_parse(int argc, char **argv, cli *out, char **err) { + memset(out, 0, sizeof *out); + sl_init(&out->shell_allowlist); + sl_init(&out->shell_denylist); + + enum { + OPT_MODEL = 256, + OPT_PROVIDER, + OPT_BASE_URL, + OPT_API_KEY, + OPT_CWD, + OPT_PREAMBLE, + OPT_MAX_TURNS, + OPT_MAX_RETRIES, + OPT_CONTEXT_LIMIT, + OPT_COMPACT_THRESHOLD, + OPT_CLEAR_THRESHOLD, + OPT_VERBOSE, + OPT_DEBUG, + OPT_NO_RUBRIC, + OPT_RUBRIC_MAX, + OPT_SKILL_INDEX_CAP, + OPT_SHELL_TIMEOUT, + OPT_MAX_OUTPUT, + OPT_MAX_FILE, + OPT_RESUME, + OPT_ALLOW_MCP, + OPT_SHELL_ALLOW, + OPT_SHELL_DENY, + OPT_CONFIRM_DESTRUCTIVE, + OPT_NO_CONFIRM_DESTRUCTIVE, + }; + static const struct option long_opts[] = { + {"model", required_argument, 0, OPT_MODEL}, + {"provider", required_argument, 0, OPT_PROVIDER}, + {"base-url", required_argument, 0, OPT_BASE_URL}, + {"api-key", required_argument, 0, OPT_API_KEY}, + {"cwd", required_argument, 0, OPT_CWD}, + {"preamble", required_argument, 0, OPT_PREAMBLE}, + {"max-turns", required_argument, 0, OPT_MAX_TURNS}, + {"max-retries", required_argument, 0, OPT_MAX_RETRIES}, + {"context-limit", required_argument, 0, OPT_CONTEXT_LIMIT}, + {"compact-threshold", required_argument, 0, OPT_COMPACT_THRESHOLD}, + {"clear-threshold", required_argument, 0, OPT_CLEAR_THRESHOLD}, + {"verbose", no_argument, 0, OPT_VERBOSE}, + {"debug", no_argument, 0, OPT_DEBUG}, + {"no-rubric-compaction", no_argument, 0, OPT_NO_RUBRIC}, + {"rubric-max-summaries", required_argument, 0, OPT_RUBRIC_MAX}, + {"skill-index-cap", required_argument, 0, OPT_SKILL_INDEX_CAP}, + {"shell-timeout-secs", required_argument, 0, OPT_SHELL_TIMEOUT}, + {"max-output-chars", required_argument, 0, OPT_MAX_OUTPUT}, + {"max-file-bytes", required_argument, 0, OPT_MAX_FILE}, + {"resume", required_argument, 0, OPT_RESUME}, + {"allow-mcp", no_argument, 0, OPT_ALLOW_MCP}, + {"shell-allow", required_argument, 0, OPT_SHELL_ALLOW}, + {"shell-deny", required_argument, 0, OPT_SHELL_DENY}, + {"confirm-destructive", no_argument, 0, OPT_CONFIRM_DESTRUCTIVE}, + {"no-confirm-destructive", no_argument, 0, OPT_NO_CONFIRM_DESTRUCTIVE}, + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'V'}, + {0, 0, 0, 0}, + }; + + int c; + int prompt_set = 0; + optind = 1; + while ((c = getopt_long(argc, argv, "C:hV", long_opts, NULL)) != -1) { + switch (c) { + case OPT_MODEL: + out->model = xstrdup(optarg); + break; + case OPT_PROVIDER: + out->provider = xstrdup(optarg); + break; + case OPT_BASE_URL: + out->base_url = xstrdup(optarg); + break; + case OPT_API_KEY: + out->api_key = xstrdup(optarg); + break; + case OPT_CWD: + case 'C': + out->cwd = xstrdup(optarg); + break; + case OPT_PREAMBLE: + out->preamble = xstrdup(optarg); + break; + case OPT_MAX_TURNS: + if (parse_ulong(optarg, &out->max_turns, err, "--max-turns") != 0) + return -1; + out->has_max_turns = true; + break; + case OPT_MAX_RETRIES: + if (parse_ulong(optarg, &out->max_retries, err, + "--max-retries") != 0) + return -1; + out->has_max_retries = true; + break; + case OPT_CONTEXT_LIMIT: + if (parse_ulong(optarg, &out->context_limit, err, + "--context-limit") != 0) + return -1; + out->has_context_limit = true; + break; + case OPT_COMPACT_THRESHOLD: + if (parse_fraction_arg(optarg, &out->compact_threshold, err, + "--compact-threshold") != 0) + return -1; + out->has_compact_threshold = true; + break; + case OPT_CLEAR_THRESHOLD: + if (parse_fraction_arg(optarg, &out->clear_threshold, err, + "--clear-threshold") != 0) + return -1; + out->has_clear_threshold = true; + break; + case OPT_VERBOSE: + out->verbose = true; + break; + case OPT_DEBUG: + out->debug = true; + break; + case OPT_NO_RUBRIC: + out->no_rubric = true; + break; + case OPT_RUBRIC_MAX: + if (parse_ulong(optarg, &out->rubric_max_summaries, err, + "--rubric-max-summaries") != 0) + return -1; + out->has_rubric_max_summaries = true; + break; + case OPT_SKILL_INDEX_CAP: + if (parse_ulong(optarg, &out->skill_index_cap, err, + "--skill-index-cap") != 0) + return -1; + out->has_skill_index_cap = true; + break; + case OPT_SHELL_TIMEOUT: + if (parse_ulong(optarg, &out->shell_timeout_secs, err, + "--shell-timeout-secs") != 0) + return -1; + out->has_shell_timeout_secs = true; + break; + case OPT_MAX_OUTPUT: + if (parse_ulong(optarg, &out->max_output_chars, err, + "--max-output-chars") != 0) + return -1; + out->has_max_output_chars = true; + break; + case OPT_MAX_FILE: + if (parse_ulong(optarg, &out->max_file_bytes, err, + "--max-file-bytes") != 0) + return -1; + out->has_max_file_bytes = true; + break; + case OPT_RESUME: + out->resume = xstrdup(optarg); + break; + case OPT_ALLOW_MCP: + out->allow_mcp = true; + break; + case OPT_SHELL_ALLOW: + if (!optarg[0]) { + if (err) + *err = xstrdup("--shell-allow requires a non-empty command"); + return -1; + } + sl_push_copy(&out->shell_allowlist, optarg); + break; + case OPT_SHELL_DENY: + if (!optarg[0]) { + if (err) + *err = xstrdup("--shell-deny requires a non-empty command"); + return -1; + } + sl_push_copy(&out->shell_denylist, optarg); + break; + case OPT_CONFIRM_DESTRUCTIVE: + out->confirm_destructive = true; + out->has_confirm_destructive = true; + break; + case OPT_NO_CONFIRM_DESTRUCTIVE: + out->confirm_destructive = false; + out->has_confirm_destructive = true; + break; + case 'h': + print_usage(stdout); + return CLI_HELP; + case 'V': + fprintf(stdout, "llsh %s\n", LLSH_VERSION); + return CLI_HELP; + case '?': + print_usage(stderr); + if (err) + *err = xstrdup("try --help"); + return -1; + default: + /* getopt_long returns '?' for unknown options; anything else + * here is a short option we did not register. */ + print_usage(stderr); + if (err) + *err = xstrdup("try --help"); + return -1; + } + } + for (int i = optind; i < argc; i++) { + if (prompt_set) { + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, "unexpected argument `%s`", argv[i]); + char *r = xbuf_steal(&b); + if (err) + *err = r; + else + free(r); + return -1; + } + out->prompt = xstrdup(argv[i]); + prompt_set = 1; + } + return 0; +} + +void cli_free(cli *c) { + free(c->prompt); + free(c->model); + free(c->provider); + free(c->base_url); + free(c->api_key); + free(c->cwd); + free(c->preamble); + free(c->resume); + sl_free(&c->shell_allowlist); + sl_free(&c->shell_denylist); +} + + +/* ------------------------------------------------------------------ */ +/* Merge + validation */ +/* ------------------------------------------------------------------ */ + +static int validate_fraction(const char *name, double value, char **err) { + if (value == 0.0) + return 0; + if (isnan(value) || isinf(value) || value <= 0.0 || value > 1.0) { + if (err) { + xbuf b; + xbuf_init(&b); + const char *rep; + if (isnan(value)) + rep = "NaN"; + else if (value > 0) + rep = "inf"; + else + rep = "-inf"; + xbuf_fmt(&b, "%s must be 0 or in (0, 1], got %s", name, rep); + *err = xbuf_steal(&b); + } + return -1; + } + return 0; +} + +static int validate_compaction(const config *c, char **err) { + if (validate_fraction("compact_threshold", c->compact_threshold, err) != 0) + return -1; + if (validate_fraction("clear_threshold", c->clear_threshold, err) != 0) + return -1; + if (c->context_limit > 0 && c->compact_threshold > 0.0 && + c->clear_threshold > 0.0 && + c->clear_threshold >= c->compact_threshold) { + if (err) { + char *ct = dtoa_dup(c->clear_threshold); + char *pt = dtoa_dup(c->compact_threshold); + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, + "clear_threshold (%s) must be less than " + "compact_threshold (%s)", + ct, pt); + *err = xbuf_steal(&b); + free(ct); + free(pt); + } + return -1; + } + return 0; +} + +int config_require_key_or_base(const config *c, char **err) { + if (c->provider != PROV_OLLAMA && !c->base_url && !c->api_key) { + if (err) { + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, + "no API key for provider `%s`: set `api_key` in " + "`.agents/models.json` or pass --api-key", + provider_debug(c->provider)); + *err = xbuf_steal(&b); + } + return -1; + } + return 0; +} + +void config_free(config *c) { + free(c->model); + free(c->base_url); + free(c->api_key); + free(c->preamble); + sl_free(&c->shell_allowlist); + sl_free(&c->shell_denylist); +} + +int config_merge(const cli *cli, config *out, char **err) { + memset(out, 0, sizeof *out); + sl_init(&out->shell_allowlist); + sl_init(&out->shell_denylist); + out->confirm_destructive = cli->confirm_destructive; + for (size_t i = 0; i < cli->shell_allowlist.len; i++) + sl_push_copy(&out->shell_allowlist, cli->shell_allowlist.items[i]); + for (size_t i = 0; i < cli->shell_denylist.len; i++) + sl_push_copy(&out->shell_denylist, cli->shell_denylist.items[i]); + + out->provider = provider_parse(cli->provider ? cli->provider : "openai", + err); + if (err && *err) + return -1; + + out->model = xstrdup(cli->model ? cli->model + : provider_default_model(out->provider)); + + out->base_url = cli->base_url ? xstrdup(cli->base_url) : NULL; + out->api_key = cli->api_key ? xstrdup(cli->api_key) : NULL; + + out->preamble = cli->preamble ? xstrdup(cli->preamble) + : default_preamble(); + + out->max_turns = cli->has_max_turns ? cli->max_turns : 0; + out->max_retries = cli->has_max_retries ? cli->max_retries + : DEFAULT_MAX_RETRIES; + out->context_limit = + cli->has_context_limit ? cli->context_limit : 0; + out->compact_threshold = cli->has_compact_threshold ? cli->compact_threshold + : DEFAULT_COMPACT_THRESHOLD; + out->clear_threshold = cli->has_clear_threshold ? cli->clear_threshold + : DEFAULT_CLEAR_THRESHOLD; + + out->rubric_enabled = !cli->no_rubric; + out->rubric_max_summaries = cli->has_rubric_max_summaries + ? cli->rubric_max_summaries + : RUBRIC_DEFAULT_MAX_SUMMARIES; + + out->verbose = cli->verbose; + + out->skill_index_cap = + cli->has_skill_index_cap ? cli->skill_index_cap + : DEFAULT_SKILL_INDEX_CAP; + if (out->skill_index_cap < 1) + out->skill_index_cap = 1; + out->shell_timeout_secs = + cli->has_shell_timeout_secs ? cli->shell_timeout_secs + : DEFAULT_SHELL_TIMEOUT_SECS; + if (out->shell_timeout_secs < 1) + out->shell_timeout_secs = 1; + out->max_output_chars = + cli->has_max_output_chars ? cli->max_output_chars + : DEFAULT_MAX_OUTPUT_CHARS; + if (out->max_output_chars < 1) + out->max_output_chars = 1; + out->max_file_bytes = + cli->has_max_file_bytes ? cli->max_file_bytes + : DEFAULT_MAX_FILE_BYTES; + if (out->max_file_bytes < 1) + out->max_file_bytes = 1; + + if (validate_compaction(out, err) != 0) + return -1; + /* The api_key/base_url requirement is checked by the caller AFTER a + * models.json preset may have supplied them (precedence: CLI flag > + * models.json preset > provider defaults). */ + return 0; +} + +int config_from_cli(const cli *cli, config *out, char **err) { + const char *cwd = cli->cwd ? cli->cwd : "."; + + if (config_merge(cli, out, err) != 0) { + config_free(out); + return -1; + } + + char *cwd_exp = expand_tilde(cwd); + if (chdir(cwd_exp) != 0) { + if (err) { + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, "failed to enter working directory `%s`: %s", + cwd_exp, strerror(errno)); + *err = xbuf_steal(&b); + } + free(cwd_exp); + config_free(out); + return -1; + } + free(cwd_exp); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* models.json presets */ +/* ------------------------------------------------------------------ */ + +char *config_apply_models_preset(config *cfg, const models_file *models, + const cli *cli, char **err) { + const char *name = models->default_name; + if (!name || !*name) { + if (err) + *err = xstrdup("models.json declares no `default` preset"); + return NULL; + } + const model_preset *preset = models_file_get(models, name); + if (!preset) { + if (err) { + xbuf b; + xbuf_init(&b); + xbuf_put(&b, "models.json default preset `"); + xbuf_put(&b, name); + xbuf_put(&b, "` is not in presets ("); + for (size_t i = 0; i < models->preset_count; i++) { + if (i) + xbuf_put(&b, ", "); + xbuf_put(&b, models->presets[i].name ? models->presets[i].name : ""); + } + xbuf_put(&b, ")"); + *err = xbuf_steal(&b); + } + return NULL; + } + if (!cli->provider && preset->provider) { + cfg->provider = provider_parse(preset->provider, err); + if (err && *err) + return NULL; + } + if (!cli->model && preset->model) { + free(cfg->model); + cfg->model = xstrdup(preset->model); + } + if (!cli->base_url && preset->base_url) { + free(cfg->base_url); + cfg->base_url = xstrdup(preset->base_url); + } + if (!cli->api_key && preset->api_key) { + free(cfg->api_key); + cfg->api_key = xstrdup(preset->api_key); + } + if (!cli->has_max_turns && preset->has_max_turns && + preset->max_turns > 0) + cfg->max_turns = preset->max_turns; + if (!cli->has_context_limit && preset->has_context_limit) + cfg->context_limit = preset->context_limit; + + if (cfg->provider != PROV_OLLAMA && !cfg->base_url && !cfg->api_key) { + if (err) { + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, + "no API key for provider `%s` after applying preset " + "`%s`: set `api_key` in the preset/config file or " + "pass --api-key", + provider_debug(cfg->provider), name); + *err = xbuf_steal(&b); + } + return NULL; + } + if (validate_compaction(cfg, err) != 0) + return NULL; + return xstrdup(name); +} |