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
This commit is contained in:
@@ -1850,6 +1850,9 @@ static void fill_request(cst_agent_t *a, cst_llm_request_t *req,
|
||||
req->session_path = cst_session_path(a->session);
|
||||
req->cancel_flag = &a->cancel;
|
||||
req->tool_out_cap = a->cfg.tool_out_cap;
|
||||
req->temperature = a->cfg.temperature;
|
||||
req->top_p = a->cfg.top_p;
|
||||
req->max_tokens = a->cfg.max_tokens;
|
||||
if (cst_protocol_is_native(proto)) {
|
||||
req->enable_tools = 1;
|
||||
req->tool_cfg = &a->cfg;
|
||||
@@ -2538,6 +2541,24 @@ cst_agent_t *cst_agent_create(const cst_agent_config_t *cfg) {
|
||||
a->cfg.max_steps = CST_DEFAULT_MAX_STEPS;
|
||||
if (a->cfg.tool_out_cap == 0)
|
||||
a->cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP;
|
||||
/* The generation parameters the user saved for this provider (Settings ->
|
||||
* Advanced in the app, "params" in settings.json). They are read here so
|
||||
* that the CLI and the TUI send what the GUI was set to; a caller that
|
||||
* named one explicitly wins. */
|
||||
{
|
||||
cst_settings_t st;
|
||||
if (cst_settings_load(&st) == 0) {
|
||||
const cst_gen_params_t *g = cst_settings_params(&st, a->cfg.provider);
|
||||
if (g) {
|
||||
if (a->cfg.temperature <= 0)
|
||||
a->cfg.temperature = g->temperature;
|
||||
if (a->cfg.top_p <= 0)
|
||||
a->cfg.top_p = g->top_p;
|
||||
if (a->cfg.max_tokens <= 0)
|
||||
a->cfg.max_tokens = g->max_tokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
a->session = cst_session_open(cfg->session_path[0] ? cfg->session_path
|
||||
: "session.jsonl");
|
||||
if (!a->session) {
|
||||
|
||||
@@ -444,6 +444,15 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *so = cJSON_AddObjectToObject(root, "stream_options");
|
||||
cJSON_AddTrueToObject(so, "include_usage");
|
||||
}
|
||||
/* The generation parameters the user set (Settings -> Advanced). A field
|
||||
* that was not set is left out: an absent field is the provider's own
|
||||
* default, which "temperature": 0 would not be. */
|
||||
if (req->temperature > 0)
|
||||
cJSON_AddNumberToObject(root, "temperature", req->temperature);
|
||||
if (req->top_p > 0)
|
||||
cJSON_AddNumberToObject(root, "top_p", req->top_p);
|
||||
if (req->max_tokens > 0)
|
||||
cJSON_AddNumberToObject(root, "max_tokens", req->max_tokens);
|
||||
cJSON *msgs = cJSON_AddArrayToObject(root, "messages");
|
||||
size_t tcap = wire_tool_cap(req);
|
||||
cJSON *prev_user = NULL; /* merge consecutive user messages */
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "cst_models.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -326,6 +328,162 @@ int cst_models_user_path(char *out, size_t out_sz) {
|
||||
return w > 0 && (size_t)w < out_sz ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --- the user file: providers the presets do not know -------------------- */
|
||||
|
||||
static int valid_provider_name(const char *name) {
|
||||
if (!name || !name[0] || strlen(name) >= 32)
|
||||
return 0;
|
||||
for (const char *q = name; *q; q++)
|
||||
if (!isalnum((unsigned char)*q) && *q != '.' && *q != '_' && *q != '-')
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The user file as a JSON object: an empty one when there is no file, NULL
|
||||
* when there is one that is not an object (never overwrite somebody else's
|
||||
* file, their edit is not worth losing). */
|
||||
static cJSON *read_user_doc(char *path, size_t path_sz) {
|
||||
if (cst_models_user_path(path, path_sz) != 0)
|
||||
return NULL;
|
||||
char *text = read_file(path);
|
||||
if (!text)
|
||||
return cJSON_CreateObject();
|
||||
cJSON *doc = cJSON_Parse(text + cst_bom_len(text, strlen(text)));
|
||||
free(text);
|
||||
if (!cJSON_IsObject(doc)) {
|
||||
cJSON_Delete(doc);
|
||||
return NULL;
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
static int write_user_doc(cJSON *doc, const char *path, char *err,
|
||||
size_t err_sz) {
|
||||
char *text = cJSON_Print(doc);
|
||||
if (!text) {
|
||||
snprintf(err, err_sz, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
char tmp[1200];
|
||||
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp)) {
|
||||
cJSON_free(text);
|
||||
snprintf(err, err_sz, "path too long");
|
||||
return -1;
|
||||
}
|
||||
int rc = -1;
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if (!f) {
|
||||
snprintf(err, err_sz, "%s: cannot write (%s)", tmp, strerror(errno));
|
||||
} else {
|
||||
const int ok = fputs(text, f) >= 0 && fputc('\n', f) != EOF;
|
||||
const int closed = fclose(f);
|
||||
if (ok && closed == 0 &&
|
||||
#ifdef _WIN32
|
||||
(remove(path), 1) &&
|
||||
#endif
|
||||
rename(tmp, path) == 0)
|
||||
rc = 0;
|
||||
else
|
||||
snprintf(err, err_sz, "%s: cannot replace (%s)", path, strerror(errno));
|
||||
if (rc != 0)
|
||||
remove(tmp);
|
||||
}
|
||||
cJSON_free(text);
|
||||
return rc;
|
||||
}
|
||||
|
||||
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) {
|
||||
char dummy[8];
|
||||
if (!err || !err_sz) {
|
||||
err = dummy;
|
||||
err_sz = sizeof(dummy);
|
||||
}
|
||||
err[0] = 0;
|
||||
if (!valid_provider_name(name)) {
|
||||
snprintf(err, err_sz,
|
||||
"a provider name is 1-31 of letters, digits, '.', '_' or '-'");
|
||||
return -1;
|
||||
}
|
||||
if (!api_base || !api_base[0]) {
|
||||
snprintf(err, err_sz, "a base URL is needed");
|
||||
return -1;
|
||||
}
|
||||
char path[1100];
|
||||
pthread_mutex_lock(&g_mu);
|
||||
cJSON *doc = read_user_doc(path, sizeof(path));
|
||||
if (!doc) {
|
||||
snprintf(err, err_sz, "%s: not a JSON object, left alone", path);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return -1;
|
||||
}
|
||||
cJSON *providers = cJSON_GetObjectItemCaseSensitive(doc, "providers");
|
||||
if (!cJSON_IsObject(providers))
|
||||
providers = cJSON_AddObjectToObject(doc, "providers");
|
||||
cJSON *entry = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(entry, "api_base", api_base);
|
||||
if (key_env && key_env[0])
|
||||
cJSON_AddStringToObject(entry, "key_env", key_env);
|
||||
if (default_model && default_model[0])
|
||||
cJSON_AddStringToObject(entry, "default_model", default_model);
|
||||
if (models && nmodels > 0) {
|
||||
cJSON *arr = cJSON_AddArrayToObject(entry, "models");
|
||||
for (int i = 0; i < nmodels; i++)
|
||||
if (models[i] && models[i][0])
|
||||
cJSON_AddItemToArray(arr, cJSON_CreateString(models[i]));
|
||||
}
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(providers, name);
|
||||
cJSON_AddItemToObject(providers, name, entry);
|
||||
const int rc = write_user_doc(doc, path, err, err_sz);
|
||||
cJSON_Delete(doc);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (rc == 0)
|
||||
cst_models_reload(); /* the provider is there from now on */
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cst_models_user_drop_provider(const char *name, char *err, size_t err_sz) {
|
||||
char dummy[8];
|
||||
if (!err || !err_sz) {
|
||||
err = dummy;
|
||||
err_sz = sizeof(dummy);
|
||||
}
|
||||
err[0] = 0;
|
||||
if (!valid_provider_name(name)) {
|
||||
snprintf(err, err_sz, "not a provider name");
|
||||
return -1;
|
||||
}
|
||||
char path[1100];
|
||||
if (cst_models_user_path(path, sizeof(path)) != 0) {
|
||||
snprintf(err, err_sz, "no config directory");
|
||||
return -1;
|
||||
}
|
||||
FILE *probe = fopen(path, "rb");
|
||||
if (!probe)
|
||||
return 0; /* no user file: nothing of ours to drop */
|
||||
fclose(probe);
|
||||
pthread_mutex_lock(&g_mu);
|
||||
cJSON *doc = read_user_doc(path, sizeof(path));
|
||||
if (!doc) {
|
||||
snprintf(err, err_sz, "%s: not a JSON object, left alone", path);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return -1;
|
||||
}
|
||||
cJSON *providers = cJSON_GetObjectItemCaseSensitive(doc, "providers");
|
||||
const int found = cJSON_IsObject(providers) &&
|
||||
cJSON_GetObjectItemCaseSensitive(providers, name) != NULL;
|
||||
if (found)
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(providers, name);
|
||||
const int rc = found ? write_user_doc(doc, path, err, err_sz) : 0;
|
||||
cJSON_Delete(doc);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (rc == 0 && found)
|
||||
cst_models_reload();
|
||||
return rc == 0 ? found : -1;
|
||||
}
|
||||
|
||||
static void load_default_locked(void) {
|
||||
reset_locked();
|
||||
char e[128];
|
||||
|
||||
@@ -72,6 +72,25 @@ int cst_settings_load(cst_settings_t *s) {
|
||||
m);
|
||||
s->nmodels++;
|
||||
}
|
||||
const cJSON *params = cJSON_GetObjectItemCaseSensitive(root, "params");
|
||||
const cJSON *p;
|
||||
cJSON_ArrayForEach(p, params) {
|
||||
if (s->nparams >= CST_SETTINGS_MAX_PARAMS || !p->string || !cJSON_IsObject(p))
|
||||
continue;
|
||||
cst_gen_params_t *g = &s->params[s->nparams];
|
||||
memset(g, 0, sizeof(*g));
|
||||
snprintf(g->provider, sizeof(g->provider), "%s", p->string);
|
||||
const cJSON *v = cJSON_GetObjectItemCaseSensitive(p, "temperature");
|
||||
if (cJSON_IsNumber(v))
|
||||
g->temperature = v->valuedouble;
|
||||
v = cJSON_GetObjectItemCaseSensitive(p, "top_p");
|
||||
if (cJSON_IsNumber(v))
|
||||
g->top_p = v->valuedouble;
|
||||
v = cJSON_GetObjectItemCaseSensitive(p, "max_tokens");
|
||||
if (cJSON_IsNumber(v))
|
||||
g->max_tokens = (int)v->valuedouble;
|
||||
s->nparams++;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
@@ -102,6 +121,21 @@ int cst_settings_save(const cst_settings_t *s) {
|
||||
cJSON *models = cJSON_AddObjectToObject(root, "models");
|
||||
for (int i = 0; i < s->nmodels; i++)
|
||||
cJSON_AddStringToObject(models, s->models[i].provider, s->models[i].model);
|
||||
if (s->nparams) {
|
||||
cJSON *params = cJSON_AddObjectToObject(root, "params");
|
||||
for (int i = 0; i < s->nparams; i++) {
|
||||
const cst_gen_params_t *g = &s->params[i];
|
||||
if (!g->provider[0])
|
||||
continue;
|
||||
cJSON *o = cJSON_AddObjectToObject(params, g->provider);
|
||||
if (g->temperature > 0)
|
||||
cJSON_AddNumberToObject(o, "temperature", g->temperature);
|
||||
if (g->top_p > 0)
|
||||
cJSON_AddNumberToObject(o, "top_p", g->top_p);
|
||||
if (g->max_tokens > 0)
|
||||
cJSON_AddNumberToObject(o, "max_tokens", g->max_tokens);
|
||||
}
|
||||
}
|
||||
char *text = cJSON_Print(root);
|
||||
cJSON_Delete(root);
|
||||
if (!text)
|
||||
@@ -180,6 +214,65 @@ int cst_settings_remember(const cst_agent_config_t *cfg) {
|
||||
snprintf(s.models[i].provider, sizeof(s.models[i].provider), "%s",
|
||||
cfg->provider);
|
||||
snprintf(s.models[i].model, sizeof(s.models[i].model), "%s", cfg->model);
|
||||
/* The generation parameters ride along with the provider choice. An
|
||||
* all-zero config (the CLI, the TUI) leaves what the GUI saved alone. */
|
||||
if (cfg->temperature > 0 || cfg->top_p > 0 || cfg->max_tokens > 0) {
|
||||
int pi = 0;
|
||||
while (pi < s.nparams && strcmp(s.params[pi].provider, cfg->provider) != 0)
|
||||
pi++;
|
||||
if (pi == s.nparams) {
|
||||
if (s.nparams >= CST_SETTINGS_MAX_PARAMS)
|
||||
return -1;
|
||||
memset(&s.params[pi], 0, sizeof(s.params[pi]));
|
||||
snprintf(s.params[pi].provider, sizeof(s.params[pi].provider), "%s",
|
||||
cfg->provider);
|
||||
s.nparams++;
|
||||
}
|
||||
s.params[pi].temperature = cfg->temperature;
|
||||
s.params[pi].top_p = cfg->top_p;
|
||||
s.params[pi].max_tokens = cfg->max_tokens;
|
||||
}
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
const cst_gen_params_t *cst_settings_params(const cst_settings_t *s,
|
||||
const char *provider) {
|
||||
for (int i = 0; s && provider && provider[0] && i < s->nparams; i++)
|
||||
if (strcmp(s->params[i].provider, provider) == 0)
|
||||
return &s->params[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cst_settings_remember_params(const char *provider,
|
||||
const cst_gen_params_t *p) {
|
||||
if (!provider || !provider[0])
|
||||
return -1;
|
||||
cst_settings_t s;
|
||||
cst_settings_load(&s);
|
||||
int i = 0;
|
||||
while (i < s.nparams && strcmp(s.params[i].provider, provider) != 0)
|
||||
i++;
|
||||
const int forget = !p || (p->temperature <= 0 && p->top_p <= 0 &&
|
||||
p->max_tokens <= 0);
|
||||
if (i == s.nparams) {
|
||||
if (forget)
|
||||
return 0; /* nothing remembered, nothing to forget */
|
||||
if (s.nparams >= CST_SETTINGS_MAX_PARAMS)
|
||||
return -1;
|
||||
memset(&s.params[i], 0, sizeof(s.params[i]));
|
||||
snprintf(s.params[i].provider, sizeof(s.params[i].provider), "%s",
|
||||
provider);
|
||||
s.nparams++;
|
||||
}
|
||||
if (forget) { /* keep the array compact when an entry goes away */
|
||||
memmove(&s.params[i], &s.params[i + 1],
|
||||
sizeof(s.params[i]) * (s.nparams - i - 1));
|
||||
s.nparams--;
|
||||
} else {
|
||||
s.params[i].temperature = p->temperature;
|
||||
s.params[i].top_p = p->top_p;
|
||||
s.params[i].max_tokens = p->max_tokens;
|
||||
}
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user