diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index c41af032..989a6b93 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -219,6 +219,10 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.DeleteBytes(body, "stream") + instructions := gjson.GetBytes(body, "instructions") + if !instructions.Exists() || instructions.Type == gjson.Null { + body, _ = sjson.SetBytes(body, "instructions", "") + } url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" httpReq, err := e.cacheHelper(ctx, from, url, req, body) diff --git a/internal/runtime/executor/codex_executor_compact_test.go b/internal/runtime/executor/codex_executor_compact_test.go new file mode 100644 index 00000000..02c6db29 --- /dev/null +++ b/internal/runtime/executor/codex_executor_compact_test.go @@ -0,0 +1,79 @@ +package executor + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/tidwall/gjson" +) + +func TestCodexExecutorCompactAddsDefaultInstructions(t *testing.T) { + cases := []struct { + name string + payload string + }{ + { + name: "missing instructions", + payload: `{"model":"gpt-5.4","input":"hello"}`, + }, + { + name: "null instructions", + payload: `{"model":"gpt-5.4","instructions":null,"input":"hello"}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var gotPath string + var gotBody []byte + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + body, _ := io.ReadAll(r.Body) + gotBody = body + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)) + })) + defer server.Close() + + executor := NewCodexExecutor(&config.Config{}) + auth := &cliproxyauth.Auth{Attributes: map[string]string{ + "base_url": server.URL, + "api_key": "test", + }} + + resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(tc.payload), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + Alt: "responses/compact", + Stream: false, + }) + if err != nil { + t.Fatalf("Execute error: %v", err) + } + if gotPath != "/responses/compact" { + t.Fatalf("path = %q, want %q", gotPath, "/responses/compact") + } + if !gjson.GetBytes(gotBody, "instructions").Exists() { + t.Fatalf("expected instructions in compact request body, got %s", string(gotBody)) + } + if gjson.GetBytes(gotBody, "instructions").Type != gjson.String { + t.Fatalf("instructions type = %v, want string", gjson.GetBytes(gotBody, "instructions").Type) + } + if gjson.GetBytes(gotBody, "instructions").String() != "" { + t.Fatalf("instructions = %q, want empty string", gjson.GetBytes(gotBody, "instructions").String()) + } + if string(resp.Payload) != `{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}` { + t.Fatalf("payload = %s", string(resp.Payload)) + } + }) + } +}