A model call returns without throwing, but generation hit its token limit before completing the reply. Saving whatever text came back would mark an unfinished reply as complete.

Check ChatResponse.FinishReason before accepting the output. In Microsoft.Extensions.AI, it tells you why generation stopped, when the underlying client supplies that information.

Decide what happens next

For a feature that requires a complete reply, map the finish reason to an application decision:

using Microsoft.Extensions.AI;

public enum GenerationDecision
{
    ValidateOutput,
    Incomplete,
    Filtered,
    ReviewToolCalls,
    Unknown
}

public static class GenerationPolicy
{
    public static GenerationDecision Decide(ChatResponse response)
    {
        var reason = response.FinishReason;

        if (reason == ChatFinishReason.Stop)
            return GenerationDecision.ValidateOutput;

        if (reason == ChatFinishReason.Length)
            return GenerationDecision.Incomplete;

        if (reason == ChatFinishReason.ContentFilter)
            return GenerationDecision.Filtered;

        if (reason == ChatFinishReason.ToolCalls)
            return GenerationDecision.ReviewToolCalls;

        return GenerationDecision.Unknown;
    }
}

Call GenerationPolicy.Decide(response) after GetResponseAsync. Only ValidateOutput enters reply validation. Other decisions need handling before the feature can report success.

Stop means a natural stopping point or configured stop sequence. Check for refusals, empty content, and invalid values. A finish reason cannot prove that the answer satisfies your application’s contract.

A Length result stays incomplete even if its last sentence looks plausible. A new attempt needs a reason to succeed: request a shorter response or increase the allowance within the remaining runtime budget. Repeating the same request and limit can fail again. This branch does not automatically retry.

Route ContentFilter to the feature’s policy for filtered output. Do not try to force the response through by retrying until the filter allows it. Providers can also report refusals or filtering through response content or errors, so this branch is not a complete refusal check.

ToolCalls means the model requested tool use. Inspect the calls and apply policy to execute, seek approval, reject, or delegate them. Automatic function invocation may handle intermediate requests before the outer call returns. Keep its loop-budget outcome separate from the final finish reason.

FinishReason is nullable, and ChatFinishReason supports custom values. Missing or unfamiliar reasons map to Unknown, withholding the completed reply. If your provider omits the reason, define a provider-specific completion check. Do not silently treat null as Stop.

Keep streamed text provisional

With streaming, preserve ChatResponseUpdate.FinishReason alongside the text. If the stream fails or ends without the completion evidence your integration requires, keep the answer incomplete. A UI may show partial text, but it must make that status clear.

Apply this check before caching a reply as complete or passing it into a write operation. Any required writes must also succeed before the workflow can report success.