diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 580126c8..9f2c2755 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -366,6 +366,13 @@ export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' export type SubscriptionType = 'standard' | 'subscription' +export interface OpenAIMessagesDispatchModelConfig { + opus_mapped_model?: string + sonnet_mapped_model?: string + haiku_mapped_model?: string + exact_model_mappings?: Record +} + export interface Group { id: number name: string @@ -388,6 +395,8 @@ export interface Group { fallback_group_id_on_invalid_request: number | null // OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程) allow_messages_dispatch?: boolean + default_mapped_model?: string + messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig require_oauth_only: boolean require_privacy_set: boolean created_at: string @@ -414,6 +423,7 @@ export interface AdminGroup extends Group { // OpenAI Messages 调度配置(仅 openai 平台使用) default_mapped_model?: string + messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig // 分组排序 sort_order: number diff --git a/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts b/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts new file mode 100644 index 00000000..15f42224 --- /dev/null +++ b/frontend/src/views/admin/__tests__/groupsMessagesDispatch.spec.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; + +import { + createDefaultMessagesDispatchFormState, + messagesDispatchConfigToFormState, + messagesDispatchFormStateToConfig, + resetMessagesDispatchFormState, +} from "../groupsMessagesDispatch"; + +describe("groupsMessagesDispatch", () => { + it("returns the expected default form state", () => { + expect(createDefaultMessagesDispatchFormState()).toEqual({ + allow_messages_dispatch: false, + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.3-codex", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: [], + }); + }); + + it("sanitizes exact model mapping rows when converting to config", () => { + const config = messagesDispatchFormStateToConfig({ + allow_messages_dispatch: true, + opus_mapped_model: " gpt-5.4 ", + sonnet_mapped_model: "gpt-5.3-codex", + haiku_mapped_model: " gpt-5.4-mini ", + exact_model_mappings: [ + { + claude_model: " claude-sonnet-4-5-20250929 ", + target_model: " gpt-5.2 ", + }, + { claude_model: "", target_model: "gpt-5.4" }, + { claude_model: "claude-opus-4-6", target_model: " " }, + ], + }); + + expect(config).toEqual({ + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.3-codex", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: { + "claude-sonnet-4-5-20250929": "gpt-5.2", + }, + }); + }); + + it("hydrates form state from api config", () => { + expect( + messagesDispatchConfigToFormState({ + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.2", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: { + "claude-opus-4-6": "gpt-5.4", + "claude-haiku-4-5-20251001": "gpt-5.4-mini", + }, + }), + ).toEqual({ + allow_messages_dispatch: false, + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.2", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: [ + { + claude_model: "claude-haiku-4-5-20251001", + target_model: "gpt-5.4-mini", + }, + { claude_model: "claude-opus-4-6", target_model: "gpt-5.4" }, + ], + }); + }); + + it("resets mutable form state when platform switches away from openai", () => { + const state = { + allow_messages_dispatch: true, + opus_mapped_model: "gpt-5.2", + sonnet_mapped_model: "gpt-5.4", + haiku_mapped_model: "gpt-5.1", + exact_model_mappings: [ + { claude_model: "claude-opus-4-6", target_model: "gpt-5.4" }, + ], + }; + + resetMessagesDispatchFormState(state); + + expect(state).toEqual({ + allow_messages_dispatch: false, + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.3-codex", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: [], + }); + }); +}); diff --git a/frontend/src/views/admin/groupsMessagesDispatch.ts b/frontend/src/views/admin/groupsMessagesDispatch.ts new file mode 100644 index 00000000..b367091c --- /dev/null +++ b/frontend/src/views/admin/groupsMessagesDispatch.ts @@ -0,0 +1,72 @@ +import type { OpenAIMessagesDispatchModelConfig } from "@/types"; + +export interface MessagesDispatchMappingRow { + claude_model: string; + target_model: string; +} + +export interface MessagesDispatchFormState { + allow_messages_dispatch: boolean; + opus_mapped_model: string; + sonnet_mapped_model: string; + haiku_mapped_model: string; + exact_model_mappings: MessagesDispatchMappingRow[]; +} + +export function createDefaultMessagesDispatchFormState(): MessagesDispatchFormState { + return { + allow_messages_dispatch: false, + opus_mapped_model: "gpt-5.4", + sonnet_mapped_model: "gpt-5.3-codex", + haiku_mapped_model: "gpt-5.4-mini", + exact_model_mappings: [], + }; +} + +export function messagesDispatchConfigToFormState( + config?: OpenAIMessagesDispatchModelConfig | null, +): MessagesDispatchFormState { + const defaults = createDefaultMessagesDispatchFormState(); + const exactMappings = Object.entries(config?.exact_model_mappings || {}) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([claude_model, target_model]) => ({ claude_model, target_model })); + + return { + allow_messages_dispatch: false, + opus_mapped_model: + config?.opus_mapped_model?.trim() || defaults.opus_mapped_model, + sonnet_mapped_model: + config?.sonnet_mapped_model?.trim() || defaults.sonnet_mapped_model, + haiku_mapped_model: + config?.haiku_mapped_model?.trim() || defaults.haiku_mapped_model, + exact_model_mappings: exactMappings, + }; +} + +export function messagesDispatchFormStateToConfig( + state: MessagesDispatchFormState, +): OpenAIMessagesDispatchModelConfig { + const exactModelMappings = Object.fromEntries( + state.exact_model_mappings + .map((row) => [row.claude_model.trim(), row.target_model.trim()] as const) + .filter(([claudeModel, targetModel]) => claudeModel && targetModel), + ); + + return { + opus_mapped_model: state.opus_mapped_model.trim(), + sonnet_mapped_model: state.sonnet_mapped_model.trim(), + haiku_mapped_model: state.haiku_mapped_model.trim(), + exact_model_mappings: exactModelMappings, + }; +} + +export function resetMessagesDispatchFormState( + target: MessagesDispatchFormState, +): void { + const defaults = createDefaultMessagesDispatchFormState(); + target.allow_messages_dispatch = defaults.allow_messages_dispatch; + target.opus_mapped_model = defaults.opus_mapped_model; + target.sonnet_mapped_model = defaults.sonnet_mapped_model; + target.haiku_mapped_model = defaults.haiku_mapped_model; + target.exact_model_mappings = []; +}