mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:28:39 +08:00
fix(composite): exempt resolved grok/CN targets from messages dispatch gate
sanitizeGroupMessagesDispatchFields forces AllowMessagesDispatch=false for every non-openai platform including composite, and the gate checked only the group platform — so composite requests resolved to grok/CN targets were rejected 403 on /v1/messages and /v1/messages/count_tokens before reaching the composite target whitelist, leaving the CN rollout unusable for Claude-protocol clients. - allowOpenAICompatibleMessagesDispatch: composite groups resolved to a grok/CN target get the same exemption as the standalone platforms; openai-resolved and unresolved targets keep requiring the switch - resolveOpenAIMessagesDispatchMappedModel: skip the group-level dispatch model mapping (openai-specific gpt defaults) for composite grok/CN targets — model rewriting stays with account-level model_mapping
This commit is contained in:
@@ -3,27 +3,74 @@ package handler
|
||||
// CN 分组 /v1/messages 调度闸门回归(修复:正常途径创建的 CN 分组曾恒 403):
|
||||
// sanitizeGroupMessagesDispatchFields 对非 openai 平台强制 AllowMessagesDispatch
|
||||
// =false,故 CN 分组必须与 grok 一样在闸门处豁免,否则原生 Anthropic 直通
|
||||
//(Claude Code 主用例)永远不可达。
|
||||
//(Claude Code 主用例)永远不可达。composite 分组同理:sanitize 对 composite
|
||||
// 恒置 false,解析到 grok/CN 目标时必须按目标平台豁免,解析到 openai 目标
|
||||
// 仍受开关控制。
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllowOpenAICompatibleMessagesDispatch_CNProvidersExempt(t *testing.T) {
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(nil), "无 key 保持放行")
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(nil, nil), "无 key 保持放行")
|
||||
|
||||
for _, platform := range []string{service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformGrok} {
|
||||
apiKey := &service.APIKey{Group: &service.Group{Platform: platform, AllowMessagesDispatch: false}}
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(apiKey),
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(nil, apiKey),
|
||||
"%s 分组必须豁免 allow_messages_dispatch 闸门", platform)
|
||||
}
|
||||
|
||||
// 非回归:openai 分组仍受开关控制。
|
||||
openaiOff := &service.APIKey{Group: &service.Group{Platform: service.PlatformOpenAI, AllowMessagesDispatch: false}}
|
||||
require.False(t, allowOpenAICompatibleMessagesDispatch(openaiOff))
|
||||
require.False(t, allowOpenAICompatibleMessagesDispatch(nil, openaiOff))
|
||||
openaiOn := &service.APIKey{Group: &service.Group{Platform: service.PlatformOpenAI, AllowMessagesDispatch: true}}
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(openaiOn))
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(nil, openaiOn))
|
||||
}
|
||||
|
||||
func TestAllowOpenAICompatibleMessagesDispatch_CompositeResolvedTargets(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
newCompositeCtx := func(model string) (*gin.Context, *service.APIKey) {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
||||
apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite, AllowMessagesDispatch: false}}
|
||||
ensureCompositeTargetPlatform(c, apiKey, model)
|
||||
return c, apiKey
|
||||
}
|
||||
|
||||
// 解析到 grok/CN 目标:与对应独立分组同语义豁免。
|
||||
for _, model := range []string{"grok-4.3", "kimi-k2-thinking", "glm-5.2", "deepseek-v3.2"} {
|
||||
c, apiKey := newCompositeCtx(model)
|
||||
require.True(t, allowOpenAICompatibleMessagesDispatch(c, apiKey), "model=%s", model)
|
||||
}
|
||||
|
||||
// 解析到 openai 目标:仍受开关控制(composite 被 sanitize 恒置 false ⇒ 拒绝)。
|
||||
c, apiKey := newCompositeCtx("gpt-5.5")
|
||||
require.False(t, allowOpenAICompatibleMessagesDispatch(c, apiKey))
|
||||
|
||||
// 未解析出目标平台:保持拒绝,不放宽。
|
||||
cNone, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
cNone.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
||||
require.False(t, allowOpenAICompatibleMessagesDispatch(cNone,
|
||||
&service.APIKey{Group: &service.Group{Platform: service.PlatformComposite, AllowMessagesDispatch: false}}))
|
||||
}
|
||||
|
||||
// composite 解析到 grok/CN 目标时,Group 级调度映射(gpt-5.x 默认值为 openai
|
||||
// 专属)不得注入,模型改写完全交给账号级 model_mapping。
|
||||
func TestResolveOpenAIMessagesDispatchMappedModel_CompositeCNTargetsSkipGroupMapping(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
for _, model := range []string{"kimi-k2-thinking", "glm-5.2", "deepseek-v3.2", "grok-4.3"} {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
||||
apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}}
|
||||
ensureCompositeTargetPlatform(c, apiKey, model)
|
||||
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(c, apiKey, "claude-sonnet-4-5-20250929"), "model=%s", model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ func (h *OpenAIGatewayHandler) CountTokens(c *gin.Context) {
|
||||
zap.Any("group_id", apiKey.GroupID),
|
||||
)
|
||||
|
||||
if !allowOpenAICompatibleMessagesDispatch(apiKey) {
|
||||
if !allowOpenAICompatibleMessagesDispatch(c, apiKey) {
|
||||
h.anthropicErrorResponse(c, http.StatusForbidden, "permission_error",
|
||||
"This group does not allow /v1/messages dispatch")
|
||||
return
|
||||
@@ -250,7 +250,7 @@ func (h *OpenAIGatewayHandler) CountTokens(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
|
||||
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(apiKey, reqModel)
|
||||
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(c, apiKey, reqModel)
|
||||
reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", parsedReq.Stream))
|
||||
|
||||
setOpsRequestContext(c, reqModel, false)
|
||||
|
||||
@@ -98,10 +98,18 @@ func openAIForwardSucceededForScheduling(result *service.OpenAIForwardResult) bo
|
||||
return result.SucceededForScheduling()
|
||||
}
|
||||
|
||||
func resolveOpenAIMessagesDispatchMappedModel(apiKey *service.APIKey, requestedModel string) string {
|
||||
func resolveOpenAIMessagesDispatchMappedModel(c *gin.Context, apiKey *service.APIKey, requestedModel string) string {
|
||||
if apiKey == nil || apiKey.Group == nil {
|
||||
return ""
|
||||
}
|
||||
// composite 解析到 grok/CN 目标时调度级映射不适用(Group 级映射的 gpt-5.x
|
||||
// 默认值是 openai 专属,发给这些上游必错),模型改写交给账号级 model_mapping。
|
||||
if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
|
||||
if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
|
||||
(platform == service.PlatformGrok || service.IsCNProvider(platform)) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(apiKey.Group.ResolveMessagesDispatchModel(requestedModel))
|
||||
}
|
||||
|
||||
@@ -190,7 +198,7 @@ func openAIResponsesRequiredCapabilityForRequest(imageIntent bool, needsResponse
|
||||
return openAIResponsesRequiredCapability(imageIntent, platform)
|
||||
}
|
||||
|
||||
func allowOpenAICompatibleMessagesDispatch(apiKey *service.APIKey) bool {
|
||||
func allowOpenAICompatibleMessagesDispatch(c *gin.Context, apiKey *service.APIKey) bool {
|
||||
if apiKey == nil || apiKey.Group == nil {
|
||||
return true
|
||||
}
|
||||
@@ -204,6 +212,15 @@ func allowOpenAICompatibleMessagesDispatch(apiKey *service.APIKey) bool {
|
||||
if service.IsCNProvider(apiKey.Group.Platform) {
|
||||
return true
|
||||
}
|
||||
// composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免:sanitize
|
||||
// 对 composite 同样恒置 false,不豁免则这些目标的 /v1/messages 永远 403;
|
||||
// 解析到 openai 目标仍受开关控制,维持现状。
|
||||
if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
|
||||
if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
|
||||
(platform == service.PlatformGrok || service.IsCNProvider(platform)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return apiKey.Group.AllowMessagesDispatch
|
||||
}
|
||||
|
||||
@@ -957,7 +974,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
)
|
||||
|
||||
// 检查分组是否允许 /v1/messages 调度
|
||||
if !allowOpenAICompatibleMessagesDispatch(apiKey) {
|
||||
if !allowOpenAICompatibleMessagesDispatch(c, apiKey) {
|
||||
h.anthropicErrorResponse(c, http.StatusForbidden, "permission_error",
|
||||
"This group does not allow /v1/messages dispatch")
|
||||
return
|
||||
@@ -1000,7 +1017,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
|
||||
}
|
||||
bindOpenAIReasoningEffortPolicyForMessagesRequest(c, apiKey, body)
|
||||
routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
|
||||
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(apiKey, reqModel)
|
||||
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(c, apiKey, reqModel)
|
||||
reqStream := gjson.GetBytes(body, "stream").Bool()
|
||||
|
||||
reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
|
||||
|
||||
@@ -646,21 +646,21 @@ func TestResolveOpenAIMessagesDispatchMappedModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Equal(t, "gpt-5.4-mini", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5-20250929"))
|
||||
require.Equal(t, "gpt-5.6-sol", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-fable-5"))
|
||||
require.Equal(t, "gpt-5.4-mini", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-sonnet-4-5-20250929"))
|
||||
require.Equal(t, "gpt-5.6-sol", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-fable-5"))
|
||||
})
|
||||
|
||||
t.Run("uses_family_default_when_no_override", func(t *testing.T) {
|
||||
apiKey := &service.APIKey{Group: &service.Group{}}
|
||||
require.Equal(t, "gpt-5.4", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-opus-4-6"))
|
||||
require.Equal(t, "gpt-5.3-codex", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5-20250929"))
|
||||
require.Equal(t, "gpt-5.4-mini", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-haiku-4-5-20251001"))
|
||||
require.Equal(t, "gpt-5.4", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-opus-4-6"))
|
||||
require.Equal(t, "gpt-5.3-codex", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-sonnet-4-5-20250929"))
|
||||
require.Equal(t, "gpt-5.4-mini", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-haiku-4-5-20251001"))
|
||||
})
|
||||
|
||||
t.Run("returns_empty_for_non_claude_or_missing_group", func(t *testing.T) {
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, "claude-sonnet-4-5-20250929"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(&service.APIKey{}, "claude-sonnet-4-5-20250929"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(&service.APIKey{Group: &service.Group{}}, "gpt-5.4"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, nil, "claude-sonnet-4-5-20250929"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, &service.APIKey{}, "claude-sonnet-4-5-20250929"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, &service.APIKey{Group: &service.Group{}}, "gpt-5.4"))
|
||||
})
|
||||
|
||||
t.Run("grok_group_maps_claude_cli_model_to_grok_default", func(t *testing.T) {
|
||||
@@ -672,8 +672,8 @@ func TestResolveOpenAIMessagesDispatchMappedModel(t *testing.T) {
|
||||
Platform: service.PlatformGrok,
|
||||
},
|
||||
}
|
||||
require.Equal(t, "grok-4.5", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(apiKey, "grok"))
|
||||
require.Equal(t, "grok-4.5", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-sonnet-4-5"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "grok"))
|
||||
})
|
||||
|
||||
t.Run("does_not_fall_back_to_group_default_mapped_model", func(t *testing.T) {
|
||||
@@ -682,8 +682,8 @@ func TestResolveOpenAIMessagesDispatchMappedModel(t *testing.T) {
|
||||
DefaultMappedModel: "gpt-5.4",
|
||||
},
|
||||
}
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(apiKey, "gpt-5.4"))
|
||||
require.Equal(t, "gpt-5.3-codex", resolveOpenAIMessagesDispatchMappedModel(apiKey, "claude-sonnet-4-5-20250929"))
|
||||
require.Empty(t, resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "gpt-5.4"))
|
||||
require.Equal(t, "gpt-5.3-codex", resolveOpenAIMessagesDispatchMappedModel(nil, apiKey, "claude-sonnet-4-5-20250929"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user