package lua import ( "encoding/json" "strings" "testing" ) // loadBuddyAdapter 装载内置 buddy 适配器。 func loadBuddyAdapter(t *testing.T) *VM { t.Helper() src, err := bundledAdapters.ReadFile("adapters/buddy.lua") if err != nil { t.Fatal(err) } vm := NewVM(t.TempDir()) t.Cleanup(vm.Stop) if err := vm.LoadAdapterSource("buddy", string(src)); err != nil { t.Fatalf("装载失败: %v", err) } return vm } // ★ buddy 适配器:两个厂商要求,各有实测依据。 // // ① glm-5.1 **必须**带非空 reasoning_effort(否则上游 400 + 11150) // ② 流式请求必须带 Accept: text/event-stream(否则只回空帧、completion_tokens=0) func TestBuddyAdapterEffortAndHeaders(t *testing.T) { vm := loadBuddyAdapter(t) // ① glm-5.1 → 补 medium out, err := vm.Transform("buddy", "transform_request", `{"model":"glm-5.1","messages":[{"role":"user","content":"hi"}],"max_tokens":100,"stream":true}`) if err != nil { t.Fatal(err) } var b map[string]interface{} if err := json.Unmarshal([]byte(out), &b); err != nil { t.Fatalf("%v\n%s", err, out) } if b["reasoning_effort"] != "medium" { t.Errorf("glm-5.1 必须补 reasoning_effort=medium(否则上游 11150),实得 %v", b["reasoning_effort"]) } // ② 其它模型**不补**(避免把单模型要求扩成全平台行为) out2, err := vm.Transform("buddy", "transform_request", `{"model":"hy3","messages":[{"role":"user","content":"hi"}],"max_tokens":100,"stream":true}`) if err != nil { t.Fatal(err) } var b2 map[string]interface{} json.Unmarshal([]byte(out2), &b2) if b2["reasoning_effort"] != nil { t.Errorf("hy3 不该被补 reasoning_effort(实测不需要),实得 %v", b2["reasoning_effort"]) } // ③ 调用方已给档位时**尊重它** out3, _ := vm.Transform("buddy", "transform_request", `{"model":"glm-5.1","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high","stream":true}`) var b3 map[string]interface{} json.Unmarshal([]byte(out3), &b3) if b3["reasoning_effort"] != "high" { t.Errorf("调用方给的档位必须保留,实得 %v", b3["reasoning_effort"]) } // ④ 头:Accept 必须是 SSE h, err := vm.BuildHeaders("buddy", map[string]interface{}{"api_key": "TOK"}) if err != nil { t.Fatal(err) } if h["Accept"] != "text/event-stream" { t.Errorf("Accept 必须是 text/event-stream(否则上游只回空帧),实得 %q", h["Accept"]) } for _, k := range []string{"X-Domain", "X-Product", "X-Product-Code"} { if h[k] == "" { t.Errorf("缺少 buddy 身份头 %s", k) } } t.Logf("✓ glm-5.1 补 effort;hy3 不补;Accept=%s", h["Accept"]) } // 配对判据:协议转换必须与 openai 一致(buddy 只是补两个字段,不该改转换)。 func TestBuddyAdapterKeepsOpenAIWire(t *testing.T) { vm := loadBuddyAdapter(t) chunk := `{"id":"x","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"hi","reasoning_content":"think"},"finish_reason":null}]}` out, err := vm.Transform("buddy", "transform_stream_chunk", chunk) if err != nil { t.Fatal(err) } if !strings.Contains(out, `"content":"hi"`) || !strings.Contains(out, `"reasoning_content":"think"`) { t.Errorf("流式转换必须保留 content 与 reasoning_content,实得 %s", out) } } // ★ 回归护栏:适配器**必须**定义全部四个转换函数。 // // 真实事故(2026-10-16):我第一版把基座继承写成"在适配器里读兄弟适配器源码", // 而适配器运行在**另一个沙箱 VM**(无 __llmsproxy_plugin_dir、无文件系统), // 于是基座加载**静默失败** ⇒ 适配器只有 transform_request, // **缺 transform_stream_chunk** ⇒ 内核无法解析 SSE ⇒ 上游明明回了 39 帧正文, // 网关却记 completion_tokens=0、客户端看到**空回复**。 // 更糟的是:hy3/glm-5.3/kimi-k3-1 全被它弄坏,而 glm-5.2 因多源兜底 // "看起来正常" ⇒ 症状指向错误方向。 // // ⇒ 判据必须咬住「四个转换函数都存在」,而不是"某个模型能回话" // // (后者会被别的源兜底掩盖 —— 本会话已栽过两次)。 func TestBuddyAdapterDefinesAllTransforms(t *testing.T) { vm := loadBuddyAdapter(t) for _, fn := range []string{"transform_request", "transform_response", "transform_stream_chunk", "transform_error"} { if !vm.HasFn("buddy", fn) { t.Errorf("适配器缺 %s ⇒ 内核该路径会静默降级(流式会变成空回复)", fn) } } t.Log("✓ 四个转换函数齐备") }