Merge pull request #7910 from liuyh06/codex/fix-responses-namespace-noop

perf(openai): lazily rebuild input when stripping namespaces
This commit is contained in:
Wesley Liddick
2026-10-08 11:28:02 +08:00
committed by GitHub
3 changed files with 112 additions and 16 deletions
@@ -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)
@@ -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)
}
})
}
}
}
@@ -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) {