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:
+20
-5
@@ -91,7 +91,7 @@ GET /api/events?since=<seq> → text/event-stream (chunked)
|
||||
| `tool` | `id`,`name`,`args`,`phase`,`detail` | `phase` = `open`/`ready`/`run`/`done` |
|
||||
| `tool_result` | `id`,`name`,`out`,`exit`,`bytes`,`truncated` | 完整输出被截到 32 KiB,`truncated=true` |
|
||||
| `todo` | `text` | `todo_write` 替换后的清单(一行一项) |
|
||||
| `permission` | `id`,`tool`,`detail`,`preview`,`sandbox` | 需要回答(见下) |
|
||||
| `permission` | `id`,`tool`,`detail`,`preview`,`sandbox`,`timeout` | 需要回答(见下);`timeout` 秒后按拒绝处理 |
|
||||
| `permission_done` | `id`,`answer` | 问题已结束(超时/取消也会发) |
|
||||
| `ask_user` | `id`,`question`,`options[]` | 需要回答 |
|
||||
| `session` | `id` | 切换/新建了会话,客户端应重新拉 `/api/session` |
|
||||
@@ -100,9 +100,16 @@ GET /api/events?since=<seq> → text/event-stream (chunked)
|
||||
`state` 字段:`workspace` `session` `provider` `model` `effort` `approval`
|
||||
(`ask|auto|yolo`) `plan_mode` `protocol_tools` `status_busy` `todo`
|
||||
`context_tokens` `undo_available` `pending_images[]` `usage{requests,prompt_tokens,
|
||||
completion_tokens,cached_tokens,cache_miss_tokens,cost,priced,currency}`。
|
||||
completion_tokens,cached_tokens,cache_miss_tokens,cost,priced,currency}`
|
||||
`turn_usage{…}`(上一轮,字段同 `usage`)`api_base` `key_set` `key_from`
|
||||
(`saved` / 环境变量名 / `session` / 空,**从不含 key 本身**)
|
||||
`model_info{label,context,vision,reasoning,source,price?}` `effort_applies`
|
||||
`sees_images`(1 / 0 / -1 未知)`max_steps` `tool_profile` `perm_timeout`。
|
||||
`cached_tokens` / `cache_miss_tokens` 来自上游的 prompt cache 统计,前端用它算命中率。
|
||||
|
||||
`think_delta` / `assistant_delta` 的 `text` 总是完整的 UTF-8:上游在多字节字符中间
|
||||
断开时,残缺的尾巴留到下一块再发。
|
||||
|
||||
### 请求
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
@@ -117,7 +124,11 @@ completion_tokens,cached_tokens,cache_miss_tokens,cost,priced,currency}`。
|
||||
| POST | `/api/cancel` | `{}` —— 取消当前轮(不会杀进程);挂起的权限问题按拒绝、提问按忽略处理 |
|
||||
| POST | `/api/permission` | `{"id":N,"answer":0\|1\|2\|3}` → deny / once / always-tool / always-prefix |
|
||||
| POST | `/api/ask_user` | `{"id":N,"text":"…"}` 或 `{"id":N,"dismiss":true}` |
|
||||
| POST | `/api/config` | `{model?,provider?,effort?,approval?,plan_mode?}` |
|
||||
| POST | `/api/config` | `{model?,effort?,approval?,plan_mode?}`;先整体校验再生效,model/effort/approval(`yolo` 除外)写进 `settings.json`;当前模型不吃 effort 时回 `warning`;带 `provider` → 400(改用 `/api/provider`) |
|
||||
| GET | `/api/providers` | `{current, providers:[{name,api_base,model,has_key,key_from,key_env,source,current}]}` |
|
||||
| GET | `/api/models[?refresh=1]` | 当前提供商的模型:`{provider,current,api_base,key_set,api_count,models:[{id,api,label,context,vision,reasoning,source,price?}]}`;先用缓存,过期/缺失/`refresh=1` 时向 `/v1/models` 拉;key 被拒 → 401(仍带内置模型和 `auth:true`),其它失败放在 `api_error` |
|
||||
| POST | `/api/provider` | `{"name":"…","key":"…"?}`:切换提供商。缺 key → 400 `{need_key:true}`;key 被拒 → 401;没这个提供商 → 404;成功 → `{ok,provider,model,warning?}`,key 验证后存进 `credentials`(0600) |
|
||||
| POST | `/api/key` | `{"key":"…"}`:换当前提供商的 key,验证与回复同上 |
|
||||
| POST | `/api/undo` | 回滚最近一轮的文件改动,回 200 + 报告文本 |
|
||||
| POST | `/api/compact` | 手动压缩上下文 |
|
||||
| POST | `/api/attach?name=x.png` | 请求体是图片原始字节(≤32 MiB),进入"待发送"列表 |
|
||||
@@ -127,8 +138,11 @@ completion_tokens,cached_tokens,cache_miss_tokens,cost,priced,currency}`。
|
||||
|
||||
未命中路由 → `404 {"error":"not found"}`;需要 token 而没给 → `401`。
|
||||
轮次运行中(或另一个修改正在进行)时,`sessions/new`、`sessions/switch`、`workspace`、
|
||||
`undo`、`compact`、`attach`(上传/清空)以及改 `model/provider/effort` 的 `config`
|
||||
一律 **409**;`approval`/`plan_mode` 可以中途改。
|
||||
`undo`、`compact`、`attach`(上传/清空)、`provider`、`key` 以及改 `model/effort` 的
|
||||
`config` 一律 **409**;`approval`/`plan_mode` 可以中途改。
|
||||
|
||||
key 只出现在请求体里:回复、事件、日志都不会带它。`/api/provider` 与 `/api/key`
|
||||
跟其它 `/api/*` 一样受 loopback 同源检查或 token 保护。
|
||||
|
||||
### 客户端接入示例(10 行)
|
||||
|
||||
@@ -176,6 +190,7 @@ TUI 里打 `/web` 会另起一个 WebUI 进程(`--web --workspace <当前目
|
||||
```bash
|
||||
make check # 含 tests/test_webui(HTTP/SSE/鉴权/消毒)
|
||||
sh scripts/e2e_webui.sh # 用录制流跑完整轮次 + 权限往返,不需要 API key
|
||||
# (含 e2e_webui_settings.py:假 /v1/models、换提供商/key、中文思考流)
|
||||
sh scripts/e2e_webui.sh tests/fixtures/sse/deepseek-ctl ctl
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user