v0.8.3: WebUI models, providers and keys; garbled thinking fixed
ci / linux (clang) (push) Successful in 5m56s
ci / linux (gcc) (push) Successful in 8m38s
ci / musl (push) Successful in 7m22s
ci / windows-cross (push) Successful in 9m21s

Streaming: Chinese reasoning showed up garbled in the WebUI. The agent
loop now feeds visible/thinking text in runs instead of byte by byte,
and the WebUI holds an incomplete UTF-8 tail until the next chunk
(cst_utf8_hold_feed), so every think_delta/assistant_delta is whole.

src/provider_switch.c: model list fetch/cache, key verify-and-save and
provider switching moved out of tui.c and shared by TUI and WebUI.

WebUI API:
- GET /api/providers, GET /api/models[?refresh=1] (401 on a rejected
  key, still listing built-ins), POST /api/provider, POST /api/key
  (key checked against /v1/models, saved to credentials 0600, never
  echoed in replies, events or logs)
- /api/config validates everything before applying, persists model,
  effort and approval (not yolo), warns when effort does not apply;
  plan_mode no longer forces read-only
- state: turn_usage, api_base, key_set/key_from, model_info,
  effort_applies, sees_images, max_steps, tool_profile, perm_timeout;
  permission events carry their timeout

Frontend: searchable model picker with refresh and manual entry,
provider picker and key dialog, effort/vision hints, toasts instead of
silent failures, Chinese error text, real protocol in the settings and
usage panels, approval countdown from the server's timeout.

Tests: utf8 hold unit test, scripts/e2e_webui_settings.py (fake
/v1/models, wrong key, switching, persistence, Chinese thinking) run
from e2e_webui.sh, scripts/tui_keys/provider.py.
This commit is contained in:
WHC
2026-10-09 15:54:53 +08:00
parent e238f5f37d
commit 2d1052fd5f
18 changed files with 1553 additions and 182 deletions
+13 -61
View File
@@ -11,6 +11,7 @@
#include "cst_mcp.h"
#include "cst_models.h"
#include "cst_plugins.h"
#include "cst_provider_switch.h"
#include "cst_skills.h"
#include "cst_subagent.h"
#include "cst_textsel.h"
@@ -3886,46 +3887,11 @@ static void am_store(const char *provider, int r, char ids[][128],
pthread_mutex_unlock(&g_am_mu);
}
/* GET /models; a good answer also refreshes models-cache/<provider>.json. */
static int fetch_models(const char *provider, const char *base,
const char *key, char ids[][128], int max, char *err,
size_t err_sz) {
cst_model_meta_t *m = calloc((size_t)max, sizeof(*m));
if (!m) {
snprintf(err, err_sz, "out of memory");
return -1;
}
int r = cst_llm_list_models_meta(base, key, m, max, err, err_sz);
if (r >= 0)
cst_models_cache_save(provider, base, m, r);
for (int i = 0; i < r; i++)
snprintf(ids[i], 128, "%s", m[i].id);
free(m);
return r;
}
/* The cached list for provider into ids: count, or -1; *fresh when it is
* younger than CST_MODELS_CACHE_TTL. */
static int cached_models(const char *provider, char ids[][128], int max,
int *fresh) {
*fresh = 0;
cst_model_meta_t *m = calloc((size_t)max, sizeof(*m));
if (!m)
return -1;
long age = 0;
int n = cst_models_cache_load(provider, m, max, &age);
for (int i = 0; i < n; i++)
snprintf(ids[i], 128, "%s", m[i].id);
free(m);
*fresh = n > 0 && age >= 0 && age < CST_MODELS_CACHE_TTL;
return n > 0 ? n : -1;
}
static void *probe_thread(void *arg) {
probe_arg_t *a = arg;
static char ids[AM_MAX][128]; /* one probe runs per provider switch */
char err[256];
int r = fetch_models(a->provider, a->base, a->key, ids, AM_MAX, err,
int r = cst_ps_fetch(a->provider, a->base, a->key, ids, AM_MAX, err,
sizeof(err));
if (r >= 0 || r == CST_MODELS_AUTH || !a->have_cache)
am_store(a->provider, r, ids, err);
@@ -3937,23 +3903,13 @@ static void *probe_thread(void *arg) {
return NULL;
}
/* Base URL for a provider: the live config for the current one (it may be
* overridden), else the preset. */
static const char *provider_base(const tui_t *t, const char *provider) {
const cst_agent_config_t *cfg = cst_agent_config(t->agent);
if (cfg && strcmp(cfg->provider, provider) == 0)
return cfg->api_base;
const cst_provider_t *p = cst_provider_find(provider);
return p ? p->api_base : "";
}
static void start_probe(tui_t *t) {
const cst_agent_config_t *cfg = cst_agent_config(t->agent);
if (!cfg || !cfg->api_key[0] || g_am_threads > 0)
return;
static char ids[AM_MAX][128];
int fresh = 0;
int nc = cached_models(cfg->provider, ids, AM_MAX, &fresh);
int nc = cst_ps_cached(cfg->provider, ids, AM_MAX, &fresh);
pthread_mutex_lock(&g_am_mu);
snprintf(g_am.provider, sizeof(g_am.provider), "%s", cfg->provider);
pthread_mutex_unlock(&g_am_mu);
@@ -4039,13 +3995,14 @@ static int obtain_key(tui_t *t, const char *provider, char *out, size_t n,
return 0;
char dir[1024] = "the config directory";
os_config_dir(dir, sizeof(dir));
const char *base = provider_base(t, provider);
const char *base = cst_ps_base(cst_agent_config(t->agent), provider);
char title[128], hint[1300];
snprintf(title, sizeof(title), "API key for %s", provider);
snprintf(hint, sizeof(hint), "%.200s · saved to %.900s/credentials (only you "
"can read it)",
base, dir);
char note[300] = "";
int flags;
for (;;) {
if (secret_input_run(t, title, hint, out, n) != 0)
return -1;
@@ -4053,8 +4010,10 @@ static int obtain_key(tui_t *t, const char *provider, char *out, size_t n,
draw_status(t);
static char ids[AM_MAX][128];
char err[256];
int r = fetch_models(provider, base, out, ids, AM_MAX, err, sizeof(err));
if (r == CST_MODELS_AUTH) {
int r = -1;
flags = cst_ps_verify_save(provider, base, out, ids, AM_MAX, &r, err,
sizeof(err));
if (flags == CST_MODELS_AUTH) {
snprintf(hint, sizeof(hint), "%s rejected that key (%.200s) — paste "
"another one",
base, err);
@@ -4064,13 +4023,13 @@ static int obtain_key(tui_t *t, const char *provider, char *out, size_t n,
snprintf(g_am.provider, sizeof(g_am.provider), "%s", provider);
pthread_mutex_unlock(&g_am_mu);
am_store(provider, r, ids, err);
if (r < 0)
if (flags & CST_PS_UNVERIFIED)
snprintf(note, sizeof(note), "could not verify the key (%.200s); "
"saved anyway",
err);
break;
}
if (cst_creds_save(provider, out) != 0)
if (flags & CST_PS_UNSAVED)
set_status(t, "warning: could not save the key; it is used for this run "
"only");
else if (note[0])
@@ -4302,7 +4261,7 @@ static void cmd_model(tui_t *t, char *arg, int *quit) {
set_status(t, "fetching the model list…");
draw_status(t);
char err[256];
na = fetch_models(cfg->provider, cfg->api_base, cfg->api_key, names,
na = cst_ps_fetch(cfg->provider, cfg->api_base, cfg->api_key, names,
AM_MAX, err, sizeof(err));
pthread_mutex_lock(&g_am_mu);
snprintf(g_am.provider, sizeof(g_am.provider), "%s", cfg->provider);
@@ -4436,15 +4395,8 @@ static void cmd_provider(tui_t *t, char *arg, int *quit) {
push_system_line(t, "(provider unchanged: %s needs an API key)", p->name);
return;
}
cst_settings_t s;
cst_settings_load(&s);
const char *model = cst_settings_model(&s, p->name);
snprintf(cfg->provider, sizeof(cfg->provider), "%s", p->name);
snprintf(cfg->api_base, sizeof(cfg->api_base), "%s", p->api_base);
snprintf(cfg->model, sizeof(cfg->model), "%s", model ? model : p->model);
snprintf(cfg->api_key, sizeof(cfg->api_key), "%s", key);
cst_ps_apply_provider(cfg, p, key);
memset(key, 0, sizeof(key));
cst_settings_remember(cfg);
push_system_line(t, "(provider → %s · %s · model %s)", p->name, p->api_base,
cfg->model);
if (am_models(p->name, NULL, 0) < 0)