diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index a0cbc0d5..30092ec7 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -616,6 +616,10 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form if promptCacheKey.Exists() { cache.ID = promptCacheKey.String() } + } else if from == "openai" { + if apiKey := strings.TrimSpace(apiKeyFromContext(ctx)); apiKey != "" { + cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String() + } } if cache.ID != "" { diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go new file mode 100644 index 00000000..d6dca031 --- /dev/null +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -0,0 +1,64 @@ +package executor + +import ( + "context" + "io" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + 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 TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFromAPIKey(t *testing.T) { + recorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(recorder) + ginCtx.Set("apiKey", "test-api-key") + + ctx := context.WithValue(context.Background(), "gin", ginCtx) + executor := &CodexExecutor{} + rawJSON := []byte(`{"model":"gpt-5.3-codex","stream":true}`) + req := cliproxyexecutor.Request{ + Model: "gpt-5.3-codex", + Payload: []byte(`{"model":"gpt-5.3-codex"}`), + } + url := "https://example.com/responses" + + httpReq, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error: %v", err) + } + + body, errRead := io.ReadAll(httpReq.Body) + if errRead != nil { + t.Fatalf("read request body: %v", errRead) + } + + expectedKey := uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:test-api-key")).String() + gotKey := gjson.GetBytes(body, "prompt_cache_key").String() + if gotKey != expectedKey { + t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedKey) + } + if gotConversation := httpReq.Header.Get("Conversation_id"); gotConversation != expectedKey { + t.Fatalf("Conversation_id = %q, want %q", gotConversation, expectedKey) + } + if gotSession := httpReq.Header.Get("Session_id"); gotSession != expectedKey { + t.Fatalf("Session_id = %q, want %q", gotSession, expectedKey) + } + + httpReq2, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + if err != nil { + t.Fatalf("cacheHelper error (second call): %v", err) + } + body2, errRead2 := io.ReadAll(httpReq2.Body) + if errRead2 != nil { + t.Fatalf("read request body (second call): %v", errRead2) + } + gotKey2 := gjson.GetBytes(body2, "prompt_cache_key").String() + if gotKey2 != expectedKey { + t.Fatalf("prompt_cache_key (second call) = %q, want %q", gotKey2, expectedKey) + } +}