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
This commit is contained in:
spiceoogway 2026-01-30 03:24:57 -05:00
parent 6af205a13a
commit 8792303ad8
3 changed files with 56 additions and 2 deletions

View File

@ -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(),

View File

@ -58,6 +58,9 @@ export type EmbeddedPiSubscribeState = {
messagingToolSentTargets: MessagingToolSend[];
pendingMessagingTexts: Map<string, string>;
pendingMessagingTargets: Map<string, MessagingToolSend>;
// Track consecutive null deltas for hang detection (Issue #4143)
consecutiveNullDeltas: number;
};
export type EmbeddedPiSubscribeContext = {

View File

@ -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) => {