-
Notifications
You must be signed in to change notification settings - Fork 0
Fix chat response truncation and raw JSON display #635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a56a3be
ca2fabc
1eb8548
d6bc411
a463641
5d51b68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,12 +52,43 @@ public async Task<LlmCompletionResult> CompleteAsync(ChatCompletionRequest reque | |
| return BuildFallbackResult(lastUserMessage, "Live provider request failed.", GetConfiguredModelOrDefault()); | ||
| } | ||
|
|
||
| if (!TryParseResponse(body, out var content, out var tokensUsed)) | ||
| if (!TryParseResponse(body, out var content, out var tokensUsed, out var finishReason)) | ||
| { | ||
| _logger.LogWarning("OpenAI completion response could not be parsed."); | ||
| return BuildFallbackResult(lastUserMessage, "Live provider response parsing failed.", GetConfiguredModelOrDefault()); | ||
| } | ||
|
|
||
| // Detect truncation: OpenAI returns finish_reason "length" when the | ||
| // response was cut off by the max_tokens limit. | ||
| if (string.Equals(finishReason, "length", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| _logger.LogWarning("OpenAI response was truncated (finish_reason=length)."); | ||
| return new LlmCompletionResult( | ||
| content, | ||
| tokensUsed, | ||
| IsActionable: false, | ||
| Provider: "OpenAI", | ||
| Model: GetConfiguredModelOrDefault(), | ||
| IsDegraded: true, | ||
| DegradedReason: "Response was truncated"); | ||
| } | ||
|
|
||
| // When JSON mode was requested and the response starts with '{' but | ||
| // does not parse as valid JSON, the output was likely truncated. | ||
| var useInstructionExtraction = request.SystemPrompt is null; | ||
| if (useInstructionExtraction && LooksLikeTruncatedJson(content)) | ||
| { | ||
| _logger.LogWarning("OpenAI JSON-mode response is not valid JSON; treating as truncated."); | ||
| return new LlmCompletionResult( | ||
| content, | ||
| tokensUsed, | ||
| IsActionable: false, | ||
| Provider: "OpenAI", | ||
| Model: GetConfiguredModelOrDefault(), | ||
| IsDegraded: true, | ||
| DegradedReason: "Response was truncated"); | ||
| } | ||
|
|
||
| // Try to parse structured instruction extraction from the LLM response | ||
| if (LlmInstructionExtractionPrompt.TryParseStructuredResponse( | ||
| content, | ||
|
|
@@ -227,10 +258,11 @@ private object BuildRequestPayload(ChatCompletionRequest request) | |
| return payload; | ||
| } | ||
|
|
||
| private static bool TryParseResponse(string responseBody, out string content, out int tokensUsed) | ||
| private static bool TryParseResponse(string responseBody, out string content, out int tokensUsed, out string? finishReason) | ||
| { | ||
| content = string.Empty; | ||
| tokensUsed = 0; | ||
| finishReason = null; | ||
|
|
||
| if (string.IsNullOrWhiteSpace(responseBody)) | ||
| { | ||
|
|
@@ -259,6 +291,12 @@ private static bool TryParseResponse(string responseBody, out string content, ou | |
| return false; | ||
| } | ||
|
|
||
| if (first.TryGetProperty("finish_reason", out var finishReasonElement) && | ||
| finishReasonElement.ValueKind == JsonValueKind.String) | ||
| { | ||
| finishReason = finishReasonElement.GetString(); | ||
| } | ||
|
|
||
| if (root.TryGetProperty("usage", out var usage) && | ||
| usage.TryGetProperty("total_tokens", out var totalTokens) && | ||
| totalTokens.TryGetInt32(out var parsedTokens)) | ||
|
|
@@ -308,6 +346,30 @@ private static LlmCompletionResult BuildFallbackResult(string userMessage, strin | |
| Instructions: instructions); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns true when <paramref name="text"/> starts with '{' but does not | ||
| /// parse as valid JSON — a strong signal the response was cut off mid-output. | ||
| /// </summary> | ||
| internal static bool LooksLikeTruncatedJson(string text) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(text)) | ||
| return false; | ||
|
|
||
| var trimmed = text.TrimStart(); | ||
| if (!trimmed.StartsWith('{')) | ||
| return false; | ||
|
|
||
| try | ||
| { | ||
| using var doc = JsonDocument.Parse(trimmed); | ||
| return false; | ||
| } | ||
| catch (JsonException) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
Comment on lines
+353
to
+371
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is identical to the one in internal static bool LooksLikeTruncatedJson(string text)
{
if (string.IsNullOrWhiteSpace(text)) return false;
var trimmed = text.Trim();
var first = trimmed.IndexOf('{');
var last = trimmed.LastIndexOf('}');
if (first >= 0 && last > first)
{
try
{
using var doc = JsonDocument.Parse(trimmed[first..(last + 1)]);
return false;
}
catch (JsonException) { }
}
return first >= 0;
} |
||
|
|
||
| private static int EstimateTokens(string text) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(text)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of
LooksLikeTruncatedJsonis fragile because it assumes the response starts exactly with a{. LLMs occasionally include conversational prefixes or markdown code blocks (e.g., "Here is the JSON: ```json ...") even when instructed to return JSON.Additionally, this logic is duplicated in
OpenAiLlmProvider.cs. Consider moving this utility to a shared location likeLlmInstructionExtractionPromptand making it more robust by searching for the first and last braces, similar to the logic used inTryParseStructuredResponse.