Skip to content

Commit ab3484b

Browse files
fix: Handle more events (#1164)
## Description Describe your changes here. Fixes #Issue_Number (if available) ### Ensure that your pull request has followed all the steps below: - [ ] Code compilation - [ ] Created tests which fail without the change (if possible) - [ ] All tests passing - [ ] Extended the README / documentation, if necessary
1 parent b575657 commit ab3484b

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

EssentialCSharp.Chat.Shared/Services/AIChatService.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ private static string SanitizeForXmlContext(string? input) =>
192192
// Track this leg's response ID so tool-call continuations chain from it,
193193
// ensuring the model's context includes the user's message + reasoning.
194194
string? currentLegResponseId = null;
195+
var textPartsWithDelta = new HashSet<string>(StringComparer.Ordinal);
196+
var refusalPartsWithDelta = new HashSet<string>(StringComparer.Ordinal);
195197
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
196198
List<FunctionCallResponseItem>? pendingFunctionCalls = null;
197199

@@ -223,8 +225,44 @@ private static string SanitizeForXmlContext(string? input) =>
223225
}
224226
else if (update is StreamingResponseOutputTextDeltaUpdate deltaUpdate)
225227
{
228+
textPartsWithDelta.Add($"{deltaUpdate.ItemId}:{deltaUpdate.OutputIndex}:{deltaUpdate.ContentIndex}");
226229
yield return (deltaUpdate.Delta.ToString(), null);
227230
}
231+
else if (update is StreamingResponseOutputTextDoneUpdate doneUpdate)
232+
{
233+
// Some SDK/server combinations emit only TextDone (no deltas) for a content part.
234+
// Emit Done text when no delta was seen for that same part to avoid duplicates.
235+
string textPartKey = $"{doneUpdate.ItemId}:{doneUpdate.OutputIndex}:{doneUpdate.ContentIndex}";
236+
if (!textPartsWithDelta.Contains(textPartKey) && !string.IsNullOrEmpty(doneUpdate.Text))
237+
yield return (doneUpdate.Text, null);
238+
}
239+
else if (update is StreamingResponseRefusalDeltaUpdate refusalDeltaUpdate)
240+
{
241+
refusalPartsWithDelta.Add($"{refusalDeltaUpdate.ItemId}:{refusalDeltaUpdate.OutputIndex}:{refusalDeltaUpdate.ContentIndex}");
242+
yield return (refusalDeltaUpdate.Delta.ToString(), null);
243+
}
244+
else if (update is StreamingResponseRefusalDoneUpdate refusalDoneUpdate)
245+
{
246+
// Refusal content can also arrive as done-only events.
247+
string refusalPartKey = $"{refusalDoneUpdate.ItemId}:{refusalDoneUpdate.OutputIndex}:{refusalDoneUpdate.ContentIndex}";
248+
if (!refusalPartsWithDelta.Contains(refusalPartKey) && !string.IsNullOrEmpty(refusalDoneUpdate.Refusal))
249+
yield return (refusalDoneUpdate.Refusal, null);
250+
}
251+
else if (update is StreamingResponseErrorUpdate errorUpdate)
252+
{
253+
throw new ChatBackendUnavailableException(
254+
$"Streaming response error: {errorUpdate.Code ?? "unknown"} - {errorUpdate.Message ?? "no message provided"}");
255+
}
256+
else if (update is StreamingResponseFailedUpdate failedUpdate)
257+
{
258+
throw new ChatBackendUnavailableException(
259+
BuildStreamingTerminalFailureMessage(failedUpdate.Response, "failed"));
260+
}
261+
else if (update is StreamingResponseIncompleteUpdate incompleteUpdate)
262+
{
263+
throw new ChatBackendUnavailableException(
264+
BuildStreamingTerminalFailureMessage(incompleteUpdate.Response, "incomplete"));
265+
}
228266
// StreamingResponseCompletedUpdate: ResponseId already emitted above — no-op.
229267
}
230268

@@ -600,6 +638,22 @@ private static CreateResponseOptions CloneOptionsWithPreviousResponseId(
600638
return arguments;
601639
}
602640

641+
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
642+
private static string BuildStreamingTerminalFailureMessage(ResponseResult response, string terminalStatus)
643+
{
644+
if (!string.IsNullOrWhiteSpace(response.Error?.Message))
645+
return $"Streaming response {terminalStatus}: {response.Error.Message}";
646+
647+
if (response.IncompleteStatusDetails?.Reason is { } reason)
648+
return $"Streaming response {terminalStatus}: {reason}";
649+
650+
if (response.Status is { } status)
651+
return $"Streaming response ended with status '{status}'.";
652+
653+
return $"Streaming response ended with status '{terminalStatus}'.";
654+
}
655+
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
656+
603657
[LoggerMessage(Level = LogLevel.Information, Message = "AI tool call invoked: tool={ToolName} iteration={Iteration} user={EndUserId}")]
604658
private static partial void LogMcpToolCallInvoked(ILogger logger, string toolName, int iteration, string? endUserId);
605659

0 commit comments

Comments
 (0)