Skip to content

feat(core): add ModelRefusalError, raise in anthropic and openai - #41568

Draft
Hunter Lovell (hntrl) wants to merge 3 commits into
masterfrom
hunter/model-refusal-error
Draft

Hunter Lovell (hntrl) wants to merge 3 commits into
masterfrom
hunter/model-refusal-error

Conversation

@hntrl

Copy link
Copy Markdown
Member

When Claude refuses a request (stop_reason="refusal"), ChatAnthropic.with_structured_output(...) fails with an OutputParserException about an empty message, and with include_raw=True that parser error is what lands in parsing_error. The only way to tell a refusal from a real parsing bug is to dig into raw.response_metadata, which is provider-specific code in the caller's hot path. OpenAI already has OpenAIRefusalError for this, but it's a plain Exception with nothing in common with other providers.

This adds one provider-agnostic error that structured-output callers can catch.

How refusals reach us

Providers report refusals as successful responses, not HTTP errors:

  • Anthropic: a 200 with stop_reason="refusal", empty content, and (on recent models) stop_details with a policy category and an explanation.
  • OpenAI Chat Completions: a refusal string on the message, or finish_reason="content_filter".
  • OpenAI Responses API: a refusal content block, or incomplete_details.reason="content_filter".

A plain model call should keep returning these messages as they are. Structured output is different: the caller asked for content the model withheld, so the parser can only fail.

The change

  • core: ModelRefusalError(ModelError) sits next to ContextOverflowError and the other standard model errors. It carries provider, stop_reason, category, explanation and raw_message, and is not retryable.
  • anthropic: with_structured_output (function_calling and json_schema) checks each output message before the parser runs. A refusal raises ModelRefusalError built from stop_details. With include_raw=True the error ends up in parsing_error through the existing fallback.
  • openai: OpenAIRefusalError now subclasses ModelRefusalError and keeps its constructor, so str(err) is still the refusal text. The same pre-parse check runs for every with_structured_output method. It raises on refusal text (Chat Completions or Responses), on finish_reason="content_filter", and on Responses incomplete_details.reason="content_filter".

The check runs on each chunk rather than as a RunnableLambda, so streamed structured output still streams partial JSON. Both providers report the stop reason on the final chunk.

Worth a careful look

  • Core floor: both partners import the new symbol, so their langchain-core floor has to be raised to the core release that ships it before they're released. As in feat(core): add standard model exception types #39538, I didn't bump the floors here.
  • Retries: OpenAIRefusalError is now a ModelError with is_retryable = False, so ModelRetryMiddleware's default predicate stops retrying it. Before, as a plain Exception, it was retried.
  • Content filter with function_calling: a content-filtered OpenAI response used to come back silently as parsed=None, because the tool parser found no tool call. It now raises, or sets parsing_error with include_raw=True.
  • Plain invoke()/stream() are unchanged for both providers.

Unit tests use mocked responses for each path: sync, async and streaming, raise vs include_raw, plain invoke not raising, and normal parsing and partial-JSON streaming still working. make lint and unit tests pass for core, anthropic and openai.

Release note

with_structured_output on ChatAnthropic and ChatOpenAI now raises langchain_core.exceptions.ModelRefusalError when the model refuses or the output is content-filtered. Before, Anthropic raised an OutputParserException and OpenAI raised an OpenAIRefusalError unrelated to other providers. OpenAIRefusalError is now a subclass, so existing except OpenAIRefusalError code keeps working. With include_raw=True, the error is reported in parsing_error.

Written with an AI agent's help.

@github-actions github-actions Bot added anthropic `langchain-anthropic` package issues & PRs core `langchain-core` package issues & PRs feature For PRs that implement a new feature; NOT A FEATURE REQUEST integration PR made that is related to a provider partner package integration internal openai `langchain-openai` package issues & PRs size: L 500-999 LOC labels Oct 30, 2026
@codspeed

codspeed Bot commented Oct 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 15 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing hunter/model-refusal-error (827f5d6) with master (864754c)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

This branch has not been deployed

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

Labels

anthropic `langchain-anthropic` package issues & PRs core `langchain-core` package issues & PRs feature For PRs that implement a new feature; NOT A FEATURE REQUEST integration PR made that is related to a provider partner package integration internal openai `langchain-openai` package issues & PRs size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

⌂ Proxy