## Summary
- Fixes [CodeQL alert
#43](https://github.com/langchain-ai/langchain/security/code-scanning/43)
(CWE-20: incomplete URL substring sanitization)
- Replaces `"azure.com" in url` substring check with `urlparse`-based
hostname validation to prevent bypass via crafted URLs (e.g.,
`https://evil-azure.com`, `https://example.com/azure.com`)
- Adds bypass-attempt test cases to the existing Azure endpoint
detection tests
## Why
The substring check `"azure.com" in url` matches URLs where `azure.com`
appears anywhere in the string, not just in the hostname. An
attacker-controlled endpoint like `https://evil-azure.com` or
`https://example.com/azure.com` would incorrectly trigger the Azure code
path. Using `urlparse` to extract and validate the hostname is the
standard fix per CodeQL guidance.
## Test plan
- [x] Existing Azure endpoint detection tests pass
- [x] New negative test cases for bypass attempts pass
- [x] `uv run pytest tests/unit_tests/test_chat_models.py -k azure` —
6/6 passing
> [!NOTE]
> This PR was authored with assistance from an AI agent (Claude Code).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
**Description:** This PR adds support for DeepSeek's beta strict mode
feature for structured
outputs and tool calling. It overrides `bind_tools()` and
`with_structured_output()` to automatically use
DeepSeek's beta endpoint (https://api.deepseek.com/beta) when
`strict=True`. Both methods need overriding because they're independent
entry points and user can call either directly. When DeepSeek's strict
mode graduates from beta, we can just remove both overriden methods. You
can read more about the beta feature here:
https://api-docs.deepseek.com/guides/function_calling#strict-mode-beta
**Issue:** Implements #32670
**Dependencies:** None
**Sample Code**
```python
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field
from typing import Optional
import os
# Enter your DeepSeek API Key here
API_KEY = "YOUR_API_KEY"
# location, temperature, condition are required fields
# humidity is optional field with default value
class WeatherInfo(BaseModel):
location: str = Field(description="City name")
temperature: int = Field(description="Temperature in Celsius")
condition: str = Field(description="Weather condition (sunny, cloudy, rainy)")
humidity: Optional[int] = Field(default=None, description="Humidity percentage")
llm = ChatDeepSeek(
model="deepseek-chat",
api_key=API_KEY,
)
# just to confirm that a new instance will use the default base url (instead of beta)
print(f"Default API base: {llm.api_base}")
# Test 1: bind_tools with strict=True shoud list all the tools calls
print("\nTest 1: bind_tools with strict=True")
llm_with_tools = llm.bind_tools([WeatherInfo], strict=True)
response = llm_with_tools.invoke("Tell me the weather in New York. It's 22 degrees, sunny.")
print(response.tool_calls)
# Test 2: with_structured_output with strict=True
print("\nTest 2: with_structured_output with strict=True")
structured_llm = llm.with_structured_output(WeatherInfo, strict=True)
result = structured_llm.invoke("Tell me the weather in New York.")
print(f" Result: {result}")
assert isinstance(result, WeatherInfo), "Result should be a WeatherInfo instance"
```
---------
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
- Removes Codespell from deps, docs, and `Makefile`s
- Python version requirements in all `pyproject.toml` files now use the
`~=` (compatible release) specifier
- All dependency groups and main dependencies now use explicit lower and
upper bounds, reducing potential for breaking changes
## Description
When ChatDeepSeek invokes a tool that returns a list, it results in an
openai.UnprocessableEntityError due to a failure in deserializing the
JSON body.
The root of the problem is that ChatDeepSeek uses BaseChatOpenAI
internally, but the APIs are not identical: OpenAI v1/chat/completions
accepts arrays as tool results, but Deepseek API does not.
As a solution added `_get_request_payload` method to ChatDeepSeek, which
inherits the behavior from BaseChatOpenAI but adds a step to stringify
tool message content in case the content is an array. I also add a unit
test for this.
From the linked issue you can find the full reproducible example the
reporter of the issue provided. After the changes it works as expected.
Source: [Deepseek
docs](https://api-docs.deepseek.com/api/create-chat-completion/)

Source: [OpenAI
docs](https://platform.openai.com/docs/api-reference/chat/create)

## Issue
Fixes#31394
## Dependencies:
No new dependencies.
## Twitter handle:
Don't have one.
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Deepseek model does not return reasoning when hosted on openrouter
(Issue [30067](https://github.com/langchain-ai/langchain/issues/30067))
the following code did not return reasoning:
```python
llm = ChatDeepSeek( model = 'deepseek/deepseek-r1:nitro', api_base="https://openrouter.ai/api/v1", api_key=os.getenv("OPENROUTER_API_KEY"))
messages = [
{"role": "system", "content": "You are an assistant."},
{"role": "user", "content": "9.11 and 9.8, which is greater? Explain the reasoning behind this decision."}
]
response = llm.invoke(messages, extra_body={"include_reasoning": True})
print(response.content)
print(f"REASONING: {response.additional_kwargs.get('reasoning_content', '')}")
print(response)
```
The fix is to extract reasoning from
response.choices[0].message["model_extra"] and from
choices[0].delta["reasoning"]. and place in response additional_kwargs.
Change is really just the addition of a couple one-sentence if
statements.
---------
Co-authored-by: andrasfe <andrasf94@gmail.com>
Co-authored-by: Chester Curme <chester.curme@gmail.com>
1. Make `_convert_chunk_to_generation_chunk` an instance method on
BaseChatOpenAI
2. Override on ChatDeepSeek to add `"reasoning_content"` to message
additional_kwargs.
Resolves https://github.com/langchain-ai/langchain/issues/29513