From 68653fb2cc766d07dc5fceeac4e7471ce01b4842 Mon Sep 17 00:00:00 2001
From: wucm667
Date: Fri, 21 Aug 2026 18:01:16 +0800
Subject: [PATCH] fix: allow messages dispatch for composite groups
---
.../openai_gateway_cn_dispatch_test.go | 19 ++++++------
.../handler/openai_gateway_handler.go | 7 ++---
.../service/openai_messages_dispatch.go | 4 ++-
.../service/openai_messages_dispatch_test.go | 22 ++++++++++++++
frontend/src/views/admin/GroupsView.vue | 30 +++++++++++++------
.../__tests__/groupsMessagesDispatch.spec.ts | 7 +++++
.../src/views/admin/groupsMessagesDispatch.ts | 4 +++
7 files changed, 70 insertions(+), 23 deletions(-)
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,