mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-04 06:41:39 +08:00
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
This commit is contained in:
@@ -876,3 +876,11 @@ nonstream-keepalive-interval: 0
|
||||
# params: # JSON paths (gjson/sjson syntax) to remove from the payload
|
||||
# - "generationConfig.thinkingConfig.thinkingBudget"
|
||||
# - "generationConfig.responseJsonSchema"
|
||||
# - models:
|
||||
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
|
||||
# protocol: "codex"
|
||||
# params:
|
||||
# # Flat tools array (standard OpenAI format or Codex CLI < 0.154):
|
||||
# - 'tools.#(name=="apply_patch")'
|
||||
# # Nested additional_tools structure in input[] (Codex CLI >= 0.154 "Responses Lite"):
|
||||
# - 'input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#'
|
||||
|
||||
@@ -262,6 +262,66 @@ func TestRequestLoggingMiddlewareCapturesLargeErrorRequestAndDeferredAPIRequest(
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestLoggingMiddleware_StreamingResponsesUpstreamSections(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
logsDir := t.TempDir()
|
||||
logger := logging.NewFileRequestLogger(true, logsDir, "", 10)
|
||||
cfg := &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(RequestLoggingMiddleware(logger))
|
||||
router.POST("/v1/responses", func(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
executorCtx := context.WithValue(context.Background(), "gin", c)
|
||||
helps.RecordAPIRequest(executorCtx, cfg, helps.UpstreamRequestLog{
|
||||
URL: "https://api.example.com/v1/responses",
|
||||
Method: http.MethodPost,
|
||||
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: []byte(`{"model":"gpt-5-codex","input":[]}`),
|
||||
})
|
||||
helps.AppendAPIResponseChunk(executorCtx, cfg, []byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
|
||||
_, _ = c.Writer.Write([]byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
|
||||
if flusher, ok := c.Writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
})
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5-codex","input":[],"stream":true}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("response status = %d, want %d", response.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
entries, errReadDir := os.ReadDir(logsDir)
|
||||
if errReadDir != nil {
|
||||
t.Fatalf("read logs dir: %v", errReadDir)
|
||||
}
|
||||
var logPath string
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logPath = logsDir + string(os.PathSeparator) + entry.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
if logPath == "" {
|
||||
t.Fatal("streaming request log was not created")
|
||||
}
|
||||
content, errReadLog := os.ReadFile(logPath)
|
||||
if errReadLog != nil {
|
||||
t.Fatalf("read log file: %v", errReadLog)
|
||||
}
|
||||
if !bytes.Contains(content, []byte("=== API REQUEST 1 ===")) {
|
||||
t.Fatalf("streaming log missing API REQUEST: %s", string(content))
|
||||
}
|
||||
if !bytes.Contains(content, []byte("=== API RESPONSE 1 ===")) {
|
||||
t.Fatalf("streaming log missing API RESPONSE: %s", string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachRequestLogSourcesUsesLoggerLogsDir(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
|
||||
runtimehelps "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
|
||||
@@ -2707,3 +2708,263 @@ func TestUpdateClientsContext_AntigravityConnectionPoolPurgesTransports(t *testi
|
||||
t.Fatalf("AntigravityTransportsLen() after reload = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
type mockServerStreamingCaptureExecutor struct {
|
||||
cfg *proxyconfig.Config
|
||||
capturedCtx context.Context
|
||||
executionCalls int
|
||||
}
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) Identifier() string { return "codex-test" }
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) Execute(ctx context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
|
||||
e.capturedCtx = ctx
|
||||
return coreexecutor.Response{Payload: []byte(`{"id":"resp-1","status":"completed"}`)}, nil
|
||||
}
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) ExecuteStream(ctx context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
|
||||
e.capturedCtx = ctx
|
||||
e.executionCalls++
|
||||
|
||||
runtimehelps.RecordAPIRequest(ctx, e.cfg, runtimehelps.UpstreamRequestLog{
|
||||
URL: "https://api.example.com/v1/responses",
|
||||
Method: http.MethodPost,
|
||||
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: []byte(`{"model":"gpt-5-codex","input":[]}`),
|
||||
})
|
||||
|
||||
ch := make(chan coreexecutor.StreamChunk, 2)
|
||||
chunkPayload := []byte("event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\"}\n\n")
|
||||
runtimehelps.AppendAPIResponseChunk(ctx, e.cfg, chunkPayload)
|
||||
ch <- coreexecutor.StreamChunk{Payload: chunkPayload}
|
||||
|
||||
terminalPayload := []byte("event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n")
|
||||
runtimehelps.AppendAPIResponseChunk(ctx, e.cfg, terminalPayload)
|
||||
ch <- coreexecutor.StreamChunk{Payload: terminalPayload}
|
||||
close(ch)
|
||||
|
||||
return &coreexecutor.StreamResult{Chunks: ch}, nil
|
||||
}
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) Refresh(_ context.Context, auth *auth.Auth) (*auth.Auth, error) {
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) CountTokens(_ context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
|
||||
return coreexecutor.Response{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (e *mockServerStreamingCaptureExecutor) HttpRequest(_ context.Context, _ *auth.Auth, _ *http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func TestServerResponsesStreamingRequestLogCapturesUpstreamSections(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
logsDir := filepath.Join(tmpDir, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0o700); err != nil {
|
||||
t.Fatalf("failed to create logs dir: %v", err)
|
||||
}
|
||||
|
||||
mockLogger := internallogging.NewFileRequestLogger(true, logsDir, "", 10)
|
||||
server := newTestServerWithOptions(t, WithRequestLoggerFactory(func(*proxyconfig.Config, string) internallogging.RequestLogger {
|
||||
return mockLogger
|
||||
}))
|
||||
server.cfg.RequestLog = true
|
||||
server.cfg.LoggingToFile = true
|
||||
|
||||
mockExec := &mockServerStreamingCaptureExecutor{cfg: server.cfg}
|
||||
server.handlers.AuthManager.RegisterExecutor(mockExec)
|
||||
|
||||
credential := &auth.Auth{
|
||||
ID: "codex-stream-auth",
|
||||
Provider: mockExec.Identifier(),
|
||||
Status: auth.StatusActive,
|
||||
}
|
||||
if _, err := server.handlers.AuthManager.Register(context.Background(), credential); err != nil {
|
||||
t.Fatalf("register auth: %v", err)
|
||||
}
|
||||
registry.GetGlobalRegistry().RegisterClient(credential.ID, credential.Provider, []*registry.ModelInfo{{ID: "gpt-5-codex"}})
|
||||
t.Cleanup(func() {
|
||||
registry.GetGlobalRegistry().UnregisterClient(credential.ID)
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
body := `{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":"hi"}],"stream":true}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer test-key")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if mockExec.executionCalls != 1 {
|
||||
t.Fatalf("executor calls = %d, want 1", mockExec.executionCalls)
|
||||
}
|
||||
|
||||
entries, errReadDir := os.ReadDir(logsDir)
|
||||
if errReadDir != nil {
|
||||
t.Fatalf("read logs dir: %v", errReadDir)
|
||||
}
|
||||
var logPath string
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logPath = filepath.Join(logsDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
if logPath == "" {
|
||||
t.Fatal("streaming request log was not created in logs dir")
|
||||
}
|
||||
content, errReadLog := os.ReadFile(logPath)
|
||||
if errReadLog != nil {
|
||||
t.Fatalf("read log file: %v", errReadLog)
|
||||
}
|
||||
logText := string(content)
|
||||
apiRequestIdx := strings.Index(logText, "=== API REQUEST 1 ===")
|
||||
if apiRequestIdx == -1 {
|
||||
t.Fatalf("streaming log missing API REQUEST 1:\n%s", logText)
|
||||
}
|
||||
apiResponseIdx := strings.Index(logText, "=== API RESPONSE 1 ===")
|
||||
if apiResponseIdx == -1 {
|
||||
t.Fatalf("streaming log missing API RESPONSE 1:\n%s", logText)
|
||||
}
|
||||
downstreamResponseIdx := strings.Index(logText, "=== RESPONSE ===")
|
||||
if downstreamResponseIdx == -1 {
|
||||
t.Fatalf("streaming log missing downstream RESPONSE:\n%s", logText)
|
||||
}
|
||||
if apiRequestIdx >= apiResponseIdx || apiResponseIdx >= downstreamResponseIdx {
|
||||
t.Fatalf("unexpected section order (req=%d, apiResp=%d, resp=%d):\n%s", apiRequestIdx, apiResponseIdx, downstreamResponseIdx, logText)
|
||||
}
|
||||
apiRequestSection := logText[apiRequestIdx:apiResponseIdx]
|
||||
apiResponseSection := logText[apiResponseIdx:downstreamResponseIdx]
|
||||
if !strings.Contains(apiRequestSection, "https://api.example.com/v1/responses") {
|
||||
t.Fatalf("API REQUEST section missing upstream URL:\n%s", apiRequestSection)
|
||||
}
|
||||
if !strings.Contains(apiRequestSection, `"gpt-5-codex"`) {
|
||||
t.Fatalf("API REQUEST section missing request body:\n%s", apiRequestSection)
|
||||
}
|
||||
if !strings.Contains(apiResponseSection, "response.output_item.added") {
|
||||
t.Fatalf("API RESPONSE section missing response chunk data:\n%s", apiResponseSection)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerCodexAPIKeyResponsesStreamingRequestLog(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
upstreamReceived := make(chan struct{}, 1)
|
||||
mockUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case upstreamReceived <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\"}\n\n"))
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}))
|
||||
defer mockUpstream.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
logsDir := filepath.Join(tmpDir, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0o700); err != nil {
|
||||
t.Fatalf("failed to create logs dir: %v", err)
|
||||
}
|
||||
|
||||
mockLogger := internallogging.NewFileRequestLogger(true, logsDir, "", 10)
|
||||
server := newTestServerWithOptions(t, WithRequestLoggerFactory(func(*proxyconfig.Config, string) internallogging.RequestLogger {
|
||||
return mockLogger
|
||||
}))
|
||||
server.cfg.RequestLog = true
|
||||
server.cfg.LoggingToFile = true
|
||||
|
||||
codexExec := executor.NewCodexExecutor(server.cfg)
|
||||
server.handlers.AuthManager.RegisterExecutor(codexExec)
|
||||
|
||||
credential := &auth.Auth{
|
||||
ID: "codex-api-key-test",
|
||||
Provider: "codex",
|
||||
Status: auth.StatusActive,
|
||||
Attributes: map[string]string{
|
||||
auth.AttributeAPIKey: "test-codex-key",
|
||||
"base_url": mockUpstream.URL,
|
||||
},
|
||||
}
|
||||
if _, err := server.handlers.AuthManager.Register(context.Background(), credential); err != nil {
|
||||
t.Fatalf("register auth: %v", err)
|
||||
}
|
||||
registry.GetGlobalRegistry().RegisterClient(credential.ID, credential.Provider, []*registry.ModelInfo{{ID: "gpt-5-codex"}})
|
||||
t.Cleanup(func() {
|
||||
registry.GetGlobalRegistry().UnregisterClient(credential.ID)
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
body := `{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":"hello"}],"stream":true}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer test-key")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
select {
|
||||
case <-upstreamReceived:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout waiting for upstream request")
|
||||
}
|
||||
|
||||
entries, errReadDir := os.ReadDir(logsDir)
|
||||
if errReadDir != nil {
|
||||
t.Fatalf("read logs dir: %v", errReadDir)
|
||||
}
|
||||
var logPath string
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logPath = filepath.Join(logsDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
if logPath == "" {
|
||||
t.Fatal("streaming request log was not created in logs dir")
|
||||
}
|
||||
content, errReadLog := os.ReadFile(logPath)
|
||||
if errReadLog != nil {
|
||||
t.Fatalf("read log file: %v", errReadLog)
|
||||
}
|
||||
logText := string(content)
|
||||
apiRequestIdx := strings.Index(logText, "=== API REQUEST 1 ===")
|
||||
if apiRequestIdx == -1 {
|
||||
t.Fatalf("streaming log missing API REQUEST 1:\n%s", logText)
|
||||
}
|
||||
apiResponseIdx := strings.Index(logText, "=== API RESPONSE 1 ===")
|
||||
if apiResponseIdx == -1 {
|
||||
t.Fatalf("streaming log missing API RESPONSE 1:\n%s", logText)
|
||||
}
|
||||
downstreamResponseIdx := strings.Index(logText, "=== RESPONSE ===")
|
||||
if downstreamResponseIdx == -1 {
|
||||
t.Fatalf("streaming log missing downstream RESPONSE:\n%s", logText)
|
||||
}
|
||||
if apiRequestIdx >= apiResponseIdx || apiResponseIdx >= downstreamResponseIdx {
|
||||
t.Fatalf("unexpected section order (req=%d, apiResp=%d, resp=%d):\n%s", apiRequestIdx, apiResponseIdx, downstreamResponseIdx, logText)
|
||||
}
|
||||
apiRequestSection := logText[apiRequestIdx:apiResponseIdx]
|
||||
apiResponseSection := logText[apiResponseIdx:downstreamResponseIdx]
|
||||
if !strings.Contains(apiRequestSection, mockUpstream.URL) {
|
||||
t.Fatalf("API REQUEST section missing upstream URL %s:\n%s", mockUpstream.URL, apiRequestSection)
|
||||
}
|
||||
if !strings.Contains(apiRequestSection, `"gpt-5-codex"`) {
|
||||
t.Fatalf("API REQUEST section missing request body:\n%s", apiRequestSection)
|
||||
}
|
||||
if !strings.Contains(apiResponseSection, "response.output_item.added") {
|
||||
t.Fatalf("API RESPONSE section missing response chunk data:\n%s", apiResponseSection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package helps
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -280,3 +281,193 @@ func BenchmarkSetStringIfDifferentLargeCanonicalPayload(b *testing.B) {
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user