From 39b9a38fbc5526b1a97b19d107398778fdd69791 Mon Sep 17 00:00:00 2001 From: MonsterQiu <72pgstan@gmail.com> Date: Tue, 31 Mar 2026 10:32:39 +0800 Subject: [PATCH] fix(codex): normalize null instructions across responses paths --- internal/runtime/executor/codex_executor.go | 21 +++--- .../codex_executor_instructions_test.go | 69 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index bd5ef00b3..c41af0321 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -114,10 +114,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.DeleteBytes(body, "stream_options") - instructions := gjson.GetBytes(body, "instructions") - if !instructions.Exists() || instructions.Type == gjson.Null { - body, _ = sjson.SetBytes(body, "instructions", "") - } + body = normalizeCodexInstructions(body) url := strings.TrimSuffix(baseURL, "/") + "/responses" httpReq, err := e.cacheHelper(ctx, from, url, req, body) @@ -315,9 +312,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.DeleteBytes(body, "stream_options") body, _ = sjson.SetBytes(body, "model", baseModel) - if !gjson.GetBytes(body, "instructions").Exists() { - body, _ = sjson.SetBytes(body, "instructions", "") - } + body = normalizeCodexInstructions(body) url := strings.TrimSuffix(baseURL, "/") + "/responses" httpReq, err := e.cacheHelper(ctx, from, url, req, body) @@ -420,9 +415,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.DeleteBytes(body, "stream_options") body, _ = sjson.SetBytes(body, "stream", false) - if !gjson.GetBytes(body, "instructions").Exists() { - body, _ = sjson.SetBytes(body, "instructions", "") - } + body = normalizeCodexInstructions(body) enc, err := tokenizerForCodexModel(baseModel) if err != nil { @@ -700,6 +693,14 @@ func newCodexStatusErr(statusCode int, body []byte) statusErr { return err } +func normalizeCodexInstructions(body []byte) []byte { + instructions := gjson.GetBytes(body, "instructions") + if !instructions.Exists() || instructions.Type == gjson.Null { + body, _ = sjson.SetBytes(body, "instructions", "") + } + return body +} + func isCodexModelCapacityError(errorBody []byte) bool { if len(errorBody) == 0 { return false diff --git a/internal/runtime/executor/codex_executor_instructions_test.go b/internal/runtime/executor/codex_executor_instructions_test.go index 0ed791c05..c5dc5aa81 100644 --- a/internal/runtime/executor/codex_executor_instructions_test.go +++ b/internal/runtime/executor/codex_executor_instructions_test.go @@ -52,3 +52,72 @@ func TestCodexExecutorExecuteNormalizesNullInstructions(t *testing.T) { t.Fatalf("instructions = %q, want empty string", gjson.GetBytes(gotBody, "instructions").String()) } } + +func TestCodexExecutorExecuteStreamNormalizesNullInstructions(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", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":0,\"status\":\"completed\",\"background\":false,\"error\":null}}\n\n")) + })) + defer server.Close() + + executor := NewCodexExecutor(&config.Config{}) + auth := &cliproxyauth.Auth{Attributes: map[string]string{ + "base_url": server.URL, + "api_key": "test", + }} + + result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{"model":"gpt-5.4","instructions":null,"input":"hello"}`), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + Stream: true, + }) + if err != nil { + t.Fatalf("ExecuteStream error: %v", err) + } + for range result.Chunks { + } + if gotPath != "/responses" { + t.Fatalf("path = %q, want %q", gotPath, "/responses") + } + 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()) + } +} + +func TestCodexExecutorCountTokensTreatsNullInstructionsAsEmpty(t *testing.T) { + executor := NewCodexExecutor(&config.Config{}) + + nullResp, err := executor.CountTokens(context.Background(), nil, cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{"model":"gpt-5.4","instructions":null,"input":"hello"}`), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + }) + if err != nil { + t.Fatalf("CountTokens(null) error: %v", err) + } + + emptyResp, err := executor.CountTokens(context.Background(), nil, cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{"model":"gpt-5.4","instructions":"","input":"hello"}`), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + }) + if err != nil { + t.Fatalf("CountTokens(empty) error: %v", err) + } + + if string(nullResp.Payload) != string(emptyResp.Payload) { + t.Fatalf("token count payload mismatch:\nnull=%s\nempty=%s", string(nullResp.Payload), string(emptyResp.Payload)) + } +}