From ba2afa9e98145904bff205c064b368a1d8f9bbea Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 10 Jul 2026 17:16:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(models):=20unifyllm=20=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E6=8E=A5=E5=85=A5,5=20=E4=B8=AA=E5=9B=BD=E9=99=85=E6=97=97?= =?UTF-8?q?=E8=88=B0=E6=A8=A1=E5=9E=8B=E8=BF=9B=20pro=20=E6=A1=A3(bump=200?= =?UTF-8?q?.57.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CHANGELOG.md | 4 ++ PROGRESS.md | 1 + RUN.md | 5 ++ config/agent.yaml | 5 ++ config/models/unifyllm.yaml | 114 ++++++++++++++++++++++++++++++++++++ core/__init__.py | 2 +- scripts/diag_unifyllm.py | 100 +++++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 config/models/unifyllm.yaml create mode 100644 scripts/diag_unifyllm.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f937ac..d115dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `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 - 长时间运行的脚本 / 命令支持**后台执行**:耗时任务(大批量数据处理、长时间计算等)自动转入后台独立运行,**不再被超时打断,服务更新也不中断**。 diff --git a/PROGRESS.md b/PROGRESS.md index e1a7b26..fcd829a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 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。状态协议纯文件(`/.zcbot_procs///{proc.json,output.log,exit_code}`,dotfile 用户不可见,exit_code 出现即终态),无 DB 无队列组件。host 模式 detach 独立 wrapper(`core/proc_wrapper.py`,stdlib-only,限时+杀树+截尾 10MB);docker 模式**专用容器** `zcbot-proc-`(`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.1**:per-user 磁盘配额 5GB→20GB(`config/agent.yaml quotas.disk_bytes_per_user`;RUN 故障兜底行、CHANGELOG 同步)。仍是应用层软配额,OS 层 xfs prjquota 兜底照旧留待外部用户开放前(§7.5 #4)。 diff --git a/RUN.md b/RUN.md index 0fb9a6a..926398d 100644 --- a/RUN.md +++ b/RUN.md @@ -42,6 +42,11 @@ # 共享同一台推理服务 http://182.54.21.126:9000/v1)。涉密任务用户显式选 `local.r1` / `local.qwq` # 代替默认 deepseek_v4.flash;未设 env 时这两条 variant 调用即抛 RuntimeError(其他模型不影响) 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 # main.py web 必填(probe/db/user 不验) PLATFORM_KEY=<≥16 字符随机串,platform 机器对机器入口校验> diff --git a/config/agent.yaml b/config/agent.yaml index 7789a76..3d0966e 100644 --- a/config/agent.yaml +++ b/config/agent.yaml @@ -28,6 +28,11 @@ model_tiers: - doubao.evolving - glm.pro - glm.pro52 + - unifyllm.fable5 # unifyllm 网关(海外旗舰,走代理出口) + - unifyllm.opus48 + - unifyllm.sonnet46 + - unifyllm.gpt56_sol + - unifyllm.gemini31_pro - seedream_5 - seedance_2_fast - seedance_2_pro diff --git a/config/models/unifyllm.yaml b/config/models/unifyllm.yaml new file mode 100644 index 0000000..7551ff7 --- /dev/null +++ b/config/models/unifyllm.yaml @@ -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 diff --git a/core/__init__.py b/core/__init__.py index 604ca1b..a6b46c4 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.56.0" +__version__ = "0.57.0" diff --git a/scripts/diag_unifyllm.py b/scripts/diag_unifyllm.py new file mode 100644 index 0000000..22e7de0 --- /dev/null +++ b/scripts/diag_unifyllm.py @@ -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())