├── assets └── demo.png ├── docker-compose.yml ├── GITHUB_METADATA.md ├── Makefile ├── internal ├── storage │ ├── storage.go │ └── badger.go ├── semantic │ ├── vector.go │ ├── vector_test.go │ ├── semantic.go │ ├── semantic_test.go │ └── openai_provider.go └── cache │ ├── cache.go │ └── cache_test.go ├── .gitignore ├── Dockerfile ├── LICENSE ├── .github └── workflows │ └── release.yml ├── main.go ├── go.mod ├── cmd └── api │ └── main.go ├── README.md └── go.sum /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/messkan/prompt-cache/HEAD/assets/demo.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | prompt-cache: 5 | build: . 6 | ports: 7 | - "8080:8080" 8 | environment: 9 | - OPENAI_API_KEY=${OPENAI_API_KEY} 10 | volumes: 11 | - ./badger_data:/root/badger_data 12 | restart: unless-stopped 13 | -------------------------------------------------------------------------------- /GITHUB_METADATA.md: -------------------------------------------------------------------------------- 1 | # GitHub Repository Metadata 2 | 3 | ## Description 4 | **PromptCache**: Cut LLM costs by 80% and reduce latency to sub-second speeds with intelligent semantic caching. Drop-in OpenAI replacement written in Go. 5 | 6 | ## Tags 7 | `go`, `llm`, `openai`, `cache`, `semantic-search`, `vector-database`, `rag`, `ai`, `performance`, `middleware`, `cost-optimization`, `badgerdb` 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=prompt-cache 2 | 3 | .PHONY: all build run test clean 4 | 5 | all: build 6 | 7 | build: 8 | go build -o $(BINARY_NAME) ./cmd/api 9 | 10 | run: build 11 | @if [ -z "$(OPENAI_API_KEY)" ]; then \ 12 | echo "Error: OPENAI_API_KEY is not set"; \ 13 | exit 1; \ 14 | fi 15 | ./$(BINARY_NAME) 16 | 17 | test: 18 | go test ./... 19 | 20 | clean: 21 | go clean 22 | rm -f $(BINARY_NAME) 23 | rm -rf badger_data 24 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Storage interface { 8 | Set(ctx context.Context, key string, value []byte) error 9 | Get(ctx context.Context, key string) ([]byte, error) 10 | Delete(ctx context.Context, key string) error 11 | GetAllEmbeddings(ctx context.Context) (map[string][]byte, error) 12 | GetPrompt(ctx context.Context, key string) (string, error) 13 | Close() 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.test 8 | *.out 9 | *.o 10 | 11 | # Go build cache, modules, etc. 12 | bin/ 13 | build/ 14 | dist/ 15 | coverage.out 16 | 17 | # Vendor (si tu l'utilises) 18 | vendor/ 19 | 20 | # Logs 21 | *.log 22 | 23 | # Editor / VSCode / JetBrains 24 | .vscode/ 25 | .idea/ 26 | 27 | # OS files 28 | .DS_Store 29 | Thumbs.db 30 | 31 | # Env files (si tu en utilises pour clés API) 32 | .env 33 | .env.* 34 | 35 | # Data 36 | badger_data/ 37 | 38 | # Debug scripts 39 | cmd/debug*/ 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM golang:1.24-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Install build dependencies 7 | RUN apk add --no-cache git make build-base 8 | 9 | # Copy go mod and sum files 10 | COPY go.mod go.sum ./ 11 | 12 | # Download dependencies 13 | RUN go mod download 14 | 15 | # Copy source code 16 | COPY . . 17 | 18 | # Build the application 19 | RUN CGO_ENABLED=0 GOOS=linux go build -o prompt-cache ./cmd/api 20 | 21 | # Final Stage 22 | FROM alpine:latest 23 | 24 | WORKDIR /root/ 25 | 26 | # Install ca-certificates for HTTPS requests 27 | RUN apk --no-cache add ca-certificates 28 | 29 | # Copy binary from builder 30 | COPY --from=builder /app/prompt-cache . 31 | 32 | # Expose port 33 | EXPOSE 8080 34 | 35 | # Run 36 | CMD ["./prompt-cache"] 37 | -------------------------------------------------------------------------------- /internal/semantic/vector.go: -------------------------------------------------------------------------------- 1 | package semantic 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | func CosineSimilarity(a, b []float32) float32 { 9 | var dot, normA, normB float32 10 | 11 | for i := range a { 12 | dot += a[i] * b[i] 13 | normA += a[i] * a[i] 14 | normB += b[i] * b[i] 15 | } 16 | 17 | if normA == 0 || normB == 0 { 18 | return 0 19 | } 20 | 21 | return dot / (float32(math.Sqrt(float64(normA))) * float32(math.Sqrt(float64(normB)))) 22 | } 23 | 24 | func BytesToFloat32(data []byte) []float32 { 25 | size := len(data) / 4 26 | res := make([]float32, size) 27 | for i := 0; i < size; i++ { 28 | bits := binary.LittleEndian.Uint32(data[i*4 : i*4+4]) 29 | res[i] = math.Float32frombits(bits) 30 | } 31 | return res 32 | } 33 | 34 | func Float32ToBytes(data []float32) []byte { 35 | size := len(data) * 4 36 | res := make([]byte, size) 37 | for i, v := range data { 38 | bits := math.Float32bits(v) 39 | binary.LittleEndian.PutUint32(res[i*4:], bits) 40 | } 41 | return res 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Messkan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Extract metadata (tags, labels) for Docker 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: | 41 | ghcr.io/${{ github.repository }} 42 | ${{ secrets.DOCKERHUB_USERNAME }}/prompt-cache 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v5 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | -------------------------------------------------------------------------------- /internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "time" 9 | 10 | "github.com/messkan/PromptCache/internal/storage" 11 | ) 12 | 13 | type Cache struct { 14 | store storage.Storage 15 | } 16 | 17 | type CacheItem struct { 18 | Response []byte `json:"response"` 19 | CreatedAt time.Time `json:"created_at"` 20 | TTL time.Duration `json:"ttl"` 21 | } 22 | 23 | func NewCache(store storage.Storage) *Cache { 24 | return &Cache{store: store} 25 | } 26 | 27 | func GenerateKey(input string) string { 28 | h := sha256.Sum256([]byte(input)) 29 | return hex.EncodeToString(h[:]) 30 | } 31 | 32 | func (c *Cache) Set(ctx context.Context, key string, response []byte, ttl time.Duration) error { 33 | item := CacheItem{ 34 | Response: response, 35 | CreatedAt: time.Now(), 36 | TTL: ttl, 37 | } 38 | 39 | data, err := json.Marshal(item) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return c.store.Set(ctx, key, data) 45 | } 46 | 47 | func (c *Cache) Get(ctx context.Context, key string) ([]byte, bool, error) { 48 | data, err := c.store.Get(ctx, key) 49 | if err != nil { 50 | return nil, false, err 51 | } 52 | if data == nil { 53 | return nil, false, nil 54 | } 55 | 56 | var item CacheItem 57 | if err := json.Unmarshal(data, &item); err != nil { 58 | return nil, false, err 59 | } 60 | 61 | if item.TTL != 0 && time.Since(item.CreatedAt) > item.TTL { 62 | return nil, false, nil 63 | } 64 | 65 | return item.Response, true, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/semantic/vector_test.go: -------------------------------------------------------------------------------- 1 | package semantic 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestCosineSimilarity(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | a []float32 12 | b []float32 13 | want float32 14 | }{ 15 | { 16 | name: "Identical vectors", 17 | a: []float32{1, 0, 0}, 18 | b: []float32{1, 0, 0}, 19 | want: 1.0, 20 | }, 21 | { 22 | name: "Orthogonal vectors", 23 | a: []float32{1, 0, 0}, 24 | b: []float32{0, 1, 0}, 25 | want: 0.0, 26 | }, 27 | { 28 | name: "Opposite vectors", 29 | a: []float32{1, 0, 0}, 30 | b: []float32{-1, 0, 0}, 31 | want: -1.0, 32 | }, 33 | { 34 | name: "Similar vectors", 35 | a: []float32{1, 1, 0}, 36 | b: []float32{1, 1, 0}, 37 | want: 1.0, // Normalized, they point in same direction 38 | }, 39 | } 40 | 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | got := CosineSimilarity(tt.a, tt.b) 44 | if math.Abs(float64(got-tt.want)) > 1e-6 { 45 | t.Errorf("CosineSimilarity() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestFloat32BytesConversion(t *testing.T) { 52 | original := []float32{0.1, 0.2, 0.5, -1.0, 3.14159} 53 | 54 | bytes := Float32ToBytes(original) 55 | restored := BytesToFloat32(bytes) 56 | 57 | if len(original) != len(restored) { 58 | t.Fatalf("Length mismatch: got %d, want %d", len(restored), len(original)) 59 | } 60 | 61 | for i := range original { 62 | if original[i] != restored[i] { 63 | t.Errorf("Index %d: got %f, want %f", i, restored[i], original[i]) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/dgraph-io/badger/v4" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func main() { 13 | opts := badger.DefaultOptions("./data") 14 | opts.WithLogger(nil) 15 | db, err := badger.Open(opts) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer db.Close() 20 | 21 | r := gin.Default() 22 | 23 | r.GET("/health", func(c *gin.Context) { 24 | c.String(http.StatusOK, "OK") 25 | }) 26 | 27 | r.POST("/cache/set", func(c *gin.Context) { 28 | var body struct { 29 | Key string `json:"key" binding:"required"` 30 | Value string `json:"value" binding:"required"` 31 | TTLSeconds int `json:"ttl_seconds" binding:"required"` 32 | } 33 | if err := c.ShouldBindJSON(&body); err != nil { 34 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 35 | return 36 | } 37 | err := db.Update(func(txn *badger.Txn) error { 38 | e := badger.NewEntry([]byte(body.Key), []byte(body.Value)) 39 | if body.TTLSeconds > 0 { 40 | e.WithTTL(time.Duration(body.TTLSeconds) * time.Second) 41 | } 42 | return txn.SetEntry(e) 43 | }) 44 | if err != nil { 45 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 46 | return 47 | } 48 | c.JSON(http.StatusOK, gin.H{"ok": true}) 49 | }) 50 | 51 | r.GET("/cache/get/:key", func(c *gin.Context) { 52 | key := c.Param("key") 53 | var val []byte 54 | err := db.View(func(txn *badger.Txn) error { 55 | item, err := txn.Get([]byte(key)) 56 | if err != nil { 57 | return err 58 | } 59 | return item.Value(func(v []byte) error { 60 | val = append([]byte{}, v...) 61 | return nil 62 | }) 63 | }) 64 | if err != nil { 65 | if err == badger.ErrKeyNotFound { 66 | c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) 67 | return 68 | } 69 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 70 | return 71 | } 72 | c.JSON(http.StatusOK, gin.H{"key": key, "value": string(val)}) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /internal/storage/badger.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dgraph-io/badger/v4" 7 | ) 8 | 9 | type BadgerStore struct { 10 | db *badger.DB 11 | } 12 | 13 | func NewBadgerStore(path string) (*BadgerStore, error) { 14 | opts := badger.DefaultOptions(path) 15 | db, err := badger.Open(opts) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return &BadgerStore{db: db}, nil 20 | } 21 | 22 | func (s *BadgerStore) Set(ctx context.Context, key string, value []byte) error { 23 | return s.db.Update(func(txn *badger.Txn) error { 24 | return txn.Set([]byte(key), value) 25 | }) 26 | } 27 | 28 | func (s *BadgerStore) Get(ctx context.Context, key string) ([]byte, error) { 29 | var valCopy []byte 30 | err := s.db.View(func(txn *badger.Txn) error { 31 | item, err := txn.Get([]byte(key)) 32 | if err != nil { 33 | return err 34 | } 35 | valCopy, err = item.ValueCopy(nil) 36 | return err 37 | }) 38 | return valCopy, err 39 | } 40 | 41 | func (s *BadgerStore) Delete(ctx context.Context, key string) error { 42 | return s.db.Update(func(txn *badger.Txn) error { 43 | return txn.Delete([]byte(key)) 44 | }) 45 | } 46 | 47 | func (s *BadgerStore) GetAllEmbeddings(ctx context.Context) (map[string][]byte, error) { 48 | results := make(map[string][]byte) 49 | err := s.db.View(func(txn *badger.Txn) error { 50 | opts := badger.DefaultIteratorOptions 51 | opts.PrefetchSize = 10 52 | it := txn.NewIterator(opts) 53 | defer it.Close() 54 | 55 | prefix := []byte("emb:") 56 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 57 | item := it.Item() 58 | k := item.Key() 59 | err := item.Value(func(v []byte) error { 60 | results[string(k)] = append([]byte{}, v...) 61 | return nil 62 | }) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | return nil 68 | }) 69 | return results, err 70 | } 71 | 72 | func (s *BadgerStore) GetPrompt(ctx context.Context, key string) (string, error) { 73 | var valCopy []byte 74 | err := s.db.View(func(txn *badger.Txn) error { 75 | item, err := txn.Get([]byte("prompt:" + key)) 76 | if err != nil { 77 | return err 78 | } 79 | valCopy, err = item.ValueCopy(nil) 80 | return err 81 | }) 82 | return string(valCopy), err 83 | } 84 | 85 | func (s *BadgerStore) Close() { 86 | s.db.Close() 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/messkan/PromptCache 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/dgraph-io/badger/v4 v4.8.0 7 | github.com/gin-gonic/gin v1.11.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/gopkg v0.1.3 // indirect 12 | github.com/bytedance/sonic v1.14.2 // indirect 13 | github.com/bytedance/sonic/loader v0.4.0 // indirect 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.6 // indirect 16 | github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 17 | github.com/dustin/go-humanize v1.0.1 // indirect 18 | github.com/gabriel-vasile/mimetype v1.4.11 // indirect 19 | github.com/gin-contrib/sse v1.1.0 // indirect 20 | github.com/go-logr/logr v1.4.3 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/go-playground/locales v0.14.1 // indirect 23 | github.com/go-playground/universal-translator v0.18.1 // indirect 24 | github.com/go-playground/validator/v10 v10.28.0 // indirect 25 | github.com/goccy/go-json v0.10.5 // indirect 26 | github.com/goccy/go-yaml v1.18.0 // indirect 27 | github.com/google/flatbuffers v25.2.10+incompatible // indirect 28 | github.com/json-iterator/go v1.1.12 // indirect 29 | github.com/klauspost/compress v1.18.0 // indirect 30 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 31 | github.com/leodido/go-urn v1.4.0 // indirect 32 | github.com/mattn/go-isatty v0.0.20 // indirect 33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 34 | github.com/modern-go/reflect2 v1.0.2 // indirect 35 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 36 | github.com/quic-go/qpack v0.6.0 // indirect 37 | github.com/quic-go/quic-go v0.57.0 // indirect 38 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 39 | github.com/ugorji/go/codec v1.3.1 // indirect 40 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 41 | go.opentelemetry.io/otel v1.37.0 // indirect 42 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 43 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 44 | go.uber.org/mock v0.6.0 // indirect 45 | golang.org/x/arch v0.23.0 // indirect 46 | golang.org/x/crypto v0.45.0 // indirect 47 | golang.org/x/net v0.47.0 // indirect 48 | golang.org/x/sys v0.38.0 // indirect 49 | golang.org/x/text v0.31.0 // indirect 50 | google.golang.org/protobuf v1.36.10 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /internal/semantic/semantic.go: -------------------------------------------------------------------------------- 1 | package semantic 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | type EmbeddingProvider interface { 9 | Embed(ctx context.Context, text string) ([]float32, error) 10 | } 11 | 12 | type Storage interface { 13 | GetAllEmbeddings(ctx context.Context) (map[string][]byte, error) 14 | GetPrompt(ctx context.Context, key string) (string, error) 15 | } 16 | 17 | type Verifier interface { 18 | CheckSimilarity(ctx context.Context, prompt1, prompt2 string) (bool, error) 19 | } 20 | 21 | type SemanticEngine struct { 22 | Provider EmbeddingProvider 23 | Store Storage 24 | Verifier Verifier 25 | HighThreshold float32 26 | LowThreshold float32 27 | } 28 | 29 | func NewSemanticEngine(p EmbeddingProvider, s Storage, v Verifier, highThreshold, lowThreshold float32) *SemanticEngine { 30 | return &SemanticEngine{ 31 | Provider: p, 32 | Store: s, 33 | Verifier: v, 34 | HighThreshold: highThreshold, 35 | LowThreshold: lowThreshold, 36 | } 37 | } 38 | 39 | func (se *SemanticEngine) FindSimilar(ctx context.Context, text string) (string, float32, error) { 40 | queryEmb, err := se.Provider.Embed(ctx, text) 41 | if err != nil { 42 | return "", 0, err 43 | } 44 | 45 | stored, err := se.Store.GetAllEmbeddings(ctx) 46 | if err != nil { 47 | return "", 0, err 48 | } 49 | 50 | bestKey := "" 51 | bestSim := float32(0) 52 | 53 | for key, embBytes := range stored { 54 | embVec := BytesToFloat32(embBytes) 55 | sim := CosineSimilarity(queryEmb, embVec) 56 | 57 | if sim > bestSim { 58 | bestSim = sim 59 | bestKey = key 60 | } 61 | } 62 | 63 | // 1. Clear Match 64 | if bestSim >= se.HighThreshold { 65 | return bestKey, bestSim, nil 66 | } 67 | 68 | // 2. Clear Mismatch 69 | if bestSim < se.LowThreshold { 70 | return "", bestSim, nil 71 | } 72 | 73 | // 3. Gray Zone -> Smart Verification 74 | // The key in storage has "emb:" prefix, we need to strip it to get the hash 75 | hashKey := strings.TrimPrefix(bestKey, "emb:") 76 | 77 | originalPrompt, err := se.Store.GetPrompt(ctx, hashKey) 78 | if err != nil { 79 | // If we can't find the prompt, we can't verify, so we assume miss to be safe 80 | return "", bestSim, nil 81 | } 82 | 83 | isMatch, err := se.Verifier.CheckSimilarity(ctx, text, originalPrompt) 84 | if err != nil { 85 | return "", bestSim, err 86 | } 87 | 88 | if isMatch { 89 | return bestKey, bestSim, nil 90 | } 91 | 92 | return "", bestSim, nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // MockStorage implements storage.Storage for testing 10 | type MockStorage struct { 11 | data map[string][]byte 12 | } 13 | 14 | func NewMockStorage() *MockStorage { 15 | return &MockStorage{ 16 | data: make(map[string][]byte), 17 | } 18 | } 19 | 20 | func (m *MockStorage) Set(ctx context.Context, key string, value []byte) error { 21 | m.data[key] = value 22 | return nil 23 | } 24 | 25 | func (m *MockStorage) Get(ctx context.Context, key string) ([]byte, error) { 26 | val, ok := m.data[key] 27 | if !ok { 28 | return nil, nil // Simulate miss 29 | } 30 | return val, nil 31 | } 32 | 33 | func (m *MockStorage) Delete(ctx context.Context, key string) error { 34 | delete(m.data, key) 35 | return nil 36 | } 37 | 38 | func (m *MockStorage) GetAllEmbeddings(ctx context.Context) (map[string][]byte, error) { 39 | return nil, nil 40 | } 41 | 42 | func (m *MockStorage) GetPrompt(ctx context.Context, key string) (string, error) { 43 | return "", nil 44 | } 45 | 46 | func (m *MockStorage) Close() {} 47 | 48 | func TestCache_SetAndGet(t *testing.T) { 49 | store := NewMockStorage() 50 | c := NewCache(store) 51 | ctx := context.Background() 52 | 53 | key := "test-key" 54 | response := []byte("test-response") 55 | ttl := 1 * time.Hour 56 | 57 | // Test Set 58 | err := c.Set(ctx, key, response, ttl) 59 | if err != nil { 60 | t.Fatalf("Set failed: %v", err) 61 | } 62 | 63 | // Test Get (Hit) 64 | got, found, err := c.Get(ctx, key) 65 | if err != nil { 66 | t.Fatalf("Get failed: %v", err) 67 | } 68 | if !found { 69 | t.Fatal("Expected cache hit, got miss") 70 | } 71 | if string(got) != string(response) { 72 | t.Errorf("Get = %s, want %s", got, response) 73 | } 74 | } 75 | 76 | func TestCache_Expiration(t *testing.T) { 77 | store := NewMockStorage() 78 | c := NewCache(store) 79 | ctx := context.Background() 80 | 81 | key := "expired-key" 82 | response := []byte("expired-response") 83 | ttl := -1 * time.Hour // Already expired 84 | 85 | err := c.Set(ctx, key, response, ttl) 86 | if err != nil { 87 | t.Fatalf("Set failed: %v", err) 88 | } 89 | 90 | // Test Get (Expired) 91 | _, found, err := c.Get(ctx, key) 92 | if err != nil { 93 | t.Fatalf("Get failed: %v", err) 94 | } 95 | if found { 96 | t.Fatal("Expected cache miss (expired), got hit") 97 | } 98 | } 99 | 100 | func TestCache_Miss(t *testing.T) { 101 | store := NewMockStorage() 102 | c := NewCache(store) 103 | ctx := context.Background() 104 | 105 | _, found, err := c.Get(ctx, "non-existent") 106 | if err != nil { 107 | t.Fatalf("Get failed: %v", err) 108 | } 109 | if found { 110 | t.Fatal("Expected cache miss, got hit") 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /internal/semantic/semantic_test.go: -------------------------------------------------------------------------------- 1 | package semantic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | // MockProvider implements EmbeddingProvider 9 | type MockProvider struct { 10 | embedding []float32 11 | } 12 | 13 | func (m *MockProvider) Embed(ctx context.Context, text string) ([]float32, error) { 14 | return m.embedding, nil 15 | } 16 | 17 | // MockStorage implements Storage 18 | type MockStorage struct { 19 | embeddings map[string][]byte 20 | } 21 | 22 | func (m *MockStorage) GetAllEmbeddings(ctx context.Context) (map[string][]byte, error) { 23 | return m.embeddings, nil 24 | } 25 | 26 | func (m *MockStorage) GetPrompt(ctx context.Context, key string) (string, error) { 27 | return "original prompt", nil 28 | } 29 | 30 | func (m *MockStorage) Set(ctx context.Context, key string, value []byte) error { return nil } 31 | func (m *MockStorage) Get(ctx context.Context, key string) ([]byte, error) { return nil, nil } 32 | func (m *MockStorage) Delete(ctx context.Context, key string) error { return nil } 33 | func (m *MockStorage) Close() {} 34 | 35 | // MockVerifier implements Verifier 36 | type MockVerifier struct { 37 | match bool 38 | } 39 | 40 | func (m *MockVerifier) CheckSimilarity(ctx context.Context, prompt1, prompt2 string) (bool, error) { 41 | return m.match, nil 42 | } 43 | 44 | func TestFindSimilar(t *testing.T) { 45 | // Setup 46 | queryVec := []float32{1, 0, 0} 47 | matchVec := []float32{0.99, 0.01, 0} // Very similar 48 | diffVec := []float32{0, 1, 0} // Orthogonal 49 | 50 | provider := &MockProvider{embedding: queryVec} 51 | 52 | store := &MockStorage{ 53 | embeddings: map[string][]byte{ 54 | "emb:match": Float32ToBytes(matchVec), 55 | "emb:diff": Float32ToBytes(diffVec), 56 | }, 57 | } 58 | 59 | verifier := &MockVerifier{match: true} 60 | 61 | engine := NewSemanticEngine(provider, store, verifier, 0.95, 0.80) 62 | 63 | // Test Match (High Confidence) 64 | key, score, err := engine.FindSimilar(context.Background(), "query") 65 | if err != nil { 66 | t.Fatalf("FindSimilar failed: %v", err) 67 | } 68 | 69 | if key != "emb:match" { 70 | t.Errorf("Expected key 'emb:match', got '%s'", key) 71 | } 72 | if score < 0.95 { 73 | t.Errorf("Expected high score, got %f", score) 74 | } 75 | } 76 | 77 | func TestFindSimilar_NoMatch(t *testing.T) { 78 | // Setup 79 | queryVec := []float32{1, 0, 0} 80 | diffVec := []float32{0, 1, 0} // Orthogonal 81 | 82 | provider := &MockProvider{embedding: queryVec} 83 | 84 | store := &MockStorage{ 85 | embeddings: map[string][]byte{ 86 | "emb:diff": Float32ToBytes(diffVec), 87 | }, 88 | } 89 | 90 | verifier := &MockVerifier{match: false} 91 | 92 | engine := NewSemanticEngine(provider, store, verifier, 0.95, 0.80) 93 | 94 | // Test No Match 95 | key, _, err := engine.FindSimilar(context.Background(), "query") 96 | if err != nil { 97 | t.Fatalf("FindSimilar failed: %v", err) 98 | } 99 | 100 | if key != "" { 101 | t.Errorf("Expected empty key (no match), got '%s'", key) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/semantic/openai_provider.go: -------------------------------------------------------------------------------- 1 | package semantic 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | type OpenAIProvider struct { 14 | apiKey string 15 | client *http.Client 16 | } 17 | 18 | func NewOpenAIProvider() *OpenAIProvider { 19 | return &OpenAIProvider{ 20 | apiKey: os.Getenv("OPENAI_API_KEY"), 21 | client: &http.Client{}, 22 | } 23 | } 24 | 25 | type EmbeddingRequest struct { 26 | Input string `json:"input"` 27 | Model string `json:"model"` 28 | } 29 | 30 | type EmbeddingResponse struct { 31 | Data []struct { 32 | Embedding []float64 `json:"embedding"` 33 | } `json:"data"` 34 | } 35 | 36 | func (p *OpenAIProvider) Embed(ctx context.Context, text string) ([]float32, error) { 37 | reqBody := EmbeddingRequest{ 38 | Input: text, 39 | Model: "text-embedding-3-small", 40 | } 41 | jsonBody, err := json.Marshal(reqBody) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/embeddings", bytes.NewBuffer(jsonBody)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | req.Header.Set("Content-Type", "application/json") 52 | req.Header.Set("Authorization", "Bearer "+p.apiKey) 53 | 54 | resp, err := p.client.Do(req) 55 | if err != nil { 56 | return nil, err 57 | } 58 | defer resp.Body.Close() 59 | 60 | if resp.StatusCode != http.StatusOK { 61 | body, _ := io.ReadAll(resp.Body) 62 | return nil, fmt.Errorf("OpenAI API error: %s", string(body)) 63 | } 64 | 65 | var embeddingResp EmbeddingResponse 66 | if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil { 67 | return nil, err 68 | } 69 | 70 | if len(embeddingResp.Data) == 0 { 71 | return nil, fmt.Errorf("no embedding data returned") 72 | } 73 | 74 | // Convert float64 to float32 75 | res := make([]float32, len(embeddingResp.Data[0].Embedding)) 76 | for i, v := range embeddingResp.Data[0].Embedding { 77 | res[i] = float32(v) 78 | } 79 | 80 | return res, nil 81 | } 82 | 83 | type VerificationRequest struct { 84 | Model string `json:"model"` 85 | Messages []Message `json:"messages"` 86 | } 87 | 88 | type Message struct { 89 | Role string `json:"role"` 90 | Content string `json:"content"` 91 | } 92 | 93 | type VerificationResponse struct { 94 | Choices []struct { 95 | Message Message `json:"message"` 96 | } `json:"choices"` 97 | } 98 | 99 | func (p *OpenAIProvider) CheckSimilarity(ctx context.Context, prompt1, prompt2 string) (bool, error) { 100 | systemPrompt := "You are a semantic judge. Determine if the two user prompts have the exact same intent and meaning. Answer only with 'YES' or 'NO'." 101 | userPrompt := fmt.Sprintf("Prompt 1: %s\nPrompt 2: %s", prompt1, prompt2) 102 | 103 | reqBody := VerificationRequest{ 104 | Model: "gpt-4o-mini", 105 | Messages: []Message{ 106 | {Role: "system", Content: systemPrompt}, 107 | {Role: "user", Content: userPrompt}, 108 | }, 109 | } 110 | 111 | jsonBody, err := json.Marshal(reqBody) 112 | if err != nil { 113 | return false, err 114 | } 115 | 116 | req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(jsonBody)) 117 | if err != nil { 118 | return false, err 119 | } 120 | 121 | req.Header.Set("Content-Type", "application/json") 122 | req.Header.Set("Authorization", "Bearer "+p.apiKey) 123 | 124 | resp, err := p.client.Do(req) 125 | if err != nil { 126 | return false, err 127 | } 128 | defer resp.Body.Close() 129 | 130 | if resp.StatusCode != http.StatusOK { 131 | body, _ := io.ReadAll(resp.Body) 132 | return false, fmt.Errorf("OpenAI API error: %s", string(body)) 133 | } 134 | 135 | var verResp VerificationResponse 136 | if err := json.NewDecoder(resp.Body).Decode(&verResp); err != nil { 137 | return false, err 138 | } 139 | 140 | if len(verResp.Choices) == 0 { 141 | return false, fmt.Errorf("no choices returned") 142 | } 143 | 144 | content := verResp.Choices[0].Message.Content 145 | return content == "YES", nil 146 | } 147 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/messkan/PromptCache/internal/cache" 15 | "github.com/messkan/PromptCache/internal/semantic" 16 | "github.com/messkan/PromptCache/internal/storage" 17 | ) 18 | 19 | type ChatCompletionRequest struct { 20 | Model string `json:"model"` 21 | Messages []Message `json:"messages"` 22 | } 23 | 24 | type Message struct { 25 | Role string `json:"role"` 26 | Content string `json:"content"` 27 | } 28 | 29 | func main() { 30 | // Initialize Storage 31 | store, err := storage.NewBadgerStore("./badger_data") 32 | if err != nil { 33 | log.Fatalf("Failed to initialize BadgerDB: %v", err) 34 | } 35 | defer store.Close() 36 | 37 | // Initialize Semantic Engine 38 | openaiProvider := semantic.NewOpenAIProvider() 39 | semanticEngine := semantic.NewSemanticEngine(openaiProvider, store, openaiProvider, 0.95, 0.80) 40 | 41 | // Initialize Cache 42 | c := cache.NewCache(store) 43 | 44 | r := gin.Default() 45 | 46 | r.POST("/v1/chat/completions", func(cGin *gin.Context) { 47 | var req ChatCompletionRequest 48 | // We need to read the body but also keep it for forwarding 49 | bodyBytes, err := io.ReadAll(cGin.Request.Body) 50 | if err != nil { 51 | cGin.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) 52 | return 53 | } 54 | // Restore body for binding 55 | cGin.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 56 | 57 | if err := json.Unmarshal(bodyBytes, &req); err != nil { 58 | cGin.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) 59 | return 60 | } 61 | 62 | // Extract prompt (last user message) 63 | prompt := "" 64 | for i := len(req.Messages) - 1; i >= 0; i-- { 65 | if req.Messages[i].Role == "user" { 66 | prompt = req.Messages[i].Content 67 | break 68 | } 69 | } 70 | 71 | if prompt == "" { 72 | cGin.JSON(http.StatusBadRequest, gin.H{"error": "No user prompt found"}) 73 | return 74 | } 75 | 76 | ctx := cGin.Request.Context() 77 | 78 | // 1. Check Semantic Cache 79 | similarKey, score, err := semanticEngine.FindSimilar(ctx, prompt) 80 | if err != nil { 81 | log.Printf("Semantic search error: %v", err) 82 | } 83 | 84 | if similarKey != "" { 85 | log.Printf("🔥 Cache HIT! Score: %f, Key: %s", score, similarKey) 86 | // The key in semantic storage has "emb:" prefix, but cache storage does not. 87 | actualKey := strings.TrimPrefix(similarKey, "emb:") 88 | cachedResp, found, err := c.Get(ctx, actualKey) 89 | if err == nil && found { 90 | cGin.Data(http.StatusOK, "application/json", cachedResp) 91 | return 92 | } 93 | } 94 | 95 | log.Println("💨 Cache MISS. Forwarding to OpenAI...") 96 | 97 | // 2. Forward to OpenAI 98 | apiKey := os.Getenv("OPENAI_API_KEY") 99 | openAIReq, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(bodyBytes)) 100 | openAIReq.Header.Set("Content-Type", "application/json") 101 | openAIReq.Header.Set("Authorization", "Bearer "+apiKey) 102 | 103 | client := &http.Client{} 104 | resp, err := client.Do(openAIReq) 105 | if err != nil { 106 | log.Printf("Failed to call OpenAI: %v", err) 107 | cGin.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to call OpenAI: " + err.Error()}) 108 | return 109 | } 110 | defer resp.Body.Close() 111 | 112 | respBody, err := io.ReadAll(resp.Body) 113 | if err != nil { 114 | log.Printf("Failed to read OpenAI response: %v", err) 115 | cGin.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read OpenAI response: " + err.Error()}) 116 | return 117 | } 118 | 119 | // 3. Cache Response & Embedding 120 | if resp.StatusCode == http.StatusOK { 121 | key := cache.GenerateKey(prompt) 122 | 123 | // Save Response 124 | if err := c.Set(ctx, key, respBody, 24*time.Hour); err != nil { 125 | log.Printf("Failed to cache response: %v", err) 126 | } 127 | 128 | // Save Prompt for Verification 129 | if err := store.Set(ctx, "prompt:"+key, []byte(prompt)); err != nil { 130 | log.Printf("Failed to save prompt: %v", err) 131 | } 132 | 133 | // Save Embedding 134 | embedding, err := openaiProvider.Embed(ctx, prompt) 135 | if err == nil { 136 | embBytes := semantic.Float32ToBytes(embedding) 137 | if err := store.Set(ctx, "emb:"+key, embBytes); err != nil { 138 | log.Printf("Failed to save embedding: %v", err) 139 | } 140 | } else { 141 | log.Printf("Failed to generate embedding: %v", err) 142 | } 143 | } 144 | 145 | cGin.Data(resp.StatusCode, "application/json", respBody) 146 | }) 147 | 148 | log.Println("🚀 PromptCache Server running on :8080") 149 | r.Run(":8080") 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 PromptCache 2 | 3 | ### **Reduce your LLM costs. Accelerate your application.** 4 | 5 | **A smart semantic cache for high-scale GenAI workloads.** 6 | 7 | ![Go](https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat\&logo=go) 8 | ![License](https://img.shields.io/badge/license-MIT-green) 9 | 10 | ![PromptCache Demo](assets/demo.png) 11 | 12 | > [!WARNING] 13 | > **v0.1.0 is currently in Alpha.** It is not yet production-ready. 14 | > Significant improvements in stability, performance, and configuration are coming in v0.2.0. 15 | --- 16 | 17 | ## 💰 The Problem 18 | 19 | In production, **a large percentage of LLM requests are repetitive**: 20 | 21 | * **RAG applications**: Variations of the same employee questions 22 | * **AI Agents**: Repeated reasoning steps or tool calls 23 | * **Support Bots**: Thousands of similar customer queries 24 | 25 | Every redundant request means **extra token cost** and **extra latency**. 26 | 27 | Why pay your LLM provider multiple times for the *same answer*? 28 | 29 | --- 30 | 31 | ## 💡 The Solution: PromptCache 32 | 33 | PromptCache is a lightweight middleware that sits between your application and your LLM provider. 34 | It uses **semantic understanding** to detect when a new prompt has *the same intent* as a previous one — and returns the cached result instantly. 35 | 36 | --- 37 | 38 | ## 📊 Key Benefits 39 | 40 | | Metric | Without Cache | With PromptCache | Benefit | 41 | | --------------------------- | ------------- | ---------------- | ------------ | 42 | | **Cost per 1,000 Requests** | ≈ $30 | **≈ $6** | Lower cost | 43 | | **Avg Latency** | ~1.5s | **~300ms** | Faster UX | 44 | | **Throughput** | API-limited | **Unlimited** | Better scale | 45 | 46 | Numbers vary per model, but the pattern holds across real workloads: 47 | **semantic caching dramatically reduces cost and latency**. 48 | 49 | \* Results may vary depending on model, usage patterns, and configuration. 50 | 51 | --- 52 | 53 | ## 🧠 Smart Semantic Matching (Safer by Design) 54 | 55 | Naive semantic caches can be risky — they may return incorrect answers when prompts look similar but differ in intent. 56 | 57 | PromptCache uses a **two-stage verification strategy** to ensure accuracy: 58 | 59 | 1. **High similarity → direct cache hit** 60 | 2. **Low similarity → skip cache directly** 61 | 3. **Gray zone → intent check using a small, cheap verification model** 62 | 63 | This ensures cached responses are **semantically correct**, not just “close enough”. 64 | 65 | --- 66 | 67 | ## 🚀 Quick Start 68 | 69 | PromptCache works as a **drop-in replacement** for the OpenAI API. 70 | 71 | ### 1. Run with Docker (Recommended) 72 | 73 | ```bash 74 | # Clone the repo 75 | git clone https://github.com/messkan/prompt-cache.git 76 | cd prompt-cache 77 | 78 | # Run with Docker Compose 79 | export OPENAI_API_KEY=your_key_here 80 | docker-compose up -d 81 | ``` 82 | 83 | ### 2. Run from Source 84 | 85 | Simply change the `base_url` in your SDK: 86 | 87 | ```python 88 | from openai import OpenAI 89 | 90 | client = OpenAI( 91 | base_url="http://localhost:8080/v1", # Point to PromptCache 92 | api_key="sk-..." 93 | ) 94 | 95 | # First request → goes to the LLM provider 96 | client.chat.completions.create( 97 | model="gpt-4", 98 | messages=[{"role": "user", "content": "Explain quantum physics"}] 99 | ) 100 | 101 | # Semantically similar request → served from PromptCache 102 | client.chat.completions.create( 103 | model="gpt-4", 104 | messages=[{"role": "user", "content": "How does quantum physics work?"}] 105 | ) 106 | ``` 107 | 108 | No code changes. Just point your client to PromptCache. 109 | 110 | --- 111 | 112 | ## 🏗 Architecture Overview 113 | 114 | Built for speed, safety, and reliability: 115 | 116 | * **Pure Go implementation** (high concurrency, minimal overhead) 117 | * **BadgerDB** for fast embedded persistent storage 118 | * **In-memory caching** for ultra-fast responses 119 | * **OpenAI-compatible API** for seamless integration 120 | * **Docker Setup** 121 | --- 122 | 123 | ## 🛣️ Roadmap 124 | 125 | ### ✔️ v0.1.0 (Released) 126 | 127 | * In-memory & BadgerDB storage 128 | * Smart semantic verification (dual-threshold + intent check) 129 | * OpenAI API compatibility 130 | 131 | ### 🚧 v0.2.0 (Planned) 132 | 133 | * **Core Improvements**: Bug fixes and performance optimizations. 134 | * **Public API**: Improve cache create/delete operations. 135 | * **Enhanced Configuration**: 136 | * Configurable "gray zone" fallback model (enable/disable env var). 137 | * User-definable similarity thresholds with sensible defaults. 138 | 139 | ### 🚧 v0.3.0 (Planned) 140 | * Built-in support for Claude & Mistral APIs 141 | 142 | ### 🚀 v1.0.0 143 | 144 | * Clustered mode (Raft or gossip-based replication) 145 | * Custom embedding backends (Ollama, local models) 146 | * Rate-limiting & request shaping 147 | * Web dashboard (hit rate, latency, cost metrics) 148 | 149 | ### ❤️ Support the Project 150 | 151 | We are working hard to reach **v1.0.0**! If you find this project useful, please give it a ⭐️ on GitHub and consider contributing. Your support helps us ship v0.2.0 and v1.0.0 faster! 152 | 153 | --- 154 | 155 | ## 📄 License 156 | 157 | MIT License. 158 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 2 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 3 | github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= 4 | github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= 5 | github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= 6 | github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 10 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= 15 | github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= 16 | github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= 17 | github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 18 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 19 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 20 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 21 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 22 | github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= 23 | github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 24 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 25 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 26 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 27 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 28 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 29 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 30 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 31 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 32 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 33 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 34 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 35 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 36 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 37 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 38 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 39 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= 40 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 41 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 42 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 43 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 44 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 45 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 46 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 47 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 48 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 50 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 51 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 52 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 53 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 54 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 55 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 56 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 57 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 58 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 59 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 60 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 64 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 65 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 66 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 70 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 71 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= 72 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 75 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 76 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 77 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 78 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 80 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 81 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 82 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 83 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 84 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 85 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 86 | github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= 87 | github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 88 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 89 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 90 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 91 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 92 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 93 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 94 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 95 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 96 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 97 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 98 | golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= 99 | golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 100 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 101 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 102 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 103 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 104 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 106 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 107 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 108 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 109 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 110 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 111 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 112 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 113 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 114 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 116 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | --------------------------------------------------------------------------------