Skip to content

fix(coderd/x/chatd/chatloop): surface reasoning-only content-filter refusals as terminal errors#27476

Draft
ibetitsmike wants to merge 1 commit into
mainfrom
mike/chatd-content-filter-refusal
Draft

fix(coderd/x/chatd/chatloop): surface reasoning-only content-filter refusals as terminal errors#27476
ibetitsmike wants to merge 1 commit into
mainfrom
mike/chatd-content-filter-refusal

Conversation

@ibetitsmike

Copy link
Copy Markdown
Collaborator

Problem

Anthropic can end a stream with stop_reason: "refusal" after reasoning content has already streamed. The content-filter guard in chatloop.GenerateAssistant only fired when the step content was completely empty, so a reasoning-only refusal bypassed it: the turn finished as status=waiting with last_error=null, and the user saw the chat silently stop mid-turn with no explanation. This looked like a Coder fault when the provider had rejected the response. Observed twice in dogfood on 2026-07-23 (chat c72f99fc, debug steps show finish_reason=content-filter with reasoning-only content).

Change

Treat a content-filter finish as terminal whenever the step produced no user-visible output. A new hasUserVisibleContent helper counts any non-reasoning part (text, tool call, tool result) as user-visible; reasoning-only or empty steps now return the existing contentFilterError, which flows through the established pipeline: classified ChatErrorKindContentFilter (non-retryable, refusal category/detail when provided), persisted chats.last_error, streamed error event, and the "Response blocked" callout in the chat UI.

Behavior for steps with visible text or tool calls is unchanged, and the frontend needs no changes.

Testing

  • New regression subtest ReasoningOnlyContentSurfacesTerminalError (reasoning stream then content-filter finish) beside the existing empty-content and partial-content subtests, which are unchanged.
  • go test ./coderd/x/chatd/... and lint pass.
  • Dogfood UAT against a local dev instance with a mock Anthropic upstream passed all three scenarios: reasoning-only refusal shows the "Response blocked" callout with last_error.kind=content_filter and no retry affordance; text-then-refusal still completes normally; empty refusal still errors.

This PR was created by Mux acting on Mike's behalf.

@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: bba267645d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant