Merge pull request #3565 from zy6p/zy6p/pr-openai-ws-http-bridge

feat(openai-ws): 支持 http_bridge ingress 模式
This commit is contained in:
Wesley Liddick
2026-07-02 17:40:32 +08:00
committed by GitHub
23 changed files with 460 additions and 52 deletions
@@ -1153,6 +1153,7 @@ import {
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_OFF,
OPENAI_WS_MODE_PASSTHROUGH,
OPENAI_WS_MODE_HTTP_BRIDGE,
isOpenAIWSModeEnabled,
resolveOpenAIWSModeConcurrencyHintKey
} from '@/utils/openaiWsMode'
@@ -1194,7 +1195,7 @@ const allOpenAIPassthroughCapable = computed(() => {
targetSelectedPlatforms.value.length === 1 &&
targetSelectedPlatforms.value[0] === 'openai' &&
targetSelectedTypes.value.length > 0 &&
targetSelectedTypes.value.every(t => t === 'oauth' || t === 'apikey')
targetSelectedTypes.value.every(t => t === 'oauth' || t === 'setup-token' || t === 'apikey')
)
})
@@ -1203,7 +1204,7 @@ const allOpenAIOAuth = computed(() => {
targetSelectedPlatforms.value.length === 1 &&
targetSelectedPlatforms.value[0] === 'openai' &&
targetSelectedTypes.value.length > 0 &&
targetSelectedTypes.value.every(t => t === 'oauth')
targetSelectedTypes.value.every(t => t === 'oauth' || t === 'setup-token')
)
})
@@ -1330,7 +1331,8 @@ const isOpenAIModelRestrictionDisabled = computed(
const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') },
{ value: OPENAI_WS_MODE_HTTP_BRIDGE, label: t('admin.accounts.openai.wsModeHttpBridge') }
])
const openAICompactModeOptions = computed(() => [
{ value: 'auto', label: t('admin.accounts.openai.compactModeAuto') },
@@ -3337,6 +3337,7 @@ import {
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_OFF,
OPENAI_WS_MODE_PASSTHROUGH,
OPENAI_WS_MODE_HTTP_BRIDGE,
isOpenAIWSModeEnabled,
resolveOpenAIWSModeConcurrencyHintKey,
type OpenAIWSMode
@@ -3703,7 +3704,8 @@ const geminiSelectedTier = computed(() => {
const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') },
{ value: OPENAI_WS_MODE_HTTP_BRIDGE, label: t('admin.accounts.openai.wsModeHttpBridge') }
])
const openaiResponsesWebSocketV2Mode = computed({
@@ -1342,7 +1342,7 @@
<!-- OpenAI 自动透传开关(OAuth/API Key) -->
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'apikey')"
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
@@ -1372,7 +1372,7 @@
<!-- OpenAI Codex 图片生成桥接账号级覆盖 -->
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'apikey')"
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="overflow-hidden rounded-lg border border-sky-100 bg-sky-50/60 shadow-sm dark:border-sky-900/50 dark:bg-sky-950/20">
@@ -1432,7 +1432,7 @@
<!-- OpenAI WS Mode 三态(off/ctx_pool/passthrough) -->
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'apikey')"
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
@@ -1446,7 +1446,7 @@
</p>
</div>
<div class="w-52">
<Select v-model="openaiResponsesWebSocketV2Mode" :options="openAIWSModeOptions" />
<Select v-model="openaiResponsesWebSocketV2Mode" data-testid="edit-openai-ws-mode-select" :options="openAIWSModeOptions" />
</div>
</div>
</div>
@@ -1680,7 +1680,7 @@
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<div
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token')"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between">
@@ -1735,7 +1735,7 @@
</div>
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'apikey')"
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="flex items-center justify-between">
@@ -2442,6 +2442,7 @@ import {
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_OFF,
OPENAI_WS_MODE_PASSTHROUGH,
OPENAI_WS_MODE_HTTP_BRIDGE,
isOpenAIWSModeEnabled,
resolveOpenAIWSModeConcurrencyHintKey,
type OpenAIWSMode,
@@ -2660,7 +2661,8 @@ const editResetTimezone = ref<string | null>(null)
const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') },
{ value: OPENAI_WS_MODE_HTTP_BRIDGE, label: t('admin.accounts.openai.wsModeHttpBridge') }
])
const openaiResponsesWebSocketV2Mode = computed({
get: () => {
@@ -3023,7 +3025,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
autoPause5hDisabled.value = extra?.auto_pause_5h_disabled === true
autoPause7dDisabled.value = extra?.auto_pause_7d_disabled === true
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
// Load OpenAI passthrough toggle (OpenAI OAuth/SetupToken/API Key)
openaiPassthroughEnabled.value = false
openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
@@ -3037,7 +3039,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
webSearchEmulationMode.value = 'default'
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'setup-token' || newAccount.type === 'apikey')) {
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
if (newAccount.type === 'apikey') {
@@ -3069,7 +3071,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
fallbackEnabledKeys: ['responses_websockets_v2_enabled', 'openai_ws_enabled'],
defaultMode: OPENAI_WS_MODE_OFF
})
if (newAccount.type === 'oauth') {
if (newAccount.type === 'oauth' || newAccount.type === 'setup-token') {
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
codexCLIOnlyAppServerEnabled.value =
extra?.codex_cli_only_allow_app_server === true
@@ -4138,12 +4140,12 @@ const handleSubmit = async () => {
updatePayload.extra = newExtra
}
// For OpenAI OAuth/API Key accounts, handle passthrough mode in extra
if (props.account.platform === 'openai' && (props.account.type === 'oauth' || props.account.type === 'apikey')) {
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
const newExtra: Record<string, unknown> = { ...currentExtra }
// For OpenAI OAuth/SetupToken/API Key accounts, handle passthrough mode in extra
if (props.account.platform === 'openai' && (props.account.type === 'oauth' || props.account.type === 'setup-token' || props.account.type === 'apikey')) {
const currentExtra = (props.account.extra as Record<string, unknown>) || {}
const newExtra: Record<string, unknown> = { ...currentExtra }
const hadCodexCLIOnlyEnabled = currentExtra.codex_cli_only === true
if (props.account.type === 'oauth') {
if (props.account.type === 'oauth' || props.account.type === 'setup-token') {
newExtra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
newExtra.openai_oauth_responses_websockets_v2_enabled = isOpenAIWSModeEnabled(openaiOAuthResponsesWebSocketV2Mode.value)
} else if (props.account.type === 'apikey') {
@@ -4198,7 +4200,7 @@ const handleSubmit = async () => {
newExtra.codex_image_generation_bridge = codexImageGenerationBridgeMode.value === 'enabled'
}
if (props.account.type === 'oauth') {
if (props.account.type === 'oauth' || props.account.type === 'setup-token') {
if (codexCLIOnlyEnabled.value) {
newExtra.codex_cli_only = true
} else if (hadCodexCLIOnlyEnabled) {
@@ -149,21 +149,21 @@ describe('BulkEditAccountModal', () => {
})
})
it('OpenAI OAuth 批量编辑应提交 OAuth 专属 WS mode 字段', async () => {
it('OpenAI OAuth 批量编辑应提交 OAuth 专属 WS mode 字段(含 http_bridge)', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],
selectedTypes: ['oauth']
})
await wrapper.get('#bulk-edit-openai-ws-mode-enabled').setValue(true)
await wrapper.get('[data-testid="bulk-edit-openai-ws-mode-select"]').setValue('passthrough')
await wrapper.get('[data-testid="bulk-edit-openai-ws-mode-select"]').setValue('http_bridge')
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_oauth_responses_websockets_v2_mode: 'passthrough',
openai_oauth_responses_websockets_v2_mode: 'http_bridge',
openai_oauth_responses_websockets_v2_enabled: true
}
})
@@ -241,6 +241,17 @@ function buildAntigravityAccount(projectId = 'configured-project') {
} as any
}
function buildOpenAISetupTokenAccount() {
return {
...buildAccount(),
type: 'setup-token',
extra: {
openai_oauth_responses_websockets_v2_mode: 'ctx_pool',
openai_oauth_responses_websockets_v2_enabled: true
}
} as any
}
function mountModal(account = buildAccount()) {
return mount(EditAccountModal, {
props: {
@@ -566,6 +577,23 @@ describe('EditAccountModal', () => {
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('codex_image_generation_bridge_enabled')
})
it('setup-token account can select and submit OAuth WS mode', async () => {
const account = buildOpenAISetupTokenAccount()
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
await wrapper.get('[data-testid="edit-openai-ws-mode-select"]').setValue('http_bridge')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_oauth_responses_websockets_v2_mode).toBe('http_bridge')
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_oauth_responses_websockets_v2_enabled).toBe(true)
})
it('allows saving apikey account when backend redacted api_key but credentials_status reports it exists', async () => {
// 新前端 + 新后端:响应已脱敏,credentials 里没有 api_key,credentials_status.has_api_key=true
const account = buildAccount()
+1
View File
@@ -3515,6 +3515,7 @@ export default {
wsModeOff: 'Off (off)',
wsModeCtxPool: 'Context Pool (ctx_pool)',
wsModePassthrough: 'Passthrough (passthrough)',
wsModeHttpBridge: 'HTTP Bridge (http_bridge)',
wsModeShared: 'Shared (shared)',
wsModeDedicated: 'Dedicated (dedicated)',
wsModeConcurrencyHint:
+1
View File
@@ -3693,6 +3693,7 @@ export default {
wsModeOff: '关闭(off)',
wsModeCtxPool: '上下文池(ctx_pool)',
wsModePassthrough: '透传(passthrough)',
wsModeHttpBridge: 'HTTP 桥接(http_bridge)',
wsModeShared: '共享(shared)',
wsModeDedicated: '独享(dedicated)',
wsModeConcurrencyHint: '启用 WS mode 后,该账号并发数将作为该账号 WS 连接池上限。',
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_HTTP_BRIDGE,
OPENAI_WS_MODE_OFF,
OPENAI_WS_MODE_PASSTHROUGH,
isOpenAIWSModeEnabled,
@@ -15,6 +16,7 @@ describe('openaiWsMode utils', () => {
expect(normalizeOpenAIWSMode('off')).toBe(OPENAI_WS_MODE_OFF)
expect(normalizeOpenAIWSMode('ctx_pool')).toBe(OPENAI_WS_MODE_CTX_POOL)
expect(normalizeOpenAIWSMode('passthrough')).toBe(OPENAI_WS_MODE_PASSTHROUGH)
expect(normalizeOpenAIWSMode('http_bridge')).toBe(OPENAI_WS_MODE_HTTP_BRIDGE)
expect(normalizeOpenAIWSMode(' Shared ')).toBe(OPENAI_WS_MODE_CTX_POOL)
expect(normalizeOpenAIWSMode('DEDICATED')).toBe(OPENAI_WS_MODE_CTX_POOL)
expect(normalizeOpenAIWSMode('invalid')).toBeNull()
@@ -54,6 +56,7 @@ describe('openaiWsMode utils', () => {
expect(isOpenAIWSModeEnabled(OPENAI_WS_MODE_OFF)).toBe(false)
expect(isOpenAIWSModeEnabled(OPENAI_WS_MODE_CTX_POOL)).toBe(true)
expect(isOpenAIWSModeEnabled(OPENAI_WS_MODE_PASSTHROUGH)).toBe(true)
expect(isOpenAIWSModeEnabled(OPENAI_WS_MODE_HTTP_BRIDGE)).toBe(true)
})
it('resolves concurrency hint key by mode', () => {
@@ -66,5 +69,8 @@ describe('openaiWsMode utils', () => {
expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_PASSTHROUGH)).toBe(
'admin.accounts.openai.wsModePassthroughHint'
)
expect(resolveOpenAIWSModeConcurrencyHintKey(OPENAI_WS_MODE_HTTP_BRIDGE)).toBe(
'admin.accounts.openai.wsModePassthroughHint'
)
})
})
+5 -2
View File
@@ -1,16 +1,19 @@
export const OPENAI_WS_MODE_OFF = 'off'
export const OPENAI_WS_MODE_CTX_POOL = 'ctx_pool'
export const OPENAI_WS_MODE_PASSTHROUGH = 'passthrough'
export const OPENAI_WS_MODE_HTTP_BRIDGE = 'http_bridge'
export type OpenAIWSMode =
| typeof OPENAI_WS_MODE_OFF
| typeof OPENAI_WS_MODE_CTX_POOL
| typeof OPENAI_WS_MODE_PASSTHROUGH
| typeof OPENAI_WS_MODE_HTTP_BRIDGE
const OPENAI_WS_MODES = new Set<OpenAIWSMode>([
OPENAI_WS_MODE_OFF,
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_PASSTHROUGH
OPENAI_WS_MODE_PASSTHROUGH,
OPENAI_WS_MODE_HTTP_BRIDGE
])
export interface ResolveOpenAIWSModeOptions {
@@ -44,7 +47,7 @@ export const isOpenAIWSModeEnabled = (mode: OpenAIWSMode): boolean => {
export const resolveOpenAIWSModeConcurrencyHintKey = (
mode: OpenAIWSMode
): 'admin.accounts.openai.wsModeConcurrencyHint' | 'admin.accounts.openai.wsModePassthroughHint' => {
if (mode === OPENAI_WS_MODE_PASSTHROUGH) {
if (mode === OPENAI_WS_MODE_PASSTHROUGH || mode === OPENAI_WS_MODE_HTTP_BRIDGE) {
return 'admin.accounts.openai.wsModePassthroughHint'
}
return 'admin.accounts.openai.wsModeConcurrencyHint'