mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
* fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies Extracted from PR #9439 (agentic conversation tracking). Most of the original scope this commit was cherry-picked from (CHAT_LOG_MAX_BODY_KB env var support, the estimateSizeFast() earlyExitAt parameterization) turned out to already be present on the current upstream/release/v3.8.50 tip -- confirmed via diff and by running check-env-doc-sync.test.ts / tests/unit/chatcore-log-truncation.test.ts against pristine upstream before making any changes here. Only two genuine gaps remained: 1. CHAT_LOG_MAX_BODY_KB was read by getChatLogMaxBodyBytes() but undocumented in .env.example and docs/reference/ENVIRONMENT.md -- tests/unit/check-env-doc-sync.test.ts flags any env var read in code but missing from both doc files. Documented it (both required -- the same test enforces the pairing). 2. truncateForLog()'s summary only computed messageCount from obj.messages (OpenAI-chat/Gemini field name) -- a large /v1/responses request (which uses input[], not messages[]) got summarized with no count at all, leaving the dashboard's "Full Conversation" panel nothing to base its "N messages not shown" placeholder on for any Responses-API conversation, even though the same summarization logic applies to it. Test plan: - TDD: tests/unit/chatcore-log-truncation.test.ts's new regression test ("captures a message count for Responses API bodies too") confirmed failing against the pre-fix code, passing after. - tests/unit/check-env-doc-sync.test.ts confirms CHAT_LOG_MAX_BODY_KB no longer appears in codeMissingEnv (remaining drift in that test is pre-existing/unrelated -- ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS, COMMANDCODE_API_URL, OMNIROUTE_STRICT_SYSTEM_PROVIDERS, TLS_FINGERPRINT_PROVIDERS -- confirmed identical on a pristine upstream/release/v3.8.50 checkout, base-red inherited: #9985). - tests/unit/chatcore-log-truncation.test.ts -- 19/19 passing. - npx tsc --noEmit / npm run lint -- clean. ⚠️ base-red inherited: #9985 * docs(logging): consolidate CHAT_LOG_MAX_BODY_KB into a single entry per file The variable was already documented (with a stale src/lib/chatLogTruncation.ts reference in .env.example); keep the new richer entries next to the CHAT_LOG_* family and drop the old duplicates. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
104 lines
4.6 KiB
TypeScript
104 lines
4.6 KiB
TypeScript
import {
|
|
getChatLogTextLimit,
|
|
getChatLogMaxDepth,
|
|
getChatLogArrayTailItems,
|
|
getChatLogMaxObjectKeys,
|
|
getChatLogMaxBodyBytes,
|
|
} from "@/lib/logEnv";
|
|
import { estimateSizeFast } from "../../utils/estimateSize.ts";
|
|
|
|
export const MEMORY_EXTRACTION_TEXT_LIMIT = 64 * 1024;
|
|
|
|
export function capMemoryExtractionText(value: string): string {
|
|
if (value.length <= MEMORY_EXTRACTION_TEXT_LIMIT) return value;
|
|
return value.slice(-MEMORY_EXTRACTION_TEXT_LIMIT);
|
|
}
|
|
|
|
export function truncateChatLogText(value: string): string {
|
|
const limit = getChatLogTextLimit();
|
|
if (value.length <= limit) return value;
|
|
const head = value.slice(0, Math.floor(limit / 2));
|
|
const tail = value.slice(-Math.ceil(limit / 2));
|
|
return `${head}\n[...truncated ${value.length - limit} chars...]\n${tail}`;
|
|
}
|
|
|
|
export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
|
|
if (value === null || value === undefined) return value;
|
|
if (typeof value === "string") return truncateChatLogText(value);
|
|
if (typeof value !== "object") return value;
|
|
if (depth >= getChatLogMaxDepth()) return "[MaxDepth]";
|
|
|
|
const maxTailItems = getChatLogArrayTailItems();
|
|
|
|
if (Array.isArray(value)) {
|
|
const retained = value.length > maxTailItems ? value.slice(-maxTailItems) : value;
|
|
const cloned = retained.map((item) => cloneBoundedChatLogPayload(item, depth + 1));
|
|
if (value.length > maxTailItems) {
|
|
return [
|
|
{
|
|
_omniroute_truncated_array: true,
|
|
originalLength: value.length,
|
|
retainedTailItems: maxTailItems,
|
|
},
|
|
...cloned,
|
|
];
|
|
}
|
|
return cloned;
|
|
}
|
|
|
|
const result: Record<string, unknown> = {};
|
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
const maxKeys = getChatLogMaxObjectKeys();
|
|
for (const [key, item] of maxKeys > 0 ? entries.slice(0, maxKeys) : entries) {
|
|
result[key] = cloneBoundedChatLogPayload(item, depth + 1);
|
|
}
|
|
if (maxKeys > 0 && entries.length > maxKeys) {
|
|
result._omniroute_truncated_keys = entries.length - maxKeys;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Truncate a large object for logging. If its JSON representation exceeds
|
|
* getChatLogMaxBodyBytes() (default 1MB; CHAT_LOG_MAX_BODY_KB env override),
|
|
* return a lightweight summary instead of the full clone. This prevents
|
|
* persistAttemptLogs from holding unbounded references to translatedBody
|
|
* across 17 call sites per request.
|
|
*
|
|
* When the summarized object carries a `tools` definition, re-attach it
|
|
* (bounded via `cloneBoundedChatLogPayload`) so the request-details view can
|
|
* still show which tools were available even though the rest of the payload
|
|
* — including the message history that triggered this summary — is dropped.
|
|
* The reused helper already caps array length, object keys, nesting depth,
|
|
* and string length, so this stays a small, bounded addition rather than a
|
|
* separately-budgeted multi-KB/MB re-clone.
|
|
*/
|
|
export function truncateForLog(value: unknown): Record<string, unknown> | null | undefined {
|
|
if (value === null || value === undefined) return value as null | undefined;
|
|
if (typeof value !== "object") return value as unknown as Record<string, unknown>;
|
|
const maxBodyBytes = getChatLogMaxBodyBytes();
|
|
// Pass maxBodyBytes as the early-exit point — otherwise estimateSizeFast's
|
|
// own default 256KB early-exit caps what it can ever report, silently
|
|
// making any configured threshold above 256KB unreachable (#trunc-limit-config).
|
|
const estimatedSize = estimateSizeFast(value, maxBodyBytes);
|
|
if (estimatedSize <= maxBodyBytes) return value as Record<string, unknown>;
|
|
// Object is too large — return a summary instead of a deep clone
|
|
const obj = value as Record<string, unknown>;
|
|
const summary: Record<string, unknown> = {
|
|
_truncated: true,
|
|
_originalBytes: estimatedSize,
|
|
};
|
|
if (typeof obj.model === "string") summary.model = obj.model;
|
|
if (typeof obj.provider === "string") summary.provider = obj.provider;
|
|
if (Array.isArray(obj.messages)) summary.messageCount = obj.messages.length;
|
|
// Responses API bodies use `input[]`, not `messages[]` (OpenAI-chat/Gemini-only
|
|
// field name) — without this, a large /v1/responses request got summarized
|
|
// with no count at all, leaving the dashboard's "Full Conversation" panel
|
|
// nothing to base its "N messages not shown" placeholder on.
|
|
else if (Array.isArray(obj.input)) summary.messageCount = obj.input.length;
|
|
if (Array.isArray(obj.contents)) summary.contentCount = obj.contents.length;
|
|
if (typeof obj.stream === "boolean") summary.stream = obj.stream;
|
|
if (Array.isArray(obj.tools)) summary.tools = cloneBoundedChatLogPayload(obj.tools);
|
|
return summary;
|
|
}
|