mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 真实缺陷(用户可见) 内核的上游 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` **两半**,语义不变但对形态重构免疫。
77 lines
3.1 KiB
Go
77 lines
3.1 KiB
Go
package core
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ 源的 chat URL = `base_url + endpoint`(内核 provider.go ChatURL)。
|
||
//
|
||
// 真实缺陷(生产实测):多个渠道平台的 `base_url` 里塞了**完整路径**,
|
||
// 而适配器默认 endpoint 又是 "/chat/completions" ⇒ 实际请求变成
|
||
//
|
||
// 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 覆盖,形态正确)。
|
||
//
|
||
// ★ 为什么这个缺陷能活很久:**模型名被多个源服务**。
|
||
//
|
||
// `deepseek-v4.1-flash` 同时由 4 个源提供(alittokenplan/vela/oin/channels-buddy…)
|
||
// ⇒ 网关在 buddy 源失败后会**继续尝试下一个源**并成功 ⇒ 用户看到"能回话",
|
||
// 于是"buddy 通道是好的"这个结论**完全没有证据**。
|
||
// 而 `hy3` 只由 buddy 一个源提供 ⇒ 一调就暴露真因。
|
||
// ⇒ 判据必须断言**源的 URL 形态**(结构不变量),不能靠"某个模型能调通"。
|
||
//
|
||
// 判据:base_url 不得自带任何 chat 路径段(它只应是 host[+协议公共前缀]),
|
||
// 路径必须在 `endpoint` 里。
|
||
func TestChannelSourceBaseURLHasNoChatPath(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
// 只在平台定义区里找 base_url(避免误判注释/文档里的示例)
|
||
declRe := regexp.MustCompile(`(?m)^\s*base_url = "([^"]+)"\s*,?$`)
|
||
// 已知适配器会把这些路径各追加一次;出现即说明 base_url 侵占了 endpoint 的地盘
|
||
banned := []string{"/chat/completions", "/v1/messages", "/generateContent", "/api/chat"}
|
||
|
||
found := 0
|
||
for _, m := range declRe.FindAllStringSubmatch(s, -1) {
|
||
url := m[1]
|
||
found++
|
||
for _, b := range banned {
|
||
if strings.Contains(url, b) {
|
||
t.Errorf("base_url 侵占了 endpoint 的路径:%s\n"+
|
||
" 内核的 URL 是 `base_url + endpoint`(provider.go ChatURL),"+
|
||
"适配器默认 endpoint=\"/chat/completions\" ⇒ 实际会请求 %s%s\n"+
|
||
" 修法:base_url 只留 host,把 `%s` 移到该平台的 `endpoint = \"...\"`",
|
||
url, url, b, b)
|
||
}
|
||
}
|
||
}
|
||
if found == 0 {
|
||
t.Fatal("没扫到任何 base_url 声明(判据失效)")
|
||
}
|
||
t.Logf("✓ %d 个 base_url 均不含 chat 路径段", found)
|
||
}
|
||
|
||
// ★ 反过来也要咬:说明该缺陷**确实已被修好**、而不是判据太松。
|
||
// 用一段**故意的坏数据**验证判据的判别力(变异测试的静态等价物)。
|
||
func TestChannelBaseURLJudgeBitesOnBadInput(t *testing.T) {
|
||
bad := ` base_url = "https://copilot.tencent.com/v2/chat/completions",`
|
||
declRe := regexp.MustCompile(`(?m)^\s*base_url = "([^"]+)"\s*,?$`)
|
||
m := declRe.FindStringSubmatch(bad)
|
||
if m == nil {
|
||
t.Fatal("判据的正则匹配不到坏数据 ⇒ 判据无判别力(形同虚设)")
|
||
}
|
||
if !strings.Contains(m[1], "/chat/completions") {
|
||
t.Fatal("坏数据未被识别 ⇒ 判据无判别力")
|
||
}
|
||
t.Log("✓ 判据对坏数据有判别力")
|
||
}
|