From a458873874cc1b6454827a77f2d4f223848ea5b8 Mon Sep 17 00:00:00 2001 From: Stefan Ernst <24554551+stefan-ernst@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:23:02 +0100 Subject: [PATCH] fix(llm): normalize the content-filter finish reason (WI-1801) Fantasy emits the canonical finish reason "content-filter", but the agent loop classified only "content_filter". A filtered response fell through to done (or empty when it carried no text), bypassing the review warning and failed-run contract. Normalize provider finish reasons to this package's spelling at the adapter boundary, alongside the existing tool-calls mapping, so the loop and every StopReason consumer read one form. --- internal/llm/fantasy_adapter.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/llm/fantasy_adapter.go b/internal/llm/fantasy_adapter.go index 28dda0513..e04856f6b 100644 --- a/internal/llm/fantasy_adapter.go +++ b/internal/llm/fantasy_adapter.go @@ -686,9 +686,15 @@ func fantasyProviderCost(metadata fantasy.ProviderMetadata) *float64 { } func normalizedFantasyResponse(content string, toolCalls []ToolCall, providerState json.RawMessage, usage Usage, finish fantasy.FinishReason) *CompletionResponse { + // Normalize the library's finish reasons to this package's stable spelling. + // Fantasy owns the provider values; the agent loop and StopReason consumers + // read the normalized form. finishReason := string(finish) - if finish == fantasy.FinishReasonToolCalls { + switch finish { + case fantasy.FinishReasonToolCalls: finishReason = "tool_calls" + case fantasy.FinishReasonContentFilter: + finishReason = "content_filter" } return &CompletionResponse{ Object: "chat.completion",