Files
ModelRouter/internal/lua/channels_gemini_auth_check_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

92 lines
4.1 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 (
"testing"
)
// ★ P1-3(**只确认不改**):channels-gemini 的「静态身份五头 + 内核兜底 Bearer」
// 本来就是**对的**,不需要像 zcode 那样补 x-api-key。
//
// 这是本轮唯一一个"看起来同型、实际不是缺陷"的渠道,所以把它钉成判据,
// 免得下一轮有人照 P1-1 的模子给它也改一遍(那会把它弄坏)。
//
// 证据链(两条独立来源一致):
//
// ① 参考实现 vendor/channel-pack/src/gemini.ts:353-366 geminiHeaders()
// headers.set('Authorization', `Bearer ${credential.access_token}`)
// 再逐字注入 GEMINI_IDENTITY_HEADERS 五头
// 并**刻意不注入** x-goog-api-key(源注释:原版不带)
// ② 参考 src/gemini.ts:110-117 的 GEMINI_IDENTITY_HEADERS 与我们
// adapter.headers 的五头逐字相同(UA / x-client-name / x-client-version /
// x-machine-id / x-vscode-sessionid)
//
// 也就是说:gemini 走的是 **OAuth Bearer**(Cloud Code 凭据),不是
// Anthropic 那种 x-api-key,也不是 Cline 那种 workos: 前缀。它的鉴权
// 由内核兜底 `Authorization: Bearer <api_key>` 提供 —— 而 gemini 未声明
// credential_shape ⇒ api_key = 凭据里的 access_token ⇒ 兜底值正确。
//
// ⇒ **结论:不改。** 本判据把这个结论钉住:一旦有人给 channels-gemini 加上
//
// build_headers 发别的鉴权头(例如照 zcode 改成 x-api-key),这里会红。
func TestChannelsGeminiAuthIsBearerByDesign(t *testing.T) {
vm, _ := zcodeHarness(t)
// ① 适配器不该自己发鉴权头:静态头里**不得**出现 Authorization /
// x-api-key / x-goog-api-key。鉴权交给内核兜底 Bearer。
hdrs, err := vm.BuildHeaders("channels-gemini", map[string]interface{}{
"api_key": "GA-OAUTH-TOKEN",
"url": "https://cloudcode-pa.googleapis.com/v1internal:generateContent",
"method": "POST",
"body": `{}`,
})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
for _, forbidden := range []string{"Authorization", "x-api-key", "x-goog-api-key"} {
if v, has := hdrs[forbidden]; has {
t.Fatalf("★ channels-gemini 不该自己发 %s(实得 %q)—— "+
"Cloud Code 走 OAuth Bearer,由内核兜底发;"+
"自己发会让它与兜底冲突或改变鉴权方案(P1-3 结论是「不改」)",
forbidden, v)
}
}
// ② 身份五头必须还在(它们是准入条件,与鉴权无关)
for k, want := range map[string]string{
"User-Agent": "antigravity/4.3.0 (cmdc-pak)",
"x-client-name": "antigravity",
"x-client-version": "4.3.0",
"x-machine-id": "cmdc-pak",
"x-vscode-sessionid": "proxy",
} {
if got := hdrs[k]; got != want {
t.Fatalf("身份头 %s 应为 %q,实得 %q(参考 GEMINI_IDENTITY_HEADERS 逐字)", k, want, got)
}
}
t.Logf("✓ channels-gemini 只发身份五头、不自带鉴权(兜底 Bearer 即正确): %v", keysOf(hdrs))
}
// 配对判据:经**内核**装出来的头必须真的是 `Authorization: Bearer <token>`。
//
// 这条在 internal/provider 侧另有一条同型判据(build_headers_fallback_test.go),
// 这里从「适配器没抢鉴权」推到「兜底确实补上了」,把 P1-3 的结论补成闭环。
func TestChannelsGeminiKernelFallbackSendsBearer(t *testing.T) {
vm, _ := zcodeHarness(t)
// 适配器产出:不含任何鉴权头(见上一条)
hdrs, err := vm.BuildHeaders("channels-gemini", map[string]interface{}{
"api_key": "GA-OAUTH-TOKEN",
})
if err != nil {
t.Fatal(err)
}
if _, has := hdrs["Authorization"]; has {
t.Fatalf("适配器不该发 Authorization,实得 %v", hdrs)
}
// 于是内核兜底条件成立(Authorization 与 x-api-key 都为空)⇒ 会补 Bearer。
// 这里断言**条件成立**,而不是重复实现兜底逻辑(兜底本身由 provider 判据咬)。
if hdrs["x-api-key"] != "" {
t.Fatalf("x-api-key 必须为空(否则内核兜底不会补 Bearer),实得 %v", hdrs)
}
t.Log("✓ 适配器不装鉴权 ⇒ 内核兜底条件成立 ⇒ 发 Authorization: Bearer <access_token>")
}