feat(groups): add deny-or-downgrade control for reasoning effort ceiling

OpenAI/Composite groups can now reject requests that exceed the max
reasoning effort instead of always rewriting them down to the ceiling.
Existing groups keep the previous auto-downgrade behavior.
This commit is contained in:
刘佳林
2026-09-01 10:15:45 +08:00
parent 2ac784c51a
commit aa7a811e66
47 changed files with 751 additions and 105 deletions
@@ -17,6 +17,22 @@
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortHint") }}</p>
</div>
<div>
<label :for="`${idPrefix}-over-limit`" class="input-label">
{{ t("admin.groups.form.maxReasoningEffortOverLimit") }}
</label>
<Select
:id="`${idPrefix}-over-limit`"
:model-value="overLimit"
:options="overLimitOptions"
:aria-label="t('admin.groups.form.maxReasoningEffortOverLimit')"
:searchable="false"
:disabled="!maxEffort"
@update:model-value="updateOverLimit"
/>
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortOverLimitHint") }}</p>
</div>
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between gap-3">
<label class="input-label mb-0">
@@ -120,6 +136,8 @@ import Select from "@/components/common/Select.vue";
import {
createReasoningEffortMappingRow,
reasoningEffortOptionsForPlatform,
reasoningEffortOverLimitDeny,
reasoningEffortOverLimitDowngrade,
validateReasoningEffortMappings,
type ReasoningEffortMappingErrorCode,
type ReasoningEffortMappingRow,
@@ -129,11 +147,13 @@ const props = defineProps<{
idPrefix: string;
platform: GroupPlatform;
maxEffort: string;
overLimit: string;
mappings: ReasoningEffortMappingRow[];
}>();
const emit = defineEmits<{
(event: "update:maxEffort", value: string): void;
(event: "update:overLimit", value: string): void;
(event: "update:mappings", value: ReasoningEffortMappingRow[]): void;
}>();
@@ -142,6 +162,16 @@ const showValidation = ref(false);
const reasoningEffortOptions = computed(() =>
reasoningEffortOptionsForPlatform(props.platform),
);
const overLimitOptions = computed(() => [
{
value: reasoningEffortOverLimitDowngrade,
label: t("admin.groups.form.maxReasoningEffortOverLimitDowngrade"),
},
{
value: reasoningEffortOverLimitDeny,
label: t("admin.groups.form.maxReasoningEffortOverLimitDeny"),
},
]);
const validationErrors = computed(() =>
validateReasoningEffortMappings(props.mappings, props.platform),
);
@@ -153,6 +183,13 @@ const updateMaxEffort = (value: string | number | boolean | null) => {
emit("update:maxEffort", asString(value));
};
const updateOverLimit = (value: string | number | boolean | null) => {
emit(
"update:overLimit",
asString(value) || reasoningEffortOverLimitDowngrade,
);
};
const updateMapping = (
id: string,
field: "from" | "to",
@@ -859,7 +859,11 @@ export default {
rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).',
maxReasoningEffort: 'Max reasoning effort',
maxReasoningEffortUnlimited: 'Unlimited (follow request)',
maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Higher values are capped; omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortOverLimit: 'Over-limit access control',
maxReasoningEffortOverLimitDowngrade: 'Automatically downgrade when over limit',
maxReasoningEffortOverLimitDeny: 'Deny access',
maxReasoningEffortOverLimitHint: 'Applies after a ceiling is set. Downgrade rewrites values above the ceiling to the ceiling. Deny rejects the request.',
reasoningEffortMappings: 'Reasoning effort mappings',
addReasoningEffortMapping: 'Add mapping',
removeReasoningEffortMapping: 'Remove mapping',
@@ -841,7 +841,11 @@ export default {
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
maxReasoningEffort: '推理强度上限',
maxReasoningEffortUnlimited: '不限制(跟随请求)',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。超过上限时自动降档,不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortOverLimit: '超限访问控制',
maxReasoningEffortOverLimitDowngrade: '超过上限时自动降档',
maxReasoningEffortOverLimitDeny: '拒绝访问',
maxReasoningEffortOverLimitHint: '设置上限后生效。自动降档会将超过上限的请求改写为上限值后转发;拒绝访问则直接返回错误。',
reasoningEffortMappings: '推理强度映射',
addReasoningEffortMapping: '添加映射',
removeReasoningEffortMapping: '删除映射',
+3
View File
@@ -557,6 +557,7 @@ export interface Group {
rate_multiplier: number
rpm_limit?: number // Group-level RPM cap (0 = unlimited); overrides user-level rpm_limit when set
max_reasoning_effort?: string // OpenAI/Codex reasoning ceiling; empty means unlimited
max_reasoning_effort_over_limit?: string // downgrade (default) or deny when over the ceiling
reasoning_effort_mappings?: ReasoningEffortMapping[]
is_exclusive: boolean
status: 'active' | 'inactive'
@@ -818,6 +819,7 @@ export interface CreateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
@@ -880,6 +882,7 @@ export interface UpdateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
+25
View File
@@ -631,6 +631,7 @@
id-prefix="create-group-reasoning"
:platform="createForm.platform"
v-model:max-effort="createForm.max_reasoning_effort"
v-model:over-limit="createForm.max_reasoning_effort_over_limit"
v-model:mappings="createForm.reasoning_effort_mappings"
/>
<div
@@ -2361,6 +2362,7 @@
id-prefix="edit-group-reasoning"
:platform="editForm.platform"
v-model:max-effort="editForm.max_reasoning_effort"
v-model:over-limit="editForm.max_reasoning_effort_over_limit"
v-model:mappings="editForm.reasoning_effort_mappings"
/>
<div v-if="editForm.subscription_type !== 'subscription'">
@@ -4495,8 +4497,10 @@ import {
} from "./groupsProfitControl";
import {
normalizeReasoningEffortForPlatform,
normalizeReasoningEffortOverLimit,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOverLimitDowngrade,
supportsReasoningEffortPolicyPlatform,
type ReasoningEffortMappingRow,
} from "./groupsReasoningEffort";
@@ -5105,6 +5109,7 @@ const createForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5467,6 +5472,7 @@ const editForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5907,6 +5913,7 @@ const closeCreateModal = () => {
createForm.copy_accounts_from_group_ids = [];
createForm.rpm_limit = 0;
createForm.max_reasoning_effort = "";
createForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = [];
createReasoningEffortPolicyRef.value?.resetValidation();
resetModelsListState(createModelsListState);
@@ -6177,6 +6184,9 @@ const handleEdit = async (group: AdminGroup) => {
group.platform,
group.max_reasoning_effort,
);
editForm.max_reasoning_effort_over_limit = normalizeReasoningEffortOverLimit(
group.max_reasoning_effort_over_limit,
);
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
group.reasoning_effort_mappings,
group.platform,
@@ -6198,6 +6208,7 @@ const closeEditModal = () => {
showEditModal.value = false;
editingGroup.value = null;
editForm.max_reasoning_effort = "";
editForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = [];
editReasoningEffortPolicyRef.value?.resetValidation();
editModelRoutingRules.value = [];
@@ -6673,6 +6684,13 @@ watch(
newVal,
createForm.max_reasoning_effort,
);
createForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
newVal,
)
? normalizeReasoningEffortOverLimit(
createForm.max_reasoning_effort_over_limit,
)
: reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(createForm.reasoning_effort_mappings),
newVal,
@@ -6723,6 +6741,13 @@ watch(
newVal,
editForm.max_reasoning_effort,
);
editForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
newVal,
)
? normalizeReasoningEffortOverLimit(
editForm.max_reasoning_effort_over_limit,
)
: reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(editForm.reasoning_effort_mappings),
newVal,
@@ -3,9 +3,12 @@ import { describe, expect, it } from "vitest";
import {
createReasoningEffortMappingRow,
normalizeReasoningEffortForPlatform,
normalizeReasoningEffortOverLimit,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOptionsForPlatform,
reasoningEffortOverLimitDeny,
reasoningEffortOverLimitDowngrade,
supportsReasoningEffortPolicyPlatform,
validateReasoningEffortMappings,
} from "../groupsReasoningEffort";
@@ -89,4 +92,22 @@ describe("groupsReasoningEffort", () => {
[row.id]: { from: "unsupportedFrom" },
});
});
it("normalizes over-limit access control to downgrade or deny", () => {
expect(normalizeReasoningEffortOverLimit(undefined)).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit("")).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit(" downgrade ")).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit("DENY")).toBe(
reasoningEffortOverLimitDeny,
);
expect(normalizeReasoningEffortOverLimit("block")).toBe(
reasoningEffortOverLimitDowngrade,
);
});
});
@@ -9,6 +9,9 @@ const openAIReasoningEffortValues = [
"max",
] as const;
export const reasoningEffortOverLimitDowngrade = "downgrade";
export const reasoningEffortOverLimitDeny = "deny";
const reasoningEffortValuesForPlatform = (
platform: GroupPlatform,
): readonly string[] =>
@@ -41,6 +44,14 @@ export function normalizeReasoningEffortForPlatform(
: "";
}
export function normalizeReasoningEffortOverLimit(
value: string | null | undefined,
): string {
return value?.trim().toLowerCase() === reasoningEffortOverLimitDeny
? reasoningEffortOverLimitDeny
: reasoningEffortOverLimitDowngrade;
}
export interface ReasoningEffortMappingRow extends ReasoningEffortMapping {
id: string;
}