v0.8.3: WebUI models, providers and keys; garbled thinking fixed
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user