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` **两半**,语义不变但对形态重构免疫。
68 lines
3.2 KiB
Go
68 lines
3.2 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ 本文件把「上游客观缺口」写成**可执行的判据**,而不是只留在文档里。
|
||
//
|
||
// 为什么需要:这些能力缺口多次被误判成"我们漏实现了"(我自己就误判了两轮),
|
||
// 于是反复派人去"补"。判据在此把事实钉住:
|
||
//
|
||
// - qoder 加密端点(agent_chat_generation)需要 `qoder-auth-wasm.wasm`
|
||
// —— 该二进制**在参考仓库里也不存在**(参考自己也是 readFileSync 一个
|
||
// 它不提供的文件)。故我们走明文端点 `/model/v1/chat/completions`,
|
||
// **不经过信封** ⇒ qoder-envelope 的解包能力在明文路径上无从生效。
|
||
// - TRAE 的 PKCE 分支参考**明确不支持**(只有"探测并拒绝",无 code→token
|
||
// 交换端点)⇒ 我们报"暂不支持"是与参考对齐的正确行为。
|
||
//
|
||
// 判据的作用是**防止误判**:任何人(包括未来的我)想"补"这些能力时,
|
||
// 必须先让这个测试失败 —— 也就是先推翻事实,再动手。
|
||
func TestUpstreamGapsAreDocumented(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
t.Run("qoder 走明文端点且不经过信封", func(t *testing.T) {
|
||
// 明文端点(OpenAI 兼容):参考 QODER_CHAT_PATH。
|
||
// ⚠ 断言 **URL 的两半**而不是那一整串字面量:本仓已把"路径从 base_url
|
||
// 移到 endpoint"(内核 URL = base_url + endpoint)—— 继续钉死整串会让
|
||
// 一个纯粹的形态重构把这条**语义**判据弄红,那是判据在咬自己。
|
||
if !strings.Contains(s, `base_url = "https://api2-v2.qoder.sh"`) ||
|
||
!strings.Contains(s, `endpoint = "/model/v1/chat/completions"`) {
|
||
t.Fatal("qoder 的 chat 端点变了 —— 若改成加密端点 agent_chat_generation," +
|
||
"则**必须**同时实现信封解包(qoder-envelope)与 WASM 签名," +
|
||
"否则响应会被当成畸形数据")
|
||
}
|
||
// 明文端点没有信封层,故解包**不该**存在(存在反而是死代码)
|
||
if strings.Contains(s, "unwrapQoderEnvelope") {
|
||
t.Fatal("出现了 qoder 信封解包 —— 明文端点没有信封层。" +
|
||
"要么端点已换成加密端点(那需要 wasm),要么这是死代码")
|
||
}
|
||
})
|
||
|
||
t.Run("TRAE PKCE 明确报不支持(与参考一致)", func(t *testing.T) {
|
||
if !strings.Contains(s, "本实现暂不支持该流程") {
|
||
t.Fatal("TRAE 的 PKCE 分支必须明确报出「暂不支持」—— " +
|
||
"参考 trae-oauth.ts 也只做探测+拒绝,没有 code→token 交换端点。" +
|
||
"若改为尝试交换,会拿 code 去打要 RefreshToken 的 ExchangeToken(必然 400)")
|
||
}
|
||
})
|
||
|
||
t.Run("codearts 目录打两个签名端点(本轮补上)", func(t *testing.T) {
|
||
for _, want := range []string{
|
||
"opengw.developer.huaweicloud.com/api/v1/gateway/config",
|
||
"snap-access.cn-north-4.myhuaweicloud.com/v1/model/builtin",
|
||
`sign = "codearts"`,
|
||
} {
|
||
if !strings.Contains(s, want) {
|
||
t.Fatalf("codearts 目录缺 %q —— 参考 fetchCodeArtsRemoteModels 打两个"+
|
||
"**必须 AK/SK 签名**的端点,只打一个会少一半模型", want)
|
||
}
|
||
}
|
||
})
|
||
}
|