feat(providers/anthropic): surface refusal stop_reason as content-filter#41
Conversation
|
Verified the claim in the
So the raw-JSON parsing is the right call today. Suggested follow-ups, not blocking:
Coder Agents generated (on behalf of @jscottmiller). |
|
Merge-order note relative to #40 (the main -> coder_2_33 sync):
Since the refusal mapping is not Coder-specific, it is also a candidate to send upstream to charmbracelet/fantasy rather than carrying it as a fork patch long-term. Coder Agents generated (on behalf of @jscottmiller). |
|
/coder-agents-review |
Map Anthropic's "refusal" stop_reason to FinishReasonContentFilter and capture stop_details (category, explanation) into the finish part's ProviderMetadata via a new RefusalMetadata type, so callers can surface why a turn produced no content. Coder Agents generated.
654801e to
798e38d
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
When a provider's real-time safety classifiers block a response (e.g. Anthropic stop_reason "refusal"), the turn previously ended silently: the fantasy SDK mapped the stop reason to unknown and the empty-content finish looked like a normal stop, leaving the UI stuck on "Thinking" with no output. Bump the fantasy fork to pick up refusal -> FinishReasonContentFilter mapping with stop_details carried as RefusalMetadata (fork change 15, coder/fantasy#41). chatloop now turns an empty content-filter finish into a terminal, non-retryable content_filter classified error, using the provider's explanation as the message and the refusal category as the detail, with a generic per-provider fallback message. The UI renders it as "Response blocked". Partial-content refusals (classifier blocks after some output) still persist the partial text and end the turn normally; surfacing those is a separate product decision. Coder Agents generated.
When a provider's real-time safety classifiers block a response (e.g. Anthropic stop_reason "refusal"), the turn previously ended silently: the fantasy SDK mapped the stop reason to unknown and the empty-content finish looked like a normal stop, leaving the UI stuck on "Thinking" with no output. Bump the fantasy fork to pick up refusal -> FinishReasonContentFilter mapping with stop_details carried as RefusalMetadata (fork change 15, coder/fantasy#41). chatloop now turns an empty content-filter finish into a terminal, non-retryable content_filter classified error, using the provider's explanation as the message and the refusal category as the detail, with a generic per-provider fallback message. The UI renders it as "Response blocked". Partial-content refusals (classifier blocks after some output) still persist the partial text and end the turn normally; surfacing those is a separate product decision. Coder Agents generated.
gpt-5.6 models were unusable with agents: fantasy's Responses allowlist did not include the new family, so `IsResponsesModel` returned false and chatd fell back to the Chat Completions path (no reasoning params, no encrypted reasoning continuity). ## Changes - Bump the `charm.land/fantasy` replace pin to coder/fantasy `6da0c3b10237` (coder_2_33), pulling in: - coder/fantasy#46: route `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` through the OpenAI Responses API as reasoning models. - coder/fantasy#41: surface Anthropic refusal stop_reason as content-filter (already on coder_2_33, rides along with the bump). - Update the fork changelog comment in go.mod. ## Verification - Probed all three gpt-5.6 models through the ai-gateway: `/v1/responses` with `reasoning.effort`, `include: ["reasoning.encrypted_content"]`, and `store: false` completes for each. - `go build ./coderd/...` and `go test ./coderd/x/chatd/chatopenai/ ./coderd/x/chatd/chatprovider/` pass against the new pin. > This PR was authored by Mux on Mike's behalf.
Cherry-pick of #27132 (`f84801ee`) onto `release/2.35`. gpt-5.6 models were unusable with agents: fantasy's Responses allowlist did not include the new family, so `IsResponsesModel` returned false and chatd fell back to the Chat Completions path (no reasoning params, no encrypted reasoning continuity). ## Changes - Bump the `charm.land/fantasy` replace pin from `2e3ddbca75dd` to coder/fantasy `6da0c3b10237` (coder_2_33), pulling in: - coder/fantasy#46: route `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` through the OpenAI Responses API as reasoning models. - coder/fantasy#41: surface Anthropic refusal stop_reason as content-filter (rides along; sits below #46 on coder_2_33). - Update the fork changelog comment in go.mod. ## Backport safety - The delta over 2.35's pin is exactly the two commits above; #27132 itself touched only go.mod/go.sum, and no coder/coder code references the refusal metadata or content-filter finish reason (verified on main and 2.35). - Finish-part provider metadata is never persisted by chatd, so a rollback to an unpatched 2.35.x binary cannot hit undecodable stored history. - `go build ./coderd/...` and `go test ./coderd/x/chatd/{chatopenai,chatprovider,chatprompt,chatloop}/` pass on this branch; `go mod tidy` is a no-op. > This PR was authored by Mux on Mike's behalf.
When a provider's real-time safety classifiers block a response (e.g. Anthropic stop_reason "refusal"), the turn previously ended silently: the fantasy SDK mapped the stop reason to unknown and the empty-content finish looked like a normal stop, leaving the UI stuck on "Thinking" with no output. Bump the fantasy fork to pick up refusal -> FinishReasonContentFilter mapping with stop_details carried as RefusalMetadata (fork change 15, coder/fantasy#41). chatloop now turns an empty content-filter finish into a terminal, non-retryable content_filter classified error, using the provider's explanation as the message and the refusal category as the detail, with a generic per-provider fallback message. The UI renders it as "Response blocked". Partial-content refusals (classifier blocks after some output) still persist the partial text and end the turn normally; surfacing those is a separate product decision. Coder Agents generated.
When a provider's real-time safety classifiers block a response (e.g. Anthropic stop_reason "refusal"), the turn previously ended silently: the fantasy SDK mapped the stop reason to unknown and the empty-content finish looked like a normal stop, leaving the UI stuck on "Thinking" with no output. Bump the fantasy fork to pick up refusal -> FinishReasonContentFilter mapping with stop_details carried as RefusalMetadata (fork change 15, coder/fantasy#41). chatloop now turns an empty content-filter finish into a terminal, non-retryable content_filter classified error, using the provider's explanation as the message and the refusal category as the detail, with a generic per-provider fallback message. The UI renders it as "Response blocked". Partial-content refusals (classifier blocks after some output) still persist the partial text and end the turn normally; surfacing those is a separate product decision. Coder Agents generated.
Summary
Map Anthropic's
refusalstop reason to a content-filter finish reason andpreserve the block details, so downstream callers can tell a safety block apart
from a normal stop and surface why a turn produced no content.
Anthropic's real-time safety classifiers end a response with
stop_reason: "refusal"and astop_detailsobject (category,explanation), and emit no content. PreviouslymapFinishReasonfell throughto
FinishReasonUnknownand the details were dropped.Changes
mapFinishReason:"refusal"->FinishReasonContentFilter.stop_detailsfrom themessage_deltaraw JSON (the Anthropic SDK doesnot model it) and attach it to the finish stream part via a new
RefusalMetadata{Category, Explanation}provider-metadata type(registered, marshal/unmarshal,
GetRefusalMetadatagetter).Testing
go test ./providers/anthropic/(newrefusal_test.gocovers the finish-reasonmapping,
stop_detailsparsing, and metadata round-trip).Notes
Consumed by coder/coder PR for chat "Response blocked" messaging.
Coder Agents generated.