Skip to content

Commit 7c9fcd2

Browse files
committed
refactor(initialize,util): 重构缓存头设置与 Anthropic 缓存处理
1. 抽离重复的缓存头设置逻辑为公共函数setCacheHeaders 2. 新增Anthropic显式缓存块追踪功能,支持标准缓存头上报 3. 优化缓存状态检测与Age头生成逻辑 4. 统一多接口的缓存头设置流程
1 parent da219c6 commit 7c9fcd2

6 files changed

Lines changed: 183 additions & 23 deletions

File tree

initialize/files.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,7 @@ func (h *Handler) chatWithFiles(c *gin.Context) {
392392

393393
// Cache breakdown is known before streaming starts, so set these headers
394394
// before the first chunk is flushed (works for both stream and non-stream).
395-
c.Header("X-Cache-Creation-Tokens", fmt.Sprintf("%d", cacheCreation))
396-
c.Header("X-Cache-Read-Tokens", fmt.Sprintf("%d", cacheRead))
395+
setCacheHeaders(c, promptHash, cacheCreation, cacheRead)
397396

398397
result := duckgo.Handler(c, response, translated_request, req.Stream, stats)
399398

initialize/handlers.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ func (h *Handler) duckduckgo(c *gin.Context) {
105105

106106
// Cache breakdown is known before streaming starts, so set these headers
107107
// before the first chunk is flushed (works for both stream and non-stream).
108-
c.Header("X-Cache-Creation-Tokens", fmt.Sprintf("%d", cacheCreation))
109-
c.Header("X-Cache-Read-Tokens", fmt.Sprintf("%d", cacheRead))
108+
setCacheHeaders(c, promptHash, cacheCreation, cacheRead)
110109

111110
result := duckgo.Handler(c, response, translated_request, original_request.Stream, stats)
112111

@@ -144,6 +143,19 @@ func messagesText(messages []officialtypes.ApiMessage) string {
144143
return sb.String()
145144
}
146145

146+
// setCacheHeaders sets both the existing custom cache headers and the standard
147+
// HTTP cache headers (X-Cache / Age) that external cache-statistics software
148+
// (Varnish, Squid, CDNs, monitoring) can read.
149+
func setCacheHeaders(c *gin.Context, promptHash string, cacheCreation, cacheRead int) {
150+
c.Header("X-Cache-Creation-Tokens", fmt.Sprintf("%d", cacheCreation))
151+
c.Header("X-Cache-Read-Tokens", fmt.Sprintf("%d", cacheRead))
152+
xCache, age := util.CacheHeaders(promptHash)
153+
c.Header("X-Cache", xCache)
154+
if xCache == "HIT" {
155+
c.Header("Age", fmt.Sprintf("%d", age))
156+
}
157+
}
158+
147159
func (h *Handler) responses(c *gin.Context) {
148160
var responseRequest officialtypes.ResponseAPIRequest
149161
err := c.BindJSON(&responseRequest)
@@ -173,6 +185,10 @@ func (h *Handler) responses(c *gin.Context) {
173185
cacheCreation, cacheRead := util.RecordCache(promptHash, inputTokens)
174186
cachedTokens := cacheRead
175187

188+
// Cache breakdown is known before streaming starts, so set these headers
189+
// before the first chunk is flushed (works for both stream and non-stream).
190+
setCacheHeaders(c, promptHash, cacheCreation, cacheRead)
191+
176192
translatedRequest, response, err := h.startDuckDuckGoRequest(chatRequest)
177193
if err != nil {
178194
c.JSON(500, gin.H{
@@ -189,8 +205,7 @@ func (h *Handler) responses(c *gin.Context) {
189205
}
190206

191207
// Cache breakdown is known before streaming; set before first flush.
192-
c.Header("X-Cache-Creation-Tokens", fmt.Sprintf("%d", cacheCreation))
193-
c.Header("X-Cache-Read-Tokens", fmt.Sprintf("%d", cacheRead))
208+
setCacheHeaders(c, promptHash, cacheCreation, cacheRead)
194209

195210
start := time.Now()
196211
stats := duckgo.HandlerStats{

initialize/messages.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ func (h *Handler) messagesHandler(c *gin.Context) {
5555

5656
// Input token counting + prompt-cache simulation.
5757
inputTokens := util.CountMessagesTokens(apiReq.Messages)
58+
cacheCreation, cacheRead := util.RecordAnthropicCache(req)
5859
promptHash := util.HashPrompt(messagesText(apiReq.Messages))
59-
cacheCreation, cacheRead := util.RecordCache(promptHash, inputTokens)
60-
cachedTokens := cacheRead
60+
if cacheCreation == 0 && cacheRead == 0 {
61+
// Fallback to prompt hash simulation if request has no explicit cache_control blocks
62+
cacheCreation, cacheRead = util.RecordCache(promptHash, inputTokens)
63+
}
6164

6265
translated, response, err := h.startDuckDuckGoRequest(apiReq)
6366
if err != nil {
@@ -77,11 +80,10 @@ func (h *Handler) messagesHandler(c *gin.Context) {
7780
}
7881

7982
// Cache breakdown is known before streaming; set before first flush.
80-
c.Header("X-Cache-Creation-Tokens", fmt.Sprintf("%d", cacheCreation))
81-
c.Header("X-Cache-Read-Tokens", fmt.Sprintf("%d", cacheRead))
83+
setCacheHeaders(c, promptHash, cacheCreation, cacheRead)
8284

8385
start := time.Now()
84-
result := handleAnthropicStream(c, response.Body, req.Model, req.Stream, start, inputTokens, cachedTokens, effort)
86+
result := handleAnthropicStream(c, response.Body, req.Model, req.Stream, start, inputTokens, cacheCreation, cacheRead, effort)
8587

8688
// Timing headers (only delivered for non-stream; for stream the same values
8789
// live in the message_delta event).
@@ -93,13 +95,13 @@ func (h *Handler) messagesHandler(c *gin.Context) {
9395
}
9496

9597
if !req.Stream {
96-
c.JSON(200, buildAnthropicResponse(req.Model, result, inputTokens, cachedTokens, effort))
98+
c.JSON(200, buildAnthropicResponse(req.Model, result, inputTokens, cacheCreation, cacheRead, effort))
9799
}
98100
}
99101

100102
// handleAnthropicStream reads DuckDuckGo's text SSE and emits Anthropic SSE events.
101103
// For non-stream it accumulates the text and returns the result.
102-
func handleAnthropicStream(c *gin.Context, body io.ReadCloser, model string, stream bool, start time.Time, inputTokens, cachedTokens int, effort string) anthropicStreamResult {
104+
func handleAnthropicStream(c *gin.Context, body io.ReadCloser, model string, stream bool, start time.Time, inputTokens, cacheCreation, cacheRead int, effort string) anthropicStreamResult {
103105
defer body.Close()
104106

105107
reader := bufio.NewReader(body)
@@ -124,7 +126,11 @@ func handleAnthropicStream(c *gin.Context, body io.ReadCloser, model string, str
124126
Role: "assistant",
125127
Model: model,
126128
Content: []anthropic.ContentBlock{},
127-
Usage: anthropic.AnthropicUsage{InputTokens: inputTokens},
129+
Usage: anthropic.AnthropicUsage{
130+
InputTokens: inputTokens,
131+
CacheCreationInputTokens: cacheCreation,
132+
CacheReadInputTokens: cacheRead,
133+
},
128134
},
129135
})
130136
writeEvent(c, "content_block_start", anthropic.ContentBlockStartEvent{
@@ -210,13 +216,12 @@ func writeEvent(c *gin.Context, eventType string, payload interface{}) {
210216
}
211217

212218
// buildAnthropicResponse builds the non-stream MessagesResponse.
213-
func buildAnthropicResponse(model string, r anthropicStreamResult, inputTokens, cachedTokens int, effort string) anthropic.MessagesResponse {
219+
func buildAnthropicResponse(model string, r anthropicStreamResult, inputTokens, cacheCreation, cacheRead int, effort string) anthropic.MessagesResponse {
214220
usage := anthropic.AnthropicUsage{
215-
InputTokens: inputTokens,
216-
OutputTokens: r.outputTokens,
217-
}
218-
if cachedTokens > 0 {
219-
usage.CacheReadInputTokens = cachedTokens
221+
InputTokens: inputTokens,
222+
OutputTokens: r.outputTokens,
223+
CacheCreationInputTokens: cacheCreation,
224+
CacheReadInputTokens: cacheRead,
220225
}
221226
return anthropic.MessagesResponse{
222227
ID: "msg_" + util.RandomHexadecimalString(),

typings/anthropic/messages.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ type ContentBlock struct {
4646
ToolUseID string `json:"tool_use_id,omitempty"` // tool_result
4747
Content json.RawMessage `json:"content,omitempty"` // tool_result
4848
IsError bool `json:"is_error,omitempty"`
49-
Thinking string `json:"thinking,omitempty"` // thinking block
50-
Signature string `json:"signature,omitempty"`
49+
Thinking string `json:"thinking,omitempty"` // thinking block
50+
Signature string `json:"signature,omitempty"`
51+
CacheControl json.RawMessage `json:"cache_control,omitempty"`
5152
}
5253

5354
// ImageSource describes a base64 image in Anthropic format.

util/cache_tracker.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package util
2+
3+
import (
4+
"encoding/json"
5+
"sync"
6+
"time"
7+
8+
anthropic "aurora/typings/anthropic"
9+
)
10+
11+
type cacheBlock struct {
12+
fp string
13+
tokens int
14+
}
15+
16+
type cacheTracker struct {
17+
mu sync.Mutex
18+
seen map[string]time.Time
19+
ttl time.Duration
20+
}
21+
22+
var globalCacheTracker = &cacheTracker{
23+
seen: make(map[string]time.Time),
24+
ttl: 5 * time.Minute,
25+
}
26+
27+
func cacheFingerprint(role, blockType, text string) string {
28+
return role + "|" + blockType + "|" + text
29+
}
30+
31+
func (t *cacheTracker) RecordAnthropic(req anthropic.MessagesRequest) (creationTokens, readTokens int) {
32+
t.mu.Lock()
33+
defer t.mu.Unlock()
34+
35+
now := time.Now()
36+
for fp, seenAt := range t.seen {
37+
if now.Sub(seenAt) > t.ttl {
38+
delete(t.seen, fp)
39+
}
40+
}
41+
42+
blocks := collectCacheBlocks(req)
43+
if len(blocks) == 0 {
44+
return 0, 0
45+
}
46+
47+
for _, b := range blocks {
48+
if _, ok := t.seen[b.fp]; ok {
49+
readTokens += b.tokens
50+
} else {
51+
creationTokens += b.tokens
52+
t.seen[b.fp] = now
53+
}
54+
}
55+
return creationTokens, readTokens
56+
}
57+
58+
func collectCacheBlocks(req anthropic.MessagesRequest) []cacheBlock {
59+
var blocks []cacheBlock
60+
61+
// system blocks
62+
if len(req.System) > 0 {
63+
if req.System[0] == '[' {
64+
var rawBlocks []map[string]interface{}
65+
if err := json.Unmarshal(req.System, &rawBlocks); err == nil {
66+
for _, m := range rawBlocks {
67+
if _, hasCache := m["cache_control"]; hasCache {
68+
text, _ := m["text"].(string)
69+
fp := cacheFingerprint("system", "text", text)
70+
tokens := CountToken(text)
71+
if tokens < 1 && len(text) > 0 {
72+
tokens = 1
73+
}
74+
blocks = append(blocks, cacheBlock{fp, tokens})
75+
}
76+
}
77+
}
78+
}
79+
}
80+
81+
// message blocks
82+
for _, msg := range req.Messages {
83+
if len(msg.Content) == 0 || msg.Content[0] != '[' {
84+
continue
85+
}
86+
var rawBlocks []map[string]interface{}
87+
if err := json.Unmarshal(msg.Content, &rawBlocks); err != nil {
88+
continue
89+
}
90+
for _, m := range rawBlocks {
91+
if _, hasCache := m["cache_control"]; hasCache {
92+
text, _ := m["text"].(string)
93+
typ, _ := m["type"].(string)
94+
fp := cacheFingerprint(msg.Role, typ, text)
95+
tokens := CountToken(text)
96+
if tokens < 1 && len(text) > 0 {
97+
tokens = 1
98+
}
99+
blocks = append(blocks, cacheBlock{fp, tokens})
100+
}
101+
}
102+
}
103+
104+
return blocks
105+
}
106+
107+
// RecordAnthropicCache parses real cache_control blocks in Anthropic requests.
108+
func RecordAnthropicCache(req anthropic.MessagesRequest) (creationTokens, readTokens int) {
109+
return globalCacheTracker.RecordAnthropic(req)
110+
}

util/util.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const cacheTTL = 5 * time.Minute
9090

9191
type cacheEntry struct {
9292
tokens int
93+
createdAt time.Time // when the entry was created (for Age header)
9394
expiresAt time.Time
9495
}
9596

@@ -119,10 +120,39 @@ func RecordCache(promptHash string, tokens int) (cacheCreation int, cacheRead in
119120
if e, ok := cacheStore[promptHash]; ok && now.Before(e.expiresAt) {
120121
return 0, e.tokens
121122
}
122-
cacheStore[promptHash] = cacheEntry{tokens: tokens, expiresAt: now.Add(cacheTTL)}
123+
cacheStore[promptHash] = cacheEntry{tokens: tokens, createdAt: now, expiresAt: now.Add(cacheTTL)}
123124
return tokens, 0
124125
}
125126

127+
// CacheStatus reports whether a prompt hash is currently cached (HIT vs MISS)
128+
// and how many seconds ago the entry was created (for the Age header).
129+
// Returns (hit bool, ageSeconds int). ageSeconds is 0 when not hit.
130+
func CacheStatus(promptHash string) (hit bool, ageSeconds int) {
131+
if promptHash == "" {
132+
return false, 0
133+
}
134+
cacheMu.Lock()
135+
defer cacheMu.Unlock()
136+
137+
now := time.Now()
138+
if e, ok := cacheStore[promptHash]; ok && now.Before(e.expiresAt) {
139+
return true, int(now.Sub(e.createdAt).Seconds())
140+
}
141+
return false, 0
142+
}
143+
144+
// CacheHeaders returns the standard HTTP cache headers so external
145+
// cache-statistics/monitoring software (Varnish, Squid, CDNs) can read them.
146+
// - X-Cache: "HIT" or "MISS"
147+
// - Age: <seconds> (omitted on MISS)
148+
func CacheHeaders(promptHash string) (xCache string, ageSeconds int) {
149+
hit, age := CacheStatus(promptHash)
150+
if hit {
151+
return "HIT", age
152+
}
153+
return "MISS", 0
154+
}
155+
126156
// HashPrompt builds a stable hash for prompt cache keying.
127157
func HashPrompt(s string) string {
128158
h := sha256.Sum256([]byte(s))

0 commit comments

Comments
 (0)