diff --git a/backend/internal/handler/openai_gateway_cn_dispatch_test.go b/backend/internal/handler/openai_gateway_cn_dispatch_test.go index c49201bd12..eefb0948cb 100644 --- a/backend/internal/handler/openai_gateway_cn_dispatch_test.go +++ b/backend/internal/handler/openai_gateway_cn_dispatch_test.go @@ -1,11 +1,10 @@ package handler // CN 分组 /v1/messages 调度闸门回归(修复:正常途径创建的 CN 分组曾恒 403): -// sanitizeGroupMessagesDispatchFields 对非 openai 平台强制 AllowMessagesDispatch +// sanitizeGroupMessagesDispatchFields 对非 openai/composite 平台强制 AllowMessagesDispatch // =false,故 CN 分组必须与 grok 一样在闸门处豁免,否则原生 Anthropic 直通 -//(Claude Code 主用例)永远不可达。composite 分组同理:sanitize 对 composite -// 恒置 false,解析到 grok/CN 目标时必须按目标平台豁免,解析到 openai 目标 -// 仍受开关控制。 +//(Claude Code 主用例)永远不可达。composite 分组解析到 grok/CN 目标时按 +// 目标平台豁免,解析到 openai 目标仍受其可配置开关控制。 import ( "net/http/httptest" @@ -35,23 +34,25 @@ func TestAllowOpenAICompatibleMessagesDispatch_CNProvidersExempt(t *testing.T) { func TestAllowOpenAICompatibleMessagesDispatch_CompositeResolvedTargets(t *testing.T) { gin.SetMode(gin.TestMode) - newCompositeCtx := func(model string) (*gin.Context, *service.APIKey) { + newCompositeCtx := func(model string, allow bool) (*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}} + apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite, AllowMessagesDispatch: allow}} 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) + c, apiKey := newCompositeCtx(model, false) require.True(t, allowOpenAICompatibleMessagesDispatch(c, apiKey), "model=%s", model) } - // 解析到 openai 目标:仍受开关控制(composite 被 sanitize 恒置 false ⇒ 拒绝)。 - c, apiKey := newCompositeCtx("gpt-5.5") + // 解析到 openai 目标:受 composite 分组自身开关控制。 + c, apiKey := newCompositeCtx("gpt-5.5", false) require.False(t, allowOpenAICompatibleMessagesDispatch(c, apiKey)) + c, apiKey = newCompositeCtx("gpt-5.5", true) + require.True(t, allowOpenAICompatibleMessagesDispatch(c, apiKey)) // 未解析出目标平台:保持拒绝,不放宽。 cNone, _ := gin.CreateTestContext(httptest.NewRecorder()) diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index 9c8ea82a08..a11113fe49 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -223,14 +223,13 @@ func allowOpenAICompatibleMessagesDispatch(c *gin.Context, apiKey *service.APIKe } // 国产供应商分组与 grok 同语义:/v1/messages 就是其主要服务形态(anthropic // 协议账号原生直通 Claude Code),无需 allow_messages_dispatch 开关授权—— - // 该开关对非 openai 平台恒被 sanitizeGroupMessagesDispatchFields 置 false, + // 该开关对非 openai/composite 平台恒被 sanitizeGroupMessagesDispatchFields 置 false, // 若不豁免,CN 分组将永远 403。 if service.IsCNProvider(apiKey.Group.Platform) { return true } - // composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免:sanitize - // 对 composite 同样恒置 false,不豁免则这些目标的 /v1/messages 永远 403; - // 解析到 openai 目标仍受开关控制,维持现状。 + // composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免; + // 解析到 openai 目标则受 composite 分组自身的可配置开关控制。 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)) { diff --git a/backend/internal/service/openai_messages_dispatch.go b/backend/internal/service/openai_messages_dispatch.go index 562e9583c5..3495bc9e09 100644 --- a/backend/internal/service/openai_messages_dispatch.go +++ b/backend/internal/service/openai_messages_dispatch.go @@ -116,7 +116,9 @@ func sanitizeGroupMessagesDispatchFields(g *Group) { if g == nil || g.Platform == PlatformOpenAI { return } - g.AllowMessagesDispatch = false + if g.Platform != PlatformComposite { + g.AllowMessagesDispatch = false + } g.DefaultMappedModel = "" g.MessagesDispatchModelConfig = OpenAIMessagesDispatchModelConfig{} } diff --git a/backend/internal/service/openai_messages_dispatch_test.go b/backend/internal/service/openai_messages_dispatch_test.go index fd6dea86fa..2856729624 100644 --- a/backend/internal/service/openai_messages_dispatch_test.go +++ b/backend/internal/service/openai_messages_dispatch_test.go @@ -69,3 +69,25 @@ func TestSanitizeGroupMessagesDispatchFields_ClearsNonOpenAIPlatform(t *testing. require.Empty(t, group.DefaultMappedModel) require.Equal(t, OpenAIMessagesDispatchModelConfig{}, group.MessagesDispatchModelConfig) } + +func TestSanitizeGroupMessagesDispatchFields_PreservesCompositeDispatchToggle(t *testing.T) { + t.Parallel() + + group := &Group{ + Platform: PlatformComposite, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.6-sol", + MessagesDispatchModelConfig: OpenAIMessagesDispatchModelConfig{ + SonnetMappedModel: "gpt-5.3-codex", + ExactModelMappings: map[string]string{ + "claude-fable-5": "gpt-5.6-sol", + }, + }, + } + + sanitizeGroupMessagesDispatchFields(group) + + require.True(t, group.AllowMessagesDispatch) + require.Empty(t, group.DefaultMappedModel) + require.Equal(t, OpenAIMessagesDispatchModelConfig{}, group.MessagesDispatchModelConfig) +} diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 24c5cf3784..d019a24b8e 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -1614,9 +1614,9 @@

- +

@@ -1655,7 +1655,13 @@ {{ t("admin.groups.openaiMessages.allowDispatchHint") }}

-
+
@@ -3336,9 +3342,9 @@

- +

@@ -3377,7 +3383,12 @@ {{ t("admin.groups.openaiMessages.allowDispatchHint") }}

-
+
@@ -4462,6 +4473,7 @@ import { messagesDispatchConfigToFormState, messagesDispatchFormStateToConfig, resetMessagesDispatchFormState, + supportsMessagesDispatchPlatform, type MessagesDispatchMappingRow, } from "./groupsMessagesDispatch"; import { @@ -6646,7 +6658,7 @@ watch( if (!["anthropic", "antigravity"].includes(newVal)) { createForm.fallback_group_id_on_invalid_request = null; } - if (newVal !== "openai") { + if (!supportsMessagesDispatchPlatform(newVal)) { resetMessagesDispatchFormState(createForm); } if (!supportsLivePlatform(newVal)) { @@ -6696,7 +6708,7 @@ watch( if (!["anthropic", "antigravity"].includes(newVal)) { editForm.fallback_group_id_on_invalid_request = null; } - if (newVal !== "openai") { + if (!supportsMessagesDispatchPlatform(newVal)) { resetMessagesDispatchFormState(editForm); } if (!supportsLivePlatform(newVal)) { @@ -6748,7 +6760,7 @@ watch( if (!['anthropic', 'antigravity'].includes(newVal)) { editForm.fallback_group_id_on_invalid_request = null } - if (newVal !== 'openai') { + if (!supportsMessagesDispatchPlatform(newVal)) { editForm.allow_messages_dispatch = false editForm.default_mapped_model = '' } diff --git a/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts b/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts index 15f422247a..e516cccb76 100644 --- a/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts +++ b/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts @@ -5,9 +5,16 @@ import { messagesDispatchConfigToFormState, messagesDispatchFormStateToConfig, resetMessagesDispatchFormState, + supportsMessagesDispatchPlatform, } from "../groupsMessagesDispatch"; describe("groupsMessagesDispatch", () => { + it("supports OpenAI and composite groups", () => { + expect(supportsMessagesDispatchPlatform("openai")).toBe(true); + expect(supportsMessagesDispatchPlatform("composite")).toBe(true); + expect(supportsMessagesDispatchPlatform("anthropic")).toBe(false); + }); + it("returns the expected default form state", () => { expect(createDefaultMessagesDispatchFormState()).toEqual({ allow_messages_dispatch: false, diff --git a/frontend/src/views/admin/groupsMessagesDispatch.ts b/frontend/src/views/admin/groupsMessagesDispatch.ts index b367091cd7..677a95e574 100644 --- a/frontend/src/views/admin/groupsMessagesDispatch.ts +++ b/frontend/src/views/admin/groupsMessagesDispatch.ts @@ -13,6 +13,10 @@ export interface MessagesDispatchFormState { exact_model_mappings: MessagesDispatchMappingRow[]; } +export function supportsMessagesDispatchPlatform(platform: string): boolean { + return platform === "openai" || platform === "composite"; +} + export function createDefaultMessagesDispatchFormState(): MessagesDispatchFormState { return { allow_messages_dispatch: false,