Skip to content

Commit 6e9820d

Browse files
committed
feat(web): add streaming buffer caps to prevent OOM
Caps responseText and reasoningText buffers at 1MB during streaming. Adds truncation markers when buffers exceed limit.
1 parent 0d600e9 commit 6e9820d

File tree

1 file changed

+38
-3
lines changed

1 file changed

+38
-3
lines changed

web/src/llm-api/openrouter.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,13 @@ export async function handleOpenRouterStream({
372372
cancel() {
373373
clearInterval(heartbeatInterval)
374374
clientDisconnected = true
375+
// Log truncated state to prevent OOM during logging (state can be up to 2MB)
375376
logger.warn(
376-
{ clientDisconnected, state },
377+
{
378+
clientDisconnected,
379+
responseTextLength: state.responseText.length,
380+
reasoningTextLength: state.reasoningText.length,
381+
},
377382
'Client cancelled stream, continuing OpenRouter consumption for billing',
378383
)
379384
},
@@ -549,6 +554,10 @@ async function handleStreamChunk({
549554
agentId: string
550555
model: string | undefined
551556
}): Promise<StreamState> {
557+
// Define a safe buffer limit to prevent OOM errors on the server while
558+
// still storing enough data for logging and billing. 1MB is a generous limit.
559+
const MAX_BUFFER_SIZE = 1 * 1024 * 1024 // 1MB
560+
552561
if ('error' in data) {
553562
// Log detailed error information for stream errors (e.g., Forbidden from Anthropic)
554563
const errorData = data.error as {
@@ -581,8 +590,34 @@ async function handleStreamChunk({
581590
return state
582591
}
583592
const choice = data.choices[0]
584-
state.responseText += choice.delta?.content ?? ''
585-
state.reasoningText += choice.delta?.reasoning ?? ''
593+
594+
// Append content and reasoning, but only up to the buffer limit.
595+
const contentDelta = choice.delta?.content ?? ''
596+
if (state.responseText.length < MAX_BUFFER_SIZE) {
597+
state.responseText += contentDelta
598+
if (state.responseText.length >= MAX_BUFFER_SIZE) {
599+
state.responseText =
600+
state.responseText.slice(0, MAX_BUFFER_SIZE) + '\n---[TRUNCATED]---'
601+
logger.warn(
602+
{ userId, agentId, model },
603+
'Response text buffer truncated at 1MB',
604+
)
605+
}
606+
}
607+
608+
const reasoningDelta = choice.delta?.reasoning ?? ''
609+
if (state.reasoningText.length < MAX_BUFFER_SIZE) {
610+
state.reasoningText += reasoningDelta
611+
if (state.reasoningText.length >= MAX_BUFFER_SIZE) {
612+
state.reasoningText =
613+
state.reasoningText.slice(0, MAX_BUFFER_SIZE) + '\n---[TRUNCATED]---'
614+
logger.warn(
615+
{ userId, agentId, model },
616+
'Reasoning text buffer truncated at 1MB',
617+
)
618+
}
619+
}
620+
586621
return state
587622
}
588623

0 commit comments

Comments
 (0)