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

@@ -82,5 +82,68 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
self.assertEqual(sel["output_language"], "Japanese")
@pytest.mark.unit
class TestResearchDepthSkippedFromEnv(unittest.TestCase):
def test_both_round_envs_skip_depth_prompt(self):
import cli.main as m
env = {
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "2",
"TRADINGAGENTS_MAX_RISK_ROUNDS": "4",
}
fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg.update({"max_debate_rounds": 2, "max_risk_discuss_rounds": 4})
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \
mock.patch.object(m, "select_research_depth") as prompt_depth, \
mock.patch.object(m, "ensure_api_key"), \
mock.patch.object(m, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(m, "ask_output_language", return_value="English"), \
mock.patch.object(m, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(m, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(m, "ask_openai_reasoning_effort", return_value=None):
sel = m.get_user_selections()
# The research-depth prompt is skipped; the value comes from the env config.
prompt_depth.assert_not_called()
self.assertEqual(sel["research_depth"], 2)
@pytest.mark.unit
class TestReasoningEffortSkippedFromEnv(unittest.TestCase):
def test_effort_env_skips_step8_prompt(self):
import cli.main as m
env = {"TRADINGAGENTS_OPENAI_REASONING_EFFORT": "high"}
fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg.update({"openai_reasoning_effort": "high"})
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \
mock.patch.object(m, "select_research_depth", return_value=1), \
mock.patch.object(m, "ensure_api_key"), \
mock.patch.object(m, "select_llm_provider", return_value=("openai", None)), \
mock.patch.object(m, "ask_output_language", return_value="English"), \
mock.patch.object(m, "select_shallow_thinking_agent", return_value="gpt-5.4-mini"), \
mock.patch.object(m, "select_deep_thinking_agent", return_value="gpt-5.5"), \
mock.patch.object(m, "ask_openai_reasoning_effort") as prompt_effort:
sel = m.get_user_selections()
# The reasoning-effort prompt is skipped; the value comes from env config.
prompt_effort.assert_not_called()
self.assertEqual(sel["openai_reasoning_effort"], "high")
if __name__ == "__main__":
unittest.main()