Files
CLIProxyAPI/internal/runtime/executor/helps/payload_mutations_test.go
Luis Pater 94d6eb535e docs(config): document payload filter examples for codex tools
- Add example payload filter rules in `config.example.yaml` for stripping tools from both flat and nested `additional_tools` Codex request payloads.

Closes: #5792
2026-09-13 22:12:28 +08:00

474 lines
16 KiB
Go

package helps
import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/tidwall/gjson"
)
type countingPayloadMarshaler struct {
calls *int
value string
}
func (m countingPayloadMarshaler) MarshalJSON() ([]byte, error) {
*m.calls = *m.calls + 1
return json.Marshal(m.value)
}
func TestSetStringIfDifferentReusesCanonicalValue(t *testing.T) {
input := []byte(`{"model":"gpt-test","messages":[]}`)
output := SetStringIfDifferent(input, "model", "gpt-test")
if &output[0] != &input[0] {
t.Fatal("canonical string caused a payload copy")
}
}
func TestSetStringIfDifferentNormalizesWrongType(t *testing.T) {
input := []byte(`{"model":123}`)
original := bytes.Clone(input)
output := SetStringIfDifferent(input, "model", "123")
model := gjson.GetBytes(output, "model")
if model.Type != gjson.String || model.String() != "123" {
t.Fatalf("model = %s, want string 123", model.Raw)
}
if !bytes.Equal(input, original) {
t.Fatal("input payload was modified in place")
}
}
func TestSetBoolIfDifferentReusesCanonicalValue(t *testing.T) {
input := []byte(`{"stream":true,"input":[]}`)
output := SetBoolIfDifferent(input, "stream", true)
if &output[0] != &input[0] {
t.Fatal("canonical boolean caused a payload copy")
}
}
func TestSetBoolIfDifferentNormalizesWrongType(t *testing.T) {
input := []byte(`{"stream":"true"}`)
output := SetBoolIfDifferent(input, "stream", true)
if stream := gjson.GetBytes(output, "stream"); stream.Type != gjson.True {
t.Fatalf("stream = %s, want boolean true", stream.Raw)
}
}
func TestSetRawIfDifferentReusesIdenticalRawValue(t *testing.T) {
input := []byte(`{"metadata":{"source":"executor"},"input":[]}`)
output := SetRawIfDifferent(input, "metadata", []byte(`{"source":"executor"}`))
if &output[0] != &input[0] {
t.Fatal("identical raw value caused a payload copy")
}
}
func TestSetRawIfDifferentUpdatesDifferentRawValue(t *testing.T) {
input := []byte(`{"metadata":"executor"}`)
output := SetRawIfDifferent(input, "metadata", []byte(`{"source":"executor"}`))
metadata := gjson.GetBytes(output, "metadata")
if !metadata.IsObject() || metadata.Get("source").String() != "executor" {
t.Fatalf("metadata = %s, want object", metadata.Raw)
}
}
func TestApplyPayloadConfigReusesCanonicalOverrides(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "gpt-test", Protocol: "openai"}},
Params: map[string]any{"stream": true, "model": "gpt-test"},
}},
OverrideRaw: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "gpt-test", Protocol: "openai"}},
Params: map[string]any{"metadata": `{"source":"executor"}`},
}},
}}
input := []byte(`{"model":"gpt-test","stream":true,"metadata":{"source":"executor"},"messages":[]}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-test", "openai", "", input, nil, "", "")
if &output[0] != &input[0] {
t.Fatal("canonical payload overrides caused a payload copy")
}
}
func TestApplyPayloadConfigWithRequestTrackedReportsContextManagementTouches(t *testing.T) {
const automatic = `{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}`
modelRules := []config.PayloadModelRule{{Name: "claude-opus-5", Protocol: "claude"}}
originalWithoutContextManagement := []byte(`{"model":"claude-opus-5"}`)
for _, test := range []struct {
name string
payload string
original []byte
payloadConfig config.PayloadConfig
wantTouched bool
}{
{
name: "default",
payload: `{"model":"claude-opus-5"}`,
original: originalWithoutContextManagement,
payloadConfig: config.PayloadConfig{Default: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": map[string]any{"edits": []any{map[string]any{"type": "default"}}}},
}}},
wantTouched: true,
},
{
name: "raw default",
payload: `{"model":"claude-opus-5"}`,
original: originalWithoutContextManagement,
payloadConfig: config.PayloadConfig{DefaultRaw: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": `{"edits":[{"type":"raw_default"}]}`},
}}},
wantTouched: true,
},
{
name: "canonical descendant override",
payload: `{"model":"claude-opus-5","context_management":` + automatic + `}`,
payloadConfig: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management.edits.0.keep": "all"},
}}},
wantTouched: true,
},
{
name: "identical raw override",
payload: `{"model":"claude-opus-5","context_management":` + automatic + `}`,
payloadConfig: config.PayloadConfig{OverrideRaw: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": automatic},
}}},
wantTouched: true,
},
{
name: "filter already absent",
payload: `{"model":"claude-opus-5"}`,
payloadConfig: config.PayloadConfig{Filter: []config.PayloadFilterRule{{
Models: modelRules,
Params: []string{"context_management"},
}}},
wantTouched: true,
},
{
name: "unrelated override",
payload: `{"model":"claude-opus-5"}`,
payloadConfig: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "enabled"},
}}},
},
{
name: "nonmatching override",
payload: `{"model":"claude-opus-5"}`,
payloadConfig: config.PayloadConfig{Override: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "other-model", Protocol: "claude"}},
Params: map[string]any{"context_management": map[string]any{"edits": []any{}}},
}}},
},
{
name: "default skipped for caller owned field",
payload: `{"model":"claude-opus-5","context_management":{"edits":[{"type":"caller"}]}}`,
original: []byte(`{"model":"claude-opus-5","context_management":{"edits":[{"type":"caller"}]}}`),
payloadConfig: config.PayloadConfig{Default: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": map[string]any{"edits": []any{map[string]any{"type": "default"}}}},
}}},
},
} {
t.Run(test.name, func(t *testing.T) {
cfg := &config.Config{Payload: test.payloadConfig}
_, touched := ApplyPayloadConfigWithRequestTracked(cfg, "claude-opus-5", "claude", "claude", "", []byte(test.payload), test.original, "claude-opus-5", "", nil, "context_management")
if touched != test.wantTouched {
t.Fatalf("context_management touched = %t, want %t", touched, test.wantTouched)
}
})
}
}
func TestApplyPayloadConfigProjectionOverrideWritesEveryMatch(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "gpt-test", Protocol: "openai"}},
Params: map[string]any{"items.#.value": []any{1, 2}},
}},
}}
input := []byte(`{"items":[{"value":1},{"value":2}]}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-test", "openai", "", input, nil, "", "")
for _, path := range []string{"items.0.value", "items.1.value"} {
if got := gjson.GetBytes(output, path).Raw; got != `[1,2]` {
t.Fatalf("%s = %s, want [1,2]", path, got)
}
}
}
func TestApplyPayloadConfigProjectionOverrideRawWritesEveryMatch(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{
OverrideRaw: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "gpt-test", Protocol: "openai"}},
Params: map[string]any{"items.#.value": `[1,2]`},
}},
}}
input := []byte(`{"items":[{"value":1},{"value":2}]}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-test", "openai", "", input, nil, "", "")
for _, path := range []string{"items.0.value", "items.1.value"} {
if got := gjson.GetBytes(output, path).Raw; got != `[1,2]` {
t.Fatalf("%s = %s, want [1,2]", path, got)
}
}
}
func TestApplyPayloadConfigNormalizesByteSliceOverride(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: "gpt-test", Protocol: "openai"}},
Params: map[string]any{"value": []byte("abc")},
}},
}}
input := []byte(`{"value":"YWJj"}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-test", "openai", "", input, nil, "", "")
value := gjson.GetBytes(output, "value")
if value.Type != gjson.String || value.String() != "abc" {
t.Fatalf("value = %s, want string abc", value.Raw)
}
}
func TestSetPayloadValueIfDifferentUsesSJSONNumberEncoding(t *testing.T) {
input := []byte(`{"value":1.2}`)
output := setPayloadValueIfDifferent(input, "value", float32(1.2))
if got := gjson.GetBytes(output, "value").Raw; got != "1.2000000476837158" {
t.Fatalf("value = %s, want sjson float32 encoding", got)
}
canonical := []byte(`{"value":1.2000000476837158}`)
reused := setPayloadValueIfDifferent(canonical, "value", float32(1.2))
if &reused[0] != &canonical[0] {
t.Fatal("canonical float32 encoding caused a payload copy")
}
}
func TestSetPayloadValueIfDifferentCallsMarshalerOnce(t *testing.T) {
for _, input := range [][]byte{[]byte(`{"value":"old"}`), []byte(`{"value":"new"}`)} {
calls := 0
value := countingPayloadMarshaler{calls: &calls, value: "new"}
output := setPayloadValueIfDifferent(input, "value", value)
if calls != 1 {
t.Fatalf("MarshalJSON calls = %d, want 1", calls)
}
if got := gjson.GetBytes(output, "value").String(); got != "new" {
t.Fatalf("value = %q, want new", got)
}
}
}
func TestRemoveToolTypeReusesArrayWithoutMatch(t *testing.T) {
input := []byte(`{"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`)
output := removeToolTypeFromToolsArray(input, "tools", "image_generation")
if &output[0] != &input[0] {
t.Fatal("tool filtering without a match caused a payload copy")
}
}
var benchmarkPayloadMutationOutput []byte
func BenchmarkSetStringIfDifferentLargeCanonicalPayload(b *testing.B) {
input := []byte(`{"model":"gpt-test","messages":[{"role":"user","content":"` + strings.Repeat("x", 8<<20) + `"}]}`)
b.ReportAllocs()
b.SetBytes(int64(len(input)))
b.ResetTimer()
for b.Loop() {
benchmarkPayloadMutationOutput = SetStringIfDifferent(input, "model", "gpt-test")
}
}
func TestConfigExampleDocumentsCodexAdditionalToolsPayloadFilter(t *testing.T) {
data, err := os.ReadFile("../../../../config.example.yaml")
if err != nil {
t.Fatalf("failed to read config.example.yaml: %v", err)
}
content := string(data)
const expectedPath = `'input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#'`
if !strings.Contains(content, expectedPath) {
t.Fatalf("config.example.yaml does not contain expected additional_tools path: %s", expectedPath)
}
}
func TestApplyPayloadConfig_CodexAdditionalToolsFilter(t *testing.T) {
cfg := &config.Config{
Payload: config.PayloadConfig{
Filter: []config.PayloadFilterRule{
{
Models: []config.PayloadModelRule{{Name: "gpt-*", Protocol: "codex"}},
Params: []string{
`input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#`,
},
},
},
},
}
t.Run("removes target tool and preserves other tools across namespaces", func(t *testing.T) {
input := []byte(`{
"model": "gpt-5-codex",
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [
{
"type": "namespace",
"name": "functions",
"tools": [
{"type": "custom", "name": "apply_patch", "description": "patch"},
{"type": "custom", "name": "exec_command", "description": "exec"}
]
},
{
"type": "namespace",
"name": "collaboration",
"tools": [
{"type": "custom", "name": "share", "description": "share"}
]
}
]
}
]
}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
var structured struct {
Input []struct {
Type string `json:"type"`
Tools []struct {
Name string `json:"name"`
Tools []struct {
Name string `json:"name"`
} `json:"tools"`
} `json:"tools"`
} `json:"input"`
}
if errUnmarshal := json.Unmarshal(output, &structured); errUnmarshal != nil {
t.Fatalf("failed to unmarshal output: %v", errUnmarshal)
}
if len(structured.Input) != 1 {
t.Fatalf("input len = %d, want 1", len(structured.Input))
}
if len(structured.Input[0].Tools) != 2 {
t.Fatalf("namespaces len = %d, want 2", len(structured.Input[0].Tools))
}
fnNamespace := structured.Input[0].Tools[0]
if fnNamespace.Name != "functions" || len(fnNamespace.Tools) != 1 || fnNamespace.Tools[0].Name != "exec_command" {
t.Fatalf("functions namespace tools = %+v, want only exec_command", fnNamespace.Tools)
}
collabNamespace := structured.Input[0].Tools[1]
if collabNamespace.Name != "collaboration" || len(collabNamespace.Tools) != 1 || collabNamespace.Tools[0].Name != "share" {
t.Fatalf("collaboration namespace tools = %+v, want share", collabNamespace.Tools)
}
})
t.Run("non-matching target tool is a no-op", func(t *testing.T) {
input := []byte(`{
"model": "gpt-5-codex",
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [
{
"type": "namespace",
"name": "functions",
"tools": [
{"type": "custom", "name": "exec_command", "description": "exec"}
]
}
]
}
]
}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
if !bytes.Equal(output, input) {
t.Fatalf("expected payload to be untouched when rule does not match, got: %s", string(output))
}
})
t.Run("removes matches across multiple additional_tools elements", func(t *testing.T) {
input := []byte(`{
"model": "gpt-5-codex",
"input": [
{
"type": "additional_tools",
"tools": [
{
"type": "namespace",
"name": "functions",
"tools": [{"type": "custom", "name": "apply_patch"}]
}
]
},
{
"type": "message",
"role": "user",
"content": "hello"
},
{
"type": "additional_tools",
"tools": [
{
"type": "namespace",
"name": "functions",
"tools": [
{"type": "custom", "name": "apply_patch"},
{"type": "custom", "name": "view_image"}
]
}
]
}
]
}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
if strings.Contains(string(output), "apply_patch") {
t.Fatalf("apply_patch remained after multi-element filter: %s", string(output))
}
if !strings.Contains(string(output), "view_image") {
t.Fatalf("view_image was removed: %s", string(output))
}
if !strings.Contains(string(output), "hello") {
t.Fatalf("user message was removed: %s", string(output))
}
})
t.Run("removes multiple matches within the same namespace array", func(t *testing.T) {
input := []byte(`{
"model": "gpt-5-codex",
"input": [
{
"type": "additional_tools",
"tools": [
{
"type": "namespace",
"name": "functions",
"tools": [
{"type": "custom", "name": "apply_patch", "id": "p1"},
{"type": "custom", "name": "exec_command"},
{"type": "custom", "name": "apply_patch", "id": "p2"}
]
}
]
}
]
}`)
output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
if strings.Contains(string(output), "apply_patch") {
t.Fatalf("apply_patch remained after multi-match array filter: %s", string(output))
}
if !strings.Contains(string(output), "exec_command") {
t.Fatalf("exec_command was removed: %s", string(output))
}
})
}