diff --git a/src/common/util/objectUtils.ts b/src/common/util/objectUtils.ts index 78520a160..4c70baff7 100644 --- a/src/common/util/objectUtils.ts +++ b/src/common/util/objectUtils.ts @@ -72,7 +72,7 @@ export function objectEstimateJsonSize(value: unknown, debugCaller: string): num case 'boolean': return val ? 4 : 5; // "true" or "false" case 'object': { - // Cycle detection + // cycle detection if (seen.has(val as object)) { console.warn(`[estimateJsonSize (${debugCaller})] Circular reference detected, returning 0 for this branch`); return 0; @@ -88,7 +88,7 @@ export function objectEstimateJsonSize(value: unknown, debugCaller: string): num return size; } - // Plain object + // plain object let size = 2; // {} const keys = Object.keys(val); for (let i = 0; i < keys.length; i++) { @@ -122,10 +122,10 @@ export function objectDeepCloneWithStringLimit(value: unknown, debugCaller: stri const seen = new WeakSet(); function clone(val: unknown): unknown { - // Handle primitives + // handle primitives first if (val === null || val === undefined) return val; - // Handle strings - truncate if too long + // handle strings - truncate if too long if (typeof val === 'string') { if (val.length <= maxBytes) return val; const ellipsis = `...[${(val.length - maxBytes).toLocaleString()} bytes]...`; @@ -133,25 +133,22 @@ export function objectDeepCloneWithStringLimit(value: unknown, debugCaller: stri return val.slice(0, half) + ellipsis + val.slice(-half); } - // Handle other primitives + // handle other primitives if (typeof val !== 'object') return val; - // Cycle detection + // cycle detection if (seen.has(val)) return '[Circular]'; seen.add(val); - // Handle arrays - if (Array.isArray(val)) { + // handle arrays - recurse + if (Array.isArray(val)) return val.map(item => clone(item)); - } - // Handle objects + // handle objects - recurse const result: Record = {}; - for (const key in val) { - if (Object.prototype.hasOwnProperty.call(val, key)) { + for (const key in val) + if (Object.prototype.hasOwnProperty.call(val, key)) result[key] = clone((val as Record)[key]); - } - } return result; } diff --git a/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts b/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts index ca9811990..ca59712bc 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts @@ -1,6 +1,8 @@ import { SERVER_DEBUG_WIRE } from '~/server/wire'; import { serverSideId } from '~/server/trpc/trpc.nanoid'; +import { objectDeepCloneWithStringLimit, objectEstimateJsonSize } from '~/common/util/objectUtils'; + import type { AixWire_Particles } from '../../api/aix.wiretypes'; import type { IParticleTransmitter, ParticleServerLogLevel } from './parsers/IParticleTransmitter'; @@ -25,70 +27,6 @@ export const IssueSymbols = { }; -/** Estimates JSON size without stringifying (avoids memory spike on large objects). */ -function _fastEstimateJsonSize(value: any): number { - if (value === null) return 4; // "null" - if (value === undefined) return 0; // omitted in JSON - if (typeof value === 'string') - return value.length + 2; // quotes - if (typeof value === 'number') - return String(value).length; - if (typeof value === 'boolean') - return value ? 4 : 5; // "true" or "false" - if (Array.isArray(value)) { - let size = 2; // [] - for (let i = 0; i < value.length; i++) { - size += _fastEstimateJsonSize(value[i]); - if (i < value.length - 1) size += 1; // comma - } - return size; - } - if (typeof value === 'object') { - let size = 2; // {} - const keys = Object.keys(value); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - size += key.length + 3; // "key": - size += _fastEstimateJsonSize(value[key]); - if (i < keys.length - 1) size += 1; // comma - } - return size; - } - return 0; -} - -/** Deep-clones an object while ellipsizing any string exceeding maxBytes in the middle. */ -function _fastEllipsizeStringsInObject(value: any, maxBytes: number = DEBUG_REQUEST_MAX_STRING_BYTES): any { - // handle primitives first - if (value === null || value === undefined) - return value; - - // handle strings - ellipsize if too long - if (typeof value === 'string') { - if (value.length <= maxBytes) - return value; - const ellipsis = `...[${(value.length - maxBytes).toLocaleString()} bytes]...`; - const half = Math.floor((maxBytes - ellipsis.length) / 2); - return value.slice(0, half) + ellipsis + value.slice(-half); - } - - // handle other primitives (number, boolean) - if (typeof value !== 'object') - return value; - - // handle arrays - recurse - if (Array.isArray(value)) - return value.map(item => _fastEllipsizeStringsInObject(item, maxBytes)); - - // handle objects - recurse - const result: any = {}; - for (const key in value) - if (value.hasOwnProperty(key)) - result[key] = _fastEllipsizeStringsInObject(value[key], maxBytes); - return result; -} - - /** * Queues up and emits small messages (particles) to the client, for the purpose of a stateful * full reconstruction of the AixWire_Parts[] objects. @@ -194,7 +132,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter { addDebugRequest(hideSensitiveData: boolean, url: string, headers: HeadersInit, body?: object) { // Ellipsize individual strings in the body object (e.g., base64 images) to reduce debug packet size - const ellipsizedBody = body ? _fastEllipsizeStringsInObject(body) : undefined; + const ellipsizedBody = body ? objectDeepCloneWithStringLimit(body, 'aix.addDebugRequest', DEBUG_REQUEST_MAX_STRING_BYTES) : undefined; const processedBody = ellipsizedBody ? JSON.stringify(ellipsizedBody, null, 2) : ''; this.transmissionQueue.push({ @@ -204,7 +142,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter { url: url, headers: hideSensitiveData ? '(hidden sensitive data)' : JSON.stringify(headers, null, 2), body: processedBody, - bodySize: body ? _fastEstimateJsonSize(body) : 0, + bodySize: body ? objectEstimateJsonSize(body, 'aix.addDebugRequest') : 0, }, }); }