mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
fix(config): 收敛 Vite 配置并兼容 AI 可选参数
- 移除重复的 Vite JS 配置并阻止 TypeScript 构建重新生成 - 精确识别 temperature 与 reasoning_effort 的 400 错误并有界降级 - 补充 Docker 只读挂载 .env 的安全边界说明
This commit is contained in:
@@ -269,23 +269,20 @@ async def _run_openai_once(
|
||||
client = _openai_client(ai_key, timeout)
|
||||
model = current_ai_model()
|
||||
req_messages = list(messages)
|
||||
kwargs = _openai_kwargs(temperature=temperature, max_tokens=max_tokens)
|
||||
while True:
|
||||
try:
|
||||
resp = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=req_messages,
|
||||
**_openai_kwargs(temperature=temperature, max_tokens=max_tokens),
|
||||
**kwargs,
|
||||
)
|
||||
break
|
||||
except Exception as exc:
|
||||
# Reasoning 类模型 (如 kimi-k2.7-code, deepseek-r1, o 系列) 拒绝非约定
|
||||
# temperature (Moonshot 报 "only 1 is allowed for this model")。不再靠
|
||||
# 模型名猜测, 而是捕获该错误后去掉 temperature 重试一次 —— 对所有此类模型都稳。
|
||||
if temperature is not None and _is_temperature_rejected(exc):
|
||||
resp = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=req_messages,
|
||||
**_openai_kwargs(temperature=None, max_tokens=max_tokens),
|
||||
)
|
||||
else:
|
||||
retry_kwargs = _openai_retry_kwargs(exc, kwargs)
|
||||
if retry_kwargs is not None:
|
||||
kwargs = retry_kwargs
|
||||
continue
|
||||
if _is_openai_transport_error(exc):
|
||||
raise RuntimeError(_format_openai_error(exc)) from exc
|
||||
raise
|
||||
@@ -315,23 +312,22 @@ async def _stream_openai(
|
||||
if delta and delta.content:
|
||||
yield delta.content
|
||||
|
||||
kwargs = _openai_kwargs(temperature=temperature, max_tokens=max_tokens)
|
||||
while True:
|
||||
try:
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=req_messages,
|
||||
**_openai_kwargs(temperature=temperature, max_tokens=max_tokens),
|
||||
**kwargs,
|
||||
stream=True,
|
||||
)
|
||||
break
|
||||
except Exception as exc:
|
||||
# 流尚未开始 yield, 可安全重建: 去掉 temperature 后重开 stream。
|
||||
if temperature is not None and _is_temperature_rejected(exc):
|
||||
stream = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=req_messages,
|
||||
**_openai_kwargs(temperature=None, max_tokens=max_tokens),
|
||||
stream=True,
|
||||
)
|
||||
else:
|
||||
# 流尚未开始 yield, 可安全移除被拒绝的可选参数后重建。
|
||||
retry_kwargs = _openai_retry_kwargs(exc, kwargs)
|
||||
if retry_kwargs is not None:
|
||||
kwargs = retry_kwargs
|
||||
continue
|
||||
if _is_openai_transport_error(exc):
|
||||
raise RuntimeError(_format_openai_error(exc)) from exc
|
||||
raise
|
||||
@@ -358,11 +354,10 @@ def _openai_client(api_key: str, timeout: float):
|
||||
)
|
||||
|
||||
|
||||
# Reasoning / thinking 类模型 (kimi-k2.7-code, deepseek-r1, OpenAI o 系列等) 不接受
|
||||
# 任意 temperature, 上游会以 400 拒绝 (如 Moonshot: "only 1 is allowed for this model")。
|
||||
# 这里不靠模型名猜测, 而是在真正命中该错误后自动去掉 temperature 重试 (见
|
||||
# _run_openai_once / _stream_openai), 对任意 reasoning 模型都稳健。
|
||||
_TEMP_REJECT_HINTS = ("temperature", "only 1 is allowed", "unsupported parameter")
|
||||
# 不同模型可能拒绝 temperature 或 reasoning_effort。这里不靠模型名猜测,
|
||||
# 只在 400 明确指出对应参数时移除该参数并重试; 每个参数最多移除一次。
|
||||
_TEMP_REJECT_HINTS = ("temperature", "only 1 is allowed")
|
||||
_REASONING_EFFORT_REJECT_HINTS = ("reasoning_effort", "reasoning effort")
|
||||
|
||||
|
||||
def _is_temperature_rejected(exc: Exception) -> bool:
|
||||
@@ -370,7 +365,41 @@ def _is_temperature_rejected(exc: Exception) -> bool:
|
||||
if getattr(exc, "status_code", None) != 400:
|
||||
return False
|
||||
text = _openai_error_detail(exc) or str(exc)
|
||||
return any(h in text.lower() for h in _TEMP_REJECT_HINTS)
|
||||
return _openai_error_param(exc) == "temperature" or any(
|
||||
h in text.lower() for h in _TEMP_REJECT_HINTS
|
||||
)
|
||||
|
||||
|
||||
def _is_reasoning_effort_rejected(exc: Exception) -> bool:
|
||||
"""True if the upstream 400 specifically rejects reasoning_effort."""
|
||||
if getattr(exc, "status_code", None) != 400:
|
||||
return False
|
||||
text = _openai_error_detail(exc) or str(exc)
|
||||
return _openai_error_param(exc) == "reasoning_effort" or any(
|
||||
h in text.lower() for h in _REASONING_EFFORT_REJECT_HINTS
|
||||
)
|
||||
|
||||
|
||||
def _openai_error_param(exc: Exception) -> str:
|
||||
body = getattr(exc, "body", None)
|
||||
if not isinstance(body, dict):
|
||||
return ""
|
||||
error = body.get("error")
|
||||
if isinstance(error, dict):
|
||||
body = error
|
||||
return str(body.get("param") or "").strip().lower()
|
||||
|
||||
|
||||
def _openai_retry_kwargs(exc: Exception, kwargs: dict) -> dict | None:
|
||||
"""Remove one explicitly rejected optional argument for a bounded retry."""
|
||||
retry_kwargs = dict(kwargs)
|
||||
if "temperature" in retry_kwargs and _is_temperature_rejected(exc):
|
||||
retry_kwargs.pop("temperature")
|
||||
return retry_kwargs
|
||||
if "reasoning_effort" in retry_kwargs and _is_reasoning_effort_rejected(exc):
|
||||
retry_kwargs.pop("reasoning_effort")
|
||||
return retry_kwargs
|
||||
return None
|
||||
|
||||
|
||||
def _openai_kwargs(*, temperature: float | None, max_tokens: int) -> dict:
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_is_temperature_rejected_matches_moonshot_message():
|
||||
assert _is_temperature_rejected(exc) is True
|
||||
|
||||
|
||||
def test_is_temperature_rejected_matches_generic_temperature_hint():
|
||||
def test_optional_openai_params_use_targeted_400_fallbacks():
|
||||
response = httpx.Response(
|
||||
400,
|
||||
json={"error": {"message": "unsupported parameter: temperature"}},
|
||||
@@ -123,6 +123,29 @@ def test_is_temperature_rejected_matches_generic_temperature_hint():
|
||||
)
|
||||
assert _is_temperature_rejected(exc) is True
|
||||
|
||||
kwargs = {"max_tokens": 1000, "temperature": 0.3, "reasoning_effort": "high"}
|
||||
assert ai_provider._openai_retry_kwargs(exc, kwargs) == {
|
||||
"max_tokens": 1000,
|
||||
"reasoning_effort": "high",
|
||||
}
|
||||
|
||||
response = httpx.Response(
|
||||
400,
|
||||
json={"error": {"message": "unrecognized request argument", "param": "reasoning_effort"}},
|
||||
request=httpx.Request("POST", "https://example.com/v1/chat/completions"),
|
||||
)
|
||||
exc = openai.BadRequestError(
|
||||
"bad request", response=response,
|
||||
body={"error": {"message": "unrecognized request argument", "param": "reasoning_effort"}},
|
||||
)
|
||||
assert _is_temperature_rejected(exc) is False
|
||||
assert ai_provider._is_reasoning_effort_rejected(exc) is True
|
||||
assert ai_provider._openai_retry_kwargs(exc, kwargs) == {
|
||||
"max_tokens": 1000,
|
||||
"temperature": 0.3,
|
||||
}
|
||||
assert kwargs == {"max_tokens": 1000, "temperature": 0.3, "reasoning_effort": "high"}
|
||||
|
||||
|
||||
def test_is_temperature_rejected_false_for_other_400():
|
||||
"""非 temperature 相关的 400 (如 model not found) 不应触发去 temperature 重试。"""
|
||||
|
||||
@@ -88,7 +88,7 @@ AUTH_PASSWORD='你的密码' # 至少 6 位;仅首次生效,已设过则不覆
|
||||
```
|
||||
|
||||
面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署可通过此环境变量预置首个密码。
|
||||
密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;Docker 启动时也会只读挂载原始 `.env`,兼容已有的未加引号配置。
|
||||
密码建议使用单引号包裹,Docker 启动时会把整个原始 `.env` 只读挂载到容器内 `/app/.env`,兼容已有的未加引号配置。容器可以读取其中的密钥但不能修改该文件,请保持主机文件权限为 `600` 并仅运行可信镜像。
|
||||
|
||||
详细步骤、SSH 转发方案、重置密码方法见 [deployment.md → 访问密码设置](./deployment.md#访问密码设置公网部署必读)。
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"composite": true
|
||||
"composite": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
|
||||
const proxyHost = ['0.0.0.0', '::'].includes(backendHost) ? '127.0.0.1' : backendHost;
|
||||
const backendPort = process.env.BACKEND_PORT || '3018';
|
||||
const backendTarget = `http://${proxyHost}:${backendPort}`;
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖
|
||||
port: 3011,
|
||||
proxy: {
|
||||
// dev 时 /api 转发到与启动脚本相同的 FastAPI 地址
|
||||
'/api': {
|
||||
target: backendTarget,
|
||||
// SSE 端点需要禁用缓冲
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (_proxyReq, req) => {
|
||||
if (req.url?.includes('/stream')) {
|
||||
_proxyReq.setHeader('Accept', 'text/event-stream');
|
||||
_proxyReq.setHeader('Cache-Control', 'no-cache');
|
||||
_proxyReq.setHeader('Connection', 'keep-alive');
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
'/health': backendTarget,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 把重型图表库拆到独立 chunk, 避免打进主包 + 让页面按需加载。
|
||||
// 用函数形式按 node_modules 路径匹配, 比对象形式更可靠。
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('echarts'))
|
||||
return 'echarts';
|
||||
if (id.includes('lightweight-charts'))
|
||||
return 'lightweight-charts';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user