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 @@
-
-
-
-
+
+
+
+ {{ getAntigravityTierLabel(row) }}
+
+
+
- {{ getOpenAICompactLabel(row) }}
-
-
- {{ getAntigravityTierLabel(row) }}
-
+
+ {{ getOpenAICompactMeta(row)?.label }}
+
@@ -983,41 +989,51 @@ function getAntigravityTierLabel(row: any): string | null {
}
}
-function getOpenAICompactState(row: any): 'supported' | 'unsupported' | 'unknown' | null {
+type OpenAICompactBadgeState = 'active' | 'blocked' | 'auto'
+
+function getOpenAICompactState(row: any): OpenAICompactBadgeState | null {
if (row.platform !== 'openai' || (row.type !== 'oauth' && row.type !== 'apikey')) return null
const extra = row.extra as Record | undefined
const mode = typeof extra?.openai_compact_mode === 'string' ? extra.openai_compact_mode : 'auto'
- if (mode === 'force_on') return 'supported'
- if (mode === 'force_off') return 'unsupported'
+ if (mode === 'force_on') return 'active'
+ if (mode === 'force_off') return 'blocked'
if (typeof extra?.openai_compact_supported === 'boolean') {
- return extra.openai_compact_supported ? 'supported' : 'unsupported'
+ return extra.openai_compact_supported ? 'active' : 'blocked'
}
- return 'unknown'
+ return 'auto'
}
-function getOpenAICompactLabel(row: any): string | null {
- switch (getOpenAICompactState(row)) {
- case 'supported': return t('admin.accounts.openai.compactSupported')
- case 'unsupported': return t('admin.accounts.openai.compactUnsupported')
- case 'unknown': return t('admin.accounts.openai.compactUnknown')
- default: return null
- }
-}
-
-function getOpenAICompactClass(row: any): string {
- switch (getOpenAICompactState(row)) {
- case 'supported': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'
- case 'unsupported': return 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300'
- case 'unknown': return 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
- default: return ''
+function getOpenAICompactMeta(row: any): { label: string; className: string; dotClass: string } | null {
+ const state = getOpenAICompactState(row)
+ if (!state) return null
+ switch (state) {
+ case 'active':
+ return {
+ label: t('admin.accounts.openai.compactSupported'),
+ className: 'text-emerald-600 dark:text-emerald-300',
+ dotClass: 'bg-emerald-500 shadow-[0_0_0_2px_rgba(16,185,129,0.14)]'
+ }
+ case 'blocked':
+ return {
+ label: t('admin.accounts.openai.compactUnsupported'),
+ className: 'text-rose-600 dark:text-rose-300',
+ dotClass: 'bg-rose-500 shadow-[0_0_0_2px_rgba(244,63,94,0.14)]'
+ }
+ case 'auto':
+ return {
+ label: t('admin.accounts.openai.compactAuto'),
+ className: 'text-slate-500 dark:text-slate-400',
+ dotClass: 'bg-slate-300 dark:bg-slate-500'
+ }
}
}
function getOpenAICompactTitle(row: any): string {
const extra = row.extra as Record | undefined
const checkedAt = typeof extra?.openai_compact_checked_at === 'string' ? extra.openai_compact_checked_at : ''
- if (!checkedAt) return getOpenAICompactLabel(row) || ''
- return `${getOpenAICompactLabel(row)} | ${t('admin.accounts.openai.compactLastChecked')}: ${formatDateTime(new Date(checkedAt))}`
+ const label = getOpenAICompactMeta(row)?.label || ''
+ if (!checkedAt) return label
+ return `${label} | ${t('admin.accounts.openai.compactLastChecked')}: ${formatDateTime(new Date(checkedAt))}`
}
function getAntigravityTierClass(row: any): string {