mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
config: resolve thinking strength across agent, provider, and model layers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user