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 处)。
144 lines
6.0 KiB
Go
144 lines
6.0 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|