Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.
Coding
切换 LLM 端点(OpenAI 兼容第三方)
Try it把脚本里的 LLM 从一个 OpenAI 兼容端点/模型换到另一个时的排查手册。 重点覆盖三类必踩的坑:**Cloudflare 拦 urllib 默认 UA 导致 403 「error code: 1010」**(会被误判成 key 无效,换真 key 也照样403)、 **切到免 key 端点时空串是真值陷阱**(`configure()` 用 `if api_key:` 会让旧付费 key 沿用 → 必 401),以及**先 --ping 预检再跑批量**的做法。 当用户说「换成 XX 模型」「加个模型」「这个 API 报 403 / 400 / 404」 「AI 调用失败」「key 明明是对的却说无效」「怎么切模型」 「换成免 key 的模型」「切过去之后 401」时使用。
What it does
OpenAI 兼容端点之间迁移,**通常只改 base + model 两个值**,请求体不用动。 但有两件事会咬人,且都会伪装成「key 无效」或「参数错误」。
The skill document
切换 LLM 端点(OpenAI 兼容第三方)
核心结论
OpenAI 兼容端点之间迁移,通常只改 base + model 两个值,请求体不用动。 但有两件事会咬人,且都会伪装成「key 无效」或「参数错误」。
坑一:⚠ 第三方端点前面挂 Cloudflare,urllib 默认 UA 被拦
症状:HTTP 403,响应体是 error code: 1010(有时只有这一个数字,
没有 JSON、没有 errmsg)。
根因:urllib.request 的默认 User-Agent 是
Python-urllib/3.x,Cloudflare 的 WAF 规则直接拦下。这不是鉴权问题,
换真 key 也照样 403——因为请求根本到不了鉴权那一步。
2026-10-01 实测(OpenCode Zen / Space Bunny):
| 请求头 | 结果 |
|---|---|
| 默认(无 UA) | HTTP 403 {"error code": 1010} |
User-Agent: Mozilla/5.0 … Chrome/126.0 | HTTP 401 Invalid API key |
403 → 401 的转换本身就是结论:请求形状已经通了,只差 key。
修法(每次请求都带上,不是只在抓网页时带):
req = urllib.request.Request(
url, data=body,
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + key,
"User-Agent": UA, # ← 关键,别漏
"Accept": "application/json"})
⚠ 别只在抓网页的函数里带 UA,LLM 请求单独漏掉——这个漏项极隐蔽, 代码看着没问题,但一换端点就403,而且所有人都会先怀疑 key。
坑二:先ping 再跑批量
换端点后先发一个极短请求验通,别直接跑一批 20 条核验任务。 一次核验的思考 token 可能上千,key 不对就是白烧时间和额度。
实现要点:ping 不碰任何业务数据、不写任何文件,只发一个 「只回这个 JSON」的请求并打印耗时。
##状态码分诊(别一律报「调用失败」)
这四类指向完全不同的问题,混在一起会让人白排查半天:
| 状态码 | 真实原因 | 下一步 |
|---|---|---|
| 403 | Cloudflare/WAF 拦 UA | 补浏览器 UA(不是 key 问题) |
| 401 | key 无效 / 没这个订阅 | 换 key、查订阅状态 |
| 404 | base 或 model 名写错 | GET /models 列可用值 |
| 400 | 参数被拒 | 多半是 response_format 或 max_tokens 超上限 |
400 最常见的两个原因:
- 端点不支持
response_format={"type":"json_object"}—— 部分第三方端点(尤其聚合/中转站)不支持, 而有些脚本的 JSON 解析链路硬依赖它。 ⚠ 这时要权衡:要么换端点,要么给该端点单独走「提示词里要求返回 JSON- 代码里容错解析」的降级路径,别默默把 JSON 模式关掉。
max_tokens超过端点上限 —— 换个更小的值试。
推理模型的 max_tokens 陷阱
思考 token 也计入 max_tokens。核验类任务(要塞进几万字网页原文、
要求结构化输出)实测一条要烧 4000+ token,其中思考可能占 3600。
⇒ 把上限放宽到能容纳「思考 + 正文」,并在 finish_reason == "length"
时明确报错(内容被截断,JSON 不完整,硬解析会得到莫名其妙的结果):
if ch.get("finish_reason") == "length":
raise RuntimeError("LLM 回答被 max_tokens 截断(finish_reason=length)"
"——模型思考太长,正文没输出完整 JSON")
⚠ 别拿「超时」当失败结论,但 finish_reason=length 是真实截断,必须处理。
base 拼接约定
base = "https://example.com/v1" ← 填到 /v1,不要带 /chat/completions
url = base + "/chat/completions"
base 尾部斜杠会被 rstrip,但 base 里必须包含版本段。
写了 /v1/ 和 /v1 效果一样;写成不带 /v1 会在拼出
/chat/completions 时404。
配置注入的两种方式
# 1) 环境变量(适合容器/CI,密钥不进代码)
BASE = os.environ.get("X_AI_BASE", "https://api.deepseek.com").rstrip("/")
MODEL = os.environ.get("X_AI_MODEL", "deepseek-chat")
KEY = os.environ.get("X_AI_KEY", "")
# 2) 运行期 configure()(适合后台 GUI 存密钥、CLI 临时切换)
def configure(api_key=None, base=None, model=None):
"""不传的项保持原值 —— 后台存 key 就靠这个语义。"""
global KEY, BASE, MODEL
if api_key: KEY = api_key
if base: BASE = base.rstrip("/")
if model: MODEL = model
configure(model="new-model") # 只换模型,沿用原 base/key
⚠ configure() 必须「不传的项保持原值」——如果写成整体覆盖,
后台只存了 key 却把 base/model 清空了,会表现为「存了密钥反而连不上」。
测试怎么写
这几条值得钉死,否则下次重构会静默回归:
- 请求头必须有浏览器 UA —— 用假的
urlopen捕获请求对象断言 (这是本次唯一的真 bug,最该被回归钉住)。 configure()只覆盖传入项(断言只传 model 时 base/key 不变)。- ⚠
configure(api_key="")必须真清空 key —— 用真值判据会静默跳过空串, 免 key 端点必401。这条是 2026-10-04 的事故,务必钉死。 configure()会 rstrip base 的尾部斜杠。- ping 在没 key 时不联网且返回非零。
- ping 对 403/401/404/400 给出各自不同的诊断文案。
- ⚠
ai_ready()/is_no_key_endpoint()的判断口径(无 key 但免 key 端点 → 就绪)。
⚠ 断言请求头时注意 dict(req.headers) 的键是小写,
self.assertIn("Authorization", headers) 会假失败。
⚠⚠ 坑三:切到免 key 端点时,空串是真值陷阱(2026-10-04 血案)
从付费端点(DeepSeek 等)切到免 key 端点时,要改的不只是 base + model。 下面每一条都实测撞过,不是理论风险。
3.1 configure() 用真值判据 ⇒ 旧 key 清不掉 ⇒ 必 401
# ❌ 错的写法
def configure(api_key=None, base=None, model=None):
if api_key: # ← "" 是 falsy,空串被跳过!
KEY = api_key
# ✅ 正解:判据是 is not None
def configure(api_key=None, base=None, model=None):
if api_key is not None:
KEY = api_key
实测后果(2026-10-04,OpenCode Zen / Space Bunny):
| 场景 | 结果 |
|---|---|
空 Bearer(AI_KEY = "") | ✅ 200 |
| 带着旧的 DeepSeek key | ❌ 401 Invalid API key |
后台 GUI「保存」时通常传空串表示「清空 key」—— 真值判据下这个请求
静默不生效,旧付费 key 被沿用,于是拿付费 key 打免 key 端点,必 401。
排查这类 401 时第一件事是打印 AI_KEY 到底是不是空,别盯着 key 有效性查。
⚠ 同一个坑还有 CLI 侧:if args.ai_key or args.ai_base or args.ai_model
会把 --ai-key ""(显式清空)当成「没传」。用
any(v is not None for v in (...))。
3.2 切端点时旧 key 要删干净,不能只改 base/model
配置文件里留着 ai_key 字段 = 每次调用都发付费 key → 401。
正确做法是整个字段删掉,不是留空串(留空串在某些读取侧等价于「有值」)。
3.3门禁别拿「有没有 key」当「能不能运行」
免 key 端点压根没有 key,所以任何 if not key: return 400
的门禁都会把这类配置全挡在门外 —— 表现为「配置明明对了但点运行就报错」。
# ✅ 单一判据,CLI / 后台 / 前端共用
NO_KEY_HOSTS = ("opencode.ai/zen",)
def is_no_key_endpoint(base=None):
return any(h in (base or AI_BASE or "") for h in NO_KEY_HOSTS)
def ai_ready(key=None, base=None):
k = AI_KEY if key is None else key
b = AI_BASE if base is None else base
return bool(k) or is_no_key_endpoint(b)
⚠ 后台状态接口要同时给三个字段,前端只能看 ready:
{"has_key": bool(k), "ready": ai_ready(k, b), "no_key_endpoint": is_no_key_endpoint(b)}
has_key 对免 key 配置恒为 False,前端拿它判「能不能跑」会把可用配置
误报成「未配置 Key」——用户会以为是配置错了,反复瞎填。
⚠ 门禁里引用配置变量前先确认它已赋值:写成
if not ai_ready(st["key"], ...) 而 st = resolve_ai_settings() 在下一行,
会得到 UnboundLocalError(Python 把它当局部变量),前端表现为
HTTP 000 / 连接被拒,日志里才有真因。
3.4 免 key 与付费端点的能力差异要提前说清
免 key 模型往往更慢(实测单条核验 152 秒 vs 付费端点的秒级),
且自报模型名不可信(实测回显 n="ChatGPT")。
换过去之前告诉用户代价,别让「能跑」被误解成「一样好用」。
「模型列表里有 free」≠「现在能调」(2026-10-01 实测)
要回答「现在有哪些免费模型能用」,必须逐个发真实请求,不能只分析模型名。
实测某端点:GET /v1/models 免 key 就能列出 84 个模型,其中 11 个 id 含
free —— 逐个发真实请求后,只有 1 个真能免 key 调用:
| 真实原因 | 数量 | 能否绕过 |
|---|---|---|
free tier can only be used from within <客户端> | 6 | ❌ 服务端校验客户端来源,伪造 X-Client/Origin/UA 全部无效 |
not available in your country | 2 | ❌ 地区限制 |
| 上游 500 / 模型已下线 | 2 | ❌ 临时故障或下线 |
⇒「模型名带 free」不能当可用性证据。免费模型可用性会漂移,
所以要有个独立探测项目定期发真请求,消费方只读它的档案(ok=true 才用)。
免 key 的正确姿势:发空 Bearer,不是省略
| 写法 | 结果 |
|---|---|
Authorization: Bearer (空值) | ✅ 200 |
完全省略 Authorization 头 | ❌ 挂到超时才报错 |
Authorization: Bearer free | ❌ 401 |
Authorization: Bearer sk-xxx(占位) | ❌ 401 |
服务端会校验 key 是否真实存在,只有空值才走免鉴权分支。
所以「我没有 key」不能表达成 Bearer 无key,只能表达成空值。
⚠ 省掉整个头是最容易踩的写法 —— 表现为超时,不像鉴权问题,很难联想到。
⚠ 档案文件绝不能包含密钥
探测项目若要输出「给别的项目消费」的档案,档案里绝不能有 key:
档案会被 HTTP 接口原样返回,也可能被消费方提交进仓库。
正确做法是只写 no_key: true/false 标记(这个源要不要 key),
key 由消费方自己配。再加一层出站脱敏兜底,防将来回归。
⚠ 写测试时别把漏洞行为固化成断言 —— 「带 key 时 auth 落进档案」这种 断言看着合理,实际是在给泄露开后门。改写成「带 key 时档案里也没有 key」。
关联
- 本机已有一个可复用的实现参考:
ai-case-library/scripts/ai_verify.py的llm()/configure()/ping_llm(),以及scripts/test_ai_verify.py里的LlmEndpointTest(7 项, 含「空串必须真清空 key」回归钉)。
⚠ 本 skill 提到的免费模型探测项目已下线(2026-10-04 清理):
free-llm-probe / llm.ydtgo.top 的站点、仓库、DNS 记录、本地目录
全部删除,配套的「可用性档案」机制也已移除。
上面「档案」相关的内容保留为方法论(怎么判断一个端点值不值得用),
但没有现成档案文件可读了 —— 要判断当前有哪些免费模型可用,
得自己写临时脚本逐个发真实请求。
Related skills
Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.
Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.
Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.
Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.
Write, debug, and tune Playwright specs with locator strategy, trace diagnosis, and CI-aware timeouts.
More from oracis
Browse all skills把本地 Skill 打包并发布到 SkillHub(腾讯 skillhub.cn)与 ClawHub,覆盖预检、发布、状态回读与竞品对标全链路,并在上传被拒时用二分/ddmin 脚本把「服务端 WAF 拦内容(566)」与「包结构/字段问题」精确区分开。当用户说「发布技能到市场」「上架 skill」「SkillHub 提交失败」「Failed to fetch」「566」「上传 zip 报错」「技能审核状态」「版本号被拒」时使用。也讲清「入库成功 ≠ 已公开」——用 `mine` 的 `reviewStatus`、重发探测 `409 VERSION_EXISTS`、`/versions/<v>/signature` 三个探针判断版本到底走到哪一步。
平台只有网页后台、没有上传 API 时,用本机已装的 Chrome/Edge + CDP 自动化走完上传/发布/填表流程——复用真实登录态、零安装(不下载 500MB Chromium)、能给文件选择框塞文件;并实测富文本编辑器(公众号 / CMS / 邮件模板)里到底哪些 CSS 能活下来,以及在决定是否接入平台自带的「一键排版 / AI 美化」前先抓它输出做结构化 diff。当用户说「只能网页上传」「后台发布」「网页填表自动提交」「发布到技能市场/各平台控制台」「复用已登录的浏览器」「公众号排版样式不生效/保存后样式没了」「一键排版要不要用」「点了按钮没反应/提交没成功也没报错」时使用。
一键安装并实测验证已验证可用的免费 LLM 到 WorkBuddy/Claude Code,无需注册、无需 API Key。当用户说「装免费模型」「加免费模型」「免费 LLM」「免费模型一键装」「验证免费模型可用吗」「免费模型怎么接入」「帮我配几个免费模型」「监控免费模型」「免费模型挂了吗」「kilo 网关」「kilo-auto」「nemotron 免费」「space-bunny」「智谱 glm 免费」「zhipu 免费」「glm-4-flash」「glm-4.7-flash」「glm-z1-flash」「glm-4v-flash」「探测国外免费模型」「国外平台免费 llm」「优先用免费模型」「读图桥接」「vision bridge」「给 glm-4-flash 加读图」「不读图的模型怎么看图」「阶跃星辰」「stepfun」「step 5」「step-5-preview」「限时免费模型」「ccproxy 读图转发」「给 claude code 加读图」时使用。也用于验证已有免费模型配置是否真的可用(tool calling 是否支持、是否限流、是否支持读图),以及定期 watch 监控免费模型是否下线/限流/失去工具调用能力。
在中国大陆网络环境下用 yt-dlp 下载视频(含 Windows GUI 封装)。当用户要下载 YouTube、Bilibili、SoundCloud 等 yt-dlp 支持的站点内容,或遇到「yt-dlp 装不上 / 下载报 502 / Tunnel connection failed / ffmpeg not found / 缺 JS runtime / TLS fingerprint / watch?v=&list= 把整个列表都下了 / Sign in to confirm you're not a bot / The page needs to be reloaded / 下到一半突然下不动 / 下回来的视频糊、有拖影残影 / 下载速度特别慢」等问题时使用。
聚合搜索百度网盘/夸克网盘/阿里云盘等网盘分享链接。当用户想按作者名、课程名、剧名、书名、软件名找网盘下载路径,或提到「网盘搜索」「PanSou」「盘搜」「找到下载链接」「夸克/百度/阿里网盘资源」时使用。包含已验证的 PanSou 协议端点、绕过系统代理的必要写法、以及可直接运行的本地 Web 应用 panradar.py。
在 WorkBuddy(Windows)环境里交付零依赖本地 Web 应用,并绕开官方测试技能完全不覆盖的本机专属坑。当用户要在本机跑起来一个本地 Web 应用 / 单文件工具 / 案例库 / 看板 / 小工具,或要验证刚写完的 HTML/JS/Python 交付物真的能工作时使用。独家覆盖:Bash 缺 coreutils、bash 是 WSL 启动桩、rm 是坏 shim、PowerShell 不回显 stdout、cwd 漂移、预览面板靠 HEAD 探活——这些在干净 Linux 环境里遇不到。