mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-21 05:21:20 +00:00
Merge pull request #7910 from liuyh06/codex/fix-responses-namespace-noop
perf(openai): lazily rebuild input when stripping namespaces
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user