package lua import ( "encoding/json" "strings" "testing" ) // loadZenVM 装载 opencodezen 适配器(照 adapter_opencodezen_test.go 的方式)。 func loadZenVM(t *testing.T) *VM { t.Helper() src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua") if err != nil { t.Fatal(err) } vm := NewVM(t.TempDir()) t.Cleanup(vm.Stop) if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil { t.Fatalf("加载失败: %v", err) } return vm } // ★ assistant 回合的 reasoning_content **必须回传**给 opencode 上游。 // // 真实缺陷(生产实测,2026-10-16): // // provider localzen: api error 400: Upstream request failed: [invalid_request_error] // The `reasoning_content` in the thinking mode must be passed back to the API. // // 根因:transform_request 里写了 `msg.reasoning_content = nil`,理由是 // "Zen 免费池不接受回传"。该假设**方向反了** —— 上游要的就是它。 // // ★★ 这个缺陷极难归因,因为失败是**交替**出现的: // 只有当上一轮**真的产出了 reasoning**(thinking 模型 + reasoning_effort 开启) // 时,回放那段历史才会被拒;普通回合照常 200。审计里表现为成串 502 紧随一个 // 成功的长上下文请求(实测 prompt_tokens=139330)之后。 // // 判据必须**咬住「被剥掉」这个具体行为**,而不是"请求成功" —— 后者在 // 非 thinking 回合下永远为真,测不出这个缺陷。 func TestOpenCodeZenKeepsAssistantReasoningContent(t *testing.T) { vm := loadZenVM(t) body := map[string]interface{}{ "model": "deepseek-v4.1-flash-free", "stream": true, "messages": []map[string]interface{}{ {"role": "user", "content": "1+1=?"}, { "role": "assistant", "content": "2", // 这段正是 agent 回放历史时会带上的东西 "reasoning_content": "一加一等于二。", }, {"role": "user", "content": "再加一"}, }, } raw, err := json.Marshal(body) if err != nil { t.Fatal(err) } out, err := vm.Transform("opencodezen", "transform_request", string(raw)) if err != nil { t.Fatal(err) } // 解析出站体,找 assistant 回合 var sent struct { Messages []map[string]interface{} `json:"messages"` } if err := json.Unmarshal([]byte(out), &sent); err != nil { t.Fatalf("出站体不是合法 JSON: %v\n%s", err, out) } var found bool for _, m := range sent.Messages { if m["role"] != "assistant" { continue } found = true rc, _ := m["reasoning_content"].(string) if rc == "" { t.Fatalf("assistant 回合的 reasoning_content 被剥掉了 ⇒ 上游会回 "+ "`The reasoning_content in the thinking mode must be passed back to the API`\n"+ "出站 messages: %s", out) } if rc != "一加一等于二。" { t.Errorf("reasoning_content 被改写:%q", rc) } } if !found { t.Fatalf("没找到 assistant 回合(判据失效)\n%s", out) } t.Log("✓ assistant 的 reasoning_content 已保留") } // ★ 反向:**空的** reasoning_content 应被丢掉(不要把空串发给上游)。 // 与上一条配对 —— 缺了这条,"改成无条件保留"这类过度修正不会被发现。 func TestOpenCodeZenDropsEmptyReasoningContent(t *testing.T) { vm := loadZenVM(t) raw := `{"model":"m","stream":true,"messages":[ {"role":"assistant","content":"hi","reasoning_content":""} ]}` out, err := vm.Transform("opencodezen", "transform_request", raw) if err != nil { t.Fatal(err) } if strings.Contains(out, `"reasoning_content":""`) { t.Errorf("空的 reasoning_content 不该发给上游(多余字段,部分上游会挑),实得: %s", out) } t.Log("✓ 空 reasoning_content 已丢弃") }