forked from WHC/Hermit
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:
@@ -338,6 +338,112 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
check(false, QStringLiteral("the dialog has a model field"));
|
||||
}
|
||||
|
||||
/* --- advanced: a custom provider, added and removed ------------------- */
|
||||
/* The run has a scratch config dir (main.cpp points XDG_CONFIG_HOME at it),
|
||||
* so the engine's user model file is this run's own. */
|
||||
check(core.genParams(QStringLiteral("deepseek")).value(QStringLiteral("temperature"))
|
||||
.toDouble() == 0.0,
|
||||
QStringLiteral("no generation parameters are saved to begin with"));
|
||||
check(core.setGenParams(QStringLiteral("deepseek"), 0.2, 0.9, 4096).isEmpty(),
|
||||
QStringLiteral("generation parameters are accepted"));
|
||||
{
|
||||
const QVariantMap g = core.genParams(QStringLiteral("deepseek"));
|
||||
check(g.value(QStringLiteral("temperature")).toDouble() > 0.19 &&
|
||||
g.value(QStringLiteral("max_tokens")).toInt() == 4096,
|
||||
QStringLiteral("they are remembered per provider (%1 / %2 / %3)")
|
||||
.arg(g.value(QStringLiteral("temperature")).toDouble())
|
||||
.arg(g.value(QStringLiteral("top_p")).toDouble())
|
||||
.arg(g.value(QStringLiteral("max_tokens")).toInt()));
|
||||
}
|
||||
check(core.addProvider(QStringLiteral("cst-scratch"), QStringLiteral("https://api.example.com/v1"),
|
||||
QStringLiteral("CST_SCRATCH_KEY"), QStringLiteral("scratch-1"),
|
||||
QStringLiteral("scratch-1, scratch-2"))
|
||||
.isEmpty(),
|
||||
QStringLiteral("a provider the presets do not know is added"));
|
||||
{
|
||||
bool found = false, custom = false;
|
||||
QString base, models;
|
||||
const QVariantList list = core.providers();
|
||||
for (const QVariant &v : list) {
|
||||
const QVariantMap p = v.toMap();
|
||||
if (p.value(QStringLiteral("name")).toString() != QLatin1String("cst-scratch"))
|
||||
continue;
|
||||
found = true;
|
||||
custom = p.value(QStringLiteral("custom")).toBool();
|
||||
base = p.value(QStringLiteral("base")).toString();
|
||||
models = core.modelChoices(QStringLiteral("cst-scratch")).join(QLatin1Char(','));
|
||||
}
|
||||
check(found && custom && base.contains(QLatin1String("api.example.com")),
|
||||
QStringLiteral("it shows up in the provider list as a custom one"));
|
||||
check(models.contains(QLatin1String("scratch-1")) &&
|
||||
models.contains(QLatin1String("scratch-2")),
|
||||
QStringLiteral("the models it lists are the picker's (%1)").arg(models));
|
||||
}
|
||||
check(!core.addProvider(QStringLiteral("bad name"), QStringLiteral("https://x/v1"),
|
||||
QString(), QString(), QString())
|
||||
.isEmpty(),
|
||||
QStringLiteral("a name that is not a provider name is refused"));
|
||||
check(core.removeProvider(QStringLiteral("cst-scratch")) == 1,
|
||||
QStringLiteral("the custom provider can be removed again"));
|
||||
{
|
||||
bool gone = true;
|
||||
const QVariantList list = core.providers();
|
||||
for (const QVariant &v : list)
|
||||
if (v.toMap().value(QStringLiteral("name")).toString() ==
|
||||
QLatin1String("cst-scratch"))
|
||||
gone = false;
|
||||
check(gone, QStringLiteral("and it is gone from the list"));
|
||||
}
|
||||
check(core.removeProvider(QStringLiteral("deepseek")) == 0,
|
||||
QStringLiteral("a preset cannot be removed (there is no user entry to drop)"));
|
||||
check(core.setGenParams(QStringLiteral("deepseek"), 0, 0, 0).isEmpty() &&
|
||||
core.genParams(QStringLiteral("deepseek"))
|
||||
.value(QStringLiteral("temperature"))
|
||||
.toDouble() == 0.0,
|
||||
QStringLiteral("parameters can be cleared again"));
|
||||
|
||||
/* The dialog drives the same two engine calls: open it, fold the advanced
|
||||
* section out and put a provider in through the form. */
|
||||
clickMouse(win, QStringLiteral("settingsButton"));
|
||||
if (panel && mfield) {
|
||||
clickMouse(win, QStringLiteral("settingsAdvanced"));
|
||||
QQuickItem *nfield = item(win, QStringLiteral("advName"));
|
||||
QQuickItem *bfield = item(win, QStringLiteral("advBase"));
|
||||
check(nfield && nfield->isVisible(), QStringLiteral("Advanced options opens in the dialog"));
|
||||
maybeShot(win, QStringLiteral("-advanced"));
|
||||
if (nfield && bfield) {
|
||||
nfield->setProperty("text", QStringLiteral("cst-form"));
|
||||
bfield->setProperty("text", QStringLiteral("https://form.example.com/v1"));
|
||||
/* The form sits at the end of the dialog's scrolling body: scroll it
|
||||
* into view so the click lands on the button, not on the frame. */
|
||||
QQuickItem *scroller = item(win, QStringLiteral("settingsBody"));
|
||||
if (scroller) {
|
||||
scroller->setProperty("contentY", scroller->property("contentHeight").toReal() -
|
||||
scroller->height());
|
||||
QTest::qWait(120); /* the flickable settles */
|
||||
}
|
||||
clickMouse(win, QStringLiteral("settingsSaveProvider"));
|
||||
bool found = false;
|
||||
const QVariantList list = core.providers();
|
||||
for (const QVariant &v : list)
|
||||
if (v.toMap().value(QStringLiteral("name")).toString() ==
|
||||
QLatin1String("cst-form"))
|
||||
found = true;
|
||||
check(found, QStringLiteral("the form in the dialog adds a provider"));
|
||||
QQuickItem *rm = item(win, QStringLiteral("settingsRemoveProvider"));
|
||||
check(rm && rm->property("enabled").toBool(),
|
||||
QStringLiteral("its entry can be removed from the form"));
|
||||
clickMouse(win, QStringLiteral("settingsRemoveProvider"));
|
||||
check(core.removeProvider(QStringLiteral("cst-form")) == 0,
|
||||
QStringLiteral("the form's provider is gone again"));
|
||||
} else {
|
||||
check(false, QStringLiteral("the advanced form has name and base URL fields"));
|
||||
}
|
||||
clickMouse(win, QStringLiteral("settingsClose"));
|
||||
} else {
|
||||
check(false, QStringLiteral("the settings dialog can be reopened"));
|
||||
}
|
||||
|
||||
/* --- attachments: a file goes next to the session -------------------- */
|
||||
const QString att = core.workspace() + QStringLiteral("/note.txt");
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user