Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions internal/server/paid_gate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading