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 处)。
319 lines
13 KiB
Go
319 lines
13 KiB
Go
package lua
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★★ P3:夹具形状防回归 —— 钉住三类**真实上游形状**。
|
||
//
|
||
// 本轮最大的教训不是某个 bug,而是「**夹具形状错 ⇒ 判据必然虚假通过**」:
|
||
// trae 的 ExchangeToken 缺陷(只读顶层、不认 `Result` 信封)在两个版本的
|
||
// 扁平夹具 `{"Token":...}` 下**全部判据都是绿的**,真实登录却报「缺少 Token」。
|
||
// 也就是说:判据写得再严,只要它喂进去的形状不是生产形状,它就毫无意义。
|
||
//
|
||
// 因此这里不只钉行为,还要钉**形状**本身,且钉在**真实的解析器/转换器**上
|
||
// (经插件探针调用同一份实现),而不是在 Go 侧重写一遍解包逻辑 ——
|
||
// 自算版会在真实函数被改坏时继续绿(plan §3 第 4 条踩过三轮)。
|
||
//
|
||
// 三类形状(依据 docs/channel-repair-plan.md §1.4 ④ 与 §3 第 1 条):
|
||
// ① Result 信封 —— trae ExchangeToken:凭据在 `Result` 里,顶层没有
|
||
// ② SOLO 自定义 SSE —— trae 流式:`{"response":...}` 扁平帧,无 choices
|
||
// ③ 嵌套 quota —— trae 余额:`pack.entitlement_base_info.quota.credits_limit`
|
||
//
|
||
// 任一形状被"改回方便测试的样子"(扁平/OpenAI 外壳/顶层字段),本判据立刻报红。
|
||
|
||
// TestFixtureShapeResultEnvelope ①:Result 信封必须被真实解析器认出来。
|
||
//
|
||
// 真实上游(2026-10-17 实测,HTTP 200):
|
||
//
|
||
// {"ResponseMetadata":{...},"Result":{"Token":"eyJ...","RefreshToken":"..."}}
|
||
//
|
||
// 顶层**只有** ResponseMetadata + Result、没有 Token。若夹具写成扁平
|
||
// `{"Token":...}`,实现里「只读顶层」的错误会被完美掩盖 —— 这正是它藏了两轮的原因。
|
||
func TestFixtureShapeResultEnvelope(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", string(src)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 真实形状:凭据只在 Result 里
|
||
real := `{"ResponseMetadata":{"RequestId":"req-1","Action":"ExchangeToken"},` +
|
||
`"Result":{"ClientID":"cid","Token":"ACCESS-FROM-RESULT",` +
|
||
`"RefreshToken":"RT-ROTATED","TokenExpireAt":1800000000000,` +
|
||
`"TokenExpireDuration":1209600000,"RefreshExpireAt":1807755929432}}`
|
||
out, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{
|
||
"body": real,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j := probeJSON(t, out)
|
||
if j["error"] != nil {
|
||
t.Fatalf("★ 真实 Result 信封解析失败: %v —— 检查是否只读顶层", j["error"])
|
||
}
|
||
if got, _ := j["access"].(string); got != "ACCESS-FROM-RESULT" {
|
||
t.Fatalf("★ access 必须来自 Result.Token,实得 %q", got)
|
||
}
|
||
if got, _ := j["refresh"].(string); got != "RT-ROTATED" {
|
||
t.Fatalf("★ 轮换后的 refresh 必须来自 Result.RefreshToken,实得 %q", got)
|
||
}
|
||
|
||
// 反例:把这个形状的 Result 剥掉(= 旧的扁平夹具),解析器必须**报错**而不是
|
||
// 悄悄返回空 —— 否则"缺 Token"永远被误报成成功。
|
||
flat := `{"ResponseMetadata":{"RequestId":"req-1"},"Token":"SHOULD-NOT-RESOLVE"}`
|
||
out2, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{
|
||
"body": flat,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j2 := probeJSON(t, out2)
|
||
// 注意:顶层 Token 是**兼容兜底**(上游可能改版),所以这里允许成功;
|
||
// 但要钉住「Result 缺失时不会崩、也不会把 ResponseMetadata 当 Result」。
|
||
if j2["error"] == nil {
|
||
if got, _ := j2["access"].(string); got != "SHOULD-NOT-RESOLVE" {
|
||
t.Fatalf("顶层兜底路径应取顶层 Token,实得 %q", got)
|
||
}
|
||
}
|
||
|
||
// 真正必须报错的形状:既无 Result.Token 也无顶层 Token
|
||
bad := `{"ResponseMetadata":{"RequestId":"req-1"},"Result":{"ClientID":"cid"}}`
|
||
out3, err := ps.Call("channels", "__trae_exchange_parse_probe", map[string]interface{}{
|
||
"body": bad,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j3 := probeJSON(t, out3)
|
||
if j3["error"] == nil {
|
||
t.Fatalf("★ 无 Token 的响应必须报错(否则「缺少 Token」会被误判成成功): %v", j3)
|
||
}
|
||
t.Logf("✓ Result 信封: access=%v refresh=%v;缺 Token 报错=%v",
|
||
j["access"], j["refresh"], j3["error"])
|
||
}
|
||
|
||
// TestFixtureShapeSoloSSE ②:trae 的 SOLO 自定义 SSE 必须被摊平成扁平 UnifiedChunk。
|
||
//
|
||
// 真实上游帧(参考 trae.ts SOLO):
|
||
//
|
||
// event: output
|
||
// data: {"response":"###","reasoning_content":null,"tool_calls":null}
|
||
//
|
||
// 内核只把 `data:` 行交给适配器(event 行在 provider.go 丢弃),且
|
||
// `json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构** ——
|
||
// 返回 OpenAI 外壳 `{choices:[{delta}]}` 会被整体丢弃(91 帧全空)。
|
||
func TestFixtureShapeSoloSSE(t *testing.T) {
|
||
vm, ps, _ := runtimePlugins(t)
|
||
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", string(src)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 真实正文帧(注意:`reasoning_content` / `tool_calls` 显式 null,不是缺失)
|
||
frames := []struct {
|
||
name string
|
||
frame string
|
||
wantKey string
|
||
wantText string
|
||
}{
|
||
{
|
||
name: "正文帧",
|
||
frame: `{"response":"### 分析","reasoning_content":null,"tool_calls":null}`,
|
||
wantKey: "content",
|
||
wantText: "### 分析",
|
||
},
|
||
{
|
||
name: "推理帧",
|
||
frame: `{"response":"","reasoning_content":"We need","tool_calls":null}`,
|
||
wantKey: "reasoning_content",
|
||
wantText: "We need",
|
||
},
|
||
}
|
||
for _, f := range frames {
|
||
out, err := vm.Transform("channels-trae", "transform_stream_chunk", f.frame)
|
||
if err != nil {
|
||
t.Fatalf("%s: transform 失败: %v", f.name, err)
|
||
}
|
||
var ck map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &ck); err != nil {
|
||
t.Fatalf("%s: 产出不是 JSON: %q", f.name, out)
|
||
}
|
||
// ★ 形状一:必须是**扁平**(顶层 content),不能是 OpenAI 外壳
|
||
if _, has := ck["choices"]; has {
|
||
t.Fatalf("★ %s 不得返回 OpenAI 外壳(choices)—— 内核按 UnifiedChunk 解析,"+
|
||
"外壳字段会被全部丢弃: %s", f.name, out)
|
||
}
|
||
got, _ := ck[f.wantKey].(string)
|
||
if got != f.wantText {
|
||
t.Fatalf("★ %s 应摊平到顶层 %s=%q,实得 %q(原始产出 %s)",
|
||
f.name, f.wantKey, f.wantText, got, out)
|
||
}
|
||
}
|
||
t.Logf("✓ SOLO SSE 扁平形状正确(content / reasoning_content 均在顶层)")
|
||
}
|
||
|
||
// TestFixtureShapeNestedQuota ③:trae 余额在**两层嵌套**里,不在 pack 顶层。
|
||
//
|
||
// 真实上游(2026-10-17 实测):
|
||
//
|
||
// pack[0] 免费 → entitlement_base_info.quota 无 credits_limit
|
||
// pack[1] 每月登录赠送 → entitlement_base_info.quota.credits_limit = 500
|
||
// pack[2] 签到奖励 → entitlement_base_info.quota.credits_limit = 100
|
||
// usage_summary: {total_amount:600, consumed_amount:0}
|
||
//
|
||
// 旧实现读 `pack.credits_limit`(**根本不存在的字段**)⇒ 余额恒 0,
|
||
// 而账号里其实有 600 —— 用户看到「额度显示异常」。
|
||
func TestFixtureShapeNestedQuota(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", string(src)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 真实形状:额度在 entitlement_base_info.quota 里(含一个无额度的免费包)
|
||
real := `{"user_entitlement_pack_list":[` +
|
||
`{"pack_type":"free","entitlement_base_info":{"quota":{"enable_free":true}}},` +
|
||
`{"pack_type":"monthly","entitlement_base_info":{"quota":{"credits_limit":500}},` +
|
||
` "usage":{}},` +
|
||
`{"pack_type":"checkin","entitlement_base_info":{"quota":{"credits_limit":100}},` +
|
||
` "usage":{"credits_amount":0}}` +
|
||
`],"usage_summary":{"total_amount":600,"consumed_amount":0}}`
|
||
out, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{
|
||
"platform": "trae",
|
||
"body": real,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j := probeJSON(t, out)
|
||
if j["error"] != nil {
|
||
t.Fatalf("★ 嵌套 quota 解析失败: %v", j["error"])
|
||
}
|
||
if got, _ := j["total"].(float64); got != 600 {
|
||
t.Fatalf("★ 余额应为 500+100=600(嵌套 entitlement_base_info.quota.credits_limit),"+
|
||
"实得 %v —— 若为 0,检查是否又去读了 pack 顶层的 credits_limit(该字段不存在)",
|
||
j["total"])
|
||
}
|
||
|
||
// 反例:把额度放到**pack 顶层**(= 旧实现假设的形状)。真实上游不这么发,
|
||
// 但实现保留了兼容路径 —— 这里钉住的是「嵌套路径是权威」,同时确认
|
||
// 顶层兼容不会把数字算错(不会重复累加)。
|
||
//
|
||
// SHAPE-OK: 这段**刻意**用退化形状(额度挂 pack 顶层),因为它要测的正是
|
||
// 兼容兵底路径。生产形状由上面的嵌套夹具铉住;本文件里“生产/退化”两种
|
||
// 形状成对出现,才是“实现能区分两者”的证据。
|
||
// (若删掉这句,TestNoDegenerateFixturesInRepo 会报红 —— 这是故意的:
|
||
// 它强迫每一个退化夹具显式声明自己是刻意的。)
|
||
topLevel := `{"user_entitlement_pack_list":[{"credits_limit":50}]}`
|
||
out2, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{
|
||
"platform": "trae",
|
||
"body": topLevel,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j2 := probeJSON(t, out2)
|
||
if j2["error"] != nil {
|
||
t.Fatalf("顶层兼容路径不该报错: %v", j2["error"])
|
||
}
|
||
if got, _ := j2["total"].(float64); got != 50 {
|
||
t.Fatalf("顶层兼容路径应算 50,实得 %v", j2["total"])
|
||
}
|
||
|
||
// 已用额度必须扣减(usage.credits_amount 也是嵌套的)
|
||
consumed := `{"user_entitlement_pack_list":[` +
|
||
`{"entitlement_base_info":{"quota":{"credits_limit":500}},` +
|
||
` "usage":{"credits_amount":120}}]}`
|
||
out3, err := ps.Call("channels", "__balance_total_probe", map[string]interface{}{
|
||
"platform": "trae",
|
||
"body": consumed,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("探针调用失败: %v", err)
|
||
}
|
||
j3 := probeJSON(t, out3)
|
||
if got, _ := j3["total"].(float64); got != 380 {
|
||
t.Fatalf("★ 余额应为 500-120=380(usage.credits_amount 是嵌套的),实得 %v", j3["total"])
|
||
}
|
||
t.Logf("✓ 嵌套 quota: 600 / 扣减后 380 / 顶层兼容 50")
|
||
}
|
||
|
||
// probeJSON 取探针返回的 {json:{...}} 那层。
|
||
func probeJSON(t *testing.T, got interface{}) map[string]interface{} {
|
||
t.Helper()
|
||
m, ok := got.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("探针返回值不是对象: %#v", got)
|
||
}
|
||
j, ok := m["json"].(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("探针返回值缺 json 层: %#v", m)
|
||
}
|
||
return j
|
||
}
|
||
|
||
// TestFixtureShapeGuardCoversAllThree 元判据:上三条**真的存在且真的咬到了
|
||
// 被测对象**。这是防「有人删掉形状判据」或「把判据改成不调真实解析器」的兜底。
|
||
//
|
||
// 它读自己的源码文本(同目录下),断言:
|
||
// ① 三个形状判据函数都在;
|
||
// ② 它们**都调用了**插件探针(而不是自己另写一份解包逻辑 ——
|
||
// 那样真实函数被改坏时判据会继续绿,这正是 trae 藏了两轮的原因)。
|
||
func TestFixtureShapeGuardCoversAllThree(t *testing.T) {
|
||
self, err := os.ReadFile("channels_fixture_shapes_test.go")
|
||
if err != nil {
|
||
t.Fatalf("读不到自身源码(元判据靠它验证三条形状判据是否还在): %v", err)
|
||
}
|
||
s := string(self)
|
||
|
||
// ① 三个形状判据必须在
|
||
for _, fn := range []string{
|
||
"TestFixtureShapeResultEnvelope",
|
||
"TestFixtureShapeSoloSSE",
|
||
"TestFixtureShapeNestedQuota",
|
||
} {
|
||
if !strings.Contains(s, "func "+fn+"(") {
|
||
t.Fatalf("★ 形状判据 %s 不见了 —— 三类真实形状必须持续被钉住", fn)
|
||
}
|
||
}
|
||
|
||
// ② 三条判据必须真的调用探针/真实适配器(不能退化成读源码字符串断言)
|
||
// Result 信封 / 嵌套 quota → 探针(同一个真实解析器)
|
||
// SOLO SSE → 已装载的 channels-trae 适配器
|
||
for _, anchor := range []string{
|
||
`__trae_exchange_parse_probe`, // ① Result 信封
|
||
`__balance_total_probe`, // ③ 嵌套 quota
|
||
`vm.Transform("channels-trae", "transform_stream_chunk"`, // ② SOLO SSE
|
||
} {
|
||
if !strings.Contains(s, anchor) {
|
||
t.Fatalf("★ 形状判据不再直接咬真实实现(缺 %s)—— "+
|
||
"自算版会在真实函数被改坏时继续绿", anchor)
|
||
}
|
||
}
|
||
|
||
// ③ 探针必须真的在插件里导出(否则上面两条会在运行期变成"探针不存在")
|
||
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, probe := range []string{"__trae_exchange_parse_probe", "__balance_total_probe"} {
|
||
if !strings.Contains(string(src), probe) {
|
||
t.Fatalf("★ 插件缺探针 %s —— 形状判据将无从咬真实解析器", probe)
|
||
}
|
||
}
|
||
t.Logf("✓ 三类形状判据齐全,且均直接咬真实实现")
|
||
}
|