Unverified Commit ef5c8e68 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1231 from LvyuanW/bulk-openai-passthrough-worktree

Support bulk editing for OpenAI passthrough
parents d571f300 bb399e56
...@@ -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,6 +140,16 @@ ...@@ -89,6 +140,16 @@
role="group" role="group"
aria-labelledby="bulk-edit-model-restriction-label" aria-labelledby="bulk-edit-model-restriction-label"
> >
<div
v-if="isOpenAIModelRestrictionDisabled"
class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20"
>
<p class="text-xs text-amber-700 dark:text-amber-400">
{{ t('admin.accounts.openai.modelRestrictionDisabledByPassthrough') }}
</p>
</div>
<template v-else>
<!-- Mode Toggle --> <!-- Mode Toggle -->
<div class="mb-4 flex gap-2"> <div class="mb-4 flex gap-2">
<button <button
...@@ -281,6 +342,7 @@ ...@@ -281,6 +342,7 @@
</button> </button>
</div> </div>
</div> </div>
</template>
</div> </div>
</div> </div>
...@@ -865,7 +927,6 @@ import { ...@@ -865,7 +927,6 @@ import {
resolveOpenAIWSModeConcurrencyHintKey resolveOpenAIWSModeConcurrencyHintKey
} from '@/utils/openaiWsMode' } from '@/utils/openaiWsMode'
import type { OpenAIWSMode } from '@/utils/openaiWsMode' import type { OpenAIWSMode } from '@/utils/openaiWsMode'
interface Props { interface Props {
show: boolean show: boolean
accountIds: number[] accountIds: number[]
...@@ -887,6 +948,15 @@ const appStore = useAppStore() ...@@ -887,6 +948,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')
)
})
const allOpenAIOAuth = computed(() => { const allOpenAIOAuth = computed(() => {
return ( return (
props.selectedPlatforms.length === 1 && props.selectedPlatforms.length === 1 &&
...@@ -939,6 +1009,7 @@ const enablePriority = ref(false) ...@@ -939,6 +1009,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 enableOpenAIWSMode = ref(false) const enableOpenAIWSMode = ref(false)
const enableRpmLimit = ref(false) const enableRpmLimit = ref(false)
...@@ -961,6 +1032,7 @@ const priority = ref(1) ...@@ -961,6 +1032,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 openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF) const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const rpmLimitEnabled = ref(false) const rpmLimitEnabled = ref(false)
const bulkBaseRpm = ref<number | null>(null) const bulkBaseRpm = ref<number | null>(null)
...@@ -988,6 +1060,13 @@ const statusOptions = computed(() => [ ...@@ -988,6 +1060,13 @@ 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
)
const openAIWSModeOptions = computed(() => [ const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') }, { value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') } { value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
...@@ -1123,7 +1202,15 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1123,7 +1202,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)
...@@ -1243,6 +1330,7 @@ const handleSubmit = async () => { ...@@ -1243,6 +1330,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 ||
...@@ -1345,11 +1433,13 @@ watch( ...@@ -1345,11 +1433,13 @@ watch(
enableRateMultiplier.value = false enableRateMultiplier.value = false
enableStatus.value = false enableStatus.value = false
enableGroups.value = false enableGroups.value = false
enableOpenAIPassthrough.value = false
enableOpenAIWSMode.value = false enableOpenAIWSMode.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 = []
......
...@@ -130,6 +130,25 @@ describe('BulkEditAccountModal', () => { ...@@ -130,6 +130,25 @@ 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 OAuth 批量编辑应提交 OAuth 专属 WS mode 字段', async () => { it('OpenAI OAuth 批量编辑应提交 OAuth 专属 WS mode 字段', async () => {
const wrapper = mountModal({ const wrapper = mountModal({
selectedPlatforms: ['openai'], selectedPlatforms: ['openai'],
...@@ -158,4 +177,44 @@ describe('BulkEditAccountModal', () => { ...@@ -158,4 +177,44 @@ describe('BulkEditAccountModal', () => {
expect(wrapper.find('#bulk-edit-openai-ws-mode-enabled').exists()).toBe(false) expect(wrapper.find('#bulk-edit-openai-ws-mode-enabled').exists()).toBe(false)
}) })
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