config: resolve thinking strength across agent, provider, and model layers

This commit is contained in:
2026-10-06 21:06:46 +08:00
parent 0c9bb2e067
commit ffd4dbbe26
6 changed files with 271 additions and 0 deletions
+3
View File
@@ -21,10 +21,13 @@ from .models import OpenAICompatibleProvider as OpenAICompatibleProvider
from .models import OpenAIProvider as OpenAIProvider
from .models import PluginsConfig as PluginsConfig
from .models import ProviderConfig as ProviderConfig
from .models import ReasoningEffortConfig as ReasoningEffortConfig
from .models import SkillDiscoverySource as SkillDiscoverySource
from .models import SkillsConfig as SkillsConfig
from .models import compose_agent_system_content as compose_agent_system_content
from .path import get_default_path as _get_default_path
from .reasoning import ReasoningConfig as ReasoningConfig
from .reasoning import resolve_reasoning as resolve_reasoning
from .routing import EffectiveProvider as EffectiveProvider
from .routing import ProviderPreset as ProviderPreset
from .routing import default_url_for_preset as default_url_for_preset
+22
View File
@@ -13,6 +13,14 @@ type DeepSeekConvention = Literal["", "openai", "anthropic", "responses"]
# Static directory pre-allow mode for ``[agent].allow_paths`` (path → mode).
type AllowPathMode = Literal["read", "write", "exec"]
# Thinking strength, normalized across API surfaces (``reasoning_effort`` on
# ``[agent]``, a provider, or a single model; the most specific one wins).
# "" = send nothing and let the provider pick its own default.
# Mapped per surface by ``config.reasoning``: chat completions / Responses send
# ``reasoning_effort`` / ``reasoning.effort`` (``max`` is DeepSeek-only and
# clamps to ``xhigh`` elsewhere), Anthropic maps it onto ``thinking``.
type ReasoningEffortConfig = Literal["", "none", "minimal", "low", "medium", "high", "xhigh", "max"]
# Built-in skill discovery sources (``[skills].discover``): our own user config
# directory, and the skill directories other harnesses install into.
type SkillDiscoverySource = Literal["plyngent", "claude"]
@@ -150,6 +158,14 @@ class AgentConfig(Struct, omit_defaults=True):
compact_user_prefix: str = ""
compact_seed_text: str = ""
# Thinking strength for every request (a provider or single model may
# override either value; the most specific one wins).
# reasoning_effort: "" | none | minimal | low | medium | high | xhigh | max.
# thinking_budget_tokens: explicit Anthropic ``thinking`` budget (0 = derive
# it from ``reasoning_effort``). See config/reasoning.py.
reasoning_effort: ReasoningEffortConfig = ""
thinking_budget_tokens: int = 0
class PluginsConfig(Struct, omit_defaults=True):
"""Third-party plugins (not tool-specific config).
@@ -268,6 +284,9 @@ class ModelConfig(Struct, omit_defaults=True):
url: str = ""
# DeepSeek API surface override ("" = inherit provider convention).
convention: DeepSeekConvention = ""
# Thinking strength for this model only ("" / 0 = inherit the provider).
reasoning_effort: ReasoningEffortConfig = ""
thinking_budget_tokens: int = 0
class HttpTimeoutConfig(Struct, omit_defaults=True):
@@ -299,6 +318,9 @@ class ProviderConfig(Struct, tag_field="preset", omit_defaults=True):
models: dict[str, ModelConfig] = field(default_factory=dict)
# float = single timeout for the session; table = connect/read split; omit = defaults.
timeout: float | HttpTimeoutConfig | None = None
# Thinking strength for every model of this provider ("" / 0 = inherit [agent]).
reasoning_effort: ReasoningEffortConfig = ""
thinking_budget_tokens: int = 0
def _default_openai_models() -> dict[str, ModelConfig]:
+80
View File
@@ -0,0 +1,80 @@
"""Thinking-strength resolution across config layers and API surfaces.
``reasoning_effort`` (and the optional Anthropic ``thinking_budget_tokens``) may
be set on ``[agent]``, on a provider, or on a single model. This module merges
the layers — the most specific one wins, per field — and normalizes the result
for the API surface the provider actually talks to, so the agent loop and the
provider bridges receive one value with a single meaning:
* ``""`` — send nothing; the provider applies its own default.
* chat completions / Responses — ``reasoning_effort`` / ``reasoning.effort``.
* Anthropic — ``thinking`` (an effort level has no wire form there).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from .routing import model_config_for, resolve_effective_provider
if TYPE_CHECKING:
from .models import AgentConfig, Provider, ReasoningEffortConfig
# DeepSeek's own ``reasoning_effort`` accepts "max"; OpenAI and Anthropic do not,
# so anywhere else it clamps down to the highest level they do understand.
_DEEPSEEK_CHAT_CONVENTIONS = frozenset({"", "openai"})
@dataclass(frozen=True, slots=True)
class ReasoningConfig:
"""Merged thinking settings for one provider + model."""
effort: ReasoningEffortConfig = ""
# Explicit Anthropic ``thinking`` budget; 0 = derive it from ``effort``.
thinking_budget_tokens: int = 0
def _clamp_effort(effort: ReasoningEffortConfig, *, preset: str, convention: str) -> ReasoningEffortConfig:
"""Keep ``max`` only on the DeepSeek chat surface that defines it."""
if effort != "max":
return effort
if preset == "deepseek" and convention in _DEEPSEEK_CHAT_CONVENTIONS:
return effort
return "xhigh"
def resolve_reasoning(
agent: AgentConfig | None,
provider: Provider,
model: str | None,
) -> ReasoningConfig:
"""Merge ``[agent]`` < provider < model thinking settings for *model*.
*agent* is ``None`` for callers that only know the provider (config tools,
tests); the model layer is the provider's entry for *model*, if any. Each
field falls back on its own, so a model may inherit only the effort (or only
the budget) from above.
"""
effective = resolve_effective_provider(provider, model=model)
model_config = model_config_for(provider, model)
effort: ReasoningEffortConfig = ""
for layer in (model_config, provider, agent):
if layer is not None and layer.reasoning_effort:
effort = layer.reasoning_effort
break
budget = 0
for layer in (model_config, provider, agent):
if layer is not None and layer.thinking_budget_tokens > 0:
budget = layer.thinking_budget_tokens
break
return ReasoningConfig(
effort=_clamp_effort(effort, preset=effective.preset, convention=effective.convention),
thinking_budget_tokens=budget,
)
__all__ = ["ReasoningConfig", "resolve_reasoning"]