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:
+24
-9
@@ -1282,13 +1282,22 @@ static void on_mm(const ctl_event_t *ev, void *ud) {
|
||||
}
|
||||
|
||||
static void feed_visible(turn_t *t, const char *data, size_t len) {
|
||||
if (!len || t->stop)
|
||||
return;
|
||||
cst_buf_append(&t->raw, data, len);
|
||||
if (t->ctl)
|
||||
ctl_parser_feed(t->ctl, data, len);
|
||||
else if (t->mm)
|
||||
mm_scan_feed(t->mm, data, len);
|
||||
/* cut after each '\n' and '>': a call only becomes ready there, and
|
||||
* raw_mark must not take in what follows it in the same chunk */
|
||||
while (len && !t->stop) {
|
||||
size_t n = 0;
|
||||
while (n < len && data[n] != '\n' && data[n] != '>')
|
||||
n++;
|
||||
if (n < len)
|
||||
n++;
|
||||
cst_buf_append(&t->raw, data, n);
|
||||
if (t->ctl)
|
||||
ctl_parser_feed(t->ctl, data, n);
|
||||
else if (t->mm)
|
||||
mm_scan_feed(t->mm, data, n);
|
||||
data += n;
|
||||
len -= n;
|
||||
}
|
||||
}
|
||||
|
||||
static void think_out(turn_t *t, const char *s, size_t n) {
|
||||
@@ -1306,10 +1315,16 @@ static void on_delta(const char *data, size_t len, void *ud) {
|
||||
const char *want = t->in_think ? "</think>" : "<think>";
|
||||
size_t wl = strlen(want);
|
||||
if (t->taglen == 0 && c != '<') {
|
||||
/* the whole run up to the next '<' in one call: byte by byte, a
|
||||
* front end would get every CJK character in three invalid pieces */
|
||||
size_t j = i + 1;
|
||||
while (j < len && data[j] != '<')
|
||||
j++;
|
||||
if (t->in_think)
|
||||
think_out(t, &c, 1);
|
||||
think_out(t, data + i, j - i);
|
||||
else
|
||||
feed_visible(t, &c, 1);
|
||||
feed_visible(t, data + i, j - i);
|
||||
i = j - 1;
|
||||
continue;
|
||||
}
|
||||
t->tagbuf[t->taglen++] = c;
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+20
@@ -99,6 +99,26 @@ size_t cst_utf8_floor(const char *s, size_t n) {
|
||||
return i - 1; /* sequence incomplete: cut before lead byte */
|
||||
}
|
||||
|
||||
char *cst_utf8_hold_feed(cst_utf8_hold_t *h, const char *s, size_t n,
|
||||
size_t *out_len) {
|
||||
size_t have = h->n;
|
||||
char *all = malloc(have + n + 1);
|
||||
if (!all)
|
||||
return NULL;
|
||||
memcpy(all, h->b, have);
|
||||
if (n)
|
||||
memcpy(all + have, s, n);
|
||||
size_t len = have + n;
|
||||
size_t cut = cst_utf8_floor(all, len);
|
||||
if (len - cut > sizeof(h->b))
|
||||
cut = len;
|
||||
h->n = len - cut;
|
||||
memcpy(h->b, all + cut, h->n);
|
||||
all[cut] = 0;
|
||||
*out_len = cut;
|
||||
return all;
|
||||
}
|
||||
|
||||
static size_t utf8_seq_ok(const unsigned char *p, size_t left) {
|
||||
unsigned char c = p[0];
|
||||
size_t need;
|
||||
|
||||
+395
-39
@@ -15,6 +15,9 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_http.h"
|
||||
#include "cst_models.h"
|
||||
#include "cst_provider_switch.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "cst_web_md.h"
|
||||
#include "cst_webui.h"
|
||||
@@ -82,6 +85,10 @@ typedef struct wui {
|
||||
uint64_t pending_seq; /* event of the open permission/ask_user, 0 = none */
|
||||
char ask_ans[WUI_ASK_MAX];
|
||||
|
||||
/* turn thread only: the tail of a UTF-8 sequence split across deltas,
|
||||
* [0] think_delta, [1] assistant_delta */
|
||||
cst_utf8_hold_t carry[2];
|
||||
|
||||
volatile int stopping;
|
||||
} wui_t;
|
||||
|
||||
@@ -157,8 +164,79 @@ static void wui_emit_text(wui_t *w, const char *type, const char *text,
|
||||
wui_emit(w, type, o);
|
||||
}
|
||||
|
||||
/* A JSON string with half a character turns into U+FFFD in the browser:
|
||||
* hold an incomplete trailing sequence until the next delta completes it. */
|
||||
static void wui_emit_stream(wui_t *w, int k, const char *type,
|
||||
const char *text, size_t len) {
|
||||
size_t n = 0;
|
||||
char *s = cst_utf8_hold_feed(&w->carry[k], text, len, &n);
|
||||
if (s && n)
|
||||
wui_emit_text(w, type, s, n);
|
||||
free(s);
|
||||
}
|
||||
|
||||
static void wui_reset_carry(wui_t *w) {
|
||||
w->carry[0].n = w->carry[1].n = 0;
|
||||
}
|
||||
|
||||
/* --- state --------------------------------------------------------------- */
|
||||
|
||||
/* Whether provider has a key and where it comes from: "saved", the env
|
||||
* variable's name, or "session" (given for this run only). The key itself
|
||||
* never leaves this function. */
|
||||
static int key_source(const cst_agent_config_t *cfg, const char *provider,
|
||||
char *from, size_t from_sz) {
|
||||
char k[512];
|
||||
const char *src = NULL;
|
||||
int found = cst_creds_resolve(provider, k, sizeof(k), &src) == 0;
|
||||
int cur = cfg && strcmp(cfg->provider, provider) == 0;
|
||||
int has = cur ? cfg->api_key[0] != 0 : found;
|
||||
if (!has)
|
||||
from[0] = 0;
|
||||
else if (cur && (!found || strcmp(k, cfg->api_key) != 0))
|
||||
snprintf(from, from_sz, "session");
|
||||
else
|
||||
snprintf(from, from_sz, "%s", src ? src : "saved");
|
||||
memset(k, 0, sizeof(k));
|
||||
return has;
|
||||
}
|
||||
|
||||
static void model_info_json(cJSON *o, const char *provider,
|
||||
const char *model) {
|
||||
cst_model_info_t mi;
|
||||
cst_model_info(provider, model, &mi);
|
||||
char label[48];
|
||||
cst_model_info_label(&mi, label, sizeof(label));
|
||||
cJSON_AddStringToObject(o, "label", label);
|
||||
cJSON_AddNumberToObject(o, "context", (double)mi.context);
|
||||
if (mi.vision < 0)
|
||||
cJSON_AddNullToObject(o, "vision");
|
||||
else
|
||||
cJSON_AddBoolToObject(o, "vision", mi.vision == 1);
|
||||
cJSON_AddBoolToObject(o, "reasoning", mi.reasoning != 0);
|
||||
cJSON_AddStringToObject(o, "source", mi.source);
|
||||
if (mi.has_price) {
|
||||
cJSON *p = cJSON_AddObjectToObject(o, "price");
|
||||
if (p) {
|
||||
cJSON_AddStringToObject(p, "currency", mi.price.currency);
|
||||
cJSON_AddNumberToObject(p, "miss", mi.price.miss);
|
||||
cJSON_AddNumberToObject(p, "hit", mi.price.hit);
|
||||
cJSON_AddNumberToObject(p, "out", mi.price.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void usage_json(cJSON *u, const cst_usage_t *s) {
|
||||
cJSON_AddNumberToObject(u, "requests", s->requests);
|
||||
cJSON_AddNumberToObject(u, "prompt_tokens", (double)s->prompt);
|
||||
cJSON_AddNumberToObject(u, "completion_tokens", (double)s->out);
|
||||
cJSON_AddNumberToObject(u, "cached_tokens", (double)s->cached);
|
||||
cJSON_AddNumberToObject(u, "cache_miss_tokens", (double)s->miss);
|
||||
cJSON_AddNumberToObject(u, "cost", s->cost);
|
||||
cJSON_AddBoolToObject(u, "priced", s->priced != 0);
|
||||
cJSON_AddStringToObject(u, "currency", s->currency);
|
||||
}
|
||||
|
||||
static cJSON *wui_state_new(wui_t *w) {
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
@@ -188,16 +266,29 @@ static cJSON *wui_state_new(wui_t *w) {
|
||||
cJSON_AddBoolToObject(o, "undo_available",
|
||||
cst_agent_undo_available(w->agent) != 0);
|
||||
cJSON *u = cJSON_AddObjectToObject(o, "usage");
|
||||
if (u) {
|
||||
cJSON_AddNumberToObject(u, "requests", sess.requests);
|
||||
cJSON_AddNumberToObject(u, "prompt_tokens", (double)sess.prompt);
|
||||
cJSON_AddNumberToObject(u, "completion_tokens", (double)sess.out);
|
||||
cJSON_AddNumberToObject(u, "cached_tokens", (double)sess.cached);
|
||||
cJSON_AddNumberToObject(u, "cache_miss_tokens", (double)sess.miss);
|
||||
cJSON_AddNumberToObject(u, "cost", sess.cost);
|
||||
cJSON_AddBoolToObject(u, "priced", sess.priced != 0);
|
||||
cJSON_AddStringToObject(u, "currency", sess.currency);
|
||||
}
|
||||
if (u)
|
||||
usage_json(u, &sess);
|
||||
u = cJSON_AddObjectToObject(o, "turn_usage");
|
||||
if (u)
|
||||
usage_json(u, &turn);
|
||||
cJSON_AddStringToObject(o, "api_base", cfg->api_base);
|
||||
char from[64];
|
||||
cJSON_AddBoolToObject(o, "key_set",
|
||||
key_source(cfg, cfg->provider, from, sizeof(from)));
|
||||
cJSON_AddStringToObject(o, "key_from", from);
|
||||
cJSON *mi = cJSON_AddObjectToObject(o, "model_info");
|
||||
if (mi)
|
||||
model_info_json(mi, cfg->provider, cfg->model);
|
||||
cJSON_AddBoolToObject(o, "effort_applies",
|
||||
cst_effort_wire(cfg->provider, cfg->model,
|
||||
cfg->effort) != NULL);
|
||||
/* 1 yes, 0 no, -1 not known */
|
||||
cJSON_AddNumberToObject(o, "sees_images", cst_agent_sees_images(w->agent));
|
||||
cJSON_AddNumberToObject(o, "max_steps", cfg->max_steps);
|
||||
cJSON_AddStringToObject(o, "tool_profile",
|
||||
cst_tool_profile_name(cfg->tool_profile));
|
||||
cJSON_AddNumberToObject(o, "perm_timeout",
|
||||
w->perm_timeout_s > 0 ? w->perm_timeout_s : 600);
|
||||
cJSON *imgs = cJSON_AddArrayToObject(o, "pending_images");
|
||||
int n = cst_agent_pending_count(w->agent);
|
||||
for (int i = 0; i < n && imgs; i++) {
|
||||
@@ -309,11 +400,11 @@ static void on_assistant_delta(const char *text, size_t len, void *ud) {
|
||||
w->md = cst_web_md_new(wui_md_emit, w);
|
||||
if (w->md)
|
||||
cst_web_md_feed(w->md, text, len);
|
||||
wui_emit_text(w, "assistant_delta", text, len);
|
||||
wui_emit_stream(w, 1, "assistant_delta", text, len);
|
||||
}
|
||||
|
||||
static void on_think_delta(const char *text, size_t len, void *ud) {
|
||||
wui_emit_text((wui_t *)ud, "think_delta", text, len);
|
||||
wui_emit_stream((wui_t *)ud, 0, "think_delta", text, len);
|
||||
}
|
||||
|
||||
static cJSON *call_json(const ctl_tool_call_t *call) {
|
||||
@@ -380,6 +471,10 @@ static int wui_wait_permission(wui_t *w, const char *tool, const char *detail,
|
||||
int has_client = w->subs != NULL;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
|
||||
int timeout = w->perm_timeout_s > 0 ? w->perm_timeout_s : 600;
|
||||
/* Nobody is watching: do not hang the turn for ten minutes. */
|
||||
if (!has_client)
|
||||
timeout = 3;
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddNumberToObject(o, "id", (double)id);
|
||||
@@ -390,12 +485,9 @@ static int wui_wait_permission(wui_t *w, const char *tool, const char *detail,
|
||||
strncmp(detail, CST_PERM_SANDBOX,
|
||||
strlen(CST_PERM_SANDBOX)) ==
|
||||
0);
|
||||
cJSON_AddNumberToObject(o, "timeout", timeout);
|
||||
wui_set_pending(w, wui_emit(w, "permission", o));
|
||||
}
|
||||
int timeout = w->perm_timeout_s > 0 ? w->perm_timeout_s : 600;
|
||||
/* Nobody is watching: do not hang the turn for ten minutes. */
|
||||
if (!has_client)
|
||||
timeout = 3;
|
||||
int answer = CST_PERM_DENY;
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
@@ -765,7 +857,9 @@ static void *wui_turn_main(void *arg) {
|
||||
hooks.userdata = w;
|
||||
char err[512];
|
||||
err[0] = 0;
|
||||
wui_reset_carry(w);
|
||||
int rc = cst_agent_run_turn_ex(w->agent, t->prompt, &hooks, err, sizeof(err));
|
||||
wui_reset_carry(w); /* a dangling partial character is dropped */
|
||||
wui_end_turn(w, rc, err);
|
||||
free(t->prompt);
|
||||
free(t);
|
||||
@@ -863,6 +957,46 @@ static void wui_release(wui_t *w) {
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
}
|
||||
|
||||
/* A copy of the config for a slow read (a model list fetch). Changes only
|
||||
* write it after claiming under mu, so none is half done while mu is held
|
||||
* and op_busy is clear; a running turn does not write these fields. */
|
||||
static int wui_cfg_snapshot(wui_t *w, cst_agent_config_t *out) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
int ok = !w->op_busy;
|
||||
if (ok)
|
||||
*out = *cst_agent_config(w->agent);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int wui_send_obj(cst_http_conn_t *c, int status, cJSON *o) {
|
||||
char *json = o ? cJSON_PrintUnformatted(o) : NULL;
|
||||
cJSON_Delete(o);
|
||||
if (!json)
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
int rc = cst_http_send_json(c, status, json);
|
||||
free(json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static cJSON *wui_body(cst_http_conn_t *c) {
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
return req->body_len ? cJSON_ParseWithLength(req->body, req->body_len)
|
||||
: NULL;
|
||||
}
|
||||
|
||||
static int wui_model_ok(const cJSON *m) {
|
||||
if (!cJSON_IsString(m))
|
||||
return 0;
|
||||
size_t n = strlen(m->valuestring);
|
||||
if (n == 0 || n > 127)
|
||||
return 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if ((unsigned char)m->valuestring[i] < 0x21)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define WUI_EXCLUSIVE(name) \
|
||||
static int name(cst_http_conn_t *c, void *ud) { \
|
||||
wui_t *w = ud; \
|
||||
@@ -1053,40 +1187,256 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "expected a JSON object");
|
||||
}
|
||||
/* everything is checked before anything changes */
|
||||
const cJSON *model = cJSON_GetObjectItem(j, "model");
|
||||
const cJSON *effort = cJSON_GetObjectItem(j, "effort");
|
||||
const cJSON *approval = cJSON_GetObjectItem(j, "approval");
|
||||
const cJSON *plan = cJSON_GetObjectItem(j, "plan_mode");
|
||||
const char *bad = NULL;
|
||||
if (cJSON_GetObjectItem(j, "provider"))
|
||||
bad = "切换提供商请用 POST /api/provider";
|
||||
else if (model && !wui_model_ok(model))
|
||||
bad = "模型名无效(1-127 个可见字符)";
|
||||
else if (effort && (!cJSON_IsString(effort) ||
|
||||
!cst_effort_valid(effort->valuestring)))
|
||||
bad = "推理强度只能是 low / medium / high / max";
|
||||
int a = 0;
|
||||
if (!bad && approval && (!cJSON_IsString(approval) ||
|
||||
cst_approval_parse(approval->valuestring, &a) != 0))
|
||||
bad = "审批模式只能是 ask / auto / yolo";
|
||||
if (!bad && plan && !cJSON_IsBool(plan))
|
||||
bad = "plan_mode 必须是 true 或 false";
|
||||
if (bad) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, bad);
|
||||
}
|
||||
/* the strings are read by the turn as it builds each request; approval
|
||||
* and plan mode may change mid-turn (say, to yolo at a pending gate) */
|
||||
int claimed = cJSON_GetObjectItem(j, "model") ||
|
||||
cJSON_GetObjectItem(j, "provider") ||
|
||||
cJSON_GetObjectItem(j, "effort");
|
||||
int claimed = model || effort;
|
||||
if (claimed && wui_claim(w) != 0) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "busy: wait for the running turn");
|
||||
return cst_http_send_err(c, 409, "正在运行,等这一轮结束再改模型");
|
||||
}
|
||||
const cJSON *item;
|
||||
if ((item = cJSON_GetObjectItem(j, "model")) && cJSON_IsString(item))
|
||||
snprintf(cfg->model, sizeof(cfg->model), "%s", item->valuestring);
|
||||
if ((item = cJSON_GetObjectItem(j, "provider")) && cJSON_IsString(item))
|
||||
snprintf(cfg->provider, sizeof(cfg->provider), "%s", item->valuestring);
|
||||
if ((item = cJSON_GetObjectItem(j, "effort")) && cJSON_IsString(item) &&
|
||||
cst_effort_valid(item->valuestring))
|
||||
snprintf(cfg->effort, sizeof(cfg->effort), "%s", item->valuestring);
|
||||
if ((item = cJSON_GetObjectItem(j, "approval")) && cJSON_IsString(item)) {
|
||||
int a = 0;
|
||||
if (cst_approval_parse(item->valuestring, &a) == 0) {
|
||||
cfg->approval = a;
|
||||
cfg->auto_approve = a == CST_APPROVAL_YOLO;
|
||||
}
|
||||
if (model)
|
||||
snprintf(cfg->model, sizeof(cfg->model), "%s", model->valuestring);
|
||||
if (effort)
|
||||
snprintf(cfg->effort, sizeof(cfg->effort), "%s", effort->valuestring);
|
||||
if (claimed)
|
||||
cst_settings_remember(cfg);
|
||||
if (approval) {
|
||||
cfg->approval = a;
|
||||
cfg->auto_approve = a == CST_APPROVAL_YOLO;
|
||||
if (a != CST_APPROVAL_YOLO) /* YOLO lasts for this run only */
|
||||
cst_settings_remember_approval(a);
|
||||
}
|
||||
if ((item = cJSON_GetObjectItem(j, "plan_mode")) && cJSON_IsBool(item)) {
|
||||
cfg->plan_mode = cJSON_IsTrue(item) ? 1 : 0;
|
||||
cfg->read_only = cfg->plan_mode;
|
||||
if (plan)
|
||||
cfg->plan_mode = cJSON_IsTrue(plan) ? 1 : 0;
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
if (effort && !cst_effort_wire(cfg->provider, cfg->model, cfg->effort))
|
||||
cJSON_AddStringToObject(o, "warning", "当前模型不接受推理强度参数:已保存,"
|
||||
"换到支持的模型时生效");
|
||||
}
|
||||
cJSON_Delete(j);
|
||||
if (claimed)
|
||||
wui_release(w);
|
||||
wui_emit_state(w);
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* GET /api/providers: the presets, whether each has a key and from where.
|
||||
* Never the key itself. */
|
||||
static int h_providers(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = malloc(sizeof(*cfg));
|
||||
if (!cfg)
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
if (wui_cfg_snapshot(w, cfg) != 0) {
|
||||
free(cfg);
|
||||
return cst_http_send_err(c, 409, "正在切换设置,稍后再试");
|
||||
}
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
free(cfg);
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
}
|
||||
cJSON_AddStringToObject(o, "current", cfg->provider);
|
||||
cJSON *arr = cJSON_AddArrayToObject(o, "providers");
|
||||
int n = cst_provider_count();
|
||||
for (int i = 0; arr && i < n; i++) {
|
||||
const cst_provider_t *p = cst_provider_at(i);
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!p || !e) {
|
||||
cJSON_Delete(e);
|
||||
continue;
|
||||
}
|
||||
int cur = strcmp(p->name, cfg->provider) == 0;
|
||||
cJSON_AddStringToObject(e, "name", p->name);
|
||||
cJSON_AddStringToObject(e, "api_base", cur ? cfg->api_base : p->api_base);
|
||||
cJSON_AddStringToObject(e, "model", cur ? cfg->model
|
||||
: (p->model ? p->model : ""));
|
||||
char from[64];
|
||||
cJSON_AddBoolToObject(e, "has_key",
|
||||
key_source(cfg, p->name, from, sizeof(from)));
|
||||
cJSON_AddStringToObject(e, "key_from", from);
|
||||
cJSON_AddStringToObject(e, "key_env", p->key_env ? p->key_env : "");
|
||||
cJSON_AddStringToObject(e, "source", cst_provider_source(p));
|
||||
cJSON_AddBoolToObject(e, "current", cur);
|
||||
cJSON_AddItemToArray(arr, e);
|
||||
}
|
||||
memset(cfg->api_key, 0, sizeof(cfg->api_key));
|
||||
free(cfg);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* GET /api/models[?refresh=1]: the picker list for the current provider.
|
||||
* A rejected key is a 401 that still carries the built-in names. */
|
||||
static int h_models(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
char rf[8] = "";
|
||||
cst_http_query(req, "refresh", rf, sizeof(rf));
|
||||
cst_agent_config_t *snap = malloc(sizeof(*snap));
|
||||
int max = CST_PS_MAX_MODELS + 32;
|
||||
cst_ps_model_t *list = calloc((size_t)max, sizeof(*list));
|
||||
if (!snap || !list) {
|
||||
free(snap);
|
||||
free(list);
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
}
|
||||
if (wui_cfg_snapshot(w, snap) != 0) {
|
||||
free(snap);
|
||||
free(list);
|
||||
return cst_http_send_err(c, 409, "正在切换设置,稍后再试");
|
||||
}
|
||||
int api_rc = -1;
|
||||
char err[256] = "";
|
||||
int n = cst_ps_models(snap, rf[0] == '1', list, max, &api_rc, err,
|
||||
sizeof(err));
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddStringToObject(o, "provider", snap->provider);
|
||||
cJSON_AddStringToObject(o, "current", snap->model);
|
||||
cJSON_AddStringToObject(o, "api_base", snap->api_base);
|
||||
cJSON_AddBoolToObject(o, "key_set", snap->api_key[0] != 0);
|
||||
cJSON_AddNumberToObject(o, "api_count", api_rc >= 0 ? api_rc : -1);
|
||||
if (api_rc == CST_MODELS_AUTH) {
|
||||
char msg[400];
|
||||
snprintf(msg, sizeof(msg), "%s 拒绝了当前 API key(%.200s),请换一个",
|
||||
snap->provider, err);
|
||||
cJSON_AddStringToObject(o, "error", msg);
|
||||
cJSON_AddBoolToObject(o, "auth", 1);
|
||||
} else if (api_rc < 0 && err[0]) {
|
||||
char msg[400];
|
||||
snprintf(msg, sizeof(msg), "拉取模型列表失败:%.300s", err);
|
||||
cJSON_AddStringToObject(o, "api_error", msg);
|
||||
}
|
||||
cJSON *arr = cJSON_AddArrayToObject(o, "models");
|
||||
for (int i = 0; arr && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "id", list[i].id);
|
||||
cJSON_AddBoolToObject(e, "api", list[i].api);
|
||||
model_info_json(e, snap->provider, list[i].id);
|
||||
cJSON_AddItemToArray(arr, e);
|
||||
}
|
||||
}
|
||||
memset(snap->api_key, 0, sizeof(snap->api_key));
|
||||
free(snap);
|
||||
free(list);
|
||||
return wui_send_obj(c, api_rc == CST_MODELS_AUTH ? 401 : 200, o);
|
||||
}
|
||||
|
||||
/* The answer to a key check / provider switch, in the browser's words. */
|
||||
static int ps_reply(wui_t *w, cst_http_conn_t *c, int r, const char *who,
|
||||
const char *err) {
|
||||
char msg[512];
|
||||
if (r == CST_PS_UNKNOWN) {
|
||||
snprintf(msg, sizeof(msg), "没有这个提供商:%.64s", who);
|
||||
return cst_http_send_err(c, 404, msg);
|
||||
}
|
||||
if (r == CST_MODELS_AUTH) {
|
||||
snprintf(msg, sizeof(msg), "%.64s 拒绝了这个 API key(%.300s)", who, err);
|
||||
return cst_http_send_err(c, 401, msg);
|
||||
}
|
||||
if (r < 0) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
snprintf(msg, sizeof(msg), "%.64s 需要 API key", who);
|
||||
cJSON_AddStringToObject(o, "error", msg);
|
||||
cJSON_AddBoolToObject(o, "need_key", 1);
|
||||
}
|
||||
return wui_send_obj(c, 400, o);
|
||||
}
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
cJSON_AddStringToObject(o, "provider", cfg->provider);
|
||||
cJSON_AddStringToObject(o, "model", cfg->model);
|
||||
msg[0] = 0;
|
||||
if (r & CST_PS_UNSAVED)
|
||||
snprintf(msg, sizeof(msg), "key 没能写进 credentials 文件,只在本次运行有效");
|
||||
else if (r & CST_PS_UNVERIFIED)
|
||||
snprintf(msg, sizeof(msg), "没能验证 key(%.300s),已先保存", err);
|
||||
if (msg[0])
|
||||
cJSON_AddStringToObject(o, "warning", msg);
|
||||
}
|
||||
wui_emit_state(w);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* Wipe a secret out of a parsed body before it is freed. */
|
||||
static void wipe_str(const cJSON *s) {
|
||||
if (cJSON_IsString(s) && s->valuestring)
|
||||
memset(s->valuestring, 0, strlen(s->valuestring));
|
||||
}
|
||||
|
||||
/* POST /api/provider {"name": "...", "key": "..."?} */
|
||||
static int h_provider_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(w->agent);
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *name = j ? cJSON_GetObjectItem(j, "name") : NULL;
|
||||
const cJSON *key = j ? cJSON_GetObjectItem(j, "key") : NULL;
|
||||
if (!cJSON_IsString(name) || (key && !cJSON_IsString(key))) {
|
||||
wipe_str(key);
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"name\": \"提供商\", \"key\": 可选}");
|
||||
}
|
||||
char who[64], err[256] = "";
|
||||
snprintf(who, sizeof(who), "%s", name->valuestring);
|
||||
int r = cst_ps_switch_provider(cfg, who, key ? key->valuestring : NULL, err,
|
||||
sizeof(err));
|
||||
wipe_str(key);
|
||||
cJSON_Delete(j);
|
||||
if (r >= 0) {
|
||||
char line[256];
|
||||
snprintf(line, sizeof(line), "提供商 → %s · 模型 %s", cfg->provider,
|
||||
cfg->model[0] ? cfg->model : "(未选)");
|
||||
wui_emit_str(w, "status", "message", line);
|
||||
}
|
||||
return ps_reply(w, c, r, who, err);
|
||||
}
|
||||
|
||||
/* POST /api/key {"key": "..."}: replace the current provider's key. */
|
||||
static int h_key_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(w->agent);
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *key = j ? cJSON_GetObjectItem(j, "key") : NULL;
|
||||
if (!cJSON_IsString(key) || !key->valuestring[0]) {
|
||||
wipe_str(key);
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"key\": \"...\"}");
|
||||
}
|
||||
char err[256] = "";
|
||||
int r = cst_ps_set_key(cfg, key->valuestring, err, sizeof(err));
|
||||
wipe_str(key);
|
||||
cJSON_Delete(j);
|
||||
return ps_reply(w, c, r, cfg->provider, err);
|
||||
}
|
||||
|
||||
static int h_undo_x(cst_http_conn_t *c, void *ud) {
|
||||
@@ -1140,6 +1490,8 @@ WUI_EXCLUSIVE(h_undo)
|
||||
WUI_EXCLUSIVE(h_compact)
|
||||
WUI_EXCLUSIVE(h_attach_upload)
|
||||
WUI_EXCLUSIVE(h_attach_clear)
|
||||
WUI_EXCLUSIVE(h_provider)
|
||||
WUI_EXCLUSIVE(h_key)
|
||||
|
||||
static int h_quit(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
@@ -1360,6 +1712,10 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
{"POST", "/api/permission", h_permission, NULL, 0, 0},
|
||||
{"POST", "/api/ask_user", h_ask_user, NULL, 0, 0},
|
||||
{"POST", "/api/config", h_config, NULL, 0, 0},
|
||||
{"GET", "/api/providers", h_providers, NULL, 0, 0},
|
||||
{"GET", "/api/models", h_models, NULL, 0, 0},
|
||||
{"POST", "/api/provider", h_provider, NULL, 0, 0},
|
||||
{"POST", "/api/key", h_key, NULL, 0, 0},
|
||||
{"POST", "/api/undo", h_undo, NULL, 0, 0},
|
||||
{"POST", "/api/compact", h_compact, NULL, 0, 0},
|
||||
{"POST", "/api/attach", h_attach_upload, NULL, 0, 32u * 1024u * 1024u},
|
||||
|
||||
Reference in New Issue
Block a user