mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
feat(deepseek): support strict beta structured output (#32727)
**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>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Literal, TypeAlias
|
||||
|
||||
@@ -12,15 +12,17 @@ from langchain_core.callbacks import (
|
||||
CallbackManagerForLLMRun,
|
||||
)
|
||||
from langchain_core.language_models import LangSmithParams, LanguageModelInput
|
||||
from langchain_core.messages import AIMessageChunk, BaseMessage
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, ChatResult
|
||||
from langchain_core.runnables import Runnable
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.utils import from_env, secret_from_env
|
||||
from langchain_openai.chat_models.base import BaseChatOpenAI
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
DEFAULT_API_BASE = "https://api.deepseek.com/v1"
|
||||
DEFAULT_BETA_API_BASE = "https://api.deepseek.com/beta"
|
||||
|
||||
_DictOrPydanticClass: TypeAlias = dict[str, Any] | type[BaseModel]
|
||||
_DictOrPydantic: TypeAlias = dict[str, Any] | BaseModel
|
||||
@@ -39,7 +41,7 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
|
||||
Key init args — completion params:
|
||||
model:
|
||||
Name of DeepSeek model to use, e.g. `"deepseek-chat"`.
|
||||
Name of DeepSeek model to use, e.g. `'deepseek-chat'`.
|
||||
temperature:
|
||||
Sampling temperature.
|
||||
max_tokens:
|
||||
@@ -368,6 +370,50 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
e.pos,
|
||||
) from e
|
||||
|
||||
def bind_tools(
|
||||
self,
|
||||
tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
|
||||
*,
|
||||
tool_choice: dict | str | bool | None = None,
|
||||
strict: bool | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, AIMessage]:
|
||||
"""Bind tool-like objects to this chat model.
|
||||
|
||||
Overrides parent to use beta endpoint when `strict=True`.
|
||||
|
||||
Args:
|
||||
tools: A list of tool definitions to bind to this chat model.
|
||||
tool_choice: Which tool to require the model to call.
|
||||
strict: If True, uses beta API for strict schema validation.
|
||||
parallel_tool_calls: Set to `False` to disable parallel tool use.
|
||||
**kwargs: Additional parameters passed to parent `bind_tools`.
|
||||
|
||||
Returns:
|
||||
A Runnable that takes same inputs as a chat model.
|
||||
"""
|
||||
# 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})
|
||||
return beta_model.bind_tools(
|
||||
tools,
|
||||
tool_choice=tool_choice,
|
||||
strict=strict,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Otherwise use parent implementation
|
||||
return super().bind_tools(
|
||||
tools,
|
||||
tool_choice=tool_choice,
|
||||
strict=strict,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def with_structured_output(
|
||||
self,
|
||||
schema: _DictOrPydanticClass | None = None,
|
||||
@@ -423,10 +469,14 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
|
||||
strict:
|
||||
Whether to enable strict schema adherence when generating the function
|
||||
call. This parameter is included for compatibility with other chat
|
||||
models, and if specified will be passed to the Chat Completions API
|
||||
in accordance with the OpenAI API specification. However, the DeepSeek
|
||||
API may ignore the parameter.
|
||||
call. When set to `True`, DeepSeek will use the beta API endpoint
|
||||
(`https://api.deepseek.com/beta`) for strict schema validation.
|
||||
This ensures model outputs exactly match the defined schema.
|
||||
|
||||
!!! note
|
||||
|
||||
DeepSeek's strict mode requires all object properties to be marked
|
||||
as required in the schema.
|
||||
|
||||
kwargs: Additional keyword args aren't supported.
|
||||
|
||||
@@ -448,6 +498,19 @@ class ChatDeepSeek(BaseChatOpenAI):
|
||||
# methods) be handled.
|
||||
if method == "json_schema":
|
||||
method = "function_calling"
|
||||
|
||||
# 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})
|
||||
return beta_model.with_structured_output(
|
||||
schema,
|
||||
method=method,
|
||||
include_raw=include_raw,
|
||||
strict=strict,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return super().with_structured_output(
|
||||
schema,
|
||||
method=method,
|
||||
|
||||
Reference in New Issue
Block a user