mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
P1-1 anthropic 基座补 build_headers(x-api-key + anthropic-version) 根因:基座只有静态 anthropic-version、没有 build_headers ⇒ 内核兜底发 Authorization: Bearer,而 Anthropic 协议要 x-api-key。 ★ 证据冲突(计划 P1-1 原文一半有误):zcode 的 /zcode-plan/anthropic/v1/messages 虽然**说 Anthropic 协议**,鉴权却是 Bearer <zcode_jwt>(参考 zcode-transport.ts:121-125;全仓 grep x-api-key = 0 次命中)。故 zcode 必须**显式覆盖** build_headers 回到 Bearer, 否则基座的正向修复会把它从"对"改成"错"。 内核改动:internal/provider/provider.go —— 兜底 Bearer 让位给 x-api-key, 避免同一请求带两套鉴权(部分 Anthropic 兼容上游对歧义鉴权直接 401)。 P1-2 channels-cline 的 build_headers 补 workos: 前缀 根因:参考 cline.ts 发 `Bearer workos:<jwt>`,而池里存的是第二步 register 换回的裸 token ⇒ 之前无 build_headers,内核兜底发裸 Bearer。 幂等:已带前缀(含大小写变体)不重复拼。 P1-3 channels-gemini 只确认不改 参考 gemini.ts:353-366 同样是 OAuth Bearer + 身份五头、刻意不发 x-goog-api-key ⇒ 我们现状本来就对。加判据把这个结论钉住,防下轮照 P1-1 的模子把它改坏。 P2 源名契约:仅 [A-Za-z0-9_-],禁 : 与 / 根因:: 与 / 是限定名分隔符,出现在源名里会让 `源名:模型名` 有两种都 自圆其说的切法,同一请求串在不同部署下可能路由到不同源,且**不报错**。 校验加在写入口 core.normalizeSource(硬拒,API 400);启动时只告警, 避免存量带 : 的源名让升级 brick 掉整个网关。 解析入口 splitPinned 不做校验(解析不该拒绝已有数据)。 P3 夹具形状判据(本轮最重要的一条) 根因:trae 的 Result 信封缺陷在扁平夹具下所有判据全绿,真实登录却报 「缺少 Token」—— 夹具形状错 ⇒ 判据必然虚假通过。 通用形状层:shapeFamilies 登记表,每族给 real/naive/fact/eval/assert, 断言 fact(real) 且 !fact(naive) 且 assert(eval(real))。 刻意**不比较 eval 输出形状**:trae 余额实现合法保留顶层兼容路径, 拿输出比对会把合法兼容判成错误;能区分真假的是 fact(额度在哪一层)。 另有仓库扫描:对特征明确的退化形状静态反查,需退化夹具时写 SHAPE-OK 豁免。 判据:11 个文件 / 30 用例。全部变异已验证会报红(9 处)。
108 lines
4.4 KiB
Go
108 lines
4.4 KiB
Go
package config
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ P2:源名允许字符集契约 —— 仅 `[A-Za-z0-9_-]`,**不得含 `:` 与 `/`**。
|
||
//
|
||
// 为何必须收窄(原因写在这里,因为 docs 会被改、判据不会):
|
||
//
|
||
// 模型寻址支持 `源名:模型名` / `源名/模型名` / `源名-模型名` 三种限定写法
|
||
// (WebUI 下拉框用 `源名:模型名` 作 option value:本实例 44 个模型被多个源
|
||
// 同时提供,必须能指定「从哪个源调」)。剥离源名的那一步是
|
||
// `internal/provider/splitPinned`:先做**最长源名前缀匹配**,再要求源名后
|
||
// 紧跟一个分隔符。
|
||
//
|
||
// ⇒ 若源名**自己含** `:` 或 `/`,那个字符同时是「源名的一部分」和「分隔符」,
|
||
// 限定名就成了歧义串:
|
||
//
|
||
// src="a:b" model="m" → "a:b:m" → ("a", "b:m") 还是 ("a:b", "m")?
|
||
// src="a/b" model="m" → "a/b/m" → ("a", "b/m") 还是 ("a/b", "m")?
|
||
//
|
||
// 两种切法都自圆其说,结果取决于**当前恰好有哪些源名** —— 同一客户端请求串
|
||
// 在不同部署/不同启动顺序下可能落到不同源,而且**不会报错**(splitPinned
|
||
// 会命中某一个分支然后路由到别处)。历史缺陷正是这一族:`IndexAny(model,
|
||
// "-:/")` 按第一个分隔符切,把 `channels-trae-xxx:model` 切成 src="channels"
|
||
// ⇒ 永不匹配 ⇒ `model ... is not configured`。
|
||
//
|
||
// 注意 `-` 是**允许**的:本仓渠道源就是 `channels-<平台>-<账号id>`,
|
||
// 分隔符在源名之后,不产生歧义。
|
||
func TestValidateSourceNameAllowsContractCharset(t *testing.T) {
|
||
allowed := []string{
|
||
"channels-trae-b8dd6b3df30e", // 真实渠道源形态(含两个 -)
|
||
"channels-buddy-buddy-main",
|
||
"DeepSeek",
|
||
"my_source_1",
|
||
"a",
|
||
"-leading-dash",
|
||
"trailing-dash-",
|
||
"123456",
|
||
}
|
||
for _, name := range allowed {
|
||
if err := ValidateSourceName(name); err != nil {
|
||
t.Errorf("★ 合法源名 %q 被拒: %v", name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 反例:`:` 与 `/` 必须**被点名拒绝**(而不是泛泛地说"非法字符")——
|
||
// 运维看到错误信息才知道这是限定名分隔符冲突,不是手滑。
|
||
func TestValidateSourceNameRejectsPinningSeparators(t *testing.T) {
|
||
cases := []string{
|
||
"a:b",
|
||
"vendor:model",
|
||
"a/b",
|
||
"openai/gpt",
|
||
"channels-trae-b8dd6b3df30e:glm-5.2", // 把"限定名"当源名写进来
|
||
"src/sub:name",
|
||
}
|
||
for _, name := range cases {
|
||
err := ValidateSourceName(name)
|
||
if err == nil {
|
||
t.Fatalf("★ 源名 %q 含限定名分隔符,必须被拒(否则 `源名:模型名` 歧义)", name)
|
||
}
|
||
msg := err.Error()
|
||
if !strings.Contains(msg, "pinning separator") {
|
||
t.Errorf("错误信息应点名「pinning separator」,实得: %v", err)
|
||
}
|
||
if !strings.Contains(msg, name) {
|
||
t.Errorf("错误信息应回显源名 %q,实得: %v", name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 其它非法字符同样拒(防止 `源 名`、`源名\n` 这种把日志/URL 搞乱的写法)。
|
||
func TestValidateSourceNameRejectsOtherIllegalChars(t *testing.T) {
|
||
for _, name := range []string{"has space", "tab\there", "new\nline", "中文名", "dot.name", "plus+name", "@at"} {
|
||
if err := ValidateSourceName(name); err == nil {
|
||
t.Errorf("★ 源名 %q 含非法字符,必须被拒(允许集只有 %s)", name, SourceNameAllowedPattern)
|
||
}
|
||
}
|
||
if err := ValidateSourceName(""); err == nil {
|
||
t.Error("空源名必须被拒")
|
||
}
|
||
}
|
||
|
||
// 存量兼容:启动时**只告警不报错**。
|
||
//
|
||
// 为什么不能直接报错:存量配置里可能已经有带 `:` 的源名,升级瞬间让网关
|
||
// 起不来(比"限定名歧义"更坏 —— 整个服务不可用)。契约从**新数据**开始强制
|
||
// (core.normalizeSource 硬拒),老数据给一条可读告警。
|
||
func TestSourceNameWarningIsSoftForLegacyConfig(t *testing.T) {
|
||
cfg := Config{Sources: []Source{
|
||
// 存量形态:带 `:` —— 必须能启动(不返回 error),但要被点名告警
|
||
{Name: "legacy:v1", BaseURL: "http://x", Models: []Model{{ID: "m1"}}},
|
||
}}
|
||
if err := cfg.ApplyDefaults(); err != nil {
|
||
t.Fatalf("★ 存量配置带 `:` 不得导致启动失败(会把整个网关 brick 掉): %v", err)
|
||
}
|
||
if w := SourceNameWarning("legacy:v1"); w == "" {
|
||
t.Fatal("★ 存量非法名必须给出告警文案(否则问题彻底静默)")
|
||
}
|
||
if w := SourceNameWarning("channels-trae-abc"); w != "" {
|
||
t.Fatalf("合法源名不该产生告警,实得 %q", w)
|
||
}
|
||
}
|