diff --git a/pkg/llmproxy/api/middleware/response_writer.go b/pkg/llmproxy/api/middleware/response_writer.go index 42cac8dfc2..f5165efc76 100644 --- a/pkg/llmproxy/api/middleware/response_writer.go +++ b/pkg/llmproxy/api/middleware/response_writer.go @@ -5,6 +5,8 @@ package middleware import ( "bytes" + "crypto/sha256" + "fmt" "net/http" "strings" "time" @@ -12,7 +14,6 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/logging" - "github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/util" ) const requestBodyOverrideContextKey = "REQUEST_BODY_OVERRIDE" @@ -200,30 +201,13 @@ func (w *ResponseWriterWrapper) captureCurrentHeaders() { w.headers = make(map[string][]string) } - // Remove previous values to avoid stale entries after header mutation. - for key := range w.headers { - delete(w.headers, key) - } - // Capture all current headers from the underlying ResponseWriter for key, values := range w.Header() { - if key == "" { - continue - } - keyLower := strings.ToLower(strings.TrimSpace(key)) - sanitizedValues := make([]string, len(values)) - for i, value := range values { - sanitizedValues[i] = sanitizeResponseHeaderValue(keyLower, value) - } - w.headers[key] = sanitizedValues - } -} - -func sanitizeResponseHeaderValue(keyLower, value string) string { - if keyLower == "authorization" || keyLower == "cookie" || keyLower == "proxy-authorization" || keyLower == "set-cookie" { - return "[redacted]" + // Make a copy of the values slice to avoid reference issues + headerValues := make([]string, len(values)) + copy(headerValues, values) + w.headers[key] = headerValues } - return util.MaskSensitiveHeaderValue(keyLower, value) } // detectStreaming determines if a response should be treated as a streaming response. @@ -355,7 +339,7 @@ func (w *ResponseWriterWrapper) extractAPIRequest(c *gin.Context) []byte { if !ok || len(data) == 0 { return nil } - return sanitizeLoggedPayloadBytes(data) + return data } func (w *ResponseWriterWrapper) extractAPIResponse(c *gin.Context) []byte { @@ -367,7 +351,7 @@ func (w *ResponseWriterWrapper) extractAPIResponse(c *gin.Context) []byte { if !ok || len(data) == 0 { return nil } - return sanitizeLoggedPayloadBytes(data) + return data } func (w *ResponseWriterWrapper) extractAPIResponseTimestamp(c *gin.Context) time.Time { @@ -387,17 +371,17 @@ func (w *ResponseWriterWrapper) extractRequestBody(c *gin.Context) []byte { switch value := bodyOverride.(type) { case []byte: if len(value) > 0 { - return sanitizeLoggedPayloadBytes(value) + return bytes.Clone(value) } case string: if strings.TrimSpace(value) != "" { - return sanitizeLoggedPayloadBytes([]byte(value)) + return []byte(value) } } } } if w.requestInfo != nil && len(w.requestInfo.Body) > 0 { - return sanitizeLoggedPayloadBytes(w.requestInfo.Body) + return w.requestInfo.Body } return nil } @@ -415,12 +399,12 @@ func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, h w.requestInfo.URL, w.requestInfo.Method, requestHeaders, - requestBody, + redactLoggedBody(requestBody), statusCode, headers, - body, - apiRequestBody, - apiResponseBody, + redactLoggedBody(body), + redactLoggedBody(apiRequestBody), + redactLoggedBody(apiResponseBody), apiResponseErrors, forceLog, w.requestInfo.RequestID, @@ -433,15 +417,23 @@ func (w *ResponseWriterWrapper) logRequest(requestBody []byte, statusCode int, h w.requestInfo.URL, w.requestInfo.Method, requestHeaders, - requestBody, + redactLoggedBody(requestBody), statusCode, headers, - body, - apiRequestBody, - apiResponseBody, + redactLoggedBody(body), + redactLoggedBody(apiRequestBody), + redactLoggedBody(apiResponseBody), apiResponseErrors, w.requestInfo.RequestID, w.requestInfo.Timestamp, apiResponseTimestamp, ) } + +func redactLoggedBody(body []byte) []byte { + if len(body) == 0 { + return nil + } + sum := sha256.Sum256(body) + return []byte(fmt.Sprintf("[redacted body len=%d sha256=%x]", len(body), sum[:8])) +} diff --git a/pkg/llmproxy/executor/codex_websockets_executor.go b/pkg/llmproxy/executor/codex_websockets_executor.go index 133916ea43..56118dbcdc 100644 --- a/pkg/llmproxy/executor/codex_websockets_executor.go +++ b/pkg/llmproxy/executor/codex_websockets_executor.go @@ -5,6 +5,7 @@ package executor import ( "bytes" "context" + "crypto/sha256" "fmt" "io" "net" @@ -1295,15 +1296,15 @@ func (e *CodexWebsocketsExecutor) closeExecutionSession(sess *codexWebsocketSess } func logCodexWebsocketConnected(sessionID string, authID string, wsURL string) { - log.Infof("codex websockets: upstream connected session=%s auth=%s url=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL)) + log.Infof("codex websockets: upstream connected session=%s auth=%s endpoint=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL)) } func logCodexWebsocketDisconnected(sessionID, authID, wsURL, reason string, err error) { if err != nil { - log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s err=%v", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason), err) + log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err) return } - log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason)) + log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason)) } func sanitizeCodexWebsocketLogField(raw string) string { @@ -1325,6 +1326,27 @@ func sanitizeCodexWebsocketLogURL(raw string) string { return parsed.String() } +func sanitizeCodexWebsocketLogEndpoint(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + parsed, err := url.Parse(trimmed) + if err != nil || parsed.Host == "" { + return "redacted-endpoint" + } + return parsed.Scheme + "://" + parsed.Host +} + +func sanitizeCodexSessionID(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + sum := sha256.Sum256([]byte(trimmed)) + return fmt.Sprintf("sess_%x", sum[:6]) +} + // CodexAutoExecutor routes Codex requests to the websocket transport only when: // 1. The downstream transport is websocket, and // 2. The selected auth enables websockets. diff --git a/pkg/llmproxy/registry/model_registry.go b/pkg/llmproxy/registry/model_registry.go index dd4b0b335c..85906a8948 100644 --- a/pkg/llmproxy/registry/model_registry.go +++ b/pkg/llmproxy/registry/model_registry.go @@ -5,6 +5,7 @@ package registry import ( "context" + "crypto/sha256" "fmt" "sort" "strings" @@ -661,7 +662,7 @@ func (r *ModelRegistry) SuspendClientModel(clientID, modelID, reason string) { registration.SuspendedClients[clientID] = reason registration.LastUpdated = time.Now() if reason != "" { - log.Debugf("Suspended client %s for model %s (reason provided)", clientID, modelID) + log.Debugf("Suspended client %s for model %s (reason provided)", logSafeRegistryID(clientID), logSafeRegistryID(modelID)) } else { log.Debug("Suspended client for model") } @@ -690,6 +691,15 @@ func (r *ModelRegistry) ResumeClientModel(clientID, modelID string) { log.Debug("Resumed suspended client for model") } +func logSafeRegistryID(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + sum := sha256.Sum256([]byte(trimmed)) + return fmt.Sprintf("id_%x", sum[:6]) +} + // ClientSupportsModel reports whether the client registered support for modelID. func (r *ModelRegistry) ClientSupportsModel(clientID, modelID string) bool { clientID = strings.TrimSpace(clientID) diff --git a/pkg/llmproxy/runtime/executor/codex_websockets_executor.go b/pkg/llmproxy/runtime/executor/codex_websockets_executor.go index a29c996c21..0c7cfeb126 100644 --- a/pkg/llmproxy/runtime/executor/codex_websockets_executor.go +++ b/pkg/llmproxy/runtime/executor/codex_websockets_executor.go @@ -5,6 +5,7 @@ package executor import ( "bytes" "context" + "crypto/sha256" "fmt" "io" "net" @@ -1295,15 +1296,15 @@ func (e *CodexWebsocketsExecutor) closeExecutionSession(sess *codexWebsocketSess } func logCodexWebsocketConnected(sessionID string, authID string, wsURL string) { - log.Infof("codex websockets: upstream connected session=%s auth=%s url=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL)) + log.Infof("codex websockets: upstream connected session=%s auth=%s endpoint=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL)) } func logCodexWebsocketDisconnected(sessionID string, authID string, wsURL string, reason string, err error) { if err != nil { - log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s err=%v", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason), err) + log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s err=%v", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason), err) return } - log.Infof("codex websockets: upstream disconnected session=%s auth=%s url=%s reason=%s", strings.TrimSpace(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogURL(wsURL), strings.TrimSpace(reason)) + log.Infof("codex websockets: upstream disconnected session=%s auth=%s endpoint=%s reason=%s", sanitizeCodexSessionID(sessionID), sanitizeCodexWebsocketLogField(authID), sanitizeCodexWebsocketLogEndpoint(wsURL), strings.TrimSpace(reason)) } func sanitizeCodexWebsocketLogField(raw string) string { @@ -1325,6 +1326,27 @@ func sanitizeCodexWebsocketLogURL(raw string) string { return parsed.String() } +func sanitizeCodexWebsocketLogEndpoint(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + parsed, err := url.Parse(trimmed) + if err != nil || parsed.Host == "" { + return "redacted-endpoint" + } + return parsed.Scheme + "://" + parsed.Host +} + +func sanitizeCodexSessionID(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + sum := sha256.Sum256([]byte(trimmed)) + return fmt.Sprintf("sess_%x", sum[:6]) +} + // CodexAutoExecutor routes Codex requests to the websocket transport only when: // 1. The downstream transport is websocket, and // 2. The selected auth enables websockets.