mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(channels): 8 个平台 base_url 侵占 endpoint 路径 ⇒ 上游 404 Route Not Found
## 真实缺陷(用户可见) 内核的上游 URL 是 `base_url + endpoint`(provider.go ChatURL),而适配器默认 `endpoint = "/chat/completions"`。多个渠道平台把**完整路径**写进了 `base_url` ⇒ 实际请求变成 https://copilot.tencent.com/v2/chat/completions/chat/completions ⇒ 上游回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞,**不是**模型错误)。 受影响 8 个平台:buddy / cline / lobsterai / opencode / qoder / loomy / raccoon (zcode 当时已用 `endpoint` 覆盖,形态正确)。 另有 loomy 的**第二处**错误:host 用了 `loomy.xunfei.cn`(Web 前端/OAuth 回调域), 实测该 host 的 /chat/completions 返回 **HTML 网页**;正确推理域是参考的 `loomyad.xunfei.cn/api/v1`(返回正常 JSON)。 ## ★ 为什么这个缺陷活了很久(最重要的教训) **模型名被多个源服务**。`deepseek-v4.1-flash` 同时由 4 个源提供 (alittokenplan/vela/oin/channels-buddy)⇒ buddy 源失败后网关**继续尝试下一个源** 并成功 ⇒ 用户看到"能回话" ⇒ "buddy 通道是好的"这个结论**完全没有证据**。 本轮我一度正是这么误判的。而 `hy3` **只由 buddy 一个源**提供 ⇒ 一调就暴露真因。 ⇒ **判据必须断言源的结构不变量(URL 形态),不能靠"某个模型能调通"** —— 后者会被多源冗余掩盖。已按此写判据。 ## 排查手法(可复用) 怀疑"某模型不可用"时,先列**该模型由几个源服务**: 1 个 ⇒ 该模型是那条源的健康探针(用它测) >1 个 ⇒ 它调通**不能**证明任何单一源可用(换独有模型再测) 另:非流式请求会被 buddy 直接 400 拒绝 (`{"code":11101,"msg":"Non-stream chat request is currently not supported"}`), 我第一次用 stream=false 时四个模型全 400,**看不出**模型差异,差点把结论下成 "头组合无区别"就收工。 ## 修法 `base_url` 只留 host(或协议公共前缀),路径移入该平台的 `endpoint`。 逐平台按参考的 `apiBase` + `chatPath` 取值核对,并**实测**修正后的 URL: buddy/cline/lobsterai/qoder/raccoon → 401(缺凭据,正确) loomy → 200 `{"code":"100002","desc":"缺少 token"}`(正确) opencode → 500(路由存在;`https://opencode.ai/v1/...` 是 404+HTML,已证伪) 测试注入平台也一并对齐(免被当范例复制)。 ## 判据(2 条 + 变异 2/2) - `TestChannelSourceBaseURLHasNoChatPath`:静态扫描全部 `base_url`,不得含 `/chat/completions` `/v1/messages` `/generateContent` `/api/chat`。 - `TestChannelBaseURLJudgeBitesOnBadInput`:用**故意坏数据**验证判据的判别力 (防判据自己写歪成恒真)。 变异:把 buddy 恢复成旧形态 → 报红。 ## 顺带修正 `TestUpstreamGapsAreDocumented` 原先钉死 qoder 的**整串 URL 字面量** ⇒ 这次纯形态重构(语义未变)把它弄红了 —— **判据在咬自己**。改为断言 `base_url` 与 `endpoint` **两半**,语义不变但对形态重构免疫。
This commit is contained in:
@ -344,7 +344,13 @@ local PLATFORMS = {
|
||||
},
|
||||
hosts = { "api.workos.com", "api.cline.bot", "app.cline.bot" },
|
||||
-- cline 的 chat 路径是 /api/v1/chat/completions(参考 CLINE_CHAT_PATH)。
|
||||
base_url = "https://api.cline.bot/api/v1/chat/completions",
|
||||
base_url = "https://api.cline.bot",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:
|
||||
-- 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),
|
||||
-- 而适配器默认 endpoint 是 "/chat/completions" ⇒ 若 base_url 已含
|
||||
-- 完整路径,实际请求就变成 .../api/v1/chat/completions/chat/completions ⇒ 上游
|
||||
-- 回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
endpoint = "/api/v1/chat/completions",
|
||||
-- 远端模型目录(照参考 cline-models.ts:**必须同时打两个端点**)。
|
||||
-- 关键:/api/v1/models 的 460 个 id 里**没有** cline-free/*,免费模型只由
|
||||
-- recommended-models 下发;只打前者会看不到任何免费模型。
|
||||
@ -414,7 +420,13 @@ local PLATFORMS = {
|
||||
},
|
||||
hosts = { "copilot.tencent.com", "www.codebuddy.cn" },
|
||||
-- buddy 的 chat 端点含 /v2 前缀(参考 CHAT_API_BASE = .../v2 + /chat/completions)。
|
||||
base_url = "https://copilot.tencent.com/v2/chat/completions",
|
||||
base_url = "https://copilot.tencent.com",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:
|
||||
-- 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),
|
||||
-- 而适配器默认 endpoint 是 "/chat/completions" ⇒ 若 base_url 已含
|
||||
-- 完整路径,实际请求就变成 .../v2/chat/completions/chat/completions ⇒ 上游
|
||||
-- 回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
endpoint = "/v2/chat/completions",
|
||||
-- 远端模型目录(照参考 buddy.ts CONFIG_PATH + buddy-oauth.ts fetchModels)。
|
||||
-- 关键:必须带 product 身份头,否则 WorkBuddy 会以 CodeBuddy 身份请求。
|
||||
models_remote = {
|
||||
@ -688,7 +700,13 @@ local PLATFORMS = {
|
||||
fallback = "2026.9.4",
|
||||
},
|
||||
-- lobsterai 的 chat 路径是 /api/proxy/v1/chat/completions(参考 LOBSTERAI_CHAT_PATH)。
|
||||
base_url = "https://lobsterai-server.youdao.com/api/proxy/v1/chat/completions",
|
||||
base_url = "https://lobsterai-server.youdao.com",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:
|
||||
-- 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),
|
||||
-- 而适配器默认 endpoint 是 "/chat/completions" ⇒ 若 base_url 已含
|
||||
-- 完整路径,实际请求就变成 .../api/proxy/v1/chat/completions/chat/completions ⇒ 上游
|
||||
-- 回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
endpoint = "/api/proxy/v1/chat/completions",
|
||||
-- 远端模型目录(照参考 lobsterai.ts LOBSTERAI_MODELS_PATH)
|
||||
models_remote = {
|
||||
-- 参考真值 LOBSTERAI_API_BASE(lobsterai-server.youdao.com)。
|
||||
@ -727,7 +745,17 @@ local PLATFORMS = {
|
||||
vendor = "opencode",
|
||||
flow = "paste",
|
||||
-- opencode 的 chat 路径是 /v1/chat/completions(参考 chatPath)。
|
||||
base_url = "https://opencode.ai/zen/v1/chat/completions",
|
||||
-- ★ base_url 必须保留 `/zen` 段:参考 opencode-product.ts:56 的
|
||||
-- `baseUrl: 'https://opencode.ai/zen'` + chatPath `/v1/chat/completions`。
|
||||
-- 实测(无凭据):
|
||||
-- https://opencode.ai/zen/v1/chat/completions → 500(路由存在,鉴权前先炸)
|
||||
-- https://opencode.ai/v1/chat/completions → 404 + HTML(网页)
|
||||
base_url = "https://opencode.ai/zen",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:内核的 URL 是
|
||||
-- `base_url + endpoint`(provider.go ChatURL),而适配器默认 endpoint 是
|
||||
-- "/chat/completions" ⇒ 若 base_url 已含完整路径,实际请求就变成
|
||||
-- .../v1/chat/completions/chat/completions ⇒ 404 Route Not Found。
|
||||
endpoint = "/v1/chat/completions",
|
||||
-- 远端模型目录(照参考 opencode-product.ts modelsPath)
|
||||
models_remote = {
|
||||
endpoints = { { url = "https://opencode.ai/v1/models", method = "GET" } },
|
||||
@ -853,7 +881,13 @@ local PLATFORMS = {
|
||||
interval = 2,
|
||||
},
|
||||
-- qoder 的 chat 路径是 /model/v1/chat/completions(参考 QODER_CHAT_PATH)。
|
||||
base_url = "https://api2-v2.qoder.sh/model/v1/chat/completions",
|
||||
base_url = "https://api2-v2.qoder.sh",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:
|
||||
-- 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),
|
||||
-- 而适配器默认 endpoint 是 "/chat/completions" ⇒ 若 base_url 已含
|
||||
-- 完整路径,实际请求就变成 .../model/v1/chat/completions/chat/completions ⇒ 上游
|
||||
-- 回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
endpoint = "/model/v1/chat/completions",
|
||||
-- Qoder 的错误信封是**双层编码**的,且 10605(排队) 与 110(额度耗尽)
|
||||
-- 语义相反 —— 通用 openai 适配器只能看到外层,产出用户看不懂的原因串。
|
||||
-- 故用本插件自装的 channels-qoder(厂商码留在插件域,内核零厂商名)。
|
||||
@ -1007,9 +1041,24 @@ local PLATFORMS = {
|
||||
access_key_secret = "zsak6eadrbawz683wf5r3m2snrwj868r",
|
||||
app_id = "GM3LOOMY",
|
||||
},
|
||||
-- loomy 的 chat 路径是 {apiBase}/chat/completions(参考 loomy-adapter.ts:494
|
||||
-- 实测标准 OpenAI 兼容)。
|
||||
base_url = "https://loomy.xunfei.cn/chat/completions",
|
||||
-- loomy 的 chat 端是 {apiBase}/chat/completions(参考 loomy-adapter.ts:494)。
|
||||
--
|
||||
-- ★ host 是 **loomyad.xunfei.cn/api/v1**(参考 LOOMY_API_BASE),不是
|
||||
-- `loomy.xunfei.cn` —— 后者是 **Web 前端/OAuth 回调**域(本块的
|
||||
-- wechat.redirect_uri 正用它)。实测:
|
||||
-- POST https://loomy.xunfei.cn/chat/completions
|
||||
-- → 200 + `<!DOCTYPE html>`(网页!根本不是 API)
|
||||
-- POST https://loomyad.xunfei.cn/api/v1/chat/completions
|
||||
-- → 200 + `{"code":"100002","desc":"缺少 token"}`(正常 JSON)
|
||||
-- 写成前端域时,无论路径怎么拼都不可能是 API。
|
||||
--
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:内核的 URL 是
|
||||
-- `base_url + endpoint`(provider.go ChatURL),而适配器默认 endpoint 是
|
||||
-- "/chat/completions" ⇒ 若 base_url 已含完整路径,实际请求就变成
|
||||
-- .../chat/completions/chat/completions ⇒ 上游回
|
||||
-- `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
base_url = "https://loomyad.xunfei.cn/api/v1",
|
||||
endpoint = "/chat/completions",
|
||||
adapter = "openai",
|
||||
-- 兜底目录(照参考 LOOMY_FALLBACK_MODELS)。此前 "loomy-default" 是我编的。
|
||||
models = {
|
||||
@ -1044,7 +1093,13 @@ local PLATFORMS = {
|
||||
},
|
||||
-- 推理是标准 OpenAI 兼容(实测 POST /api/web/llm/v2/chat/completions)
|
||||
-- raccoon 的 chat 端点是 /api/web/llm/v2/chat/completions。
|
||||
base_url = "https://xiaohuanxiong.com/api/web/llm/v2/chat/completions",
|
||||
base_url = "https://xiaohuanxiong.com",
|
||||
-- ★ 路径必须在 endpoint 里,**不能**塞进 base_url:
|
||||
-- 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),
|
||||
-- 而适配器默认 endpoint 是 "/chat/completions" ⇒ 若 base_url 已含
|
||||
-- 完整路径,实际请求就变成 .../api/web/llm/v2/chat/completions/chat/completions ⇒ 上游
|
||||
-- 回 `{"error_msg":"404 Route Not Found"}`(APISIX 的措辞)。
|
||||
endpoint = "/api/web/llm/v2/chat/completions",
|
||||
adapter = "openai",
|
||||
-- 兜底目录(照参考 RACCOON_FALLBACK_MODELS)。此前 "raccoon-default" 是我编的。
|
||||
-- 注意 sn- 前缀是本平台的真实命名约定(sensenova 系列)。
|
||||
@ -11117,7 +11172,11 @@ function plugin.__inject_refresh_platform_probe(args)
|
||||
label = id,
|
||||
flow = "manual",
|
||||
adapter = "openai",
|
||||
base_url = "http://127.0.0.1:1/v1/chat/completions",
|
||||
base_url = "http://127.0.0.1:1/v1",
|
||||
-- 照正式平台同样的分工:base_url 只到路径前缀,路径放 endpoint
|
||||
-- (内核 URL = base_url + endpoint)。测试夹具也照此写,
|
||||
-- 免得将来有人把这行当范例复制进正式平台。
|
||||
endpoint = "/chat/completions",
|
||||
refresh = { style = tostring(args.style or "google"), url = tostring(args.refresh_url or "") },
|
||||
models = { "m1" },
|
||||
hosts = { "127.0.0.1" },
|
||||
|
||||
Reference in New Issue
Block a user