mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/agent: send the resolved reasoning effort with each request
This commit is contained in:
@@ -287,6 +287,8 @@ class ChatAgent:
|
|||||||
session_id: int | None
|
session_id: int | None
|
||||||
max_rounds: int
|
max_rounds: int
|
||||||
temperature: float | None
|
temperature: float | None
|
||||||
|
# Resolved thinking strength ("" = provider default); see config.reasoning.
|
||||||
|
reasoning_effort: str
|
||||||
on_limit: LimitContinueHook | None
|
on_limit: LimitContinueHook | None
|
||||||
stream: bool
|
stream: bool
|
||||||
system_prompt: str | None
|
system_prompt: str | None
|
||||||
@@ -318,6 +320,7 @@ class ChatAgent:
|
|||||||
session_id: int | None = None,
|
session_id: int | None = None,
|
||||||
max_rounds: int = DEFAULT_MAX_ROUNDS,
|
max_rounds: int = DEFAULT_MAX_ROUNDS,
|
||||||
temperature: float | None = None,
|
temperature: float | None = None,
|
||||||
|
reasoning_effort: str = "",
|
||||||
messages: Sequence[AnyChatMessage] | None = None,
|
messages: Sequence[AnyChatMessage] | None = None,
|
||||||
on_limit: LimitContinueHook | None = None,
|
on_limit: LimitContinueHook | None = None,
|
||||||
stream: bool = True,
|
stream: bool = True,
|
||||||
@@ -340,6 +343,7 @@ class ChatAgent:
|
|||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.max_rounds = max_rounds
|
self.max_rounds = max_rounds
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
|
self.reasoning_effort = reasoning_effort
|
||||||
self.on_limit = on_limit
|
self.on_limit = on_limit
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.system_prompt = system_prompt
|
self.system_prompt = system_prompt
|
||||||
@@ -597,6 +601,7 @@ class ChatAgent:
|
|||||||
tools=self.tools,
|
tools=self.tools,
|
||||||
max_rounds=self.max_rounds,
|
max_rounds=self.max_rounds,
|
||||||
temperature=self.temperature,
|
temperature=self.temperature,
|
||||||
|
reasoning_effort=self.reasoning_effort,
|
||||||
on_limit=self.on_limit,
|
on_limit=self.on_limit,
|
||||||
stream=self.stream,
|
stream=self.stream,
|
||||||
max_tool_result_chars=self.max_tool_result_chars,
|
max_tool_result_chars=self.max_tool_result_chars,
|
||||||
@@ -740,6 +745,7 @@ class ChatAgent:
|
|||||||
session_id=None,
|
session_id=None,
|
||||||
max_rounds=rounds,
|
max_rounds=rounds,
|
||||||
temperature=self.temperature,
|
temperature=self.temperature,
|
||||||
|
reasoning_effort=self.reasoning_effort,
|
||||||
on_limit=self.on_limit,
|
on_limit=self.on_limit,
|
||||||
stream=self.stream,
|
stream=self.stream,
|
||||||
system_prompt=self.system_prompt if system_prompt is None else system_prompt,
|
system_prompt=self.system_prompt if system_prompt is None else system_prompt,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from plyngent.lmproto.anthropic.client import AnthropicClient
|
from plyngent.lmproto.anthropic.client import AnthropicClient
|
||||||
from plyngent.lmproto.openai.client import OpenAIClient
|
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_nag import TodoNagStrategy
|
||||||
from .todo_stack import TodoStack
|
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,
|
tools: ToolRegistry | None = None,
|
||||||
max_rounds: int = DEFAULT_MAX_ROUNDS,
|
max_rounds: int = DEFAULT_MAX_ROUNDS,
|
||||||
temperature: float | None = None,
|
temperature: float | None = None,
|
||||||
|
reasoning_effort: str = "",
|
||||||
on_limit: LimitContinueHook | None = None,
|
on_limit: LimitContinueHook | None = None,
|
||||||
stream: bool = True,
|
stream: bool = True,
|
||||||
max_tool_result_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS,
|
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``.
|
Multiple tool calls in one round run in parallel when ``parallel_tools``.
|
||||||
Request payloads may shrink older tool results when over ``max_context_tokens``.
|
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.
|
*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
|
``web_search``) merged into the request as opaque dicts; never executed by the
|
||||||
:class:`ToolRegistry`. Ignored by non-Responses clients.
|
: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,
|
model=model,
|
||||||
temperature=temperature if temperature is not None else UNSET,
|
temperature=temperature if temperature is not None else UNSET,
|
||||||
tools=list(tool_items) if tool_items 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)
|
pre_len = len(messages)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from plyngent.lmproto.anthropic.model import (
|
|||||||
AnthropicMessagesParam,
|
AnthropicMessagesParam,
|
||||||
AnthropicResponseText,
|
AnthropicResponseText,
|
||||||
AnthropicTextContent,
|
AnthropicTextContent,
|
||||||
|
AnthropicThinkingConfig,
|
||||||
AnthropicToolChoice,
|
AnthropicToolChoice,
|
||||||
AnthropicToolDefinition,
|
AnthropicToolDefinition,
|
||||||
AnthropicToolResultContent,
|
AnthropicToolResultContent,
|
||||||
@@ -273,16 +274,87 @@ def _max_tokens_from_param(param: ChatCompletionsParam) -> int:
|
|||||||
return 8192
|
return 8192
|
||||||
|
|
||||||
|
|
||||||
def chat_param_to_anthropic_param(param: ChatCompletionsParam) -> AnthropicMessagesParam:
|
# Effort level → Anthropic ``thinking`` budget (an effort has no wire form there).
|
||||||
"""Build :class:`AnthropicMessagesParam` from a chat completions param."""
|
# 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)
|
system, messages = chat_messages_to_anthropic(param.messages)
|
||||||
tools = tool_items_to_anthropic_tools(param.tools if param.tools is not UNSET else None)
|
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] = {
|
kwargs: dict[str, Any] = {
|
||||||
"model": param.model,
|
"model": param.model,
|
||||||
"max_tokens": _max_tokens_from_param(param),
|
"max_tokens": max_tokens,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
if thinking is not UNSET:
|
||||||
|
kwargs["thinking"] = thinking
|
||||||
if system:
|
if system:
|
||||||
kwargs["system"] = system
|
kwargs["system"] = system
|
||||||
if tools:
|
if tools:
|
||||||
|
|||||||
@@ -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:
|
class _ToolBlockState:
|
||||||
"""Tracks an in-flight tool_use content block during SSE streaming."""
|
"""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
|
``message_stop``. Errors raise ``RuntimeError`` so the agent loop can
|
||||||
surface them as retryable failures.
|
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)
|
stream = await client.messages(create, stream=True)
|
||||||
state = _StreamState(param.model)
|
state = _StreamState(param.model)
|
||||||
|
|
||||||
@@ -249,7 +254,7 @@ async def dispatch_messages(
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||||
"""Run one Anthropic Messages turn and return a chat-completions-shaped result."""
|
"""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:
|
if stream:
|
||||||
return _stream_as_chat_chunks(client, param)
|
return _stream_as_chat_chunks(client, param)
|
||||||
response = await client.messages(create, stream=False)
|
response = await client.messages(create, stream=False)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from plyngent.lmproto.openai.model import (
|
|||||||
ResponseEasyInputMessage,
|
ResponseEasyInputMessage,
|
||||||
ResponseFunctionTool,
|
ResponseFunctionTool,
|
||||||
ResponseFunctionToolCallOutput,
|
ResponseFunctionToolCallOutput,
|
||||||
|
ResponseReasoningConfig,
|
||||||
response_function_calls,
|
response_function_calls,
|
||||||
response_output_text,
|
response_output_text,
|
||||||
)
|
)
|
||||||
@@ -336,6 +337,8 @@ def chat_param_to_responses_kwargs(
|
|||||||
kwargs["max_output_tokens"] = param.max_tokens
|
kwargs["max_output_tokens"] = param.max_tokens
|
||||||
if param.parallel_tool_calls is not UNSET:
|
if param.parallel_tool_calls is not UNSET:
|
||||||
kwargs["parallel_tool_calls"] = param.parallel_tool_calls
|
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):
|
if param.tool_choice is not UNSET and isinstance(param.tool_choice, str):
|
||||||
kwargs["tool_choice"] = param.tool_choice
|
kwargs["tool_choice"] = param.tool_choice
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|||||||
@@ -211,6 +211,20 @@ async def test_run_chat_loop_text_only() -> None:
|
|||||||
assert len(client.calls) == 1
|
assert len(client.calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_chat_loop_carries_reasoning_effort() -> None:
|
||||||
|
client = ScriptedClient([_response(AssistantChatMessage(content="hello"))])
|
||||||
|
messages: list[AnyChatMessage] = [UserChatMessage(content="hi")]
|
||||||
|
_ = [e async for e in run_chat_loop(client, messages, model="m", stream=False, reasoning_effort="high")]
|
||||||
|
assert client.calls[0].reasoning_effort == "high"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_chat_loop_omits_unset_reasoning_effort() -> None:
|
||||||
|
client = ScriptedClient([_response(AssistantChatMessage(content="hello"))])
|
||||||
|
messages: list[AnyChatMessage] = [UserChatMessage(content="hi")]
|
||||||
|
_ = [e async for e in run_chat_loop(client, messages, model="m", stream=False)]
|
||||||
|
assert client.calls[0].reasoning_effort is UNSET
|
||||||
|
|
||||||
|
|
||||||
async def test_non_stream_emits_usage() -> None:
|
async def test_non_stream_emits_usage() -> None:
|
||||||
client = ScriptedClient(
|
client = ScriptedClient(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from msgspec import UNSET
|
from msgspec import UNSET
|
||||||
|
|
||||||
from plyngent.agent.messages_bridge import (
|
from plyngent.agent.messages_bridge import (
|
||||||
@@ -29,6 +31,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
|||||||
AssistantFunctionToolCall,
|
AssistantFunctionToolCall,
|
||||||
ChatCompletionsParam,
|
ChatCompletionsParam,
|
||||||
DeveloperChatMessage,
|
DeveloperChatMessage,
|
||||||
|
ReasoningEffort,
|
||||||
SystemChatMessage,
|
SystemChatMessage,
|
||||||
ToolChatMessage,
|
ToolChatMessage,
|
||||||
ToolFunction,
|
ToolFunction,
|
||||||
@@ -36,6 +39,9 @@ from plyngent.lmproto.openai_compatible.model import (
|
|||||||
UserChatMessage,
|
UserChatMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from plyngent.typedef import Unset
|
||||||
|
|
||||||
|
|
||||||
def test_tool_items_to_anthropic_tools() -> None:
|
def test_tool_items_to_anthropic_tools() -> None:
|
||||||
items = [
|
items = [
|
||||||
@@ -198,3 +204,53 @@ def test_chat_param_to_anthropic_param() -> None:
|
|||||||
assert body.tools is not UNSET
|
assert body.tools is not UNSET
|
||||||
assert len(body.tools) == 1
|
assert len(body.tools) == 1
|
||||||
assert body.temperature == 0.2
|
assert body.temperature == 0.2
|
||||||
|
# No effort configured → no thinking block, provider default applies.
|
||||||
|
assert body.thinking is UNSET
|
||||||
|
|
||||||
|
|
||||||
|
def _effort_param(effort: ReasoningEffort | Unset = UNSET) -> ChatCompletionsParam:
|
||||||
|
return ChatCompletionsParam(
|
||||||
|
model="claude-test",
|
||||||
|
messages=[UserChatMessage(content="hi")],
|
||||||
|
reasoning_effort=effort,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_anthropic_param_thinking_from_effort() -> None:
|
||||||
|
body = chat_param_to_anthropic_param(_effort_param("medium"))
|
||||||
|
assert body.thinking is not UNSET
|
||||||
|
assert body.thinking.type == "enabled"
|
||||||
|
assert body.thinking.budget_tokens == 8192
|
||||||
|
# A budget equal to max_tokens is invalid for Anthropic, so the default
|
||||||
|
# output limit grows to leave room after it.
|
||||||
|
assert body.max_tokens == 8192 + 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_anthropic_param_thinking_raises_max_tokens() -> None:
|
||||||
|
# Anthropic requires budget_tokens < max_tokens; the answer keeps room after
|
||||||
|
# the reasoning budget instead of the budget being shrunk to fit.
|
||||||
|
body = chat_param_to_anthropic_param(_effort_param("high"))
|
||||||
|
assert body.thinking is not UNSET
|
||||||
|
assert body.thinking.budget_tokens == 16384
|
||||||
|
assert body.max_tokens == 16384 + 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_anthropic_param_explicit_thinking_budget() -> None:
|
||||||
|
body = chat_param_to_anthropic_param(_effort_param("minimal"), thinking_budget_tokens=20000)
|
||||||
|
assert body.thinking is not UNSET
|
||||||
|
assert body.thinking.budget_tokens == 20000
|
||||||
|
assert body.max_tokens == 20000 + 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_anthropic_param_thinking_budget_floor() -> None:
|
||||||
|
body = chat_param_to_anthropic_param(_effort_param("minimal"), thinking_budget_tokens=100)
|
||||||
|
assert body.thinking is not UNSET
|
||||||
|
assert body.thinking.budget_tokens == 1024
|
||||||
|
assert body.max_tokens == 8192
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_anthropic_param_thinking_disabled() -> None:
|
||||||
|
body = chat_param_to_anthropic_param(_effort_param("none"))
|
||||||
|
assert body.thinking is not UNSET
|
||||||
|
assert body.thinking.type == "disabled"
|
||||||
|
assert body.thinking.budget_tokens is UNSET
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class ScriptedMessagesClient:
|
|||||||
|
|
||||||
kind: str = "messages"
|
kind: str = "messages"
|
||||||
calls: list[AnthropicMessagesParam]
|
calls: list[AnthropicMessagesParam]
|
||||||
|
# Mirrors AnthropicClient: explicit thinking budget (0 = derive from effort).
|
||||||
|
thinking_budget_tokens: int
|
||||||
_non_stream: list[AnthropicMessageResponse]
|
_non_stream: list[AnthropicMessageResponse]
|
||||||
_stream_events: list[list[AnthropicStreamEvent]]
|
_stream_events: list[list[AnthropicStreamEvent]]
|
||||||
|
|
||||||
@@ -81,6 +83,7 @@ class ScriptedMessagesClient:
|
|||||||
stream_events: Sequence[Sequence[AnthropicStreamEvent]] | None = None,
|
stream_events: Sequence[Sequence[AnthropicStreamEvent]] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
self.thinking_budget_tokens = 0
|
||||||
self._non_stream = list(non_stream or ())
|
self._non_stream = list(non_stream or ())
|
||||||
self._stream_events = [list(events) for events in (stream_events or ())]
|
self._stream_events = [list(events) for events in (stream_events or ())]
|
||||||
|
|
||||||
@@ -186,6 +189,46 @@ async def test_run_chat_loop_messages_kind_text() -> None:
|
|||||||
assert any(isinstance(e, UsageEvent) and e.usage.prompt_tokens == 10 for e in events)
|
assert any(isinstance(e, UsageEvent) and e.usage.prompt_tokens == 10 for e in events)
|
||||||
assert isinstance(messages[-1], AssistantChatMessage)
|
assert isinstance(messages[-1], AssistantChatMessage)
|
||||||
assert messages[-1].content == "done"
|
assert messages[-1].content == "done"
|
||||||
|
# No thinking configured → the request carries no thinking block.
|
||||||
|
assert client.calls[0].thinking is UNSET
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_chat_loop_passes_reasoning_effort_and_client_thinking_budget() -> None:
|
||||||
|
client = ScriptedMessagesClient(non_stream=[_message_response(text="done")])
|
||||||
|
client.thinking_budget_tokens = 12000
|
||||||
|
events = [
|
||||||
|
event
|
||||||
|
async for event in run_chat_loop(
|
||||||
|
cast("Any", client),
|
||||||
|
[UserChatMessage(content="go")],
|
||||||
|
model="claude-test",
|
||||||
|
stream=False,
|
||||||
|
reasoning_effort="high",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
assert any(isinstance(e, TextDeltaEvent) for e in events)
|
||||||
|
thinking = client.calls[0].thinking
|
||||||
|
assert thinking is not UNSET
|
||||||
|
assert thinking.type == "enabled"
|
||||||
|
# The client's explicit budget wins over the level's derived one.
|
||||||
|
assert thinking.budget_tokens == 12000
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_chat_loop_reasoning_effort_none_disables_thinking() -> None:
|
||||||
|
client = ScriptedMessagesClient(non_stream=[_message_response(text="done")])
|
||||||
|
_ = [
|
||||||
|
event
|
||||||
|
async for event in run_chat_loop(
|
||||||
|
cast("Any", client),
|
||||||
|
[UserChatMessage(content="go")],
|
||||||
|
model="claude-test",
|
||||||
|
stream=False,
|
||||||
|
reasoning_effort="none",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
thinking = client.calls[0].thinking
|
||||||
|
assert thinking is not UNSET
|
||||||
|
assert thinking.type == "disabled"
|
||||||
|
|
||||||
|
|
||||||
async def test_run_chat_loop_messages_kind_tool_round() -> None:
|
async def test_run_chat_loop_messages_kind_tool_round() -> None:
|
||||||
|
|||||||
@@ -235,6 +235,18 @@ def test_chat_param_to_responses_kwargs() -> None:
|
|||||||
assert kwargs["store"] is False
|
assert kwargs["store"] is False
|
||||||
assert kwargs["temperature"] == 0.2
|
assert kwargs["temperature"] == 0.2
|
||||||
assert len(kwargs["tools"]) == 1
|
assert len(kwargs["tools"]) == 1
|
||||||
|
# No effort configured → no reasoning block.
|
||||||
|
assert "reasoning" not in kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_param_to_responses_kwargs_reasoning_effort() -> None:
|
||||||
|
param = ChatCompletionsParam(
|
||||||
|
model="gpt-test",
|
||||||
|
messages=[UserChatMessage(content="hi")],
|
||||||
|
reasoning_effort="xhigh",
|
||||||
|
)
|
||||||
|
kwargs = chat_param_to_responses_kwargs(param)
|
||||||
|
assert kwargs["reasoning"].effort == "xhigh"
|
||||||
|
|
||||||
|
|
||||||
def test_provider_tools_merged_after_local_functions() -> None:
|
def test_provider_tools_merged_after_local_functions() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user