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 处)。
123 lines
4.5 KiB
Go
123 lines
4.5 KiB
Go
package lua
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ P1-1:anthropic 基座必须发 `x-api-key`,不是 `Authorization: Bearer`。
|
||
//
|
||
// 真实缺陷:anthropic.lua 只有一个静态 `anthropic-version`、**没有**
|
||
// build_headers ⇒ 内核走兜底 `Authorization: Bearer <api_key>` ⇒ Anthropic
|
||
// 协议一律回 401 `invalid x-api-key`。继承该基座的渠道源(channels-zcode)
|
||
// 一有账号就会立刻暴露,与 trae 修复前完全同型(静态头 + 内核兜底 Bearer,
|
||
// 缺的正是协议要的那一个头)。
|
||
//
|
||
// 判据咬的是**真实产出表**(vm.BuildHeaders 就是内核发请求前走的那条路径),
|
||
// 不造 HTTP 假上游 —— 假上游需要 URL 覆盖/凭据 ref/路径匹配全部对齐,任一环
|
||
// 错都会让「没发请求」伪装成「头不对」(plan §3 第 4 条踩过三轮)。
|
||
func TestAnthropicBuildHeadersSendsXAPIKey(t *testing.T) {
|
||
vm := NewVM(freshAdapterDir(t))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
if !vm.HasFn("anthropic", "build_headers") {
|
||
t.Fatal("★ anthropic 基座没有 build_headers —— 内核会兜底发 " +
|
||
"Authorization: Bearer,而 Anthropic 协议要 x-api-key ⇒ 404/401")
|
||
}
|
||
|
||
hdrs, err := vm.BuildHeaders("anthropic", map[string]interface{}{
|
||
"api_key": "sk-ant-KEY-123",
|
||
"url": "https://api.anthropic.com/v1/messages",
|
||
"method": "POST",
|
||
"body": `{"model":"claude"}`,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("BuildHeaders: %v", err)
|
||
}
|
||
|
||
// ① 键集合:必须**恰好**是这两个。多出来的 Authorization 会让上游面对
|
||
// 两套鉴权方案(部分 Anthropic 兼容上游对歧义鉴权直接拒)。
|
||
if len(hdrs) != 2 {
|
||
t.Fatalf("★ 头集合应恰好 2 个(x-api-key + anthropic-version),实得 %d: %v",
|
||
len(hdrs), keysOf(hdrs))
|
||
}
|
||
if got := hdrs["x-api-key"]; got != "sk-ant-KEY-123" {
|
||
t.Fatalf("★ x-api-key 必须取凭据原值,实得 %q(头: %v)", got, keysOf(hdrs))
|
||
}
|
||
if got := hdrs["anthropic-version"]; got != "2023-06-01" {
|
||
t.Fatalf("anthropic-version 应为 2023-06-01,实得 %q", got)
|
||
}
|
||
if _, has := hdrs["Authorization"]; has {
|
||
t.Fatalf("★ 不得同时发 Authorization(与 x-api-key 冲突,上游视为歧义): %v", hdrs)
|
||
}
|
||
// ② 值形状:不能带 Bearer 前缀(x-api-key 的值是裸 key)
|
||
if strings.Contains(hdrs["x-api-key"], "Bearer") {
|
||
t.Fatalf("★ x-api-key 的值是**裸 key**,不能带 Bearer 前缀,实得 %q", hdrs["x-api-key"])
|
||
}
|
||
// ③ key 绝不外泄到别的头里(防止某次改动把 key 顺手塞进 UA 之类)
|
||
for k, v := range hdrs {
|
||
if k != "x-api-key" && strings.Contains(v, "sk-ant-KEY-123") {
|
||
t.Fatalf("★ 凭据只能出现在 x-api-key,泄露到了 %s=%q", k, v)
|
||
}
|
||
}
|
||
t.Logf("✓ anthropic 头: %v", keysOf(hdrs))
|
||
}
|
||
|
||
// 配对判据:没有凭据时仍要发 anthropic-version(协议头),但**不得**发空
|
||
// x-api-key —— 空值头会让上游回 `invalid x-api-key` 而不是清晰的"未配置凭据"。
|
||
func TestAnthropicBuildHeadersWithoutCredential(t *testing.T) {
|
||
vm := NewVM(freshAdapterDir(t))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
for _, meta := range []map[string]interface{}{nil, {}, {"api_key": ""}} {
|
||
hdrs, err := vm.BuildHeaders("anthropic", meta)
|
||
if err != nil {
|
||
t.Fatalf("BuildHeaders(%v): %v", meta, err)
|
||
}
|
||
if hdrs["anthropic-version"] != "2023-06-01" {
|
||
t.Fatalf("mate=%v 时仍须有 anthropic-version,实得 %v", meta, hdrs)
|
||
}
|
||
if _, has := hdrs["x-api-key"]; has {
|
||
t.Fatalf("meta=%v 时不得发空 x-api-key,实得 %v", meta, hdrs)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 兜底路径:有些调用方只把 key 放在 source.meta.api_key(kimicode 同约定)。
|
||
// 基座必须与 kimicode 一致地认这第二种位置,否则换调用方就静默少头。
|
||
func TestAnthropicBuildHeadersReadsSourceMetaKey(t *testing.T) {
|
||
vm := NewVM(freshAdapterDir(t))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer vm.Stop()
|
||
|
||
hdrs, err := vm.BuildHeaders("anthropic", map[string]interface{}{
|
||
"source": map[string]interface{}{
|
||
"name": "claude-direct",
|
||
"meta": map[string]interface{}{"api_key": "KEY-FROM-META"},
|
||
},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("BuildHeaders: %v", err)
|
||
}
|
||
if hdrs["x-api-key"] != "KEY-FROM-META" {
|
||
t.Fatalf("★ 应读 source.meta.api_key 兜底,实得 %q(头: %v)",
|
||
hdrs["x-api-key"], keysOf(hdrs))
|
||
}
|
||
}
|
||
|
||
func keysOf(m map[string]string) []string {
|
||
out := make([]string, 0, len(m))
|
||
for k := range m {
|
||
out = append(out, k)
|
||
}
|
||
return out
|
||
}
|