Files
CLIProxyAPI/internal/runtime/executor/helps/claude_code_session.go
Luis Pater 7c390a7a2e feat(runtime): add Claude Code session handling with caching and tests
- Introduced `ClaudeCodeSessionID` resolution logic, preferring headers over payload metadata.
- Added `ClaudeCodePromptCache` to map sessions to stable prompt cache keys.
- Refactored existing logic to integrate `ClaudeCodePromptCache` for session-based handling.
- Included extensive unit tests to validate session ID extraction, cache reuse, and header prioritization.
2026-06-23 13:19:13 +08:00

72 lines
2.1 KiB
Go

package helps
import (
"context"
"net/http"
"regexp"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/tidwall/gjson"
)
const ClaudeCodeSessionHeader = "X-Claude-Code-Session-Id"
var claudeCodeSessionSuffixPattern = regexp.MustCompile(`_session_([a-f0-9-]+)$`)
// ExtractClaudeCodeSessionID resolves a Claude Code session ID, preferring X-Claude-Code-Session-Id over payload metadata.
func ExtractClaudeCodeSessionID(ctx context.Context, payload []byte, headers http.Header) string {
if headers != nil {
if sessionID := strings.TrimSpace(headers.Get(ClaudeCodeSessionHeader)); sessionID != "" {
return sessionID
}
}
if ctx != nil {
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
if sessionID := strings.TrimSpace(ginCtx.Request.Header.Get(ClaudeCodeSessionHeader)); sessionID != "" {
return sessionID
}
}
}
return extractClaudeCodeSessionIDFromPayload(payload)
}
func extractClaudeCodeSessionIDFromPayload(payload []byte) string {
if len(payload) == 0 {
return ""
}
userID := gjson.GetBytes(payload, "metadata.user_id").String()
if userID == "" {
return ""
}
if matches := claudeCodeSessionSuffixPattern.FindStringSubmatch(userID); len(matches) >= 2 {
return matches[1]
}
if len(userID) > 0 && userID[0] == '{' {
return strings.TrimSpace(gjson.Get(userID, "session_id").String())
}
return ""
}
// ClaudeCodePromptCache maps a Claude Code session to a stable upstream prompt_cache_key.
func ClaudeCodePromptCache(ctx context.Context, modelName string, payload []byte, headers http.Header) (CodexCache, bool, error) {
sessionID := ExtractClaudeCodeSessionID(ctx, payload, headers)
if sessionID == "" {
return CodexCache{}, false, nil
}
key := CodexPromptCacheKey(modelName, "claude:"+sessionID)
if cache, ok, errCache := GetCodexCacheRequired(ctx, key); errCache != nil || ok {
return cache, ok, errCache
}
cache := CodexCache{
ID: uuid.New().String(),
Expire: time.Now().Add(1 * time.Hour),
}
if errSet := SetCodexCacheRequired(ctx, key, cache); errSet != nil {
return CodexCache{}, false, errSet
}
return cache, true, nil
}