History

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.

Luis Pater committed 2 months ago

Commit 94d6eb51 parent f702bc14 files · +520 −0Browse files

Unified · Split

4 files
  1. config.example.yaml +8 −0
  2. internal/api/middleware/request_logging_test.go +60 −0
  3. internal/api/server_test.go +261 −0
  4. internal/runtime/executor/helps/payload_mutations_test.go +191 −0
config.example.yaml+8−0
@@ -876,3 +876,11 @@ nonstream-keepalive-interval: 0 · 875 unmodified lines
876# params: # JSON paths (gjson/sjson syntax) to remove from the payload876# params: # JSON paths (gjson/sjson syntax) to remove from the payload
877# - "generationConfig.thinkingConfig.thinkingBudget"877# - "generationConfig.thinkingConfig.thinkingBudget"
878# - "generationConfig.responseJsonSchema"878# - "generationConfig.responseJsonSchema"
879# - models:
880# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
881# protocol: "codex"
882# params:
883# # Flat tools array (standard OpenAI format or Codex CLI < 0.154):
884# - 'tools.#(name=="apply_patch")'
885# # Nested additional_tools structure in input[] (Codex CLI >= 0.154 "Responses Lite"):
886# - 'input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#'
internal/api/middleware/request_logging_test.go+60−0
@@ -262,6 +262,66 @@ func TestRequestLoggingMiddlewareCapturesLargeErrorRequestAndDeferredAPIRequest( · 261 unmodified lines
262 }262 }
263}263}
264 264
265func TestRequestLoggingMiddleware_StreamingResponsesUpstreamSections(t *testing.T) {
266 gin.SetMode(gin.TestMode)
267
268 logsDir := t.TempDir()
269 logger := logging.NewFileRequestLogger(true, logsDir, "", 10)
270 cfg := &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}}
271
272 router := gin.New()
273 router.Use(RequestLoggingMiddleware(logger))
274 router.POST("/v1/responses", func(c *gin.Context) {
275 c.Header("Content-Type", "text/event-stream")
276 executorCtx := context.WithValue(context.Background(), "gin", c)
277 helps.RecordAPIRequest(executorCtx, cfg, helps.UpstreamRequestLog{
278 URL: "https://api.example.com/v1/responses",
279 Method: http.MethodPost,
280 Headers: http.Header{"Content-Type": []string{"application/json"}},
281 Body: []byte(`{"model":"gpt-5-codex","input":[]}`),
282 })
283 helps.AppendAPIResponseChunk(executorCtx, cfg, []byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
284 _, _ = c.Writer.Write([]byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
285 if flusher, ok := c.Writer.(http.Flusher); ok {
286 flusher.Flush()
287 }
288 })
289
290 request := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5-codex","input":[],"stream":true}`))
291 request.Header.Set("Content-Type", "application/json")
292 response := httptest.NewRecorder()
293 router.ServeHTTP(response, request)
294
295 if response.Code != http.StatusOK {
296 t.Fatalf("response status = %d, want %d", response.Code, http.StatusOK)
297 }
298
299 entries, errReadDir := os.ReadDir(logsDir)
300 if errReadDir != nil {
301 t.Fatalf("read logs dir: %v", errReadDir)
302 }
303 var logPath string
304 for _, entry := range entries {
305 if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
306 logPath = logsDir + string(os.PathSeparator) + entry.Name()
307 break
308 }
309 }
310 if logPath == "" {
311 t.Fatal("streaming request log was not created")
312 }
313 content, errReadLog := os.ReadFile(logPath)
314 if errReadLog != nil {
315 t.Fatalf("read log file: %v", errReadLog)
316 }
317 if !bytes.Contains(content, []byte("=== API REQUEST 1 ===")) {
318 t.Fatalf("streaming log missing API REQUEST: %s", string(content))
319 }
320 if !bytes.Contains(content, []byte("=== API RESPONSE 1 ===")) {
321 t.Fatalf("streaming log missing API RESPONSE: %s", string(content))
322 }
323}
324
265func TestAttachRequestLogSourcesUsesLoggerLogsDir(t *testing.T) {325func TestAttachRequestLogSourcesUsesLoggerLogsDir(t *testing.T) {
266 gin.SetMode(gin.TestMode)326 gin.SetMode(gin.TestMode)
267 327
internal/api/server_test.go+261−0
@@ -26,6 +26,7 @@ import ( · 25 unmodified lines
26 "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"26 "github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
27 "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"27 "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
28 "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"28 "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
29 runtimehelps "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
29 sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"30 sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
30 "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"31 "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
31 "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"32 "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
@@ -2707,3 +2708,263 @@ func TestUpdateClientsContext_AntigravityConnectionPoolPurgesTransports(t *testi · 2675 unmodified lines
2707 t.Fatalf("AntigravityTransportsLen() after reload = %d, want 0", got)2708 t.Fatalf("AntigravityTransportsLen() after reload = %d, want 0", got)
2708 }2709 }
2709}2710}
2711
2712type mockServerStreamingCaptureExecutor struct {
2713 cfg *proxyconfig.Config
2714 capturedCtx context.Context
2715 executionCalls int
2716}
2717
2718func (e *mockServerStreamingCaptureExecutor) Identifier() string { return "codex-test" }
2719
2720func (e *mockServerStreamingCaptureExecutor) Execute(ctx context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
2721 e.capturedCtx = ctx
2722 return coreexecutor.Response{Payload: []byte(`{"id":"resp-1","status":"completed"}`)}, nil
2723}
2724
2725func (e *mockServerStreamingCaptureExecutor) ExecuteStream(ctx context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
2726 e.capturedCtx = ctx
2727 e.executionCalls++
2728
2729 runtimehelps.RecordAPIRequest(ctx, e.cfg, runtimehelps.UpstreamRequestLog{
2730 URL: "https://api.example.com/v1/responses",
2731 Method: http.MethodPost,
2732 Headers: http.Header{"Content-Type": []string{"application/json"}},
2733 Body: []byte(`{"model":"gpt-5-codex","input":[]}`),
2734 })
2735
2736 ch := make(chan coreexecutor.StreamChunk, 2)
2737 chunkPayload := []byte("event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\"}\n\n")
2738 runtimehelps.AppendAPIResponseChunk(ctx, e.cfg, chunkPayload)
2739 ch <- coreexecutor.StreamChunk{Payload: chunkPayload}
2740
2741 terminalPayload := []byte("event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n")
2742 runtimehelps.AppendAPIResponseChunk(ctx, e.cfg, terminalPayload)
2743 ch <- coreexecutor.StreamChunk{Payload: terminalPayload}
2744 close(ch)
2745
2746 return &coreexecutor.StreamResult{Chunks: ch}, nil
2747}
2748
2749func (e *mockServerStreamingCaptureExecutor) Refresh(_ context.Context, auth *auth.Auth) (*auth.Auth, error) {
2750 return auth, nil
2751}
2752
2753func (e *mockServerStreamingCaptureExecutor) CountTokens(_ context.Context, _ *auth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
2754 return coreexecutor.Response{}, errors.New("not implemented")
2755}
2756
2757func (e *mockServerStreamingCaptureExecutor) HttpRequest(_ context.Context, _ *auth.Auth, _ *http.Request) (*http.Response, error) {
2758 return nil, errors.New("not implemented")
2759}
2760
2761func TestServerResponsesStreamingRequestLogCapturesUpstreamSections(t *testing.T) {
2762 gin.SetMode(gin.TestMode)
2763
2764 tmpDir := t.TempDir()
2765 logsDir := filepath.Join(tmpDir, "logs")
2766 if err := os.MkdirAll(logsDir, 0o700); err != nil {
2767 t.Fatalf("failed to create logs dir: %v", err)
2768 }
2769
2770 mockLogger := internallogging.NewFileRequestLogger(true, logsDir, "", 10)
2771 server := newTestServerWithOptions(t, WithRequestLoggerFactory(func(*proxyconfig.Config, string) internallogging.RequestLogger {
2772 return mockLogger
2773 }))
2774 server.cfg.RequestLog = true
2775 server.cfg.LoggingToFile = true
2776
2777 mockExec := &mockServerStreamingCaptureExecutor{cfg: server.cfg}
2778 server.handlers.AuthManager.RegisterExecutor(mockExec)
2779
2780 credential := &auth.Auth{
2781 ID: "codex-stream-auth",
2782 Provider: mockExec.Identifier(),
2783 Status: auth.StatusActive,
2784 }
2785 if _, err := server.handlers.AuthManager.Register(context.Background(), credential); err != nil {
2786 t.Fatalf("register auth: %v", err)
2787 }
2788 registry.GetGlobalRegistry().RegisterClient(credential.ID, credential.Provider, []*registry.ModelInfo{{ID: "gpt-5-codex"}})
2789 t.Cleanup(func() {
2790 registry.GetGlobalRegistry().UnregisterClient(credential.ID)
2791 })
2792
2793 rr := httptest.NewRecorder()
2794 body := `{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":"hi"}],"stream":true}`
2795 req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
2796 req.Header.Set("Authorization", "Bearer test-key")
2797 req.Header.Set("Content-Type", "application/json")
2798 server.engine.ServeHTTP(rr, req)
2799
2800 if rr.Code != http.StatusOK {
2801 t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
2802 }
2803 if mockExec.executionCalls != 1 {
2804 t.Fatalf("executor calls = %d, want 1", mockExec.executionCalls)
2805 }
2806
2807 entries, errReadDir := os.ReadDir(logsDir)
2808 if errReadDir != nil {
2809 t.Fatalf("read logs dir: %v", errReadDir)
2810 }
2811 var logPath string
2812 for _, entry := range entries {
2813 if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
2814 logPath = filepath.Join(logsDir, entry.Name())
2815 break
2816 }
2817 }
2818 if logPath == "" {
2819 t.Fatal("streaming request log was not created in logs dir")
2820 }
2821 content, errReadLog := os.ReadFile(logPath)
2822 if errReadLog != nil {
2823 t.Fatalf("read log file: %v", errReadLog)
2824 }
2825 logText := string(content)
2826 apiRequestIdx := strings.Index(logText, "=== API REQUEST 1 ===")
2827 if apiRequestIdx == -1 {
2828 t.Fatalf("streaming log missing API REQUEST 1:\n%s", logText)
2829 }
2830 apiResponseIdx := strings.Index(logText, "=== API RESPONSE 1 ===")
2831 if apiResponseIdx == -1 {
2832 t.Fatalf("streaming log missing API RESPONSE 1:\n%s", logText)
2833 }
2834 downstreamResponseIdx := strings.Index(logText, "=== RESPONSE ===")
2835 if downstreamResponseIdx == -1 {
2836 t.Fatalf("streaming log missing downstream RESPONSE:\n%s", logText)
2837 }
2838 if apiRequestIdx >= apiResponseIdx || apiResponseIdx >= downstreamResponseIdx {
2839 t.Fatalf("unexpected section order (req=%d, apiResp=%d, resp=%d):\n%s", apiRequestIdx, apiResponseIdx, downstreamResponseIdx, logText)
2840 }
2841 apiRequestSection := logText[apiRequestIdx:apiResponseIdx]
2842 apiResponseSection := logText[apiResponseIdx:downstreamResponseIdx]
2843 if !strings.Contains(apiRequestSection, "https://api.example.com/v1/responses") {
2844 t.Fatalf("API REQUEST section missing upstream URL:\n%s", apiRequestSection)
2845 }
2846 if !strings.Contains(apiRequestSection, `"gpt-5-codex"`) {
2847 t.Fatalf("API REQUEST section missing request body:\n%s", apiRequestSection)
2848 }
2849 if !strings.Contains(apiResponseSection, "response.output_item.added") {
2850 t.Fatalf("API RESPONSE section missing response chunk data:\n%s", apiResponseSection)
2851 }
2852}
2853
2854func TestServerCodexAPIKeyResponsesStreamingRequestLog(t *testing.T) {
2855 gin.SetMode(gin.TestMode)
2856
2857 upstreamReceived := make(chan struct{}, 1)
2858 mockUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2859 select {
2860 case upstreamReceived <- struct{}{}:
2861 default:
2862 }
2863 w.Header().Set("Content-Type", "text/event-stream")
2864 w.WriteHeader(http.StatusOK)
2865 _, _ = w.Write([]byte("data: {\"type\":\"response.output_item.added\"}\n\n"))
2866 if f, ok := w.(http.Flusher); ok {
2867 f.Flush()
2868 }
2869 _, _ = w.Write([]byte("data: {\"type\":\"response.completed\"}\n\n"))
2870 if f, ok := w.(http.Flusher); ok {
2871 f.Flush()
2872 }
2873 }))
2874 defer mockUpstream.Close()
2875
2876 tmpDir := t.TempDir()
2877 logsDir := filepath.Join(tmpDir, "logs")
2878 if err := os.MkdirAll(logsDir, 0o700); err != nil {
2879 t.Fatalf("failed to create logs dir: %v", err)
2880 }
2881
2882 mockLogger := internallogging.NewFileRequestLogger(true, logsDir, "", 10)
2883 server := newTestServerWithOptions(t, WithRequestLoggerFactory(func(*proxyconfig.Config, string) internallogging.RequestLogger {
2884 return mockLogger
2885 }))
2886 server.cfg.RequestLog = true
2887 server.cfg.LoggingToFile = true
2888
2889 codexExec := executor.NewCodexExecutor(server.cfg)
2890 server.handlers.AuthManager.RegisterExecutor(codexExec)
2891
2892 credential := &auth.Auth{
2893 ID: "codex-api-key-test",
2894 Provider: "codex",
2895 Status: auth.StatusActive,
2896 Attributes: map[string]string{
2897 auth.AttributeAPIKey: "test-codex-key",
2898 "base_url": mockUpstream.URL,
2899 },
2900 }
2901 if _, err := server.handlers.AuthManager.Register(context.Background(), credential); err != nil {
2902 t.Fatalf("register auth: %v", err)
2903 }
2904 registry.GetGlobalRegistry().RegisterClient(credential.ID, credential.Provider, []*registry.ModelInfo{{ID: "gpt-5-codex"}})
2905 t.Cleanup(func() {
2906 registry.GetGlobalRegistry().UnregisterClient(credential.ID)
2907 })
2908
2909 rr := httptest.NewRecorder()
2910 body := `{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":"hello"}],"stream":true}`
2911 req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
2912 req.Header.Set("Authorization", "Bearer test-key")
2913 req.Header.Set("Content-Type", "application/json")
2914 server.engine.ServeHTTP(rr, req)
2915
2916 if rr.Code != http.StatusOK {
2917 t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
2918 }
2919 select {
2920 case <-upstreamReceived:
2921 case <-time.After(5 * time.Second):
2922 t.Fatal("timeout waiting for upstream request")
2923 }
2924
2925 entries, errReadDir := os.ReadDir(logsDir)
2926 if errReadDir != nil {
2927 t.Fatalf("read logs dir: %v", errReadDir)
2928 }
2929 var logPath string
2930 for _, entry := range entries {
2931 if strings.HasPrefix(entry.Name(), "v1-responses-") && strings.HasSuffix(entry.Name(), ".log") {
2932 logPath = filepath.Join(logsDir, entry.Name())
2933 break
2934 }
2935 }
2936 if logPath == "" {
2937 t.Fatal("streaming request log was not created in logs dir")
2938 }
2939 content, errReadLog := os.ReadFile(logPath)
2940 if errReadLog != nil {
2941 t.Fatalf("read log file: %v", errReadLog)
2942 }
2943 logText := string(content)
2944 apiRequestIdx := strings.Index(logText, "=== API REQUEST 1 ===")
2945 if apiRequestIdx == -1 {
2946 t.Fatalf("streaming log missing API REQUEST 1:\n%s", logText)
2947 }
2948 apiResponseIdx := strings.Index(logText, "=== API RESPONSE 1 ===")
2949 if apiResponseIdx == -1 {
2950 t.Fatalf("streaming log missing API RESPONSE 1:\n%s", logText)
2951 }
2952 downstreamResponseIdx := strings.Index(logText, "=== RESPONSE ===")
2953 if downstreamResponseIdx == -1 {
2954 t.Fatalf("streaming log missing downstream RESPONSE:\n%s", logText)
2955 }
2956 if apiRequestIdx >= apiResponseIdx || apiResponseIdx >= downstreamResponseIdx {
2957 t.Fatalf("unexpected section order (req=%d, apiResp=%d, resp=%d):\n%s", apiRequestIdx, apiResponseIdx, downstreamResponseIdx, logText)
2958 }
2959 apiRequestSection := logText[apiRequestIdx:apiResponseIdx]
2960 apiResponseSection := logText[apiResponseIdx:downstreamResponseIdx]
2961 if !strings.Contains(apiRequestSection, mockUpstream.URL) {
2962 t.Fatalf("API REQUEST section missing upstream URL %s:\n%s", mockUpstream.URL, apiRequestSection)
2963 }
2964 if !strings.Contains(apiRequestSection, `"gpt-5-codex"`) {
2965 t.Fatalf("API REQUEST section missing request body:\n%s", apiRequestSection)
2966 }
2967 if !strings.Contains(apiResponseSection, "response.output_item.added") {
2968 t.Fatalf("API RESPONSE section missing response chunk data:\n%s", apiResponseSection)
2969 }
2970}
internal/runtime/executor/helps/payload_mutations_test.go+191−0
@@ -3,6 +3,7 @@ package helps · 2 unmodified lines
3import (3import (
4 "bytes"4 "bytes"
5 "encoding/json"5 "encoding/json"
6 "os"
6 "strings"7 "strings"
7 "testing"8 "testing"
8 9
@@ -280,3 +281,193 @@ func BenchmarkSetStringIfDifferentLargeCanonicalPayload(b *testing.B) { · 271 unmodified lines
280 benchmarkPayloadMutationOutput = SetStringIfDifferent(input, "model", "gpt-test")281 benchmarkPayloadMutationOutput = SetStringIfDifferent(input, "model", "gpt-test")
281 }282 }
282}283}
284
285func TestConfigExampleDocumentsCodexAdditionalToolsPayloadFilter(t *testing.T) {
286 data, err := os.ReadFile("../../../../config.example.yaml")
287 if err != nil {
288 t.Fatalf("failed to read config.example.yaml: %v", err)
289 }
290 content := string(data)
291 const expectedPath = `'input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#'`
292 if !strings.Contains(content, expectedPath) {
293 t.Fatalf("config.example.yaml does not contain expected additional_tools path: %s", expectedPath)
294 }
295}
296
297func TestApplyPayloadConfig_CodexAdditionalToolsFilter(t *testing.T) {
298 cfg := &config.Config{
299 Payload: config.PayloadConfig{
300 Filter: []config.PayloadFilterRule{
301 {
302 Models: []config.PayloadModelRule{{Name: "gpt-*", Protocol: "codex"}},
303 Params: []string{
304 `input.#(type=="additional_tools")#.tools.#(name=="functions")#.tools.#(name=="apply_patch")#`,
305 },
306 },
307 },
308 },
309 }
310
311 t.Run("removes target tool and preserves other tools across namespaces", func(t *testing.T) {
312 input := []byte(`{
313 "model": "gpt-5-codex",
314 "input": [
315 {
316 "type": "additional_tools",
317 "role": "developer",
318 "tools": [
319 {
320 "type": "namespace",
321 "name": "functions",
322 "tools": [
323 {"type": "custom", "name": "apply_patch", "description": "patch"},
324 {"type": "custom", "name": "exec_command", "description": "exec"}
325 ]
326 },
327 {
328 "type": "namespace",
329 "name": "collaboration",
330 "tools": [
331 {"type": "custom", "name": "share", "description": "share"}
332 ]
333 }
334 ]
335 }
336 ]
337 }`)
338
339 output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
340
341 var structured struct {
342 Input []struct {
343 Type string `json:"type"`
344 Tools []struct {
345 Name string `json:"name"`
346 Tools []struct {
347 Name string `json:"name"`
348 } `json:"tools"`
349 } `json:"tools"`
350 } `json:"input"`
351 }
352 if errUnmarshal := json.Unmarshal(output, &structured); errUnmarshal != nil {
353 t.Fatalf("failed to unmarshal output: %v", errUnmarshal)
354 }
355 if len(structured.Input) != 1 {
356 t.Fatalf("input len = %d, want 1", len(structured.Input))
357 }
358 if len(structured.Input[0].Tools) != 2 {
359 t.Fatalf("namespaces len = %d, want 2", len(structured.Input[0].Tools))
360 }
361 fnNamespace := structured.Input[0].Tools[0]
362 if fnNamespace.Name != "functions" || len(fnNamespace.Tools) != 1 || fnNamespace.Tools[0].Name != "exec_command" {
363 t.Fatalf("functions namespace tools = %+v, want only exec_command", fnNamespace.Tools)
364 }
365 collabNamespace := structured.Input[0].Tools[1]
366 if collabNamespace.Name != "collaboration" || len(collabNamespace.Tools) != 1 || collabNamespace.Tools[0].Name != "share" {
367 t.Fatalf("collaboration namespace tools = %+v, want share", collabNamespace.Tools)
368 }
369 })
370
371 t.Run("non-matching target tool is a no-op", func(t *testing.T) {
372 input := []byte(`{
373 "model": "gpt-5-codex",
374 "input": [
375 {
376 "type": "additional_tools",
377 "role": "developer",
378 "tools": [
379 {
380 "type": "namespace",
381 "name": "functions",
382 "tools": [
383 {"type": "custom", "name": "exec_command", "description": "exec"}
384 ]
385 }
386 ]
387 }
388 ]
389 }`)
390
391 output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
392 if !bytes.Equal(output, input) {
393 t.Fatalf("expected payload to be untouched when rule does not match, got: %s", string(output))
394 }
395 })
396
397 t.Run("removes matches across multiple additional_tools elements", func(t *testing.T) {
398 input := []byte(`{
399 "model": "gpt-5-codex",
400 "input": [
401 {
402 "type": "additional_tools",
403 "tools": [
404 {
405 "type": "namespace",
406 "name": "functions",
407 "tools": [{"type": "custom", "name": "apply_patch"}]
408 }
409 ]
410 },
411 {
412 "type": "message",
413 "role": "user",
414 "content": "hello"
415 },
416 {
417 "type": "additional_tools",
418 "tools": [
419 {
420 "type": "namespace",
421 "name": "functions",
422 "tools": [
423 {"type": "custom", "name": "apply_patch"},
424 {"type": "custom", "name": "view_image"}
425 ]
426 }
427 ]
428 }
429 ]
430 }`)
431
432 output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
433 if strings.Contains(string(output), "apply_patch") {
434 t.Fatalf("apply_patch remained after multi-element filter: %s", string(output))
435 }
436 if !strings.Contains(string(output), "view_image") {
437 t.Fatalf("view_image was removed: %s", string(output))
438 }
439 if !strings.Contains(string(output), "hello") {
440 t.Fatalf("user message was removed: %s", string(output))
441 }
442 })
443
444 t.Run("removes multiple matches within the same namespace array", func(t *testing.T) {
445 input := []byte(`{
446 "model": "gpt-5-codex",
447 "input": [
448 {
449 "type": "additional_tools",
450 "tools": [
451 {
452 "type": "namespace",
453 "name": "functions",
454 "tools": [
455 {"type": "custom", "name": "apply_patch", "id": "p1"},
456 {"type": "custom", "name": "exec_command"},
457 {"type": "custom", "name": "apply_patch", "id": "p2"}
458 ]
459 }
460 ]
461 }
462 ]
463 }`)
464
465 output := ApplyPayloadConfigWithRoot(cfg, "gpt-5-codex", "codex", "", input, nil, "", "")
466 if strings.Contains(string(output), "apply_patch") {
467 t.Fatalf("apply_patch remained after multi-match array filter: %s", string(output))
468 }
469 if !strings.Contains(string(output), "exec_command") {
470 t.Fatalf("exec_command was removed: %s", string(output))
471 }
472 })
473}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t