Compare commits

..

3 Commits

Author SHA1 Message Date
ccurme
8e35924083 fix(openai): sanitize chat completions text content blocks (#35217) 2026-02-15 15:31:02 -05:00
nightcityblade
ecac3d891c fix(openai): improve error message for null choices in OpenAI-compatible APIs (#35236) 2026-02-15 10:59:04 -05:00
Mason Daugherty
9a2a10ec21 fix(infra): create GitHub releases for partner package releases (#35234)
- GitHub releases have not been created for partner package releases
since #34784 (Jan 16). PyPI publishes were unaffected.

#34784 added `test-dependents` to the `publish` job's dependency chain.
`test-dependents` only runs for core/langchain releases, so it's skipped
for everything else. `publish` handles this with `if: ${{ !cancelled()
&& !failure() }}`, but `mark-release` (which creates the GitHub release)
doesn't have the same guard — so GitHub Actions skips it whenever
`test-dependents` is skipped.

## Missing GitHub releases
`langchain-xai==1.2.2`, `langchain-standard-tests==1.1.3`,
`langchain-groq==1.1.2`, `langchain-anthropic==1.3.2`,
`langchain-standard-tests==1.1.4`, `langchain-openai==1.1.8`,
`langchain-openai==1.1.9`, `langchain-anthropic==1.3.3`,
`langchain-openrouter==0.0.2`
2026-02-15 04:27:26 -05:00
5 changed files with 53 additions and 3 deletions

View File

@@ -586,6 +586,8 @@ jobs:
- test-pypi-publish
- pre-release-checks
- publish
# Run if all needed jobs succeeded or were skipped (test-dependents only runs for core/langchain_v1)
if: ${{ !cancelled() && !failure() }}
runs-on: ubuntu-latest
permissions:
# This permission is needed by `ncipollo/release-action` to

View File

@@ -219,6 +219,7 @@ jobs:
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
PPLX_API_KEY: ${{ secrets.PPLX_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
with:

View File

@@ -236,6 +236,26 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
return ChatMessage(content=_dict.get("content", ""), role=role, id=id_) # type: ignore[arg-type]
def _sanitize_chat_completions_content(content: str | list[dict]) -> str | list[dict]:
"""Sanitize content for chat/completions API.
For list content, filters text blocks to only keep 'type' and 'text' keys.
"""
if isinstance(content, list):
sanitized = []
for block in content:
if (
isinstance(block, dict)
and block.get("type") == "text"
and "text" in block
):
sanitized.append({"type": "text", "text": block["text"]})
else:
sanitized.append(block)
return sanitized
return content
def _format_message_content(
content: Any,
api: Literal["chat/completions", "responses"] = "chat/completions",
@@ -368,7 +388,9 @@ def _convert_message_to_dict(
elif isinstance(message, ToolMessage):
message_dict["role"] = "tool"
message_dict["tool_call_id"] = message.tool_call_id
message_dict["content"] = _sanitize_chat_completions_content(
message_dict["content"]
)
supported_props = {"content", "role", "tool_call_id"}
message_dict = {k: v for k, v in message_dict.items() if k in supported_props}
else:
@@ -1520,7 +1542,15 @@ class BaseChatOpenAI(BaseChatModel):
raise KeyError(msg) from e
if choices is None:
msg = "Received response with null value for `choices`."
# Some OpenAI-compatible APIs (e.g., vLLM) may return null choices
# when the response format differs or an error occurs without
# populating the error field. Provide a more helpful error message.
msg = (
"Received response with null value for `choices`. "
"This can happen when using OpenAI-compatible APIs (e.g., vLLM) "
"that return a response in an unexpected format. "
f"Full response keys: {list(response_dict.keys())}"
)
raise TypeError(msg)
token_usage = response_dict.get("usage")

View File

@@ -1264,6 +1264,23 @@ def test__get_request_payload() -> None:
assert payload == expected
def test_sanitize_chat_completions_text_blocks() -> None:
messages = [
ToolMessage(
content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],
tool_call_id="def456",
),
]
payload = ChatOpenAI(model="gpt-5.2")._get_request_payload(messages)
assert payload["messages"] == [
{
"content": [{"type": "text", "text": "foo"}],
"role": "tool",
"tool_call_id": "def456",
}
]
def test_init_o1() -> None:
with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("error") # Treat warnings as errors

View File

@@ -239,7 +239,7 @@ class ChatOpenRouter(BaseChatModel):
"""
route: str | None = None
"""Route preference for OpenRouter. E.g. `'fallback'`."""
"""Route preference for OpenRouter, e.g. `'fallback'`."""
plugins: list[dict[str, Any]] | None = None
"""Plugins configuration for OpenRouter."""