mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 1. ★ zcode 身份块适配器(channels-zcode)—— 准入的唯一开关
参考实测(三账号 17 次请求):zcode 的 /zcode-plan/anthropic 通道做**请求体
内容检查**,system 缺官方身份块结构时直接 HTTP 405 + {"code":3012}:
无 system → 3012 ✗
仅 cliPrefix(42 字符) → 3012 ✗
cliPrefix + stable(约 2898 字符)→ 200 ✓
⇒ 判据是「身份块结构是否存在」,与 HTTP 头、版本号、频率、多轮历史、tools
声明、运行时全部无关。我们此前用通用 anthropic 适配器 ⇒ **即使拿到 zcode
账号也必然被拒**(现在 0 账号,但这是能不能用起来的前提)。
实现:插件自装 channels-zcode —— 读内核 anthropic.lua 执行取基座,**只覆盖
transform_request** 注入身份块(协议转换始终与 anthropic 那一份一致,不漂移):
block[1] cliPrefix(42) / block[2] stable(2852) / block[3] Environment 段
/ block[4] 调用方 system(**必须追加在最后**:上游看的是前缀结构)
只给最后一块打 cache_control(Anthropic 上限 4 断点,逐块打会占满预算)
日期块插入首条 user 的 content **数组**最前(拼进字符串会改变结构)
文本与参考**逐字节一致**(已核对:42 + 2856 = 2898)。
★ 基座读不到时**响亮失败**,不静默退化 —— 否则会产出"身份块看着对、工具/图像
全没转换"的请求,上游可能 200 但模型拿不到工具定义,症状是"模型变笨了",
与准入毫不相干、排查完全跑偏。
判据 4 条 + 变异 2/2:不注入 → 报红;截断到 100 字符 → 报红。
另加"协议转换不退化"判据(工具 input_schema / 图像块存活)。
## 2. ★★ 同一缺陷家族第 3 个实例:Close() 不停调度器
修完 on_unload 后全量回归 internal/lua **600s 超时**,goroutine 快照显示
**69 个卡在 stateSaver.loop 与 PluginRuntime.loop**。
根因:Runtime.Stop() **只在单插件 Unload 里调**,Close() 不调 ⇒ 每个插件的
调度循环在"进程关闭/测试结束"路径上永不停止。与 on_unload 完全同一形状。
修法:Close() 新增 stopSchedulersAll(),顺序 = 停调度 → 落盘 → on_unload
(调度器必须停在落盘之前,否则它写一个已收尾的 state)。
★ 顺带:Stop() 不幂等(裸 close 会 panic: close of closed channel),
两处都调之后可达 —— 用 sync.Once 加固。给函数加第二个调用点前先查幂等性。
实测 internal/lua 从 600s 超时 → 62s 通过。
## 3. codearts 模型目录:补两个**必须 AK/SK 签名**的端点
codearts 有 10 个账号但目录里只有硬编码 1 个模型。参考打两个端点:
① opengw.../gateway/config → result.models(benefit)
② snap-access.../v1/model/builtin → builtinModels(常规)
而 refreshRemoteModels **只支持 Bearer** ⇒ 这两端点从来没被打过。
已加 ep.sign="codearts" 分支 + 两个解析形态 + opengw host 白名单。
判据:两端点各自真实命中 + 每请求带 SDK-HMAC-SHA256 头 + **合并**(非取其一)。
## 4. TRAE PKCE 候选来源补齐(3 → 5)
参考 authCodeCandidates 有 5 个来源,我们只有 3(漏了 authCodeInfo 为 JSON 的
authCode 字段、以及**纯字符串**形态)。★ 且不能写成 `a or b or c` 链 ——
jsonField 取不到时返回**空串**,Lua 里空串是真值,链会在第一个空串处短路。
判据 5 个参数化用例 + 变异 2/2 咬住(正是 or 链取不到的两个形态)。
## 5. ★ 部署脚本:备份裁剪只修了一半("两处只修一处"再现)
prune_adapter_backups 有完整实现(含数量硬上限),但**只针对 adapters.bak**;
sync_plugins 同样每次建 plugins.bak.* —— 修了 adapters 的堆积、plugins 继续涨。
实测线上 plugins.bak **36 个**、/etc/llmsproxy 192M。
泛化为 prune_backups <prefix> + prune_all_backups(两个前缀都裁),加空前缀守卫。
判据不止逐字检查,而是**扫描脚本里所有 .bak.$(date) 创建点**,要求每个前缀都在
裁剪范围内 —— 将来新增第三种备份目录会立刻报红,而不是等它涨到几百兆。
变异 2/2 咬住(只裁 adapters / 去掉守卫)。
## 6. 上游客观缺口钉成可执行判据(TestUpstreamGapsAreDocumented)
qoder 加密端点需 .wasm(**参考仓库里也不存在**)、TRAE PKCE 参考**明确不支持**、
zcode 滑块需真实浏览器交互。写成判据:任何人(包括未来的我)想"补"这些能力,
必须先让判据失败 —— 即先推翻事实再动手。防的正是我前两轮的反复误判。
清理:channels-buddy.lua(0 字节历史遗留,每次启动报 must return a table)。
64 lines
2.8 KiB
Go
64 lines
2.8 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ 本文件把「上游客观缺口」写成**可执行的判据**,而不是只留在文档里。
|
||
//
|
||
// 为什么需要:这些能力缺口多次被误判成"我们漏实现了"(我自己就误判了两轮),
|
||
// 于是反复派人去"补"。判据在此把事实钉住:
|
||
//
|
||
// - qoder 加密端点(agent_chat_generation)需要 `qoder-auth-wasm.wasm`
|
||
// —— 该二进制**在参考仓库里也不存在**(参考自己也是 readFileSync 一个
|
||
// 它不提供的文件)。故我们走明文端点 `/model/v1/chat/completions`,
|
||
// **不经过信封** ⇒ qoder-envelope 的解包能力在明文路径上无从生效。
|
||
// - TRAE 的 PKCE 分支参考**明确不支持**(只有"探测并拒绝",无 code→token
|
||
// 交换端点)⇒ 我们报"暂不支持"是与参考对齐的正确行为。
|
||
//
|
||
// 判据的作用是**防止误判**:任何人(包括未来的我)想"补"这些能力时,
|
||
// 必须先让这个测试失败 —— 也就是先推翻事实,再动手。
|
||
func TestUpstreamGapsAreDocumented(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
t.Run("qoder 走明文端点且不经过信封", func(t *testing.T) {
|
||
// 明文端点(OpenAI 兼容):参考 QODER_CHAT_PATH
|
||
if !strings.Contains(s, "https://api2-v2.qoder.sh/model/v1/chat/completions") {
|
||
t.Fatal("qoder 的 chat 端点变了 —— 若改成加密端点 agent_chat_generation," +
|
||
"则**必须**同时实现信封解包(qoder-envelope)与 WASM 签名," +
|
||
"否则响应会被当成畸形数据")
|
||
}
|
||
// 明文端点没有信封层,故解包**不该**存在(存在反而是死代码)
|
||
if strings.Contains(s, "unwrapQoderEnvelope") {
|
||
t.Fatal("出现了 qoder 信封解包 —— 明文端点没有信封层。" +
|
||
"要么端点已换成加密端点(那需要 wasm),要么这是死代码")
|
||
}
|
||
})
|
||
|
||
t.Run("TRAE PKCE 明确报不支持(与参考一致)", func(t *testing.T) {
|
||
if !strings.Contains(s, "本实现暂不支持该流程") {
|
||
t.Fatal("TRAE 的 PKCE 分支必须明确报出「暂不支持」—— " +
|
||
"参考 trae-oauth.ts 也只做探测+拒绝,没有 code→token 交换端点。" +
|
||
"若改为尝试交换,会拿 code 去打要 RefreshToken 的 ExchangeToken(必然 400)")
|
||
}
|
||
})
|
||
|
||
t.Run("codearts 目录打两个签名端点(本轮补上)", func(t *testing.T) {
|
||
for _, want := range []string{
|
||
"opengw.developer.huaweicloud.com/api/v1/gateway/config",
|
||
"snap-access.cn-north-4.myhuaweicloud.com/v1/model/builtin",
|
||
`sign = "codearts"`,
|
||
} {
|
||
if !strings.Contains(s, want) {
|
||
t.Fatalf("codearts 目录缺 %q —— 参考 fetchCodeArtsRemoteModels 打两个"+
|
||
"**必须 AK/SK 签名**的端点,只打一个会少一半模型", want)
|
||
}
|
||
}
|
||
})
|
||
}
|