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 处)。
77 lines
2.7 KiB
Go
77 lines
2.7 KiB
Go
package provider
|
||
|
||
import (
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"llmsproxy/internal/config"
|
||
"llmsproxy/internal/lua"
|
||
)
|
||
|
||
// ★ 内核兜底鉴权必须让位给适配器已经给出的凭据头。
|
||
//
|
||
// 背景:anthropic 基座(及继承它的 channels-zcode / channels-cline 等)现在在
|
||
// build_headers 里发 `x-api-key`(Anthropic 协议用它,不是 Bearer)。
|
||
// 而内核在 buildHeaders 末尾有一段兜底:`Authorization` 为空就补
|
||
// `Bearer <api_key>`。若不改,同一请求会同时携带两套鉴权方案 ——
|
||
// 部分 Anthropic 兼容上游对歧义鉴权直接 401,等于把刚修好的头废掉。
|
||
//
|
||
// 本判据咬的是**真实产出**(p.buildHeaders 就是发请求前那条路径)。
|
||
func TestBuildHeadersSkipsBearerWhenAdapterSuppliesXAPIKey(t *testing.T) {
|
||
vm := lua.NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(vm.Stop)
|
||
|
||
s := src("claude-direct", "http://127.0.0.1:1", "anthropic", "claude-x")
|
||
s.APIKey = "sk-ant-SECRET"
|
||
p := New(s, vm)
|
||
|
||
h, err := p.buildHeaders(`{"model":"claude-x"}`, "http://127.0.0.1:1/v1/messages", "")
|
||
if err != nil {
|
||
t.Fatalf("buildHeaders: %v", err)
|
||
}
|
||
if got := h.Get("X-Api-Key"); got != "sk-ant-SECRET" {
|
||
t.Fatalf("★ x-api-key 应携带凭据,实得 %q", got)
|
||
}
|
||
if got := h.Get("Authorization"); got != "" {
|
||
t.Fatalf("★ 适配器已给 x-api-key 时,内核**不得**再补 Authorization: Bearer(两套鉴权冲突)"+
|
||
",实得 %q", got)
|
||
}
|
||
}
|
||
|
||
// 配对判据:普通适配器(不支持 x-api-key 的那些)兜底行为必须**不变**。
|
||
// 这条防「改 anthropic 顺手把其它十几个适配器的鉴权搞没了」。
|
||
func TestBuildHeadersKeepsBearerFallbackForNonAnthropicAdapters(t *testing.T) {
|
||
vm := lua.NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||
if err := vm.Start(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(vm.Stop)
|
||
|
||
for _, adapter := range []string{"openai", "deepseek", "groq"} {
|
||
t.Run(adapter, func(t *testing.T) {
|
||
src := config.Source{
|
||
Name: "s-" + adapter,
|
||
BaseURL: "http://127.0.0.1:1",
|
||
APIKey: "sk-plain",
|
||
Adapter: adapter,
|
||
MaxConcurrent: 1,
|
||
Models: []config.Model{{ID: "m", Kind: "chat"}},
|
||
}
|
||
p := New(src, vm)
|
||
h, err := p.buildHeaders(`{"model":"m"}`, "http://127.0.0.1:1/chat/completions", "")
|
||
if err != nil {
|
||
t.Fatalf("buildHeaders: %v", err)
|
||
}
|
||
if got := h.Get("Authorization"); got != "Bearer sk-plain" {
|
||
t.Fatalf("★ %s 的兜底 Bearer 必须保持原样,实得 %q", adapter, got)
|
||
}
|
||
if got := h.Get("X-Api-Key"); got != "" {
|
||
t.Fatalf("%s 不该凭空多出 x-api-key,实得 %q", adapter, got)
|
||
}
|
||
})
|
||
}
|
||
}
|