Commit 73d72651 authored by Wang Lvyuan's avatar Wang Lvyuan
Browse files

feat: support bulk OpenAI passthrough toggle

parent bda7c39e
...@@ -31,6 +31,57 @@ ...@@ -31,6 +31,57 @@
</p> </p>
</div> </div>
<!-- OpenAI passthrough -->
<div
v-if="allOpenAIPassthroughCapable"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="mb-3 flex items-center justify-between">
<div class="flex-1 pr-4">
<label
id="bulk-edit-openai-passthrough-label"
class="input-label mb-0"
for="bulk-edit-openai-passthrough-enabled"
>
{{ t('admin.accounts.openai.oauthPassthrough') }}
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.oauthPassthroughDesc') }}
</p>
</div>
<input
v-model="enableOpenAIPassthrough"
id="bulk-edit-openai-passthrough-enabled"
type="checkbox"
aria-controls="bulk-edit-openai-passthrough-body"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
</div>
<div
id="bulk-edit-openai-passthrough-body"
:class="!enableOpenAIPassthrough && 'pointer-events-none opacity-50'"
role="group"
aria-labelledby="bulk-edit-openai-passthrough-label"
>
<button
id="bulk-edit-openai-passthrough-toggle"
type="button"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
openaiPassthroughEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
@click="openaiPassthroughEnabled = !openaiPassthroughEnabled"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
openaiPassthroughEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
</div>
<!-- Base URL (API Key only) --> <!-- Base URL (API Key only) -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
...@@ -89,66 +140,30 @@ ...@@ -89,66 +140,30 @@
role="group" role="group"
aria-labelledby="bulk-edit-model-restriction-label" aria-labelledby="bulk-edit-model-restriction-label"
> >
<!-- Mode Toggle --> <div
<div class="mb-4 flex gap-2"> v-if="isOpenAIModelRestrictionDisabled"
<button class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20"
type="button" >
:class="[ <p class="text-xs text-amber-700 dark:text-amber-400">
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all', {{ t('admin.accounts.openai.modelRestrictionDisabledByPassthrough') }}
modelRestrictionMode === 'whitelist' </p>
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
]"
@click="modelRestrictionMode = 'whitelist'"
>
<svg
class="mr-1.5 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{{ t('admin.accounts.modelWhitelist') }}
</button>
<button
type="button"
:class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
modelRestrictionMode === 'mapping'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
]"
@click="modelRestrictionMode = 'mapping'"
>
<svg
class="mr-1.5 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/>
</svg>
{{ t('admin.accounts.modelMapping') }}
</button>
</div> </div>
<!-- Whitelist Mode --> <template v-else>
<div v-if="modelRestrictionMode === 'whitelist'"> <!-- Mode Toggle -->
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20"> <div class="mb-4 flex gap-2">
<p class="text-xs text-blue-700 dark:text-blue-400"> <button
type="button"
:class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
modelRestrictionMode === 'whitelist'
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
]"
@click="modelRestrictionMode = 'whitelist'"
>
<svg <svg
class="mr-1 inline h-4 w-4" class="mr-1.5 inline h-4 w-4"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
...@@ -157,32 +172,23 @@ ...@@ -157,32 +172,23 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
{{ t('admin.accounts.selectAllowedModels') }} {{ t('admin.accounts.modelWhitelist') }}
</p> </button>
</div> <button
type="button"
<ModelWhitelistSelector :class="[
v-model="allowedModels" 'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
:platforms="selectedPlatforms" modelRestrictionMode === 'mapping'
/> ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
<p class="text-xs text-gray-500 dark:text-gray-400"> ]"
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} @click="modelRestrictionMode = 'mapping'"
<span v-if="allowedModels.length === 0">{{ >
t('admin.accounts.supportsAllModels')
}}</span>
</p>
</div>
<!-- Mapping Mode -->
<div v-else>
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
<p class="text-xs text-purple-700 dark:text-purple-400">
<svg <svg
class="mr-1 inline h-4 w-4" class="mr-1.5 inline h-4 w-4"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
...@@ -191,28 +197,124 @@ ...@@ -191,28 +197,124 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/> />
</svg> </svg>
{{ t('admin.accounts.mapRequestModels') }} {{ t('admin.accounts.modelMapping') }}
</button>
</div>
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-400">
<svg
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{{ t('admin.accounts.selectAllowedModels') }}
</p>
</div>
<ModelWhitelistSelector
v-model="allowedModels"
:platforms="selectedPlatforms"
/>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
t('admin.accounts.supportsAllModels')
}}</span>
</p> </p>
</div> </div>
<!-- Model Mapping List --> <!-- Mapping Mode -->
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2"> <div v-else>
<div <div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
v-for="(mapping, index) in modelMappings" <p class="text-xs text-purple-700 dark:text-purple-400">
:key="index" <svg
class="flex items-center gap-2" class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{{ t('admin.accounts.mapRequestModels') }}
</p>
</div>
<!-- Model Mapping List -->
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2">
<div
v-for="(mapping, index) in modelMappings"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="mapping.from"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.requestModel')"
/>
<svg
class="h-4 w-4 flex-shrink-0 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
<input
v-model="mapping.to"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.actualModel')"
/>
<button
type="button"
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
@click="removeModelMapping(index)"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
<button
type="button"
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
@click="addModelMapping"
> >
<input
v-model="mapping.from"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.requestModel')"
/>
<svg <svg
class="h-4 w-4 flex-shrink-0 text-gray-400" class="mr-1 inline h-4 w-4"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
...@@ -221,66 +323,26 @@ ...@@ -221,66 +323,26 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
<input {{ t('admin.accounts.addMapping') }}
v-model="mapping.to" </button>
type="text"
class="input flex-1" <!-- Quick Add Buttons -->
:placeholder="t('admin.accounts.actualModel')" <div class="flex flex-wrap gap-2">
/>
<button <button
v-for="preset in filteredPresets"
:key="preset.label"
type="button" type="button"
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20" :class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
@click="removeModelMapping(index)" @click="addPresetMapping(preset.from, preset.to)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + {{ preset.label }}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>
</template>
<button
type="button"
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
@click="addModelMapping"
>
<svg
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
{{ t('admin.accounts.addMapping') }}
</button>
<!-- Quick Add Buttons -->
<div class="flex flex-wrap gap-2">
<button
v-for="preset in filteredPresets"
:key="preset.label"
type="button"
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
@click="addPresetMapping(preset.from, preset.to)"
>
+ {{ preset.label }}
</button>
</div>
</div>
</div> </div>
</div> </div>
...@@ -821,7 +883,6 @@ import { ...@@ -821,7 +883,6 @@ import {
buildModelMappingObject as buildModelMappingPayload, buildModelMappingObject as buildModelMappingPayload,
getPresetMappingsByPlatform getPresetMappingsByPlatform
} from '@/composables/useModelWhitelist' } from '@/composables/useModelWhitelist'
interface Props { interface Props {
show: boolean show: boolean
accountIds: number[] accountIds: number[]
...@@ -843,6 +904,15 @@ const appStore = useAppStore() ...@@ -843,6 +904,15 @@ const appStore = useAppStore()
// Platform awareness // Platform awareness
const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1) const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1)
const allOpenAIPassthroughCapable = computed(() => {
return (
props.selectedPlatforms.length === 1 &&
props.selectedPlatforms[0] === 'openai' &&
props.selectedTypes.length > 0 &&
props.selectedTypes.every(t => t === 'oauth' || t === 'apikey')
)
})
// 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示) // 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示)
const allAnthropicOAuthOrSetupToken = computed(() => { const allAnthropicOAuthOrSetupToken = computed(() => {
return ( return (
...@@ -886,6 +956,7 @@ const enablePriority = ref(false) ...@@ -886,6 +956,7 @@ const enablePriority = ref(false)
const enableRateMultiplier = ref(false) const enableRateMultiplier = ref(false)
const enableStatus = ref(false) const enableStatus = ref(false)
const enableGroups = ref(false) const enableGroups = ref(false)
const enableOpenAIPassthrough = ref(false)
const enableRpmLimit = ref(false) const enableRpmLimit = ref(false)
// State - field values // State - field values
...@@ -907,6 +978,7 @@ const priority = ref(1) ...@@ -907,6 +978,7 @@ const priority = ref(1)
const rateMultiplier = ref(1) const rateMultiplier = ref(1)
const status = ref<'active' | 'inactive'>('active') const status = ref<'active' | 'inactive'>('active')
const groupIds = ref<number[]>([]) const groupIds = ref<number[]>([])
const openaiPassthroughEnabled = ref(false)
const rpmLimitEnabled = ref(false) const rpmLimitEnabled = ref(false)
const bulkBaseRpm = ref<number | null>(null) const bulkBaseRpm = ref<number | null>(null)
const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered') const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered')
...@@ -933,6 +1005,11 @@ const statusOptions = computed(() => [ ...@@ -933,6 +1005,11 @@ const statusOptions = computed(() => [
{ value: 'active', label: t('common.active') }, { value: 'active', label: t('common.active') },
{ value: 'inactive', label: t('common.inactive') } { value: 'inactive', label: t('common.inactive') }
]) ])
const isOpenAIModelRestrictionDisabled = computed(() =>
allOpenAIPassthroughCapable.value &&
enableOpenAIPassthrough.value &&
openaiPassthroughEnabled.value
)
// Model mapping helpers // Model mapping helpers
const addModelMapping = () => { const addModelMapping = () => {
...@@ -1015,6 +1092,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1015,6 +1092,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
const updates: Record<string, unknown> = {} const updates: Record<string, unknown> = {}
const credentials: Record<string, unknown> = {} const credentials: Record<string, unknown> = {}
let credentialsChanged = false let credentialsChanged = false
const ensureExtra = (): Record<string, unknown> => {
if (!updates.extra) {
updates.extra = {}
}
return updates.extra as Record<string, unknown>
}
if (enableProxy.value) { if (enableProxy.value) {
// 后端期望 proxy_id: 0 表示清除代理,而不是 null // 后端期望 proxy_id: 0 表示清除代理,而不是 null
...@@ -1055,7 +1138,15 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1055,7 +1138,15 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
} }
} }
if (enableModelRestriction.value) { if (enableOpenAIPassthrough.value) {
const extra = ensureExtra()
extra.openai_passthrough = openaiPassthroughEnabled.value
if (!openaiPassthroughEnabled.value) {
extra.openai_oauth_passthrough = false
}
}
if (enableModelRestriction.value && !isOpenAIModelRestrictionDisabled.value) {
// 统一使用 model_mapping 字段 // 统一使用 model_mapping 字段
if (modelRestrictionMode.value === 'whitelist') { if (modelRestrictionMode.value === 'whitelist') {
// 白名单模式:将模型转换为 model_mapping 格式(key=value) // 白名单模式:将模型转换为 model_mapping 格式(key=value)
...@@ -1091,7 +1182,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1091,7 +1182,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
// RPM limit settings (写入 extra 字段) // RPM limit settings (写入 extra 字段)
if (enableRpmLimit.value) { if (enableRpmLimit.value) {
const extra: Record<string, unknown> = {} const extra = ensureExtra()
if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) { if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) {
extra.base_rpm = bulkBaseRpm.value extra.base_rpm = bulkBaseRpm.value
extra.rpm_strategy = bulkRpmStrategy.value extra.rpm_strategy = bulkRpmStrategy.value
...@@ -1111,8 +1202,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1111,8 +1202,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
// UMQ mode(独立于 RPM 保存) // UMQ mode(独立于 RPM 保存)
if (userMsgQueueMode.value !== null) { if (userMsgQueueMode.value !== null) {
if (!updates.extra) updates.extra = {} const umqExtra = ensureExtra()
const umqExtra = updates.extra as Record<string, unknown>
umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖 umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge) umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
} }
...@@ -1168,6 +1258,7 @@ const handleSubmit = async () => { ...@@ -1168,6 +1258,7 @@ const handleSubmit = async () => {
const hasAnyFieldEnabled = const hasAnyFieldEnabled =
enableBaseUrl.value || enableBaseUrl.value ||
enableOpenAIPassthrough.value ||
enableModelRestriction.value || enableModelRestriction.value ||
enableCustomErrorCodes.value || enableCustomErrorCodes.value ||
enableInterceptWarmup.value || enableInterceptWarmup.value ||
...@@ -1269,10 +1360,12 @@ watch( ...@@ -1269,10 +1360,12 @@ watch(
enableRateMultiplier.value = false enableRateMultiplier.value = false
enableStatus.value = false enableStatus.value = false
enableGroups.value = false enableGroups.value = false
enableOpenAIPassthrough.value = false
enableRpmLimit.value = false enableRpmLimit.value = false
// Reset all values // Reset all values
baseUrl.value = '' baseUrl.value = ''
openaiPassthroughEnabled.value = false
modelRestrictionMode.value = 'whitelist' modelRestrictionMode.value = 'whitelist'
allowedModels.value = [] allowedModels.value = []
modelMappings.value = [] modelMappings.value = []
......
...@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) { ...@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) {
stubs: { stubs: {
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' }, BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
ConfirmDialog: true, ConfirmDialog: true,
Select: true, Select: {
props: ['modelValue', 'options'],
emits: ['update:modelValue'],
template: `
<select
v-bind="$attrs"
:value="modelValue"
@change="$emit('update:modelValue', $event.target.value)"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
`
},
ProxySelector: true, ProxySelector: true,
GroupSelector: true, GroupSelector: true,
Icon: true Icon: true
...@@ -115,4 +129,63 @@ describe('BulkEditAccountModal', () => { ...@@ -115,4 +129,63 @@ describe('BulkEditAccountModal', () => {
} }
}) })
}) })
it('OpenAI 账号批量编辑可开启自动透传', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],
selectedTypes: ['oauth']
})
await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true)
await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click')
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
extra: {
openai_passthrough: true
}
})
})
it('OpenAI 账号批量编辑可关闭自动透传', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],
selectedTypes: ['apikey']
})
await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true)
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
extra: {
openai_passthrough: false,
openai_oauth_passthrough: false
}
})
})
it('开启 OpenAI 自动透传时不再同时提交模型限制', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],
selectedTypes: ['oauth']
})
await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true)
await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click')
await wrapper.get('#bulk-edit-model-restriction-enabled').setValue(true)
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
extra: {
openai_passthrough: true
}
})
expect(wrapper.text()).toContain('admin.accounts.openai.modelRestrictionDisabledByPassthrough')
})
}) })
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment