File size: 2,326 Bytes
6bc074c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package handler

import (
	"strings"
	"sync"
	"time"
)

// cacheTracker simulates prompt-caching metrics per conversation. ChatGPT
// Web does not expose real caching, so we fingerprint text blocks and report
// "cache_write" on first sight and "cached" on reuse. Design ported from
// D:/Go/claude2api/handlers/cache.go.
type cacheTracker struct {
	mu   sync.Mutex
	seen map[string]time.Time // fingerprint -> first seen
	ttl  time.Duration
}

var globalCacheTracker = &cacheTracker{
	seen: make(map[string]time.Time),
	ttl:  5 * time.Minute,
}

// estTokens returns a rough token count (~4 chars/token).
func estTokens(text string) int {
	text = strings.TrimSpace(text)
	if text == "" {
		return 0
	}
	n := len(text) / 4
	if n < 1 {
		n = 1
	}
	return n
}

// cacheFP derives a stable key for a content block.
func cacheFP(part string) string {
	return part
}

// record processes the request text blocks and returns (cacheWriteTokens, cachedTokens).
// Return order is creation-first so the public RecordCache matches the
// test expectations: first sight yields cache_write > 0, reuse yields cached > 0.
func (t *cacheTracker) record(conversationID, instructions, input string) (cacheWriteTokens, cachedTokens int) {
	t.mu.Lock()
	defer t.mu.Unlock()

	t.gc()

	type block struct {
		fp     string
		tokens int
	}
	var blocks []block
	if instructions != "" {
		blocks = append(blocks, block{cacheFP("instructions:" + instructions), estTokens(instructions)})
	}
	if input != "" {
		blocks = append(blocks, block{cacheFP("input:" + input), estTokens(input)})
	}

	for _, b := range blocks {
		if b.tokens == 0 {
			continue
		}
		if _, ok := t.seen[b.fp]; ok {
			cachedTokens += b.tokens
		} else {
			cacheWriteTokens += b.tokens
			t.seen[b.fp] = time.Now()
		}
	}
	return
}

// gc evicts expired fingerprints.
func (t *cacheTracker) gc() {
	now := time.Now()
	for fp, seen := range t.seen {
		if now.Sub(seen) > t.ttl {
			delete(t.seen, fp)
		}
	}
}

// RecordCache is the package-level entry point.
// Returns (cacheWriteTokens, cachedTokens) — creation first, matching the
// claude2api convention and the cacheTracker.record implementation.
func RecordCache(conversationID, instructions, input string) (cacheWriteTokens, cachedTokens int) {
	return globalCacheTracker.record(conversationID, instructions, input)
}