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
@@ -0,0 +1,197 @@
<template>
<div class="space-y-4">
<div>
<label :for="`${idPrefix}-max-effort`" class="input-label">
{{ t("admin.groups.form.maxReasoningEffort") }}
</label>
<Select
:id="`${idPrefix}-max-effort`"
:model-value="maxEffort"
:options="reasoningEffortOptions"
:placeholder="t('admin.groups.form.maxReasoningEffortUnlimited')"
:aria-label="t('admin.groups.form.maxReasoningEffort')"
:searchable="false"
clearable
@update:model-value="updateMaxEffort"
/>
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortHint") }}</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">
{{ t("admin.groups.form.reasoningEffortMappings") }}
</label>
<button
type="button"
class="inline-flex min-h-11 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-primary-600 transition-colors hover:bg-primary-50 hover:text-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:text-primary-400 dark:hover:bg-primary-900/20 dark:hover:text-primary-300"
@click="addMapping"
>
<Icon name="plus" size="sm" />
{{ t("admin.groups.form.addReasoningEffortMapping") }}
</button>
</div>
<div v-if="mappings.length > 0" class="space-y-2">
<div
v-for="row in mappings"
:key="row.id"
class="rounded-lg border border-gray-200 bg-gray-50/40 p-3 dark:border-dark-600 dark:bg-dark-800/40"
>
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto] md:items-start">
<div>
<label :for="`${idPrefix}-${row.id}-from`" class="input-label">
{{ t("admin.groups.form.reasoningEffortFrom") }}
</label>
<Select
:id="`${idPrefix}-${row.id}-from`"
:model-value="row.from"
:options="reasoningEffortOptions"
:placeholder="t('admin.groups.form.reasoningEffortFromPlaceholder')"
:error="showValidation && !!validationErrors[row.id]?.from"
:aria-label="t('admin.groups.form.reasoningEffortFrom')"
:aria-describedby="showValidation && validationErrors[row.id]?.from ? `${idPrefix}-${row.id}-from-error` : undefined"
:searchable="false"
clearable
@update:model-value="updateMapping(row.id, 'from', $event)"
/>
<p
v-if="showValidation && validationErrors[row.id]?.from"
:id="`${idPrefix}-${row.id}-from-error`"
class="mt-1 text-xs text-red-600 dark:text-red-400"
role="alert"
>
{{ mappingErrorText(validationErrors[row.id]?.from) }}
</p>
</div>
<div class="hidden pt-8 text-gray-400 md:block dark:text-dark-400">
<Icon name="arrowRight" size="sm" />
</div>
<div>
<label :for="`${idPrefix}-${row.id}-to`" class="input-label">
{{ t("admin.groups.form.reasoningEffortTo") }}
</label>
<Select
:id="`${idPrefix}-${row.id}-to`"
:model-value="row.to"
:options="reasoningEffortOptions"
:placeholder="t('admin.groups.form.reasoningEffortToPlaceholder')"
:error="showValidation && !!validationErrors[row.id]?.to"
:aria-label="t('admin.groups.form.reasoningEffortTo')"
:aria-describedby="showValidation && validationErrors[row.id]?.to ? `${idPrefix}-${row.id}-to-error` : undefined"
:searchable="false"
clearable
@update:model-value="updateMapping(row.id, 'to', $event)"
/>
<p
v-if="showValidation && validationErrors[row.id]?.to"
:id="`${idPrefix}-${row.id}-to-error`"
class="mt-1 text-xs text-red-600 dark:text-red-400"
role="alert"
>
{{ mappingErrorText(validationErrors[row.id]?.to) }}
</p>
</div>
<button
type="button"
class="flex h-11 w-11 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 focus:outline-none focus:ring-2 focus:ring-red-500/30 md:mt-6 dark:hover:bg-red-900/20 dark:hover:text-red-400"
:title="t('admin.groups.form.removeReasoningEffortMapping')"
:aria-label="t('admin.groups.form.removeReasoningEffortMapping')"
@click="removeMapping(row.id)"
>
<Icon name="trash" size="sm" />
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import type { GroupPlatform } from "@/types";
import Icon from "@/components/icons/Icon.vue";
import Select from "@/components/common/Select.vue";
import {
createReasoningEffortMappingRow,
reasoningEffortOptionsForPlatform,
validateReasoningEffortMappings,
type ReasoningEffortMappingErrorCode,
type ReasoningEffortMappingRow,
} from "@/views/admin/groupsReasoningEffort";
const props = defineProps<{
idPrefix: string;
platform: GroupPlatform;
maxEffort: string;
mappings: ReasoningEffortMappingRow[];
}>();
const emit = defineEmits<{
(event: "update:maxEffort", value: string): void;
(event: "update:mappings", value: ReasoningEffortMappingRow[]): void;
}>();
const { t } = useI18n();
const showValidation = ref(false);
const reasoningEffortOptions = computed(() =>
reasoningEffortOptionsForPlatform(props.platform),
);
const validationErrors = computed(() =>
validateReasoningEffortMappings(props.mappings, props.platform),
);
const asString = (value: string | number | boolean | null): string =>
value == null ? "" : String(value);
const updateMaxEffort = (value: string | number | boolean | null) => {
emit("update:maxEffort", asString(value));
};
const updateMapping = (
id: string,
field: "from" | "to",
value: string | number | boolean | null,
) => {
emit(
"update:mappings",
props.mappings.map((row) =>
row.id === id ? { ...row, [field]: asString(value) } : row,
),
);
};
const addMapping = () => {
emit("update:mappings", [
...props.mappings,
createReasoningEffortMappingRow(),
]);
};
const removeMapping = (id: string) => {
emit(
"update:mappings",
props.mappings.filter((row) => row.id !== id),
);
};
const mappingErrorText = (
code: ReasoningEffortMappingErrorCode | undefined,
): string => (code ? t(`admin.groups.form.${code}`) : "");
const validate = (): boolean => {
showValidation.value = true;
return Object.keys(validationErrors.value).length === 0;
};
const resetValidation = () => {
showValidation.value = false;
};
defineExpose({ validate, resetValidation });
</script>
+7 -1
View File
@@ -7,7 +7,9 @@
:disabled="disabled"
:aria-expanded="isOpen"
:aria-haspopup="true"
aria-label="Select option"
:id="id"
:aria-label="ariaLabel ?? 'Select option'"
:aria-describedby="ariaDescribedby"
:class="[
'select-trigger',
isOpen && 'select-trigger-open',
@@ -65,6 +67,7 @@
v-model="searchQuery"
type="text"
:placeholder="searchPlaceholderText"
:aria-label="searchPlaceholderText"
class="select-search-input"
@click.stop
/>
@@ -148,6 +151,9 @@ interface Props {
creatable?: boolean
creatablePrefix?: string
clearable?: boolean
id?: string
ariaLabel?: string
ariaDescribedby?: string
}
interface Emits {
@@ -848,6 +848,21 @@ export default {
rpmLimit: 'Requests Per Minute (RPM)',
rpmLimitPlaceholder: '0 = unlimited',
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. Higher values are capped; omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
reasoningEffortMappings: 'Reasoning effort mappings',
addReasoningEffortMapping: 'Add mapping',
removeReasoningEffortMapping: 'Remove mapping',
reasoningEffortFrom: 'Request value',
reasoningEffortTo: 'Forwarded value',
reasoningEffortFromPlaceholder: 'Select A',
reasoningEffortToPlaceholder: 'Select B',
fromRequired: 'Select request value A',
toRequired: 'Select forwarded value B',
unsupportedFrom: 'Request value is not supported by this platform',
unsupportedTo: 'Forwarded value is not supported by this platform',
duplicateFrom: 'Request value A must be unique',
exclusiveLabel: 'Exclusive Group',
exclusiveHint: 'Exclusive group, can be manually assigned to users',
platformLabel: 'Platform Restriction',
@@ -830,6 +830,21 @@ export default {
rpmLimit: '每分钟请求数 (RPM)',
rpmLimitPlaceholder: '0 表示不限制',
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
maxReasoningEffort: '推理强度上限',
maxReasoningEffortUnlimited: '不限制(跟随请求)',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;超过上限时自动降档,不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
reasoningEffortMappings: '推理强度映射',
addReasoningEffortMapping: '添加映射',
removeReasoningEffortMapping: '删除映射',
reasoningEffortFrom: '请求值',
reasoningEffortTo: '转发值',
reasoningEffortFromPlaceholder: '请选择 A',
reasoningEffortToPlaceholder: '请选择 B',
fromRequired: '请选择请求值 A',
toRequired: '请选择转发值 B',
unsupportedFrom: '请求值不受当前平台支持',
unsupportedTo: '转发值不受当前平台支持',
duplicateFrom: '请求值 A 不能重复',
exclusiveLabel: '专属分组',
exclusiveHint: '专属分组,可以手动指定给用户',
platformLabel: '平台限制',
+11
View File
@@ -503,6 +503,11 @@ export interface OpenAIMessagesDispatchModelConfig {
exact_model_mappings?: Record<string, string>
}
export interface ReasoningEffortMapping {
from: string
to: string
}
export interface Group {
id: number
name: string
@@ -510,6 +515,8 @@ export interface Group {
platform: GroupPlatform
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
reasoning_effort_mappings?: ReasoningEffortMapping[]
is_exclusive: boolean
status: 'active' | 'inactive'
subscription_type: SubscriptionType
@@ -683,6 +690,8 @@ export interface CreateGroupRequest {
model_routing?: Record<string, number[]> | null
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
// 从指定分组复制账号
@@ -731,6 +740,8 @@ export interface UpdateGroupRequest {
model_routing?: Record<string, number[]> | null
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
copy_accounts_from_group_ids?: number[]
+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;
}