package middleware import ( "bytes" "io" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/tidwall/gjson" ) type cacheCaptureWriter struct { gin.ResponseWriter statusCode int body bytes.Buffer } func (w *cacheCaptureWriter) WriteHeader(code int) { w.statusCode = code w.ResponseWriter.WriteHeader(code) } func (w *cacheCaptureWriter) Write(data []byte) (int, error) { if w.statusCode == 0 { w.statusCode = http.StatusOK } w.body.Write(data) return w.ResponseWriter.Write(data) } func (w *cacheCaptureWriter) WriteString(s string) (int, error) { if w.statusCode == 0 { w.statusCode = http.StatusOK } w.body.WriteString(s) return w.ResponseWriter.WriteString(s) } func ResponseCacheMiddleware(rc cache.ResponseCache, cfg config.ResponseCacheConfig) gin.HandlerFunc { return func(c *gin.Context) { if rc == nil || !cfg.Enabled || c.Request == nil || c.Request.Method != http.MethodPost { c.Next() return } if c.FullPath() == "" { c.Next() return } contentType := c.GetHeader("Content-Type") if contentType != "" && !strings.Contains(strings.ToLower(contentType), "application/json") { c.Next() return } raw, err := io.ReadAll(c.Request.Body) if err != nil { c.Next() return } c.Request.Body = io.NopCloser(bytes.NewReader(raw)) if len(raw) == 0 { c.Next() return } model := gjson.GetBytes(raw, "model").String() isStreaming := gjson.GetBytes(raw, "stream").Bool() if !cache.ShouldCache(cfg, model, isStreaming, 0) { c.Next() return } modelKey := c.FullPath() + "|" + model key := cache.ResponseCacheKey(modelKey, raw) if entry, ok := rc.Get(c.Request.Context(), key); ok && entry != nil { for k, v := range entry.Headers { if k != "" && v != "" { c.Header(k, v) } } c.Header("X-Cache", "HIT") c.Status(entry.StatusCode) _, _ = c.Writer.Write(entry.Body) c.Abort() return } writer := &cacheCaptureWriter{ResponseWriter: c.Writer} c.Writer = writer c.Next() status := writer.statusCode if status == 0 { status = c.Writer.Status() } if status < 200 || status >= 300 { return } body := writer.body.Bytes() if !cache.ShouldCache(cfg, model, isStreaming, len(body)) { return } headers := make(map[string]string) for k, values := range c.Writer.Header() { if len(values) > 0 { headers[k] = values[0] } } ttl := time.Duration(cfg.TTLSeconds) * time.Second if ttl <= 0 { ttl = 300 * time.Second } _ = rc.Set(c.Request.Context(), key, &cache.CachedResponse{ StatusCode: status, Headers: headers, Body: bytes.Clone(body), CachedAt: time.Now(), Model: model, }, ttl) } }