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-08-20 16:15:26
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-openai (Old)
 and      /work/SRC/openSUSE:Factory/.python-langchain-openai.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langchain-openai"

Thu Aug 20 16:15:26 2026 rev:11 rq:1372139 version:1.6.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langchain-openai/python-langchain-openai.changes
  2026-08-18 16:38:03.024295929 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langchain-openai.new.1258/python-langchain-openai.changes
        2026-08-20 16:15:35.467876957 +0200
@@ -1,0 +2,31 @@
+Thu Aug 20 05:52:43 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.6.0:
+  * Map OpenAI SDK failures onto the new standard model exception
+    types from langchain-core 1.6.0
+  * Raise a clear error on an unexpected response type in
+    _create_chat_result
+- Follow upstream and raise the langchain-core requirement floor
+  to 1.6.0
+
+-------------------------------------------------------------------
+Wed Aug 19 07:22:00 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.5.2:
+  * Preserve consecutive reasoning-item boundaries when converting
+    v1 content blocks back to Responses API input: fragments are
+    only merged when their IDs match, so a later tool turn no
+    longer loses a required reasoning item
+  * Extract LangSmith gateway metadata from response headers when
+    present and attach it to traces
+  * Support o-series models (o1, o3, o4 families) in
+    get_num_tokens_from_messages instead of raising
+    NotImplementedError
+- Follow upstream and raise the langchain-core requirement floor
+  to 1.5.6
+- Add python-httpx2 as a test BuildRequires so the chat-model unit
+  tests can collect (they now import httpx2 at module level)
+- Deselect the new o-series token-counting unit test that would
+  download a tiktoken BPE encoding in the offline build
+
+-------------------------------------------------------------------

Old:
----
  langchain_openai-1.5.1.tar.gz

New:
----
  langchain_openai-1.6.0.tar.gz

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

Other differences:
------------------
++++++ python-langchain-openai.spec ++++++
--- /var/tmp/diff_new_pack.A8yKOO/_old  2026-08-20 16:15:36.270905367 +0200
+++ /var/tmp/diff_new_pack.A8yKOO/_new  2026-08-20 16:15:36.271905403 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-langchain-openai
-Version:        1.5.1
+Version:        1.6.0
 Release:        0
 Summary:        An integration package connecting OpenAI and LangChain
 License:        MIT
@@ -28,14 +28,15 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-certifi >= 2024.6.2
-Requires:       python-langchain-core >= 1.5.4
+Requires:       python-langchain-core >= 1.6.0
 Requires:       python-openai >= 2.45.0
 Requires:       python-tiktoken >= 0.7.0
 BuildArch:      noarch
 # SECTION test requirements
 BuildRequires:  %{python_module certifi >= 2024.6.2}
+BuildRequires:  %{python_module httpx2}
 BuildRequires:  %{python_module httpx}
-BuildRequires:  %{python_module langchain-core >= 1.5.4}
+BuildRequires:  %{python_module langchain-core >= 1.6.0}
 BuildRequires:  %{python_module openai >= 2.45.0}
 BuildRequires:  %{python_module pytest-asyncio}
 BuildRequires:  %{python_module tiktoken >= 0.7.0}
@@ -78,8 +79,8 @@
 # (pytest-cov is not needed for the build). The deselected token-counting
 # tests call tiktoken with a model whose BPE encoding is not bundled, so
 # tiktoken tries to download it from openaipublic.blob.core.windows.net,
-# which fails in the offline build.
-%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 --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/embeddings/test_base.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[gp
 t-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]"
+# 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 --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_base.py::test_embed_documents_with_custom_chunk_size
 --deselect tests/unit_tests/embeddings/test_base.py::test_embe
 ddings_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}
 %doc README.md

++++++ langchain_openai-1.5.1.tar.gz -> langchain_openai-1.6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.5.1/PKG-INFO 
new/langchain_openai-1.6.0/PKG-INFO
--- old/langchain_openai-1.5.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langchain-openai
-Version: 1.5.1
+Version: 1.6.0
 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.5.4
+Requires-Dist: langchain-core<2.0.0,>=1.6.0
 Requires-Dist: openai<4.0.0,>=2.45.0
 Requires-Dist: tiktoken<1.0.0,>=0.7.0
 Description-Content-Type: text/markdown
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.5.1/langchain_openai/_version.py 
new/langchain_openai-1.6.0/langchain_openai/_version.py
--- old/langchain_openai-1.5.1/langchain_openai/_version.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/langchain_openai/_version.py     2020-02-02 
01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
 """Version information for `langchain-openai`."""
 
-__version__ = "1.5.1"
+__version__ = "1.6.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/langchain_openai/chat_models/_compat.py 
new/langchain_openai-1.6.0/langchain_openai/chat_models/_compat.py
--- old/langchain_openai-1.5.1/langchain_openai/chat_models/_compat.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/langchain_openai/chat_models/_compat.py  
2020-02-02 01:00:00.000000000 +0100
@@ -212,6 +212,15 @@
     return dict(annotation)
 
 
+def _same_reasoning_item(first: dict[str, Any], second: dict[str, Any]) -> 
bool:
+    """Return whether two reasoning fragments share an item identity."""
+    first_has_id = "id" in first
+    second_has_id = "id" in second
+    return first_has_id == second_has_id and (
+        not first_has_id or first["id"] == second["id"]
+    )
+
+
 def _implode_reasoning_blocks(blocks: list[dict[str, Any]]) -> 
Iterable[dict[str, Any]]:
     i = 0
     n = len(blocks)
@@ -249,7 +258,11 @@
         i += 1
         while i < n:
             next_ = blocks[i]
-            if next_.get("type") == "reasoning" and "reasoning" in next_:
+            if (
+                next_.get("type") == "reasoning"
+                and "reasoning" in next_
+                and _same_reasoning_item(block, next_)
+            ):
                 summary.append(
                     {"type": "summary_text", "text": next_.get("reasoning", 
"")}
                 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/langchain_openai/chat_models/base.py 
new/langchain_openai-1.6.0/langchain_openai/chat_models/base.py
--- old/langchain_openai-1.5.1/langchain_openai/chat_models/base.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/langchain_openai/chat_models/base.py     
2020-02-02 01:00:00.000000000 +0100
@@ -51,7 +51,17 @@
     AsyncCallbackManagerForLLMRun,
     CallbackManagerForLLMRun,
 )
-from langchain_core.exceptions import ContextOverflowError
+from langchain_core.exceptions import (
+    ContextOverflowError,
+    ModelAPIError,
+    ModelAuthenticationError,
+    ModelConnectionError,
+    ModelInvalidRequestError,
+    ModelNotFoundError,
+    ModelPermissionDeniedError,
+    ModelRateLimitError,
+    ModelTimeoutError,
+)
 from langchain_core.language_models import (
     LanguageModelInput,
     ModelProfileRegistry,
@@ -108,7 +118,11 @@
 from langchain_core.tools import BaseTool
 from langchain_core.tools.base import _stringify
 from langchain_core.utils import get_pydantic_field_names
-from langchain_core.utils._gateway import _resolve_gateway_config
+from langchain_core.utils._gateway import (
+    GATEWAY_METADATA_RESPONSE_KEY,
+    _parse_gateway_metadata,
+    _resolve_gateway_config,
+)
 from langchain_core.utils.function_calling import (
     convert_to_openai_function,
     convert_to_openai_tool,
@@ -559,6 +573,40 @@
     """APIError raised when input exceeds OpenAI's context limit."""
 
 
+class OpenAIAuthenticationError(openai.AuthenticationError, 
ModelAuthenticationError):
+    """OpenAI authentication error classified as a LangChain model error."""
+
+
+class OpenAIPermissionDeniedError(
+    openai.PermissionDeniedError, ModelPermissionDeniedError
+):
+    """OpenAI permission error classified as a LangChain model error."""
+
+
+class OpenAIInvalidRequestError(openai.BadRequestError, 
ModelInvalidRequestError):
+    """OpenAI bad-request error classified as a LangChain model error."""
+
+
+class OpenAIModelNotFoundError(openai.NotFoundError, ModelNotFoundError):
+    """OpenAI not-found error classified as a LangChain model error."""
+
+
+class OpenAIRateLimitError(openai.RateLimitError, ModelRateLimitError):
+    """OpenAI rate-limit error classified as a LangChain model error."""
+
+
+class OpenAIAPIError(openai.InternalServerError, ModelAPIError):
+    """OpenAI server error classified as a LangChain model error."""
+
+
+class OpenAIConnectionError(openai.APIConnectionError, ModelConnectionError):
+    """OpenAI connection error classified as a LangChain model error."""
+
+
+class OpenAITimeoutError(openai.APITimeoutError, ModelTimeoutError):
+    """OpenAI timeout error classified as a LangChain model error."""
+
+
 def _handle_openai_bad_request(e: openai.BadRequestError) -> None:
     if (
         "context_length_exceeded" in str(e)
@@ -579,7 +627,9 @@
             'specify `method="function_calling"`.'
         )
         warnings.warn(message)
-        raise e
+        raise OpenAIInvalidRequestError(
+            message=e.message, response=e.response, body=e.body
+        ) from e
     if "Invalid schema for response_format" in e.message:
         message = (
             "Invalid schema for OpenAI's structured output feature, which is 
the "
@@ -589,8 +639,9 @@
             
"https://platform.openai.com/docs/guides/structured-outputs#supported-schemas";
         )
         warnings.warn(message)
-        raise e
-    raise
+    raise OpenAIInvalidRequestError(
+        message=e.message, response=e.response, body=e.body
+    ) from e
 
 
 def _handle_openai_api_error(e: openai.APIError) -> None:
@@ -599,9 +650,46 @@
         raise OpenAIAPIContextOverflowError(
             message=e.message, request=e.request, body=e.body
         ) from e
+    if isinstance(e, openai.AuthenticationError):
+        raise OpenAIAuthenticationError(
+            message=e.message, response=e.response, body=e.body
+        ) from e
+    if isinstance(e, openai.PermissionDeniedError):
+        raise OpenAIPermissionDeniedError(
+            message=e.message, response=e.response, body=e.body
+        ) from e
+    if isinstance(e, openai.NotFoundError):
+        raise OpenAIModelNotFoundError(
+            message=e.message, response=e.response, body=e.body
+        ) from e
+    if isinstance(e, openai.RateLimitError):
+        raise OpenAIRateLimitError(
+            message=e.message, response=e.response, body=e.body
+        ) from e
+    if isinstance(e, openai.InternalServerError):
+        raise OpenAIAPIError(message=e.message, response=e.response, 
body=e.body) from e
+    if isinstance(e, openai.APITimeoutError):
+        raise OpenAITimeoutError(e.request) from e
+    if isinstance(e, openai.APIConnectionError):
+        raise OpenAIConnectionError(message=e.message, request=e.request) from 
e
     raise
 
 
+def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) 
-> None:
+    """Add parsed LangSmith gateway metadata to `generation_info`, if present.
+
+    Args:
+        generation_info: Generation info to mutate in place.
+        raw_response: The raw provider response, or None.
+    """
+    headers = getattr(raw_response, "headers", None)
+    if headers is None:
+        return
+    gateway_metadata = _parse_gateway_metadata(headers)
+    if gateway_metadata is not None:
+        generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata
+
+
 _RESPONSES_API_ONLY_PREFIXES = (
     "gpt-5-pro",
     "gpt-5.2-pro",
@@ -1122,6 +1210,19 @@
     model_config = ConfigDict(populate_by_name=True)
 
     @property
+    def _uses_gateway(self) -> bool:
+        """Whether requests are routed through the LangSmith gateway.
+
+        Detected from the resolved API key: LangSmith keys (used to 
authenticate
+        to the gateway) carry the `lsv2_` prefix. Callable keys cannot be
+        inspected without invoking them, so they are treated as non-gateway.
+        """
+        api_key = self.openai_api_key
+        if isinstance(api_key, SecretStr):
+            return api_key.get_secret_value().startswith("lsv2_")
+        return False
+
+    @property
     def model(self) -> str:
         """Same as model_name."""
         return self.model_name
@@ -1497,16 +1598,19 @@
         self._ensure_sync_client_available()
         kwargs["stream"] = True
         payload = self._get_request_payload(messages, stop=stop, **kwargs)
+        headers: dict = {}
+        base_generation_info: dict = {}
         try:
-            if self.include_response_headers:
+            if self.include_response_headers or self._uses_gateway:
                 raw_context_manager = (
                     
self.root_client.with_raw_response.responses.create(**payload)
                 )
                 context_manager = raw_context_manager.parse()
-                headers = {"headers": dict(raw_context_manager.headers)}
+                if self.include_response_headers:
+                    headers = {"headers": dict(raw_context_manager.headers)}
+                _add_gateway_metadata(base_generation_info, 
raw_context_manager)
             else:
                 context_manager = self.root_client.responses.create(**payload)
-                headers = {}
             original_schema_obj = kwargs.get("response_format")
 
             with context_manager as response:
@@ -1533,6 +1637,11 @@
                         output_version=self.output_version,
                     )
                     if generation_chunk:
+                        if is_first_chunk and base_generation_info:
+                            generation_chunk.generation_info = {
+                                **base_generation_info,
+                                **(generation_chunk.generation_info or {}),
+                            }
                         if run_manager:
                             run_manager.on_llm_new_token(
                                 generation_chunk.text, chunk=generation_chunk
@@ -1555,20 +1664,23 @@
     ) -> AsyncIterator[ChatGenerationChunk]:
         kwargs["stream"] = True
         payload = self._get_request_payload(messages, stop=stop, **kwargs)
+        headers: dict = {}
+        base_generation_info: dict = {}
         try:
-            if self.include_response_headers:
+            if self.include_response_headers or self._uses_gateway:
                 raw_context_manager = (
                     await 
self.root_async_client.with_raw_response.responses.create(
                         **payload
                     )
                 )
                 context_manager = raw_context_manager.parse()
-                headers = {"headers": dict(raw_context_manager.headers)}
+                if self.include_response_headers:
+                    headers = {"headers": dict(raw_context_manager.headers)}
+                _add_gateway_metadata(base_generation_info, 
raw_context_manager)
             else:
                 context_manager = await 
self.root_async_client.responses.create(
                     **payload
                 )
-                headers = {}
             original_schema_obj = kwargs.get("response_format")
 
             async with context_manager as response:
@@ -1599,6 +1711,11 @@
                         output_version=self.output_version,
                     )
                     if generation_chunk:
+                        if is_first_chunk and base_generation_info:
+                            generation_chunk.generation_info = {
+                                **base_generation_info,
+                                **(generation_chunk.generation_info or {}),
+                            }
                         if run_manager:
                             await run_manager.on_llm_new_token(
                                 generation_chunk.text, chunk=generation_chunk
@@ -1662,10 +1779,12 @@
                 )
                 context_manager = response_stream
             else:
-                if self.include_response_headers:
+                if self.include_response_headers or self._uses_gateway:
                     raw_response = 
self.client.with_raw_response.create(**payload)
                     response = raw_response.parse()
-                    base_generation_info = {"headers": 
dict(raw_response.headers)}
+                    if self.include_response_headers:
+                        base_generation_info = {"headers": 
dict(raw_response.headers)}
+                    _add_gateway_metadata(base_generation_info, raw_response)
                 else:
                     response = self.client.create(**payload)
                 context_manager = response
@@ -1737,12 +1856,26 @@
                 response = raw_response.parse()
                 if self.include_response_headers:
                     generation_info = {"headers": dict(raw_response.headers)}
-                return _construct_lc_result_from_responses_api(
+                generation_info = generation_info or {}
+                _add_gateway_metadata(generation_info, raw_response)
+                # Gateway metadata belongs on `generation_info`, not the 
message
+                # `response_metadata` that `metadata` populates.
+                gateway_metadata = generation_info.pop(
+                    GATEWAY_METADATA_RESPONSE_KEY, None
+                )
+                result = _construct_lc_result_from_responses_api(
                     response,
                     schema=original_schema_obj,
                     metadata=generation_info,
                     output_version=self.output_version,
                 )
+                if gateway_metadata is not None:
+                    for generation in result.generations:
+                        generation.generation_info = 
generation.generation_info or {}
+                        
generation.generation_info[GATEWAY_METADATA_RESPONSE_KEY] = (
+                            gateway_metadata
+                        )
+                return result
             else:
                 raw_response = self.client.with_raw_response.create(**payload)
                 response = raw_response.parse()
@@ -1760,6 +1893,8 @@
             and hasattr(raw_response, "headers")
         ):
             generation_info = {"headers": dict(raw_response.headers)}
+        generation_info = generation_info or {}
+        _add_gateway_metadata(generation_info, raw_response)
         return self._create_chat_result(response, generation_info)
 
     def _use_responses_api(self, payload: dict) -> bool:
@@ -1815,6 +1950,19 @@
     ) -> ChatResult:
         generations = []
 
+        if not isinstance(response, dict | openai.BaseModel):
+            # `parse()` yields a `str` when the endpoint returns a non-JSON 
body,
+            # e.g. an HTML error page served after a redirect.
+            preview = repr(response)
+            if len(preview) > 200:
+                preview = f"{preview[:200]}..."
+            msg = (
+                "Unexpected response type from OpenAI-compatible endpoint. "
+                "Expected a dict or openai.BaseModel, got "
+                f"{type(response).__name__}: {preview}"
+            )
+            raise ValueError(msg)
+
         response_dict = (
             response
             if isinstance(response, dict)
@@ -1922,12 +2070,14 @@
                 )
                 context_manager = response_stream
             else:
-                if self.include_response_headers:
+                if self.include_response_headers or self._uses_gateway:
                     raw_response = await 
self.async_client.with_raw_response.create(
                         **payload
                     )
                     response = raw_response.parse()
-                    base_generation_info = {"headers": 
dict(raw_response.headers)}
+                    if self.include_response_headers:
+                        base_generation_info = {"headers": 
dict(raw_response.headers)}
+                    _add_gateway_metadata(base_generation_info, raw_response)
                 else:
                     response = await self.async_client.create(**payload)
                 context_manager = response
@@ -1980,7 +2130,7 @@
         **kwargs: Any,
     ) -> ChatResult:
         payload = self._get_request_payload(messages, stop=stop, **kwargs)
-        generation_info = None
+        generation_info = {}
         raw_response = None
         try:
             if "response_format" in payload:
@@ -2006,12 +2156,25 @@
                 response = raw_response.parse()
                 if self.include_response_headers:
                     generation_info = {"headers": dict(raw_response.headers)}
-                return _construct_lc_result_from_responses_api(
+                _add_gateway_metadata(generation_info, raw_response)
+                # Gateway metadata belongs on `generation_info`, not the 
message
+                # `response_metadata` that `metadata` populates.
+                gateway_metadata = generation_info.pop(
+                    GATEWAY_METADATA_RESPONSE_KEY, None
+                )
+                result = _construct_lc_result_from_responses_api(
                     response,
                     schema=original_schema_obj,
                     metadata=generation_info,
                     output_version=self.output_version,
                 )
+                if gateway_metadata is not None:
+                    for generation in result.generations:
+                        generation.generation_info = 
generation.generation_info or {}
+                        
generation.generation_info[GATEWAY_METADATA_RESPONSE_KEY] = (
+                            gateway_metadata
+                        )
+                return result
             else:
                 raw_response = await 
self.async_client.with_raw_response.create(
                     **payload
@@ -2031,6 +2194,7 @@
             and hasattr(raw_response, "headers")
         ):
             generation_info = {"headers": dict(raw_response.headers)}
+        _add_gateway_metadata(generation_info, raw_response)
         return await run_in_executor(
             None, self._create_chat_result, response, generation_info
         )
@@ -2158,7 +2322,7 @@
             tokens_per_message = 4
             # if there's a name, the role is omitted
             tokens_per_name = -1
-        elif model.startswith(("gpt-3.5-turbo", "gpt-4", "gpt-5")):
+        elif model.startswith(("gpt-3.5-turbo", "gpt-4", "gpt-5", "o1", "o3", 
"o4")):
             tokens_per_message = 3
             tokens_per_name = 1
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.5.1/pyproject.toml 
new/langchain_openai-1.6.0/pyproject.toml
--- old/langchain_openai-1.5.1/pyproject.toml   2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_openai-1.6.0/pyproject.toml   2020-02-02 01:00:00.000000000 
+0100
@@ -20,10 +20,10 @@
     "Topic :: Scientific/Engineering :: Artificial Intelligence",
 ]
 
-version = "1.5.1"
+version = "1.6.0"
 requires-python = ">=3.10.0,<4.0.0"
 dependencies = [
-    "langchain-core>=1.5.4,<2.0.0",
+    "langchain-core>=1.6.0,<2.0.0",
     "certifi>=2024.6.2",
     "openai>=2.45.0,<4.0.0",
     "tiktoken>=0.7.0,<1.0.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/__snapshots__/test_base_standard.ambr
 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/__snapshots__/test_base_standard.ambr
--- 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/__snapshots__/test_base_standard.ambr
       2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/__snapshots__/test_base_standard.ambr
       2020-02-02 01:00:00.000000000 +0100
@@ -11,6 +11,7 @@
       'max_retries': 2,
       'max_tokens': 100,
       'model_name': 'gpt-3.5-turbo',
+      'openai_api_base': 'https://api.openai.com/v1',
       'openai_api_key': dict({
         'id': list([
           'OPENAI_API_KEY',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/__snapshots__/test_responses_standard.ambr
 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/__snapshots__/test_responses_standard.ambr
--- 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/__snapshots__/test_responses_standard.ambr
  2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/__snapshots__/test_responses_standard.ambr
  2020-02-02 01:00:00.000000000 +0100
@@ -11,6 +11,7 @@
       'max_retries': 2,
       'max_tokens': 100,
       'model_name': 'gpt-3.5-turbo',
+      'openai_api_base': 'https://api.openai.com/v1',
       'openai_api_key': dict({
         'id': list([
           'OPENAI_API_KEY',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_base.py 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_base.py
--- old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_base.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_base.py        
2020-02-02 01:00:00.000000000 +0100
@@ -10,9 +10,21 @@
 from unittest.mock import AsyncMock, MagicMock, patch
 
 import httpx
+import httpx2
 import openai
 import pytest
-from langchain_core.exceptions import ContextOverflowError
+from langchain_core.exceptions import (
+    ContextOverflowError,
+    ModelAPIError,
+    ModelAuthenticationError,
+    ModelConnectionError,
+    ModelError,
+    ModelInvalidRequestError,
+    ModelNotFoundError,
+    ModelPermissionDeniedError,
+    ModelRateLimitError,
+    ModelTimeoutError,
+)
 from langchain_core.load import dumps, loads
 from langchain_core.messages import (
     AIMessage,
@@ -36,12 +48,14 @@
 from langchain_core.runnables.base import RunnableBinding, RunnableSequence
 from langchain_core.tracers.base import BaseTracer
 from langchain_core.tracers.schemas import Run
+from langchain_core.utils._gateway import GATEWAY_METADATA_RESPONSE_KEY
 from langchain_core.utils.pydantic import PYDANTIC_VERSION
 from openai.types.responses import (
     ResponseApplyPatchToolCall,
     ResponseApplyPatchToolCallOutput,
     ResponseOutputMessage,
     ResponseReasoningItem,
+    ResponseTextDeltaEvent,
 )
 from openai.types.responses.response import IncompleteDetails, Response
 from openai.types.responses.response_apply_patch_tool_call import 
OperationCreateFile
@@ -722,6 +736,160 @@
     assert mock_async_client.with_raw_response.create.called
 
 
+class _GatewayMetadataTracer(BaseTracer):
+    """Captures gateway metadata promoted onto completed LLM runs."""
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.gateway_metadata: dict | None = None
+
+    def _persist_run(self, run: Run) -> None:
+        """No-op; runs are inspected as they complete."""
+
+    def _on_llm_end(self, run: Run) -> None:
+        metadata = run.extra.get("metadata", {})
+        if "ls_gateway_info" in metadata:
+            self.gateway_metadata = metadata["ls_gateway_info"]
+
+
+_GATEWAY_METADATA_HEADERS = httpx2.Headers(
+    {"x-langsmith-gateway-metadata": '{"provider": "openai"}'}
+)
+
+_RESPONSES_API_COMPLETION = Response(
+    id="resp_123",
+    created_at=1234567890,
+    model=OPENAI_TEST_MODEL,
+    object="response",
+    parallel_tool_calls=True,
+    tools=[],
+    tool_choice="auto",
+    output=[
+        ResponseOutputMessage(
+            type="message",
+            id="msg_123",
+            content=[
+                ResponseOutputText(type="output_text", text="Bar Baz", 
annotations=[])
+            ],
+            role="assistant",
+            status="completed",
+        )
+    ],
+)
+
+_RESPONSES_API_STREAM = [
+    ResponseTextDeltaEvent(
+        content_index=0,
+        delta="Bar Baz",
+        item_id="msg_123",
+        output_index=0,
+        sequence_number=0,
+        logprobs=[],
+        type="response.output_text.delta",
+    ),
+]
+
+
[email protected]("use_responses_api", [False, True])
+def test_openai_invoke_surfaces_gateway_metadata(
+    mock_completion: dict, *, use_responses_api: bool
+) -> None:
+    """Gateway metadata header is surfaced on `generation_info`, not the 
message."""
+    llm = ChatOpenAI(use_responses_api=use_responses_api)
+    mock_client = MagicMock()
+    mock_resp = MagicMock()
+    mock_resp.headers = _GATEWAY_METADATA_HEADERS
+    if use_responses_api:
+        mock_resp.parse.return_value = _RESPONSES_API_COMPLETION
+        mock_client.responses.with_raw_response.create.return_value = mock_resp
+        client_attr = "root_client"
+    else:
+        mock_resp.parse.return_value = mock_completion
+        mock_client.with_raw_response.create.return_value = mock_resp
+        client_attr = "client"
+
+    tracer = _GatewayMetadataTracer()
+    with patch.object(llm, client_attr, mock_client):
+        res = llm.invoke("bar", config={"callbacks": [tracer]})
+
+    # Gateway metadata reaches the tracer via `generation_info`...
+    assert tracer.gateway_metadata == {"provider": "openai"}
+    # ...but is kept off the user-facing message `response_metadata`.
+    assert GATEWAY_METADATA_RESPONSE_KEY not in res.response_metadata
+
+
[email protected]("use_responses_api", [False, True])
+def test_openai_stream_surfaces_gateway_metadata(
+    mock_openai_completion: list, *, use_responses_api: bool
+) -> None:
+    """Gateway metadata reaches the tracer for a gateway-routed stream."""
+    # A LangSmith API key signals gateway routing, so streaming fetches raw
+    # headers.
+    llm = ChatOpenAI(
+        model=OPENAI_TEST_MODEL,
+        api_key="lsv2_pt_example",  # type: ignore[arg-type]
+        use_responses_api=use_responses_api,
+    )
+    mock_client = MagicMock()
+    mock_resp = MagicMock()
+    mock_resp.headers = _GATEWAY_METADATA_HEADERS
+    if use_responses_api:
+        mock_resp.parse.return_value = 
MockSyncContextManager(_RESPONSES_API_STREAM)
+        mock_client.with_raw_response.responses.create.return_value = mock_resp
+        mock_client.responses.create.return_value = MockSyncContextManager(
+            _RESPONSES_API_STREAM
+        )
+        client_attr = "root_client"
+    else:
+        mock_resp.parse.return_value = 
MockSyncContextManager(mock_openai_completion)
+        mock_client.with_raw_response.create.return_value = mock_resp
+        client_attr = "client"
+
+    tracer = _GatewayMetadataTracer()
+    with patch.object(llm, client_attr, mock_client):
+        for chunk in llm.stream("what is your name?", config={"callbacks": 
[tracer]}):
+            # Gateway metadata is kept off the user-facing chunk metadata.
+            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata
+
+    assert tracer.gateway_metadata == {"provider": "openai"}
+
+
[email protected]("use_responses_api", [False, True])
+async def test_openai_astream_surfaces_gateway_metadata(
+    mock_openai_completion: list, *, use_responses_api: bool
+) -> None:
+    """Gateway metadata reaches the tracer for a gateway-routed async 
stream."""
+    llm = ChatOpenAI(
+        model=OPENAI_TEST_MODEL,
+        api_key="lsv2_pt_example",  # type: ignore[arg-type]
+        use_responses_api=use_responses_api,
+    )
+    mock_client = AsyncMock()
+    mock_resp = MagicMock()
+    mock_resp.headers = _GATEWAY_METADATA_HEADERS
+    if use_responses_api:
+        mock_resp.parse.return_value = 
MockAsyncContextManager(_RESPONSES_API_STREAM)
+        mock_client.with_raw_response.responses.create.return_value = mock_resp
+        mock_client.responses.create.return_value = MockAsyncContextManager(
+            _RESPONSES_API_STREAM
+        )
+        client_attr = "root_async_client"
+    else:
+        mock_resp.parse.return_value = 
MockAsyncContextManager(mock_openai_completion)
+        mock_client.with_raw_response.create.return_value = mock_resp
+        client_attr = "async_client"
+
+    tracer = _GatewayMetadataTracer()
+    with patch.object(llm, client_attr, mock_client):
+        async for chunk in llm.astream(
+            "what is your name?", config={"callbacks": [tracer]}
+        ):
+            # Gateway metadata is kept off the user-facing chunk metadata.
+            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata
+
+    assert tracer.gateway_metadata == {"provider": "openai"}
+
+
 @pytest.mark.parametrize(
     "model",
     [
@@ -1099,6 +1267,26 @@
     assert actual
 
 
[email protected](
+    "model", ["o1", "o1-preview", "o1-mini", "o3", "o3-mini", "o4-mini"]
+)
+def test_get_num_tokens_from_messages_o_series(model: str) -> None:
+    """o-series models use the same message token format as gpt-4/gpt-5.
+
+    Regression test: these raised NotImplementedError.
+    """
+    llm = ChatOpenAI(model=model)
+    messages = [
+        SystemMessage("you're a good assistant"),
+        HumanMessage("how are you"),
+    ]
+    actual = llm.get_num_tokens_from_messages(messages)
+    expected = 
ChatOpenAI(model=OPENAI_TEST_MODEL).get_num_tokens_from_messages(
+        messages
+    )
+    assert actual == expected
+
+
 class Foo(BaseModel):
     bar: int
 
@@ -1923,6 +2111,30 @@
     )
 
 
+def test_create_chat_result_raises_on_unexpected_response_type() -> None:
+    """A non-JSON response body must surface a clear error, not an 
`AttributeError`."""
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+
+    with pytest.raises(ValueError, match="got str") as exc_info:
+        llm._create_chat_result("<html><body>Moved</body></html>")  # type: 
ignore[arg-type]
+    assert "Moved" in str(exc_info.value)
+
+    with pytest.raises(ValueError, match="got object"):
+        llm._create_chat_result(object())  # type: ignore[arg-type]
+
+
+def test_create_chat_result_truncates_unexpected_response_body() -> None:
+    """A large response body must not be echoed in full in the error 
message."""
+    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)
+    body = "<html>" + "x" * 5000 + "</html>"
+
+    with pytest.raises(ValueError, match="got str") as exc_info:
+        llm._create_chat_result(body)  # type: ignore[arg-type]
+    message = str(exc_info.value)
+    assert len(message) < len(body)
+    assert message.endswith("...")
+
+
 @pytest.mark.skipif(
     (PYDANTIC_VERSION.major, PYDANTIC_VERSION.minor) < (2, 8),
     reason=(
@@ -3830,6 +4042,86 @@
     assert message_v1 != result
 
 
+def test_convert_from_v1_to_responses_preserves_reasoning_item_boundaries() -> 
None:
+    content: list[types.ContentBlock] = [
+        {
+            "type": "reasoning",
+            "id": "rs_123",
+            "reasoning": "first ",
+            "extras": {
+                "encrypted_content": "encrypted-123",
+                "status": "completed",
+            },
+        },
+        {
+            "type": "reasoning",
+            "id": "rs_123",
+            "reasoning": "second",
+        },
+        {
+            "type": "reasoning",
+            "id": "rs_456",
+            "reasoning": "third",
+            "extras": {
+                "encrypted_content": "encrypted-456",
+                "status": "incomplete",
+            },
+        },
+        {"type": "reasoning", "reasoning": "legacy "},
+        {"type": "reasoning", "reasoning": "reasoning"},
+        {"type": "reasoning", "id": "rs_789", "reasoning": "last"},
+        cast(
+            types.ContentBlock,
+            {
+                "type": "reasoning",
+                "id": "rs_native",
+                "summary": [{"type": "summary_text", "text": "already 
native"}],
+                "encrypted_content": "encrypted-native",
+            },
+        ),
+    ]
+
+    result = _convert_from_v1_to_responses(content, [])
+
+    assert result == [
+        {
+            "type": "reasoning",
+            "id": "rs_123",
+            "summary": [
+                {"type": "summary_text", "text": "first "},
+                {"type": "summary_text", "text": "second"},
+            ],
+            "encrypted_content": "encrypted-123",
+            "status": "completed",
+        },
+        {
+            "type": "reasoning",
+            "id": "rs_456",
+            "summary": [{"type": "summary_text", "text": "third"}],
+            "encrypted_content": "encrypted-456",
+            "status": "incomplete",
+        },
+        {
+            "type": "reasoning",
+            "summary": [
+                {"type": "summary_text", "text": "legacy "},
+                {"type": "summary_text", "text": "reasoning"},
+            ],
+        },
+        {
+            "type": "reasoning",
+            "id": "rs_789",
+            "summary": [{"type": "summary_text", "text": "last"}],
+        },
+        {
+            "type": "reasoning",
+            "id": "rs_native",
+            "summary": [{"type": "summary_text", "text": "already native"}],
+            "encrypted_content": "encrypted-native",
+        },
+    ]
+
+
 def test_convert_from_v1_to_responses_missing_type() -> None:
     """Regression: blocks without 'type' should be skipped, not raise 
KeyError."""
     content: list = [
@@ -4576,6 +4868,57 @@
     assert isinstance(exc_info.value, ContextOverflowError)
 
 
[email protected](
+    ("status_code", "sdk_error_type", "model_error_type", "is_retryable"),
+    [
+        (400, openai.BadRequestError, ModelInvalidRequestError, False),
+        (401, openai.AuthenticationError, ModelAuthenticationError, False),
+        (403, openai.PermissionDeniedError, ModelPermissionDeniedError, False),
+        (404, openai.NotFoundError, ModelNotFoundError, False),
+        (429, openai.RateLimitError, ModelRateLimitError, True),
+        (500, openai.InternalServerError, ModelAPIError, True),
+    ],
+)
+def test_openai_error_classification(
+    status_code: int,
+    sdk_error_type: type[openai.APIStatusError],
+    model_error_type: type[ModelError],
+    *,
+    is_retryable: bool,
+) -> None:
+    """Provider errors are raised as both the SDK type and the LangChain 
type."""
+    request = httpx2.Request("POST", 
"https://api.openai.com/v1/chat/completions";)
+    response = httpx2.Response(status_code, request=request)
+    sdk_error = sdk_error_type("model request failed", response=response, 
body=None)
+    model = ChatOpenAI(api_key=SecretStr("test"))
+
+    with patch.object(model.client, "with_raw_response") as mock_client:
+        mock_client.create.side_effect = sdk_error
+        with pytest.raises(sdk_error_type) as exc_info:
+            model.invoke("test")
+
+    assert isinstance(exc_info.value, model_error_type)
+    assert exc_info.value.is_retryable is is_retryable
+
+
+def test_openai_transport_error_classification() -> None:
+    """Timeout and connection failures are classified without a status code."""
+    request = httpx2.Request("POST", 
"https://api.openai.com/v1/chat/completions";)
+    model = ChatOpenAI(api_key=SecretStr("test"))
+
+    for sdk_error, model_error_type in (
+        (openai.APITimeoutError(request), ModelTimeoutError),
+        (openai.APIConnectionError(request=request), ModelConnectionError),
+    ):
+        with patch.object(model.client, "with_raw_response") as mock_client:
+            mock_client.create.side_effect = sdk_error
+            with pytest.raises(type(sdk_error)) as exc_info:
+                model.invoke("test")
+
+        assert isinstance(exc_info.value, model_error_type)
+        assert exc_info.value.is_retryable is True
+
+
 def test_metadata_versions() -> None:
     """Test that metadata reports the correct version info."""
     llm = ChatOpenAI()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_base_standard.py 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_base_standard.py
--- 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_base_standard.py   
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_base_standard.py   
    2020-02-02 01:00:00.000000000 +0100
@@ -12,6 +12,13 @@
         return ChatOpenAI
 
     @property
+    def chat_model_params(self) -> dict:
+        return {
+            "base_url": "https://api.openai.com/v1";,
+            "stream_usage": True,
+        }
+
+    @property
     def init_from_env_params(self) -> tuple[dict, dict, dict]:
         return (
             {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_responses_standard.py
 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_responses_standard.py
--- 
old/langchain_openai-1.5.1/tests/unit_tests/chat_models/test_responses_standard.py
  2020-02-02 01:00:00.000000000 +0100
+++ 
new/langchain_openai-1.6.0/tests/unit_tests/chat_models/test_responses_standard.py
  2020-02-02 01:00:00.000000000 +0100
@@ -13,7 +13,11 @@
 
     @property
     def chat_model_params(self) -> dict:
-        return {"use_responses_api": True}
+        return {
+            "use_responses_api": True,
+            "base_url": "https://api.openai.com/v1";,
+            "stream_usage": True,
+        }
 
     @property
     def init_from_env_params(self) -> tuple[dict, dict, dict]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_openai-1.5.1/uv.lock 
new/langchain_openai-1.6.0/uv.lock
--- old/langchain_openai-1.5.1/uv.lock  2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_openai-1.6.0/uv.lock  2020-02-02 01:00:00.000000000 +0100
@@ -707,7 +707,7 @@
 
 [[package]]
 name = "langchain-core"
-version = "1.5.4"
+version = "1.6.0"
 source = { editable = "../../core" }
 dependencies = [
     { name = "httpx" },
@@ -771,7 +771,7 @@
 
 [[package]]
 name = "langchain-openai"
-version = "1.5.1"
+version = "1.6.0"
 source = { editable = "." }
 dependencies = [
     { name = "certifi" },

Reply via email to