From 3d80078db72bc5574a8056665ae8fcd20a3ca288 Mon Sep 17 00:00:00 2001 From: qs3c <2749950753@qq.com> Date: Fri, 6 Mar 2026 11:36:17 +0800 Subject: [PATCH] fix(openai_compat): gate prompt_cache_key to OpenAI endpoints --- pkg/providers/openai_compat/provider.go | 16 +++- pkg/providers/openai_compat/provider_test.go | 86 ++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 1904ee153..d25844989 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -155,9 +155,9 @@ func (p *Provider) Chat( // The key is typically the agent ID — stable per agent, shared across requests. // See: https://platform.openai.com/docs/guides/prompt-caching // Prompt caching is only supported by OpenAI-native endpoints. - // Gemini and other providers reject unknown fields, so skip for non-OpenAI APIs. + // Other OpenAI-compatible providers may reject unknown fields. if cacheKey, ok := options["prompt_cache_key"].(string); ok && cacheKey != "" { - if !strings.Contains(p.apiBase, "generativelanguage.googleapis.com") { + if supportsPromptCacheKey(p.apiBase) { requestBody["prompt_cache_key"] = cacheKey } } @@ -195,6 +195,18 @@ func (p *Provider) Chat( return parseResponse(body) } +func supportsPromptCacheKey(apiBase string) bool { + host := "" + if parsed, err := url.Parse(apiBase); err == nil { + host = parsed.Hostname() + } + if host == "" { + host = apiBase + } + host = strings.ToLower(strings.TrimSpace(host)) + return host == "openai.com" || strings.HasSuffix(host, ".openai.com") +} + func parseResponse(body []byte) (*LLMResponse, error) { var apiResponse struct { Choices []struct { diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 174bcf00d..43d7a0d4a 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -376,6 +376,92 @@ func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) { } } +func TestProviderChat_SkipsPromptCacheKeyForNonOpenAIEndpoint(t *testing.T) { + var requestBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + resp := map[string]any{ + "choices": []map[string]any{ + { + "message": map[string]any{"content": "ok"}, + "finish_reason": "stop", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + _, err := p.Chat( + t.Context(), + []Message{{Role: "user", Content: "hi"}}, + nil, + "gpt-4o", + map[string]any{"prompt_cache_key": "agent-123"}, + ) + if err != nil { + t.Fatalf("Chat() error = %v", err) + } + + if _, ok := requestBody["prompt_cache_key"]; ok { + t.Fatalf("did not expect prompt_cache_key for non-OpenAI endpoint") + } +} + +func TestSupportsPromptCacheKey(t *testing.T) { + tests := []struct { + name string + apiBase string + want bool + }{ + { + name: "openai endpoint", + apiBase: "https://api.openai.com/v1", + want: true, + }, + { + name: "openai apex endpoint", + apiBase: "https://openai.com/v1", + want: true, + }, + { + name: "nvidia endpoint", + apiBase: "https://integrate.api.nvidia.com/v1", + want: false, + }, + { + name: "gemini endpoint", + apiBase: "https://generativelanguage.googleapis.com/v1beta", + want: false, + }, + { + name: "openrouter endpoint", + apiBase: "https://openrouter.ai/api/v1", + want: false, + }, + { + name: "local endpoint", + apiBase: "http://localhost:11434/v1", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := supportsPromptCacheKey(tt.apiBase) + if got != tt.want { + t.Fatalf("supportsPromptCacheKey(%q) = %v, want %v", tt.apiBase, got, tt.want) + } + }) + } +} + func TestNormalizeModel_UsesAPIBase(t *testing.T) { if got := normalizeModel("deepseek/deepseek-chat", "https://api.deepseek.com/v1"); got != "deepseek-chat" { t.Fatalf("normalizeModel(deepseek) = %q, want %q", got, "deepseek-chat")