mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(deepseek): route strict mode to the beta endpoint (#40249)
Co-authored-by: Humberto Bernal <hbernalmendivil@gmail.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
2 files changed
+96
-9
No files matched your search
@@ -334,6 +334,19 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
def _resolve_model_profile(self) -> ModelProfile | None:
|
||||
return _get_default_model_profile(self.model_name) or None
|
||||
|
||||
def _with_beta_api_base(self) -> Self:
|
||||
"""Return a copy of this model that targets DeepSeek's beta endpoint."""
|
||||
beta_model = self.model_copy(
|
||||
update={
|
||||
"api_base": DEFAULT_BETA_API_BASE,
|
||||
"client": None,
|
||||
"async_client": None,
|
||||
"root_client": None,
|
||||
"root_async_client": None,
|
||||
}
|
||||
)
|
||||
return beta_model.validate_environment() # type: ignore[operator]
|
||||
|
||||
def _get_request_payload(
|
||||
self,
|
||||
input_: LanguageModelInput,
|
||||
@@ -521,7 +534,7 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
# If strict mode is enabled and using default API base, switch to beta endpoint
|
||||
if strict is True and self.api_base == DEFAULT_API_BASE:
|
||||
# Create a new instance with beta endpoint
|
||||
beta_model = self.model_copy(update={"api_base": DEFAULT_BETA_API_BASE})
|
||||
beta_model = self._with_beta_api_base()
|
||||
return beta_model.bind_tools(
|
||||
tools,
|
||||
tool_choice=tool_choice,
|
||||
@@ -627,7 +640,7 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
# If strict mode is enabled and using default API base, switch to beta endpoint
|
||||
if strict is True and self.api_base == DEFAULT_API_BASE:
|
||||
# Create a new instance with beta endpoint
|
||||
beta_model = self.model_copy(update={"api_base": DEFAULT_BETA_API_BASE})
|
||||
beta_model = self._with_beta_api_base()
|
||||
return beta_model.with_structured_output(
|
||||
schema,
|
||||
method=method,
|
||||
|
||||
@@ -14,7 +14,11 @@ from openai.types.chat.chat_completion import Choice
|
||||
from pydantic import BaseModel as PydanticBaseModel
|
||||
from pydantic import Field, SecretStr
|
||||
|
||||
from langchain_deepseek.chat_models import DEFAULT_API_BASE, ChatDeepSeek
|
||||
from langchain_deepseek.chat_models import (
|
||||
DEFAULT_API_BASE,
|
||||
DEFAULT_BETA_API_BASE,
|
||||
ChatDeepSeek,
|
||||
)
|
||||
|
||||
MODEL_NAME = "deepseek-chat"
|
||||
|
||||
@@ -262,6 +266,29 @@ class SampleTool(PydanticBaseModel):
|
||||
value: str = Field(description="A test value")
|
||||
|
||||
|
||||
_MAX_RUNNABLE_DEPTH = 6
|
||||
|
||||
|
||||
def _find_chat_model(runnable: Any, depth: int = 0) -> ChatDeepSeek | None:
|
||||
"""Walk a composed runnable and return the first `ChatDeepSeek` found."""
|
||||
if isinstance(runnable, ChatDeepSeek):
|
||||
return runnable
|
||||
if depth > _MAX_RUNNABLE_DEPTH:
|
||||
return None
|
||||
for attr in ("bound", "first", "last", "runnable", "steps", "steps__"):
|
||||
value = getattr(runnable, attr, None)
|
||||
if value is None:
|
||||
continue
|
||||
candidates = value if isinstance(value, (list, tuple)) else [value]
|
||||
if isinstance(value, dict):
|
||||
candidates = list(value.values())
|
||||
for candidate in candidates:
|
||||
found = _find_chat_model(candidate, depth + 1)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
class TestChatDeepSeekStrictMode:
|
||||
"""Tests for DeepSeek strict mode support.
|
||||
|
||||
@@ -283,10 +310,36 @@ class TestChatDeepSeekStrictMode:
|
||||
# Bind tools with strict=True
|
||||
bound_model = llm.bind_tools([SampleTool], strict=True)
|
||||
|
||||
# The bound model should have its internal model using beta endpoint
|
||||
# We can't directly access the internal model, but we can verify the behavior
|
||||
# by checking that the binding operation succeeds
|
||||
assert bound_model is not None
|
||||
# The bound model must target the beta endpoint, and so must the client
|
||||
# that actually issues the request — updating `api_base` alone leaves
|
||||
# the inherited `openai` clients pointing at the default base URL.
|
||||
beta_model = _find_chat_model(bound_model)
|
||||
assert beta_model is not None
|
||||
assert beta_model.api_base == DEFAULT_BETA_API_BASE
|
||||
assert str(beta_model.root_client.base_url).startswith(DEFAULT_BETA_API_BASE)
|
||||
assert str(beta_model.root_async_client.base_url).startswith(
|
||||
DEFAULT_BETA_API_BASE
|
||||
)
|
||||
|
||||
# The original model is left untouched
|
||||
assert llm.api_base == DEFAULT_API_BASE
|
||||
assert str(llm.root_client.base_url).startswith(DEFAULT_API_BASE)
|
||||
|
||||
def test_beta_copy_rebuilds_clients(self) -> None:
|
||||
"""The beta copy must use new clients."""
|
||||
llm = ChatDeepSeek(
|
||||
model="deepseek-chat",
|
||||
api_key=SecretStr("test_key"),
|
||||
)
|
||||
|
||||
beta_model = llm._with_beta_api_base()
|
||||
|
||||
assert beta_model.root_client is not llm.root_client
|
||||
assert beta_model.root_async_client is not llm.root_async_client
|
||||
assert str(beta_model.root_client.base_url).startswith(DEFAULT_BETA_API_BASE)
|
||||
assert str(beta_model.root_async_client.base_url).startswith(
|
||||
DEFAULT_BETA_API_BASE
|
||||
)
|
||||
|
||||
def test_bind_tools_without_strict_mode_uses_default_endpoint(self) -> None:
|
||||
"""Test bind_tools without strict or with strict=False uses default endpoint."""
|
||||
@@ -303,6 +356,19 @@ class TestChatDeepSeekStrictMode:
|
||||
bound_model_none = llm.bind_tools([SampleTool])
|
||||
assert bound_model_none is not None
|
||||
|
||||
def test_strict_mode_preserves_custom_api_base(self) -> None:
|
||||
"""A custom API base must bypass the DeepSeek beta endpoint."""
|
||||
llm = ChatDeepSeek(
|
||||
model="deepseek-chat",
|
||||
api_key=SecretStr("test_key"),
|
||||
base_url="https://proxy.example/v1",
|
||||
)
|
||||
|
||||
bound_model = llm.bind_tools([SampleTool], strict=True)
|
||||
|
||||
assert _find_chat_model(bound_model) is llm
|
||||
assert str(llm.root_client.base_url).startswith("https://proxy.example/v1")
|
||||
|
||||
def test_with_structured_output_strict_mode_uses_beta_endpoint(self) -> None:
|
||||
"""Test that with_structured_output with strict=True uses beta endpoint."""
|
||||
llm = ChatDeepSeek(
|
||||
@@ -316,8 +382,16 @@ class TestChatDeepSeekStrictMode:
|
||||
# Create structured output with strict=True
|
||||
structured_model = llm.with_structured_output(SampleTool, strict=True)
|
||||
|
||||
# The structured model should work with beta endpoint
|
||||
assert structured_model is not None
|
||||
# Walk the resulting runnable to the underlying model and assert that
|
||||
# the client it would call is pointed at the beta endpoint.
|
||||
beta_model = _find_chat_model(structured_model)
|
||||
assert beta_model is not None
|
||||
assert beta_model.api_base == DEFAULT_BETA_API_BASE
|
||||
assert str(beta_model.root_client.base_url).startswith(DEFAULT_BETA_API_BASE)
|
||||
|
||||
# The original model is left untouched
|
||||
assert llm.api_base == DEFAULT_API_BASE
|
||||
assert str(llm.root_client.base_url).startswith(DEFAULT_API_BASE)
|
||||
|
||||
|
||||
class TestChatDeepSeekAzureToolChoice:
|
||||
|
||||
Reference in new issue
Block a user