Repository navigation
feat(core): add ModelRefusalError, raise in anthropic and openai - #41568
Draft
Hunter Lovell (hntrl) wants to merge 3 commits into
Draft
Hunter Lovell (hntrl) wants to merge 3 commits into
Hunter Lovell (hntrl) wants to merge 3 commits into
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When Claude refuses a request (
stop_reason="refusal"),ChatAnthropic.with_structured_output(...)fails with anOutputParserExceptionabout an empty message, and withinclude_raw=Truethat parser error is what lands inparsing_error. The only way to tell a refusal from a real parsing bug is to dig intoraw.response_metadata, which is provider-specific code in the caller's hot path. OpenAI already hasOpenAIRefusalErrorfor this, but it's a plainExceptionwith 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:
stop_reason="refusal", empty content, and (on recent models)stop_detailswith a policycategoryand anexplanation.refusalstring on the message, orfinish_reason="content_filter".refusalcontent block, orincomplete_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
ModelRefusalError(ModelError)sits next toContextOverflowErrorand the other standard model errors. It carriesprovider,stop_reason,category,explanationandraw_message, and is not retryable.with_structured_output(function_callingandjson_schema) checks each output message before the parser runs. A refusal raisesModelRefusalErrorbuilt fromstop_details. Withinclude_raw=Truethe error ends up inparsing_errorthrough the existing fallback.OpenAIRefusalErrornow subclassesModelRefusalErrorand keeps its constructor, sostr(err)is still the refusal text. The same pre-parse check runs for everywith_structured_outputmethod. It raises on refusal text (Chat Completions or Responses), onfinish_reason="content_filter", and on Responsesincomplete_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
langchain-corefloor 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.OpenAIRefusalErroris now aModelErrorwithis_retryable = False, soModelRetryMiddleware's default predicate stops retrying it. Before, as a plainException, it was retried.function_calling: a content-filtered OpenAI response used to come back silently asparsed=None, because the tool parser found no tool call. It now raises, or setsparsing_errorwithinclude_raw=True.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 lintand unit tests pass for core, anthropic and openai.Release note
with_structured_outputonChatAnthropicandChatOpenAInow raiseslangchain_core.exceptions.ModelRefusalErrorwhen the model refuses or the output is content-filtered. Before, Anthropic raised anOutputParserExceptionand OpenAI raised anOpenAIRefusalErrorunrelated to other providers.OpenAIRefusalErroris now a subclass, so existingexcept OpenAIRefusalErrorcode keeps working. Withinclude_raw=True, the error is reported inparsing_error.Written with an AI agent's help.