History

fix(caching): ensure prompt-caching beta is always appended and add multi-turn cache control tests

Luis Pater committed 9 months agov6.7.34

Commit 7ff39361 parent f36a5f52 files · +53 −1Browse files

Unified · Split

2 files
  1. internal/runtime/executor/caching_verify_test.go +48 −0
  2. internal/runtime/executor/claude_executor.go +5 −1
internal/runtime/executor/caching_verify_test.go+48−0
@@ -165,6 +165,54 @@ func TestEnsureCacheControl(t *testing.T) { · 164 unmodified lines
165 t.Errorf("system should have cache_control even with empty tools array")165 t.Errorf("system should have cache_control even with empty tools array")
166 }166 }
167 })167 })
168
169 // Test case 8: Messages caching for multi-turn (second-to-last user)
170 t.Run("Messages Caching Second-To-Last User", func(t *testing.T) {
171 input := []byte(`{
172 "model": "claude-3-5-sonnet",
173 "messages": [
174 {"role": "user", "content": "First user"},
175 {"role": "assistant", "content": "Assistant reply"},
176 {"role": "user", "content": "Second user"},
177 {"role": "assistant", "content": "Assistant reply 2"},
178 {"role": "user", "content": "Third user"}
179 ]
180 }`)
181 output := ensureCacheControl(input)
182
183 cacheType := gjson.GetBytes(output, "messages.2.content.0.cache_control.type")
184 if cacheType.String() != "ephemeral" {
185 t.Errorf("cache_control not found on second-to-last user turn. Output: %s", string(output))
186 }
187
188 lastUserCache := gjson.GetBytes(output, "messages.4.content.0.cache_control")
189 if lastUserCache.Exists() {
190 t.Errorf("last user turn should NOT have cache_control")
191 }
192 })
193
194 // Test case 9: Existing message cache_control should skip injection
195 t.Run("Messages Skip When Cache Control Exists", func(t *testing.T) {
196 input := []byte(`{
197 "model": "claude-3-5-sonnet",
198 "messages": [
199 {"role": "user", "content": [{"type": "text", "text": "First user"}]},
200 {"role": "assistant", "content": [{"type": "text", "text": "Assistant reply", "cache_control": {"type": "ephemeral"}}]},
201 {"role": "user", "content": [{"type": "text", "text": "Second user"}]}
202 ]
203 }`)
204 output := ensureCacheControl(input)
205
206 userCache := gjson.GetBytes(output, "messages.0.content.0.cache_control")
207 if userCache.Exists() {
208 t.Errorf("cache_control should NOT be injected when a message already has cache_control")
209 }
210
211 existingCache := gjson.GetBytes(output, "messages.1.content.0.cache_control.type")
212 if existingCache.String() != "ephemeral" {
213 t.Errorf("existing cache_control should be preserved. Output: %s", string(output))
214 }
215 })
168}216}
169 217
170// TestCacheControlOrder verifies the correct order: tools -> system -> messages218// TestCacheControlOrder verifies the correct order: tools -> system -> messages
internal/runtime/executor/claude_executor.go+5−1
@@ -642,13 +642,17 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, · 641 unmodified lines
642 ginHeaders = ginCtx.Request.Header642 ginHeaders = ginCtx.Request.Header
643 }643 }
644 644
645 baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,prompt-caching-2024-07-31"645 promptCachingBeta := "prompt-caching-2024-07-31"
646 baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14," + promptCachingBeta
646 if val := strings.TrimSpace(ginHeaders.Get("Anthropic-Beta")); val != "" {647 if val := strings.TrimSpace(ginHeaders.Get("Anthropic-Beta")); val != "" {
647 baseBetas = val648 baseBetas = val
648 if !strings.Contains(val, "oauth") {649 if !strings.Contains(val, "oauth") {
649 baseBetas += ",oauth-2025-04-20"650 baseBetas += ",oauth-2025-04-20"
650 }651 }
651 }652 }
653 if !strings.Contains(baseBetas, promptCachingBeta) {
654 baseBetas += "," + promptCachingBeta
655 }
652 656
653 // Merge extra betas from request body657 // Merge extra betas from request body
654 if len(extraBetas) > 0 {658 if len(extraBetas) > 0 {