Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langchain-openai for 
openSUSE:Factory checked in at 2026-11-07 23:33:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-openai (Old)
 and      /work/SRC/openSUSE:Factory/.python-langchain-openai.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langchain-openai"

Sat Nov  7 23:33:12 2026 rev:19 rq:1396002 version:1.7.4

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langchain-openai/python-langchain-openai.changes
  2026-10-27 16:59:06.646981712 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-langchain-openai.new.3759290/python-langchain-openai.changes
     2026-11-07 23:33:45.426383408 +0100
@@ -1,0 +2,9 @@
+Sat Nov  7 12:43:38 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.7.4:
+  * Standardize model refusal content blocks
+  * Refresh OpenAI model profile data
+- Raise the langchain-core requirement to 1.7.5, as upstream does
+- Spec cleanup: use the bare files-macro form
+
+-------------------------------------------------------------------

Old:
----
  langchain_openai-1.7.3.tar.gz

New:
----
  langchain_openai-1.7.4.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langchain-openai.spec ++++++
--- /var/tmp/diff_new_pack.IirUKP/_old  2026-11-07 23:33:46.572430761 +0100
+++ /var/tmp/diff_new_pack.IirUKP/_new  2026-11-07 23:33:46.574430844 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python-langchain-openai
-Version:        1.7.3
+Version:        1.7.4
 Release:        0
 Summary:        An integration package connecting OpenAI and LangChain
 License:        MIT
@@ -28,7 +28,7 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-certifi >= 2024.6.2
-Requires:       python-langchain-core >= 1.7.0
+Requires:       python-langchain-core >= 1.7.5
 Requires:       python-openai >= 2.45.0
 Requires:       python-tiktoken >= 0.7.0
 BuildArch:      noarch
@@ -36,7 +36,7 @@
 BuildRequires:  %{python_module certifi >= 2024.6.2}
 BuildRequires:  %{python_module httpx2}
 BuildRequires:  %{python_module httpx}
-BuildRequires:  %{python_module langchain-core >= 1.7.0}
+BuildRequires:  %{python_module langchain-core >= 1.7.5}
 BuildRequires:  %{python_module openai >= 2.45.0}
 BuildRequires:  %{python_module pytest-asyncio}
 BuildRequires:  %{python_module tiktoken >= 0.7.0}
@@ -82,7 +82,7 @@
 # which fails in the offline build (including the o-series variant).
 %pytest tests/unit_tests -o addopts='' --ignore 
tests/unit_tests/embeddings/test_azure_standard.py --ignore 
tests/unit_tests/embeddings/test_base_standard.py --ignore 
tests/unit_tests/chat_models/test_azure_standard.py --ignore 
tests/unit_tests/chat_models/test_base_standard.py --ignore 
tests/unit_tests/chat_models/test_responses_standard.py --ignore 
tests/unit_tests/chat_models/test_responses_stream.py --ignore 
tests/unit_tests/middleware/test_openai_moderation_middleware.py --ignore 
tests/unit_tests/middleware/test_auto_mode.py --ignore 
tests/unit_tests/middleware/test_model_router.py --deselect 
tests/unit_tests/chat_models/test_base.py::test_openai_stream_events_v3_lifecycle
 --deselect tests/unit_tests/chat_models/test_base.py::test__get_encoding_model 
--deselect 
tests/unit_tests/chat_models/test_base.py::test_get_num_tokens_from_messages 
--deselect 
tests/unit_tests/chat_models/test_base.py::test_get_num_tokens_from_messages_o_series
 --deselect tests/unit_tests/embeddings/test_ba
 se.py::test_embed_documents_with_custom_chunk_size --deselect 
tests/unit_tests/embeddings/test_base.py::test_embeddings_respects_token_limit 
--deselect 
"tests/unit_tests/llms/test_base.py::test_get_token_ids[gpt-3.5-turbo-instruct]"
 --deselect 
"tests/unit_tests/test_token_counts.py::test_chat_openai_get_num_tokens[gpt-5.5]"
 --deselect 
"tests/unit_tests/test_token_counts.py::test_chat_openai_get_num_tokens[gpt-5-nano]"
 --deselect 
"tests/unit_tests/test_token_counts.py::test_chat_openai_get_num_tokens[o3]"
 
-%files %{python_files}
+%files %python_files
 %doc README.md
 %license LICENSE
 %{python_sitelib}/langchain_openai

++++++ langchain_openai-1.7.3.tar.gz -> langchain_openai-1.7.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.7.3/PKG-INFO 
new/langchain_openai-1.7.4/PKG-INFO
--- old/langchain_openai-1.7.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langchain-openai
-Version: 1.7.3
+Version: 1.7.4
 Summary: An integration package connecting OpenAI and LangChain
 Project-URL: Homepage, 
https://docs.langchain.com/oss/python/integrations/providers/openai
 Project-URL: Documentation, 
https://reference.langchain.com/python/integrations/langchain_openai/
@@ -24,7 +24,7 @@
 Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
 Requires-Python: <4.0.0,>=3.10.0
 Requires-Dist: certifi>=2024.6.2
-Requires-Dist: langchain-core<2.0.0,>=1.7.0
+Requires-Dist: langchain-core<2.0.0,>=1.7.5
 Requires-Dist: openai>=2.45.0
 Requires-Dist: tiktoken>=0.7.0
 Provides-Extra: azure-identity
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.7.3/langchain_openai/_version.py 
new/langchain_openai-1.7.4/langchain_openai/_version.py
--- old/langchain_openai-1.7.3/langchain_openai/_version.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/langchain_openai/_version.py     2020-02-02 
01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
 """Version information for `langchain-openai`."""
 
-__version__ = "1.7.3"
+__version__ = "1.7.4"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/langchain_openai/chat_models/_compat.py 
new/langchain_openai-1.7.4/langchain_openai/chat_models/_compat.py
--- old/langchain_openai-1.7.3/langchain_openai/chat_models/_compat.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/langchain_openai/chat_models/_compat.py  
2020-02-02 01:00:00.000000000 +0100
@@ -176,6 +176,12 @@
                 if block_type == "text":
                     # Strip annotations
                     new_content.append({"type": "text", "text": block["text"]})
+                elif block_type == "refusal":
+                    # Refusals without text, e.g. from Anthropic, have nothing 
to send.
+                    if block.get("refusal"):
+                        new_content.append(
+                            {"type": "refusal", "refusal": block["refusal"]}
+                        )
                 elif block_type in ("reasoning", "tool_call", 
"invalid_tool_call"):
                     pass
                 else:
@@ -442,6 +448,18 @@
                 _convert_annotation_from_v1(a) for a in block["annotations"]
             ]
             new_content.append(new_block)
+        elif block["type"] == "refusal":
+            # Refusals without text, e.g. from Anthropic, have nothing to send.
+            if not block.get("refusal"):
+                continue
+            new_block = {"type": "refusal", "refusal": block["refusal"]}
+            if "id" in block:
+                new_block["id"] = block["id"]
+            if "phase" in block:
+                new_block["phase"] = block.get("phase")
+            elif "phase" in block.get("extras", {}):
+                new_block["phase"] = block["extras"]["phase"]
+            new_content.append(new_block)
         elif block["type"] == "tool_call":
             new_block = {"type": "function_call", "call_id": block["id"]}
             if "extras" in block and "item_id" in block["extras"]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/langchain_openai/chat_models/azure.py 
new/langchain_openai-1.7.4/langchain_openai/chat_models/azure.py
--- old/langchain_openai-1.7.3/langchain_openai/chat_models/azure.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/langchain_openai/chat_models/azure.py    
2020-02-02 01:00:00.000000000 +0100
@@ -954,6 +954,9 @@
 
                 The final output is always a `dict` with keys `'raw'`, 
`'parsed'`, and
                 `'parsing_error'`.
+
+                If the model refuses, `ModelRefusalError` is raised, or 
returned as
+                `'parsing_error'` when `include_raw` is `True`.
             strict:
 
                 - True:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/langchain_openai/chat_models/base.py 
new/langchain_openai-1.7.4/langchain_openai/chat_models/base.py
--- old/langchain_openai-1.7.3/langchain_openai/chat_models/base.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/langchain_openai/chat_models/base.py     
2020-02-02 01:00:00.000000000 +0100
@@ -60,6 +60,7 @@
     ModelNotFoundError,
     ModelPermissionDeniedError,
     ModelRateLimitError,
+    ModelRefusalError,
     ModelTimeoutError,
 )
 from langchain_core.language_models import (
@@ -110,6 +111,7 @@
 from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, 
ChatResult
 from langchain_core.runnables import (
     Runnable,
+    RunnableGenerator,
     RunnableLambda,
     RunnableMap,
     RunnablePassthrough,
@@ -248,6 +250,8 @@
                     )
         if audio := _dict.get("audio"):
             additional_kwargs["audio"] = audio
+        if refusal := _dict.get("refusal"):
+            additional_kwargs["refusal"] = refusal
         return AIMessage(
             content=content,
             additional_kwargs=additional_kwargs,
@@ -2669,6 +2673,9 @@
 
                 The final output is always a `dict` with keys `'raw'`, 
`'parsed'`, and
                 `'parsing_error'`.
+
+                If the model refuses, `ModelRefusalError` is raised, or 
returned as
+                `'parsing_error'` when `include_raw` is `True`.
             strict:
 
                 - `True`:
@@ -2866,16 +2873,21 @@
             )
             raise ValueError(msg)
 
+        # Checked per chunk so streamed structured output still streams.
+        checked_parser = (
+            RunnableGenerator(_check_refusal, _acheck_refusal) | output_parser
+        )
+
         if include_raw:
             parser_assign = RunnablePassthrough.assign(
-                parsed=itemgetter("raw") | output_parser, parsing_error=lambda 
_: None
+                parsed=itemgetter("raw") | checked_parser, 
parsing_error=lambda _: None
             )
             parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
             parser_with_fallback = parser_assign.with_fallbacks(
                 [parser_none], exception_key="parsing_error"
             )
             return RunnableMap(raw=llm) | parser_with_fallback
-        return llm | output_parser
+        return llm | checked_parser
 
     def _filter_disabled_params(self, **kwargs: Any) -> dict[str, Any]:
         if not self.disabled_params:
@@ -2900,8 +2912,7 @@
         if isinstance(chat_message, AIMessage):
             usage_metadata = chat_message.usage_metadata
             # Skip tool_calls, already sent as chunks
-            if "tool_calls" in chat_message.additional_kwargs:
-                chat_message.additional_kwargs.pop("tool_calls")
+            chat_message.additional_kwargs.pop("tool_calls", None)
         else:
             usage_metadata = None
         message = AIMessageChunk(
@@ -3925,6 +3936,9 @@
 
                 The final output is always a `dict` with keys `'raw'`, 
`'parsed'`, and
                 `'parsing_error'`.
+
+                If the model refuses, `ModelRefusalError` is raised, or 
returned as
+                `'parsing_error'` when `include_raw` is `True`.
             strict:
 
                 - `True`:
@@ -4453,22 +4467,7 @@
         if isinstance(parsed, dict):
             return schema(**parsed)
         return parsed
-    if ai_msg.additional_kwargs.get("refusal"):
-        raise OpenAIRefusalError(ai_msg.additional_kwargs["refusal"])
-    if any(
-        isinstance(block, dict)
-        and block.get("type") == "non_standard"
-        and "refusal" in block["value"]  # type: ignore[typeddict-item]
-        for block in ai_msg.content_blocks
-    ):
-        refusal = next(
-            block["value"]["refusal"]
-            for block in ai_msg.content_blocks
-            if isinstance(block, dict)
-            and block["type"] == "non_standard"
-            and "refusal" in block["value"]
-        )
-        raise OpenAIRefusalError(refusal)
+    _raise_on_refusal(ai_msg)
     if ai_msg.tool_calls:
         return None
     msg = (
@@ -4478,11 +4477,50 @@
     raise ValueError(msg)
 
 
-class OpenAIRefusalError(Exception):
-    """Error raised when OpenAI Structured Outputs API returns a refusal.
+def _raise_on_refusal(message: BaseMessage) -> None:
+    """Raise `OpenAIRefusalError` if the model refused or output was 
filtered."""
+    response_metadata = message.response_metadata
+    finish_reason = response_metadata.get("finish_reason")
+    for block in message.content_blocks:
+        if block["type"] == "refusal":
+            if text := block.get("refusal"):
+                raise OpenAIRefusalError(text, raw_message=message)
+            raise OpenAIRefusalError(raw_message=message)
+    incomplete_details = response_metadata.get("incomplete_details")
+    incomplete_reason = (
+        incomplete_details.get("reason")
+        if isinstance(incomplete_details, dict)
+        else None
+    )
+    if "content_filter" in (finish_reason, incomplete_reason):
+        msg = "Model output was stopped by a content filter"
+        raise OpenAIRefusalError(msg, raw_message=message)
+
+
+def _check_refusal(messages: Iterator[BaseMessage]) -> Iterator[BaseMessage]:
+    for message in messages:
+        _raise_on_refusal(message)
+        yield message
+
+
+async def _acheck_refusal(
+    messages: AsyncIterator[BaseMessage],
+) -> AsyncIterator[BaseMessage]:
+    async for message in messages:
+        _raise_on_refusal(message)
+        yield message
+
+
+class OpenAIRefusalError(ModelRefusalError):
+    """Error raised when an OpenAI model refuses a structured output request.
 
     When using OpenAI's Structured Outputs API with user-generated input, the 
model
-    may occasionally refuse to fulfill the request for safety reasons.
+    may occasionally refuse to fulfill the request for safety reasons. The same
+    error is raised when the response stops because of a content filter
+    (`finish_reason="content_filter"`).
+
+    Subclasses `ModelRefusalError`, so provider-agnostic code can catch that
+    instead.
 
     See [more on 
refusals](https://platform.openai.com/docs/guides/structured-outputs/refusals).
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/langchain_openai/data/_profiles.py 
new/langchain_openai-1.7.4/langchain_openai/data/_profiles.py
--- old/langchain_openai-1.7.3/langchain_openai/data/_profiles.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/langchain_openai/data/_profiles.py       
2020-02-02 01:00:00.000000000 +0100
@@ -18,6 +18,7 @@
 _PROFILES: dict[str, dict[str, Any]] = {
     "chatgpt-image-latest": {
         "name": "chatgpt-image-latest",
+        "status": "deprecated",
         "release_date": "2025-12-16",
         "last_updated": "2025-12-16",
         "open_weights": False,
@@ -1415,35 +1416,9 @@
         "tool_choice": True,
         "tool_call_streaming": True,
     },
-    "gpt-image-1": {
-        "name": "gpt-image-1",
-        "status": "deprecated",
-        "release_date": "2025-04-24",
-        "last_updated": "2025-04-24",
-        "open_weights": False,
-        "max_input_tokens": 0,
-        "max_output_tokens": 0,
-        "text_inputs": True,
-        "image_inputs": True,
-        "audio_inputs": False,
-        "video_inputs": False,
-        "text_outputs": False,
-        "image_outputs": True,
-        "audio_outputs": False,
-        "video_outputs": False,
-        "reasoning_output": False,
-        "tool_calling": False,
-        "attachment": True,
-        "temperature": False,
-        "image_url_inputs": True,
-        "pdf_inputs": True,
-        "pdf_tool_message": True,
-        "image_tool_message": True,
-        "tool_choice": True,
-        "tool_call_streaming": True,
-    },
     "gpt-image-1-mini": {
         "name": "gpt-image-1-mini",
+        "status": "deprecated",
         "release_date": "2025-09-26",
         "last_updated": "2025-09-26",
         "open_weights": False,
@@ -1470,6 +1445,7 @@
     },
     "gpt-image-1.5": {
         "name": "gpt-image-1.5",
+        "status": "deprecated",
         "release_date": "2025-11-25",
         "last_updated": "2025-11-25",
         "open_weights": False,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.7.3/pyproject.toml 
new/langchain_openai-1.7.4/pyproject.toml
--- old/langchain_openai-1.7.3/pyproject.toml   2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_openai-1.7.4/pyproject.toml   2020-02-02 01:00:00.000000000 
+0100
@@ -20,10 +20,10 @@
     "Topic :: Scientific/Engineering :: Artificial Intelligence",
 ]
 
-version = "1.7.3"
+version = "1.7.4"
 requires-python = ">=3.10.0,<4.0.0"
 dependencies = [
-    "langchain-core>=1.7.0,<2.0.0",
+    "langchain-core>=1.7.5,<2.0.0",
     "certifi>=2024.6.2",
     "openai>=2.45.0",
     "tiktoken>=0.7.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/tests/unit_tests/chat_models/test_base.py 
new/langchain_openai-1.7.4/tests/unit_tests/chat_models/test_base.py
--- old/langchain_openai-1.7.3/tests/unit_tests/chat_models/test_base.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/tests/unit_tests/chat_models/test_base.py        
2020-02-02 01:00:00.000000000 +0100
@@ -24,6 +24,7 @@
     ModelNotFoundError,
     ModelPermissionDeniedError,
     ModelRateLimitError,
+    ModelRefusalError,
     ModelTimeoutError,
 )
 from langchain_core.load import dumps, loads
@@ -44,7 +45,7 @@
 from langchain_core.messages.block_translators.openai import (
     _convert_from_v03_ai_message,
 )
-from langchain_core.outputs import ChatGeneration, ChatResult
+from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, 
ChatResult
 from langchain_core.runnables import RunnableLambda
 from langchain_core.runnables.base import RunnableBinding, RunnableSequence
 from langchain_core.tracers.base import BaseTracer
@@ -5817,3 +5818,246 @@
         "type": "configuration_update",
         "reasoning": {"effort": "low"},
     }
+
+
+class _RefusalSchema(BaseModel):
+    answer: str
+
+
+def _chat_completions_result(
+    *, content: str | None, finish_reason: str, refusal: str | None = None
+) -> ChatResult:
+    from openai.types.chat import ChatCompletion
+
+    completion = ChatCompletion.model_validate(
+        {
+            "id": "chatcmpl-123",
+            "object": "chat.completion",
+            "created": 1234567890,
+            "model": OPENAI_TEST_MODEL,
+            "choices": [
+                {
+                    "index": 0,
+                    "finish_reason": finish_reason,
+                    "message": {
+                        "role": "assistant",
+                        "content": content,
+                        "refusal": refusal,
+                    },
+                }
+            ],
+            "usage": {"prompt_tokens": 5, "completion_tokens": 0, 
"total_tokens": 5},
+        }
+    )
+    return ChatOpenAI(model=OPENAI_TEST_MODEL)._create_chat_result(completion)
+
+
+def _responses_result(
+    *, output: list, incomplete_reason: str | None = None
+) -> ChatResult:
+    response = Response(
+        id="resp_123",
+        created_at=1234567890,
+        model=OPENAI_TEST_MODEL,
+        object="response",
+        parallel_tool_calls=True,
+        tools=[],
+        tool_choice="auto",
+        output=output,
+        status="incomplete" if incomplete_reason else "completed",
+        incomplete_details=(
+            IncompleteDetails(reason=incomplete_reason)  # type: 
ignore[arg-type]
+            if incomplete_reason
+            else None
+        ),
+    )
+    return _construct_lc_result_from_responses_api(response)
+
+
+def _responses_refusal_output() -> list:
+    return [
+        ResponseOutputMessage(
+            type="message",
+            id="msg_123",
+            status="completed",
+            role="assistant",
+            content=[
+                ResponseOutputRefusal(
+                    type="refusal", refusal="I can't help with that request."
+                )
+            ],
+        )
+    ]
+
+
+def test_openai_refusal_error_is_model_refusal_error() -> None:
+    err = OpenAIRefusalError("I can't help with that.")
+
+    assert isinstance(err, ModelRefusalError)
+    assert str(err) == "I can't help with that."
+    assert err.is_retryable is False
+
+
[email protected](
+    ("method", "schema"),
+    [
+        ("json_schema", _RefusalSchema),
+        ("json_schema", _RefusalSchema.model_json_schema()),
+        ("function_calling", _RefusalSchema),
+    ],
+)
+def test_with_structured_output_raises_on_chat_completions_refusal(
+    method: Literal["json_schema", "function_calling"], schema: Any
+) -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(schema, method=method)
+    result = _chat_completions_result(
+        content=None, finish_reason="stop", refusal="I can't help with that 
request."
+    )
+
+    with (
+        patch.object(ChatOpenAI, "_generate", return_value=result),
+        pytest.raises(OpenAIRefusalError) as exc_info,
+    ):
+        structured.invoke("Write a keylogger")
+
+    err = exc_info.value
+    assert isinstance(err, ModelRefusalError)
+    assert str(err) == "I can't help with that request."
+    assert isinstance(err.raw_message, AIMessage)
+
+
[email protected]("method", ["json_schema", "function_calling", 
"json_mode"])
+def test_with_structured_output_raises_on_content_filter(
+    method: Literal["json_schema", "function_calling", "json_mode"],
+) -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(
+        _RefusalSchema.model_json_schema(), method=method
+    )
+    result = _chat_completions_result(content=None, 
finish_reason="content_filter")
+
+    with (
+        patch.object(ChatOpenAI, "_generate", return_value=result),
+        pytest.raises(ModelRefusalError) as exc_info,
+    ):
+        structured.invoke("Something filtered")
+
+    assert str(exc_info.value) == "Model output was stopped by a content 
filter"
+
+
+def test_with_structured_output_include_raw_reports_responses_refusal() -> 
None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True)
+    structured = llm.with_structured_output(
+        _RefusalSchema.model_json_schema(), method="json_schema", 
include_raw=True
+    )
+    result = _responses_result(output=_responses_refusal_output())
+
+    with patch.object(ChatOpenAI, "_generate", return_value=result):
+        response = structured.invoke("Write a keylogger")
+
+    assert isinstance(response, dict)
+    assert response["parsed"] is None
+    assert isinstance(response["parsing_error"], OpenAIRefusalError)
+    assert str(response["parsing_error"]) == "I can't help with that request."
+
+
+def test_with_structured_output_raises_on_responses_content_filter() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True)
+    structured = llm.with_structured_output(_RefusalSchema, 
method="json_schema")
+    result = _responses_result(output=[], incomplete_reason="content_filter")
+
+    with (
+        patch.object(ChatOpenAI, "_generate", return_value=result),
+        pytest.raises(ModelRefusalError) as exc_info,
+    ):
+        structured.invoke("Something filtered")
+
+    assert str(exc_info.value) == "Model output was stopped by a content 
filter"
+
+
+async def test_with_structured_output_raises_on_refusal_async() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(_RefusalSchema, 
method="json_schema")
+    result = _chat_completions_result(
+        content=None, finish_reason="stop", refusal="I can't help with that 
request."
+    )
+
+    async def _agenerate(*_: Any, **__: Any) -> ChatResult:
+        return result
+
+    with (
+        patch.object(ChatOpenAI, "_agenerate", _agenerate),
+        pytest.raises(OpenAIRefusalError),
+    ):
+        await structured.ainvoke("Write a keylogger")
+
+
+def test_invoke_does_not_raise_on_refusal_or_content_filter() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    for result in (
+        _chat_completions_result(
+            content=None, finish_reason="stop", refusal="I can't help."
+        ),
+        _chat_completions_result(content=None, finish_reason="content_filter"),
+    ):
+        with patch.object(ChatOpenAI, "_generate", return_value=result):
+            assert isinstance(llm.invoke("hi"), AIMessage)
+
+
+def test_with_structured_output_parses_when_not_refused() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(
+        _RefusalSchema.model_json_schema(), method="json_schema"
+    )
+    result = _chat_completions_result(content='{"answer": "42"}', 
finish_reason="stop")
+
+    with patch.object(ChatOpenAI, "_generate", return_value=result):
+        assert structured.invoke("What is 6 x 7?") == {"answer": "42"}
+
+
+def test_with_structured_output_stream_raises_on_content_filter() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(
+        _RefusalSchema.model_json_schema(), method="json_schema"
+    )
+    chunks = [
+        ChatGenerationChunk(message=AIMessageChunk(content='{"ans')),
+        ChatGenerationChunk(
+            message=AIMessageChunk(
+                content="", response_metadata={"finish_reason": 
"content_filter"}
+            )
+        ),
+    ]
+
+    with (
+        patch.object(ChatOpenAI, "_stream", return_value=iter(chunks)),
+        pytest.raises(ModelRefusalError) as exc_info,
+    ):
+        list(structured.stream("Something filtered"))
+
+    assert str(exc_info.value) == "Model output was stopped by a content 
filter"
+
+
+def test_with_structured_output_still_streams_partial_json() -> None:
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    structured = llm.with_structured_output(
+        _RefusalSchema.model_json_schema(), method="json_schema"
+    )
+    chunks = [
+        ChatGenerationChunk(message=AIMessageChunk(content=text))
+        for text in ['{"ans', 'wer": "4', '2"}']
+    ]
+    chunks.append(
+        ChatGenerationChunk(
+            message=AIMessageChunk(
+                content="", response_metadata={"finish_reason": "stop"}
+            )
+        )
+    )
+
+    with patch.object(ChatOpenAI, "_stream", return_value=iter(chunks)):
+        outputs = list(structured.stream("What is 6 x 7?"))
+
+    assert len(outputs) > 1
+    assert outputs[-1] == {"answer": "42"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.7.3/tests/unit_tests/chat_models/test_refusal.py 
new/langchain_openai-1.7.4/tests/unit_tests/chat_models/test_refusal.py
--- old/langchain_openai-1.7.3/tests/unit_tests/chat_models/test_refusal.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/tests/unit_tests/chat_models/test_refusal.py     
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,197 @@
+"""Replay standardized refusals through OpenAI APIs."""
+
+from collections.abc import AsyncIterator, Iterator
+from typing import Any
+from unittest.mock import patch
+
+import pytest
+from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
+from langchain_core.outputs import ChatGenerationChunk
+from openai.types.chat import ChatCompletion
+from pydantic import BaseModel
+
+from langchain_openai import ChatOpenAI
+from langchain_openai.chat_models._compat import 
_convert_from_v1_to_chat_completions
+from langchain_openai.chat_models.base import (
+    OpenAIRefusalError,
+    _construct_responses_api_input,
+    _oai_structured_outputs_parser,
+)
+
+
[email protected]("native", [False, True])
+def test_refusal_responses_replay(*, native: bool) -> None:
+    message = AIMessage(
+        content=[
+            {
+                "type": "refusal",
+                "refusal": "blocked",
+                "id": "msg_1",
+                **(
+                    {"phase": "final_answer"}
+                    if native
+                    else {"extras": {"phase": "final_answer"}}
+                ),
+            }
+        ]
+    )
+    result = _construct_responses_api_input([message])
+    assert result == [
+        {
+            "type": "message",
+            "role": "assistant",
+            "id": "msg_1",
+            "phase": "final_answer",
+            "content": [{"type": "refusal", "refusal": "blocked"}],
+        }
+    ]
+
+
+def test_refusal_chat_completions_replay() -> None:
+    message = AIMessage(
+        content_blocks=[
+            {
+                "type": "refusal",
+                "refusal": "blocked",
+                "extras": {"phase": "final_answer"},
+            }
+        ]
+    )
+    assert _convert_from_v1_to_chat_completions(message).content == [
+        {"type": "refusal", "refusal": "blocked"}
+    ]
+
+
+ANTHROPIC_REFUSAL_HISTORY = AIMessage(
+    content=[
+        {"type": "text", "text": "Here is"},
+        {"type": "refusal", "refusal": "", "index": "lc_refusal"},
+    ],
+    response_metadata={"model_provider": "anthropic", "output_version": "v1"},
+)
+
+
+class _Answer(BaseModel):
+    answer: str
+
+
[email protected]("use_responses_api", [False, True])
+def test_empty_refusal_from_other_provider_not_sent(*, use_responses_api: 
bool) -> None:
+    llm = ChatOpenAI(model="gpt-5", use_responses_api=use_responses_api)
+    payload = llm._get_request_payload(
+        [HumanMessage("hi"), ANTHROPIC_REFUSAL_HISTORY, HumanMessage("next")]
+    )
+    sent = payload["input"] if use_responses_api else payload["messages"]
+    assert "refusal" not in str(sent)
+
+
+def test_empty_refusal_raises_openai_refusal_error() -> None:
+    message = AIMessage(
+        content=[{"type": "refusal", "refusal": "", "id": "msg_1"}],
+        response_metadata={"model_provider": "openai"},
+    )
+    with pytest.raises(OpenAIRefusalError) as exc_info:
+        _oai_structured_outputs_parser(message, _Answer)
+    assert str(exc_info.value) == "Model refused to produce the requested 
output"
+
+
+def _refusal_chunks() -> list[ChatGenerationChunk]:
+    return [
+        ChatGenerationChunk(
+            message=AIMessageChunk(
+                content=[{"type": "refusal", "refusal": "No.", "index": 0}],
+                response_metadata={"model_provider": "openai"},
+            )
+        )
+    ]
+
+
+def test_streamed_responses_refusal_raises() -> None:
+    llm = ChatOpenAI(model="gpt-5", use_responses_api=True)
+    structured = llm.with_structured_output(_Answer, method="json_schema")
+    with (
+        patch.object(ChatOpenAI, "_stream", 
return_value=iter(_refusal_chunks())),
+        pytest.raises(OpenAIRefusalError) as exc_info,
+    ):
+        list(structured.stream("hi"))
+    assert str(exc_info.value) == "No."
+
+
+async def test_streamed_responses_refusal_raises_async() -> None:
+    llm = ChatOpenAI(model="gpt-5", use_responses_api=True)
+    structured = llm.with_structured_output(_Answer, method="json_schema")
+
+    async def _astream(*_: Any, **__: Any) -> 
AsyncIterator[ChatGenerationChunk]:
+        for chunk in _refusal_chunks():
+            yield chunk
+
+    with (
+        patch.object(ChatOpenAI, "_astream", _astream),
+        pytest.raises(OpenAIRefusalError),
+    ):
+        async for _ in structured.astream("hi"):
+            pass
+
+
+def _refusal_delta(refusal: str, *, first: bool = False) -> dict[str, Any]:
+    delta: dict[str, Any] = {"refusal": refusal}
+    if first:
+        delta.update(role="assistant", content="")
+    return {
+        "id": "chatcmpl-1",
+        "object": "chat.completion.chunk",
+        "created": 0,
+        "model": "gpt-5",
+        "choices": [{"index": 0, "delta": delta, "finish_reason": None}],
+    }
+
+
+STREAMED_REFUSAL = [
+    _refusal_delta("I'm sorry, ", first=True),
+    _refusal_delta("I cannot comply."),
+]
+
+
+class _BetaStream:
+    """`beta.chat.completions.stream`: chunk events, then the final 
completion."""
+
+    def __enter__(self) -> "_BetaStream":
+        return self
+
+    def __exit__(self, *args: object) -> None:
+        return None
+
+    def __iter__(self) -> Iterator[dict[str, Any]]:
+        return iter({"type": "chunk", "chunk": chunk} for chunk in 
STREAMED_REFUSAL)
+
+    def get_final_completion(self) -> ChatCompletion:
+        return ChatCompletion.model_validate(
+            {
+                "id": "chatcmpl-1",
+                "object": "chat.completion",
+                "created": 0,
+                "model": "gpt-5",
+                "choices": [
+                    {
+                        "index": 0,
+                        "finish_reason": "stop",
+                        "message": {
+                            "role": "assistant",
+                            "content": None,
+                            "refusal": "I'm sorry, I cannot comply.",
+                        },
+                    }
+                ],
+            }
+        )
+
+
[email protected]("output_version", ["v0", "v1"])
+def test_streamed_chat_completions_refusal_raises(output_version: str) -> None:
+    llm = ChatOpenAI(model="gpt-5", output_version=output_version)
+    structured = llm.with_structured_output(_Answer, method="json_schema")
+    with patch.object(llm, "root_client") as root_client:
+        root_client.beta.chat.completions.stream.return_value = _BetaStream()
+        with pytest.raises(OpenAIRefusalError) as exc_info:
+            list(structured.stream("hi"))
+    assert str(exc_info.value) == "I'm sorry, I cannot comply."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.7.3/uv.lock 
new/langchain_openai-1.7.4/uv.lock
--- old/langchain_openai-1.7.3/uv.lock  2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.7.4/uv.lock  2020-02-02 01:00:00.000000000 +0100
@@ -1069,7 +1069,7 @@
 
 [[package]]
 name = "langchain"
-version = "1.4.7"
+version = "1.4.8"
 source = { editable = "../../langchain_v1" }
 dependencies = [
     { name = "langchain-core" },
@@ -1137,9 +1137,10 @@
 
 [[package]]
 name = "langchain-core"
-version = "1.7.0"
+version = "1.7.5"
 source = { editable = "../../core" }
 dependencies = [
+    { name = "certifi" },
     { name = "httpx" },
     { name = "jsonpatch" },
     { name = "langchain-protocol" },
@@ -1154,6 +1155,7 @@
 
 [package.metadata]
 requires-dist = [
+    { name = "certifi", specifier = ">=2026.10.19" },
     { name = "httpx", specifier = ">=0.23.0" },
     { name = "jsonpatch", specifier = ">=1.33.0" },
     { name = "langchain-protocol", specifier = ">=0.0.20" },
@@ -1201,7 +1203,7 @@
 
 [[package]]
 name = "langchain-openai"
-version = "1.7.3"
+version = "1.7.4"
 source = { editable = "." }
 dependencies = [
     { name = "certifi" },
@@ -1331,7 +1333,7 @@
     { name = "numpy", marker = "python_full_version >= '3.13'", specifier = 
">=2.1.0" },
     { name = "pytest", specifier = ">=9.0.3" },
     { name = "pytest-asyncio", specifier = ">=1.3.0" },
-    { name = "pytest-benchmark" },
+    { name = "pytest-benchmark", specifier = ">=5.3.1" },
     { name = "pytest-codspeed" },
     { name = "pytest-recording" },
     { name = "pytest-socket", specifier = ">=0.7.0" },
@@ -2372,12 +2374,12 @@
 ]
 
 [[package]]
-name = "py-cpuinfo"
-version = "9.0.0"
+name = "py-cpuinfo2"
+version = "10.1.1"
 source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz";,
 hash = 
"sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size 
= 104716, upload-time = "2022-10-25T20:38:06.303Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/dc/97/a8b1ddada14c8280a047c0746f95cb05d94a31b1a331cea22bcdc2b2a82d/py_cpuinfo2-10.1.1.tar.gz";,
 hash = 
"sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771", size 
= 100840, upload-time = "2026-03-25T21:49:40.797Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl";,
 hash = 
"sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size 
= 22335, upload-time = "2022-10-25T20:38:27.636Z" },
+    { url = 
"https://files.pythonhosted.org/packages/23/0a/ba69d2dde1ae12ef1d389ea5a216384c5ff6ef7a1e7a48d1e9b6686f6790/py_cpuinfo2-10.1.1-py3-none-any.whl";,
 hash = 
"sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d", size 
= 23791, upload-time = "2026-03-25T21:49:39.574Z" },
 ]
 
 [[package]]
@@ -2578,15 +2580,15 @@
 
 [[package]]
 name = "pytest-benchmark"
-version = "5.0.1"
+version = "5.3.1"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "py-cpuinfo" },
+    { name = "py-cpuinfo2" },
     { name = "pytest" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/a3/48/b79272b2b8938513a66a62204a0649ef730dcf6cb52c812f4dc4daa62cd5/pytest-benchmark-5.0.1.tar.gz";,
 hash = 
"sha256:8138178618c85586ce056c70cc5e92f4283c2e6198e8422c2c825aeb3ace6afd", size 
= 337310, upload-time = "2024-10-30T01:12:16.991Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/13/d1/1bc61155de46f25473a06476ad1b3a94f96d40d49cfa98f82c1184117808/pytest_benchmark-5.3.1.tar.gz";,
 hash = 
"sha256:c1ee70a58f9bfb1ac955cc634247a340b0df7bcb949071cfca396f16c6fc3516", size 
= 385467, upload-time = "2026-11-01T03:32:10.048Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/f7/e2/c0da4989a933d6bac364f215217c47de37d2f641953aa69a37b66efd6d1b/pytest_benchmark-5.0.1-py3-none-any.whl";,
 hash = 
"sha256:d75fec4cbf0d4fd91e020f425ce2d845e9c127c21bae35e77c84db8ed84bfaa6", size 
= 44062, upload-time = "2024-10-30T01:12:13.716Z" },
+    { url = 
"https://files.pythonhosted.org/packages/78/75/919ac85ad3f762e45aa369d2f68d40390e316474b6bb8434aeb6e3c8bf8b/pytest_benchmark-5.3.1-py3-none-any.whl";,
 hash = 
"sha256:d7732a9952b1c4a4a32277adc693739126eab37afc3a711847f53805ccadc020", size 
= 49142, upload-time = "2026-11-01T03:32:08.359Z" },
 ]
 
 [[package]]

Reply via email to