@@ -17,59 +17,44 @@ function ensureLogDir(): void {
1717 * or response bodies.
1818 */
1919function maskSensitive ( text : string ) : string {
20- return (
21- text
22- // Mask Bearer tokens in Authorization headers
23- . replace ( / ( A u t h o r i z a t i o n : \s * B e a r e r \s + ) [ ^ \s \r \n ] + / gi, "$1***MASKED***" )
24- // Mask "apiKey" or "api_key" values in JSON-like strings
25- . replace ( / ( (?: a p i [ K k ] e y | a p i _ k e y | s e c r e t ) \s * [: = ] \s * " ? ) [ ^ " , } \s ] + / gi, "$1***MASKED***" )
26- ) ;
20+ return text
21+ . replace ( / ( A u t h o r i z a t i o n : \s * (?: B e a r e r \s + ) ? ) [ ^ \s \r \n ] + / gi, "$1***MASKED***" )
22+ . replace ( / \b s k - [ A - Z a - z 0 - 9 _ - ] { 8 , } \b / g, "sk-***MASKED***" )
23+ . replace ( / ( (?: a p i [ K k ] e y | a p i _ k e y | s e c r e t | t o k e n | a u t h o r i z a t i o n ) \s * [: = ] \s * " ? ) [ ^ " , } \s ] + / gi, "$1***MASKED***" ) ;
2724}
2825
29- const CONTENT_TRUNCATE_PREVIEW = 100 ;
26+ const MAX_STRING_LENGTH = 500 ;
27+ const MAX_LOG_LINE_CHARS = 32_000 ;
28+ const MAX_ENTRIES = 50 ;
3029
31- /**
32- * Truncate a content string for logging: keep a short prefix and append the
33- * total length so the payload structure is preserved while content bloat is
34- * avoided.
35- */
36- function truncateContent ( value : string ) : string {
37- if ( value . length <= CONTENT_TRUNCATE_PREVIEW ) {
38- return value ;
30+ function truncateString ( value : string ) : string {
31+ const masked = maskSensitive ( value ) ;
32+ if ( masked . length <= MAX_STRING_LENGTH ) {
33+ return masked ;
3934 }
40- return `${ value . slice ( 0 , CONTENT_TRUNCATE_PREVIEW ) } ...(total ${ value . length } chars)` ;
35+ return `${ masked . slice ( 0 , MAX_STRING_LENGTH ) } ...(total ${ masked . length } chars)` ;
4136}
4237
4338/**
44- * Deep-clone a request payload, only truncating `content` fields whose value
45- * is a string. Every other field is kept exactly as-is so the logged request
46- * mirrors the original API payload (no fields added or removed) .
39+ * Deep-clone a payload, masking and truncating every string value. This keeps
40+ * the structure for debugging without logging full prompts, tool arguments, or
41+ * response bodies .
4742 */
48- function sanitizeRequestPayload ( request : Record < string , unknown > ) : Record < string , unknown > {
49- function walk ( value : unknown ) : unknown {
50- if ( ! value || typeof value !== "object" ) {
51- return value ;
52- }
53-
54- if ( Array . isArray ( value ) ) {
55- return value . map ( walk ) ;
56- }
57-
58- const record = value as Record < string , unknown > ;
59- const result : Record < string , unknown > = { } ;
60-
61- for ( const [ key , val ] of Object . entries ( record ) ) {
62- if ( key === "content" && typeof val === "string" ) {
63- result [ key ] = truncateContent ( val ) ;
64- } else {
65- result [ key ] = walk ( val ) ;
66- }
67- }
68-
69- return result ;
43+ function sanitizePayload ( value : unknown ) : unknown {
44+ if ( typeof value === "string" ) {
45+ return truncateString ( value ) ;
7046 }
71-
72- return walk ( request ) as Record < string , unknown > ;
47+ if ( ! value || typeof value !== "object" ) {
48+ return value ;
49+ }
50+ if ( Array . isArray ( value ) ) {
51+ return value . map ( sanitizePayload ) ;
52+ }
53+ const result : Record < string , unknown > = { } ;
54+ for ( const [ key , val ] of Object . entries ( value ) ) {
55+ result [ key ] = sanitizePayload ( val ) ;
56+ }
57+ return result ;
7358}
7459
7560export type ApiErrorLogEntry = {
@@ -99,18 +84,30 @@ export function logApiError(entry: ApiErrorLogEntry): void {
9984 model : entry . model ,
10085 baseURL : entry . baseURL ,
10186 error : sanitizeError ( entry . error ) ,
102- request : sanitizeRequestPayload ( entry . request ) ,
87+ request : sanitizePayload ( entry . request ) ,
10388 } ;
10489
10590 if ( entry . response !== undefined ) {
106- logLine . response = typeof entry . response === "string" ? maskSensitive ( entry . response ) : entry . response ;
91+ logLine . response = sanitizePayload ( entry . response ) ;
10792 }
10893
109- const newLine = JSON . stringify ( logLine ) + "\n" ;
110- fs . appendFileSync ( ERROR_LOG_PATH , newLine , "utf8" ) ;
94+ let newLine = JSON . stringify ( logLine ) ;
95+ if ( newLine . length > MAX_LOG_LINE_CHARS ) {
96+ newLine = JSON . stringify ( {
97+ timestamp : entry . timestamp ,
98+ location : entry . location ,
99+ requestId : entry . requestId ,
100+ sessionId : entry . sessionId ,
101+ model : entry . model ,
102+ baseURL : entry . baseURL ,
103+ error : sanitizeError ( entry . error ) ,
104+ truncated : true ,
105+ note : `entry exceeded ${ MAX_LOG_LINE_CHARS } chars` ,
106+ } ) ;
107+ }
108+ fs . appendFileSync ( ERROR_LOG_PATH , `${ newLine } \n` , "utf8" ) ;
111109
112110 // Keep only the last N entries
113- const MAX_ENTRIES = 20 ;
114111 const raw = fs . readFileSync ( ERROR_LOG_PATH , "utf8" ) ;
115112 const lines = raw . split ( "\n" ) . filter ( ( line ) => line . trim ( ) . length > 0 ) ;
116113 if ( lines . length > MAX_ENTRIES ) {
@@ -124,8 +121,8 @@ export function logApiError(entry: ApiErrorLogEntry): void {
124121function sanitizeError ( error : LlmErrorDetails ) : LlmErrorDetails {
125122 return {
126123 ...error ,
127- message : maskSensitive ( error . message ) ,
128- stack : error . stack ? maskSensitive ( error . stack ) : undefined ,
124+ message : truncateString ( error . message ) ,
125+ stack : error . stack ? truncateString ( error . stack ) : undefined ,
129126 causes : error . causes ?. map ( sanitizeError ) ,
130127 } ;
131128}
0 commit comments