mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
fix: Added in debug mode, support for storing raw data in json (#2156)
Integrated into release/v3.8.0 — configurable chat log truncation, CHAT_DEBUG_FILE mode, cloudflared state file lock
This commit is contained in:
14
.env.example
14
.env.example
@@ -621,6 +621,14 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 512
|
||||
# CALL_LOG_PIPELINE_MAX_SIZE_KB=512
|
||||
|
||||
# Call log payload truncation limits — controls how much of request/response
|
||||
# bodies is retained in the database.
|
||||
# Used by: open-sse/handlers/chatCore.ts — cloneBoundedChatLogPayload()
|
||||
# CHAT_LOG_TEXT_LIMIT=65536 # Max string length before truncation (default: 64 KB)
|
||||
# CHAT_LOG_ARRAY_TAIL_ITEMS=24 # Number of array items retained from tail (default: 24)
|
||||
# CHAT_LOG_MAX_DEPTH=6 # Max nesting depth before truncation (default: 6)
|
||||
# CHAT_LOG_MAX_OBJECT_KEYS=80 # Max object keys retained (default: 80, 0 = no limit)
|
||||
|
||||
# Maximum rows in the proxy_logs SQLite table.
|
||||
# Default: 100000
|
||||
# PROXY_LOGS_TABLE_MAX_ROWS=100000
|
||||
@@ -777,6 +785,12 @@ APP_LOG_TO_FILE=true
|
||||
# Log Responses API SSE-to-JSON translation details.
|
||||
# DEBUG_RESPONSES_SSE_TO_JSON=true
|
||||
|
||||
# Write raw (untruncated) request/response JSON in call log artifacts.
|
||||
# When enabled, serializeArtifactForStorage skips size-based truncation.
|
||||
# Also enabled automatically when APP_LOG_LEVEL=debug.
|
||||
# WARNING: produces large files — use only for temporary debugging.
|
||||
# CHAT_DEBUG_FILE=true
|
||||
|
||||
# Enable E2E test mode — relaxes auth and enables test harness hooks.
|
||||
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
|
||||
|
||||
|
||||
44
.github/workflows/claude-code-review.yml
vendored
Normal file
44
.github/workflows/claude-code-review.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
50
.github/workflows/claude.yml
vendored
Normal file
50
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr *)'
|
||||
|
||||
@@ -45,7 +45,13 @@ import {
|
||||
} from "../services/errorClassifier.ts";
|
||||
import { updateProviderConnection } from "@/lib/db/providers";
|
||||
import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs";
|
||||
import { getCallLogPipelineCaptureStreamChunks } from "@/lib/logEnv";
|
||||
import {
|
||||
getCallLogPipelineCaptureStreamChunks,
|
||||
getChatLogTextLimit,
|
||||
getChatLogArrayTailItems,
|
||||
getChatLogMaxDepth,
|
||||
getChatLogMaxObjectKeys,
|
||||
} from "@/lib/logEnv";
|
||||
import { logAuditEvent } from "@/lib/compliance";
|
||||
import { extractProviderWarnings } from "@/lib/compliance/providerAudit";
|
||||
import { adaptBodyForCompression } from "../services/compression/bodyAdapter.ts";
|
||||
@@ -181,10 +187,6 @@ import { fetchLiveProviderLimits } from "@/lib/usage/providerLimits";
|
||||
import { isClaudeExtraUsageBlockEnabled } from "@/lib/providers/claudeExtraUsage";
|
||||
|
||||
const MEMORY_EXTRACTION_TEXT_LIMIT = 64 * 1024;
|
||||
const CHAT_LOG_TEXT_LIMIT = 64 * 1024;
|
||||
const CHAT_LOG_ARRAY_TAIL_ITEMS = 24;
|
||||
const CHAT_LOG_MAX_DEPTH = 6;
|
||||
const CHAT_LOG_MAX_OBJECT_KEYS = 80;
|
||||
|
||||
function capMemoryExtractionText(value: string): string {
|
||||
if (value.length <= MEMORY_EXTRACTION_TEXT_LIMIT) return value;
|
||||
@@ -192,28 +194,30 @@ function capMemoryExtractionText(value: string): string {
|
||||
}
|
||||
|
||||
function truncateChatLogText(value: string): string {
|
||||
if (value.length <= CHAT_LOG_TEXT_LIMIT) return value;
|
||||
const head = value.slice(0, Math.floor(CHAT_LOG_TEXT_LIMIT / 2));
|
||||
const tail = value.slice(-Math.ceil(CHAT_LOG_TEXT_LIMIT / 2));
|
||||
return `${head}\n[...truncated ${value.length - CHAT_LOG_TEXT_LIMIT} chars...]\n${tail}`;
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 >= CHAT_LOG_MAX_DEPTH) return "[MaxDepth]";
|
||||
if (depth >= getChatLogMaxDepth()) return "[MaxDepth]";
|
||||
|
||||
const maxTailItems = getChatLogArrayTailItems();
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const retained =
|
||||
value.length > CHAT_LOG_ARRAY_TAIL_ITEMS ? value.slice(-CHAT_LOG_ARRAY_TAIL_ITEMS) : value;
|
||||
const retained = value.length > maxTailItems ? value.slice(-maxTailItems) : value;
|
||||
const cloned = retained.map((item) => cloneBoundedChatLogPayload(item, depth + 1));
|
||||
if (value.length > CHAT_LOG_ARRAY_TAIL_ITEMS) {
|
||||
if (value.length > maxTailItems) {
|
||||
return [
|
||||
{
|
||||
_omniroute_truncated_array: true,
|
||||
originalLength: value.length,
|
||||
retainedTailItems: CHAT_LOG_ARRAY_TAIL_ITEMS,
|
||||
retainedTailItems: maxTailItems,
|
||||
},
|
||||
...cloned,
|
||||
];
|
||||
@@ -223,11 +227,12 @@ function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
const entries = Object.entries(value as Record<string, unknown>);
|
||||
for (const [key, item] of entries.slice(0, CHAT_LOG_MAX_OBJECT_KEYS)) {
|
||||
const maxKeys = getChatLogMaxObjectKeys();
|
||||
for (const [key, item] of maxKeys > 0 ? entries.slice(0, maxKeys) : entries) {
|
||||
result[key] = cloneBoundedChatLogPayload(item, depth + 1);
|
||||
}
|
||||
if (entries.length > CHAT_LOG_MAX_OBJECT_KEYS) {
|
||||
result._omniroute_truncated_keys = entries.length - CHAT_LOG_MAX_OBJECT_KEYS;
|
||||
if (maxKeys > 0 && entries.length > maxKeys) {
|
||||
result._omniroute_truncated_keys = entries.length - maxKeys;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fuse.js": "^7.3.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"ioredis": "^5.10.1",
|
||||
@@ -180,7 +181,6 @@
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"glob": "^13.0.6",
|
||||
"gray-matter": "^4.0.3",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^16.4.0",
|
||||
|
||||
@@ -134,6 +134,7 @@ let tunnelProcess: ReturnType<typeof spawn> | null = null;
|
||||
let tunnelPid: number | null = null;
|
||||
let installPromise: Promise<string> | null = null;
|
||||
let startPromise: Promise<CloudflaredTunnelStatus> | null = null;
|
||||
let stateFileQueue: Promise<void> = Promise.resolve();
|
||||
const NON_ACTIONABLE_CLOUDFLARED_WARNING_PATTERNS = [
|
||||
/failed to sufficiently increase receive buffer size/i,
|
||||
] as const;
|
||||
@@ -205,14 +206,35 @@ async function readStateFile(): Promise<PersistedTunnelState> {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStateFile(state: PersistedTunnelState) {
|
||||
async function writeStateFileNow(state: PersistedTunnelState) {
|
||||
await ensureTunnelDir();
|
||||
await fs.writeFile(getStateFilePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
async function withStateFileLock<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const previous = stateFileQueue;
|
||||
let release!: () => void;
|
||||
stateFileQueue = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
|
||||
await previous;
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStateFile(state: PersistedTunnelState) {
|
||||
await withStateFileLock(() => writeStateFileNow(state));
|
||||
}
|
||||
|
||||
async function updateStateFile(patch: PersistedTunnelState) {
|
||||
const current = await readStateFile();
|
||||
await writeStateFile({ ...current, ...patch });
|
||||
await withStateFileLock(async () => {
|
||||
const current = await readStateFile();
|
||||
await writeStateFileNow({ ...current, ...patch });
|
||||
});
|
||||
}
|
||||
|
||||
async function clearPidFile() {
|
||||
|
||||
@@ -16,6 +16,12 @@ function parsePositiveInt(value: string | undefined, fallback: number): number {
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseNonNegativeInt(value: string | undefined, fallback: number): number {
|
||||
if (value === undefined || value === "") return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
||||
if (!value) return fallback;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
@@ -101,3 +107,26 @@ export function getAppLogLevel(defaultLevel: string): string {
|
||||
export function getAppLogFormat(defaultFormat: string): string {
|
||||
return process.env.APP_LOG_FORMAT || defaultFormat;
|
||||
}
|
||||
|
||||
// ─── Chat log truncation limits ─────────────────────────────────────────────
|
||||
|
||||
export function getChatLogTextLimit(): number {
|
||||
return parsePositiveInt(process.env.CHAT_LOG_TEXT_LIMIT, 64 * 1024);
|
||||
}
|
||||
|
||||
export function getChatLogArrayTailItems(): number {
|
||||
return parsePositiveInt(process.env.CHAT_LOG_ARRAY_TAIL_ITEMS, 24);
|
||||
}
|
||||
|
||||
export function getChatLogMaxDepth(): number {
|
||||
return parsePositiveInt(process.env.CHAT_LOG_MAX_DEPTH, 6);
|
||||
}
|
||||
|
||||
export function getChatLogMaxObjectKeys(): number {
|
||||
return parseNonNegativeInt(process.env.CHAT_LOG_MAX_OBJECT_KEYS, 80);
|
||||
}
|
||||
|
||||
export function isChatDebugFileEnabled(): boolean {
|
||||
if (parseBoolean(process.env.CHAT_DEBUG_FILE, false)) return true;
|
||||
return process.env.APP_LOG_LEVEL?.trim().toLowerCase() === "debug";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { RequestPipelinePayloads } from "@omniroute/open-sse/utils/requestLogger.ts";
|
||||
import { resolveDataDir } from "../dataPaths";
|
||||
import { getCallLogPipelineMaxSizeBytes } from "../logEnv";
|
||||
import { getCallLogPipelineMaxSizeBytes, isChatDebugFileEnabled } from "../logEnv";
|
||||
|
||||
const isCloud = typeof globalThis.caches === "object" && globalThis.caches !== null;
|
||||
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
||||
@@ -151,6 +151,11 @@ function serializeFinalSizeLimitFallback(artifact: CallLogArtifact, maxBytes: nu
|
||||
}
|
||||
|
||||
function serializeArtifactForStorage(artifact: CallLogArtifact): string {
|
||||
// Debug mode: write full untruncated payload
|
||||
if (isChatDebugFileEnabled()) {
|
||||
return JSON.stringify(artifact, null, 2);
|
||||
}
|
||||
|
||||
const maxBytes = getArtifactMaxBytes(artifact);
|
||||
const serialized = JSON.stringify(artifact, null, 2);
|
||||
if (Buffer.byteLength(serialized) <= maxBytes) {
|
||||
|
||||
Reference in New Issue
Block a user