feat(executor): add tests for preserving key order in cache control functions
Added comprehensive tests to ensure key order is maintained when modifying payloads in `normalizeCacheControlTTL` and `enforceCacheControlLimit` functions. Removed unused helper functions and refactored implementations for better readability and efficiency.
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1463,182 +1462,6 @@ func countCacheControls(payload []byte) int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePayloadObject(payload []byte) (map[string]any, bool) {
|
|
||||||
if len(payload) == 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
var root map[string]any
|
|
||||||
if err := json.Unmarshal(payload, &root); err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return root, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func marshalPayloadObject(original []byte, root map[string]any) []byte {
|
|
||||||
if root == nil {
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
out, err := json.Marshal(root)
|
|
||||||
if err != nil {
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func asObject(v any) (map[string]any, bool) {
|
|
||||||
obj, ok := v.(map[string]any)
|
|
||||||
return obj, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func asArray(v any) ([]any, bool) {
|
|
||||||
arr, ok := v.([]any)
|
|
||||||
return arr, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func countCacheControlsMap(root map[string]any) int {
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
if system, ok := asArray(root["system"]); ok {
|
|
||||||
for _, item := range system {
|
|
||||||
if obj, ok := asObject(item); ok {
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tools, ok := asArray(root["tools"]); ok {
|
|
||||||
for _, item := range tools {
|
|
||||||
if obj, ok := asObject(item); ok {
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if messages, ok := asArray(root["messages"]); ok {
|
|
||||||
for _, msg := range messages {
|
|
||||||
msgObj, ok := asObject(msg)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content, ok := asArray(msgObj["content"])
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, item := range content {
|
|
||||||
if obj, ok := asObject(item); ok {
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeTTLForBlock(obj map[string]any, seen5m *bool) bool {
|
|
||||||
ccRaw, exists := obj["cache_control"]
|
|
||||||
if !exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
cc, ok := asObject(ccRaw)
|
|
||||||
if !ok {
|
|
||||||
*seen5m = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ttlRaw, ttlExists := cc["ttl"]
|
|
||||||
ttl, ttlIsString := ttlRaw.(string)
|
|
||||||
if !ttlExists || !ttlIsString || ttl != "1h" {
|
|
||||||
*seen5m = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if *seen5m {
|
|
||||||
delete(cc, "ttl")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLastCacheControlIndex(arr []any) int {
|
|
||||||
last := -1
|
|
||||||
for idx, item := range arr {
|
|
||||||
obj, ok := asObject(item)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
last = idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripCacheControlExceptIndex(arr []any, preserveIdx int, excess *int) {
|
|
||||||
for idx, item := range arr {
|
|
||||||
if *excess <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
obj, ok := asObject(item)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := obj["cache_control"]; exists && idx != preserveIdx {
|
|
||||||
delete(obj, "cache_control")
|
|
||||||
*excess--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripAllCacheControl(arr []any, excess *int) {
|
|
||||||
for _, item := range arr {
|
|
||||||
if *excess <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
obj, ok := asObject(item)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
delete(obj, "cache_control")
|
|
||||||
*excess--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripMessageCacheControl(messages []any, excess *int) {
|
|
||||||
for _, msg := range messages {
|
|
||||||
if *excess <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msgObj, ok := asObject(msg)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content, ok := asArray(msgObj["content"])
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, item := range content {
|
|
||||||
if *excess <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
obj, ok := asObject(item)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := obj["cache_control"]; exists {
|
|
||||||
delete(obj, "cache_control")
|
|
||||||
*excess--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeCacheControlTTL ensures cache_control TTL values don't violate the
|
// normalizeCacheControlTTL ensures cache_control TTL values don't violate the
|
||||||
// prompt-caching-scope-2026-01-05 ordering constraint: a 1h-TTL block must not
|
// prompt-caching-scope-2026-01-05 ordering constraint: a 1h-TTL block must not
|
||||||
// appear after a 5m-TTL block anywhere in the evaluation order.
|
// appear after a 5m-TTL block anywhere in the evaluation order.
|
||||||
@@ -1651,58 +1474,75 @@ func stripMessageCacheControl(messages []any, excess *int) {
|
|||||||
// Strategy: walk all cache_control blocks in evaluation order. Once a 5m block
|
// Strategy: walk all cache_control blocks in evaluation order. Once a 5m block
|
||||||
// is seen, strip ttl from ALL subsequent 1h blocks (downgrading them to 5m).
|
// is seen, strip ttl from ALL subsequent 1h blocks (downgrading them to 5m).
|
||||||
func normalizeCacheControlTTL(payload []byte) []byte {
|
func normalizeCacheControlTTL(payload []byte) []byte {
|
||||||
root, ok := parsePayloadObject(payload)
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
if !ok {
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
original := payload
|
||||||
seen5m := false
|
seen5m := false
|
||||||
modified := false
|
modified := false
|
||||||
|
|
||||||
if tools, ok := asArray(root["tools"]); ok {
|
processBlock := func(path string, obj gjson.Result) {
|
||||||
for _, tool := range tools {
|
cc := obj.Get("cache_control")
|
||||||
if obj, ok := asObject(tool); ok {
|
if !cc.Exists() {
|
||||||
if normalizeTTLForBlock(obj, &seen5m) {
|
return
|
||||||
|
}
|
||||||
|
if !cc.IsObject() {
|
||||||
|
seen5m = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ttl := cc.Get("ttl")
|
||||||
|
if ttl.Type != gjson.String || ttl.String() != "1h" {
|
||||||
|
seen5m = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !seen5m {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ttlPath := path + ".cache_control.ttl"
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, ttlPath)
|
||||||
|
if errDel != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if system, ok := asArray(root["system"]); ok {
|
tools := gjson.GetBytes(payload, "tools")
|
||||||
for _, item := range system {
|
if tools.IsArray() {
|
||||||
if obj, ok := asObject(item); ok {
|
tools.ForEach(func(idx, item gjson.Result) bool {
|
||||||
if normalizeTTLForBlock(obj, &seen5m) {
|
processBlock(fmt.Sprintf("tools.%d", int(idx.Int())), item)
|
||||||
modified = true
|
return true
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if messages, ok := asArray(root["messages"]); ok {
|
system := gjson.GetBytes(payload, "system")
|
||||||
for _, msg := range messages {
|
if system.IsArray() {
|
||||||
msgObj, ok := asObject(msg)
|
system.ForEach(func(idx, item gjson.Result) bool {
|
||||||
if !ok {
|
processBlock(fmt.Sprintf("system.%d", int(idx.Int())), item)
|
||||||
continue
|
return true
|
||||||
}
|
})
|
||||||
content, ok := asArray(msgObj["content"])
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, item := range content {
|
|
||||||
if obj, ok := asObject(item); ok {
|
|
||||||
if normalizeTTLForBlock(obj, &seen5m) {
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messages := gjson.GetBytes(payload, "messages")
|
||||||
|
if messages.IsArray() {
|
||||||
|
messages.ForEach(func(msgIdx, msg gjson.Result) bool {
|
||||||
|
content := msg.Get("content")
|
||||||
|
if !content.IsArray() {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
content.ForEach(func(itemIdx, item gjson.Result) bool {
|
||||||
|
processBlock(fmt.Sprintf("messages.%d.content.%d", int(msgIdx.Int()), int(itemIdx.Int())), item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if !modified {
|
if !modified {
|
||||||
return payload
|
return original
|
||||||
}
|
}
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforceCacheControlLimit removes excess cache_control blocks from a payload
|
// enforceCacheControlLimit removes excess cache_control blocks from a payload
|
||||||
@@ -1722,64 +1562,166 @@ func normalizeCacheControlTTL(payload []byte) []byte {
|
|||||||
// Phase 4: remaining system blocks (last system).
|
// Phase 4: remaining system blocks (last system).
|
||||||
// Phase 5: remaining tool blocks (last tool).
|
// Phase 5: remaining tool blocks (last tool).
|
||||||
func enforceCacheControlLimit(payload []byte, maxBlocks int) []byte {
|
func enforceCacheControlLimit(payload []byte, maxBlocks int) []byte {
|
||||||
root, ok := parsePayloadObject(payload)
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
if !ok {
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
total := countCacheControlsMap(root)
|
total := countCacheControls(payload)
|
||||||
if total <= maxBlocks {
|
if total <= maxBlocks {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
excess := total - maxBlocks
|
excess := total - maxBlocks
|
||||||
|
|
||||||
var system []any
|
system := gjson.GetBytes(payload, "system")
|
||||||
if arr, ok := asArray(root["system"]); ok {
|
if system.IsArray() {
|
||||||
system = arr
|
lastIdx := -1
|
||||||
|
system.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if item.Get("cache_control").Exists() {
|
||||||
|
lastIdx = int(idx.Int())
|
||||||
}
|
}
|
||||||
var tools []any
|
return true
|
||||||
if arr, ok := asArray(root["tools"]); ok {
|
})
|
||||||
tools = arr
|
if lastIdx >= 0 {
|
||||||
|
system.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
var messages []any
|
i := int(idx.Int())
|
||||||
if arr, ok := asArray(root["messages"]); ok {
|
if i == lastIdx {
|
||||||
messages = arr
|
return true
|
||||||
|
}
|
||||||
|
if !item.Get("cache_control").Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("system.%d.cache_control", i)
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, path)
|
||||||
|
if errDel != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
|
excess--
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(system) > 0 {
|
|
||||||
stripCacheControlExceptIndex(system, findLastCacheControlIndex(system), &excess)
|
|
||||||
}
|
}
|
||||||
if excess <= 0 {
|
if excess <= 0 {
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
tools := gjson.GetBytes(payload, "tools")
|
||||||
stripCacheControlExceptIndex(tools, findLastCacheControlIndex(tools), &excess)
|
if tools.IsArray() {
|
||||||
|
lastIdx := -1
|
||||||
|
tools.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if item.Get("cache_control").Exists() {
|
||||||
|
lastIdx = int(idx.Int())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if lastIdx >= 0 {
|
||||||
|
tools.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i := int(idx.Int())
|
||||||
|
if i == lastIdx {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !item.Get("cache_control").Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("tools.%d.cache_control", i)
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, path)
|
||||||
|
if errDel != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
|
excess--
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if excess <= 0 {
|
if excess <= 0 {
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(messages) > 0 {
|
messages := gjson.GetBytes(payload, "messages")
|
||||||
stripMessageCacheControl(messages, &excess)
|
if messages.IsArray() {
|
||||||
|
messages.ForEach(func(msgIdx, msg gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
content := msg.Get("content")
|
||||||
|
if !content.IsArray() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
content.ForEach(func(itemIdx, item gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !item.Get("cache_control").Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("messages.%d.content.%d.cache_control", int(msgIdx.Int()), int(itemIdx.Int()))
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, path)
|
||||||
|
if errDel != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
|
excess--
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if excess <= 0 {
|
if excess <= 0 {
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(system) > 0 {
|
system = gjson.GetBytes(payload, "system")
|
||||||
stripAllCacheControl(system, &excess)
|
if system.IsArray() {
|
||||||
|
system.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !item.Get("cache_control").Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("system.%d.cache_control", int(idx.Int()))
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, path)
|
||||||
|
if errDel != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
|
excess--
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if excess <= 0 {
|
if excess <= 0 {
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
tools = gjson.GetBytes(payload, "tools")
|
||||||
stripAllCacheControl(tools, &excess)
|
if tools.IsArray() {
|
||||||
|
tools.ForEach(func(idx, item gjson.Result) bool {
|
||||||
|
if excess <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !item.Get("cache_control").Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("tools.%d.cache_control", int(idx.Int()))
|
||||||
|
updated, errDel := sjson.DeleteBytes(payload, path)
|
||||||
|
if errDel != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload = updated
|
||||||
|
excess--
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return marshalPayloadObject(payload, root)
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// injectMessagesCacheControl adds cache_control to the second-to-last user turn for multi-turn caching.
|
// injectMessagesCacheControl adds cache_control to the second-to-last user turn for multi-turn caching.
|
||||||
|
|||||||
@@ -965,6 +965,28 @@ func TestNormalizeCacheControlTTL_PreservesOriginalBytesWhenNoChange(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeCacheControlTTL_PreservesKeyOrderWhenModified(t *testing.T) {
|
||||||
|
payload := []byte(`{"model":"m","messages":[{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral","ttl":"1h"}}]}],"tools":[{"name":"t1","cache_control":{"type":"ephemeral"}}],"system":[{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}]}`)
|
||||||
|
|
||||||
|
out := normalizeCacheControlTTL(payload)
|
||||||
|
|
||||||
|
if gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").Exists() {
|
||||||
|
t.Fatalf("messages.0.content.0.cache_control.ttl should be removed after a default-5m block")
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := string(out)
|
||||||
|
idxModel := strings.Index(outStr, `"model"`)
|
||||||
|
idxMessages := strings.Index(outStr, `"messages"`)
|
||||||
|
idxTools := strings.Index(outStr, `"tools"`)
|
||||||
|
idxSystem := strings.Index(outStr, `"system"`)
|
||||||
|
if idxModel == -1 || idxMessages == -1 || idxTools == -1 || idxSystem == -1 {
|
||||||
|
t.Fatalf("failed to locate top-level keys in output: %s", outStr)
|
||||||
|
}
|
||||||
|
if !(idxModel < idxMessages && idxMessages < idxTools && idxTools < idxSystem) {
|
||||||
|
t.Fatalf("top-level key order changed:\noriginal: %s\ngot: %s", payload, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnforceCacheControlLimit_StripsNonLastToolBeforeMessages(t *testing.T) {
|
func TestEnforceCacheControlLimit_StripsNonLastToolBeforeMessages(t *testing.T) {
|
||||||
payload := []byte(`{
|
payload := []byte(`{
|
||||||
"tools": [
|
"tools": [
|
||||||
@@ -994,6 +1016,31 @@ func TestEnforceCacheControlLimit_StripsNonLastToolBeforeMessages(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnforceCacheControlLimit_PreservesKeyOrderWhenModified(t *testing.T) {
|
||||||
|
payload := []byte(`{"model":"m","messages":[{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral"}},{"type":"text","text":"u2","cache_control":{"type":"ephemeral"}}]}],"tools":[{"name":"t1","cache_control":{"type":"ephemeral"}},{"name":"t2","cache_control":{"type":"ephemeral"}}],"system":[{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}]}`)
|
||||||
|
|
||||||
|
out := enforceCacheControlLimit(payload, 4)
|
||||||
|
|
||||||
|
if got := countCacheControls(out); got != 4 {
|
||||||
|
t.Fatalf("cache_control count = %d, want 4", got)
|
||||||
|
}
|
||||||
|
if gjson.GetBytes(out, "tools.0.cache_control").Exists() {
|
||||||
|
t.Fatalf("tools.0.cache_control should be removed first (non-last tool)")
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr := string(out)
|
||||||
|
idxModel := strings.Index(outStr, `"model"`)
|
||||||
|
idxMessages := strings.Index(outStr, `"messages"`)
|
||||||
|
idxTools := strings.Index(outStr, `"tools"`)
|
||||||
|
idxSystem := strings.Index(outStr, `"system"`)
|
||||||
|
if idxModel == -1 || idxMessages == -1 || idxTools == -1 || idxSystem == -1 {
|
||||||
|
t.Fatalf("failed to locate top-level keys in output: %s", outStr)
|
||||||
|
}
|
||||||
|
if !(idxModel < idxMessages && idxMessages < idxTools && idxTools < idxSystem) {
|
||||||
|
t.Fatalf("top-level key order changed:\noriginal: %s\ngot: %s", payload, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnforceCacheControlLimit_ToolOnlyPayloadStillRespectsLimit(t *testing.T) {
|
func TestEnforceCacheControlLimit_ToolOnlyPayloadStillRespectsLimit(t *testing.T) {
|
||||||
payload := []byte(`{
|
payload := []byte(`{
|
||||||
"tools": [
|
"tools": [
|
||||||
|
|||||||
Reference in New Issue
Block a user