package lua import ( "encoding/json" "path/filepath" "strings" "testing" ) func TestAnthropicSerializerShape(t *testing.T) { vm := NewVM(filepath.Join(t.TempDir(), "adapters")) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() if !vm.HasFn("anthropic", "serialize_response") { t.Fatal("anthropic 未声明 serialize_response") } out, err := vm.Transform("anthropic", "serialize_response", `{"model":"claude-x","id":"msg_1","content":"hello","finish_reason":"stop","token_usage":{"prompt":10,"completion":5,"total":15}}`) if err != nil { t.Fatal(err) } var m map[string]interface{} if err := json.Unmarshal([]byte(out), &m); err != nil { t.Fatalf("not JSON: %s", out) } if m["type"] != "message" || m["role"] != "assistant" { t.Errorf("Anthropic 形状不对: %s", out) } if m["stop_reason"] != "end_turn" { t.Errorf("stop_reason = %v, want end_turn", m["stop_reason"]) } blocks, _ := m["content"].([]interface{}) if len(blocks) != 1 { t.Fatalf("blocks = %v", m["content"]) } b0 := blocks[0].(map[string]interface{}) if b0["type"] != "text" || b0["text"] != "hello" { t.Errorf("首个块应为 text/hello: %v", b0) } u := m["usage"].(map[string]interface{}) if u["input_tokens"] != float64(10) || u["output_tokens"] != float64(5) { t.Errorf("usage 字段名/值不对: %v", u) } // 内核不得混入 OpenAI 字段 for _, k := range []string{"object", "choices", "usage.prompt_tokens"} { if strings.Contains(out, `"`+k+`"`) { t.Errorf("OpenAI 字段 %q 混入 Anthropic 输出: %s", k, out) } } } // TestAnthropicDeserializeRequestShape 验证 anthropic 适配器的 ingress 解析 // (deserialize_request)输出与内核原 Go 兜底(anthropicRequest.toUnified)逐字段 // 一致。这是"让出"的前提:若两边产出不同,"谁解析"就会悄悄改变行为。 func TestAnthropicDeserializeRequestShape(t *testing.T) { vm := NewVM(filepath.Join(t.TempDir(), "adapters")) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() // 适配器必须声明 ingress 并提供钩子,内核才会让出。 if !vm.HasFn("anthropic", "deserialize_request") { t.Fatal("anthropic 未声明 deserialize_request") } // system 两种拼写 + content 字符串/块数组 + thinking 缺省 in := `{ "model": "claude-x", "max_tokens": 128, "system": [{"type":"text","text":"be terse"},{"type":"text","text":"!"}], "messages": [ {"role":"user","content":"hi"}, {"role":"assistant","content":[{"type":"text","text":"hello"}]} ], "temperature": 0.5, "top_p": 0.9, "top_k": 40, "stop_sequences": ["STOP"], "stream": true }` out, err := vm.Transform("anthropic", "deserialize_request", in) if err != nil { t.Fatalf("deserialize_request: %v", err) } var u struct { Model string `json:"model"` System string `json:"system"` Messages []json.RawMessage `json:"messages"` MaxTokens int `json:"max_tokens"` Stream bool `json:"stream"` Temperature *float64 `json:"temperature"` TopP *float64 `json:"top_p"` TopK int `json:"top_k"` StopSequences []string `json:"stop_sequences"` Thinking *struct { Enabled *bool `json:"enabled"` } `json:"thinking"` } if err := json.Unmarshal([]byte(out), &u); err != nil { t.Fatalf("not JSON: %s", out) } if u.Model != "claude-x" { t.Errorf("model = %q", u.Model) } // Go 侧 systemToString 把 [{text:"be terse"},{text:"!"}] 拼成 "be terse!" // (text 块按序拼接,无分隔符)。Lua 必须一致。 if u.System != "be terse!" { t.Errorf("system = %q, want %q (Go 侧拼接语义)", u.System, "be terse!") } if len(u.Messages) != 2 { t.Fatalf("messages = %d", len(u.Messages)) } if u.MaxTokens != 128 || !u.Stream { t.Errorf("max_tokens/stream = %d/%v", u.MaxTokens, u.Stream) } if u.Temperature == nil || *u.Temperature != 0.5 { t.Errorf("temperature = %v", u.Temperature) } if u.TopP == nil || *u.TopP != 0.9 || u.TopK != 40 { t.Errorf("top_p/top_k = %v/%d", u.TopP, u.TopK) } if len(u.StopSequences) != 1 || u.StopSequences[0] != "STOP" { t.Errorf("stop_sequences = %v", u.StopSequences) } // 缺省 thinking → Go 侧显式发 {enabled:false};Lua 必须一致。 if u.Thinking == nil || u.Thinking.Enabled == nil || *u.Thinking.Enabled != false { t.Errorf("缺省 thinking 应为 {enabled:false}, 实得 %s", out) } } // TestAnthropicDeserializeRejectsMissingMessages 验证 Lua 侧与 Go 侧一样, // 对缺 messages 的请求报错(而不是产出空请求)。 func TestAnthropicDeserializeRejectsMissingMessages(t *testing.T) { vm := NewVM(filepath.Join(t.TempDir(), "adapters")) if err := vm.Start(); err != nil { t.Fatal(err) } defer vm.Stop() if _, err := vm.Transform("anthropic", "deserialize_request", `{"model":"x","max_tokens":8}`); err == nil { t.Error("缺 messages 应报错") } }