diff --git a/HANDOFF_2026-10-19.md b/HANDOFF_2026-10-19.md new file mode 100644 index 0000000..ff1abce --- /dev/null +++ b/HANDOFF_2026-10-19.md @@ -0,0 +1,497 @@ +# ModelRouter / llmsproxy 交接文档 + +更新时间:2026-10-19 +工作目录:`/home/program/llmsproxy` +当前分支:`release/v2.0.x` + +## 1. 当前 Git 状态 + +最近提交: + +- `c9f3453 fix(codearts): 识别 InferHub 未注册模型` +- `12bfd53 fix: 收口渠道源迁移与测试页合并` +- `25970a2 refactor(scheduler)!: 移除 priority 废弃字段 + 一通道一源 + discover 复用凭据` + +`c9f3453` 已推送到 `origin/release/v2.0.x`。 + +注意:当前工作树在最后一轮添加 provider 回归测试时出现了一个**未完成且可能无法编译的测试编辑**。最后一次工具报错: + +```text +internal/provider/provider_test.go:99: missing ',' in argument list +``` + +下一位 agent 接手后的第一步必须是: + +```bash +cd /home/program/llmsproxy +git status --short +gofmt -w internal/provider/provider_test.go 2>&1 || true +go test -tags luajit ./internal/provider/ 2>&1 +``` + +如果测试文件处于半编辑状态,先检查 `TestProviderChatStreamNormalizesStopAfterToolCall`,修正 Go 字符串转义或删除该未完成测试后再继续。不要直接部署当前工作树。 + +## 2. 生产服务状态 + +生产服务:`llmsproxy.service` + +- 当前服务 active +- 监听:`0.0.0.0:8081` +- 二进制已通过 `./deploy.sh` 原子替换并重启 +- 最近一次部署健康检查通过 +- 当前生产插件 SHA256 前缀曾核对为:`1bf26cbb3abcb107` +- 当前生产二进制在最近部署后已更新 + +不要把密钥写入交接记录。生产 admin key 保存在本机运维环境中,不应复制到聊天、文档或提交。 + +## 3. Channel 源当前状态 + +生产源已经从旧的“一账号一源”收口成“一渠道一源”: + +```text +channels-trae +channels-minimax +channels-lobsterai +channels-buddy +channels-codearts +channels-zcode +``` + +旧形态源已经清理: + +```text +channels-- +``` + +此前旧源和新源并存,造成同一渠道同一模型出现多份,只能靠账号 hex 区分。 + +根因已修复:内核 `list_sources` 返回裸数组,但旧插件代码按 `{sources = [...]}` 信封读取,导致迁移清理循环永远为空。当前 `channels.lua` 已兼容: + +- 裸数组 +- `{sources = [...]}` +- `{json = {sources = [...]}}` + +相关代码: + +```text +internal/lua/plugins/channels.lua: syncProviderSource +internal/core/channels_legacy_source_cleanup_test.go +``` + +当前生产源列表已实测只有 6 个新形态源。 + +## 4. CodeArts 当前状态 + +### 已确认可用的部分 + +CodeArts 使用: + +```text +adapter = codearts +base_url = https://snap-access.cn-north-4.myhuaweicloud.com +``` + +CodeArts 的 chat 请求需要完整 AK/SK/security token 凭据,由插件适配器签名,不是裸 Bearer token。 + +真实流式工具调用测试已经成功: + +请求形状: + +```json +{ + "model": "channels-codearts:openpangu-2.0-flash", + "messages": [{"role": "user", "content": "Use the weather tool for Beijing."}], + "tools": [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"] + } + } + }], + "tool_choice": "auto", + "stream": true, + "max_tokens": 64 +} +``` + +结果: + +- HTTP 200 +- `Content-Type: text/event-stream` +- 返回结构化 `tool_calls` +- 函数名 `get_weather` +- 参数最终为 `{"city":"Beijing"}` +- `finish_reason = tool_calls` + +因此 CodeArts 的流式工具调用链路当前是通的。 + +### CodeArts 模型刷新 + +生产调用: + +```http +POST /api/plugins/http/channels/model/refresh +{"platform":"codearts"} +``` + +实测返回: + +```json +{ + "ok": true, + "results": [ + {"platform": "codearts", "ok": true, "count": 11} + ] +} +``` + +CodeArts 模型目录来自两个签名端点并合并: + +1. `opengw.developer.huaweicloud.com/api/v1/gateway/config` + - `result.models` +2. `snap-access.cn-north-4.myhuaweicloud.com/v1/model/builtin` + - `builtinModels` + +相关解析: + +```text +internal/lua/plugins/channels.lua: parseRemoteModels +internal/lua/plugins/channels.lua: refreshRemoteModels +internal/core/channels_codearts_catalog_test.go +``` + +### 已确认失效并禁用的 CodeArts 模型 + +下面 4 个模型曾收到明确的上游 404: + +```text +CodeArts-Pangu +deepseek-v4-flash-0731 +deepseek-v4-pro-0813 +glm-5.3-flash +``` + +生产上已通过 `channels/model/set_disabled` 禁用。它们仍可能在 `model/list` 中出现,但 `disabled=true`,不会作为可用模型路由。 + +新增的死模型判据: + +```lua +model is not registered +``` + +对应提交:`c9f3453`。 + +注意:当前插件的 dead-model 机制只在收到真实请求失败后记录模型;刷新目录不会自动根据“远端目录没有该模型”删除模型,这是刻意设计,避免误删远端目录不完整的平台模型。 + +CodeArts 当前仍有一些模型返回 429/TPM 限流,例如: + +```text +deepseek-v4-flash +deepseek-v4.1-flash +``` + +429 不是模型失效,不能禁用。 + +## 5. 其他 Channel 实测结果 + +### Trae + +真实流式 + tools 请求: + +```text +channels-trae:DeepSeek-V4-Flash +``` + +结果: + +- HTTP 200 +- SSE 正常返回 +- 结构化 tool call 返回 +- 但上游最后的结束原因可能是 `stop`,即使前面已经出现 tool-call fragments + +这会让 Agent 把工具调用误判成普通文本完成。 + +当前准备中的修复在统一 provider 流式层: + +```text +internal/provider/provider.go +``` + +逻辑是:只要本次流出现真实 tool-call 片段,后续 `FinishReason == "stop"` 就归一成 `tool_calls`。 + +另外,Trae 的非流式接口偶尔返回 SSE。当前 `trae.lua` 对非流式 SSE 只取最后一条 data,不能完整合并多条工具调用/推理片段。这是后续仍需处理的协议问题,但不能用非流式手工请求结果推断 Pi 的真实请求模式。 + +### Buddy + +真实流式 + tools 请求: + +```text +channels-buddy:glm-5.3-flash +``` + +结果: + +- HTTP 200 +- SSE 正常返回 +- tool-call 参数分片可见 +- 最终有 `finish_reason = tool_calls` +- 但某些上游 chunk 的 `tool_calls` 可能是空对象 `{}` + +空对象不是合法的 OpenAI tool-call 数组,部分 Agent 客户端会拒绝。 + +当前准备中的修复在: + +```text +internal/lua/adapters/buddy.lua +``` + +只在 `type(delta.tool_calls) == "table" and #delta.tool_calls > 0` 时输出 `tool_calls`,避免 Lua 空表被编码为 `{}`。 + +Buddy 的非流式错误: + +```text +11101: Non-stream chat request is currently not supported +``` + +之前出现的该错误来自手工 `curl` 测试中显式设置 `stream:false`,不能归因到 Pi。真实 `stream:true + tools` 已经证明 Buddy 流式工具调用可以工作。 + +### MiniMax + +生产日志反复出现: + +```text +adapter "anthropic": status=401 body={"error":"invalid access token"} +provider channels-minimax: api error 401 +``` + +这是凭据失效问题,不是 tools 形状问题。 + +当前源: + +```text +channels-minimax +adapter = anthropic +endpoint = /mavis/api/v1/llm/v1/messages +``` + +需要用户重新登录/提供有效凭据。不要擅自刷新一次性 refresh token,之前已经发生过 refresh token 被消耗后无法再次使用的事故。 + +## 6. 当前未提交的代码改动 + +最后一轮已经改动但尚未完成提交: + +```text +internal/provider/provider.go +internal/lua/adapters/trae.lua +internal/lua/adapters/buddy.lua +internal/provider/provider_test.go # 可能有半编辑、需先修复 +``` + +已完成的意图: + +1. provider 流式层记录 `sawToolCalls` +2. 看到真实 tool-call 后,把后续 `stop` 归一为 `tool_calls` +3. Trae 不输出空工具调用表 +4. Buddy 不输出空工具调用表 +5. provider 回归测试模拟“tool fragment + stop”场景 + +但 provider 测试最后一次编辑工具报错: + +```text +internal/provider/provider_test.go:99: missing ',' in argument list +``` + +因此必须先修测试文件,再提交。 + +## 7. 推荐接手顺序 + +### 第一步:修复工作树 + +```bash +cd /home/program/llmsproxy +git status --short +sed -n '1,150p' internal/provider/provider_test.go +gofmt -w internal/provider/provider_test.go 2>&1 || true +go test -tags luajit ./internal/provider/ 2>&1 +``` + +若测试函数字符串转义难以修复,先删除半成品测试,再重新用 raw string 写测试。 + +### 第二步:跑定向验证 + +```bash +luac -p internal/lua/adapters/trae.lua +luac -p internal/lua/adapters/buddy.lua +go test -tags luajit ./internal/provider/ +go test -tags luajit ./internal/lua/ -run 'Adapter|Buddy|Trae|Stream' +``` + +### 第三步:构建并部署临时验证版 + +修复确认后: + +```bash +go build ./... +./deploy.sh +``` + +部署前确认: + +```bash +sha256sum internal/lua/plugins/channels.lua +sha256sum /etc/llmsproxy/plugins/channels.lua +systemctl is-active llmsproxy +``` + +### 第四步:真实流式回归 + +不要用 `stream:false` 测试流式问题。使用: + +```bash +KEY='<从本机安全环境读取,不写入文档>' +TOOL='{"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}' + +curl --no-buffer --max-time 90 \ + -H "Authorization: Bearer $KEY" \ + -H 'Content-Type: application/json' \ + -d "{\"model\":\"channels-codearts:openpangu-2.0-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Use the weather tool for Beijing.\"}],\"tools\":[$TOOL],\"tool_choice\":\"auto\",\"stream\":true,\"max_tokens\":64}" \ + http://127.0.0.1:8081/v1/chat/completions +``` + +分别将 model 替换为: + +```text +channels-codearts:openpangu-2.0-flash +channels-trae:DeepSeek-V4-Flash +channels-buddy:glm-5.3-flash +``` + +验收: + +- HTTP 200 +- `Content-Type: text/event-stream` +- 不能出现 `tool_calls: {}` +- 有 tool-call fragments 时最终必须是 `finish_reason: tool_calls` +- CodeArts 当前应继续返回结构化工具调用 + +### 第五步:CodeArts 刷新验收 + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer $KEY" \ + -H 'Content-Type: application/json' \ + -d '{"platform":"codearts"}' \ + http://127.0.0.1:8081/api/plugins/http/channels/model/refresh +``` + +预期: + +```json +{"ok":true,"results":[{"platform":"codearts","ok":true,"count":11}]} +``` + +再检查: + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer $KEY" \ + -H 'Content-Type: application/json' \ + -d '{"platform":"codearts"}' \ + http://127.0.0.1:8081/api/plugins/http/channels/model/list +``` + +预期:远端目录 11 个 + 兜底补位,已确认 404 的 4 个仍保持 `disabled=true`。 + +## 8. 发布/安装包状态 + +目前没有完成“全部安装包”发布闭环。 + +已经发生的事情: + +- 核心包构建成功,但脚本实际使用了 `git describe`,产物版本类似: + `2.0.40~4-gc9f3453` +- Linux GUI 包构建成功,版本为 `1.9.7` +- Windows 主机 Wine 构建失败,生成了一个 264KB 的坏 `ModelRouter Setup 1.9.7.exe`,绝对不能发布 +- Docker Windows 构建也失败在拉取 Docker Hub 基础镜像 `golang:1.25` 网络超时 +- apt 无法找到 `wine32:i386` +- 旧的 1.9.5/1.9.6 Windows 包是历史产物,不能作为当前版本 + +不要把旧 Windows exe 当作新包发布。 + +仓库已有 Docker 方案: + +```text +cmd/gui/scripts/dist-win-docker.sh +cmd/gui/docker/win-builder/Dockerfile +``` + +但当前 Docker Hub 网络不可用。若要继续做 Windows 包: + +1. 在可访问 Docker Hub 的构建机/CI 执行 Docker 方案;或 +2. 提供包含 Wine32/Wine64 的完整构建环境;或 +3. 暂时只发布 Linux/core 包,并明确说明 Windows 包未完成。 + +版本脚本注意: + +- 核心包脚本使用位置参数: + + ```bash + ./packaging/core-dist.sh 2.0.41 + ``` + + 不是 `VERSION=2.0.41 ./packaging/core-dist.sh`。 +- GUI 版本来自 `cmd/gui/package.json`,环境变量 `VERSION` 不会自动修改 Electron 产品版本。 +- 清理旧产物后再打包,并在全部包完成后统一计算 `SHA256SUMS`。 + +## 9. 参考插件状态 + +用户要求按: + +```text +https://github.com/Ebony-Vinyl/dsh-our-free-model +``` + +逐厂商对照参考插件。 + +当前本地路径: + +```text +/tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model +``` + +但该目录当前不是有效可读源码副本: + +- `vendor/channel-pack/src` 下面只有空目录; +- `.git` 没有有效 `HEAD`/refs; +- 无法从本地 Git 对象恢复源码; +- 本地也没有 `buddy.ts`、`trae.ts`、`codearts.ts` 等文件。 + +因此不能声称当前适配器是逐行照参考插件移植的。若要继续按用户要求完成,必须先重新 clone/恢复该 GitHub 仓库源码,确认以下文件实际存在后再对照: + +```text +vendor/channel-pack/src/trae.ts +vendor/channel-pack/src/buddy.ts +vendor/channel-pack/src/codearts.ts +vendor/channel-pack/src/*adapter* +vendor/channel-pack/src/*model* +``` + +当前仓库代码只能作为临时协议修复基础,不能代替参考插件源码。 + +## 10. 关键结论 + +- Pi 是否发送流式请求,历史日志没有保存入站 `stream/tools` 字段,不能靠旧日志判断。 +- 本次真实 `stream:true + tools` 测试证明 ModelRouter 流式路径能工作: + - CodeArts:通过; + - Trae:通过但结束原因需归一; + - Buddy:通过但需过滤空 `{}`; + - MiniMax:凭据 401,独立问题。 +- 不要再用 `stream:false` 的手工请求推断 Pi 的问题。 +- CodeArts 刷新接口当前真实返回 `ok=true,count=11`,刷新链路不是当前阻塞点。 +- 当前最紧急的代码状态是修复 `provider_test.go` 的半编辑,然后验证未提交的统一流式修复。 +- 未经测试、未重新构建、未重新部署,不要继续改生产账号或刷新一次性凭据。