TradingAgents-astock/tests/test_output_token_limit.py
Simon Lin 383a38dc31 fix: codex 审计五处修复(截断告警/防护漏洞/胜率算反/持有期/服务器探测)
五条全部实测复现,无一误报。

1. 截断告警对默认 provider 根本不会响
   只认 Anthropic 的 stop_reason=max_tokens 与 Chat 的 finish_reason=length,
   而 openai 是默认 provider 且走 Responses API(status=incomplete +
   incomplete_details.reason=max_output_tokens),Gemini 是大写 MAX_TOKENS。
   v0.5.1 加的这个告警在默认配置下一次都不会触发。现覆盖四种形状。

2. 非 A 股防护漏了三个 vendor
   get_dragon_tiger_board / get_lockup_expiry / get_industry_comparison 直接调
   safe_ticker_component 绕过了 _normalize_ticker。实测拿 00700 调用返回的是
   看起来完全正常的报告("近30日未上龙虎榜"),模型会当成腾讯的事实。
   v0.5.3 声称的"一个卡点覆盖 15 个接口"并不成立。

3. 绩效统计对看空评级把对错算反
   给 Sell 后股价下跌是判断正确,原实现只看收益是否为正,记成失败。新增
   direction_accuracy(按方向判定,Hold 不计入),原指标改名 up_rate /
   outperform_rate —— 命名即口径,报告写明它们与判断对错无关。

4. 平均持有期永远缺失
   记忆日志写 "5d",int() 抛异常被吞成 None。改为提取数字部分。

5. mootdx 提前收手会漏掉可用服务器
   三台远端拒绝推不出本地封了协议,靠后的服务器可能是好的。移除提前退出,
   跑完整张表。实测 18.7s → 23.7s(最初 >170s,bestip 全表测速仍单独规避)。

测试:新增 13 例,316 passed / 13 skipped / 0 failed
2026-08-09 13:38:45 +12:00

153 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""输出上限与 Anthropic 兼容端点配置(#91 / #89
#91「报告输出到一半结束」走 anthropic 通道跑第三方模型Kimi 等)时,
langchain-anthropic 认不出模型名,会落到一个很小的兜底输出上限,报告写不完就
被截断——而返回值本身完全合法,没有任何报错。
#89「调用 kimi 失败 401」用第三方模型名走 anthropic 通道却没配端点,请求会被
发到 api.anthropic.com拿第三方 token 认证,报一句看不懂的 invalid x-api-key。
"""
import logging
import pytest
from tradingagents.llm_clients.anthropic_client import (
_THIRD_PARTY_DEFAULT_MAX_TOKENS,
AnthropicClient,
)
from tradingagents.llm_clients.base_client import warn_if_truncated
from tradingagents.llm_clients.openai_client import OpenAIClient
class FakeResponse:
"""够用的 AIMessage 替身:只需要 response_metadata。"""
def __init__(self, metadata):
self.response_metadata = metadata
self.content = "写到一半就断了的报告"
# ---------------------------------------------------------------------------
# #91 输出上限
# ---------------------------------------------------------------------------
def test_third_party_model_gets_explicit_max_tokens():
"""Kimi 这类第三方模型必须拿到显式上限,不能听凭 langchain 的小兜底值。"""
client = AnthropicClient(
"kimi-k2-0905-preview", base_url="https://api.kimi.com/coding/"
)
llm = client.get_llm()
assert llm.max_tokens == _THIRD_PARTY_DEFAULT_MAX_TOKENS
def test_real_claude_model_keeps_provider_default():
"""真 Claude 模型不要动——它的上限比我们的默认值大得多。"""
llm = AnthropicClient("claude-sonnet-4-6").get_llm()
assert llm.max_tokens > _THIRD_PARTY_DEFAULT_MAX_TOKENS
def test_dated_claude_model_id_is_not_treated_as_third_party():
"""带日期的正规 Claude ID 不在我们的目录里,但绝不能被砍到第三方默认值。"""
llm = AnthropicClient("claude-sonnet-4-5-20250929").get_llm()
assert llm.max_tokens > _THIRD_PARTY_DEFAULT_MAX_TOKENS
def test_explicit_max_tokens_wins():
"""用户显式配置的上限优先于任何默认值。"""
client = AnthropicClient(
"kimi-k2-0905-preview",
base_url="https://api.kimi.com/coding/",
max_tokens=32000,
)
assert client.get_llm().max_tokens == 32000
def test_openai_client_forwards_max_tokens():
"""OpenAI 兼容通道同样要能配上限,否则这个配置项对半数用户是死的。"""
client = OpenAIClient(
"deepseek-chat", provider="deepseek", api_key="test-key", max_tokens=12345
)
assert client.get_llm().max_tokens == 12345
@pytest.mark.parametrize(
"metadata",
[
{"stop_reason": "max_tokens"}, # Anthropic
{"finish_reason": "length"}, # OpenAI 兼容 Chat
{"finish_reason": "MAX_TOKENS"}, # Gemini大写
# OpenAI Responses API —— `openai` 是**默认 provider** 且走这条路径,
# 漏掉它等于默认配置下这个告警根本不会响codex P1
{"status": "incomplete",
"incomplete_details": {"reason": "max_output_tokens"}},
],
)
def test_truncated_response_is_reported(metadata, caplog):
"""被截断必须喊出来——否则用户只会以为模型没写完(#91 的真正痛点)。"""
with caplog.at_level(logging.WARNING):
warn_if_truncated(FakeResponse(metadata), "some-model")
assert any("max_tokens" in r.getMessage() for r in caplog.records)
@pytest.mark.parametrize(
"metadata",
[
{"stop_reason": "end_turn"},
{"finish_reason": "stop"},
{"finish_reason": "STOP"},
# 因为别的原因 incomplete如内容过滤不是输出上限不该报 max_tokens
{"status": "incomplete", "incomplete_details": {"reason": "content_filter"}},
{"status": "completed"},
{},
],
)
def test_normal_response_is_not_reported(metadata, caplog):
"""正常收尾不要报警,否则告警变噪音就没人看了。"""
with caplog.at_level(logging.WARNING):
warn_if_truncated(FakeResponse(metadata), "some-model")
assert not caplog.records
# ---------------------------------------------------------------------------
# #89 Anthropic 兼容端点
# ---------------------------------------------------------------------------
def test_third_party_model_without_endpoint_fails_loudly(monkeypatch):
"""没配端点就用第三方模型名 → 当场说清楚,而不是让 Anthropic 回一句 401。"""
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
with pytest.raises(RuntimeError) as exc:
AnthropicClient("kimi-k2-0905-preview").get_llm()
message = str(exc.value)
assert "backend_url" in message
assert "ANTHROPIC_API_KEY" in message
# 必须点破这个常见误配ANTHROPIC_AUTH_TOKEN 是 Claude Code CLI 的写法
assert "ANTHROPIC_AUTH_TOKEN" in message
def test_anthropic_base_url_env_is_honoured(monkeypatch):
"""端点也允许走环境变量给,不是只能写 config。"""
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://api.kimi.com/coding/")
llm = AnthropicClient("kimi-k2-0905-preview").get_llm()
assert "kimi" in str(llm.anthropic_api_url)
def test_claude_model_without_endpoint_still_works(monkeypatch):
"""真 Claude 模型不配端点是完全正常的用法,不能被新校验误伤。"""
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
assert AnthropicClient("claude-sonnet-4-6").get_llm() is not None