Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
73d72651
Commit
73d72651
authored
Mar 23, 2026
by
Wang Lvyuan
Browse files
feat: support bulk OpenAI passthrough toggle
parent
bda7c39e
Changes
2
Show whitespace changes
Inline
Side-by-side
frontend/src/components/account/BulkEditAccountModal.vue
View file @
73d72651
...
...
@@ -31,6 +31,57 @@
<
/p
>
<
/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
)
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
...
...
@@ -89,6 +140,16 @@
role
=
"
group
"
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
-->
<
div
class
=
"
mb-4 flex gap-2
"
>
<
button
...
...
@@ -281,6 +342,7 @@
<
/button
>
<
/div
>
<
/div
>
<
/template
>
<
/div
>
<
/div
>
...
...
@@ -821,7 +883,6 @@ import {
buildModelMappingObject
as
buildModelMappingPayload
,
getPresetMappingsByPlatform
}
from
'
@/composables/useModelWhitelist
'
interface
Props
{
show
:
boolean
accountIds
:
number
[]
...
...
@@ -843,6 +904,15 @@ const appStore = useAppStore()
// Platform awareness
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 配置仅在此条件下显示)
const
allAnthropicOAuthOrSetupToken
=
computed
(()
=>
{
return
(
...
...
@@ -886,6 +956,7 @@ const enablePriority = ref(false)
const
enableRateMultiplier
=
ref
(
false
)
const
enableStatus
=
ref
(
false
)
const
enableGroups
=
ref
(
false
)
const
enableOpenAIPassthrough
=
ref
(
false
)
const
enableRpmLimit
=
ref
(
false
)
// State - field values
...
...
@@ -907,6 +978,7 @@ const priority = ref(1)
const
rateMultiplier
=
ref
(
1
)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
groupIds
=
ref
<
number
[]
>
([])
const
openaiPassthroughEnabled
=
ref
(
false
)
const
rpmLimitEnabled
=
ref
(
false
)
const
bulkBaseRpm
=
ref
<
number
|
null
>
(
null
)
const
bulkRpmStrategy
=
ref
<
'
tiered
'
|
'
sticky_exempt
'
>
(
'
tiered
'
)
...
...
@@ -933,6 +1005,11 @@ const statusOptions = computed(() => [
{
value
:
'
active
'
,
label
:
t
(
'
common.active
'
)
}
,
{
value
:
'
inactive
'
,
label
:
t
(
'
common.inactive
'
)
}
])
const
isOpenAIModelRestrictionDisabled
=
computed
(()
=>
allOpenAIPassthroughCapable
.
value
&&
enableOpenAIPassthrough
.
value
&&
openaiPassthroughEnabled
.
value
)
// Model mapping helpers
const
addModelMapping
=
()
=>
{
...
...
@@ -1015,6 +1092,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
const
updates
:
Record
<
string
,
unknown
>
=
{
}
const
credentials
:
Record
<
string
,
unknown
>
=
{
}
let
credentialsChanged
=
false
const
ensureExtra
=
():
Record
<
string
,
unknown
>
=>
{
if
(
!
updates
.
extra
)
{
updates
.
extra
=
{
}
}
return
updates
.
extra
as
Record
<
string
,
unknown
>
}
if
(
enableProxy
.
value
)
{
// 后端期望 proxy_id: 0 表示清除代理,而不是 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 字段
if
(
modelRestrictionMode
.
value
===
'
whitelist
'
)
{
// 白名单模式:将模型转换为 model_mapping 格式(key=value)
...
...
@@ -1091,7 +1182,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
// RPM limit settings (写入 extra 字段)
if
(
enableRpmLimit
.
value
)
{
const
extra
:
Record
<
string
,
unknown
>
=
{
}
const
extra
=
ensureExtra
()
if
(
rpmLimitEnabled
.
value
&&
bulkBaseRpm
.
value
!=
null
&&
bulkBaseRpm
.
value
>
0
)
{
extra
.
base_rpm
=
bulkBaseRpm
.
value
extra
.
rpm_strategy
=
bulkRpmStrategy
.
value
...
...
@@ -1111,8 +1202,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
// UMQ mode(独立于 RPM 保存)
if
(
userMsgQueueMode
.
value
!==
null
)
{
if
(
!
updates
.
extra
)
updates
.
extra
=
{
}
const
umqExtra
=
updates
.
extra
as
Record
<
string
,
unknown
>
const
umqExtra
=
ensureExtra
()
umqExtra
.
user_msg_queue_mode
=
userMsgQueueMode
.
value
// '' = 清除账号级覆盖
umqExtra
.
user_msg_queue_enabled
=
false
// 清理旧字段(JSONB merge)
}
...
...
@@ -1168,6 +1258,7 @@ const handleSubmit = async () => {
const
hasAnyFieldEnabled
=
enableBaseUrl
.
value
||
enableOpenAIPassthrough
.
value
||
enableModelRestriction
.
value
||
enableCustomErrorCodes
.
value
||
enableInterceptWarmup
.
value
||
...
...
@@ -1269,10 +1360,12 @@ watch(
enableRateMultiplier
.
value
=
false
enableStatus
.
value
=
false
enableGroups
.
value
=
false
enableOpenAIPassthrough
.
value
=
false
enableRpmLimit
.
value
=
false
// Reset all values
baseUrl
.
value
=
''
openaiPassthroughEnabled
.
value
=
false
modelRestrictionMode
.
value
=
'
whitelist
'
allowedModels
.
value
=
[]
modelMappings
.
value
=
[]
...
...
frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts
View file @
73d72651
...
...
@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) {
stubs
:
{
BaseDialog
:
{
template
:
'
<div><slot /><slot name="footer" /></div>
'
},
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
,
GroupSelector
:
true
,
Icon
:
true
...
...
@@ -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
'
)
})
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment