diff --git a/internal/providers/adapter_codex_test.go b/internal/providers/adapter_codex_test.go index 88b7c6ba..edc8dc0f 100644 --- a/internal/providers/adapter_codex_test.go +++ b/internal/providers/adapter_codex_test.go @@ -388,18 +388,20 @@ func TestCodexAdapter_FromResponse_PromptTokensDetailsAlias(t *testing.T) { } func TestCodexAdapter_ToRequestPromptCacheControls(t *testing.T) { - a, _ := NewCodexAdapter(ProviderConfig{}) - body, _, err := a.ToRequest(ChatRequest{ + req := ChatRequest{ Messages: []Message{{Role: "user", Content: "hi"}}, Options: map[string]any{ OptPromptCacheKey: "tenant/agent/session", OptPromptCacheRetention: "1h", }, - }) + } + + // Native OpenAI endpoint accepts prompt cache params. + nativeAdapter, _ := NewCodexAdapter(ProviderConfig{BaseURL: "https://api.openai.com/v1"}) + body, _, err := nativeAdapter.ToRequest(req) if err != nil { t.Fatalf("ToRequest: %v", err) } - var payload map[string]any if err := json.Unmarshal(body, &payload); err != nil { t.Fatalf("decode body: %v", err) @@ -410,6 +412,23 @@ func TestCodexAdapter_ToRequestPromptCacheControls(t *testing.T) { if got := payload["prompt_cache_retention"]; got != "1h" { t.Fatalf("prompt_cache_retention = %v, want 1h", got) } + + // Default ChatGPT OAuth backend rejects these params with HTTP 400 — must omit. + oauthAdapter, _ := NewCodexAdapter(ProviderConfig{}) + oauthBody, _, err := oauthAdapter.ToRequest(req) + if err != nil { + t.Fatalf("ToRequest (oauth): %v", err) + } + var oauthPayload map[string]any + if err := json.Unmarshal(oauthBody, &oauthPayload); err != nil { + t.Fatalf("decode oauth body: %v", err) + } + if _, ok := oauthPayload["prompt_cache_key"]; ok { + t.Fatal("prompt_cache_key must not be sent to the ChatGPT OAuth backend") + } + if _, ok := oauthPayload["prompt_cache_retention"]; ok { + t.Fatal("prompt_cache_retention must not be sent to the ChatGPT OAuth backend") + } } func TestCodexAdapter_CapabilitiesCacheControl(t *testing.T) { diff --git a/internal/providers/codex_build.go b/internal/providers/codex_build.go index 4c296802..d538d046 100644 --- a/internal/providers/codex_build.go +++ b/internal/providers/codex_build.go @@ -140,11 +140,18 @@ func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[strin body["reasoning"] = map[string]any{"effort": level} } - if cacheKey, ok := req.Options[OptPromptCacheKey]; ok { - body["prompt_cache_key"] = cacheKey - } - if retention, ok := req.Options[OptPromptCacheRetention]; ok { - body["prompt_cache_retention"] = retention + // Prompt caching params (prompt_cache_key, prompt_cache_retention) are accepted + // only by native OpenAI endpoints. The ChatGPT subscription OAuth backend + // (chatgpt.com/backend-api) rejects them with HTTP 400, so gate on the endpoint — + // the same native-only policy CacheMiddleware already applies. Server-side prefix + // caching still works on the OAuth backend without these params. + if isOpenAINativeEndpoint(p.apiBase) { + if cacheKey, ok := req.Options[OptPromptCacheKey]; ok { + body["prompt_cache_key"] = cacheKey + } + if retention, ok := req.Options[OptPromptCacheRetention]; ok { + body["prompt_cache_retention"] = retention + } } return body diff --git a/internal/providers/codex_test.go b/internal/providers/codex_test.go index f2a1d806..6e3ea6d2 100644 --- a/internal/providers/codex_test.go +++ b/internal/providers/codex_test.go @@ -1131,21 +1131,33 @@ func TestCodexBuildRequestBody_NilFunction_HandlesGracefully(t *testing.T) { } func TestCodexBuildRequestBodyPromptCacheControls(t *testing.T) { - p := NewCodexProvider("test", &staticTokenSource{token: "tok"}, "", "gpt-4o") - - body := p.buildRequestBody(ChatRequest{ + req := ChatRequest{ Messages: []Message{{Role: "user", Content: "Hello"}}, Options: map[string]any{ OptPromptCacheKey: "agent/session/provider", OptPromptCacheRetention: "24h", }, - }, true) + } + // Native OpenAI endpoint accepts prompt cache params. + native := NewCodexProvider("test", &staticTokenSource{token: "tok"}, "https://api.openai.com/v1", "gpt-4o") + body := native.buildRequestBody(req, true) if got := body["prompt_cache_key"]; got != "agent/session/provider" { - t.Fatalf("prompt_cache_key = %v, want agent/session/provider", got) + t.Fatalf("native prompt_cache_key = %v, want agent/session/provider", got) } if got := body["prompt_cache_retention"]; got != "24h" { - t.Fatalf("prompt_cache_retention = %v, want 24h", got) + t.Fatalf("native prompt_cache_retention = %v, want 24h", got) + } + + // ChatGPT subscription OAuth backend (default apiBase) rejects these params + // with HTTP 400, so they must be omitted. + oauth := NewCodexProvider("test", &staticTokenSource{token: "tok"}, "", "gpt-4o") + oauthBody := oauth.buildRequestBody(req, true) + if _, ok := oauthBody["prompt_cache_key"]; ok { + t.Fatal("prompt_cache_key must not be sent to the ChatGPT OAuth backend") + } + if _, ok := oauthBody["prompt_cache_retention"]; ok { + t.Fatal("prompt_cache_retention must not be sent to the ChatGPT OAuth backend") } }