Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion open-sse/utils/requestLogger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getPendingById } from "@/lib/usage/usageHistory";
import { getChatLogMaxDepth } from "@/lib/logEnv";
import { sanitizeErrorMessage } from "./error.ts";

type JsonRecord = Record<string, unknown>;
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/lib/logEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/request-logger-bounded-clone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Loading