mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 16:29:29 +00:00
Merge pull request #4674 from AdrianZhaoDev/main
feat(groups): add OpenAI reasoning policy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user