From 61e1afdd499b37668fdcda374ea51aac7ec9ee3f Mon Sep 17 00:00:00 2001 From: liuyh06 <3404164523@qq.com> Date: Thu, 8 Oct 2026 01:11:23 +0800 Subject: [PATCH] perf(openai): lazily rebuild input when stripping namespaces --- .../service/openai_responses_namespace.go | 34 ++++++------ ...enai_responses_namespace_benchmark_test.go | 55 +++++++++++++++++++ .../openai_responses_namespace_test.go | 39 +++++++++++++ 3 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 backend/internal/service/openai_responses_namespace_benchmark_test.go diff --git a/backend/internal/service/openai_responses_namespace.go b/backend/internal/service/openai_responses_namespace.go index 2d110a9e05..17ee436064 100644 --- a/backend/internal/service/openai_responses_namespace.go +++ b/backend/internal/service/openai_responses_namespace.go @@ -202,37 +202,39 @@ func stripOpenAIResponsesInputNamespaces(body []byte, keepToolCallNamespaces boo } var rebuilt bytes.Buffer - rebuilt.Grow(len(input.Raw)) - _ = rebuilt.WriteByte('[') changed := false - first := true + // Offsets below are relative to input.Raw, not the full request body. + copyFrom := 0 var stripErr error input.ForEach(func(_, item gjson.Result) bool { - if !first { - _ = rebuilt.WriteByte(',') + if !item.IsObject() || !item.Get("namespace").Exists() || + (keepToolCallNamespaces && isOpenAIResponsesToolCallItemType(item.Get("type").String())) { + return true } - first = false - itemBody := []byte(item.Raw) - // 先判存在再判类型:长历史里绝大多数是 message/reasoning 等不带 namespace - // 的项,这样它们无需再扫一次 type。 - if item.IsObject() && item.Get("namespace").Exists() && - (!keepToolCallNamespaces || !isOpenAIResponsesToolCallItemType(item.Get("type").String())) { - itemBody, stripErr = sjson.DeleteBytes(itemBody, "namespace") - if stripErr != nil { - return false - } + itemBody, err := sjson.DeleteBytes([]byte(item.Raw), "namespace") + if err != nil { + stripErr = err + return false + } + if !changed { + // Allocate only on the first change; copy unchanged spans without + // traversing their items again or converting them to byte slices. + rebuilt.Grow(len(input.Raw)) changed = true } + itemStart := item.Index - input.Index + _, _ = rebuilt.WriteString(input.Raw[copyFrom:itemStart]) _, _ = rebuilt.Write(itemBody) + copyFrom = itemStart + len(item.Raw) return true }) - _ = rebuilt.WriteByte(']') if stripErr != nil { return body, fmt.Errorf("delete OpenAI input namespace: %w", stripErr) } if !changed { return body, nil } + _, _ = rebuilt.WriteString(input.Raw[copyFrom:]) stripped, err := sjson.SetRawBytes(body, "input", rebuilt.Bytes()) if err != nil { return body, fmt.Errorf("replace OpenAI input after namespace deletion: %w", err) diff --git a/backend/internal/service/openai_responses_namespace_benchmark_test.go b/backend/internal/service/openai_responses_namespace_benchmark_test.go new file mode 100644 index 0000000000..9297354b02 --- /dev/null +++ b/backend/internal/service/openai_responses_namespace_benchmark_test.go @@ -0,0 +1,55 @@ +package service + +import ( + "bytes" + "fmt" + "runtime" + "strings" + "testing" + + "github.com/tidwall/gjson" +) + +func BenchmarkStripOpenAIResponsesInputNamespaces(b *testing.B) { + for _, size := range []int{1 << 20, 8 << 20} { + message := `{"type":"message","content":"` + strings.Repeat("x", size) + `"}` + call := `{"type":"function_call","namespace":"files","name":"search"}` + for _, tc := range []struct { + name string + input string + keep bool + stripIndex int + }{ + {"tools_only", message, false, -1}, + {"nested_only", `{"content":{"namespace":"nested"}},` + message, false, -1}, + {"keep_calls", call + "," + message, true, -1}, + {"strip_first", call + "," + message, false, 0}, + {"strip_last", message + "," + call, false, 1}, + } { + b.Run(fmt.Sprintf("%s/%dMiB", tc.name, size>>20), func(b *testing.B) { + body := []byte(`{"tools":[{"type":"namespace","name":"files","tools":[]}],"input":[` + tc.input + `]}`) + out, err := stripOpenAIResponsesInputNamespaces(body, tc.keep) + if err != nil { + b.Fatal(err) + } + if tc.stripIndex < 0 { + if !bytes.Equal(body, out) { + b.Fatal("no-op changed body") + } + } else if !gjson.ValidBytes(out) || gjson.GetBytes(out, fmt.Sprintf("input.%d.namespace", tc.stripIndex)).Exists() { + b.Fatal("namespace was not removed") + } + b.ReportAllocs() + b.SetBytes(int64(len(body))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + out, err = stripOpenAIResponsesInputNamespaces(body, tc.keep) + if err != nil { + b.Fatal(err) + } + runtime.KeepAlive(out) + } + }) + } + } +} diff --git a/backend/internal/service/openai_responses_namespace_test.go b/backend/internal/service/openai_responses_namespace_test.go index 5d847981b7..22364c6265 100644 --- a/backend/internal/service/openai_responses_namespace_test.go +++ b/backend/internal/service/openai_responses_namespace_test.go @@ -2,6 +2,7 @@ package service import ( "strconv" + "strings" "testing" "github.com/stretchr/testify/require" @@ -179,6 +180,8 @@ func TestStripOpenAIResponsesInputNamespaces(t *testing.T) { func TestStripOpenAIResponsesInputNamespacesLeavesOtherShapesByteExact(t *testing.T) { tests := [][]byte{ + []byte(`{"input":[],"tools":[{"type":"namespace"}]}`), + []byte(`{"input":[null,42,"namespace",{"content":{"namespace":"nested"}}]}`), []byte(`{"input":"text","namespace":"top-level"}`), []byte(`{"input":{"namespace":"single-object"}}`), []byte(`{"input":[{"content":{"namespace":"nested-only"}}],"tools":[{"namespace":"keep"}]}`), @@ -192,6 +195,42 @@ func TestStripOpenAIResponsesInputNamespacesLeavesOtherShapesByteExact(t *testin } } +func TestStripOpenAIResponsesInputNamespacesPreservesUnchangedSpans(t *testing.T) { + for _, input := range []string{ + `[ {"namespace":"drop","type":"message"} ]`, + `[ null, 9007199254740993, {"namespace":"drop","type":"message"} ]`, + `[ {"namespace":"drop","type":"message"}, "tail", {"content":{"namespace":"nested"}} ]`, + `[ {"namespace":"drop","type":"message"}, {"type":"function_call","namespace":"keep"}, {"namespace":"drop","type":"message"} ]`, + } { + t.Run(input, func(t *testing.T) { + body := []byte(" \n{\"prefix\":9007199254740993,\"input\":" + input + ",\"tail\":true} \n") + original := append([]byte(nil), body...) + out, err := stripOpenAIResponsesInputNamespaces(body, true) + require.NoError(t, err) + require.True(t, gjson.ValidBytes(out)) + require.Equal(t, strings.ReplaceAll(string(body), `"namespace":"drop",`, ""), string(out)) + require.Equal(t, original, body, "must not mutate the input body") + }) + } +} + +func TestStripOpenAIResponsesInputNamespacesNullAndKeptPrefix(t *testing.T) { + for _, keep := range []bool{false, true} { + body := []byte(`{"input":[null,42,"keep",{"type":"function_call","namespace":"files"},{"type":"message","namespace":null,"content":"keep"}]}`) + out, err := stripOpenAIResponsesInputNamespaces(body, keep) + require.NoError(t, err) + require.True(t, gjson.ValidBytes(out)) + require.Equal(t, 5, len(gjson.GetBytes(out, "input").Array())) + for index := 0; index < 3; index++ { + path := "input." + strconv.Itoa(index) + require.Equal(t, gjson.GetBytes(body, path).Raw, gjson.GetBytes(out, path).Raw) + } + require.Equal(t, keep, gjson.GetBytes(out, "input.3.namespace").Exists()) + require.False(t, gjson.GetBytes(out, "input.4.namespace").Exists()) + require.Equal(t, "keep", gjson.GetBytes(out, "input.4.content").String()) + } +} + // 保留模式下只有工具调用项留住 namespace:上游按 namespace 解析历史调用, // 而 message / reasoning / 输出项带该字段会被 schema 拒绝。 func TestStripOpenAIResponsesInputNamespacesKeepsToolCallNamespaces(t *testing.T) {