feat(models): unifyllm 网关接入,5 个国际旗舰模型进 pro 档(bump 0.57.0)

- config/models/unifyllm.yaml:Claude Fable 5 / Opus 4.8 / Sonnet 4.6 /
  GPT-5.6 Sol / Gemini 3.1 Pro,litellm openai/ 前缀 + api_base 覆盖,零代码
- 实测:gpt-5.x litellm 硬拦 temp!=1、gemini-3.1-pro temp=0.3 返回空,
  两者 optimal_temperature=1.0;流式+tool calling+parallel 5 模型全通
- scripts/diag_unifyllm.py 冒烟脚本(--stream/--temp)
- 服务器需代理出口(直连 unifyllm.ai TLS 失败),RUN.md env 段已注明

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-10 17:16:46 +08:00
parent daf0db854a
commit ba2afa9e98
7 changed files with 230 additions and 1 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.57.0 — 2026-07-10
- Pro 档新增 5 个国际旗舰模型:Claude Fable 5、Claude Opus 4.8、Claude Sonnet 4.6、GPT-5.6 Sol、Gemini 3.1 Pro,模型选单中直接选用。
## 0.56.0 — 2026-07-10 ## 0.56.0 — 2026-07-10
- 长时间运行的脚本 / 命令支持**后台执行**:耗时任务(大批量数据处理、长时间计算等)自动转入后台独立运行,**不再被超时打断,服务更新也不中断**。 - 长时间运行的脚本 / 命令支持**后台执行**:耗时任务(大批量数据处理、长时间计算等)自动转入后台独立运行,**不再被超时打断,服务更新也不中断**。

View File

@ -23,6 +23,7 @@
### 2026-07 ### 2026-07
- **07-10 / 0.57.0**:**unifyllm 网关接入(5 个国际旗舰模型进 pro 档)**:新档案 `config/models/unifyllm.yaml`(fable5/opus48/sonnet46/gpt56_sol/gemini31_pro,litellm `openai/` 前缀 + `api_base=https://unifyllm.ai/v1` + `UNIFYLLM_API_KEY`,零代码改动),`agent.yaml model_tiers.pro` 加 5 条。实测坑(全记在 yaml 头注释):gpt-5.x 被 litellm 硬拦 temp≠1、gemini-3.1-pro temp=0.3 返回空 → 两者 `optimal_temperature: 1.0`;`parallel_tool_calls` 参数网关接受(claude 实测 2 并行);流式 + include_usage 5 模型全通。新 `scripts/diag_unifyllm.py`(--stream/--temp,列模型 GET /v1/models)。**部署注意:服务器直连 unifyllm.ai TLS 握手失败,需代理出口(HTTPS_PROXY),部署前先跑 diag 验证**。价格字段暂 0(网关价目未知,成本先记 0)。
- **07-10 / 0.56.0**:**后台进程 bg proc(DESIGN §8.12)**——解掉"长脚本被工具超时掐死 / 占死 run / 蓝绿切换陪葬"三连:`shell`/`run_python` 加 `background=true`(模型按预计时长自选,前台超时报错里提示改走后台;默认上限 7200s、cap 86400s、每用户并发 3 个 `ZCBOT_MAX_BG_PROCS`),新 `check_process` 工具查状态/日志尾部/kill。状态协议纯文件(`<user_root>/.zcbot_procs/<task_id>/<proc_id>/{proc.json,output.log,exit_code}`,dotfile 用户不可见,exit_code 出现即终态),无 DB 无队列组件。host 模式 detach 独立 wrapper(`core/proc_wrapper.py`,stdlib-only,限时+杀树+截尾 10MB);docker 模式**专用容器** `zcbot-proc-<id>`(`pool.run_proc_container`,同款硬化/iptables init,product=proc + 无 instance label → 与 sandbox 的 idle reaper/shutdown_all 生命周期解耦,dockerd 托管扛蓝绿)。回收:check_process 见终态顺手 rm 容器 + web lifespan 每小时 `procs.sweep`(终态目录 7d TTL、exited 孤儿容器)。**前端可视 + 通知**:`GET /v1/procs`(用户级)+ `POST /v1/tasks/{id}/procs/{pid}/kill` 两端点(纯文件读取无 DB);`procs.js` 把 `[Background]` 工具结果卡活化(spinner+summary 跳秒+停止按钮,与前台工具卡同体验;历史重渲同样恢复活态/终态定格 exit·耗时)+ running→终态弹 toast(右下角,跨 task 也提醒、点击跳转对应任务)+ **对话锁**(bg proc 运行期 composer 发送→停止、Enter 拦截,观感与前台执行一致,完成的那次轮询解锁;锁仅前端,服务端不 409——停止入口须可达);轮询式(仅有 running proc 时 5s 一拉,触发点:登录/选 task/run 收尾/[Background] 工具结果)——不走 SSE,proc 完成时刻往往没有活跃 run。LLM 超时最终定为**显式化默认 600s + env 可调**(长工具已走后台,LLM 单调用无正当 600s 静默;被掐再调 `ZCBOT_LLM_TIMEOUT_S`)。手工验证 `scripts/test_bgproc_manual.py` 全过(win host 模式:启动/探活/exit 0/日志/kill 137/sweep),unittest 全量 201 过。同批带上:**seedance `resume_task_id` 续查**(超时/中断后拿 cgt_id 跳过提交直接续轮询,不过配额闸不重复计费;fast/pro poll_timeout 统一 1200s)+ **LLM 超时显式化**(`core/llm.py` 显式传 litellm timeout,env `ZCBOT_LLM_TIMEOUT_S` 可调,见后)。 - **07-10 / 0.56.0**:**后台进程 bg proc(DESIGN §8.12)**——解掉"长脚本被工具超时掐死 / 占死 run / 蓝绿切换陪葬"三连:`shell`/`run_python` 加 `background=true`(模型按预计时长自选,前台超时报错里提示改走后台;默认上限 7200s、cap 86400s、每用户并发 3 个 `ZCBOT_MAX_BG_PROCS`),新 `check_process` 工具查状态/日志尾部/kill。状态协议纯文件(`<user_root>/.zcbot_procs/<task_id>/<proc_id>/{proc.json,output.log,exit_code}`,dotfile 用户不可见,exit_code 出现即终态),无 DB 无队列组件。host 模式 detach 独立 wrapper(`core/proc_wrapper.py`,stdlib-only,限时+杀树+截尾 10MB);docker 模式**专用容器** `zcbot-proc-<id>`(`pool.run_proc_container`,同款硬化/iptables init,product=proc + 无 instance label → 与 sandbox 的 idle reaper/shutdown_all 生命周期解耦,dockerd 托管扛蓝绿)。回收:check_process 见终态顺手 rm 容器 + web lifespan 每小时 `procs.sweep`(终态目录 7d TTL、exited 孤儿容器)。**前端可视 + 通知**:`GET /v1/procs`(用户级)+ `POST /v1/tasks/{id}/procs/{pid}/kill` 两端点(纯文件读取无 DB);`procs.js` 把 `[Background]` 工具结果卡活化(spinner+summary 跳秒+停止按钮,与前台工具卡同体验;历史重渲同样恢复活态/终态定格 exit·耗时)+ running→终态弹 toast(右下角,跨 task 也提醒、点击跳转对应任务)+ **对话锁**(bg proc 运行期 composer 发送→停止、Enter 拦截,观感与前台执行一致,完成的那次轮询解锁;锁仅前端,服务端不 409——停止入口须可达);轮询式(仅有 running proc 时 5s 一拉,触发点:登录/选 task/run 收尾/[Background] 工具结果)——不走 SSE,proc 完成时刻往往没有活跃 run。LLM 超时最终定为**显式化默认 600s + env 可调**(长工具已走后台,LLM 单调用无正当 600s 静默;被掐再调 `ZCBOT_LLM_TIMEOUT_S`)。手工验证 `scripts/test_bgproc_manual.py` 全过(win host 模式:启动/探活/exit 0/日志/kill 137/sweep),unittest 全量 201 过。同批带上:**seedance `resume_task_id` 续查**(超时/中断后拿 cgt_id 跳过提交直接续轮询,不过配额闸不重复计费;fast/pro poll_timeout 统一 1200s)+ **LLM 超时显式化**(`core/llm.py` 显式传 litellm timeout,env `ZCBOT_LLM_TIMEOUT_S` 可调,见后)。
- **07-10 / 0.55.2**:文件面板根目录隐藏系统工作目录:`/v1/files`(仅根层)与 `/v1/folders`(新建任务目录候选)过滤定时任务执行目录 / 渠道镜像对话目录(`web/app.py::_system_wd_names`,DB 回查 `scheduled_job_id`/`channel` 判定而非硬编码名字前缀 —— 孤儿目录(task 已删)无任务入口,刻意留在根目录可见)。只是列表降噪非权限拦截:带 path 直接访问照常放行,点定时任务运行历史 / 微信卡片时文件面板自动跳入该目录不受影响(chat.js 既有逻辑,前端零改动)。 - **07-10 / 0.55.2**:文件面板根目录隐藏系统工作目录:`/v1/files`(仅根层)与 `/v1/folders`(新建任务目录候选)过滤定时任务执行目录 / 渠道镜像对话目录(`web/app.py::_system_wd_names`,DB 回查 `scheduled_job_id`/`channel` 判定而非硬编码名字前缀 —— 孤儿目录(task 已删)无任务入口,刻意留在根目录可见)。只是列表降噪非权限拦截:带 path 直接访问照常放行,点定时任务运行历史 / 微信卡片时文件面板自动跳入该目录不受影响(chat.js 既有逻辑,前端零改动)。
- **07-10 / 0.55.1**:per-user 磁盘配额 5GB→20GB(`config/agent.yaml quotas.disk_bytes_per_user`;RUN 故障兜底行、CHANGELOG 同步)。仍是应用层软配额,OS 层 xfs prjquota 兜底照旧留待外部用户开放前(§7.5 #4)。 - **07-10 / 0.55.1**:per-user 磁盘配额 5GB→20GB(`config/agent.yaml quotas.disk_bytes_per_user`;RUN 故障兜底行、CHANGELOG 同步)。仍是应用层软配额,OS 层 xfs prjquota 兜底照旧留待外部用户开放前(§7.5 #4)。

5
RUN.md
View File

@ -42,6 +42,11 @@
# 共享同一台推理服务 http://182.54.21.126:9000/v1)。涉密任务用户显式选 `local.r1` / `local.qwq` # 共享同一台推理服务 http://182.54.21.126:9000/v1)。涉密任务用户显式选 `local.r1` / `local.qwq`
# 代替默认 deepseek_v4.flash;未设 env 时这两条 variant 调用即抛 RuntimeError(其他模型不影响) # 代替默认 deepseek_v4.flash;未设 env 时这两条 variant 调用即抛 RuntimeError(其他模型不影响)
LOCAL_LLM_API_KEY=... LOCAL_LLM_API_KEY=...
# unifyllm 网关(config/models/unifyllm.yaml,Claude/GPT/Gemini 国际旗舰,pro 档):可选。
# 未设:unifyllm.* 5 个 variant 调用即抛 RuntimeError(其他模型不影响)。
# ★ 服务器直连 unifyllm.ai TLS 握手失败,需代理出口(HTTPS_PROXY);
# 部署前 `.venv/Scripts/python.exe scripts/diag_unifyllm.py` 冒烟验证(5 模型 tool-calling)
UNIFYLLM_API_KEY=sk-...
ZCBOT_DB_URL=postgresql://user:pass@host:5432/zcbot ZCBOT_DB_URL=postgresql://user:pass@host:5432/zcbot
# main.py web 必填(probe/db/user 不验) # main.py web 必填(probe/db/user 不验)
PLATFORM_KEY=<≥16 字符随机串,platform 机器对机器入口校验> PLATFORM_KEY=<≥16 字符随机串,platform 机器对机器入口校验>

View File

@ -28,6 +28,11 @@ model_tiers:
- doubao.evolving - doubao.evolving
- glm.pro - glm.pro
- glm.pro52 - glm.pro52
- unifyllm.fable5 # unifyllm 网关(海外旗舰,走代理出口)
- unifyllm.opus48
- unifyllm.sonnet46
- unifyllm.gpt56_sol
- unifyllm.gemini31_pro
- seedream_5 - seedream_5
- seedance_2_fast - seedance_2_fast
- seedance_2_pro - seedance_2_pro

114
config/models/unifyllm.yaml Normal file
View File

@ -0,0 +1,114 @@
# unifyllm 网关模型档案(https://unifyllm.ai,多协议 LLM 网关)
# 走 OpenAI 兼容端点:litellm `openai/` 前缀 + api_base 覆盖,key 用 Bearer 认证。
# 网关上还有 claude-sonnet-5 / opus 4-5~4-7 / gpt-5.4/5.5 / gemini-3.5-flash 等,
# 按需加 variant 即可(GET /v1/models 可拉全量,见 scripts/diag_unifyllm.py)。
#
# 注意事项(2026-07-10 实测,见 diag_unifyllm.py):
# - gpt-5.x:litellm 客户端硬拦 temperature != 1,optimal_temperature 必须 1.0
# - gemini-3.1-pro:temperature 0.3 时返回空内容(0 tool_call 0 text),必须 1.0
# - parallel_tool_calls 参数网关接受(claude 实测返回 2 个并行调用)
# - thinking/reasoning_effort 透传未验证,先全 false(同 glm.yaml 的 TODO 思路)
# - 价格字段留 0:网关价目未知,拿到价目表后补(成本先记 0)
# - 服务器直连 unifyllm.ai TLS 握手失败,需代理出口(HTTPS_PROXY);部署前先跑 diag 验证
family: unifyllm
variants:
fable5:
display_name: Claude Fable 5
model_id: openai/claude-fable-5
api_base: https://unifyllm.ai/v1
api_key_env: UNIFYLLM_API_KEY
max_context: 200000
reliable_context: 120000
max_output: 8192
parallel_tools: true
tool_calling_quality: excellent
thinking_mode: false
reasoning_effort_levels: []
default_reasoning_effort: ""
code_quality: excellent
enable_run_python: true
max_iterations: 150
optimal_temperature: 0.3
prompt_caching: false # anthropic-beta header 走 OpenAI 兼容端点无效,不发
extended_thinking: false
opus48:
display_name: Claude Opus 4.8
model_id: openai/claude-opus-4-8
api_base: https://unifyllm.ai/v1
api_key_env: UNIFYLLM_API_KEY
max_context: 200000
reliable_context: 120000
max_output: 8192
parallel_tools: true
tool_calling_quality: excellent
thinking_mode: false
reasoning_effort_levels: []
default_reasoning_effort: ""
code_quality: excellent
enable_run_python: true
max_iterations: 150
optimal_temperature: 0.3
prompt_caching: false
extended_thinking: false
sonnet46:
display_name: Claude Sonnet 4.6
model_id: openai/claude-sonnet-4-6
api_base: https://unifyllm.ai/v1
api_key_env: UNIFYLLM_API_KEY
max_context: 200000
reliable_context: 120000
max_output: 8192
parallel_tools: true
tool_calling_quality: excellent
thinking_mode: false
reasoning_effort_levels: []
default_reasoning_effort: ""
code_quality: excellent
enable_run_python: true
max_iterations: 120
optimal_temperature: 0.3
prompt_caching: false
extended_thinking: false
gpt56_sol:
display_name: GPT-5.6 Sol
model_id: openai/gpt-5.6-sol
api_base: https://unifyllm.ai/v1
api_key_env: UNIFYLLM_API_KEY
max_context: 262144
reliable_context: 131072
max_output: 8192
parallel_tools: true
tool_calling_quality: excellent
thinking_mode: false
reasoning_effort_levels: []
default_reasoning_effort: ""
code_quality: excellent
enable_run_python: true
max_iterations: 120
optimal_temperature: 1.0 # 硬约束:litellm 拦 gpt-5* 的 temp != 1
prompt_caching: false
extended_thinking: false
gemini31_pro:
display_name: Gemini 3.1 Pro
model_id: openai/gemini-3.1-pro-preview
api_base: https://unifyllm.ai/v1
api_key_env: UNIFYLLM_API_KEY
max_context: 1000000
reliable_context: 262144
max_output: 8192
parallel_tools: false # gemini 走网关未实测该参数,保守关闭
tool_calling_quality: good
thinking_mode: false
reasoning_effort_levels: []
default_reasoning_effort: ""
code_quality: excellent
enable_run_python: true
max_iterations: 120
optimal_temperature: 1.0 # 硬约束:0.3 时返回空内容(实测)
prompt_caching: false
extended_thinking: false

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.56.0" __version__ = "0.57.0"

100
scripts/diag_unifyllm.py Normal file
View File

@ -0,0 +1,100 @@
"""unifyllm 网关连通性 / tool-calling 冒烟测试。
用法(.env 在仓库根, UNIFYLLM_API_KEY):
.venv/Scripts/python.exe scripts/diag_unifyllm.py [model_id ...]
不带参数时测默认 5 个精选模型 litellm openai/ 前缀 + api_base,
core/llm.py 线上路径一致输出一律 ASCII(Windows 控制台 GBK)
"""
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env_file = os.path.join(ROOT, ".env")
if os.path.exists(env_file):
for line in open(env_file, encoding="utf-8"):
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")
import litellm # noqa: E402
API_BASE = "https://unifyllm.ai/v1"
DEFAULT_MODELS = [
"claude-fable-5",
"claude-opus-4-8",
"claude-sonnet-4-6",
"gpt-5.6-sol",
"gemini-3.1-pro-preview",
]
TOOLS = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
def main() -> int:
key = os.environ.get("UNIFYLLM_API_KEY")
if not key:
print("[FAIL] UNIFYLLM_API_KEY not set (check .env)")
return 1
args = sys.argv[1:]
force_temp = None
stream = False
for a in list(args):
if a.startswith("--temp="):
force_temp = float(a.split("=", 1)[1])
args.remove(a)
elif a == "--stream":
stream = True
args.remove(a)
models = args or DEFAULT_MODELS
failed = []
for m in models:
# litellm 客户端硬拦 gpt-5* 的 temperature != 1
temp = force_temp if force_temp is not None else (1.0 if m.startswith("gpt-5") else 0.3)
try:
kwargs = dict(
model=f"openai/{m}",
api_base=API_BASE,
api_key=key,
messages=[{"role": "user", "content": "What's the weather in Beijing? Use the tool."}],
tools=TOOLS,
temperature=temp,
timeout=120,
)
if stream:
# 与 core/llm.py chat_stream 同路径:流式 + include_usage,chunk 拼回完整 response
chunks = list(litellm.completion(**kwargs, stream=True,
stream_options={"include_usage": True}))
resp = litellm.stream_chunk_builder(chunks)
else:
resp = litellm.completion(**kwargs)
msg = resp.choices[0].message
tc = msg.tool_calls or []
usage = resp.usage
if tc:
print(f"[OK] {m}: tool_call={tc[0].function.name}({tc[0].function.arguments.strip()}) "
f"tokens={usage.prompt_tokens}+{usage.completion_tokens}")
else:
text = (msg.content or "")[:80].replace("\n", " ")
print(f"[WARN] {m}: no tool_call, text={text!r}")
except Exception as e:
failed.append(m)
print(f"[FAIL] {m}: {type(e).__name__}: {str(e)[:200]}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())