mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-17 20:00:52 +08:00
- Add ensureCacheControl() to auto-inject cache breakpoints - Cache tools (last tool), system (last element), and messages (2nd-to-last user turn) - Add prompt-caching-2024-07-31 beta header - Return original payload on sjson error to prevent corruption - Include verification test for caching logic Enables up to 90% cost reduction on cached tokens. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
7.1 KiB
Go
211 lines
7.1 KiB
Go
package executor
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestEnsureCacheControl(t *testing.T) {
|
|
// Test case 1: System prompt as string
|
|
t.Run("String System Prompt", func(t *testing.T) {
|
|
input := []byte(`{"model": "claude-3-5-sonnet", "system": "This is a long system prompt", "messages": []}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
res := gjson.GetBytes(output, "system.0.cache_control.type")
|
|
if res.String() != "ephemeral" {
|
|
t.Errorf("cache_control not found in system string. Output: %s", string(output))
|
|
}
|
|
})
|
|
|
|
// Test case 2: System prompt as array
|
|
t.Run("Array System Prompt", func(t *testing.T) {
|
|
input := []byte(`{"model": "claude-3-5-sonnet", "system": [{"type": "text", "text": "Part 1"}, {"type": "text", "text": "Part 2"}], "messages": []}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
// cache_control should only be on the LAST element
|
|
res0 := gjson.GetBytes(output, "system.0.cache_control")
|
|
res1 := gjson.GetBytes(output, "system.1.cache_control.type")
|
|
|
|
if res0.Exists() {
|
|
t.Errorf("cache_control should NOT be on the first element")
|
|
}
|
|
if res1.String() != "ephemeral" {
|
|
t.Errorf("cache_control not found on last system element. Output: %s", string(output))
|
|
}
|
|
})
|
|
|
|
// Test case 3: Tools are cached
|
|
t.Run("Tools Caching", func(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "claude-3-5-sonnet",
|
|
"tools": [
|
|
{"name": "tool1", "description": "First tool", "input_schema": {"type": "object"}},
|
|
{"name": "tool2", "description": "Second tool", "input_schema": {"type": "object"}}
|
|
],
|
|
"system": "System prompt",
|
|
"messages": []
|
|
}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
// cache_control should only be on the LAST tool
|
|
tool0Cache := gjson.GetBytes(output, "tools.0.cache_control")
|
|
tool1Cache := gjson.GetBytes(output, "tools.1.cache_control.type")
|
|
|
|
if tool0Cache.Exists() {
|
|
t.Errorf("cache_control should NOT be on the first tool")
|
|
}
|
|
if tool1Cache.String() != "ephemeral" {
|
|
t.Errorf("cache_control not found on last tool. Output: %s", string(output))
|
|
}
|
|
|
|
// System should also have cache_control
|
|
systemCache := gjson.GetBytes(output, "system.0.cache_control.type")
|
|
if systemCache.String() != "ephemeral" {
|
|
t.Errorf("cache_control not found in system. Output: %s", string(output))
|
|
}
|
|
})
|
|
|
|
// Test case 4: Tools and system are INDEPENDENT breakpoints
|
|
// Per Anthropic docs: Up to 4 breakpoints allowed, tools and system are cached separately
|
|
t.Run("Independent Cache Breakpoints", func(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "claude-3-5-sonnet",
|
|
"tools": [
|
|
{"name": "tool1", "description": "First tool", "input_schema": {"type": "object"}, "cache_control": {"type": "ephemeral"}}
|
|
],
|
|
"system": [{"type": "text", "text": "System"}],
|
|
"messages": []
|
|
}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
// Tool already has cache_control - should not be changed
|
|
tool0Cache := gjson.GetBytes(output, "tools.0.cache_control.type")
|
|
if tool0Cache.String() != "ephemeral" {
|
|
t.Errorf("existing cache_control was incorrectly removed")
|
|
}
|
|
|
|
// System SHOULD get cache_control because it is an INDEPENDENT breakpoint
|
|
// Tools and system are separate cache levels in the hierarchy
|
|
systemCache := gjson.GetBytes(output, "system.0.cache_control.type")
|
|
if systemCache.String() != "ephemeral" {
|
|
t.Errorf("system should have its own cache_control breakpoint (independent of tools)")
|
|
}
|
|
})
|
|
|
|
// Test case 5: Only tools, no system
|
|
t.Run("Only Tools No System", func(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "claude-3-5-sonnet",
|
|
"tools": [
|
|
{"name": "tool1", "description": "Tool", "input_schema": {"type": "object"}}
|
|
],
|
|
"messages": [{"role": "user", "content": "Hi"}]
|
|
}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
toolCache := gjson.GetBytes(output, "tools.0.cache_control.type")
|
|
if toolCache.String() != "ephemeral" {
|
|
t.Errorf("cache_control not found on tool. Output: %s", string(output))
|
|
}
|
|
})
|
|
|
|
// Test case 6: Many tools (Claude Code scenario)
|
|
t.Run("Many Tools (Claude Code Scenario)", func(t *testing.T) {
|
|
// Simulate Claude Code with many tools
|
|
toolsJSON := `[`
|
|
for i := 0; i < 50; i++ {
|
|
if i > 0 {
|
|
toolsJSON += ","
|
|
}
|
|
toolsJSON += fmt.Sprintf(`{"name": "tool%d", "description": "Tool %d", "input_schema": {"type": "object"}}`, i, i)
|
|
}
|
|
toolsJSON += `]`
|
|
|
|
input := []byte(fmt.Sprintf(`{
|
|
"model": "claude-3-5-sonnet",
|
|
"tools": %s,
|
|
"system": [{"type": "text", "text": "You are Claude Code"}],
|
|
"messages": [{"role": "user", "content": "Hello"}]
|
|
}`, toolsJSON))
|
|
|
|
output := ensureCacheControl(input)
|
|
|
|
// Only the last tool (index 49) should have cache_control
|
|
for i := 0; i < 49; i++ {
|
|
path := fmt.Sprintf("tools.%d.cache_control", i)
|
|
if gjson.GetBytes(output, path).Exists() {
|
|
t.Errorf("tool %d should NOT have cache_control", i)
|
|
}
|
|
}
|
|
|
|
lastToolCache := gjson.GetBytes(output, "tools.49.cache_control.type")
|
|
if lastToolCache.String() != "ephemeral" {
|
|
t.Errorf("last tool (49) should have cache_control")
|
|
}
|
|
|
|
// System should also have cache_control
|
|
systemCache := gjson.GetBytes(output, "system.0.cache_control.type")
|
|
if systemCache.String() != "ephemeral" {
|
|
t.Errorf("system should have cache_control")
|
|
}
|
|
|
|
t.Log("test passed: 50 tools - cache_control only on last tool")
|
|
})
|
|
|
|
// Test case 7: Empty tools array
|
|
t.Run("Empty Tools Array", func(t *testing.T) {
|
|
input := []byte(`{"model": "claude-3-5-sonnet", "tools": [], "system": "Test", "messages": []}`)
|
|
output := ensureCacheControl(input)
|
|
|
|
// System should still get cache_control
|
|
systemCache := gjson.GetBytes(output, "system.0.cache_control.type")
|
|
if systemCache.String() != "ephemeral" {
|
|
t.Errorf("system should have cache_control even with empty tools array")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCacheControlOrder verifies the correct order: tools -> system -> messages
|
|
func TestCacheControlOrder(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "claude-sonnet-4",
|
|
"tools": [
|
|
{"name": "Read", "description": "Read file", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}},
|
|
{"name": "Write", "description": "Write file", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}}}
|
|
],
|
|
"system": [
|
|
{"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."},
|
|
{"type": "text", "text": "Additional instructions here..."}
|
|
],
|
|
"messages": [
|
|
{"role": "user", "content": "Hello"}
|
|
]
|
|
}`)
|
|
|
|
output := ensureCacheControl(input)
|
|
|
|
// 1. Last tool has cache_control
|
|
if gjson.GetBytes(output, "tools.1.cache_control.type").String() != "ephemeral" {
|
|
t.Error("last tool should have cache_control")
|
|
}
|
|
|
|
// 2. First tool has NO cache_control
|
|
if gjson.GetBytes(output, "tools.0.cache_control").Exists() {
|
|
t.Error("first tool should NOT have cache_control")
|
|
}
|
|
|
|
// 3. Last system element has cache_control
|
|
if gjson.GetBytes(output, "system.1.cache_control.type").String() != "ephemeral" {
|
|
t.Error("last system element should have cache_control")
|
|
}
|
|
|
|
// 4. First system element has NO cache_control
|
|
if gjson.GetBytes(output, "system.0.cache_control").Exists() {
|
|
t.Error("first system element should NOT have cache_control")
|
|
}
|
|
|
|
t.Log("cache order correct: tools -> system")
|
|
}
|