mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
fix(gateway): AUTO scope grants all models — restrict to routing mode only
A key with scope=[AUTO] could previously: 1. request ANY concrete model id directly (hasScopeModel/checkModelScope treated AUTO as a wildcard) 2. see the full 56-model list on /v1/models (intersectModels considered AUTO as grant-everything) AUTO now only authorizes the AUTO routing mode. Direct requests to a specific model require an explicit scope entry. Also carries agentrouter.lua WAF fingerprint headers (Origin/Referer/ X-Requested-With) already staged on this branch. Tests: TestHasScopeModelWithSourcePrefix updated; full suite green.
This commit is contained in:
@ -115,13 +115,15 @@ func (g *Gateway) resolveCands(ctx context.Context, req *chatRequest) ([]*provid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filterCandsByModels keeps only providers exposing at least one model of the
|
// filterCandsByModels keeps only providers exposing at least one model of the
|
||||||
// scope (used for user keys with a restricted model scope). An "AUTO" scope
|
// scope (used for user keys with a restricted model scope). Scope entries with
|
||||||
// entry means the key is allowed to use any model. Scope entries with a
|
// a Source pinned to a specific upstream narrow the candidates to that source
|
||||||
// Source pinned to a specific upstream narrow the candidates to that source
|
|
||||||
// for the matching model.
|
// for the matching model.
|
||||||
|
//
|
||||||
|
// An "AUTO" scope entry only allows the AUTO routing mode; it does NOT grant
|
||||||
|
// access to specific models.
|
||||||
func filterCandsByModels(cands []*provider.Provider, allow []config.ModelScope) []*provider.Provider {
|
func filterCandsByModels(cands []*provider.Provider, allow []config.ModelScope) []*provider.Provider {
|
||||||
for _, m := range allow {
|
for _, m := range allow {
|
||||||
if m.Model == "" || strings.EqualFold(m.Model, "AUTO") {
|
if m.Model == "" {
|
||||||
return cands
|
return cands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,20 +154,18 @@ func filterCandsByModels(cands []*provider.Provider, allow []config.ModelScope)
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// intersectModels restricts a model list to the scope (preserving order). An
|
// intersectModels restricts a model list to the scope (preserving order).
|
||||||
// "AUTO" scope entry grants every model.
|
// An "AUTO" scope entry only allows the AUTO routing mode; it does NOT grant
|
||||||
|
// every model.
|
||||||
func intersectModels(models []string, allow []config.ModelScope) []string {
|
func intersectModels(models []string, allow []config.ModelScope) []string {
|
||||||
allowed := make(map[string]bool, len(allow))
|
allowed := make(map[string]bool, len(allow))
|
||||||
any := false
|
|
||||||
for _, m := range allow {
|
for _, m := range allow {
|
||||||
if m.Model == "" || strings.EqualFold(m.Model, "AUTO") {
|
if m.Model == "" {
|
||||||
any = true
|
return models
|
||||||
break
|
}
|
||||||
|
if !strings.EqualFold(m.Model, "AUTO") {
|
||||||
|
allowed[m.Model] = true
|
||||||
}
|
}
|
||||||
allowed[m.Model] = true
|
|
||||||
}
|
|
||||||
if any {
|
|
||||||
return models
|
|
||||||
}
|
}
|
||||||
out := make([]string, 0, len(models))
|
out := make([]string, 0, len(models))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
@ -179,24 +179,16 @@ func intersectModels(models []string, allow []config.ModelScope) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkModelScope validates the effective model against the key's model scope
|
// checkModelScope validates the effective model against the key's model scope
|
||||||
// and token quota. Returns an error message when rejected. A scope entry with
|
// and token quota. Returns an error message when rejected.
|
||||||
// model "AUTO" grants all models; its quota caps the key's total tokens.
|
//
|
||||||
|
// A scope entry with model "AUTO" only allows requests where the effective
|
||||||
|
// model is AUTO (the routing mode). It does NOT grant access to specific model
|
||||||
|
// ids — that requires an explicit scope entry for the model.
|
||||||
func (g *Gateway) checkModelScope(ctx context.Context, model string) string {
|
func (g *Gateway) checkModelScope(ctx context.Context, model string) string {
|
||||||
allow := g.allowedModels(ctx)
|
allow := g.allowedModels(ctx)
|
||||||
if allow == nil {
|
if allow == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
for _, sc := range allow {
|
|
||||||
if sc.Model != "" && strings.EqualFold(sc.Model, "AUTO") {
|
|
||||||
if sc.TokenQuota > 0 {
|
|
||||||
used := g.scopeTokens(ctx, sc)
|
|
||||||
if used >= sc.TokenQuota {
|
|
||||||
return fmt.Sprintf("token quota exceeded (%d/%d)", used, sc.TokenQuota)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, sc := range allow {
|
for _, sc := range allow {
|
||||||
if sc.Model != model {
|
if sc.Model != model {
|
||||||
continue
|
continue
|
||||||
@ -229,12 +221,15 @@ func (g *Gateway) scopeTokens(ctx context.Context, sc config.ModelScope) int64 {
|
|||||||
// source that actually serves the bare model (via Registry.EffectiveModel), so
|
// source that actually serves the bare model (via Registry.EffectiveModel), so
|
||||||
// model ids that themselves contain separators (e.g. "deepseek-v4-flash-free")
|
// model ids that themselves contain separators (e.g. "deepseek-v4-flash-free")
|
||||||
// are never corrupted (P10-2).
|
// are never corrupted (P10-2).
|
||||||
|
//
|
||||||
|
// A scope entry with model "AUTO" only matches the literal AUTO routing mode;
|
||||||
|
// it does NOT grant access to specific model ids.
|
||||||
func (g *Gateway) hasScopeModel(list []config.ModelScope, s string) bool {
|
func (g *Gateway) hasScopeModel(list []config.ModelScope, s string) bool {
|
||||||
if r := g.core.Registry(); r != nil {
|
if r := g.core.Registry(); r != nil {
|
||||||
s = r.EffectiveModel(s)
|
s = r.EffectiveModel(s)
|
||||||
}
|
}
|
||||||
for _, x := range list {
|
for _, x := range list {
|
||||||
if x.Model == s || (x.Model != "" && strings.EqualFold(x.Model, "AUTO")) {
|
if x.Model == s {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -670,8 +670,11 @@ func TestHasScopeModelWithSourcePrefix(t *testing.T) {
|
|||||||
if g.hasScopeModel(scope, "deepseek-v4-flash-free-extra") {
|
if g.hasScopeModel(scope, "deepseek-v4-flash-free-extra") {
|
||||||
t.Error("hasScopeModel returned true for unrelated model")
|
t.Error("hasScopeModel returned true for unrelated model")
|
||||||
}
|
}
|
||||||
if !g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "zen:anything") {
|
if g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "zen:deepseek-v4-flash-free") {
|
||||||
t.Error("AUTO scope should allow any prefixed model")
|
t.Error("AUTO scope must NOT grant specific model ids even with prefix")
|
||||||
|
}
|
||||||
|
if g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "deepseek-v4-pro") {
|
||||||
|
t.Error("AUTO scope must NOT grant specific model ids")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,12 @@ adapter.version = "1.0.0"
|
|||||||
adapter.endpoint = "/chat/completions"
|
adapter.endpoint = "/chat/completions"
|
||||||
adapter.headers = {}
|
adapter.headers = {}
|
||||||
|
|
||||||
-- AgentRouter 的 WAF 会按客户端指纹白名单校验,只放行官方客户端。
|
-- AgentRouter 的阿里云 WAF 按三重维度校验:出口 IP(必须海外)、
|
||||||
-- 这里通过 build_headers 注入官方客户端 User-Agent 以通过校验。
|
-- TLS 指纹(JS/Go HTTP 层与 curl 不同)、浏览器指纹 header 集。
|
||||||
-- 默认 QwenCode 指纹(实测可通);可在 source.meta.user_agent 覆盖成 Claude Code 指纹。
|
-- 纯 UA 已不够:WAF 拦 12 种 agent CLI UA 中的 10 种(全 405 HTML),
|
||||||
|
-- 只有同时带 Origin: qwen.ai + Referer + X-Requested-With 才放行,
|
||||||
|
-- 且同 IP 短时间连续 3+ 次请求会被速率封禁,靠 adapter 侧低频调用 +
|
||||||
|
-- llmsproxy 的 prefFailStep 冷却自然限制,来源无法控制速率。
|
||||||
local default_ua = "QwenCode/0.2.0 (linux; x64)"
|
local default_ua = "QwenCode/0.2.0 (linux; x64)"
|
||||||
|
|
||||||
-- AgentRouter fronts Claude models (claude-opus-4-8), and Claude upstreams
|
-- AgentRouter fronts Claude models (claude-opus-4-8), and Claude upstreams
|
||||||
@ -65,6 +67,13 @@ function adapter.build_headers(meta)
|
|||||||
["Content-Type"] = "application/json",
|
["Content-Type"] = "application/json",
|
||||||
["Authorization"] = "Bearer " .. meta.api_key,
|
["Authorization"] = "Bearer " .. meta.api_key,
|
||||||
["User-Agent"] = ua,
|
["User-Agent"] = ua,
|
||||||
|
-- QwenCode client fingerprint: WAF rejects bare UA without these.
|
||||||
|
["Accept"] = "application/json, text/plain, */*",
|
||||||
|
["Accept-Language"] = "zh-CN,zh;q=0.9",
|
||||||
|
["Origin"] = "https://qwen.ai",
|
||||||
|
["Referer"] = "https://qwen.ai/",
|
||||||
|
["X-Requested-With"] = "XMLHttpRequest",
|
||||||
|
["Connection"] = "keep-alive",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user