mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix: allow messages dispatch for composite groups
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
// CN 分组 /v1/messages 调度闸门回归(修复:正常途径创建的 CN 分组曾恒 403):
|
// CN 分组 /v1/messages 调度闸门回归(修复:正常途径创建的 CN 分组曾恒 403):
|
||||||
// sanitizeGroupMessagesDispatchFields 对非 openai 平台强制 AllowMessagesDispatch
|
// sanitizeGroupMessagesDispatchFields 对非 openai/composite 平台强制 AllowMessagesDispatch
|
||||||
// =false,故 CN 分组必须与 grok 一样在闸门处豁免,否则原生 Anthropic 直通
|
// =false,故 CN 分组必须与 grok 一样在闸门处豁免,否则原生 Anthropic 直通
|
||||||
//(Claude Code 主用例)永远不可达。composite 分组同理:sanitize 对 composite
|
//(Claude Code 主用例)永远不可达。composite 分组解析到 grok/CN 目标时按
|
||||||
// 恒置 false,解析到 grok/CN 目标时必须按目标平台豁免,解析到 openai 目标
|
// 目标平台豁免,解析到 openai 目标仍受其可配置开关控制。
|
||||||
// 仍受开关控制。
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -35,23 +34,25 @@ func TestAllowOpenAICompatibleMessagesDispatch_CNProvidersExempt(t *testing.T) {
|
|||||||
func TestAllowOpenAICompatibleMessagesDispatch_CompositeResolvedTargets(t *testing.T) {
|
func TestAllowOpenAICompatibleMessagesDispatch_CompositeResolvedTargets(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
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, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
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)
|
ensureCompositeTargetPlatform(c, apiKey, model)
|
||||||
return c, apiKey
|
return c, apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析到 grok/CN 目标:与对应独立分组同语义豁免。
|
// 解析到 grok/CN 目标:与对应独立分组同语义豁免。
|
||||||
for _, model := range []string{"grok-4.3", "kimi-k2-thinking", "glm-5.2", "deepseek-v3.2"} {
|
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)
|
require.True(t, allowOpenAICompatibleMessagesDispatch(c, apiKey), "model=%s", model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析到 openai 目标:仍受开关控制(composite 被 sanitize 恒置 false ⇒ 拒绝)。
|
// 解析到 openai 目标:受 composite 分组自身开关控制。
|
||||||
c, apiKey := newCompositeCtx("gpt-5.5")
|
c, apiKey := newCompositeCtx("gpt-5.5", false)
|
||||||
require.False(t, allowOpenAICompatibleMessagesDispatch(c, apiKey))
|
require.False(t, allowOpenAICompatibleMessagesDispatch(c, apiKey))
|
||||||
|
c, apiKey = newCompositeCtx("gpt-5.5", true)
|
||||||
|
require.True(t, allowOpenAICompatibleMessagesDispatch(c, apiKey))
|
||||||
|
|
||||||
// 未解析出目标平台:保持拒绝,不放宽。
|
// 未解析出目标平台:保持拒绝,不放宽。
|
||||||
cNone, _ := gin.CreateTestContext(httptest.NewRecorder())
|
cNone, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
|||||||
@@ -223,14 +223,13 @@ func allowOpenAICompatibleMessagesDispatch(c *gin.Context, apiKey *service.APIKe
|
|||||||
}
|
}
|
||||||
// 国产供应商分组与 grok 同语义:/v1/messages 就是其主要服务形态(anthropic
|
// 国产供应商分组与 grok 同语义:/v1/messages 就是其主要服务形态(anthropic
|
||||||
// 协议账号原生直通 Claude Code),无需 allow_messages_dispatch 开关授权——
|
// 协议账号原生直通 Claude Code),无需 allow_messages_dispatch 开关授权——
|
||||||
// 该开关对非 openai 平台恒被 sanitizeGroupMessagesDispatchFields 置 false,
|
// 该开关对非 openai/composite 平台恒被 sanitizeGroupMessagesDispatchFields 置 false,
|
||||||
// 若不豁免,CN 分组将永远 403。
|
// 若不豁免,CN 分组将永远 403。
|
||||||
if service.IsCNProvider(apiKey.Group.Platform) {
|
if service.IsCNProvider(apiKey.Group.Platform) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免:sanitize
|
// composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免;
|
||||||
// 对 composite 同样恒置 false,不豁免则这些目标的 /v1/messages 永远 403;
|
// 解析到 openai 目标则受 composite 分组自身的可配置开关控制。
|
||||||
// 解析到 openai 目标仍受开关控制,维持现状。
|
|
||||||
if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
|
if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
|
||||||
if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
|
if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
|
||||||
(platform == service.PlatformGrok || service.IsCNProvider(platform)) {
|
(platform == service.PlatformGrok || service.IsCNProvider(platform)) {
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ func sanitizeGroupMessagesDispatchFields(g *Group) {
|
|||||||
if g == nil || g.Platform == PlatformOpenAI {
|
if g == nil || g.Platform == PlatformOpenAI {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.AllowMessagesDispatch = false
|
if g.Platform != PlatformComposite {
|
||||||
|
g.AllowMessagesDispatch = false
|
||||||
|
}
|
||||||
g.DefaultMappedModel = ""
|
g.DefaultMappedModel = ""
|
||||||
g.MessagesDispatchModelConfig = OpenAIMessagesDispatchModelConfig{}
|
g.MessagesDispatchModelConfig = OpenAIMessagesDispatchModelConfig{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,3 +69,25 @@ func TestSanitizeGroupMessagesDispatchFields_ClearsNonOpenAIPlatform(t *testing.
|
|||||||
require.Empty(t, group.DefaultMappedModel)
|
require.Empty(t, group.DefaultMappedModel)
|
||||||
require.Equal(t, OpenAIMessagesDispatchModelConfig{}, group.MessagesDispatchModelConfig)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1614,9 +1614,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
|
<!-- OpenAI Messages 调度配置(OpenAI 与 Composite 平台) -->
|
||||||
<div
|
<div
|
||||||
v-if="createForm.platform === 'openai'"
|
v-if="supportsMessagesDispatchPlatform(createForm.platform)"
|
||||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||||
>
|
>
|
||||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
@@ -1655,7 +1655,13 @@
|
|||||||
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
|
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="createForm.allow_messages_dispatch" class="mt-3">
|
<div
|
||||||
|
v-if="
|
||||||
|
createForm.platform === 'openai' &&
|
||||||
|
createForm.allow_messages_dispatch
|
||||||
|
"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
|
class="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
|
||||||
>
|
>
|
||||||
@@ -3336,9 +3342,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
|
<!-- OpenAI Messages 调度配置(OpenAI 与 Composite 平台) -->
|
||||||
<div
|
<div
|
||||||
v-if="editForm.platform === 'openai'"
|
v-if="supportsMessagesDispatchPlatform(editForm.platform)"
|
||||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||||
>
|
>
|
||||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
@@ -3377,7 +3383,12 @@
|
|||||||
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
|
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="editForm.allow_messages_dispatch" class="mt-3">
|
<div
|
||||||
|
v-if="
|
||||||
|
editForm.platform === 'openai' && editForm.allow_messages_dispatch
|
||||||
|
"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
|
class="relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
|
||||||
>
|
>
|
||||||
@@ -4462,6 +4473,7 @@ import {
|
|||||||
messagesDispatchConfigToFormState,
|
messagesDispatchConfigToFormState,
|
||||||
messagesDispatchFormStateToConfig,
|
messagesDispatchFormStateToConfig,
|
||||||
resetMessagesDispatchFormState,
|
resetMessagesDispatchFormState,
|
||||||
|
supportsMessagesDispatchPlatform,
|
||||||
type MessagesDispatchMappingRow,
|
type MessagesDispatchMappingRow,
|
||||||
} from "./groupsMessagesDispatch";
|
} from "./groupsMessagesDispatch";
|
||||||
import {
|
import {
|
||||||
@@ -6646,7 +6658,7 @@ watch(
|
|||||||
if (!["anthropic", "antigravity"].includes(newVal)) {
|
if (!["anthropic", "antigravity"].includes(newVal)) {
|
||||||
createForm.fallback_group_id_on_invalid_request = null;
|
createForm.fallback_group_id_on_invalid_request = null;
|
||||||
}
|
}
|
||||||
if (newVal !== "openai") {
|
if (!supportsMessagesDispatchPlatform(newVal)) {
|
||||||
resetMessagesDispatchFormState(createForm);
|
resetMessagesDispatchFormState(createForm);
|
||||||
}
|
}
|
||||||
if (!supportsLivePlatform(newVal)) {
|
if (!supportsLivePlatform(newVal)) {
|
||||||
@@ -6696,7 +6708,7 @@ watch(
|
|||||||
if (!["anthropic", "antigravity"].includes(newVal)) {
|
if (!["anthropic", "antigravity"].includes(newVal)) {
|
||||||
editForm.fallback_group_id_on_invalid_request = null;
|
editForm.fallback_group_id_on_invalid_request = null;
|
||||||
}
|
}
|
||||||
if (newVal !== "openai") {
|
if (!supportsMessagesDispatchPlatform(newVal)) {
|
||||||
resetMessagesDispatchFormState(editForm);
|
resetMessagesDispatchFormState(editForm);
|
||||||
}
|
}
|
||||||
if (!supportsLivePlatform(newVal)) {
|
if (!supportsLivePlatform(newVal)) {
|
||||||
@@ -6748,7 +6760,7 @@ watch(
|
|||||||
if (!['anthropic', 'antigravity'].includes(newVal)) {
|
if (!['anthropic', 'antigravity'].includes(newVal)) {
|
||||||
editForm.fallback_group_id_on_invalid_request = null
|
editForm.fallback_group_id_on_invalid_request = null
|
||||||
}
|
}
|
||||||
if (newVal !== 'openai') {
|
if (!supportsMessagesDispatchPlatform(newVal)) {
|
||||||
editForm.allow_messages_dispatch = false
|
editForm.allow_messages_dispatch = false
|
||||||
editForm.default_mapped_model = ''
|
editForm.default_mapped_model = ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,16 @@ import {
|
|||||||
messagesDispatchConfigToFormState,
|
messagesDispatchConfigToFormState,
|
||||||
messagesDispatchFormStateToConfig,
|
messagesDispatchFormStateToConfig,
|
||||||
resetMessagesDispatchFormState,
|
resetMessagesDispatchFormState,
|
||||||
|
supportsMessagesDispatchPlatform,
|
||||||
} from "../groupsMessagesDispatch";
|
} from "../groupsMessagesDispatch";
|
||||||
|
|
||||||
describe("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", () => {
|
it("returns the expected default form state", () => {
|
||||||
expect(createDefaultMessagesDispatchFormState()).toEqual({
|
expect(createDefaultMessagesDispatchFormState()).toEqual({
|
||||||
allow_messages_dispatch: false,
|
allow_messages_dispatch: false,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface MessagesDispatchFormState {
|
|||||||
exact_model_mappings: MessagesDispatchMappingRow[];
|
exact_model_mappings: MessagesDispatchMappingRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function supportsMessagesDispatchPlatform(platform: string): boolean {
|
||||||
|
return platform === "openai" || platform === "composite";
|
||||||
|
}
|
||||||
|
|
||||||
export function createDefaultMessagesDispatchFormState(): MessagesDispatchFormState {
|
export function createDefaultMessagesDispatchFormState(): MessagesDispatchFormState {
|
||||||
return {
|
return {
|
||||||
allow_messages_dispatch: false,
|
allow_messages_dispatch: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user