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" },