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.
393 lines
8.5 KiB
C
393 lines
8.5 KiB
C
#include "cst_util.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
#include <wctype.h>
|
|
|
|
static int buf_reserve(cst_buf_t *b, size_t extra) {
|
|
if (b->len + extra + 1 <= b->cap)
|
|
return 0;
|
|
size_t nc = b->cap ? b->cap : 256;
|
|
while (nc < b->len + extra + 1)
|
|
nc *= 2;
|
|
char *nb = realloc(b->buf, nc);
|
|
if (!nb)
|
|
return -1;
|
|
b->buf = nb;
|
|
b->cap = nc;
|
|
return 0;
|
|
}
|
|
|
|
int cst_buf_append(cst_buf_t *b, const char *s, size_t n) {
|
|
if (!b)
|
|
return -1;
|
|
if (buf_reserve(b, n) != 0)
|
|
return -1;
|
|
if (n)
|
|
memcpy(b->buf + b->len, s, n);
|
|
b->len += n;
|
|
b->buf[b->len] = 0;
|
|
return 0;
|
|
}
|
|
|
|
int cst_buf_puts(cst_buf_t *b, const char *s) {
|
|
return cst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
|
|
}
|
|
|
|
int cst_buf_printf(cst_buf_t *b, const char *fmt, ...) {
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
va_list ap2;
|
|
va_copy(ap2, ap);
|
|
int n = vsnprintf(NULL, 0, fmt, ap);
|
|
va_end(ap);
|
|
if (n < 0 || buf_reserve(b, (size_t)n) != 0) {
|
|
va_end(ap2);
|
|
return -1;
|
|
}
|
|
vsnprintf(b->buf + b->len, (size_t)n + 1, fmt, ap2);
|
|
va_end(ap2);
|
|
b->len += (size_t)n;
|
|
return 0;
|
|
}
|
|
|
|
void cst_buf_free(cst_buf_t *b) {
|
|
if (!b)
|
|
return;
|
|
free(b->buf);
|
|
b->buf = NULL;
|
|
b->len = b->cap = 0;
|
|
}
|
|
|
|
char *cst_buf_steal(cst_buf_t *b) {
|
|
if (buf_reserve(b, 0) != 0)
|
|
return NULL;
|
|
char *s = b->buf;
|
|
b->buf = NULL;
|
|
b->len = b->cap = 0;
|
|
return s;
|
|
}
|
|
|
|
size_t cst_utf8_floor(const char *s, size_t n) {
|
|
if (!s || n == 0)
|
|
return 0;
|
|
size_t i = n;
|
|
/* walk back over at most 3 continuation bytes to the lead byte */
|
|
size_t back = 0;
|
|
while (i > 0 && back < 3 && ((unsigned char)s[i - 1] & 0xC0) == 0x80) {
|
|
i--;
|
|
back++;
|
|
}
|
|
if (i == 0)
|
|
return n;
|
|
unsigned char lead = (unsigned char)s[i - 1];
|
|
size_t need = 1;
|
|
if (lead >= 0xF0)
|
|
need = 4;
|
|
else if (lead >= 0xE0)
|
|
need = 3;
|
|
else if (lead >= 0xC0)
|
|
need = 2;
|
|
if (need == 1)
|
|
return n; /* ASCII lead; any continuation bytes after it are invalid
|
|
anyway and get sanitized later */
|
|
if (back + 1 >= need)
|
|
return 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;
|
|
unsigned cp;
|
|
if (c < 0x80)
|
|
return 1;
|
|
if ((c & 0xE0) == 0xC0) {
|
|
need = 2;
|
|
cp = c & 0x1F;
|
|
} else if ((c & 0xF0) == 0xE0) {
|
|
need = 3;
|
|
cp = c & 0x0F;
|
|
} else if ((c & 0xF8) == 0xF0) {
|
|
need = 4;
|
|
cp = c & 0x07;
|
|
} else {
|
|
return 0;
|
|
}
|
|
if (left < need)
|
|
return 0;
|
|
for (size_t i = 1; i < need; i++) {
|
|
if ((p[i] & 0xC0) != 0x80)
|
|
return 0;
|
|
cp = (cp << 6) | (p[i] & 0x3F);
|
|
}
|
|
if ((need == 2 && cp < 0x80) || (need == 3 && cp < 0x800) ||
|
|
(need == 4 && (cp < 0x10000 || cp > 0x10FFFF)) ||
|
|
(cp >= 0xD800 && cp <= 0xDFFF))
|
|
return 0;
|
|
return need;
|
|
}
|
|
|
|
char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len) {
|
|
char *out = malloc(n + 1);
|
|
if (!out)
|
|
return NULL;
|
|
size_t o = 0;
|
|
const unsigned char *p = (const unsigned char *)(s ? s : "");
|
|
size_t i = 0;
|
|
while (i < n) {
|
|
size_t k = utf8_seq_ok(p + i, n - i);
|
|
if (k == 0) {
|
|
out[o++] = '?';
|
|
i++;
|
|
continue;
|
|
}
|
|
memcpy(out + o, p + i, k);
|
|
o += k;
|
|
i += k;
|
|
}
|
|
out[o] = 0;
|
|
if (out_len)
|
|
*out_len = o;
|
|
return out;
|
|
}
|
|
|
|
int cst_utf8_valid(const char *s, size_t n) {
|
|
const unsigned char *p = (const unsigned char *)s;
|
|
for (size_t i = 0; i < n;) {
|
|
size_t k = utf8_seq_ok(p + i, n - i);
|
|
if (k == 0)
|
|
return 0;
|
|
i += k;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
size_t cst_bom_len(const char *s, size_t n) {
|
|
return n >= 3 && memcmp(s, "\xEF\xBB\xBF", 3) == 0 ? 3 : 0;
|
|
}
|
|
|
|
void cst_strip_bom(char *s, size_t *len) {
|
|
if (!s)
|
|
return;
|
|
size_t n = len ? *len : strlen(s);
|
|
if (!cst_bom_len(s, n))
|
|
return;
|
|
memmove(s, s + 3, n - 3 + 1);
|
|
if (len)
|
|
*len = n - 3;
|
|
}
|
|
|
|
size_t cst_crlf_to_lf(char *s, size_t n) {
|
|
size_t o = 0;
|
|
for (size_t i = 0; i < n; i++)
|
|
if (!(s[i] == '\r' && i + 1 < n && s[i + 1] == '\n'))
|
|
s[o++] = s[i];
|
|
if (o < n)
|
|
s[o] = 0;
|
|
return o;
|
|
}
|
|
|
|
int cst_text_is_crlf(const char *s, size_t n) {
|
|
int crlf = 0;
|
|
for (size_t i = 0; s && i < n; i++) {
|
|
if (s[i] != '\n')
|
|
continue;
|
|
if (i == 0 || s[i - 1] != '\r')
|
|
return 0;
|
|
crlf = 1;
|
|
}
|
|
return crlf;
|
|
}
|
|
|
|
char *cst_text_restyle(const char *s, size_t n, int crlf, int bom,
|
|
size_t *out_len) {
|
|
size_t skip = cst_bom_len(s, n), extra = bom ? 3 : 0;
|
|
for (size_t i = skip; crlf && i < n; i++)
|
|
extra += s[i] == '\n' && (i == skip || s[i - 1] != '\r');
|
|
char *o = malloc(n - skip + extra + 1);
|
|
if (!o)
|
|
return NULL;
|
|
size_t k = 0;
|
|
if (bom) {
|
|
memcpy(o, "\xEF\xBB\xBF", 3);
|
|
k = 3;
|
|
}
|
|
for (size_t i = skip; i < n; i++) {
|
|
if (crlf && s[i] == '\n' && (i == skip || s[i - 1] != '\r'))
|
|
o[k++] = '\r';
|
|
o[k++] = s[i];
|
|
}
|
|
o[k] = 0;
|
|
if (out_len)
|
|
*out_len = k;
|
|
return o;
|
|
}
|
|
|
|
/* Next code point of a UTF-8 string; a stray byte stands for itself. */
|
|
static int utf8_next(const char **ps) {
|
|
const unsigned char *p = (const unsigned char *)*ps;
|
|
size_t left = 0;
|
|
while (left < 4 && p[left])
|
|
left++;
|
|
size_t k = utf8_seq_ok(p, left);
|
|
if (k <= 1) {
|
|
*ps += 1;
|
|
return p[0];
|
|
}
|
|
int c = p[0] & (0x7F >> k);
|
|
for (size_t i = 1; i < k; i++)
|
|
c = (c << 6) | (p[i] & 0x3F);
|
|
*ps += k;
|
|
return c;
|
|
}
|
|
|
|
static int fold_cp(int c) {
|
|
if (c >= 'A' && c <= 'Z')
|
|
return c + 32;
|
|
if (c > 0x7F && c <= 0xFFFF)
|
|
return (int)towlower((wint_t)c);
|
|
return c;
|
|
}
|
|
|
|
/* p points just past '['. 1/0 = (no) match with *pp moved past ']';
|
|
* -1 = no closing ']' (the '[' is then a literal). */
|
|
static int fnm_bracket(const char **pp, int c, int fold) {
|
|
const char *p = *pp;
|
|
int neg = 0, hit = 0;
|
|
if (*p == '!' || *p == '^') {
|
|
neg = 1;
|
|
p++;
|
|
}
|
|
int fc = fold ? fold_cp(c) : c;
|
|
for (int first = 1; *p && (*p != ']' || first); first = 0) {
|
|
if (p[0] == '[' && p[1] == ':') {
|
|
const char *e = strstr(p + 2, ":]");
|
|
if (e) {
|
|
char name[16];
|
|
size_t nl = (size_t)(e - (p + 2));
|
|
if (nl < sizeof(name)) {
|
|
memcpy(name, p + 2, nl);
|
|
name[nl] = 0;
|
|
wctype_t t = wctype(name);
|
|
if (t && c <= 0xFFFF && iswctype((wint_t)c, t))
|
|
hit = 1;
|
|
}
|
|
p = e + 2;
|
|
continue;
|
|
}
|
|
}
|
|
if (*p == '\\' && p[1])
|
|
p++;
|
|
int lo = utf8_next(&p), hi = lo;
|
|
if (p[0] == '-' && p[1] && p[1] != ']') {
|
|
p++;
|
|
if (*p == '\\' && p[1])
|
|
p++;
|
|
hi = utf8_next(&p);
|
|
}
|
|
if ((c >= lo && c <= hi) ||
|
|
(fold && fc >= fold_cp(lo) && fc <= fold_cp(hi)))
|
|
hit = 1;
|
|
}
|
|
if (*p != ']')
|
|
return -1;
|
|
*pp = p + 1;
|
|
return hit != neg;
|
|
}
|
|
|
|
int cst_fnmatch(const char *pat, const char *str, int flags) {
|
|
#ifdef _WIN32
|
|
flags |= CST_FNM_CASEFOLD;
|
|
#endif
|
|
int fold = (flags & CST_FNM_CASEFOLD) != 0;
|
|
const char *p = pat, *s = str;
|
|
const char *star_p = NULL, *star_s = NULL;
|
|
if ((flags & CST_FNM_PERIOD) && s[0] == '.' && p[0] != '.' &&
|
|
!(p[0] == '\\' && p[1] == '.'))
|
|
return 1;
|
|
while (*s) {
|
|
if (*p == '*') {
|
|
while (*p == '*')
|
|
p++;
|
|
if (!*p)
|
|
return 0;
|
|
star_p = p;
|
|
star_s = s;
|
|
continue;
|
|
}
|
|
const char *sn = s;
|
|
int c = utf8_next(&sn);
|
|
if (*p == '?') {
|
|
p++;
|
|
s = sn;
|
|
continue;
|
|
}
|
|
if (*p == '[') {
|
|
const char *q = p + 1;
|
|
int r = fnm_bracket(&q, c, fold);
|
|
if (r == 1) {
|
|
p = q;
|
|
s = sn;
|
|
continue;
|
|
}
|
|
if (r == 0)
|
|
goto backtrack;
|
|
}
|
|
{
|
|
const char *q = p;
|
|
if (*q == '\\' && q[1])
|
|
q++;
|
|
if (*q) {
|
|
int pc = utf8_next(&q);
|
|
if (pc == c || (fold && fold_cp(pc) == fold_cp(c))) {
|
|
p = q;
|
|
s = sn;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
backtrack:
|
|
if (!star_p)
|
|
return 1;
|
|
utf8_next(&star_s);
|
|
s = star_s;
|
|
p = star_p;
|
|
}
|
|
while (*p == '*')
|
|
p++;
|
|
return *p ? 1 : 0;
|
|
}
|
|
|
|
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n) {
|
|
const unsigned char *p = data;
|
|
for (size_t i = 0; i < n; i++) {
|
|
h ^= p[i];
|
|
h *= 1099511628211ULL;
|
|
}
|
|
return h;
|
|
}
|