package service import ( "encoding/json" "fmt" "slices" "strings" "testing" "oci-portal/internal/aiwire" ) func respReq(t *testing.T, raw string) aiwire.RespRequest { t.Helper() var req aiwire.RespRequest if err := json.Unmarshal([]byte(raw), &req); err != nil { t.Fatalf("unmarshal req: %v", err) } return req } func deref(p *int) int { if p == nil { return 0 } return *p } func TestRespPassthroughValidate(t *testing.T) { prev := "resp_1" bg := true tests := []struct { name string req aiwire.RespRequest wantErr bool }{ {"合法", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}}}, false}, {"混用function", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}, {Type: "function", Name: "f"}}}, false}, {"缺model", aiwire.RespRequest{Tools: []aiwire.RespTool{{Type: "web_search"}}}, true}, {"服务端工具流式放行", aiwire.RespRequest{Model: "m", Stream: true, Tools: []aiwire.RespTool{{Type: "web_search"}}}, false}, {"无工具流式放行", aiwire.RespRequest{Model: "m", Stream: true}, false}, {"function工具流式放行", aiwire.RespRequest{Model: "m", Stream: true, Tools: []aiwire.RespTool{{Type: "function", Name: "f"}}}, false}, {"code_interpreter放行", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "code_interpreter"}}}, false}, {"mcp流式放行", aiwire.RespRequest{Model: "m", Stream: true, Tools: []aiwire.RespTool{{Type: "mcp"}}}, false}, {"有状态拒绝", aiwire.RespRequest{Model: "m", PreviousResponseID: prev}, true}, {"background拒绝", aiwire.RespRequest{Model: "m", Background: &bg}, true}, {"未知工具拒绝", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "web_search"}, {Type: "file_search"}}}, true}, {"namespace放行", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "namespace", Name: "multi_agent_v1"}}}, false}, {"custom放行", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "custom", Name: "run_script"}}}, false}, {"tool_search放行", aiwire.RespRequest{Model: "m", Tools: []aiwire.RespTool{{Type: "tool_search"}}}, false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := RespPassthroughValidate(test.req) if (err != nil) != test.wantErr { t.Fatalf("err = %v, wantErr %v", err, test.wantErr) } }) } } func TestRespPassthroughBody(t *testing.T) { raw := []byte(`{"model":"m","input":"hi","stream":true,"store":true,"max_output_tokens":128,"custom_field":{"a":1.5}}`) out, compat, err := RespPassthroughBody(raw) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } if len(compat.Dropped) != 0 || len(compat.Flattened) != 0 || len(compat.NsRefs) != 0 { t.Errorf("无 codex 工具不应有兼容改写: %+v", compat) } var body map[string]any if err := json.Unmarshal(out, &body); err != nil { t.Fatalf("unmarshal out: %v", err) } if body["store"] != false { t.Errorf("store 应强制 false, got %v", body["store"]) } if body["stream"] != true { t.Errorf("stream 应原样保留, got %v", body["stream"]) } if string(out) == "" || !strings.Contains(string(out), `"max_output_tokens":128`) { t.Errorf("数值字段应保真: %s", out) } if !strings.Contains(string(out), `"custom_field"`) { t.Errorf("未知字段应保留: %s", out) } if _, _, err := RespPassthroughBody([]byte("not-json")); err == nil { t.Error("非法 JSON 应报错") } } // TestRespQualifyNsName 断言限定名生成规则与 CLIProxyAPI 对齐。 func TestRespQualifyNsName(t *testing.T) { tests := []struct{ name, ns, tool, want string }{ {"常规拼接", "multi_agent_v1", "spawn_agent", "multi_agent_v1__spawn_agent"}, {"mcp子工具不加前缀", "mcp__sites", "mcp__sites__create_site", "mcp__sites__create_site"}, {"ns以双下划线结尾", "codex_app__", "update", "codex_app__update"}, {"已带前缀不重复", "ns1", "ns1__tool", "ns1__tool"}, {"空ns原样", "", "tool", "tool"}, {"空名返回空", "ns1", "", ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := respQualifyNsName(test.ns, test.tool); got != test.want { t.Fatalf("respQualifyNsName(%q,%q) = %q, want %q", test.ns, test.tool, got, test.want) } }) } } // TestRespPassthroughBodyCompatTools 断言 codex 工具兼容改写:namespace 拍平为 // 限定名 function 并建映射,web_search 降权剥离,剔空连删 tool_choice。 func TestRespPassthroughBodyCompatTools(t *testing.T) { tests := []struct { name string raw string wantFlattened []string wantDropped []string wantRefs map[string]RespNsRef wantContains []string wantAbsent []string }{ { "namespace拍平保留function", `{"model":"m","tools":[{"type":"function","name":"exec"},{"type":"namespace","name":"multi_agent_v1","tools":[{"type":"function","name":"spawn_agent","parameters":{"type":"object"}}]}]}`, []string{"namespace:multi_agent_v1"}, nil, map[string]RespNsRef{"multi_agent_v1__spawn_agent": {Namespace: "multi_agent_v1", Name: "spawn_agent"}}, []string{`"name":"multi_agent_v1__spawn_agent"`, `"name":"exec"`}, []string{`"type":"namespace"`}, }, { "mcp子工具保名并补parameters", `{"model":"m","tools":[{"type":"namespace","name":"mcp__sites","tools":[{"type":"function","name":"mcp__sites__create"}]}]}`, []string{"namespace:mcp__sites"}, nil, map[string]RespNsRef{"mcp__sites__create": {Namespace: "mcp__sites", Name: "mcp__sites__create"}}, []string{`"name":"mcp__sites__create"`, `"parameters":{"properties":{},"type":"object"}`}, []string{`"type":"namespace"`}, }, { "非function子工具剥离", `{"model":"m","tools":[{"type":"namespace","name":"ns1","tools":[{"type":"custom","name":"patch"},{"type":"function","name":"run"}]}]}`, []string{"namespace:ns1"}, []string{"ns1.patch(type=custom)"}, map[string]RespNsRef{"ns1__run": {Namespace: "ns1", Name: "run"}}, []string{`"name":"ns1__run"`}, []string{"patch"}, }, { "剔空连删tool_choice", `{"model":"m","tool_choice":"auto","parallel_tool_calls":false,"tools":[{"type":"web_search","external_web_access":false}]}`, nil, []string{"web_search(external_web_access=false)"}, nil, nil, []string{`"tools"`, `"tool_choice"`, `"parallel_tool_calls"`}, }, { "web_search全访问仅删键", `{"model":"m","tools":[{"type":"web_search","external_web_access":true}]}`, nil, nil, nil, []string{`"tools":[{"type":"web_search"}]`}, []string{"external_web_access"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { out, compat, err := RespPassthroughBody([]byte(test.raw)) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } if !slices.Equal(compat.Flattened, test.wantFlattened) { t.Errorf("Flattened = %v, want %v", compat.Flattened, test.wantFlattened) } if !slices.Equal(compat.Dropped, test.wantDropped) { t.Errorf("Dropped = %v, want %v", compat.Dropped, test.wantDropped) } for q, ref := range test.wantRefs { if compat.NsRefs[q] != ref { t.Errorf("NsRefs[%s] = %+v, want %+v", q, compat.NsRefs[q], ref) } } for _, s := range test.wantContains { if !strings.Contains(string(out), s) { t.Errorf("应包含 %s: %s", s, out) } } for _, s := range test.wantAbsent { if strings.Contains(string(out), s) { t.Errorf("不应包含 %s: %s", s, out) } } }) } } // TestRespCompatHistoryAndToolChoice 断言多轮历史 function_call 与对象 tool_choice // 的 namespace 字段被重限定并删除。 func TestRespCompatHistoryAndToolChoice(t *testing.T) { raw := `{"model":"m", "tool_choice":{"type":"function","name":"spawn_agent","namespace":"multi_agent_v1"}, "input":[ {"type":"function_call","name":"spawn_agent","namespace":"multi_agent_v1","call_id":"c1","arguments":"{}"}, {"type":"function_call","name":"exec","call_id":"c2","arguments":"{}"}, {"type":"function_call_output","call_id":"c1","output":"ok"} ], "tools":[{"type":"namespace","name":"multi_agent_v1","tools":[{"type":"function","name":"spawn_agent","parameters":{}}]}]}` out, _, err := RespPassthroughBody([]byte(raw)) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } s := string(out) if !strings.Contains(s, `"tool_choice":{"name":"multi_agent_v1__spawn_agent","type":"function"}`) { t.Errorf("tool_choice 应限定化: %s", s) } if strings.Contains(s, `"namespace":"multi_agent_v1"`) { t.Errorf("namespace 字段应全部删除: %s", s) } if !strings.Contains(s, `"name":"exec"`) { t.Errorf("无 namespace 的历史项应原样: %s", s) } if c := strings.Count(s, "multi_agent_v1__spawn_agent"); c != 3 { t.Errorf("限定名应出现 3 次(tools/input/tool_choice), got %d: %s", c, s) } } // TestRespRestoreNamespace 断言非流式响应把限定名还原为短名+namespace 双字段。 func TestRespRestoreNamespace(t *testing.T) { compat := RespCompat{NsRefs: map[string]RespNsRef{"multi_agent_v1__spawn_agent": {Namespace: "multi_agent_v1", Name: "spawn_agent"}}} payload := []byte(`{"id":"r1","output":[{"type":"function_call","name":"multi_agent_v1__spawn_agent","call_id":"c1","arguments":"{}"},{"type":"message","content":[]}],"usage":{"total_tokens":7}}`) out := RespRestoreToolCalls(payload, compat) s := string(out) if !strings.Contains(s, `"name":"spawn_agent"`) || !strings.Contains(s, `"namespace":"multi_agent_v1"`) { t.Errorf("应还原短名并补 namespace: %s", s) } if !strings.Contains(s, `"total_tokens":7`) { t.Errorf("其余字段应保真: %s", s) } for name, payload := range map[string][]byte{ "未命中原样": []byte(`{"output":[{"type":"function_call","name":"other","call_id":"c"}]}`), "非法JSON": []byte(`xx`), } { if got := RespRestoreToolCalls(payload, compat); string(got) != string(payload) { t.Errorf("%s: 应返回原字节, got %s", name, got) } } if got := RespRestoreToolCalls(payload, RespCompat{}); string(got) != string(payload) { t.Error("无需还原时应返回原字节") } } // TestRespRestoreNamespaceEvent 断言流式事件还原覆盖 item 与 response.output // 两个位置,无关事件保持未改动。 func TestRespRestoreNamespaceEvent(t *testing.T) { compat := RespCompat{NsRefs: map[string]RespNsRef{"ns1__run": {Namespace: "ns1", Name: "run"}}} tests := []struct { name string data string wantChanged bool wantSub string }{ {"output_item.done", `{"type":"response.output_item.done","item":{"type":"function_call","name":"ns1__run","call_id":"c1"}}`, true, `"namespace":"ns1"`}, {"completed快照", `{"type":"response.completed","response":{"output":[{"type":"function_call","name":"ns1__run"}],"usage":{"total_tokens":1}}}`, true, `"name":"run"`}, {"文本增量不动", `{"type":"response.output_text.delta","delta":"hi"}`, false, ""}, {"未命中不动", `{"type":"response.output_item.done","item":{"type":"function_call","name":"exec"}}`, false, ""}, {"非法JSON不动", `not-json`, false, ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { out, changed := RespRestoreToolCallsEvent([]byte(test.data), compat) if changed != test.wantChanged { t.Fatalf("changed = %v, want %v", changed, test.wantChanged) } if !changed && string(out) != test.data { t.Fatalf("未改动应返回原字节: %s", out) } if test.wantSub != "" && !strings.Contains(string(out), test.wantSub) { t.Fatalf("应包含 %s: %s", test.wantSub, out) } }) } } func TestRespPassthroughUsage(t *testing.T) { tests := []struct { name string payload string wantNil bool prompt, cached int }{ {"完整", `{"usage":{"input_tokens":9,"output_tokens":3,"total_tokens":12,"input_tokens_details":{"cached_tokens":5}}}`, false, 9, 5}, {"无细分", `{"usage":{"input_tokens":9,"output_tokens":3,"total_tokens":12}}`, false, 9, 0}, {"无usage", `{"id":"resp_1"}`, true, 0, 0}, {"非法JSON", `xx`, true, 0, 0}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { usage := RespPassthroughUsage([]byte(test.payload)) if (usage == nil) != test.wantNil { t.Fatalf("usage = %v, wantNil %v", usage, test.wantNil) } if usage == nil { return } if usage.PromptTokens != test.prompt || usage.CachedTokens() != test.cached { t.Fatalf("prompt=%d cached=%d, want %d/%d", usage.PromptTokens, usage.CachedTokens(), test.prompt, test.cached) } }) } } // TestRespStreamCompletedUsage 断言流式 usage 只从 completed 事件提取。 func TestRespStreamCompletedUsage(t *testing.T) { completed := []byte(`{"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15,"input_tokens_details":{"cached_tokens":4}}}}`) if u := RespStreamCompletedUsage(completed); u == nil || u.PromptTokens != 10 || u.CompletionTokens != 5 || u.PromptTokensDetails == nil || u.PromptTokensDetails.CachedTokens != 4 { t.Fatalf("completed 事件 usage 解析失败: %+v", u) } for _, data := range []string{ `{"type":"response.output_text.delta","delta":"hi"}`, `{"type":"response.completed"}`, `not-json`, } { if u := RespStreamCompletedUsage([]byte(data)); u != nil { t.Fatalf("非 completed 或缺 response 应返回 nil: %s", data) } } } // TestRespGuardBytes 断言保险丝计量:instructions+tools 原始字节和,缺字段计 0, // 解析失败返回 0 放行。 func TestRespGuardBytes(t *testing.T) { for _, tc := range []struct { name, body string want int }{ {"双字段", `{"instructions":"abcd","tools":[{"type":"web_search"}],"input":"xxxxxxxx"}`, len(`"abcd"`) + len(`[{"type":"web_search"}]`)}, {"仅 instructions", `{"instructions":"abcd"}`, len(`"abcd"`)}, {"均缺失 input 不计", `{"input":"xxxxxxxxxxxxxxxx"}`, 0}, {"解析失败放行", `not-json`, 0}, } { t.Run(tc.name, func(t *testing.T) { if got := RespGuardBytes([]byte(tc.body)); got != tc.want { t.Fatalf("RespGuardBytes() = %d, want %d", got, tc.want) } }) } } // TestRespInjectGrokTools 断言 grok 搜索工具默认注入:仅 xai. 模型、开关可控、 // 已带同名工具不覆盖、注入清单正确。 func TestRespInjectGrokTools(t *testing.T) { for _, tc := range []struct { name, body, model string web, x bool wantInjected []string wantContains []string }{ {"非 xai 不注入", `{"tools":[]}`, "meta.llama-3.3", true, true, nil, nil}, {"双开无 tools 字段注入两个", `{"model":"xai.grok-4.3"}`, "xai.grok-4.3", true, true, []string{"web_search", "x_search"}, []string{`"web_search"`, `"x_search"`}}, {"已带 web_search 只注入 x_search", `{"tools":[{"type":"web_search","filters":{"x":1}}]}`, "xai.grok-4.3", true, true, []string{"x_search"}, []string{`"filters"`}}, {"开关全关不注入", `{"tools":[]}`, "xai.grok-4.3", false, false, nil, nil}, {"仅开 x_search", `{"tools":[]}`, "xai.grok-4.3", false, true, []string{"x_search"}, nil}, {"解析失败原样", `not-json`, "xai.grok-4.3", true, true, nil, nil}, } { t.Run(tc.name, func(t *testing.T) { out, injected := RespInjectGrokTools([]byte(tc.body), tc.model, tc.web, tc.x) if fmt.Sprint(injected) != fmt.Sprint(tc.wantInjected) { t.Fatalf("injected = %v, want %v", injected, tc.wantInjected) } if len(injected) == 0 && string(out) != tc.body { t.Fatalf("未注入时应原样返回: %s", out) } for _, sub := range append(tc.wantContains, toolTypes(injected)...) { if !strings.Contains(string(out), sub) { t.Fatalf("输出缺少 %s: %s", sub, out) } } }) } } // toolTypes 把注入清单转为输出应包含的片段断言。 func toolTypes(injected []string) []string { var out []string for _, typ := range injected { out = append(out, `{"type":"`+typ+`"}`) } return out } // TestRespDisableStream 断言流式升级回退把 stream 置 false 且其余字段保留。 func TestRespDisableStream(t *testing.T) { out, err := RespDisableStream([]byte(`{"model":"m","stream":true,"max_output_tokens":64}`)) if err != nil { t.Fatalf("RespDisableStream: %v", err) } if !strings.Contains(string(out), `"stream":false`) || !strings.Contains(string(out), `"max_output_tokens":64`) { t.Fatalf("字段不符: %s", out) } if _, err := RespDisableStream([]byte("x")); err == nil { t.Error("非法 JSON 应报错") } } // TestRespSynthSSEEvents 断言合成事件序列:created(in_progress,空 output)→ // 逐项 output_item.done → completed(完整响应)。 func TestRespSynthSSEEvents(t *testing.T) { payload := []byte(`{"id":"r1","status":"completed","output":[{"type":"reasoning","summary":[]},{"type":"function_call","name":"run","call_id":"c1"}],"usage":{"total_tokens":9}}`) events, err := RespSynthSSEEvents(payload) if err != nil { t.Fatalf("RespSynthSSEEvents: %v", err) } if len(events) != 4 { t.Fatalf("事件数 = %d, want 4", len(events)) } first := string(events[0]) if !strings.Contains(first, `"type":"response.created"`) || !strings.Contains(first, `"status":"in_progress"`) || !strings.Contains(first, `"output":[]`) { t.Errorf("created 事件不符: %s", first) } if !strings.Contains(string(events[1]), `"type":"response.output_item.done"`) || !strings.Contains(string(events[1]), `"output_index":0`) { t.Errorf("item.done 事件不符: %s", events[1]) } if !strings.Contains(string(events[2]), `"name":"run"`) { t.Errorf("第二项应为 function_call: %s", events[2]) } last := string(events[3]) if !strings.Contains(last, `"type":"response.completed"`) || !strings.Contains(last, `"status":"completed"`) || !strings.Contains(last, `"total_tokens":9`) || !strings.Contains(last, `"name":"run"`) { t.Errorf("completed 事件不符: %s", last) } if _, err := RespSynthSSEEvents([]byte("x")); err == nil { t.Error("非法 JSON 应报错") } } // TestRespCompatCustomTools 断言 custom 工具转换:apply_patch 丢弃,其余转 // function 并补 input 包装 schema、记入 CustomNames;format 字段移除。 func TestRespCompatCustomTools(t *testing.T) { raw := `{"model":"m","tools":[ {"type":"custom","name":"apply_patch","description":"edit files"}, {"type":"custom","name":"run_script","description":"run it","format":{"type":"grammar"}}, {"type":"function","name":"exec","parameters":{}}]}` out, compat, err := RespPassthroughBody([]byte(raw)) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } if !slices.Equal(compat.Dropped, []string{"custom:apply_patch"}) { t.Errorf("Dropped = %v", compat.Dropped) } if !slices.Equal(compat.Converted, []string{"custom:run_script"}) { t.Errorf("Converted = %v", compat.Converted) } if _, ok := compat.CustomNames["run_script"]; !ok { t.Errorf("CustomNames 缺 run_script: %v", compat.CustomNames) } s := string(out) if strings.Contains(s, "apply_patch") || strings.Contains(s, `"type":"custom"`) || strings.Contains(s, "grammar") { t.Errorf("apply_patch/custom/format 应消失: %s", s) } if !strings.Contains(s, `"required":["input"]`) || !strings.Contains(s, `"input":{"type":"string"}`) { t.Errorf("run_script 应补 input 包装 schema: %s", s) } } // TestRespCompatCustomHistory 断言 input 历史的 custom_tool_call(_output) 转换 // 与 arguments 包装三分支。 func TestRespCompatCustomHistory(t *testing.T) { raw := `{"model":"m","input":[ {"type":"custom_tool_call","call_id":"c1","name":"run_script","input":"plain text"}, {"type":"custom_tool_call","call_id":"c2","name":"run_script","input":"{\"a\":1}"}, {"type":"custom_tool_call","call_id":"c3","name":"run_script"}, {"type":"custom_tool_call_output","call_id":"c1","output":"done"}, {"type":"custom_tool_call_output","call_id":"c2","output":{"ok":true}}]}` out, _, err := RespPassthroughBody([]byte(raw)) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } s := string(out) if strings.Contains(s, "custom_tool_call") { t.Fatalf("custom_tool_call 系应全部转换: %s", s) } for _, want := range []string{ `"arguments":"{\"input\":\"plain text\"}"`, `"arguments":"{\"a\":1}"`, `"arguments":"{}"`, `"output":"done"`, `"output":"{\"ok\":true}"`, } { if !strings.Contains(s, want) { t.Errorf("应包含 %s: %s", want, s) } } } // TestRespRestoreCustomToolCalls 断言响应侧回转:命中 CustomNames 的 // function_call 变 custom_tool_call 且 input 解包;流式事件同样生效。 func TestRespRestoreCustomToolCalls(t *testing.T) { compat := RespCompat{CustomNames: map[string]struct{}{"run_script": {}}} payload := []byte(`{"output":[{"type":"function_call","name":"run_script","call_id":"c1","arguments":"{\"input\":\"echo hi\"}"},{"type":"function_call","name":"exec","call_id":"c2","arguments":"{}"}]}`) s := string(RespRestoreToolCalls(payload, compat)) if !strings.Contains(s, `"type":"custom_tool_call"`) || !strings.Contains(s, `"input":"echo hi"`) { t.Errorf("应回转 custom_tool_call 并解包 input: %s", s) } if strings.Contains(s, `"arguments":"{\"input\":\"echo hi\"}"`) { t.Errorf("arguments 应删除: %s", s) } if !strings.Contains(s, `"name":"exec"`) || strings.Count(s, "custom_tool_call") != 1 { t.Errorf("非转换名不应动: %s", s) } ev := []byte(`{"type":"response.output_item.done","item":{"type":"function_call","name":"run_script","arguments":"not-json"}}`) got, changed := RespRestoreToolCallsEvent(ev, compat) if !changed || !strings.Contains(string(got), `"input":"not-json"`) { t.Errorf("非 JSON arguments 应整串作 input: %s", got) } } // TestRespUnwrapCustomInput 断言解包分支:input 键取值、非串取原文、其余整串。 func TestRespUnwrapCustomInput(t *testing.T) { tests := []struct { name string args any want string }{ {"包装取input", `{"input":"hello"}`, "hello"}, {"input非串取原文", `{"input":{"x":1}}`, `{"x":1}`}, {"无input键整串", `{"cmd":"ls"}`, `{"cmd":"ls"}`}, {"非JSON整串", "raw text", "raw text"}, {"非串类型空串", 42, ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := respUnwrapCustomInput(test.args); got != test.want { t.Fatalf("respUnwrapCustomInput(%v) = %q, want %q", test.args, got, test.want) } }) } } // TestRespCompatToolSearchDrop 断言 tool_search 剥离(拍平后语义冗余,上游不识别)。 func TestRespCompatToolSearchDrop(t *testing.T) { raw := `{"model":"m","tools":[{"type":"tool_search"},{"type":"function","name":"exec"}]}` out, compat, err := RespPassthroughBody([]byte(raw)) if err != nil { t.Fatalf("RespPassthroughBody: %v", err) } if !slices.Equal(compat.Dropped, []string{"tool_search"}) { t.Errorf("Dropped = %v", compat.Dropped) } if strings.Contains(string(out), "tool_search") || !strings.Contains(string(out), `"name":"exec"`) { t.Errorf("tool_search 应剥离且 function 保留: %s", out) } }