Files
ModelRouter/internal/lua/adapter_zcode_build_headers_test.go
JianFeeeee 4174310421 fix(channels): 渠道鉴权头三修 + 源名契约 + 夹具形状判据
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 处)。
2026-10-18 00:38:49 +08:00

144 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lua
import (
"encoding/json"
"strings"
"testing"
)
// ★★ zcode 的 Messages 端点鉴权是 **Bearer**,不是 x-api-key。
//
// 背景(本轮**最重要的一处证据冲突**,必须写清):
//
// 任务前提是「anthropic 协议 ⇒ x-api-key,所以 channels-zcode 继承基座就 401」。
// 但逆向参考(vendor/channel-pack,全部结论来自真实流量)明确写着相反的事:
//
// src/zcode-transport.ts:121-125 buildChannelRequest()
// headers = buildZcodeHeaders(credential, { json: true })
// headers.Authorization = `Bearer ${token}` // token = zcode_jwt
// src/zcode-upstream.ts:83-108 buildZcodeHeaders()
// 只发 User-Agent / HTTP-Referer / X-Device-Mid / anthropic-version …
// Authorization 是**可选注入项**,且全仓 `grep -rn "x-api-key" src/` = **0 次**
//
// 也就是说 zcode 的这条路径是「**Anthropic 请求体形状 + Bearer 鉴权**」——
// 协议与鉴权是两个独立维度,被同一家上游拆开了。
//
// 为什么修复前它其实是**对的**:zcode 未声明 credential_shape ⇒ api_key = 凭据里的
// access_token = zcode_jwt;而基座当时没有 build_headers ⇒ 内核兜底发了
// `Authorization: Bearer <zcode_jwt>`,与参考逐字一致。
//
// ⇒ 真正的风险来自**本轮的正向修复**:给 anthropic 基座补 x-api-key 之后,
// 若 zcode 只是「继承」而不覆盖,它就会从 Bearer 变成 x-api-key ⇒ 立刻 401。
// 所以 zcode 必须**显式覆盖** build_headers,把 Bearer 钉住。
//
// 判据咬真实产出(vm.BuildHeaders 是内核发请求前的那条路径),不造 HTTP 假上游。
func TestZcodeBuildHeadersUsesBearerNotXAPIKey(t *testing.T) {
vm, _ := zcodeHarness(t)
if !vm.HasFn("channels-zcode", "build_headers") {
t.Fatal("★ channels-zcode 必须有**自己的** build_headers(覆盖基座)," +
"否则会继承基座的 x-api-key 而丢掉参考要求的 Bearer")
}
hdrs, err := vm.BuildHeaders("channels-zcode", map[string]interface{}{
"api_key": "ZCODE-JWT-ABC",
"url": "https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages",
"method": "POST",
"body": `{"model":"GLM-5.3"}`,
})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
// ① 鉴权必须是 Bearer <zcode_jwt>(参考 zcode-transport.ts:125)
if got := hdrs["Authorization"]; got != "Bearer ZCODE-JWT-ABC" {
t.Fatalf("★ zcode 的 Authorization 必须是 `Bearer <zcode_jwt>`,实得 %q(头: %v)\n"+
" 参考 zcode-transport.ts:125 逐字如此;全仓没有一处发 x-api-key",
got, keysOf(hdrs))
}
// ② 不得同时发 x-api-key(那就是双鉴权;且参考从不发它)
if v, has := hdrs["x-api-key"]; has {
t.Fatalf("★ zcode 不得发 x-api-key(实得 %q)—— 参考全程没有这个头,"+
"发了就是两套鉴权方案", v)
}
// ③ 协议头 anthropic-version 必须保留(它与鉴权无关,参考也发)
if got := hdrs["anthropic-version"]; got != "2023-06-01" {
t.Fatalf("anthropic-version 应保留为 2023-06-01,实得 %q", got)
}
// ④ 键集合恰好 2 个,不多不少(防止顺手塞进别的身份头)
if len(hdrs) != 2 {
t.Fatalf("头集合应恰好 2 个(Authorization + anthropic-version),实得 %d: %v",
len(hdrs), keysOf(hdrs))
}
t.Logf("✓ zcode 头: %v", keysOf(hdrs))
}
// 配对判据:**基座**必须发 x-api-key(这是 P1-1 的正向要求,也是与 zcode 的差异点)。
//
// 两条判据放在一起才有意义:它们证明「同一份基座 + 显式覆盖」能同时满足
// 两种鉴权,而不是把整个 anthropic 生态压成一种。
func TestAnthropicBaseAndZcodeDifferOnAuthHeader(t *testing.T) {
vm, _ := zcodeHarness(t)
base, err := vm.BuildHeaders("anthropic", map[string]interface{}{"api_key": "SK-ANT"})
if err != nil {
t.Fatalf("anthropic BuildHeaders: %v", err)
}
zc, err := vm.BuildHeaders("channels-zcode", map[string]interface{}{"api_key": "JWT-Z"})
if err != nil {
t.Fatalf("zcode BuildHeaders: %v", err)
}
// 基座:x-api-key,无 Authorization
if base["x-api-key"] != "SK-ANT" {
t.Fatalf("★ anthropic 基座必须发 x-api-key,实得 %v", base)
}
if _, has := base["Authorization"]; has {
t.Fatalf("★ anthropic 基座不得发 Authorization(x-api-key 才是它的鉴权),实得 %v", base)
}
// zcode:Authorization,无 x-api-key
if zc["Authorization"] == "" {
t.Fatalf("★ zcode 必须发 Authorization: Bearer,实得 %v", zc)
}
if _, has := zc["x-api-key"]; has {
t.Fatalf("★ zcode 不得发 x-api-key,实得 %v", zc)
}
// 两者必须**真的不同**——若某天有人删掉 zcode 的覆盖,这条会立刻红
if base["x-api-key"] == "" || zc["Authorization"] == "" {
t.Fatal("两者鉴权头方案必须各自成立")
}
b, _ := json.Marshal(base)
z, _ := json.Marshal(zc)
if string(b) == string(z) {
t.Fatalf("★ 基座与 zcode 的鉴权方案不能相同:%s", b)
}
// 协议头必须相同(anthropic-version 与鉴权无关)
if base["anthropic-version"] != zc["anthropic-version"] {
t.Fatalf("两者都必须发 anthropic-version:base=%v zcode=%v", base, zc)
}
if strings.Contains(string(b), "Bearer") {
t.Fatalf("基座不该出现 Bearer:%s", b)
}
t.Logf("✓ 基座=%s | zcode=%s", b, z)
}
// 无凭据时:两边都只发协议头,都不发空鉴权(空值会让上游回认证失败,
// 而真因是「没配置凭据」,会把人带偏)。
func TestAuthHeadersOmitEmptyCredential(t *testing.T) {
vm, _ := zcodeHarness(t)
for _, name := range []string{"anthropic", "channels-zcode"} {
h, err := vm.BuildHeaders(name, map[string]interface{}{})
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if h["anthropic-version"] != "2023-06-01" {
t.Errorf("%s 仍须发 anthropic-version,实得 %v", name, h)
}
if _, has := h["x-api-key"]; has {
t.Errorf("%s 无凭据时不得发空 x-api-key,实得 %v", name, h)
}
if _, has := h["Authorization"]; has {
t.Errorf("%s 无凭据时不得发空 Authorization,实得 %v", name, h)
}
}
}