From 8792303ad82bfb5b7856d123cdd609aecc420d3a Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 03:24:57 -0500 Subject: [PATCH] fix: handle null deltas from Kimi 2.5 API (#4143) - Add explicit null/undefined detection for streaming deltas - Log warnings when null values are encountered - Throw clear error after consecutive null deltas - Provide actionable feedback to users - Track consecutiveNullDeltas in subscription state Fixes #4143 --- ...pi-embedded-subscribe.handlers.messages.ts | 53 ++++++++++++++++++- .../pi-embedded-subscribe.handlers.types.ts | 3 ++ src/agents/pi-embedded-subscribe.ts | 2 + 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.ts b/src/agents/pi-embedded-subscribe.handlers.messages.ts index 1f515e113..ddbb3b33a 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.ts +++ b/src/agents/pi-embedded-subscribe.handlers.messages.ts @@ -54,8 +54,57 @@ export function handleMessageUpdate( return; } - const delta = typeof assistantRecord?.delta === "string" ? assistantRecord.delta : ""; - const content = typeof assistantRecord?.content === "string" ? assistantRecord.content : ""; + // Explicit null/undefined detection (Issue #4143) + // Some providers (e.g., Kimi 2.5) may return null deltas, causing silent hangs + const deltaRaw = assistantRecord?.delta; + const contentRaw = assistantRecord?.content; + const MAX_CONSECUTIVE_NULLS = 5; + + // Check for null/undefined delta in streaming events + if (evtType === "text_delta" && (deltaRaw === null || deltaRaw === undefined)) { + ctx.state.consecutiveNullDeltas++; + ctx.log.warn( + `Received null delta in text_delta event (count: ${ctx.state.consecutiveNullDeltas}/${MAX_CONSECUTIVE_NULLS}). ` + + `This may indicate an API issue with the model provider.`, + ); + + if (ctx.state.consecutiveNullDeltas >= MAX_CONSECUTIVE_NULLS) { + const error = new Error( + `Model provider returned ${ctx.state.consecutiveNullDeltas} consecutive null deltas. ` + + `This typically indicates an API issue or incompatibility. ` + + `Try a different model or contact the provider.`, + ); + error.name = "NullDeltaError"; + + // Emit error event for better user feedback + emitAgentEvent({ + runId: ctx.params.runId, + stream: "error", + data: { + error: error.message, + }, + }); + + throw error; + } + return; // Skip processing this null delta + } + + // Reset counter on valid delta + if (evtType === "text_delta" && typeof deltaRaw === "string" && deltaRaw.length > 0) { + ctx.state.consecutiveNullDeltas = 0; + } + + // Check for null content in text_end + if (evtType === "text_end" && contentRaw === null && !deltaRaw) { + ctx.log.warn( + `Received null content in text_end event with no delta. ` + + `The model may have failed to generate a response.`, + ); + } + + const delta = typeof deltaRaw === "string" ? deltaRaw : ""; + const content = typeof contentRaw === "string" ? contentRaw : ""; appendRawStream({ ts: Date.now(), diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/pi-embedded-subscribe.handlers.types.ts index 4a464c5e2..bac52f39c 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/pi-embedded-subscribe.handlers.types.ts @@ -58,6 +58,9 @@ export type EmbeddedPiSubscribeState = { messagingToolSentTargets: MessagingToolSend[]; pendingMessagingTexts: Map; pendingMessagingTargets: Map; + + // Track consecutive null deltas for hang detection (Issue #4143) + consecutiveNullDeltas: number; }; export type EmbeddedPiSubscribeContext = { diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index a4a4b906a..2f6261443 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -65,6 +65,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar messagingToolSentTargets: [], pendingMessagingTexts: new Map(), pendingMessagingTargets: new Map(), + consecutiveNullDeltas: 0, }; const assistantTexts = state.assistantTexts; @@ -96,6 +97,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar state.lastAssistantTextNormalized = undefined; state.lastAssistantTextTrimmed = undefined; state.assistantTextBaseline = nextAssistantTextBaseline; + state.consecutiveNullDeltas = 0; // Reset null delta counter (Issue #4143) }; const rememberAssistantText = (text: string) => {