Skip to content

feat(gateway): add pending log tracking for timeout detection - #1572

Closed
steebchen wants to merge 1 commit into
mainfrom
add-timeout-log-tracking
Closed

steebchen wants to merge 1 commit into
mainfrom
add-timeout-log-tracking

Conversation

@steebchen

@steebchen steebchen commented Feb 2, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Insert pending log entry at the start of /v1/chat/completions requests to track requests that may timeout or be cancelled
  • Add pending status to UnifiedFinishReason enum for tracking in-flight requests
  • Add insertPendingLog() function for synchronous pending log insertion at request start
  • Add updateLog() function and LOG_UPDATE_QUEUE for async log updates when request completes
  • Add processLogUpdateQueue() worker loop to handle log updates

Test plan

  • Verify pending logs are created at request start
  • Verify logs are updated to final status on successful completion
  • Verify logs are updated on error/timeout/cancellation
  • Query for unifiedFinishReason='pending' logs older than 5 minutes to detect stuck requests

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added in-flight request tracking to provide real-time visibility into ongoing operations
    • Introduced asynchronous log update capability for more accurate final state recording after request completion
    • New "pending" status to distinguish in-progress requests from completed ones
  • Infrastructure

    • Enhanced backend worker with retry logic and queue processing for reliable log updates

Copilot AI review requested due to automatic review settings February 2, 2026 17:23
@coderabbitai

coderabbitai Bot commented Feb 2, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces a pending log tracking system for in-flight requests. It adds insertPendingLog to create early log entries, updateLog to modify existing logs, and a finalizeLog helper to unify log finalization across multiple response paths. A new LOG_UPDATE_QUEUE enables asynchronous log updates processed by the worker with retry logic. The PENDING finish reason is added to the schema.

Changes

Cohort / File(s) Summary
Pending Log Tracking
apps/gateway/src/chat/chat.ts
Inserts pending log early via insertPendingLog, introduces finalizeLog helper to unify logging decisions, and replaces direct insertLog calls across all response paths (cached streaming/non-streaming, timeouts, cancellations, errors, and final responses).
Log Operations
apps/gateway/src/lib/logs.ts
Adds insertPendingLog(data) to create placeholder pending entries and returns logId; adds updateLog(logData) with finish-reason computation and error handling; exports new PendingLogData and LogUpdateData interfaces.
Queue Infrastructure
packages/cache/src/redis.ts
Adds new exported constant LOG_UPDATE_QUEUE for log update messages, mirroring existing LOG_QUEUE with environment-specific naming.
Worker Queue Processing
apps/worker/src/worker.ts
Implements processLogUpdateQueue() to consume log update messages with sensitive field stripping and retry logic; adds runLogUpdateQueueLoop() and integrates into worker startup to run in parallel with existing queues.
Schema Enhancement
packages/db/src/schema.ts
Adds PENDING: "pending" enum value to UnifiedFinishReason, expanding accepted finish-reason options.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ChatGateway as Chat Gateway
    participant PendingDB as Pending Log DB
    participant ResponsePath as Response Processing
    participant UpdateQueue as LOG_UPDATE_QUEUE
    participant Worker

    Client->>ChatGateway: Chat request
    ChatGateway->>PendingDB: insertPendingLog (early tracking)
    PendingDB-->>ChatGateway: logId
    ChatGateway->>ResponsePath: Process request (cache/stream/etc)
    ResponsePath->>ChatGateway: Response received
    ChatGateway->>UpdateQueue: finalizeLog → updateLog (via queue)
    UpdateQueue-->>Worker: Consume log update message
    Worker->>PendingDB: Update log with final data + retry
    PendingDB-->>Worker: Log updated
    Worker-->>Client: Request complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(gateway): add pending log tracking for timeout detection' directly and clearly summarizes the main change: adding pending log tracking functionality to the gateway for timeout detection purposes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-timeout-log-tracking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Insert a pending log entry at the start of each /v1/chat/completions
request to track requests that may timeout or be cancelled before
completing normally.

Changes:
- Add PENDING status to UnifiedFinishReason enum
- Add insertPendingLog() for synchronous DB insert at request start
- Add updateLog() for async updates via LOG_UPDATE_QUEUE
- Add processLogUpdateQueue() worker loop for handling updates
- Convert all insertLog() calls to finalizeLog() helper that updates
  pending logs or falls back to insert

This allows detecting timed out or cancelled requests by querying for
logs with unifiedFinishReason = 'pending' that are older than expected.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds pending log tracking to detect requests that timeout or are cancelled before completing. The implementation creates a pending log entry at the start of each /v1/chat/completions request and updates it when the request completes, allowing detection of stuck requests by querying for old pending logs.

Changes:

  • Adds PENDING status to UnifiedFinishReason enum for tracking in-flight requests
  • Implements insertPendingLog() for synchronous DB insert at request start and updateLog() for async updates via queue
  • Adds LOG_UPDATE_QUEUE with corresponding worker processing loop processLogUpdateQueue()
  • Converts all insertLog() calls to finalizeLog() helper that updates pending logs or falls back to insert

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/db/src/schema.ts Adds PENDING status to UnifiedFinishReason enum
packages/cache/src/redis.ts Adds LOG_UPDATE_QUEUE constant for log update messages
apps/worker/src/worker.ts Implements processLogUpdateQueue() function and runLogUpdateQueueLoop() with retry logic
apps/gateway/src/lib/logs.ts Adds insertPendingLog(), updateLog(), and related type definitions
apps/gateway/src/chat/chat.ts Integrates pending log tracking with finalizeLog() helper replacing insertLog() calls

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +214 to +251
try {
await db.insert(log).values({
id: logId,
requestId: data.requestId,
organizationId: data.organizationId,
projectId: data.projectId,
apiKeyId: data.apiKeyId,
requestedModel: data.requestedModel,
requestedProvider: data.requestedProvider,
// Set placeholder values for required fields - these will be updated later
usedModel: data.requestedModel,
usedProvider: data.requestedProvider || "unknown",
duration: 0,
responseSize: 0,
mode: data.mode,
usedMode: data.usedMode,
// Set pending status
unifiedFinishReason: UnifiedFinishReason.PENDING,
// Include available data
messages: data.messages,
streamed: data.streamed,
source: data.source,
userAgent: data.userAgent,
// Initialize other fields
hasError: false,
canceled: false,
cached: false,
dataStorageCost: "0",
});

return logId;
} catch (error) {
logger.error("Failed to insert pending log", {
requestId: data.requestId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The insertPendingLog function inserts a log synchronously to the database, which could impact request latency. Since this operation happens on every request before any caching or processing begins, any database slowness or connection issues will directly affect the user-facing API response time. Consider moving this to an async fire-and-forget pattern or making it optional based on a feature flag to reduce the performance impact on the critical path.

Suggested change
try {
await db.insert(log).values({
id: logId,
requestId: data.requestId,
organizationId: data.organizationId,
projectId: data.projectId,
apiKeyId: data.apiKeyId,
requestedModel: data.requestedModel,
requestedProvider: data.requestedProvider,
// Set placeholder values for required fields - these will be updated later
usedModel: data.requestedModel,
usedProvider: data.requestedProvider || "unknown",
duration: 0,
responseSize: 0,
mode: data.mode,
usedMode: data.usedMode,
// Set pending status
unifiedFinishReason: UnifiedFinishReason.PENDING,
// Include available data
messages: data.messages,
streamed: data.streamed,
source: data.source,
userAgent: data.userAgent,
// Initialize other fields
hasError: false,
canceled: false,
cached: false,
dataStorageCost: "0",
});
return logId;
} catch (error) {
logger.error("Failed to insert pending log", {
requestId: data.requestId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
// Perform the database insert in a fire-and-forget manner so it does not
// block the request's critical path. Any errors are logged but do not
// affect the main request flow.
void (async () => {
try {
await db.insert(log).values({
id: logId,
requestId: data.requestId,
organizationId: data.organizationId,
projectId: data.projectId,
apiKeyId: data.apiKeyId,
requestedModel: data.requestedModel,
requestedProvider: data.requestedProvider,
// Set placeholder values for required fields - these will be updated later
usedModel: data.requestedModel,
usedProvider: data.requestedProvider || "unknown",
duration: 0,
responseSize: 0,
mode: data.mode,
usedMode: data.usedMode,
// Set pending status
unifiedFinishReason: UnifiedFinishReason.PENDING,
// Include available data
messages: data.messages,
streamed: data.streamed,
source: data.source,
userAgent: data.userAgent,
// Initialize other fields
hasError: false,
canceled: false,
cached: false,
dataStorageCost: "0",
});
} catch (error) {
logger.error("Failed to insert pending log", {
requestId: data.requestId,
error: error instanceof Error ? error.message : String(error),
});
}
})();
return logId;

Copilot uses AI. Check for mistakes.
Comment on lines +1662 to +1686
// Insert pending log to track requests that may timeout or be cancelled
// This is done before cache checks so we can detect issues with the request flow
let pendingLogId: string | null = null;
try {
pendingLogId = await insertPendingLog({
requestId,
organizationId: project.organizationId,
projectId: apiKey.projectId,
apiKeyId: apiKey.id,
requestedModel: initialRequestedModel,
requestedProvider: requestedProvider || null,
mode: project.mode,
usedMode: providerKey?.id ? "api-keys" : "credits",
messages,
streamed: stream || false,
source: source || null,
userAgent: userAgent || null,
});
} catch (error) {
// Log the error but don't fail the request - we can still process without tracking
logger.warn("Failed to insert pending log, continuing without tracking", {
requestId,
error: error instanceof Error ? error.message : String(error),
});
}

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pending log insertion happens at line 1666 (based on the diff), but any exceptions thrown after this point and before the first finalizeLog call will leave orphaned pending logs in the database. While the PR description mentions detecting stuck requests by querying for pending logs older than 5 minutes, there's no cleanup mechanism implemented. Without a cleanup job, these orphaned logs will accumulate indefinitely. Consider adding a periodic cleanup task in the worker to mark or remove truly stuck pending logs.

Copilot uses AI. Check for mistakes.
Comment thread packages/db/src/schema.ts
import type z from "zod";

export const UnifiedFinishReason = {
PENDING: "pending",

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The database schema defines unifiedFinishReason as a text field without any index. Querying for pending logs (e.g., WHERE unified_finish_reason = 'pending' AND created_at < NOW() - INTERVAL '5 minutes') will require a full table scan as the log table grows. Consider adding an index on (unifiedFinishReason, createdAt) or a partial index specifically for pending logs to support efficient querying for stuck requests.

Copilot uses AI. Check for mistakes.
Comment thread apps/worker/src/worker.ts
Comment on lines +974 to +977
const { logId, ...fieldsToUpdate } = updateData;
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processLogUpdateQueue function destructures logId from updateData and passes the rest as fieldsToUpdate using the spread operator. However, LogInsertData type explicitly omits id, createdAt, and updatedAt fields (see packages/db/src/types.ts line 102-105). When updating a log, you shouldn't be setting createdAt or potentially updatedAt explicitly. Ensure that the update operation doesn't accidentally set these fields, or explicitly filter them out before the update.

Suggested change
const { logId, ...fieldsToUpdate } = updateData;
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)
// Explicitly exclude fields that should not be updated directly
const { logId, createdAt: _createdAt, updatedAt: _updatedAt, id: _id, ...safeFieldsToUpdate } =
updateData;
await db
.update(log)
.set(safeFieldsToUpdate as Partial<LogInsertData>)

Copilot uses AI. Check for mistakes.
Comment thread apps/worker/src/worker.ts
Comment on lines +924 to +925
*/
interface LogUpdateData extends LogInsertData {

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LogUpdateData interface is defined twice with different definitions. In apps/gateway/src/lib/logs.ts (line 258), it's defined as Omit<LogInsertData, "id"> with a logId field, while in apps/worker/src/worker.ts (line 925), it's defined as extending LogInsertData with a logId field. This inconsistency means the worker's version includes the id field from LogInsertData which should have been omitted. This could lead to type confusion. Ensure both definitions are consistent, likely by using the same definition from logs.ts or importing it.

Suggested change
*/
interface LogUpdateData extends LogInsertData {
* Mirrors the definition in apps/gateway/src/lib/logs.ts.
*/
interface LogUpdateData extends Omit<LogInsertData, "id"> {

Copilot uses AI. Check for mistakes.
Comment thread apps/worker/src/worker.ts
Comment on lines +988 to +995
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s, ...
logger.warn(
`Failed to update logs (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms...`,
lastError,
);
await new Promise((resolve) => {
setTimeout(resolve, delay);
});

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry logic uses exponential backoff (1s, 2s, 4s, 8s, 16s) but all retries happen synchronously within the same message processing cycle. With 5 retries, a single failed update could block the queue for up to 31 seconds (1+2+4+8+16), preventing other log updates from being processed. If multiple messages fail, this could create significant queue delays. Consider making the backoff asynchronous or using a dead-letter queue pattern where failed messages are moved aside after the first few retries and processed separately.

Suggested change
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s, ...
logger.warn(
`Failed to update logs (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms...`,
lastError,
);
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
logger.warn(
`Failed to update logs (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying immediately...`,
lastError,
);
// Removed blocking exponential backoff to avoid stalling queue processing.
// Next retry will be attempted immediately in the next loop iteration.

Copilot uses AI. Check for mistakes.
Comment thread apps/worker/src/worker.ts
Comment on lines +975 to +978
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)
.where(eq(log.id, logId));

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple update messages for the same log ID arrive in the queue (e.g., due to race conditions or retries), they will be processed independently and could lead to data loss. The last update to complete will win, potentially overwriting earlier updates that completed first. Consider implementing an optimistic locking mechanism or ensuring idempotency by checking timestamps or version numbers before applying updates.

Suggested change
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)
.where(eq(log.id, logId));
// Use optimistic locking when an updatedAt field is provided:
// only apply the update if the existing row is older than the
// incoming update. This helps avoid "last write wins" data loss
// when multiple updates for the same log are processed.
const baseCondition = eq(log.id, logId);
const condition =
"updatedAt" in fieldsToUpdate && fieldsToUpdate.updatedAt
? and(
baseCondition,
lt(
// @ts-expect-error: updatedAt is expected to exist on the log table
log.updatedAt,
fieldsToUpdate.updatedAt as unknown as Date,
),
)
: baseCondition;
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)
.where(condition);

Copilot uses AI. Check for mistakes.
Comment thread apps/worker/src/worker.ts
Comment on lines +929 to +1030
export async function processLogUpdateQueue(): Promise<void> {
const message = await consumeFromQueue(LOG_UPDATE_QUEUE);

if (!message) {
return;
}

const MAX_RETRIES = 5;

try {
const logUpdateData = message.map((i) => JSON.parse(i) as LogUpdateData);

const processedLogData: LogUpdateData[] = await Promise.all(
logUpdateData.map(async (data) => {
const org = await db.query.organization.findFirst({
where: {
id: {
eq: data.organizationId,
},
},
});

if (org?.retentionLevel === "none") {
const {
messages: _messages,
content: _content,
reasoningContent: _reasoningContent,
tools: _tools,
toolChoice: _toolChoice,
toolResults: _toolResults,
...metadataOnly
} = data;
return metadataOnly as LogUpdateData;
}

return data;
}),
);

// Update logs with retry logic
let lastError: Error | undefined;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
// Process each update individually since we need to update by ID
for (const updateData of processedLogData) {
const { logId, ...fieldsToUpdate } = updateData;
await db
.update(log)
.set(fieldsToUpdate as Partial<LogInsertData>)
.where(eq(log.id, logId));
}
return; // Success, exit function
} catch (updateError) {
lastError =
updateError instanceof Error
? updateError
: new Error(String(updateError));

if (attempt < MAX_RETRIES) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s, ...
logger.warn(
`Failed to update logs (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms...`,
lastError,
);
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
}
}

// All retries exhausted, push messages back to queue for later processing
logger.error(
`Failed to update logs after ${MAX_RETRIES + 1} attempts, pushing back to queue`,
lastError,
);

// Re-add messages to queue
for (const msg of message) {
await publishToQueue(LOG_UPDATE_QUEUE, JSON.parse(msg));
}
} catch (error) {
logger.error(
"Error processing log update message",
error instanceof Error ? error : new Error(String(error)),
);

// Re-add messages to queue on unexpected errors
try {
for (const msg of message) {
await publishToQueue(LOG_UPDATE_QUEUE, JSON.parse(msg));
}
} catch (requeueError) {
logger.error(
"Failed to re-queue log update messages",
requeueError instanceof Error
? requeueError
: new Error(String(requeueError)),
);
}
}
}

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new processLogUpdateQueue function lacks test coverage. Given that the existing worker has test files (worker.spec.ts and log-processing.spec.ts), and this function introduces new critical functionality for updating pending logs, it should have comprehensive test coverage. Tests should validate the retry logic, error handling, data retention filtering, and queue re-publishing behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +295
export async function insertPendingLog(data: PendingLogData): Promise<string> {
const logId = shortid();

try {
await db.insert(log).values({
id: logId,
requestId: data.requestId,
organizationId: data.organizationId,
projectId: data.projectId,
apiKeyId: data.apiKeyId,
requestedModel: data.requestedModel,
requestedProvider: data.requestedProvider,
// Set placeholder values for required fields - these will be updated later
usedModel: data.requestedModel,
usedProvider: data.requestedProvider || "unknown",
duration: 0,
responseSize: 0,
mode: data.mode,
usedMode: data.usedMode,
// Set pending status
unifiedFinishReason: UnifiedFinishReason.PENDING,
// Include available data
messages: data.messages,
streamed: data.streamed,
source: data.source,
userAgent: data.userAgent,
// Initialize other fields
hasError: false,
canceled: false,
cached: false,
dataStorageCost: "0",
});

return logId;
} catch (error) {
logger.error("Failed to insert pending log", {
requestId: data.requestId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}

/**
* Data for updating an existing log entry.
* Includes the log ID and all the fields to update.
*/
export interface LogUpdateData extends Omit<LogInsertData, "id"> {
logId: string;
}

/**
* Update an existing log entry via the message queue.
* This is called at the end of a request to update the pending log with final data.
*/
export async function updateLog(logData: LogUpdateData): Promise<unknown> {
if (logData.unifiedFinishReason === undefined) {
if (logData.canceled) {
logData.unifiedFinishReason = UnifiedFinishReason.CANCELED;
} else {
logData.unifiedFinishReason = getUnifiedFinishReason(
logData.finishReason,
logData.usedProvider,
);

if (
logData.unifiedFinishReason === UnifiedFinishReason.UNKNOWN &&
logData.finishReason &&
!isExpectedUnknownFinishReason(
logData.finishReason,
logData.usedProvider,
)
) {
logger.error("Unknown finish reason encountered", {
requestId: logData.requestId,
finishReason: logData.finishReason,
provider: logData.usedProvider,
model: logData.usedModel,
});
}
}
}
await publishToQueue(LOG_UPDATE_QUEUE, logData);
return 1;
}

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new functions insertPendingLog and updateLog lack test coverage. The existing logs.spec.ts file has tests for other log-related functions, so these new functions should also have tests. Tests should cover successful insertion, error handling, queue publishing, and the unified finish reason logic in updateLog.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/gateway/src/lib/logs.ts`:
- Around line 191-242: The pending log currently always writes request messages
to the DB; update insertPendingLog and PendingLogData to accept a retentionLevel
("none" | ...) and, when retentionLevel === "none", avoid storing messages
(e.g., set messages: null or omit it) so sensitive content isn't persisted;
update the call sites to pass the retention level into insertPendingLog and
adjust any types/DB schema usage accordingly (refer to PendingLogData and
insertPendingLog to locate changes and add the conditional use of
data.retentionLevel when assigning the messages field).

Comment on lines +191 to +242
export interface PendingLogData {
requestId: string;
organizationId: string;
projectId: string;
apiKeyId: string;
requestedModel: string;
requestedProvider: string | null;
mode: "api-keys" | "credits" | "hybrid";
usedMode: "api-keys" | "credits";
messages: unknown;
streamed: boolean;
source: string | null;
userAgent: string | null;
}

/**
* Insert a pending log entry synchronously to the database.
* This is called at the start of a request to track requests that may timeout or be cancelled.
* Returns the log ID that should be used in the subsequent updateLog() call.
*/
export async function insertPendingLog(data: PendingLogData): Promise<string> {
const logId = shortid();

try {
await db.insert(log).values({
id: logId,
requestId: data.requestId,
organizationId: data.organizationId,
projectId: data.projectId,
apiKeyId: data.apiKeyId,
requestedModel: data.requestedModel,
requestedProvider: data.requestedProvider,
// Set placeholder values for required fields - these will be updated later
usedModel: data.requestedModel,
usedProvider: data.requestedProvider || "unknown",
duration: 0,
responseSize: 0,
mode: data.mode,
usedMode: data.usedMode,
// Set pending status
unifiedFinishReason: UnifiedFinishReason.PENDING,
// Include available data
messages: data.messages,
streamed: data.streamed,
source: data.source,
userAgent: data.userAgent,
// Initialize other fields
hasError: false,
canceled: false,
cached: false,
dataStorageCost: "0",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Respect retentionLevel when inserting pending logs.

Pending logs are written directly to the DB, so for retentionLevel = "none" the request messages can persist indefinitely (the update path won’t clear them). This breaks retention guarantees and can leak sensitive content.

🛡️ Proposed fix (avoid storing messages when retention is "none")
 export interface PendingLogData {
 	requestId: string;
 	organizationId: string;
 	projectId: string;
 	apiKeyId: string;
 	requestedModel: string;
 	requestedProvider: string | null;
 	mode: "api-keys" | "credits" | "hybrid";
 	usedMode: "api-keys" | "credits";
 	messages: unknown;
 	streamed: boolean;
 	source: string | null;
 	userAgent: string | null;
+	retentionLevel?: "retain" | "none" | null;
 }

 export async function insertPendingLog(data: PendingLogData): Promise<string> {
 	const logId = shortid();
+	const storeMessages = data.retentionLevel !== "none";

 	try {
 		await db.insert(log).values({
 			id: logId,
 			requestId: data.requestId,
 			organizationId: data.organizationId,
 			projectId: data.projectId,
 			apiKeyId: data.apiKeyId,
 			requestedModel: data.requestedModel,
 			requestedProvider: data.requestedProvider,
 			usedModel: data.requestedModel,
 			usedProvider: data.requestedProvider || "unknown",
 			duration: 0,
 			responseSize: 0,
 			mode: data.mode,
 			usedMode: data.usedMode,
 			unifiedFinishReason: UnifiedFinishReason.PENDING,
-			messages: data.messages,
+			messages: storeMessages ? data.messages : null,
 			streamed: data.streamed,
 			source: data.source,
 			userAgent: data.userAgent,
 			hasError: false,
 			canceled: false,
 			cached: false,
 			dataStorageCost: "0",
 		});

And pass the retention level at the call site:

 	pendingLogId = await insertPendingLog({
 		requestId,
 		organizationId: project.organizationId,
 		projectId: apiKey.projectId,
 		apiKeyId: apiKey.id,
 		requestedModel: initialRequestedModel,
 		requestedProvider: requestedProvider || null,
 		mode: project.mode,
 		usedMode: providerKey?.id ? "api-keys" : "credits",
 		messages,
 		streamed: stream || false,
 		source: source || null,
 		userAgent: userAgent || null,
+		retentionLevel,
 	});
🤖 Prompt for AI Agents
In `@apps/gateway/src/lib/logs.ts` around lines 191 - 242, The pending log
currently always writes request messages to the DB; update insertPendingLog and
PendingLogData to accept a retentionLevel ("none" | ...) and, when
retentionLevel === "none", avoid storing messages (e.g., set messages: null or
omit it) so sensitive content isn't persisted; update the call sites to pass the
retention level into insertPendingLog and adjust any types/DB schema usage
accordingly (refer to PendingLogData and insertPendingLog to locate changes and
add the conditional use of data.retentionLevel when assigning the messages
field).

@steebchen steebchen closed this Feb 2, 2026
@steebchen
steebchen deleted the add-timeout-log-tracking branch February 2, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants