Merge pull request #4674 from AdrianZhaoDev/main

feat(groups): add OpenAI reasoning policy
This commit is contained in:
Wesley Liddick
2026-07-22 09:37:06 +08:00
committed by GitHub
43 changed files with 1637 additions and 11 deletions
+85
View File
@@ -599,6 +599,14 @@
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<ReasoningEffortPolicyFields
v-if="createForm.platform === 'openai'"
ref="createReasoningEffortPolicyRef"
id-prefix="create-group-reasoning"
:platform="createForm.platform"
v-model:max-effort="createForm.max_reasoning_effort"
v-model:mappings="createForm.reasoning_effort_mappings"
/>
<div
v-if="createForm.subscription_type !== 'subscription'"
data-tour="group-form-exclusive"
@@ -2111,6 +2119,14 @@
/>
<p class="input-hint">{{ t("admin.groups.form.rpmLimitHint") }}</p>
</div>
<ReasoningEffortPolicyFields
v-if="editForm.platform === 'openai'"
ref="editReasoningEffortPolicyRef"
id-prefix="edit-group-reasoning"
:platform="editForm.platform"
v-model:max-effort="editForm.max_reasoning_effort"
v-model:mappings="editForm.reasoning_effort_mappings"
/>
<div v-if="editForm.subscription_type !== 'subscription'">
<div class="mb-1.5 flex items-center gap-1">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -3601,6 +3617,7 @@ import Icon from "@/components/icons/Icon.vue";
import GroupRateMultipliersModal from "@/components/admin/group/GroupRateMultipliersModal.vue";
import GroupRPMOverridesModal from "@/components/admin/group/GroupRPMOverridesModal.vue";
import GroupCapacityBadge from "@/components/common/GroupCapacityBadge.vue";
import ReasoningEffortPolicyFields from "@/components/admin/group/ReasoningEffortPolicyFields.vue";
import { VueDraggable } from "vue-draggable-plus";
import { createStableObjectKeyResolver } from "@/utils/stableObjectKey";
import { extractApiErrorMessage } from "@/utils/apiError";
@@ -3623,6 +3640,12 @@ import {
} from "./groupsModelsList";
import { createModelsListCandidatesTracker } from "./groupsModelsListCandidates";
import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModelScopes";
import {
normalizeReasoningEffortForPlatform,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
type ReasoningEffortMappingRow,
} from "./groupsReasoningEffort";
import {
getDefaultImagePreviewPrice,
getDefaultVideoPreviewPrice,
@@ -4000,6 +4023,12 @@ const createModelsListState = reactive(createInitialModelsListState());
const editModelsListState = reactive(createInitialModelsListState());
const createModelsListLoading = ref(false);
const editModelsListLoading = ref(false);
type ReasoningEffortPolicyFieldsExpose = {
validate: () => boolean;
resetValidation: () => void;
};
const createReasoningEffortPolicyRef = ref<ReasoningEffortPolicyFieldsExpose | null>(null);
const editReasoningEffortPolicyRef = ref<ReasoningEffortPolicyFieldsExpose | null>(null);
const modelsListCandidatesTracker = createModelsListCandidatesTracker();
const createModelsListSelectedCount = computed(
() => createModelsListState.items.filter((item) => item.selected).length,
@@ -4064,6 +4093,8 @@ const createForm = reactive({
copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
// 简单账号类型(用于模型路由选择)
@@ -4412,6 +4443,8 @@ const editForm = reactive({
copy_accounts_from_group_ids: [] as number[],
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
type ImagePricingFormState = {
@@ -4801,6 +4834,9 @@ const closeCreateModal = () => {
createForm.mcp_xml_inject = true;
createForm.copy_accounts_from_group_ids = [];
createForm.rpm_limit = 0;
createForm.max_reasoning_effort = "";
createForm.reasoning_effort_mappings = [];
createReasoningEffortPolicyRef.value?.resetValidation();
resetModelsListState(createModelsListState);
createModelRoutingRules.value = [];
};
@@ -4839,6 +4875,13 @@ const handleCreateGroup = async () => {
appStore.showError(t("admin.groups.nameRequired"));
return;
}
if (
createForm.platform === "openai" &&
createReasoningEffortPolicyRef.value &&
!createReasoningEffortPolicyRef.value.validate()
) {
return;
}
submitting.value = true;
try {
// 构建请求数据,包含模型路由配置
@@ -4871,6 +4914,9 @@ const handleCreateGroup = async () => {
exact_model_mappings: createForm.exact_model_mappings,
})
: undefined,
reasoning_effort_mappings: reasoningEffortMappingsToAPI(
createForm.reasoning_effort_mappings,
),
};
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const emptyToNull = (v: any) => (v === "" ? null : v);
@@ -4985,6 +5031,14 @@ const handleEdit = async (group: AdminGroup) => {
editForm.mcp_xml_inject = group.mcp_xml_inject ?? true;
editForm.copy_accounts_from_group_ids = []; // 复制账号字段每次编辑时重置为空
editForm.rpm_limit = group.rpm_limit ?? 0;
editForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
group.platform,
group.max_reasoning_effort,
);
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
group.reasoning_effort_mappings,
group.platform,
);
resetModelsListState(editModelsListState, group.models_list_config);
// 加载模型路由规则(异步加载账号名称)
editModelRoutingRules.value = await convertApiFormatToRoutingRules(
@@ -5001,6 +5055,9 @@ const closeEditModal = () => {
clearAllAccountSearchState();
showEditModal.value = false;
editingGroup.value = null;
editForm.max_reasoning_effort = "";
editForm.reasoning_effort_mappings = [];
editReasoningEffortPolicyRef.value?.resetValidation();
editModelRoutingRules.value = [];
editForm.copy_accounts_from_group_ids = [];
editForm.peak_rate_enabled = false;
@@ -5023,6 +5080,13 @@ const handleUpdateGroup = async () => {
appStore.showError(t("admin.groups.nameRequired"));
return;
}
if (
editForm.platform === "openai" &&
editReasoningEffortPolicyRef.value &&
!editReasoningEffortPolicyRef.value.validate()
) {
return;
}
submitting.value = true;
try {
@@ -5062,6 +5126,9 @@ const handleUpdateGroup = async () => {
exact_model_mappings: editForm.exact_model_mappings,
})
: undefined,
reasoning_effort_mappings: reasoningEffortMappingsToAPI(
editForm.reasoning_effort_mappings,
),
};
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const emptyToNull = (v: any) => (v === "" ? null : v);
@@ -5227,6 +5294,15 @@ watch(
if (newVal !== "openai") {
resetMessagesDispatchFormState(createForm);
}
createForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
newVal,
createForm.max_reasoning_effort,
);
createForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(createForm.reasoning_effort_mappings),
newVal,
);
createReasoningEffortPolicyRef.value?.resetValidation();
if (!["openai", "antigravity", "anthropic", "gemini"].includes(newVal)) {
createForm.require_oauth_only = false;
createForm.require_privacy_set = false;
@@ -5260,6 +5336,15 @@ watch(
if (newVal !== "openai") {
resetMessagesDispatchFormState(editForm);
}
editForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
newVal,
editForm.max_reasoning_effort,
);
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(editForm.reasoning_effort_mappings),
newVal,
);
editReasoningEffortPolicyRef.value?.resetValidation();
if (!["openai", "antigravity", "anthropic", "gemini"].includes(newVal)) {
editForm.require_oauth_only = false;
editForm.require_privacy_set = false;
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import {
createReasoningEffortMappingRow,
normalizeReasoningEffortForPlatform,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOptionsForPlatform,
validateReasoningEffortMappings,
} from "../groupsReasoningEffort";
describe("groupsReasoningEffort", () => {
it("provides fixed OpenAI choices without none", () => {
expect(
reasoningEffortOptionsForPlatform("openai").map((option) => option.value),
).toEqual([
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
for (const platform of [
"anthropic",
"gemini",
"antigravity",
"grok",
] as const) {
expect(reasoningEffortOptionsForPlatform(platform)).toEqual([]);
}
});
it("hydrates supported rows and drops stale custom values", () => {
const rows = reasoningEffortMappingsToRows(
[
{ from: " max ", to: " xhigh " },
{ from: "ultra", to: "high" },
],
"openai",
);
expect(rows).toHaveLength(1);
expect(reasoningEffortMappingsToAPI(rows)).toEqual([
{ from: "max", to: "xhigh" },
]);
});
it("clears values unsupported by OpenAI or used on another platform", () => {
expect(normalizeReasoningEffortForPlatform("openai", " MAX ")).toBe("max");
expect(normalizeReasoningEffortForPlatform("grok", "max")).toBe("");
expect(normalizeReasoningEffortForPlatform("openai", "none")).toBe("");
});
it("requires both sides of every mapping", () => {
const first = createReasoningEffortMappingRow({ to: "low" });
const second = createReasoningEffortMappingRow({ from: "max" });
expect(validateReasoningEffortMappings([first, second])).toEqual({
[first.id]: { from: "fromRequired" },
[second.id]: { to: "toRequired" },
});
});
it("rejects duplicate source values case insensitively", () => {
const first = createReasoningEffortMappingRow({ from: "MAX", to: "xhigh" });
const second = createReasoningEffortMappingRow({ from: " max ", to: "high" });
expect(validateReasoningEffortMappings([first, second])).toEqual({
[first.id]: { from: "duplicateFrom" },
[second.id]: { from: "duplicateFrom" },
});
});
it("rejects custom mappings", () => {
const row = createReasoningEffortMappingRow({ from: "ultra", to: "high" });
expect(validateReasoningEffortMappings([row], "openai")).toEqual({
[row.id]: { from: "unsupportedFrom" },
});
});
});
@@ -0,0 +1,120 @@
import type { GroupPlatform, ReasoningEffortMapping } from "@/types";
const openAIReasoningEffortValues = [
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
] as const;
const reasoningEffortValuesForPlatform = (
platform: GroupPlatform,
): readonly string[] =>
platform === "openai" ? openAIReasoningEffortValues : [];
export function reasoningEffortOptionsForPlatform(platform: GroupPlatform) {
return reasoningEffortValuesForPlatform(platform).map((value) => ({
value,
label: value,
}));
}
export function normalizeReasoningEffortForPlatform(
platform: GroupPlatform,
value: string | null | undefined,
): string {
const normalized = value?.trim().toLowerCase() ?? "";
return reasoningEffortValuesForPlatform(platform).some(
(allowed) => allowed === normalized,
)
? normalized
: "";
}
export interface ReasoningEffortMappingRow extends ReasoningEffortMapping {
id: string;
}
export type ReasoningEffortMappingErrorCode =
| "fromRequired"
| "toRequired"
| "duplicateFrom"
| "unsupportedFrom"
| "unsupportedTo";
export type ReasoningEffortMappingErrors = Record<
string,
Partial<Record<"from" | "to", ReasoningEffortMappingErrorCode>>
>;
let nextMappingRowID = 0;
export function createReasoningEffortMappingRow(
mapping: Partial<ReasoningEffortMapping> = {},
): ReasoningEffortMappingRow {
nextMappingRowID += 1;
return {
id: `reasoning-effort-mapping-${nextMappingRowID}`,
from: mapping.from ?? "",
to: mapping.to ?? "",
};
}
export function reasoningEffortMappingsToRows(
mappings?: ReasoningEffortMapping[] | null,
platform: GroupPlatform = "openai",
): ReasoningEffortMappingRow[] {
return (mappings ?? []).flatMap((mapping) => {
const from = normalizeReasoningEffortForPlatform(platform, mapping.from);
const to = normalizeReasoningEffortForPlatform(platform, mapping.to);
return from && to
? [createReasoningEffortMappingRow({ from, to })]
: [];
});
}
export function reasoningEffortMappingsToAPI(
rows: ReasoningEffortMappingRow[],
): ReasoningEffortMapping[] {
return rows.map((row) => ({
from: row.from.trim(),
to: row.to.trim(),
}));
}
export function validateReasoningEffortMappings(
rows: ReasoningEffortMappingRow[],
platform: GroupPlatform = "openai",
): ReasoningEffortMappingErrors {
const errors: ReasoningEffortMappingErrors = {};
const sourceRows = new Map<string, ReasoningEffortMappingRow[]>();
rows.forEach((row) => {
const from = row.from.trim();
const to = row.to.trim();
if (!from) {
errors[row.id] = { ...errors[row.id], from: "fromRequired" };
} else if (!normalizeReasoningEffortForPlatform(platform, from)) {
errors[row.id] = { ...errors[row.id], from: "unsupportedFrom" };
} else {
const key = from.toLowerCase();
sourceRows.set(key, [...(sourceRows.get(key) ?? []), row]);
}
if (!to) {
errors[row.id] = { ...errors[row.id], to: "toRequired" };
} else if (!normalizeReasoningEffortForPlatform(platform, to)) {
errors[row.id] = { ...errors[row.id], to: "unsupportedTo" };
}
});
sourceRows.forEach((duplicateRows) => {
if (duplicateRows.length < 2) return;
duplicateRows.forEach((row) => {
errors[row.id] = { ...errors[row.id], from: "duplicateFrom" };
});
});
return errors;
}