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) + } +}