mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 08:28:34 +00:00
Merge pull request #6702 from a1gaoshanlaiyige/fix/message-cache-json-escaping
fix(gateway): use JSON escaping for message cache promotion
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
@@ -157,5 +158,8 @@ func injectCacheControlOnLastContentBlock(body []byte, idx int, msg *gjson.Resul
|
||||
// mustJSONString 把一个 Go string 序列化为合法 JSON string(含引号),
|
||||
// 用于 sjson.SetRawBytes 场景下手工拼 JSON。
|
||||
func mustJSONString(s string) string {
|
||||
return fmt.Sprintf("%q", s)
|
||||
// Go string quoting can emit non-JSON escapes such as \x7f or \a.
|
||||
// Marshaling a string cannot fail.
|
||||
encoded, _ := json.Marshal(s)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestAddMessageCacheBreakpoints_JSONStringEscaping(t *testing.T) {
|
||||
var controls strings.Builder
|
||||
for r := rune(0); r <= 0x9f; r++ {
|
||||
controls.WriteRune(r)
|
||||
}
|
||||
tests := []struct{ name, text string }{
|
||||
{"delete_character", "before\x7fafter"},
|
||||
{"bell_and_vertical_tab", "before\a\vafter"},
|
||||
{"nul_and_escape", "before\x00\x1bafter"},
|
||||
{"all_ascii_and_c1_controls", controls.String()},
|
||||
{"supplementary_nonprinting_rune", "before\U000e0067after"},
|
||||
{"unicode_and_json_metacharacters", "中文 😀 <>& \"quote\" \\slash\n\r\t\b\f"},
|
||||
{"literal_backslash_sequences", `literal \x7f \a \v \U000e0067`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Both the final message and the second-to-last user turn are promoted
|
||||
// from strings to text blocks by the cache rewrite.
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"messages": []map[string]any{
|
||||
{"role": "user", "content": tt.text},
|
||||
{"role": "assistant", "content": "ack"},
|
||||
{"role": "user", "content": "next"},
|
||||
{"role": "assistant", "content": tt.text},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
out := addMessageCacheBreakpoints(body)
|
||||
// gjson accepts some malformed input, so validate with encoding/json
|
||||
// before inspecting the rewritten content and cache metadata.
|
||||
var decoded any
|
||||
require.NoError(t, json.Unmarshal(out, &decoded))
|
||||
for _, path := range []string{"messages.0.content", "messages.3.content"} {
|
||||
content := gjson.GetBytes(out, path)
|
||||
require.True(t, content.IsArray())
|
||||
require.Len(t, content.Array(), 1)
|
||||
block := content.Array()[0]
|
||||
require.Equal(t, "text", block.Get("type").String())
|
||||
require.Equal(t, tt.text, block.Get("text").String())
|
||||
require.Equal(t, "ephemeral", block.Get("cache_control.type").String())
|
||||
require.Equal(t, "5m", block.Get("cache_control.ttl").String())
|
||||
}
|
||||
require.JSONEq(t, string(out), string(addMessageCacheBreakpoints(out)))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user