| package app |
|
|
| import ( |
| "bytes" |
| "net/http" |
| "strings" |
| "sync" |
| "time" |
|
|
| "ccLoad/internal/protocol" |
| "ccLoad/internal/util" |
|
|
| "github.com/bytedance/sonic" |
| ) |
|
|
| |
| |
| |
|
|
| type codexSessionEntry struct { |
| id string |
| expire time.Time |
| } |
|
|
| const ( |
| codexSessionTTL = time.Hour |
| codexSessionCleanupInterval = 15 * time.Minute |
| ) |
|
|
| var ( |
| codexSessionMap = make(map[string]codexSessionEntry) |
| codexSessionMu sync.RWMutex |
| codexSessionOnce sync.Once |
| ) |
|
|
| |
| func getOrCreateCodexSessionID(cacheKey string) string { |
| if cacheKey == "" { |
| return "" |
| } |
| codexSessionOnce.Do(startCodexSessionCleanup) |
| now := time.Now() |
|
|
| codexSessionMu.Lock() |
| defer codexSessionMu.Unlock() |
| if entry, ok := codexSessionMap[cacheKey]; ok && entry.id != "" && entry.expire.After(now) { |
| entry.expire = now.Add(codexSessionTTL) |
| codexSessionMap[cacheKey] = entry |
| return entry.id |
| } |
| id := util.NewUUIDv4() |
| codexSessionMap[cacheKey] = codexSessionEntry{id: id, expire: now.Add(codexSessionTTL)} |
| return id |
| } |
|
|
| |
| |
| func codexSessionIDForOpenAIKey(apiKey string) string { |
| apiKey = strings.TrimSpace(apiKey) |
| if apiKey == "" { |
| return "" |
| } |
| return util.NewUUIDv5(util.NameSpaceOID, "ccload:codex:prompt-cache:"+apiKey) |
| } |
|
|
| |
| |
| |
| |
| |
| func resolveCodexSessionHint(reqCtx *requestContext, translatedBody []byte, apiKey string, header http.Header) string { |
| if reqCtx == nil || runtimeUpstreamProtocol(reqCtx, nil) != string(protocol.Codex) { |
| return "" |
| } |
| switch reqCtx.clientProtocol { |
| case protocol.Anthropic: |
| if userID := extractAnthropicUserID(reqCtx.originalBody); userID != "" { |
| model := strings.TrimSpace(reqCtx.originalModel) |
| if model == "" { |
| model = "unknown" |
| } |
| return getOrCreateCodexSessionID(model + "-" + userID) |
| } |
| if sid := strings.TrimSpace(header.Get("X-Claude-Code-Session-Id")); sid != "" { |
| return util.NewUUIDv5(util.NameSpaceOID, "ccload:codex:prompt-cache:session:"+sid) |
| } |
| return codexSessionIDForOpenAIKey(apiKey) |
| case protocol.Codex: |
| return readCodexPromptCacheKey(translatedBody) |
| case protocol.OpenAI: |
| return codexSessionIDForOpenAIKey(apiKey) |
| } |
| return "" |
| } |
|
|
| |
| |
| func injectCodexPromptCacheKey(body []byte, id string) []byte { |
| if len(body) == 0 || id == "" { |
| return body |
| } |
| if readCodexPromptCacheKey(body) != "" { |
| return body |
| } |
| var payload map[string]any |
| if err := sonic.Unmarshal(body, &payload); err != nil || payload == nil { |
| return body |
| } |
|
|
| encodedID, err := sonic.Marshal(id) |
| if err != nil { |
| return body |
| } |
| end := len(body) |
| for end > 0 { |
| switch body[end-1] { |
| case ' ', '\n', '\r', '\t': |
| end-- |
| default: |
| goto foundEnd |
| } |
| } |
| foundEnd: |
| if end == 0 || body[end-1] != '}' { |
| return body |
| } |
| start := 0 |
| for start < end { |
| switch body[start] { |
| case ' ', '\n', '\r', '\t': |
| start++ |
| default: |
| goto foundStart |
| } |
| } |
| foundStart: |
| if start >= end || body[start] != '{' { |
| return body |
| } |
|
|
| hasFields := len(bytes.TrimSpace(body[start+1:end-1])) > 0 |
| insertLen := len(`"prompt_cache_key":`) + len(encodedID) |
| if hasFields { |
| insertLen++ |
| } |
| out := make([]byte, 0, len(body)+insertLen) |
| out = append(out, body[:end-1]...) |
| if hasFields { |
| out = append(out, ',') |
| } |
| out = append(out, `"prompt_cache_key":`...) |
| out = append(out, encodedID...) |
| out = append(out, body[end-1:]...) |
| return out |
| } |
|
|
| func extractAnthropicUserID(body []byte) string { |
| if len(body) == 0 { |
| return "" |
| } |
| var payload struct { |
| Metadata struct { |
| UserID string `json:"user_id"` |
| } `json:"metadata"` |
| } |
| if err := sonic.Unmarshal(body, &payload); err != nil { |
| return "" |
| } |
| return strings.TrimSpace(payload.Metadata.UserID) |
| } |
|
|
| func readCodexPromptCacheKey(body []byte) string { |
| if len(body) == 0 { |
| return "" |
| } |
| var payload struct { |
| PromptCacheKey string `json:"prompt_cache_key"` |
| } |
| if err := sonic.Unmarshal(body, &payload); err != nil { |
| return "" |
| } |
| return strings.TrimSpace(payload.PromptCacheKey) |
| } |
|
|
| func startCodexSessionCleanup() { |
| go func() { |
| t := time.NewTicker(codexSessionCleanupInterval) |
| defer t.Stop() |
| for range t.C { |
| now := time.Now() |
| codexSessionMu.Lock() |
| for k, v := range codexSessionMap { |
| if !v.expire.After(now) { |
| delete(codexSessionMap, k) |
| } |
| } |
| codexSessionMu.Unlock() |
| } |
| }() |
| } |
|
|
| |
|
|