Files
sanguo_vnpy_v2/tests/api/test_llm_provider.py
claude_dev 85d14a1e56
CI/CD / test (push) Successful in 41s
CI/CD / nas-deploy (push) Successful in 31s
CI/CD / nas-verify (push) Successful in 8s
fix(api): LLM client 补 anthropic 协议+glm_cn 改指 coding plan 接入点——修生产「AI 生成草稿」429 1113 [vps]
根因(VPS 实测): glm_cn 默认 base_url=paas/v4 是按量计费端点,coding plan
的 key 打它=HTTP 429 code 1113 余额不足;同一 key 在 coding plan 接入点
/api/anthropic/v1/messages 实测 200(glm-5.2 与 glm-5.3-flash 均通)。
修复: ProviderSpec 增 protocol 字段(openai|anthropic,env SANGUO_LLM_PROTOCOL
可覆盖), glm_cn 改指 anthropic 端点; client/probe 共用 chat_endpoint() 按协议
选 URL+鉴权头; anthropic 差异面=system 提顶层/无 response_format/content 块
只取 text(glm-5.x thinking 块跳过)/usage input→prompt 归一(漏斗计量不漂移)。
草稿/分解/测试连接/漏斗 lane 四消费方同 client 一处修复全生效。
venv310 实测 82 绿(含 anthropic 协议 8 新测); 档随码走=总设计 §13 P5.1 补
端点勘误节。
2026-10-08 23:07:20 +08:00

138 lines
5.7 KiB
Python

# tests/api/test_llm_provider.py
"""LLM provider 表与 env 解析(C6:TradingAgents ProviderSpec 范式,glm_cn 默认行)."""
import pytest
from sanguo_api.llm.provider import (
LLMConfig, LLMConfigError, PROVIDERS, chat_endpoint, chat_url,
messages_url, resolve_config,
)
def _env(**kw):
base = {
"SANGUO_LLM_PROVIDER": "",
"SANGUO_LLM_BASE_URL": "",
"SANGUO_LLM_MODEL": "",
"SANGUO_LLM_API_KEY": "sk-test",
}
base.update(kw)
return base
class TestProviderTable:
def test_glm_cn_default_row(self):
# 10-08 勘误:glm_cn 指向 coding plan 接入点(anthropic 兼容)——
# paas/v4 是按量计费端点,coding key 打它=1113 余额不足(VPS 实测)
spec = PROVIDERS["glm_cn"]
assert spec.base_url == "https://open.bigmodel.cn/api/anthropic"
assert spec.default_model == "glm-5.3-flash"
assert spec.api_key_env == "SANGUO_LLM_API_KEY"
assert spec.protocol == "anthropic"
def test_openai_compat_row_requires_env_fill(self):
spec = PROVIDERS["openai_compat"]
assert spec.base_url == "" and spec.default_model == ""
class TestResolveConfig:
def test_defaults_to_glm_cn(self):
cfg = resolve_config(_env())
assert (cfg.provider, cfg.model) == ("glm_cn", "glm-5.3-flash")
assert cfg.base_url == "https://open.bigmodel.cn/api/anthropic"
assert cfg.protocol == "anthropic"
assert cfg.api_key == "sk-test"
def test_env_overrides_win(self):
cfg = resolve_config(_env(SANGUO_LLM_BASE_URL="http://10.0.0.1:8000/v4/",
SANGUO_LLM_MODEL="glm-5.3"))
assert cfg.base_url == "http://10.0.0.1:8000/v4/"
assert cfg.model == "glm-5.3"
def test_openai_compat_requires_base_url_and_model(self):
with pytest.raises(LLMConfigError) as e:
resolve_config(_env(SANGUO_LLM_PROVIDER="openai_compat"))
msg = str(e.value)
assert "SANGUO_LLM_BASE_URL" in msg and "SANGUO_LLM_MODEL" in msg
def test_missing_api_key_lists_env_name(self):
with pytest.raises(LLMConfigError) as e:
resolve_config(_env(SANGUO_LLM_API_KEY=""))
assert "SANGUO_LLM_API_KEY" in str(e.value)
class TestChatUrl:
def test_appends_chat_completions_without_v1_magic(self):
# GLM 式 /api/paas/v4/ 不补 /v1(tickflow 归一化教训:补了就 404)
assert chat_url("https://open.bigmodel.cn/api/paas/v4/") == \
"https://open.bigmodel.cn/api/paas/v4/chat/completions"
assert chat_url("http://10.0.0.1:8000/v4") == \
"http://10.0.0.1:8000/v4/chat/completions"
class TestAnthropicProtocol:
"""10-08 coding plan 端点:glm_cn 默认走 anthropic 兼容协议(/v1/messages)."""
def test_messages_url_appends_v1_messages(self):
assert messages_url("https://open.bigmodel.cn/api/anthropic") == \
"https://open.bigmodel.cn/api/anthropic/v1/messages"
assert messages_url("https://open.bigmodel.cn/api/anthropic/") == \
"https://open.bigmodel.cn/api/anthropic/v1/messages"
def test_chat_endpoint_anthropic_uses_x_api_key(self):
cfg = LLMConfig("glm_cn", "https://x/api/anthropic", "m", "sk",
protocol="anthropic")
url, headers = chat_endpoint(cfg)
assert url == "https://x/api/anthropic/v1/messages"
assert headers == {"x-api-key": "sk",
"anthropic-version": "2023-06-01"}
def test_chat_endpoint_openai_keeps_bearer(self):
cfg = LLMConfig("openai_compat", "http://x/v4/", "m", "sk")
url, headers = chat_endpoint(cfg)
assert url == "http://x/v4/chat/completions"
assert headers == {"Authorization": "Bearer sk"}
def test_protocol_env_override_wins(self):
cfg = resolve_config(_env(SANGUO_LLM_PROTOCOL="openai"))
assert cfg.protocol == "openai"
def test_openai_compat_row_defaults_openai_protocol(self):
assert PROVIDERS["openai_compat"].protocol == "openai"
class TestResolveConfigOverrides:
"""P5.1 三层:DB overrides > env > 默认;overrides=None 零行为变化."""
def test_override_beats_env_and_default(self):
cfg = resolve_config(_env(), overrides={
"provider": "openai_compat", "base_url": "http://x/v1/",
"model": "m2", "api_key": "sk-db", "timeout": "30"})
assert (cfg.provider, cfg.base_url, cfg.model, cfg.api_key) == \
("openai_compat", "http://x/v1/", "m2", "sk-db")
assert cfg.timeout == 30.0
def test_env_fills_when_override_absent(self):
cfg = resolve_config(_env(SANGUO_LLM_MODEL="env-model"),
overrides={"api_key": "sk-db"})
assert cfg.model == "env-model" and cfg.api_key == "sk-db"
def test_empty_override_falls_through(self):
# 空串 override=无此层(设置页"清空"语义由 DELETE 行实现,不靠空串)
cfg = resolve_config(_env(), overrides={"model": " "})
assert cfg.model == "glm-5.3-flash"
def test_disabled_override_raises(self):
with pytest.raises(LLMConfigError, match="停用"):
resolve_config(_env(), overrides={"enabled": "0"})
def test_timeout_env_fallback_and_bounds(self):
assert resolve_config(
_env(SANGUO_LLM_TIMEOUT="15")).timeout == 15.0
with pytest.raises(LLMConfigError, match="timeout"):
resolve_config(_env(), overrides={"timeout": "900"})
with pytest.raises(LLMConfigError, match="非数字"):
resolve_config(_env(), overrides={"timeout": "abc"})
def test_no_overrides_backward_compatible(self):
assert resolve_config(_env()) == resolve_config(_env(), overrides=None)