From 7a9c1d7edde466db4aae24d934633d38afb84d2c Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 7 May 2026 11:07:33 +0800 Subject: [PATCH] feat(frontend): add account Codex image bridge control --- .../components/account/EditAccountModal.vue | 121 +++++++++++++++++- .../__tests__/EditAccountModal.spec.ts | 21 +++ frontend/src/i18n/locales/en.ts | 15 ++- frontend/src/i18n/locales/zh.ts | 15 ++- frontend/src/views/admin/AccountsView.vue | 86 ++++++++----- 5 files changed, 220 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 56874474..80f0b890 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1317,6 +1317,66 @@ + +
+
+
+
+ +
+
+
+ + + {{ codexImageGenerationBridgeBadgeLabel }} + +
+

+ {{ t('admin.accounts.openai.codexImageGenerationBridgeDesc') }} +

+
+
+
+
+ +
+
+
+
+
('auto') const openaiOAuthResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF) const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OFF) const codexCLIOnlyEnabled = ref(false) +type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled' +const codexImageGenerationBridgeMode = ref('inherit') const anthropicPassthroughEnabled = ref(false) const webSearchEmulationMode = ref('default') const webSearchGlobalEnabled = ref(false) @@ -2325,6 +2387,47 @@ const openaiResponsesWebSocketV2Mode = computed({ const openAIWSModeConcurrencyHintKey = computed(() => resolveOpenAIWSModeConcurrencyHintKey(openaiResponsesWebSocketV2Mode.value) ) +const codexImageGenerationBridgeOptions = computed>(() => [ + { + value: 'inherit', + label: t('admin.accounts.openai.codexImageGenerationBridgeInherit'), + description: t('admin.accounts.openai.codexImageGenerationBridgeInheritDesc') + }, + { + value: 'enabled', + label: t('admin.accounts.openai.codexImageGenerationBridgeEnabled'), + description: t('admin.accounts.openai.codexImageGenerationBridgeEnabledDesc') + }, + { + value: 'disabled', + label: t('admin.accounts.openai.codexImageGenerationBridgeDisabled'), + description: t('admin.accounts.openai.codexImageGenerationBridgeDisabledDesc') + } +]) +const codexImageGenerationBridgeBadgeLabel = computed(() => { + switch (codexImageGenerationBridgeMode.value) { + case 'enabled': + return t('admin.accounts.openai.codexImageGenerationBridgeBadgeEnabled') + case 'disabled': + return t('admin.accounts.openai.codexImageGenerationBridgeBadgeDisabled') + default: + return t('admin.accounts.openai.codexImageGenerationBridgeBadgeInherit') + } +}) +const codexImageGenerationBridgeBadgeClass = computed(() => { + switch (codexImageGenerationBridgeMode.value) { + case 'enabled': + return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' + case 'disabled': + return 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300' + default: + return 'bg-slate-100 text-slate-600 dark:bg-dark-600 dark:text-slate-300' + } +}) const openAICompactModeOptions = computed(() => [ { value: 'auto', label: t('admin.accounts.openai.compactModeAuto') }, { value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') }, @@ -2344,7 +2447,7 @@ const openAICompactStatusKey = computed(() => { ? 'admin.accounts.openai.compactSupported' : 'admin.accounts.openai.compactUnsupported' } - return 'admin.accounts.openai.compactUnknown' + return 'admin.accounts.openai.compactAuto' }) // Computed: current preset mappings based on platform @@ -2483,11 +2586,20 @@ const syncFormFromAccount = (newAccount: Account | null) => { openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF codexCLIOnlyEnabled.value = false + codexImageGenerationBridgeMode.value = 'inherit' anthropicPassthroughEnabled.value = false webSearchEmulationMode.value = 'default' if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) { openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto' + const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean' + ? extra.codex_image_generation_bridge + : extra?.codex_image_generation_bridge_enabled + if (codexImageGenerationBridgeValue === true) { + codexImageGenerationBridgeMode.value = 'enabled' + } else if (codexImageGenerationBridgeValue === false) { + codexImageGenerationBridgeMode.value = 'disabled' + } openaiOAuthResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, { modeKey: 'openai_oauth_responses_websockets_v2_mode', enabledKey: 'openai_oauth_responses_websockets_v2_enabled', @@ -3610,6 +3722,13 @@ const handleSubmit = async () => { newExtra.openai_compact_mode = openAICompactMode.value } + delete newExtra.codex_image_generation_bridge_enabled + if (codexImageGenerationBridgeMode.value === 'inherit') { + delete newExtra.codex_image_generation_bridge + } else { + newExtra.codex_image_generation_bridge = codexImageGenerationBridgeMode.value === 'enabled' + } + if (props.account.type === 'oauth') { if (codexCLIOnlyEnabled.value) { newExtra.codex_cli_only = true diff --git a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts index c4e2a9bc..04486154 100644 --- a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts @@ -216,4 +216,25 @@ describe('EditAccountModal', () => { 'gpt-5.4': 'gpt-5.4-openai-compact' }) }) + + it('submits account-level Codex image generation bridge override', async () => { + const account = buildAccount() + account.extra = { + codex_image_generation_bridge: false, + codex_image_generation_bridge_enabled: true + } + updateAccountMock.mockReset() + checkMixedChannelRiskMock.mockReset() + checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account) + + await wrapper.get('button[data-testid="codex-image-bridge-enabled"]').trigger('click') + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + + expect(updateAccountMock).toHaveBeenCalledTimes(1) + expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.codex_image_generation_bridge).toBe(true) + expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('codex_image_generation_bridge_enabled') + }) }) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index ca00f622..743f9415 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3136,6 +3136,18 @@ export default { codexCLIOnly: 'Codex official clients only', codexCLIOnlyDesc: 'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.', + codexImageGenerationBridge: 'Codex image-generation bridge', + codexImageGenerationBridgeDesc: + 'Account policy takes precedence over channel and global settings. Only controls whether Codex requests through the /responses text endpoint receive the image_generation tool; standalone image-generation endpoints are unaffected.', + codexImageGenerationBridgeInherit: 'Follow channel', + codexImageGenerationBridgeInheritDesc: 'Do not write an account override; use the channel or global policy.', + codexImageGenerationBridgeEnabled: 'Force on', + codexImageGenerationBridgeEnabledDesc: 'Allow image tool injection for Codex /responses requests.', + codexImageGenerationBridgeDisabled: 'Force off', + codexImageGenerationBridgeDisabledDesc: 'Block image tool injection for Codex /responses requests.', + codexImageGenerationBridgeBadgeInherit: 'Channel policy', + codexImageGenerationBridgeBadgeEnabled: 'Account on', + codexImageGenerationBridgeBadgeDisabled: 'Account off', compactMode: 'Compact mode', compactModeDesc: 'Controls how this account participates in /responses/compact routing. Auto follows probe results, Force On always allows, Force Off always excludes.', @@ -3147,7 +3159,8 @@ export default { 'Only applies to /responses/compact. Use this when the upstream compact endpoint requires a special compact model.', compactSupported: 'Compact supported', compactUnsupported: 'Compact unsupported', - compactUnknown: 'Compact unknown', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', compactLastChecked: 'Last compact probe', testMode: 'Test mode', testModeDefault: 'Default request', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 8b4f32f8..246f9832 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3281,6 +3281,18 @@ export default { responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。', codexCLIOnly: '仅允许 Codex 官方客户端', codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。', + codexImageGenerationBridge: 'Codex 图片生成桥接', + codexImageGenerationBridgeDesc: + '账号级策略优先于渠道和全局配置。仅控制 Codex 走 /responses 文本端点时是否注入 image_generation 工具;不影响独立图片生成接口。', + codexImageGenerationBridgeInherit: '跟随渠道', + codexImageGenerationBridgeInheritDesc: '不写入账号覆盖,继续使用渠道或全局策略。', + codexImageGenerationBridgeEnabled: '强制开启', + codexImageGenerationBridgeEnabledDesc: '允许 Codex /responses 请求获得图片工具注入。', + codexImageGenerationBridgeDisabled: '强制关闭', + codexImageGenerationBridgeDisabledDesc: '阻断 Codex /responses 的图片工具注入。', + codexImageGenerationBridgeBadgeInherit: '渠道策略', + codexImageGenerationBridgeBadgeEnabled: '账号开启', + codexImageGenerationBridgeBadgeDisabled: '账号关闭', compactMode: 'Compact 模式', compactModeDesc: '控制本账号在 /responses/compact 调度中的参与方式。Auto 跟随探测结果,Force On 强制允许,Force Off 强制排除。', @@ -3292,7 +3304,8 @@ export default { '仅在 /responses/compact 请求中生效。当上游 compact 端点需要特殊 compact 模型时使用。', compactSupported: '支持 Compact', compactUnsupported: '不支持 Compact', - compactUnknown: 'Compact 未知', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', compactLastChecked: '最近探测', testMode: '测试模式', testModeDefault: '常规请求', diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 126e4a61..04c376cc 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -196,21 +196,27 @@ -