diff --git a/open-sse/utils/requestLogger.ts b/open-sse/utils/requestLogger.ts index f2ef74e84e..79a307c688 100644 --- a/open-sse/utils/requestLogger.ts +++ b/open-sse/utils/requestLogger.ts @@ -1,4 +1,5 @@ import { getPendingById } from "@/lib/usage/usageHistory"; +import { getChatLogMaxDepth } from "@/lib/logEnv"; import { sanitizeErrorMessage } from "./error.ts"; type JsonRecord = Record; @@ -148,7 +149,7 @@ export function cloneBoundedForLog(value: unknown, depth = 0, key: string | null if (ArrayBuffer.isView(value)) { return `[binary ${(value as ArrayBufferView).byteLength} bytes]`; } - if (depth >= 6) return "[MaxDepth]"; + if (depth >= getChatLogMaxDepth()) return "[MaxDepth]"; if (Array.isArray(value)) { // Idempotence (#7847): an already-bounded array is [marker, ...tail] — MAX_LOG_ARRAY_ITEMS + 1 diff --git a/src/lib/logEnv.ts b/src/lib/logEnv.ts index 8486628b4e..bcba501db0 100644 --- a/src/lib/logEnv.ts +++ b/src/lib/logEnv.ts @@ -150,8 +150,17 @@ export function getChatLogArrayTailItems(): number { return parsePositiveInt(process.env.CHAT_LOG_ARRAY_TAIL_ITEMS, 24); } +/** + * Was a hardcoded 6 — trivially too shallow for real Chat Completions tool + * calls: `body.choices[0].message.tool_calls[0].function` alone is already + * 6 levels deep (body→choices→[i]→message→tool_calls→[i]→function), so + * EVERY logged tool call got its `function` field (name + arguments) + * replaced outright with the literal string "[MaxDepth]" before the name/ + * arguments one level further in were ever reached — not an edge case, a + * universal truncation of tool-call data in call log artifacts. + */ export function getChatLogMaxDepth(): number { - return parsePositiveInt(process.env.CHAT_LOG_MAX_DEPTH, 6); + return parsePositiveInt(process.env.CHAT_LOG_MAX_DEPTH, 20); } export function getChatLogMaxObjectKeys(): number { diff --git a/tests/unit/request-logger-bounded-clone.test.ts b/tests/unit/request-logger-bounded-clone.test.ts index bcaa8b6cc4..9de71b9b5c 100644 --- a/tests/unit/request-logger-bounded-clone.test.ts +++ b/tests/unit/request-logger-bounded-clone.test.ts @@ -38,6 +38,40 @@ test("cloneBoundedForLog: nested tools field still exempt", () => { assert.equal(result.body.tools.length, 30); }); +// Regression: a Chat Completions response's tool_calls[].function is 6 levels +// deep from the response body (body -> choices -> [i] -> message -> tool_calls +// -> [i] -> function) — the depth cap used to be a hardcoded 6, so every +// logged tool call's `function` (name + arguments) got replaced outright with +// the literal string "[MaxDepth]", not just deeply truncated. This broke tool +// call rendering in the request-detail view for ANY response with a tool +// call — not an edge case, universal. +test("cloneBoundedForLog: tool_calls[].function survives at its natural depth (was clobbered to '[MaxDepth]')", () => { + const body = { + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "write", arguments: '{"path":"/tmp/x","content":"hi"}' }, + }, + ], + }, + }, + ], + }; + const result = cloneBoundedForLog(body) as { + choices: Array<{ message: { tool_calls: Array<{ function: unknown }> } }>; + }; + const fn = result.choices[0].message.tool_calls[0].function; + assert.notEqual(fn, "[MaxDepth]", "function must not be clobbered to the MaxDepth placeholder"); + assert.deepEqual(fn, { name: "write", arguments: '{"path":"/tmp/x","content":"hi"}' }); +}); + test("cloneBoundedForLog: top-level array without key context still truncated", () => { const arr = Array.from({ length: 45 }, (_, i) => i); const result = cloneBoundedForLog(arr) as unknown[];