fix(cli): honor env precedence for LLM and run config

Interactive selections and flag defaults overrode TRADINGAGENTS_* env vars.
Rule: an explicit env value or CLI flag wins; otherwise the env-applied
default is kept.

- Research depth: skip the prompt when both round-count env vars are set, and
  stop overwriting them (#977).
- Checkpoint: --checkpoint/--no-checkpoint is tri-state; omitting it keeps
  TRADINGAGENTS_CHECKPOINT_ENABLED (#976).
- Docker ollama: use TRADINGAGENTS_LLM_PROVIDER + OLLAMA_BASE_URL, not a bare
  LLM_PROVIDER the overlay never reads (#975).
- Reasoning/thinking knobs: settable via env; the prompt is skipped when set.
- Effort gating: forward effort only to models that accept it (Anthropic
  Opus 4.5+/Sonnet 4.6+, OpenAI reasoning models); drop it elsewhere.
- Boolean env values: raise a named error on invalid input instead of
  silently becoming False.
This commit is contained in:
Yijia-Xiao
2026-06-21 21:03:05 +00:00
parent c15200dc28
commit a420ad0f3b
11 changed files with 363 additions and 59 deletions

View File

@@ -18,13 +18,35 @@ _ENV_OVERRIDES = {
"TRADINGAGENTS_CHECKPOINT_ENABLED": "checkpoint_enabled",
"TRADINGAGENTS_BENCHMARK_TICKER": "benchmark_ticker",
"TRADINGAGENTS_TEMPERATURE": "temperature",
# Provider-specific reasoning/thinking knobs (None = each provider's own
# default). Settable here for non-interactive runs; the CLI also offers an
# interactive choice, which is skipped when the matching var is set.
"TRADINGAGENTS_GOOGLE_THINKING_LEVEL": "google_thinking_level",
"TRADINGAGENTS_OPENAI_REASONING_EFFORT": "openai_reasoning_effort",
"TRADINGAGENTS_ANTHROPIC_EFFORT": "anthropic_effort",
}
_BOOL_TRUE = ("true", "1", "yes", "on")
_BOOL_FALSE = ("false", "0", "no", "off")
def _coerce(value: str, reference):
"""Coerce env-var string to the type of the existing default value."""
"""Coerce env-var string to the type of the existing default value.
Invalid values raise ``ValueError`` rather than silently falling back to a
default — a misspelled boolean (e.g. ``treu``) or non-numeric int should fail
loudly at startup, not quietly misconfigure an unattended run.
"""
if isinstance(reference, bool):
return value.strip().lower() in ("true", "1", "yes", "on")
normalized = value.strip().lower()
if normalized in _BOOL_TRUE:
return True
if normalized in _BOOL_FALSE:
return False
raise ValueError(
f"expected a boolean ({'/'.join(_BOOL_TRUE + _BOOL_FALSE)}), got {value!r}"
)
if isinstance(reference, int) and not isinstance(reference, bool):
return int(value)
if isinstance(reference, float):
@@ -38,7 +60,10 @@ def _apply_env_overrides(config: dict) -> dict:
raw = os.environ.get(env_var)
if raw is None or raw == "":
continue
config[key] = _coerce(raw, config.get(key))
try:
config[key] = _coerce(raw, config.get(key))
except ValueError as exc:
raise ValueError(f"Invalid value for {env_var}: {exc}") from exc
return config

View File

@@ -12,20 +12,27 @@ _PASSTHROUGH_KWARGS = (
)
# Anthropic's extended-thinking ``effort`` parameter is accepted by Opus 4.5+
# and Sonnet 4.5+ only. Haiku (any version shipped to date) 400s with
# ``"This model does not support the effort parameter"`` (#831). Future
# ``claude-{opus,sonnet}-X-Y`` releases inherit effort support via the
# forward-compat pattern below; future Haiku stays excluded by default.
# and Sonnet 4.6+ only. Sonnet 4.5 and any Haiku version 400 with
# ``"This model does not support the effort parameter"`` (#831). The per-family
# minimum version below is forward-compatible: future ``claude-{opus,sonnet}-X-Y``
# releases inherit support automatically, while Sonnet 4.5 and Haiku stay excluded.
_EFFORT_EXACT = {
"claude-mythos-preview", # non-standard preview name; effort-capable
}
_EFFORT_PATTERN = re.compile(r"^claude-(opus|sonnet)-\d+-\d+$")
_EFFORT_MODEL = re.compile(r"^claude-(opus|sonnet)-(\d+)-(\d+)$")
_EFFORT_MIN_VERSION = {"opus": (4, 5), "sonnet": (4, 6)}
def _supports_effort(model: str) -> bool:
"""Whether Anthropic accepts the ``effort`` parameter for this model."""
model_lc = model.lower()
return model_lc in _EFFORT_EXACT or bool(_EFFORT_PATTERN.match(model_lc))
if model_lc in _EFFORT_EXACT:
return True
match = _EFFORT_MODEL.match(model_lc)
if not match:
return False
family, major, minor = match.group(1), int(match.group(2)), int(match.group(3))
return (major, minor) >= _EFFORT_MIN_VERSION[family]
class NormalizedChatAnthropic(ChatAnthropic):

View File

@@ -1,4 +1,5 @@
import os
import re
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlparse
@@ -150,6 +151,18 @@ _PASSTHROUGH_KWARGS = (
"api_key", "callbacks", "http_client", "http_async_client",
)
# OpenAI's ``reasoning_effort`` is only accepted by reasoning models — the GPT-5
# family and the o-series. Non-reasoning models (gpt-4.1, gpt-4o, ...) 400 with
# "Unsupported parameter: 'reasoning.effort' is not supported with this model".
# Drop the kwarg for those rather than crash the run.
_OPENAI_REASONING_MODEL = re.compile(r"^(gpt-5|o[1-9])")
def _supports_reasoning_effort(model: str) -> bool:
"""Whether the (native OpenAI) model accepts ``reasoning_effort``."""
return bool(_OPENAI_REASONING_MODEL.match(model.lower().strip()))
@dataclass(frozen=True)
class ProviderSpec:
"""Declarative config for one OpenAI-compatible provider.
@@ -291,8 +304,11 @@ class OpenAIClient(BaseLLMClient):
# Forward user-provided kwargs
for key in _PASSTHROUGH_KWARGS:
if key in self.kwargs:
llm_kwargs[key] = self.kwargs[key]
if key not in self.kwargs:
continue
if key == "reasoning_effort" and not _supports_reasoning_effort(self.model):
continue
llm_kwargs[key] = self.kwargs[key]
# The subclass (provider quirks) comes from the registry spec.
return chat_cls(**llm_kwargs)