494 lines
20 KiB
Go
494 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|