From aeab07727a6e1741eb965fd3db0de003a3da0e95 Mon Sep 17 00:00:00 2001 From: swysgh Date: Sat, 24 Oct 2026 21:11:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(model):=20=E5=9B=BD=E9=99=85=E7=89=88?= =?UTF-8?q?=E6=A7=BD=E4=BD=8D=E6=97=81=E8=B7=AF=E6=A8=A1=E5=9E=8B=E4=B8=8D?= =?UTF-8?q?=E8=A2=AB=E8=AE=A2=E9=98=85=E4=B8=93=E4=BA=AB=E9=97=B8=E9=97=A8?= =?UTF-8?q?=E8=AF=AF=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 052df84 的入口闸门按目录 identity_list 判「订阅专享 + 本地区全是 Free → 403」, 但 identity_list 描述的是「直调该模型名」那条路。gpt-6-sol / gpt-6-luna 在 Free 号上 走的是租户槽位 custom_model_gpt-5(槽位里挂 openai/gpt-5.6-sol|luna),实测能出正文; 闸门在 setSlotInBody 之前判,把它们一起 403 挡掉了。 实测(同一台机、同一批 intl 号,只换二进制): 升级前(ff67785):intl:gpt-6-sol → 200 / 2.9s 升级后(db9e30a):intl:gpt-6-sol → 403 model_requires_subscription 日志:拒绝订阅专享模型 gpt-6-sol:intl 地区没有订阅档账号(Free 号调它必 1005,不罚号) 即:新版把国际版唯一能出正文的两个模型砍掉了。 放行条件收紧到「命中 IntlSlotRoute 且 realm=intl」,其余订阅专享模型照旧 403、照旧 不烧请求不罚号。补 TestPaidOnlySlotRoutedIntlModelNotGated 守住这条: 在纯 db9e30a 上该测试 FAIL(403),加上本补丁后 PASS(走上游 1 次)。 --- internal/server/handler.go | 6 +++++- internal/server/paid_gate_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/server/handler.go b/internal/server/handler.go index 4470c8d..e172fa8 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -799,7 +799,11 @@ func (h *Handler) chatCompletions(w http.ResponseWriter, r *http.Request) { // 12 小时。实测(2026-10-21,单国际号)4 个免费模型连打 4/4 全 200,再打一次 // gpt-6.1-sol 就把整个号冷却到次日,剩下 34 个能用的国际模型一起 503。 // 计划是按模型给的,罚却是按号罚的,这里把它挡在发请求之前。 - if h.modelPaidOnly(realm, configName) && !h.realmHasPaidAccount(realm) { + // 例外:走国际版槽位旁路的模型(gpt-6-sol / gpt-6-luna)。目录里的 identity_list 只描述 + // 「直调这个模型名」那条路,槽位里挂的是 openai/gpt-5.6-sol|luna,Free 号实测能出正文。 + // 闸门按目录判会把这条路一起挡掉(本地实测:升级前 200/2.9s,升级后 403)。 + _, slotRouted := upstream.IntlSlotRoute(configName) + if h.modelPaidOnly(realm, configName) && !h.realmHasPaidAccount(realm) && !(slotRouted && realm == auth.RealmIntl) { log.Printf("拒绝订阅专享模型 %s:%s 地区没有订阅档账号(Free 号调它必 1005,不罚号)", configName, realm) writeOpenAIError(w, http.StatusForbidden, "model_requires_subscription", "model "+configName+" requires a paid Trae subscription; every account in this realm is a Free plan") diff --git a/internal/server/paid_gate_test.go b/internal/server/paid_gate_test.go index bae5052..6b5e64b 100644 --- a/internal/server/paid_gate_test.go +++ b/internal/server/paid_gate_test.go @@ -243,3 +243,30 @@ func TestPaidOnlyIsPerRealm(t *testing.T) { t.Fatalf("cn:glm-5.2 code=%d body=%s,国内免费不该被国际版的档位连坐", rec.Code, rec.Body) } } + +// 国际版槽位旁路模型(gpt-6-sol / gpt-6-luna)在目录里也被标成订阅专享,但必须放行: +// identity_list 描述的是「直调模型名」那条路,槽位里挂的是 openai/gpt-5.6-sol, +// Free 号实测能出正文。按目录一刀切会把国际版唯一能用的模型砍掉(升级回归)。 +func TestPaidOnlySlotRoutedIntlModelNotGated(t *testing.T) { + installCatalog(t, upstream.ModelInfo{ID: "gpt-6-sol", ContextWindow: 200000, PaidOnly: true}) + dynamicModelsCache.Lock() + dynamicModelsCache.realmByModel["gpt-6-sol"] = auth.RealmIntl + dynamicModelsCache.paidOnlyByModel[auth.RealmIntl+"/gpt-6-sol"] = true + dynamicModelsCache.Unlock() + + up := &paidGateUpstream{identity: 0} + pool := testPoolWith( + &auth.Auth{UID: "cn1", AccessToken: "at1", ExpiresAt: 9999999999}, + &auth.Auth{UID: "intl1", AccessToken: "at2", ExpiresAt: 9999999999, Domain: "www.trae.ai"}, + ) + h := NewHandler(Config{Pool: pool, Upstream: up.client()}) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, chatReq("intl:gpt-6-sol")) + if rec.Code != 200 { + t.Fatalf("intl:gpt-6-sol code=%d body=%s,槽位旁路模型不该被 403 挡", rec.Code, rec.Body) + } + if up.chats != 1 { + t.Errorf("聊天请求打了 %d 次,期望 1(应真的走上游)", up.chats) + } +}