mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-05 15:20:30 +08:00
Feat/agent thinking switch (#15446)
### What problem does this PR solve? This PR adds an Agent LLM setting to control thinking mode for official providers that expose a thinking switch. Related to #12842. Closes #15445. Some providers expose thinking controls through provider-specific request fields, but Agent LLM settings did not have a unified option for users to enable or disable thinking mode. This PR adds a `Thinking` selector with: - System default - Enabled - Disabled <img width="452" height="278" alt="8566b0b4-0546-4c8a-913d-f9bbd38319f6" src="https://github.com/user-attachments/assets/25b497f7-1ba0-4bfe-940d-6fe79287d6ab" /> <img width="471" height="971" alt="8a0a6bee-f45f-48d5-bd83-17af260de3db" src="https://github.com/user-attachments/assets/41ad43c1-5087-48f1-bf37-f2ca14c2be2f" /> Initial support is limited to the verified official providers: - Qwen / DashScope: `enable_thinking` - Kimi / Moonshot: `thinking.type` - GLM / ZHIPU-AI: `thinking.type` For LiteLLM-based providers, provider-specific fields are forwarded through `extra_body` before `drop_params` filtering so the request parameters are preserved. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: jiashi <jiashi19@outlook.com> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
This commit is contained in:
@@ -95,10 +95,12 @@ ALLOWED_GEN_CONF_KEYS = frozenset(
|
||||
|
||||
# LiteLLM additionally understands reasoning-control parameters that the
|
||||
# model-family policies may inject into `gen_conf` (e.g. `thinking` for
|
||||
# Anthropic / Kimi reasoning models, `reasoning_effort` for OpenAI o-series).
|
||||
# Anthropic / Kimi reasoning models, `enable_thinking` for Qwen models,
|
||||
# `reasoning_effort` for OpenAI o-series).
|
||||
LITELLM_ALLOWED_GEN_CONF_KEYS = ALLOWED_GEN_CONF_KEYS | frozenset(
|
||||
{
|
||||
"thinking",
|
||||
"enable_thinking",
|
||||
"reasoning_effort",
|
||||
"extra_body",
|
||||
}
|
||||
@@ -117,9 +119,43 @@ def _apply_model_family_policies(
|
||||
sanitized_gen_conf = deepcopy(gen_conf) if gen_conf else {}
|
||||
sanitized_kwargs = dict(request_kwargs) if request_kwargs else {}
|
||||
|
||||
# Qwen3 family disables thinking by extra_body on non-stream chat requests.
|
||||
def _thinking_type():
|
||||
val = sanitized_gen_conf.get("thinking")
|
||||
if isinstance(val, dict):
|
||||
val = val.get("type")
|
||||
|
||||
enable_thinking = sanitized_gen_conf.get("enable_thinking")
|
||||
|
||||
if isinstance(val, str) and val in {"enabled", "disabled"}:
|
||||
return val
|
||||
if isinstance(enable_thinking, bool):
|
||||
return "enabled" if enable_thinking else "disabled"
|
||||
return None
|
||||
|
||||
def _pop_thinking_controls():
|
||||
sanitized_gen_conf.pop("thinking", None)
|
||||
sanitized_gen_conf.pop("enable_thinking", None)
|
||||
|
||||
def _merge_extra_body(target: dict, extra: dict) -> None:
|
||||
body = target.get("extra_body")
|
||||
if not isinstance(body, dict):
|
||||
body = {}
|
||||
body.update(extra)
|
||||
target["extra_body"] = body
|
||||
|
||||
thinking_type = _thinking_type()
|
||||
|
||||
# Qwen3 keeps RAGFlow's system default of disabling thinking unless explicitly overridden.
|
||||
if "qwen3" in model_name_lower:
|
||||
sanitized_kwargs["extra_body"] = {"enable_thinking": False}
|
||||
_pop_thinking_controls()
|
||||
enable_thinking = thinking_type == "enabled" if thinking_type else False
|
||||
if backend == "litellm" and provider in {
|
||||
SupportedLiteLLMProvider.Tongyi_Qianwen,
|
||||
SupportedLiteLLMProvider.Dashscope,
|
||||
}:
|
||||
sanitized_gen_conf["enable_thinking"] = enable_thinking
|
||||
else:
|
||||
_merge_extra_body(sanitized_kwargs, {"enable_thinking": enable_thinking})
|
||||
|
||||
if backend == "base":
|
||||
return sanitized_gen_conf, sanitized_kwargs
|
||||
@@ -137,27 +173,50 @@ def _apply_model_family_policies(
|
||||
if provider == SupportedLiteLLMProvider.HunYuan:
|
||||
for key in ("presence_penalty", "frequency_penalty"):
|
||||
sanitized_gen_conf.pop(key, None)
|
||||
elif "kimi-k2.5" in model_name_lower or "kimi-k2.6" in model_name_lower:
|
||||
reasoning = sanitized_gen_conf.pop("reasoning", None)
|
||||
thinking = {"type": "enabled"}
|
||||
if reasoning is not None:
|
||||
thinking = {"type": "enabled"} if reasoning else {"type": "disabled"}
|
||||
elif not isinstance(thinking, dict) or thinking.get("type") not in {"enabled", "disabled"}:
|
||||
thinking = {"type": "disabled"}
|
||||
sanitized_gen_conf["thinking"] = thinking
|
||||
elif provider == SupportedLiteLLMProvider.Moonshot:
|
||||
if thinking_type:
|
||||
_pop_thinking_controls()
|
||||
sanitized_gen_conf["thinking"] = {"type": thinking_type}
|
||||
|
||||
thinking_enabled = thinking.get("type") == "enabled"
|
||||
sanitized_gen_conf["temperature"] = 1.0 if thinking_enabled else 0.6
|
||||
sanitized_gen_conf["top_p"] = 0.95
|
||||
sanitized_gen_conf["n"] = 1
|
||||
sanitized_gen_conf["presence_penalty"] = 0.0
|
||||
sanitized_gen_conf["frequency_penalty"] = 0.0
|
||||
if thinking_type or "kimi-k2.5" in model_name_lower or "kimi-k2.6" in model_name_lower:
|
||||
sanitized_gen_conf.pop("temperature", None)
|
||||
sanitized_gen_conf["top_p"] = 0.95
|
||||
sanitized_gen_conf["n"] = 1
|
||||
sanitized_gen_conf["presence_penalty"] = 0.0
|
||||
sanitized_gen_conf["frequency_penalty"] = 0.0
|
||||
elif (
|
||||
provider == SupportedLiteLLMProvider.ZHIPU_AI
|
||||
and "glm" in model_name_lower
|
||||
and thinking_type
|
||||
):
|
||||
_pop_thinking_controls()
|
||||
sanitized_gen_conf["thinking"] = {"type": thinking_type}
|
||||
|
||||
return sanitized_gen_conf, sanitized_kwargs
|
||||
|
||||
return sanitized_gen_conf, sanitized_kwargs
|
||||
|
||||
|
||||
def _move_litellm_provider_body_fields(provider: SupportedLiteLLMProvider | str | None, completion_args: dict) -> dict:
|
||||
provider_body_fields = {
|
||||
SupportedLiteLLMProvider.Tongyi_Qianwen: {"enable_thinking"},
|
||||
SupportedLiteLLMProvider.Dashscope: {"enable_thinking"},
|
||||
SupportedLiteLLMProvider.Moonshot: {"thinking"},
|
||||
SupportedLiteLLMProvider.ZHIPU_AI: {"thinking"},
|
||||
}.get(provider, set())
|
||||
|
||||
body = completion_args.get("extra_body")
|
||||
if not isinstance(body, dict):
|
||||
body = {}
|
||||
moved = False
|
||||
for key in provider_body_fields:
|
||||
if key in completion_args:
|
||||
body[key] = completion_args.pop(key)
|
||||
moved = True
|
||||
if moved or body:
|
||||
completion_args["extra_body"] = body
|
||||
return completion_args
|
||||
|
||||
class Base(ABC):
|
||||
def __init__(self, key, model_name, base_url, **kwargs):
|
||||
timeout = int(os.environ.get("LLM_TIMEOUT_SECONDS", 600))
|
||||
@@ -197,12 +256,6 @@ class Base(ABC):
|
||||
return LLMErrorCode.ERROR_GENERIC
|
||||
|
||||
def _clean_conf(self, gen_conf):
|
||||
gen_conf, _ = _apply_model_family_policies(
|
||||
self.model_name,
|
||||
backend="base",
|
||||
gen_conf=gen_conf,
|
||||
)
|
||||
|
||||
if "max_tokens" in gen_conf:
|
||||
del gen_conf["max_tokens"]
|
||||
|
||||
@@ -213,10 +266,17 @@ class Base(ABC):
|
||||
logging.info("[HISTORY STREAMLY]" + json.dumps(history, ensure_ascii=False, indent=4))
|
||||
reasoning_start = False
|
||||
|
||||
gen_conf, extra_request_kwargs = _apply_model_family_policies(
|
||||
self.model_name,
|
||||
backend="base",
|
||||
gen_conf=gen_conf,
|
||||
request_kwargs={},
|
||||
)
|
||||
request_kwargs = {"model": self.model_name, "messages": history, "stream": True, **gen_conf}
|
||||
stop = kwargs.get("stop")
|
||||
if stop:
|
||||
request_kwargs["stop"] = stop
|
||||
request_kwargs.update(extra_request_kwargs)
|
||||
|
||||
response = await self.async_client.chat.completions.create(**request_kwargs)
|
||||
async for resp in response:
|
||||
@@ -407,6 +467,12 @@ class Base(ABC):
|
||||
async def async_chat_with_tools(self, system: str, history: list, gen_conf: dict | None = None):
|
||||
gen_conf = dict(gen_conf or {})
|
||||
gen_conf = self._clean_conf(gen_conf)
|
||||
gen_conf, extra_request_kwargs = _apply_model_family_policies(
|
||||
self.model_name,
|
||||
backend="base",
|
||||
gen_conf=gen_conf,
|
||||
request_kwargs={},
|
||||
)
|
||||
if system and history and history[0].get("role") != "system":
|
||||
history.insert(0, {"role": "system", "content": system})
|
||||
|
||||
@@ -418,7 +484,7 @@ class Base(ABC):
|
||||
try:
|
||||
for _ in range(self.max_rounds + 1):
|
||||
logging.info(f"{self.tools=}")
|
||||
response = await self.async_client.chat.completions.create(model=self.model_name, messages=history, tools=self.tools, tool_choice="auto", **gen_conf)
|
||||
response = await self.async_client.chat.completions.create(model=self.model_name, messages=history, tools=self.tools, tool_choice="auto", **gen_conf, **extra_request_kwargs)
|
||||
tk_count += total_token_count_from_response(response)
|
||||
if not response.choices or not response.choices[0].message:
|
||||
raise Exception(f"500 response structure error. Response: {response}")
|
||||
@@ -473,6 +539,12 @@ class Base(ABC):
|
||||
async def async_chat_streamly_with_tools(self, system: str, history: list, gen_conf: dict | None = None):
|
||||
gen_conf = dict(gen_conf or {})
|
||||
gen_conf = self._clean_conf(gen_conf)
|
||||
gen_conf, extra_request_kwargs = _apply_model_family_policies(
|
||||
self.model_name,
|
||||
backend="base",
|
||||
gen_conf=gen_conf,
|
||||
request_kwargs={},
|
||||
)
|
||||
tools = self.tools
|
||||
if system and history and history[0].get("role") != "system":
|
||||
history.insert(0, {"role": "system", "content": system})
|
||||
@@ -487,7 +559,7 @@ class Base(ABC):
|
||||
reasoning_start = False
|
||||
logging.info(f"[ToolLoop] round={_round} model={self.model_name} tools={[t['function']['name'] for t in tools]}")
|
||||
|
||||
response = await self.async_client.chat.completions.create(model=self.model_name, messages=history, stream=True, tools=tools, tool_choice="auto", **gen_conf)
|
||||
response = await self.async_client.chat.completions.create(model=self.model_name, messages=history, stream=True, tools=tools, tool_choice="auto", **gen_conf, **extra_request_kwargs)
|
||||
|
||||
final_tool_calls = {}
|
||||
answer = ""
|
||||
@@ -573,7 +645,15 @@ class Base(ABC):
|
||||
logging.warning(f"Exceed max rounds: {self.max_rounds}")
|
||||
history.append({"role": "user", "content": f"Exceed max rounds: {self.max_rounds}"})
|
||||
|
||||
response = await self.async_client.chat.completions.create(model=self.model_name, messages=history, stream=True, tools=tools, tool_choice="auto", **gen_conf)
|
||||
response = await self.async_client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=history,
|
||||
stream=True,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
**gen_conf,
|
||||
**extra_request_kwargs,
|
||||
)
|
||||
|
||||
async for resp in response:
|
||||
if not hasattr(resp, "choices") or not resp.choices:
|
||||
@@ -619,9 +699,10 @@ class Base(ABC):
|
||||
|
||||
return final_ans.strip(), tol_token
|
||||
|
||||
_, kwargs = _apply_model_family_policies(
|
||||
gen_conf, kwargs = _apply_model_family_policies(
|
||||
self.model_name,
|
||||
backend="base",
|
||||
gen_conf=gen_conf,
|
||||
request_kwargs=kwargs,
|
||||
)
|
||||
|
||||
@@ -2080,6 +2161,7 @@ class LiteLLMBase(ABC):
|
||||
api_base = completion_args.get("api_base", self.base_url)
|
||||
separator = "&" if "?" in api_base else "?"
|
||||
completion_args["api_base"] = f"{api_base}{separator}GroupId={self.group_id}"
|
||||
_move_litellm_provider_body_fields(self.provider, completion_args)
|
||||
if extra_headers:
|
||||
completion_args["extra_headers"] = extra_headers
|
||||
return completion_args
|
||||
|
||||
Reference in New Issue
Block a user