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]]