core/agent: send the resolved reasoning effort with each request

This commit is contained in:
2026-10-06 21:07:23 +08:00
parent 445aba7e5f
commit fb8ef88d9f
9 changed files with 225 additions and 6 deletions
+6
View File
@@ -287,6 +287,8 @@ class ChatAgent:
session_id: int | None
max_rounds: int
temperature: float | None
# Resolved thinking strength ("" = provider default); see config.reasoning.
reasoning_effort: str
on_limit: LimitContinueHook | None
stream: bool
system_prompt: str | None
@@ -318,6 +320,7 @@ class ChatAgent:
session_id: int | None = None,
max_rounds: int = DEFAULT_MAX_ROUNDS,
temperature: float | None = None,
reasoning_effort: str = "",
messages: Sequence[AnyChatMessage] | None = None,
on_limit: LimitContinueHook | None = None,
stream: bool = True,
@@ -340,6 +343,7 @@ class ChatAgent:
self.session_id = session_id
self.max_rounds = max_rounds
self.temperature = temperature
self.reasoning_effort = reasoning_effort
self.on_limit = on_limit
self.stream = stream
self.system_prompt = system_prompt
@@ -597,6 +601,7 @@ class ChatAgent:
tools=self.tools,
max_rounds=self.max_rounds,
temperature=self.temperature,
reasoning_effort=self.reasoning_effort,
on_limit=self.on_limit,
stream=self.stream,
max_tool_result_chars=self.max_tool_result_chars,
@@ -740,6 +745,7 @@ class ChatAgent:
session_id=None,
max_rounds=rounds,
temperature=self.temperature,
reasoning_effort=self.reasoning_effort,
on_limit=self.on_limit,
stream=self.stream,
system_prompt=self.system_prompt if system_prompt is None else system_prompt,
+9 -1
View File
@@ -57,7 +57,7 @@ if TYPE_CHECKING:
from plyngent.lmproto.anthropic.client import AnthropicClient
from plyngent.lmproto.openai.client import OpenAIClient
from plyngent.lmproto.openai_compatible.model import AnyChatMessage, AnyToolItem
from plyngent.lmproto.openai_compatible.model import AnyChatMessage, AnyToolItem, ReasoningEffort
from .todo_nag import TodoNagStrategy
from .todo_stack import TodoStack
@@ -418,6 +418,7 @@ async def run_chat_loop( # noqa: C901, PLR0912 — multi-phase tool loop
tools: ToolRegistry | None = None,
max_rounds: int = DEFAULT_MAX_ROUNDS,
temperature: float | None = None,
reasoning_effort: str = "",
on_limit: LimitContinueHook | None = None,
stream: bool = True,
max_tool_result_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS,
@@ -438,6 +439,10 @@ async def run_chat_loop( # noqa: C901, PLR0912 — multi-phase tool loop
Multiple tool calls in one round run in parallel when ``parallel_tools``.
Request payloads may shrink older tool results when over ``max_context_tokens``.
*reasoning_effort* is the resolved thinking strength (``""`` = send nothing,
the provider's own default); each API surface maps it onto its own wire field
(see ``config.reasoning``).
*provider_tools* are hosted/provider-side tools (OpenAI Responses only, e.g.
``web_search``) merged into the request as opaque dicts; never executed by the
:class:`ToolRegistry`. Ignored by non-Responses clients.
@@ -482,6 +487,9 @@ async def run_chat_loop( # noqa: C901, PLR0912 — multi-phase tool loop
model=model,
temperature=temperature if temperature is not None else UNSET,
tools=list(tool_items) if tool_items is not None else UNSET,
# "max" is DeepSeek-only; config.reasoning already clamped it away
# for every other surface, so this cast does not hide a mistake.
reasoning_effort=cast("ReasoningEffort", reasoning_effort) if reasoning_effort else UNSET,
)
pre_len = len(messages)
+75 -3
View File
@@ -19,6 +19,7 @@ from plyngent.lmproto.anthropic.model import (
AnthropicMessagesParam,
AnthropicResponseText,
AnthropicTextContent,
AnthropicThinkingConfig,
AnthropicToolChoice,
AnthropicToolDefinition,
AnthropicToolResultContent,
@@ -273,16 +274,87 @@ def _max_tokens_from_param(param: ChatCompletionsParam) -> int:
return 8192
def chat_param_to_anthropic_param(param: ChatCompletionsParam) -> AnthropicMessagesParam:
"""Build :class:`AnthropicMessagesParam` from a chat completions param."""
# Effort level → Anthropic ``thinking`` budget (an effort has no wire form there).
# Unknown levels fall back to "medium"; ``none`` is handled separately (disabled).
_ANTHROPIC_THINKING_BUDGETS: dict[str, int] = {
"minimal": 1024,
"low": 4096,
"medium": 8192,
"high": 16384,
"xhigh": 32768,
"max": 32768,
}
# Anthropic rejects ``budget_tokens < 1024`` and ``budget_tokens >= max_tokens``.
_ANTHROPIC_MIN_THINKING_BUDGET = 1024
# Headroom left for the answer when a thinking budget has to raise max_tokens.
_ANTHROPIC_MIN_ANSWER_TOKENS = 1024
def _thinking_from_param(
param: ChatCompletionsParam,
*,
thinking_budget_tokens: int,
) -> AnthropicThinkingConfig | Unset:
"""Build the ``thinking`` block for *param* (UNSET = send nothing).
``reasoning_effort = "none"`` disables thinking; any other level enables it
with the explicit *thinking_budget_tokens*, else with the budget that level
maps onto.
"""
effort = param.reasoning_effort
if effort is UNSET:
return UNSET
if effort == "none":
return AnthropicThinkingConfig(type="disabled")
fallback = _ANTHROPIC_THINKING_BUDGETS["medium"]
budget = thinking_budget_tokens or _ANTHROPIC_THINKING_BUDGETS.get(str(effort), fallback)
return AnthropicThinkingConfig(
type="enabled",
budget_tokens=max(_ANTHROPIC_MIN_THINKING_BUDGET, int(budget)),
)
def _thinking_and_max_tokens(
param: ChatCompletionsParam,
*,
thinking_budget_tokens: int,
) -> tuple[int, AnthropicThinkingConfig | Unset]:
"""Resolve the ``thinking`` block and the ``max_tokens`` that has to fit it.
Anthropic rejects ``budget_tokens >= max_tokens``; rather than silently
shrink a requested reasoning budget, raise ``max_tokens`` so the answer still
has room.
"""
max_tokens = _max_tokens_from_param(param)
thinking = _thinking_from_param(param, thinking_budget_tokens=thinking_budget_tokens)
if thinking is not UNSET and thinking.budget_tokens is not UNSET:
budget = int(thinking.budget_tokens)
if budget >= max_tokens:
max_tokens = budget + _ANTHROPIC_MIN_ANSWER_TOKENS
return max_tokens, thinking
def chat_param_to_anthropic_param(
param: ChatCompletionsParam,
*,
thinking_budget_tokens: int = 0,
) -> AnthropicMessagesParam:
"""Build :class:`AnthropicMessagesParam` from a chat completions param.
*thinking_budget_tokens* is the configured explicit ``thinking`` budget
(0 = derive it from the param's ``reasoning_effort``).
"""
system, messages = chat_messages_to_anthropic(param.messages)
tools = tool_items_to_anthropic_tools(param.tools if param.tools is not UNSET else None)
max_tokens, thinking = _thinking_and_max_tokens(param, thinking_budget_tokens=thinking_budget_tokens)
kwargs: dict[str, Any] = {
"model": param.model,
"max_tokens": _max_tokens_from_param(param),
"max_tokens": max_tokens,
"messages": messages,
}
if thinking is not UNSET:
kwargs["thinking"] = thinking
if system:
kwargs["system"] = system
if tools:
+7 -2
View File
@@ -44,6 +44,11 @@ if TYPE_CHECKING:
)
def _thinking_budget(client: AnthropicClient) -> int:
"""Configured explicit ``thinking`` budget on *client* (0 = derive from effort)."""
return max(0, int(getattr(client, "thinking_budget_tokens", 0)))
class _ToolBlockState:
"""Tracks an in-flight tool_use content block during SSE streaming."""
@@ -219,7 +224,7 @@ async def _stream_as_chat_chunks(
``message_stop``. Errors raise ``RuntimeError`` so the agent loop can
surface them as retryable failures.
"""
create = chat_param_to_anthropic_param(param)
create = chat_param_to_anthropic_param(param, thinking_budget_tokens=_thinking_budget(client))
stream = await client.messages(create, stream=True)
state = _StreamState(param.model)
@@ -249,7 +254,7 @@ async def dispatch_messages(
stream: bool = False,
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
"""Run one Anthropic Messages turn and return a chat-completions-shaped result."""
create = chat_param_to_anthropic_param(param)
create = chat_param_to_anthropic_param(param, thinking_budget_tokens=_thinking_budget(client))
if stream:
return _stream_as_chat_chunks(client, param)
response = await client.messages(create, stream=False)
+3
View File
@@ -16,6 +16,7 @@ from plyngent.lmproto.openai.model import (
ResponseEasyInputMessage,
ResponseFunctionTool,
ResponseFunctionToolCallOutput,
ResponseReasoningConfig,
response_function_calls,
response_output_text,
)
@@ -336,6 +337,8 @@ def chat_param_to_responses_kwargs(
kwargs["max_output_tokens"] = param.max_tokens
if param.parallel_tool_calls is not UNSET:
kwargs["parallel_tool_calls"] = param.parallel_tool_calls
if param.reasoning_effort is not UNSET:
kwargs["reasoning"] = ResponseReasoningConfig(effort=param.reasoning_effort)
if param.tool_choice is not UNSET and isinstance(param.tool_choice, str):
kwargs["tool_choice"] = param.tool_choice
return kwargs