Files
Hermit/src/provider_switch.c
T
WHC 2d1052fd5f
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
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.
2026-10-09 15:54:53 +08:00

194 lines
6.0 KiB
C

#include "cst_provider_switch.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cst_ps_fetch(const char *provider, const char *api_base, const char *key,
char ids[][128], int max, char *err, size_t err_sz) {
cst_model_meta_t *m = calloc((size_t)(max > 0 ? max : 1), sizeof(*m));
if (!m) {
snprintf(err, err_sz, "out of memory");
return -1;
}
int r = cst_llm_list_models_meta(api_base, key, m, max, err, err_sz);
if (r >= 0)
cst_models_cache_save(provider, api_base, m, r);
for (int i = 0; i < r; i++)
snprintf(ids[i], 128, "%s", m[i].id);
free(m);
return r;
}
int cst_ps_cached(const char *provider, char ids[][128], int max, int *fresh) {
*fresh = 0;
cst_model_meta_t *m = calloc((size_t)(max > 0 ? max : 1), 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;
}
const char *cst_ps_base(const cst_agent_config_t *cfg, const char *provider) {
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 : "";
}
int cst_ps_models(const cst_agent_config_t *cfg, int refresh,
cst_ps_model_t *out, int max, int *api_rc, char *err,
size_t err_sz) {
err[0] = 0;
*api_rc = -1;
char(*ids)[128] = calloc(CST_PS_MAX_MODELS, sizeof(*ids));
if (!ids) {
snprintf(err, err_sz, "out of memory");
return 0;
}
int fresh = 0;
int na = cst_ps_cached(cfg->provider, ids, CST_PS_MAX_MODELS, &fresh);
*api_rc = na;
if ((refresh || !fresh) && cfg->api_key[0]) {
char(*got)[128] = calloc(CST_PS_MAX_MODELS, sizeof(*got));
if (got) {
int r = cst_ps_fetch(cfg->provider, cfg->api_base, cfg->api_key, got,
CST_PS_MAX_MODELS, err, err_sz);
*api_rc = r;
if (r >= 0) {
memcpy(ids, got, (size_t)r * sizeof(*ids));
na = r;
}
free(got);
}
}
int n = 0;
for (int i = 0; i < na && n < max; i++, n++) {
snprintf(out[n].id, sizeof(out[n].id), "%s", ids[i]);
out[n].api = 1;
cst_model_info(cfg->provider, ids[i], &out[n].info);
}
char builtin[32][128];
int nb = cst_models_list(cfg->model, builtin, 32);
for (int i = 0; i < nb && n < max; i++) {
int dup = 0;
for (int k = 0; k < na && !dup; k++)
dup = strcmp(ids[k], builtin[i]) == 0;
if (dup)
continue;
memcpy(out[n].id, builtin[i], sizeof(out[n].id));
out[n].id[sizeof(out[n].id) - 1] = 0;
out[n].api = 0;
cst_model_info(cfg->provider, builtin[i], &out[n].info);
n++;
}
free(ids);
return n;
}
int cst_ps_verify_save(const char *provider, const char *api_base,
const char *key, char ids[][128], int max, int *nids,
char *err, size_t err_sz) {
err[0] = 0;
char(*own)[128] = NULL;
if (!ids) {
own = calloc(CST_PS_MAX_MODELS, sizeof(*own));
if (!own) {
snprintf(err, err_sz, "out of memory");
return CST_PS_UNVERIFIED | CST_PS_UNSAVED;
}
ids = own;
max = CST_PS_MAX_MODELS;
}
int r = cst_ps_fetch(provider, api_base, key, ids, max, err, err_sz);
free(own);
if (nids)
*nids = r;
if (r == CST_MODELS_AUTH)
return CST_MODELS_AUTH;
int flags = r < 0 ? CST_PS_UNVERIFIED : 0;
if (r >= 0)
err[0] = 0;
if (cst_creds_save(provider, key) != 0)
flags |= CST_PS_UNSAVED;
return flags;
}
void cst_ps_apply_provider(cst_agent_config_t *cfg, const cst_provider_t *p,
const char *key) {
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_settings_remember(cfg);
}
int cst_ps_switch_provider(cst_agent_config_t *cfg, const char *name,
const char *key, char *err, size_t err_sz) {
err[0] = 0;
const cst_provider_t *p = name ? cst_provider_find(name) : NULL;
if (!p) {
snprintf(err, err_sz, "unknown provider '%.64s'", name ? name : "");
return CST_PS_UNKNOWN;
}
char k[512];
int flags = 0;
char(*ids)[128] = calloc(CST_PS_MAX_MODELS, sizeof(*ids));
int nids = -1;
if (key && key[0]) {
snprintf(k, sizeof(k), "%s", key);
flags = cst_ps_verify_save(p->name, p->api_base, k, ids,
ids ? CST_PS_MAX_MODELS : 0, &nids, err,
err_sz);
if (flags < 0) {
memset(k, 0, sizeof(k));
free(ids);
return flags;
}
} else if (cst_creds_resolve(p->name, k, sizeof(k), NULL) != 0) {
free(ids);
snprintf(err, err_sz, "%s needs an API key", p->name);
return CST_PS_NEED_KEY;
}
cst_ps_apply_provider(cfg, p, k);
memset(k, 0, sizeof(k));
if (!cfg->model[0] && ids) {
/* a router preset: the first model the API lists */
int fresh = 0;
if (nids <= 0)
nids = cst_ps_cached(p->name, ids, CST_PS_MAX_MODELS, &fresh);
if (nids <= 0) {
char e2[256];
nids = cst_ps_fetch(p->name, cfg->api_base, cfg->api_key, ids,
CST_PS_MAX_MODELS, e2, sizeof(e2));
}
if (nids > 0) {
snprintf(cfg->model, sizeof(cfg->model), "%s", ids[0]);
cst_settings_remember(cfg);
}
}
free(ids);
return flags;
}
int cst_ps_set_key(cst_agent_config_t *cfg, const char *key, char *err,
size_t err_sz) {
if (!key || !key[0]) {
snprintf(err, err_sz, "empty key");
return CST_PS_NEED_KEY;
}
int r = cst_ps_verify_save(cfg->provider, cfg->api_base, key, NULL, 0, NULL,
err, err_sz);
if (r >= 0)
snprintf(cfg->api_key, sizeof(cfg->api_key), "%s", key);
return r;
}