feat: add Codex image tool strip policy

This commit is contained in:
Anderson
2026-07-03 22:36:39 +08:00
parent 28c2d20d9f
commit f385cdceb7
10 changed files with 296 additions and 15 deletions
@@ -1430,6 +1430,58 @@
</div>
</div>
<!-- OpenAI Codex 显式 image_generation tool 策略 -->
<div
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-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800">
<div class="flex items-start gap-3 px-4 py-3">
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-gray-50 text-slate-600 ring-1 ring-gray-200 dark:bg-dark-700 dark:text-slate-300 dark:ring-dark-600">
<Icon name="filter" size="sm" />
</div>
<div class="min-w-0 flex-1">
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexImageToolPolicy') }}</label>
<p class="mt-1 text-xs leading-5 text-slate-600 dark:text-slate-300">
{{ t('admin.accounts.openai.codexImageToolPolicyDesc') }}
</p>
</div>
</div>
<div class="border-t border-gray-200 bg-gray-50/70 p-2 dark:border-dark-600 dark:bg-dark-700/50">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<button
v-for="option in codexImageToolPolicyOptions"
:key="option.value"
type="button"
:data-testid="`codex-image-tool-policy-${option.value}`"
@click="codexImageToolPolicyMode = option.value"
:class="[
'group flex min-h-[62px] items-start gap-2 rounded-md border px-3 py-2 text-left transition-all',
codexImageToolPolicyMode === option.value
? 'border-slate-300 bg-white text-slate-900 shadow-sm ring-1 ring-slate-200 dark:border-slate-600 dark:bg-dark-800 dark:text-slate-100 dark:ring-dark-500'
: 'border-transparent bg-transparent text-slate-600 hover:border-gray-200 hover:bg-white dark:text-slate-300 dark:hover:border-dark-500 dark:hover:bg-dark-800'
]"
>
<span
:class="[
'mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition-colors',
codexImageToolPolicyMode === option.value
? 'border-slate-600 bg-slate-700 text-white dark:border-slate-400 dark:bg-slate-500'
: 'border-gray-300 text-transparent group-hover:border-gray-400 dark:border-dark-500'
]"
>
<Icon name="check" size="xs" :stroke-width="2" />
</span>
<span class="min-w-0">
<span class="block text-sm font-medium">{{ option.label }}</span>
<span class="mt-0.5 block text-xs leading-4 text-slate-500 dark:text-slate-400">{{ option.description }}</span>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- OpenAI WS Mode 三态(off/ctx_pool/passthrough) -->
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
@@ -2629,6 +2681,8 @@ const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled'
const codexImageGenerationBridgeMode = ref<CodexImageGenerationBridgeMode>('inherit')
type CodexImageToolPolicyMode = 'allow' | 'strip'
const codexImageToolPolicyMode = ref<CodexImageToolPolicyMode>('allow')
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
const anthropicPassthroughEnabled = ref(false)
const anthropicAPIKeyAuthScheme = ref<AnthropicAPIKeyAuthScheme>('x_api_key')
@@ -2703,6 +2757,22 @@ const codexImageGenerationBridgeOptions = computed<Array<{
description: t('admin.accounts.openai.codexImageGenerationBridgeDisabledDesc')
}
])
const codexImageToolPolicyOptions = computed<Array<{
value: CodexImageToolPolicyMode
label: string
description: string
}>>(() => [
{
value: 'allow',
label: t('admin.accounts.openai.codexImageToolPolicyAllow'),
description: t('admin.accounts.openai.codexImageToolPolicyAllowDesc')
},
{
value: 'strip',
label: t('admin.accounts.openai.codexImageToolPolicyStrip'),
description: t('admin.accounts.openai.codexImageToolPolicyStripDesc')
}
])
const codexImageGenerationBridgeBadgeLabel = computed(() => {
switch (codexImageGenerationBridgeMode.value) {
case 'enabled':
@@ -3036,6 +3106,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
codexImageGenerationBridgeMode.value = 'inherit'
codexImageToolPolicyMode.value = 'allow'
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
webSearchEmulationMode.value = 'default'
@@ -3059,6 +3130,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
} else if (codexImageGenerationBridgeValue === false) {
codexImageGenerationBridgeMode.value = 'disabled'
}
codexImageToolPolicyMode.value = extra?.codex_image_generation_explicit_tool_policy === 'strip'
? 'strip'
: 'allow'
openaiOAuthResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, {
modeKey: 'openai_oauth_responses_websockets_v2_mode',
enabledKey: 'openai_oauth_responses_websockets_v2_enabled',
@@ -4199,6 +4273,11 @@ const handleSubmit = async () => {
} else {
newExtra.codex_image_generation_bridge = codexImageGenerationBridgeMode.value === 'enabled'
}
if (codexImageToolPolicyMode.value === 'strip') {
newExtra.codex_image_generation_explicit_tool_policy = 'strip'
} else {
delete newExtra.codex_image_generation_explicit_tool_policy
}
if (props.account.type === 'oauth' || props.account.type === 'setup-token') {
if (codexCLIOnlyEnabled.value) {
@@ -577,6 +577,41 @@ describe('EditAccountModal', () => {
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('codex_image_generation_bridge_enabled')
})
it('submits account-level Codex image_generation tool strip policy', async () => {
const account = buildAccount()
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
await wrapper.get('button[data-testid="codex-image-tool-policy-strip"]').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_explicit_tool_policy).toBe('strip')
})
it('keeps account-level Codex image_generation tool policy unset when allowing', async () => {
const account = buildAccount()
account.extra = {
codex_image_generation_explicit_tool_policy: 'strip'
}
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
await wrapper.get('button[data-testid="codex-image-tool-policy-allow"]').trigger('click')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('codex_image_generation_explicit_tool_policy')
})
it('setup-token account can select and submit OAuth WS mode', async () => {
const account = buildOpenAISetupTokenAccount()
updateAccountMock.mockReset()
+7
View File
@@ -3569,6 +3569,13 @@ export default {
codexImageGenerationBridgeBadgeInherit: 'Channel policy',
codexImageGenerationBridgeBadgeEnabled: 'Account on',
codexImageGenerationBridgeBadgeDisabled: 'Account off',
codexImageToolPolicy: 'Codex image tool forwarding',
codexImageToolPolicyDesc:
'Only applies to client-provided image_generation tools in Codex /responses text requests; standalone image-generation endpoints are unaffected.',
codexImageToolPolicyAllow: 'Allow',
codexImageToolPolicyAllowDesc: 'Default behavior. Keep client-provided image_generation tools.',
codexImageToolPolicyStrip: 'Remove',
codexImageToolPolicyStripDesc: 'Delete image_generation tools and matching tool_choice before forwarding.',
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.',
+6
View File
@@ -3742,6 +3742,12 @@ export default {
codexImageGenerationBridgeBadgeInherit: '渠道策略',
codexImageGenerationBridgeBadgeEnabled: '账号开启',
codexImageGenerationBridgeBadgeDisabled: '账号关闭',
codexImageToolPolicy: 'Codex 图片工具传递策略',
codexImageToolPolicyDesc: '仅作用于 Codex /responses 文本端点中客户端显式携带的 image_generation tool;不影响独立图片生成接口。',
codexImageToolPolicyAllow: '放行',
codexImageToolPolicyAllowDesc: '默认行为,保留客户端显式发送的 image_generation tool。',
codexImageToolPolicyStrip: '移除',
codexImageToolPolicyStripDesc: '转发前删除 image_generation tool 和指向它的 tool_choice。',
compactMode: 'Compact 模式',
compactModeDesc:
'控制本账号在 /responses/compact 调度中的参与方式。Auto 跟随探测结果,Force On 强制允许,Force Off 强制排除。',