chore(adapters): 移除非官方调用方式的适配器,保留官方适配器与模板

移除 4 个靠伪造客户端身份/绕过 WAF 才能工作的适配器,内核不再发行任何
非官方调用方式:

- opencodezen / opencode / opencodego:靠 x-opencode-* 客户端指纹冒充官方
  客户端;zen 免费池明确要求「free tier can only be used in OpenCode」
- agentrouter:伪造 Origin: qwen.ai + Referer + X-Requested-With 绕过阿里云 WAF

保留 12 个官方适配器(openai/deepseek/anthropic/gemini/github/groq/mistral/
ollama/kimicode/qwen38/sensenova/trae),其中 sensenova/qwen38 实测零伪装特征。

本地保留两处:adapters/_local_archive/(gitignored 非递归子目录,不加载)
与线上 /etc/llmsproxy/adapters/(adapter_dir 权威副本,cp -f 不删文件)。

测试同步:删除仅服务于被移除适配器的 11 个测试函数与 usage_cost_test.go;
改写 2 处借 opencode 之名的 provider 测试为用 openai 的 HTML 错误页压缩能力
验证同一契约(适配器钩子拥有错误格式、原文不外泄)。

验证:go test -tags luajit ./... 全绿;二进制内 x-opencode-client /
QwenCode/0.2.0 / qwen.ai 等指纹字符串计数归零。
This commit is contained in:
JianFeeeee
2026-10-11 08:17:56 +08:00
parent d3203d88fc
commit 6113924655
12 changed files with 25 additions and 1825 deletions

View File

@ -32,7 +32,7 @@ func TestLoadBundledAdapters(t *testing.T) {
for _, a := range adapters {
names[a.Name] = true
}
for _, want := range []string{"openai", "deepseek", "anthropic", "gemini", "ollama", "kimicode", "opencode"} {
for _, want := range []string{"openai", "deepseek", "anthropic", "gemini", "ollama", "kimicode"} {
if !names[want] {
t.Errorf("missing adapter %s (got %v)", want, names)
}
@ -112,179 +112,6 @@ func TestBuildHeadersFallbackStatic(t *testing.T) {
}
}
func TestOpenCodeAdapterFingerprint(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
hdrs, err := vm.BuildHeaders("opencode", map[string]interface{}{
"api_key": "public",
"timestamp": 1700000000,
"body": `{"model":"x"}`,
"url": "https://opencode.ai/zen/v1/chat/completions",
"method": "POST",
})
if err != nil {
t.Fatalf("build headers: %v", err)
}
// zen 按客户端指纹路由请求池:UA 必须是 opencode 真实格式,
// 且必须携带 x-opencode-* 身份头,否则带 tools 的请求会被上游
// 判为匿名客户端并返回 network_error 空流(表现为空回复)。
if ua := hdrs["User-Agent"]; !strings.HasPrefix(ua, "opencode/") ||
!strings.Contains(ua, "ai-sdk/provider-utils/") {
t.Fatalf("opencode adapter must send the real opencode client UA, got %q", ua)
}
for _, h := range []string{"x-opencode-client", "x-opencode-project", "x-opencode-session", "x-opencode-request"} {
if hdrs[h] == "" {
t.Fatalf("opencode adapter must send %q (zen client fingerprint), got %v", h, hdrs)
}
}
hdrs2, _ := vm.BuildHeaders("opencode", map[string]interface{}{"api_key": "public", "timestamp": 1700000001, "body": `{"model":"y"}`, "method": "POST"})
if hdrs["x-opencode-request"] == hdrs2["x-opencode-request"] {
t.Fatalf("x-opencode-request must vary per request, got %q twice", hdrs["x-opencode-request"])
}
if hdrs["Authorization"] != "" {
t.Fatalf("opencode adapter must not hardcode Authorization (config api_key supplies it), got %q", hdrs["Authorization"])
}
// passthrough behaves like the openai adapter
out, err := vm.Transform("opencode", "transform_request", `{"model":"x","disable_thinking":true,"extra_body":{},"messages":[{"role":"user","content":"hi"}]}`)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "disable_thinking") || strings.Contains(out, "extra_body") {
t.Fatalf("opencode transform_request must strip provider fields: %s", out)
}
}
func TestOpenCodeNormalizesRoles(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
// zen 上游只接受 system/user/assistant/tool/latest_reminder;
// OpenAI 系客户端发 `developer`(Claude Code 还会用 `function`)必须归一化成 system。
body := `{"model":"x","messages":[
{"role":"developer","content":"you are helpful"},
{"role":"system","content":"sys"},
{"role":"user","content":"hi"},
{"role":"assistant","content":"ok"},
{"role":"tool","tool_call_id":"t1","content":"{\"ok\":true}"}
]}`
out, err := vm.Transform("opencode", "transform_request", body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, `"developer"`) {
t.Fatalf("developer role must be normalized: %s", out)
}
if !strings.Contains(out, `"system"`) || !strings.Contains(out, `"user"`) {
t.Fatalf("allowed roles must survive: %s", out)
}
if !strings.Contains(out, `"assistant"`) {
t.Fatalf("assistant role must survive: %s", out)
}
if !strings.Contains(out, `"tool"`) {
t.Fatalf("tool role must survive: %s", out)
}
}
func TestOpenCodeStripsMultiModalParts(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
body := `{"model":"x","messages":[
{"role":"user","content":[
{"type":"text","text":"describe"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,QUJD"}},
{"type":"input_audio","input_audio":{"data":"QQ","format":"wav"}}
]},
{"role":"user","content":[{"type":"image_url","image_url":{"url":"https://ex.com/a.png"}}]},
{"role":"assistant","content":"plain"}
]}`
out, err := vm.Transform("opencode", "transform_request", body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "image_url") || strings.Contains(out, "input_audio") {
t.Fatalf("opencode must strip multimodal parts: %s", out)
}
if !strings.Contains(out, "describe") {
t.Fatalf("text parts must survive: %s", out)
}
// image-only message dropped: only the text message and plain assistant message remain
if got := strings.Count(out, `"role"`); got != 2 {
t.Fatalf("image-only message must be dropped, got %d messages: %s", got, out)
}
}
// TestOpenCodeKeepsToolCallWithEmptyContent pins the fix for an infinite
// "repeated tool call" loop: an assistant turn whose content serialises to an
// empty ARRAY while carrying tool_calls must survive the multimodal strip.
// Dropping it orphans the following tool result, so the model never sees the
// call it already made and re-issues it on every turn.
func TestOpenCodeKeepsToolCallWithEmptyContent(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
body := `{"model":"x","messages":[
{"role":"user","content":"read /tmp/x"},
{"role":"assistant","content":[],"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"read","arguments":"{\"path\":\"/tmp/x\"}"}}]},
{"role":"tool","tool_call_id":"call_abc","content":"hello"},
{"role":"user","content":"now summarize"}
]}`
out, err := vm.Transform("opencode", "transform_request", body)
if err != nil {
t.Fatalf("transform_request: %v", err)
}
var req struct {
Messages []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
} `json:"tool_calls"`
ToolCallID string `json:"tool_call_id"`
} `json:"messages"`
}
if err := json.Unmarshal([]byte(out), &req); err != nil {
t.Fatalf("unmarshal: %v (%s)", err, out)
}
if len(req.Messages) != 4 {
t.Fatalf("expected 4 messages (tool call must not be dropped), got %d: %s", len(req.Messages), out)
}
asst := req.Messages[1]
if asst.Role != "assistant" || len(asst.ToolCalls) != 1 || asst.ToolCalls[0].ID != "call_abc" {
t.Fatalf("assistant tool_calls lost: %s", out)
}
if string(asst.Content) != `""` {
t.Fatalf("empty content must be rewritten to \"\", got %s", asst.Content)
}
if req.Messages[2].Role != "tool" || req.Messages[2].ToolCallID != "call_abc" {
t.Fatalf("tool result must stay paired with its call: %s", out)
}
// Negative control: a message with neither text nor tool calls is still dropped.
only := `{"model":"x","messages":[
{"role":"user","content":[{"type":"image_url","image_url":{"url":"https://ex.com/a.png"}}]},
{"role":"user","content":"keep me"}
]}`
out2, err := vm.Transform("opencode", "transform_request", only)
if err != nil {
t.Fatal(err)
}
if got := strings.Count(out2, `"role"`); got != 1 {
t.Fatalf("image-only message without tool calls must still be dropped, got %d: %s", got, out2)
}
}
func TestBuildHeadersCustomHook(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
@ -432,74 +259,6 @@ func TestMultimodalTransform(t *testing.T) {
}
}
func TestOpenCodeAdapterNormalizesDeveloperRole(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatalf("start: %v", err)
}
defer vm.Stop()
raw := `{"model":"zen","messages":[` +
`{"role":"developer","content":"be concise"},` +
`{"role":"user","content":"hi"},` +
`{"role":"assistant","content":"hello"},` +
`{"role":"function","content":"{\"a\":1}"},` +
`{"role":"latest_reminder","content":"remind"}]}`
out, err := vm.Transform("opencode", "transform_request", raw)
if err != nil {
t.Fatalf("transform_request: %v", err)
}
var req struct {
Messages []struct {
Role string `json:"role"`
} `json:"messages"`
}
if err := json.Unmarshal([]byte(out), &req); err != nil {
t.Fatalf("output not JSON: %v\n%s", err, out)
}
want := []string{"system", "user", "assistant", "system", "latest_reminder"}
if len(req.Messages) != len(want) {
t.Fatalf("got %d messages, want %d: %s", len(req.Messages), len(want), out)
}
for i, w := range want {
if req.Messages[i].Role != w {
t.Errorf("message %d role = %q, want %q", i, req.Messages[i].Role, w)
}
}
}
func TestOpenCodeAdapterKeepsWhitelistedRolesAndDropsMultimodal(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
t.Fatalf("start: %v", err)
}
defer vm.Stop()
raw := `{"messages":[` +
`{"role":"system","content":"sys"},` +
`{"role":"user","content":[{"type":"text","text":"keep"},{"type":"image_url","url":"x"}]},` +
`{"role":"user","content":[{"type":"image_url","url":"x"}]}]}`
out, err := vm.Transform("opencode", "transform_request", raw)
if err != nil {
t.Fatalf("transform_request: %v", err)
}
var req struct {
Messages []struct {
Role string `json:"role"`
Content any `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal([]byte(out), &req); err != nil {
t.Fatalf("output not JSON: %v\n%s", err, out)
}
if len(req.Messages) != 2 {
t.Fatalf("got %d messages, want 2 (multimodal-only dropped): %s", len(req.Messages), out)
}
if req.Messages[0].Role != "system" || req.Messages[1].Role != "user" {
t.Fatalf("unexpected roles: %s", out)
}
}
func TestAdaptersPassFinishReason(t *testing.T) {
vm := NewVM(freshAdapterDir(t))
if err := vm.Start(); err != nil {
@ -511,7 +270,7 @@ func TestAdaptersPassFinishReason(t *testing.T) {
// every chunk) must NOT terminate the stream.
chunk := `{"choices":[{"index":0,"finish_reason":"tool_calls","delta":{"content":""}}]}`
empty := `{"choices":[{"index":0,"finish_reason":"","delta":{"content":"x"}}]}`
for _, name := range []string{"openai", "deepseek", "github", "groq", "kimicode", "mistral", "opencode"} {
for _, name := range []string{"openai", "deepseek", "github", "groq", "kimicode", "mistral"} {
out, err := vm.Transform(name, "transform_stream_chunk", chunk)
if err != nil {
t.Fatalf("%s: %v", name, err)
@ -736,8 +495,7 @@ func TestAdaptersReportZeroCacheHit(t *testing.T) {
defer vm.Stop()
// OpenAI-shaped upstreams: usage.prompt_tokens_details.cached_tokens = 0
openaiLike := []string{"openai", "deepseek", "sensenova", "opencode",
"agentrouter", "github", "groq", "kimicode", "mistral"}
openaiLike := []string{"openai", "deepseek", "sensenova", "github", "groq", "kimicode", "mistral"}
respBody := `{"choices":[{"message":{"content":"hi"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12,
"prompt_tokens_details":{"cached_tokens":0}}}`
@ -1465,7 +1223,7 @@ func TestNonStreamToolCallsPreserved(t *testing.T) {
for _, name := range []string{
"openai", "sensenova", "trae", "deepseek", "github",
"groq", "kimicode", "mistral", "opencode", "agentrouter",
"groq", "kimicode", "mistral",
} {
out, err := vm.Transform(name, "transform_response", body)
if err != nil {
@ -1741,9 +1499,8 @@ func TestToolIDSanitize(t *testing.T) {
}
}
// ---- openai + agentrouter: same rewrite, and both sites must stay in sync ----
// agentrouter fronts claude-opus-4-8, so it inherits the same id rule.
for _, adapterName := range []string{"openai", "agentrouter"} {
// ---- openai: tool-id rewrite ----
for _, adapterName := range []string{"openai"} {
for _, id := range []string{"bash:0", "call_ok_123", long} {
out, err := vm.Transform(adapterName, "transform_request", body(id))
if err != nil {
@ -1838,12 +1595,12 @@ func TestToolIDSanitizeResponse(t *testing.T) {
legal := regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
// ---- openai + agentrouter non-streaming ----
// ---- openai non-streaming ----
resp := `{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[
{"id":"bash:0","type":"function","function":{"name":"bash","arguments":"{\"command\":\"whoami\"}"}}]},
"finish_reason":"tool_calls"}]}`
var nonStreamID string
for _, adapterName := range []string{"openai", "agentrouter"} {
for _, adapterName := range []string{"openai"} {
out, err := vm.Transform(adapterName, "transform_response", resp)
if err != nil {
t.Fatalf("%s transform_response: %v", adapterName, err)
@ -1878,7 +1635,7 @@ func TestToolIDSanitizeResponse(t *testing.T) {
ID string `json:"id"`
} `json:"tool_calls"`
}
for _, adapterName := range []string{"openai", "agentrouter"} {
for _, adapterName := range []string{"openai"} {
out, err := vm.Transform(adapterName, "transform_stream_chunk", chunk)
if err != nil {
t.Fatalf("%s transform_stream_chunk: %v", adapterName, err)
@ -1902,7 +1659,7 @@ func TestToolIDSanitizeResponse(t *testing.T) {
// ---- a later argument fragment has no id and must stay id-less ----
argChunk := `{"choices":[{"delta":{"tool_calls":[
{"index":0,"function":{"arguments":"{\"command\":\"whoami\"}"}}]},"finish_reason":null}]}`
for _, adapterName := range []string{"openai", "agentrouter"} {
for _, adapterName := range []string{"openai"} {
out, err := vm.Transform(adapterName, "transform_stream_chunk", argChunk)
if err != nil {
t.Fatalf("%s arg fragment: %v", adapterName, err)