From f31c9a3b815bc38777496794733e6ae42f98af56 Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Wed, 26 Aug 2026 09:54:46 +0530 Subject: [PATCH 1/4] fix(runtime-node): stop redaction masking GenAI/usage token counts The /token/ sensitive-key pattern is an unanchored substring, so it also masks non-secret usage attributes like gen_ai.usage.*_tokens, prompt_tokens, total_tokens, token_count and max_tokens -- unhelpful for an SDK with llmTracing. Anchoring to /token$/ keeps masking secret token keys (token, access_token, authToken) while leaving usage counts intact, matching the existing anti-over-redaction anchoring for card/author. Value-level token scrubbing still catches token-shaped values. Adds tests. --- packages/runtime-node/src/redact.ts | 2 +- packages/runtime-node/test/redact.test.mjs | 31 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/runtime-node/src/redact.ts b/packages/runtime-node/src/redact.ts index 93431a7..b1feb1c 100644 --- a/packages/runtime-node/src/redact.ts +++ b/packages/runtime-node/src/redact.ts @@ -43,7 +43,7 @@ export interface RedactOptions { const SENSITIVE_KEY_PATTERNS: RegExp[] = [ /e-?mail/, /pass(word|wd|phrase)|^pass$/, - /token/, + /token$/, /secret/, /credential/, /(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/, diff --git a/packages/runtime-node/test/redact.test.mjs b/packages/runtime-node/test/redact.test.mjs index 3cf0a1d..32b6428 100644 --- a/packages/runtime-node/test/redact.test.mjs +++ b/packages/runtime-node/test/redact.test.mjs @@ -124,3 +124,34 @@ test("empty/nullish input yields an empty object", () => { assert.deepEqual(redactAttributes(), {}); assert.deepEqual(redactAttributes(null), {}); }); + +test("keeps GenAI/usage token-count attributes (not secrets)", () => { +const out = redactAttributes({ +"gen_ai.usage.input_tokens": 512, +"gen_ai.usage.output_tokens": 128, +prompt_tokens: 512, +completion_tokens: 128, +total_tokens: 640, +token_count: 42, +max_tokens: 1000, +}); +assert.deepEqual(out, { +"gen_ai.usage.input_tokens": 512, +"gen_ai.usage.output_tokens": 128, +prompt_tokens: 512, +completion_tokens: 128, +total_tokens: 640, +token_count: 42, +max_tokens: 1000, +}); +}); + +test("still masks secret token keys ending in 'token'", () => { +const out = redactAttributes({ +token: "raw", +access_token: "raw", +refresh_token: "raw", +authToken: "raw", +}); +for (const value of Object.values(out)) assert.equal(value, MASK); +}); From 56c2227c32a15259aab66c17a9b44413c00c06f5 Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Wed, 26 Aug 2026 12:05:02 +0530 Subject: [PATCH 2/4] fix(runtime-node): preserve usage token attributes --- packages/runtime-node/src/redact.ts | 21 ++++++++++++++++++--- packages/runtime-node/test/redact.test.mjs | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/runtime-node/src/redact.ts b/packages/runtime-node/src/redact.ts index b1feb1c..52af5dd 100644 --- a/packages/runtime-node/src/redact.ts +++ b/packages/runtime-node/src/redact.ts @@ -43,7 +43,7 @@ export interface RedactOptions { const SENSITIVE_KEY_PATTERNS: RegExp[] = [ /e-?mail/, /pass(word|wd|phrase)|^pass$/, - /token$/, + /token/, /secret/, /credential/, /(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/, @@ -149,9 +149,24 @@ function redactValue(value: unknown, r: CompiledRedactor, depth: number): unknow return value; } +const USAGE_TOKEN_KEYS = new Set([ + "input_tokens", + "output_tokens", + "prompt_tokens", + "completion_tokens", + "total_tokens", + "token_count", + "max_tokens", +]); + function isSensitiveKey(key: string, r: CompiledRedactor): boolean { - const lowered = key.toLowerCase(); - return r.keyPatterns.some((re) => re.test(lowered)); + const lowered = key.toLowerCase(); + + // Known GenAI usage-count attributes are safe to keep. + const usageKey = lowered.split(".").pop() ?? lowered; + if (USAGE_TOKEN_KEYS.has(usageKey)) return false; + + return r.keyPatterns.some((re) => re.test(lowered)); } /** diff --git a/packages/runtime-node/test/redact.test.mjs b/packages/runtime-node/test/redact.test.mjs index 32b6428..30b3974 100644 --- a/packages/runtime-node/test/redact.test.mjs +++ b/packages/runtime-node/test/redact.test.mjs @@ -152,6 +152,10 @@ token: "raw", access_token: "raw", refresh_token: "raw", authToken: "raw", +token_value: "raw", +tokenString: "raw", +token_id: "raw", +id_token_hint: "raw", }); for (const value of Object.values(out)) assert.equal(value, MASK); }); From d001c3be3b3d7ac1e7de4d230913ac7d502c4a75 Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Wed, 26 Aug 2026 12:40:01 +0530 Subject: [PATCH 3/4] fix(runtime-node): tighten token usage redaction --- packages/runtime-node/src/redact.ts | 12 ++++++++---- packages/runtime-node/test/redact.test.mjs | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/runtime-node/src/redact.ts b/packages/runtime-node/src/redact.ts index 52af5dd..005ac7f 100644 --- a/packages/runtime-node/src/redact.ts +++ b/packages/runtime-node/src/redact.ts @@ -156,15 +156,19 @@ const USAGE_TOKEN_KEYS = new Set([ "completion_tokens", "total_tokens", "token_count", - "max_tokens", + "gen_ai.usage.input_tokens", + "gen_ai.usage.output_tokens", + "gen_ai.usage.prompt_tokens", + "gen_ai.usage.completion_tokens", + "gen_ai.usage.total_tokens", + "gen_ai.usage.token_count", ]); function isSensitiveKey(key: string, r: CompiledRedactor): boolean { const lowered = key.toLowerCase(); - // Known GenAI usage-count attributes are safe to keep. - const usageKey = lowered.split(".").pop() ?? lowered; - if (USAGE_TOKEN_KEYS.has(usageKey)) return false; + // Only explicitly supported GenAI usage attributes bypass key redaction. + if (USAGE_TOKEN_KEYS.has(lowered)) return false; return r.keyPatterns.some((re) => re.test(lowered)); } diff --git a/packages/runtime-node/test/redact.test.mjs b/packages/runtime-node/test/redact.test.mjs index 30b3974..3f7ba65 100644 --- a/packages/runtime-node/test/redact.test.mjs +++ b/packages/runtime-node/test/redact.test.mjs @@ -125,7 +125,7 @@ test("empty/nullish input yields an empty object", () => { assert.deepEqual(redactAttributes(null), {}); }); -test("keeps GenAI/usage token-count attributes (not secrets)", () => { +test("keeps only supported GenAI/usage token-count attributes", () => { const out = redactAttributes({ "gen_ai.usage.input_tokens": 512, "gen_ai.usage.output_tokens": 128, @@ -134,7 +134,9 @@ completion_tokens: 128, total_tokens: 640, token_count: 42, max_tokens: 1000, +"secret.input_tokens": 999, }); + assert.deepEqual(out, { "gen_ai.usage.input_tokens": 512, "gen_ai.usage.output_tokens": 128, @@ -142,10 +144,12 @@ prompt_tokens: 512, completion_tokens: 128, total_tokens: 640, token_count: 42, -max_tokens: 1000, +max_tokens: MASK, +"secret.input_tokens": MASK, }); }); + test("still masks secret token keys ending in 'token'", () => { const out = redactAttributes({ token: "raw", From 29a86db7bb2abdfb5f925d660a01ac8fe89d8014 Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Wed, 26 Aug 2026 22:41:34 +0530 Subject: [PATCH 4/4] fix(runtime-node): tighten GenAI usage redaction --- packages/runtime-node/src/redact.ts | 30 ++++++---- packages/runtime-node/test/redact.test.mjs | 68 +++++++++++++--------- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/packages/runtime-node/src/redact.ts b/packages/runtime-node/src/redact.ts index 005ac7f..8557ff5 100644 --- a/packages/runtime-node/src/redact.ts +++ b/packages/runtime-node/src/redact.ts @@ -142,7 +142,7 @@ function redactValue(value: unknown, r: CompiledRedactor, depth: number): unknow if (typeof value === "object" && value !== null && depth > 0) { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { - out[k] = isSensitiveKey(k, r) ? r.mask : redactValue(v, r, depth - 1); + out[k] = isSensitiveKey(k, v, r) ? r.mask : redactValue(v, r, depth - 1); } return out; } @@ -150,12 +150,6 @@ function redactValue(value: unknown, r: CompiledRedactor, depth: number): unknow } const USAGE_TOKEN_KEYS = new Set([ - "input_tokens", - "output_tokens", - "prompt_tokens", - "completion_tokens", - "total_tokens", - "token_count", "gen_ai.usage.input_tokens", "gen_ai.usage.output_tokens", "gen_ai.usage.prompt_tokens", @@ -164,12 +158,24 @@ const USAGE_TOKEN_KEYS = new Set([ "gen_ai.usage.token_count", ]); -function isSensitiveKey(key: string, r: CompiledRedactor): boolean { +function isSensitiveKey( + key: string, + value: unknown, + r: CompiledRedactor, +): boolean { const lowered = key.toLowerCase(); - // Only explicitly supported GenAI usage attributes bypass key redaction. - if (USAGE_TOKEN_KEYS.has(lowered)) return false; - + // Canonical GenAI usage attributes are safe when they contain + // valid non-negative numeric counts. + if (USAGE_TOKEN_KEYS.has(lowered)) { + return !( + typeof value === "number" && + Number.isFinite(value) && + value >= 0 + ); + } + + // All other sensitive keys, including token-like keys, are redacted. return r.keyPatterns.some((re) => re.test(lowered)); } @@ -195,7 +201,7 @@ function redactWith( if (!attributes) return out; for (const [key, value] of Object.entries(attributes)) { if (value === undefined) continue; - out[key] = isSensitiveKey(key, r) + out[key] = isSensitiveKey(key, value, r) ? r.mask : (redactValue(value, r, maxDepth) as Attributes[string]); } diff --git a/packages/runtime-node/test/redact.test.mjs b/packages/runtime-node/test/redact.test.mjs index 3f7ba65..cc73e06 100644 --- a/packages/runtime-node/test/redact.test.mjs +++ b/packages/runtime-node/test/redact.test.mjs @@ -125,32 +125,48 @@ test("empty/nullish input yields an empty object", () => { assert.deepEqual(redactAttributes(null), {}); }); -test("keeps only supported GenAI/usage token-count attributes", () => { -const out = redactAttributes({ -"gen_ai.usage.input_tokens": 512, -"gen_ai.usage.output_tokens": 128, -prompt_tokens: 512, -completion_tokens: 128, -total_tokens: 640, -token_count: 42, -max_tokens: 1000, -"secret.input_tokens": 999, -}); - -assert.deepEqual(out, { -"gen_ai.usage.input_tokens": 512, -"gen_ai.usage.output_tokens": 128, -prompt_tokens: 512, -completion_tokens: 128, -total_tokens: 640, -token_count: 42, -max_tokens: MASK, -"secret.input_tokens": MASK, -}); -}); - - -test("still masks secret token keys ending in 'token'", () => { +test("keeps canonical GenAI usage counts only when numeric", () => { + const out = redactAttributes({ + "gen_ai.usage.input_tokens": 512, + "gen_ai.usage.output_tokens": 128, + "gen_ai.usage.prompt_tokens": 256, + "gen_ai.usage.completion_tokens": 128, + "gen_ai.usage.total_tokens": 640, + "gen_ai.usage.token_count": 42, + "gen_ai.usage.input_tokens_string": "secret", + max_tokens: 1000, + "secret.input_tokens": 999, + }); + + assert.deepEqual(out, { + "gen_ai.usage.input_tokens": 512, + "gen_ai.usage.output_tokens": 128, + "gen_ai.usage.prompt_tokens": 256, + "gen_ai.usage.completion_tokens": 128, + "gen_ai.usage.total_tokens": 640, + "gen_ai.usage.token_count": 42, + "gen_ai.usage.input_tokens_string": MASK, + max_tokens: MASK, + "secret.input_tokens": MASK, + }); +}); + +test("redacts non-numeric canonical GenAI usage values", () => { + const out = redactAttributes({ + "gen_ai.usage.input_tokens": "secret", + "gen_ai.usage.output_tokens": -1, + "gen_ai.usage.total_tokens": NaN, + "gen_ai.usage.token_count": Infinity, + }); + + assert.deepEqual(out, { + "gen_ai.usage.input_tokens": MASK, + "gen_ai.usage.output_tokens": MASK, + "gen_ai.usage.total_tokens": MASK, + "gen_ai.usage.token_count": MASK, + }); +}); +test("still masks secret token keys ending in token", () => { const out = redactAttributes({ token: "raw", access_token: "raw",