mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
* fix(ai): 兼容 reasoning 模型 temperature 限制 + 透出上游真实错误 Kimi kimi-k2.7-code 等 reasoning 模型拒绝非约定 temperature (Moonshot 报 "only 1 is allowed for this model"), 之前无条件下发导致 400 配置失败。 - 捕获 temperature 相关 400 后自动去掉 temperature 重试一次 (非流式 + 流式), 不再依赖模型名猜测, 对任意 reasoning 模型稳健 - _format_openai_error 优先透出上游真实 detail, 仅无可读 detail 时回落到 状态码通用文案, 避免吞掉 "model not found" 等排障关键信息 - 前端 Kimi 预设 model 更正为 kimi-k2.7-code * chore(polling): 调整各档位轮询间隔范围 实时行情 (quote_service): - pro 最小间隔 2s → 3s, starter 3s → 6s (expert/free 不变) - DEFAULT_INTERVAL 10s → 6s 五档盘口 (depth_service): - expert 区间上限 300s → 120s - 默认值 20s → 10s 前端兜底默认值同步 (Data/Monitoring/DepthConfigCard): - quote interval fallback 10→6, min 5→6 - depth interval fallback 20→10, expert hi 300→120 --------- Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
143 lines
5.5 KiB
Python
143 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
import openai
|
|
|
|
from app.services.ai_provider import (
|
|
_format_openai_error,
|
|
_is_temperature_rejected,
|
|
normalize_openai_base_url,
|
|
)
|
|
|
|
|
|
def test_normalize_openai_base_url_adds_v1_for_root_gateway():
|
|
assert normalize_openai_base_url("http://ai.zedbox.cn:8080") == "http://ai.zedbox.cn:8080/v1"
|
|
|
|
|
|
def test_normalize_openai_base_url_preserves_v1_base():
|
|
assert normalize_openai_base_url("http://ai.zedbox.cn:8080/v1") == "http://ai.zedbox.cn:8080/v1"
|
|
|
|
|
|
def test_normalize_openai_base_url_strips_chat_completions_path():
|
|
assert normalize_openai_base_url("http://ai.zedbox.cn:8080/v1/chat/completions") == "http://ai.zedbox.cn:8080/v1"
|
|
|
|
|
|
def test_normalize_openai_base_url_preserves_glm_v4():
|
|
"""智谱 GLM 用 /api/paas/v4, 不能强制补成 /v4/v1 (会 404)。"""
|
|
assert normalize_openai_base_url("https://open.bigmodel.cn/api/paas/v4") == "https://open.bigmodel.cn/api/paas/v4"
|
|
|
|
|
|
def test_normalize_openai_base_url_strips_chat_completions_from_glm_v4():
|
|
"""用户填完整 /v4/chat/completions 时, 去掉后缀归一化为 /v4。"""
|
|
assert normalize_openai_base_url("https://open.bigmodel.cn/api/paas/v4/chat/completions") == "https://open.bigmodel.cn/api/paas/v4"
|
|
|
|
|
|
def test_normalize_openai_base_url_preserves_other_version_segments():
|
|
"""其它非 v1 版本号 (/v2 等) 也应保持原样。"""
|
|
assert normalize_openai_base_url("https://example.com/api/v2") == "https://example.com/api/v2"
|
|
|
|
|
|
def test_normalize_openai_base_url_strips_trailing_slash():
|
|
assert normalize_openai_base_url("https://open.bigmodel.cn/api/paas/v4/") == "https://open.bigmodel.cn/api/paas/v4"
|
|
|
|
|
|
def test_format_openai_error_hides_html_gateway_body():
|
|
response = httpx.Response(
|
|
504,
|
|
headers={"content-type": "text/html; charset=utf-8"},
|
|
text="<!DOCTYPE html><html><body><h1>Gateway Timeout</h1></body></html>",
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.InternalServerError("gateway timeout", response=response, body=response.text)
|
|
|
|
message = _format_openai_error(exc)
|
|
|
|
assert message == "AI 服务请求失败(504): AI 上游服务超时, 请稍后重试或检查 AI Base URL / 网络"
|
|
assert "html" not in message.lower()
|
|
assert "Gateway Timeout" not in message
|
|
|
|
|
|
def test_format_openai_error_prefers_upstream_detail_when_available():
|
|
"""有可读的上游 detail 时优先透出, 而不是用 400 通用文案吞掉。"""
|
|
response = httpx.Response(
|
|
400,
|
|
json={"error": {"message": "model context length exceeded"}},
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.BadRequestError(
|
|
"bad request",
|
|
response=response,
|
|
body={"error": {"message": "model context length exceeded"}},
|
|
)
|
|
|
|
message = _format_openai_error(exc)
|
|
|
|
assert message == "AI 服务请求失败(400): model context length exceeded"
|
|
|
|
|
|
def test_format_openai_error_falls_back_to_status_message_without_detail():
|
|
"""上游无可读 detail (如 HTML 网关页) 时, 才回落到 400 通用文案。"""
|
|
response = httpx.Response(
|
|
400,
|
|
headers={"content-type": "text/html; charset=utf-8"},
|
|
text="<!DOCTYPE html><html></html>",
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.BadRequestError("bad request", response=response, body=None)
|
|
|
|
message = _format_openai_error(exc)
|
|
|
|
assert message == "AI 服务请求失败(400): 请求参数无效, 请检查模型名称和上下文长度"
|
|
|
|
|
|
def test_is_temperature_rejected_matches_moonshot_message():
|
|
"""Moonshot 对 reasoning 模型报 'only 1 is allowed for this model'。"""
|
|
response = httpx.Response(
|
|
400,
|
|
json={"error": {"message": "invalid temperature: only 1 is allowed for this model"}},
|
|
request=httpx.Request("POST", "https://api.moonshot.cn/v1/chat/completions"),
|
|
)
|
|
exc = openai.BadRequestError(
|
|
"bad request",
|
|
response=response,
|
|
body={"error": {"message": "invalid temperature: only 1 is allowed for this model"}},
|
|
)
|
|
assert _is_temperature_rejected(exc) is True
|
|
|
|
|
|
def test_is_temperature_rejected_matches_generic_temperature_hint():
|
|
response = httpx.Response(
|
|
400,
|
|
json={"error": {"message": "unsupported parameter: temperature"}},
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.BadRequestError(
|
|
"bad request", response=response,
|
|
body={"error": {"message": "unsupported parameter: temperature"}},
|
|
)
|
|
assert _is_temperature_rejected(exc) is True
|
|
|
|
|
|
def test_is_temperature_rejected_false_for_other_400():
|
|
"""非 temperature 相关的 400 (如 model not found) 不应触发去 temperature 重试。"""
|
|
response = httpx.Response(
|
|
400,
|
|
json={"error": {"message": "model not found"}},
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.BadRequestError(
|
|
"bad request", response=response,
|
|
body={"error": {"message": "model not found"}},
|
|
)
|
|
assert _is_temperature_rejected(exc) is False
|
|
|
|
|
|
def test_is_temperature_rejected_false_for_non_400():
|
|
response = httpx.Response(
|
|
401,
|
|
json={"error": {"message": "invalid api key"}},
|
|
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
|
)
|
|
exc = openai.AuthenticationError("unauthorized", response=response, body=None)
|
|
assert _is_temperature_rejected(exc) is False
|