Skip to content

Commit 8a7d86b

Browse files
committed
Keep LangChain reasoning routing provider-safe
Review feedback showed that provider-based routing sent custom and other OpenAI-compatible providers through the DeepSeek LangChain integration. Restrict the DeepSeek-specific wrapper to the DeepSeek provider, preserve provider metadata from ModelService, and cover the routing boundaries in LangChain tests. Constraint: ChatDeepSeek is provider-specific while ChatOpenAI remains the generic OpenAI-compatible path. Rejected: Routing tongyi, zhipuai, moonshot, minimax, or custom through ChatDeepSeek | those providers are not guaranteed to follow DeepSeek integration semantics. Confidence: high Scope-risk: moderate Directive: Add provider-specific LangChain wrappers only when the provider integration is explicitly supported and tested. Tested: uv run --python 3.10 --all-extras pytest tests/unittests/integration/test_langchain.py tests/unittests/server/test_reasoning.py tests/unittests/server/test_openai_protocol.py::TestOpenAIReasoningContent tests/unittests/server/test_agui_protocol.py::TestAGUIReasoningContent Tested: uv run --python 3.10 --all-extras make mypy-check Tested: uv run --python 3.10 --all-extras make coverage Signed-off-by: congxiao.wxx <congxiao.wxx@alibaba-inc.com> Change-Id: I29d5dc26571cad77280860b8bb854bc1ad994f24
1 parent 09b94b1 commit 8a7d86b

6 files changed

Lines changed: 101 additions & 28 deletions

File tree

agentrun/integration/langchain/model_adapter.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,7 @@
99
)
1010
from agentrun.integration.utils.adapter import ModelAdapter
1111

12-
# 支持 reasoning_content 的供应商列表
13-
_REASONING_PROVIDERS = frozenset({
14-
"tongyi",
15-
"custom",
16-
"deepseek",
17-
"zhipuai",
18-
"moonshot",
19-
"minimax",
20-
})
12+
_DEEPSEEK_PROVIDER = "deepseek"
2113

2214

2315
class LangChainModelAdapter(ModelAdapter):
@@ -34,15 +26,23 @@ def wrap_model(self, common_model: Any) -> Any:
3426
info = common_model.get_model_info() # 确保模型可用
3527
provider = (info.provider or "").lower()
3628

37-
if provider in _REASONING_PROVIDERS:
29+
if provider == _DEEPSEEK_PROVIDER:
3830
return self._create_reasoning_model(info)
3931
return self._create_openai_model(info)
4032

4133
def _create_reasoning_model(self, info: Any) -> Any:
4234
"""创建支持 reasoning_content 的模型(使用 ChatDeepSeek)"""
43-
from langchain_deepseek import ChatDeepSeek
35+
try:
36+
from langchain_deepseek import ChatDeepSeek
37+
except ImportError as e:
38+
raise ImportError(
39+
"import langchain_deepseek failed. "
40+
"Install it with: pip install 'agentrun-sdk[langchain]' "
41+
"or pip install 'agentrun-sdk[langgraph]'"
42+
) from e
4443

4544
return ChatDeepSeek(
45+
name=info.model,
4646
model=info.model,
4747
api_key=info.api_key,
4848
api_base=info.base_url,
@@ -53,7 +53,14 @@ def _create_reasoning_model(self, info: Any) -> Any:
5353

5454
def _create_openai_model(self, info: Any) -> Any:
5555
"""创建标准 OpenAI 兼容模型"""
56-
from langchain_openai import ChatOpenAI
56+
try:
57+
from langchain_openai import ChatOpenAI
58+
except ImportError as e:
59+
raise ImportError(
60+
"import langchain_openai failed. "
61+
"Install it with: pip install 'agentrun-sdk[langchain]' "
62+
"or pip install 'agentrun-sdk[langgraph]'"
63+
) from e
5764

5865
return ChatOpenAI(
5966
name=info.model,

agentrun/model/__model_service_async_template.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,5 @@ def model_info(self, config: Optional[Config] = None) -> BaseInfo:
233233
base_url=self.provider_settings.base_url,
234234
model=default_model,
235235
headers=cfg.get_headers(),
236+
provider=self.provider,
236237
)

agentrun/model/model_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,5 @@ def model_info(self, config: Optional[Config] = None) -> BaseInfo:
404404
base_url=self.provider_settings.base_url,
405405
model=default_model,
406406
headers=cfg.get_headers(),
407+
provider=self.provider,
407408
)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ tablestore = [
7373
langgraph = [
7474
"langgraph>=0.2.0; python_version >= '3.10'",
7575
"langchain-core>=0.3.0; python_version >= '3.10'",
76+
"langchain-openai>=1.0.0; python_version >= '3.10'",
77+
"langchain-deepseek>=1.0.1; python_version >= '3.10'",
7678
]
7779

7880
[dependency-groups]

scripts/smoke_reasoning_protocol.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,12 @@
66
import os
77
from typing import Any, Dict, Iterable, List, Optional
88

9-
import httpx
109
from dotenv import load_dotenv
10+
import httpx
1111

1212
from agentrun.model import BackendType, ModelClient
1313
from agentrun.model.api.data import ModelDataAPI
14-
from agentrun.server import (
15-
AgentEvent,
16-
AgentRequest,
17-
AgentRunServer,
18-
EventType,
19-
)
14+
from agentrun.server import AgentEvent, AgentRequest, AgentRunServer, EventType
2015
from agentrun.server.agui_protocol import AGUIProtocolHandler
2116
from agentrun.server.openai_protocol import OpenAIProtocolHandler
2217
from agentrun.utils.reasoning import (
@@ -236,8 +231,7 @@ async def call_real_model(
236231

237232
if response.is_error:
238233
raise RuntimeError(
239-
f"real model request failed: {response.status_code} "
240-
f"{response.text}"
234+
f"real model request failed: {response.status_code} {response.text}"
241235
)
242236

243237
if not stream:
@@ -257,13 +251,16 @@ def resolve_model_endpoint(
257251
raise RuntimeError(
258252
f"model service {model_resource} has no provider settings"
259253
)
254+
default_model = (
255+
settings.model_names[0] if settings.model_names else None
256+
)
260257
return (
261258
settings.base_url,
262259
{
263260
"authorization": f"Bearer {settings.api_key}",
264261
"content-type": "application/json",
265262
},
266-
(settings.model_names or [None])[0],
263+
default_model,
267264
)
268265

269266
data_api = ModelDataAPI(model_resource)
@@ -398,12 +395,18 @@ async def main() -> None:
398395
validate_result("agui", content, reasoning, args)
399396
results["agui"] = summarize(content, reasoning, raw)
400397

401-
print(json.dumps({
402-
"thinkingEnabled": is_thinking_enabled_from_env(),
403-
"protocol": args.protocol,
404-
"responseMode": args.response_mode,
405-
"results": results,
406-
}, ensure_ascii=False, indent=2))
398+
print(
399+
json.dumps(
400+
{
401+
"thinkingEnabled": is_thinking_enabled_from_env(),
402+
"protocol": args.protocol,
403+
"responseMode": args.response_mode,
404+
"results": results,
405+
},
406+
ensure_ascii=False,
407+
indent=2,
408+
)
409+
)
407410

408411

409412
def summarize(content: str, reasoning: str, raw: Any) -> Dict[str, Any]:

tests/unittests/integration/test_langchain.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
from agentrun.integration.builtin.model import CommonModel
1717
from agentrun.integration.utils.tool import CommonToolSet, tool
18+
from agentrun.model import ModelService, ProviderSettings
1819
from agentrun.model.model_proxy import ModelProxy
20+
from agentrun.utils.config import Config
1921

2022
from .base import IntegrationTestBase, IntegrationTestResult, ToolCallInfo
2123
from .mock_llm_server import MockLLMServer
@@ -173,6 +175,20 @@ def _msg_to_dict(self, msg: Any) -> dict:
173175
class TestLangChainIntegration(LangChainTestMixin):
174176
"""LangChain Integration 测试类"""
175177

178+
def _model_service_model(self, provider: Optional[str]) -> CommonModel:
179+
return CommonModel(
180+
ModelService(
181+
model_service_name=f"{provider or 'default'}-service",
182+
provider=provider,
183+
provider_settings=ProviderSettings(
184+
api_key="sk-test",
185+
base_url="https://model.example/v1",
186+
model_names=["test-model"],
187+
),
188+
),
189+
config=Config(headers={"x-test-header": "yes"}),
190+
)
191+
176192
@pytest.fixture
177193
def mock_server(self, monkeypatch: Any, respx_mock: Any) -> MockLLMServer:
178194
"""创建并安装 Mock LLM Server
@@ -320,6 +336,49 @@ def test_stream_options_in_requests(
320336
assert llm.stream_usage is True
321337
assert llm.streaming is True
322338

339+
def test_model_service_model_info_exposes_provider(self):
340+
model = self._model_service_model("deepseek")
341+
342+
assert model.get_model_info().provider == "deepseek"
343+
344+
def test_deepseek_provider_uses_chat_deepseek(self):
345+
from langchain_deepseek import ChatDeepSeek
346+
347+
model = self._model_service_model("deepseek")
348+
349+
llm = model.to_langchain()
350+
351+
assert isinstance(llm, ChatDeepSeek)
352+
assert llm.name == "test-model"
353+
assert llm.model_name == "test-model"
354+
assert llm.api_base == "https://model.example/v1"
355+
assert llm.default_headers == {"x-test-header": "yes"}
356+
assert llm.openai_api_key.get_secret_value() == "sk-test"
357+
assert llm.stream_usage is True
358+
assert llm.streaming is True
359+
360+
@pytest.mark.parametrize(
361+
"provider",
362+
[None, "custom", "tongyi", "zhipuai", "moonshot", "minimax", "unknown"],
363+
)
364+
def test_non_deepseek_providers_use_chat_openai(
365+
self, provider: Optional[str]
366+
):
367+
from langchain_openai import ChatOpenAI
368+
369+
model = self._model_service_model(provider)
370+
371+
llm = model.to_langchain()
372+
373+
assert isinstance(llm, ChatOpenAI)
374+
assert llm.name == "test-model"
375+
assert llm.model_name == "test-model"
376+
assert llm.openai_api_base == "https://model.example/v1"
377+
assert llm.default_headers == {"x-test-header": "yes"}
378+
assert llm.openai_api_key.get_secret_value() == "sk-test"
379+
assert llm.stream_usage is True
380+
assert llm.streaming is True
381+
323382
def test_stream_options_validation(
324383
self,
325384
mock_server: MockLLMServer,

0 commit comments

Comments
 (0)