Files
WHC 82dee65942 v0.8.15: custom providers and model parameters under Advanced options
The settings dialog could pick a preset, a model and a key and nothing else.
Two things were missing: a provider the presets do not know, and what the
model is asked to do while it answers. Both live under "Advanced options"
now, next to the entry they belong to.

Custom providers (engine)
- cst_models_user_set_provider() / cst_models_user_drop_provider() write the
  engine's own user layer, <config>/cst-agent/models.json:
  {"providers": {"myllm": {"api_base": ..., "key_env": ...,
                           "default_model": ..., "models": [...]}}}
  and reload the registry, so the provider is at once in the provider list,
  in the wire models[] and in the CLI's /provider -- the file a hand-written
  override would go into is the same one, so the GUI and an editor agree.
  The name is checked (1-31 of letters, digits, '.', '_', '-'), a preset's
  name overrides that preset (a self-hosted endpoint, a proxy), everything
  else in the file (model rules) is kept, and a file that is not a JSON
  object is never overwritten. Dropping only removes the user's own entry.
- AgentCore::providers() gained "custom" (source == "user"), and
  addProvider() / removeProvider() drive the two calls above.

Model parameters (engine + wire)
- cst_agent_config_t and cst_llm_request_t carry temperature, top_p and
  max_tokens. 0 means the field is not sent at all -- an absent field is the
  provider's own default, which "temperature": 0 is not (deterministic
  decoding is a real choice), so cst_llm_build_body() only adds what was set.
  tests/test_llm_body.c:test_gen_params pins both halves.
- settings.json keeps them per provider ("params": {...}) via
  cst_settings_remember_params(); cst_settings_remember() carries them along
  with the provider choice, and cst_agent_create() reads them, so a CLI or
  TUI run sends what the window was set to. An all-zero config (the CLI)
  leaves what the GUI saved alone.
- AgentCore::genParams() / setGenParams(), and useProvider() applies both the
  remembered parameters of the provider it lands on and a base URL that was
  edited in the dialog.

The dialog
- Rewritten around one scrolling body with a fixed footer: a long model list
  used to push the key field and the buttons out of the dialog. The model
  chips are filtered (a filter field appears past twelve) and capped at 40
  with "... +N more", which is what a router with hundreds of models needs.
- "Advanced options" folds out the provider entry (name, base URL, key
  variable, default model, models; Save provider / Remove from models.json)
  and the model parameters (temperature, top-p, max tokens; empty = not
  sent). The fields follow the picked provider and are what gets saved, so a
  hand-typed edit is never lost to a stale property.

Verified: --uitest 67/67 (15 new checks: parameters round-trip through the
engine, a provider added through the form and removed again, a bad name
refused, a preset not removable), --selftest, ctest cst-app-selftest, and
make check with the new body test.

Version 0.8.15.

mode = coding
2026-10-11 13:26:02 +08:00

109 lines
4.8 KiB
C

#ifndef CST_MODELS_H
#define CST_MODELS_H
/* Model registry: what cst-agent knows about providers and models.
*
* Three layers, highest first, merged field by field:
* 1. <config>/models.json user overrides and extra providers
* 2. <config>/models-cache/<provider>.json GET /models metadata, exact id
* 3. built-in data (src/models_builtin.json, embedded at build time)
* then defaults (128000 tokens, vision unknown). Model rules match the name
* without its "vendor/" prefix, case-insensitively: a plain "match" is a
* prefix, one with '*' is a glob, one with '/' is tested against the full
* id. Within a layer the most specific rule (most literal characters) that
* sets a field wins, so table order does not matter.
*
* CST_CTX_TOKENS, CST_PRICE, CST_MODELS and CST_NO_CACHE_HINTS still
* override everything. */
#include "cst_agent.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
long context; /* tokens */
int vision; /* 1 takes images, 0 text only, -1 unknown */
int cache_style; /* cst_cache_style_t */
int reasoning; /* takes OpenAI-style reasoning_effort */
int has_price;
cst_price_t price;
/* Layers that supplied anything, highest first: "user+api+built-in",
* "default", "env+built-in"... */
char source[48];
} cst_model_info_t;
/* provider may be NULL: then every provider's fetched cache is searched. */
void cst_model_info(const char *provider, const char *model,
cst_model_info_t *out);
/* Short picker label: "1M · vision", "200K · text only", "128K". */
void cst_model_info_label(const cst_model_info_t *mi, char *out, size_t n);
/* One entry of GET /models, with the optional OpenRouter-style metadata. */
typedef struct {
char id[128];
long context; /* 0 = not given */
int vision; /* -1 = not given */
int has_price; /* prices in USD per 1M tokens */
double miss, hit, write, out;
} cst_model_meta_t;
/* Like cst_llm_list_models, keeping context_length,
* architecture.input_modalities (or modalities) and pricing when present. */
int cst_llm_list_models_meta(const char *api_base, const char *api_key,
cst_model_meta_t *out, int max_out, char *err,
size_t err_sz);
#define CST_MODELS_CACHE_TTL (24L * 3600)
/* Store a fetched list as <config>/models-cache/<provider>.json and use it
* from now on. 0 / -1. */
int cst_models_cache_save(const char *provider, const char *api_base,
const cst_model_meta_t *list, int n);
/* The cached list for provider: count (up to max), or -1 when there is
* none. *age_s gets the seconds since it was fetched. */
int cst_models_cache_load(const char *provider, cst_model_meta_t *out, int max,
long *age_s);
/* The "models" a provider entry lists (built-in or user file). */
int cst_provider_models(const cst_provider_t *p, char out[][128], int max);
/* Where a provider entry came from: "built-in" or "user". */
const char *cst_provider_source(const cst_provider_t *p);
/* The user file's path (it may not exist). */
int cst_models_user_path(char *out, size_t out_sz);
/* Add or replace a provider in the user file (layer 1 above) and reload the
* registry: this is how a provider the presets do not know becomes one.
* name is 1-31 characters of letters, digits, '.', '_' or '-'; api_base is
* required; key_env names the variable its key may come from; default_model
* and models[] are what the model picker offers (a fetched /models list still
* wins). An entry with a preset's name overrides that preset, everything else
* in the file (model rules) is kept. 0, or -1 with err filled. */
int cst_models_user_set_provider(const char *name, const char *api_base,
const char *key_env,
const char *default_model,
const char *const *models, int nmodels,
char *err, size_t err_sz);
/* Drop a provider from the user file (a preset of the same name stays).
* 1 = removed, 0 = it was not there, -1 with err. */
int cst_models_user_drop_provider(const char *name, char *err, size_t err_sz);
/* Drop everything and read the built-in data, the user file and the caches
* again. Provider pointers stay valid but may name another provider. */
void cst_models_reload(void);
/* Tests: load these documents instead of the built-in data and the user
* file (NULL = none). Caches still come from <config>. 0, or -1 with err. */
int cst_models_load_json(const char *builtin_json, const char *user_json,
char *err, size_t err_sz);
/* The embedded built-in document. */
const char *cst_models_builtin_json(void);
/* Problems found while loading (bad user file...), or "". */
const char *cst_models_load_error(void);
#ifdef __cplusplus
}
#endif
#endif