mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat: add Codex image tool strip policy
This commit is contained in:
@@ -4,6 +4,13 @@ import "strings"
|
||||
|
||||
const featureKeyCodexImageGenerationBridge = "codex_image_generation_bridge"
|
||||
|
||||
const (
|
||||
featureKeyCodexImageGenerationExplicitToolPolicy = "codex_image_generation_explicit_tool_policy"
|
||||
|
||||
codexImageGenerationExplicitToolPolicyAllow = "allow"
|
||||
codexImageGenerationExplicitToolPolicyStrip = "strip"
|
||||
)
|
||||
|
||||
func boolOverridePtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
@@ -20,6 +27,27 @@ func boolOverrideFromMap(values map[string]any, keys ...string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringOverrideFromMap(values map[string]any, keys ...string) (string, bool) {
|
||||
if values == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, key := range keys {
|
||||
if v, ok := values[key].(string); ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func normalizeCodexImageGenerationExplicitToolPolicy(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case codexImageGenerationExplicitToolPolicyStrip, "remove", "drop":
|
||||
return codexImageGenerationExplicitToolPolicyStrip
|
||||
default:
|
||||
return codexImageGenerationExplicitToolPolicyAllow
|
||||
}
|
||||
}
|
||||
|
||||
func platformBoolOverride(values map[string]any, key string, platform string) *bool {
|
||||
if values == nil {
|
||||
return nil
|
||||
@@ -62,3 +90,20 @@ func (a *Account) CodexImageGenerationBridgeOverride() *bool {
|
||||
openaiConfig, _ := a.Extra[PlatformOpenAI].(map[string]any)
|
||||
return boolOverrideFromMap(openaiConfig, featureKeyCodexImageGenerationBridge, "codex_image_generation_bridge_enabled")
|
||||
}
|
||||
|
||||
// CodexImageGenerationExplicitToolPolicy returns the account-level policy for
|
||||
// client-provided Codex /responses image_generation tools. Unknown or unset
|
||||
// values default to allow to preserve existing behavior.
|
||||
func (a *Account) CodexImageGenerationExplicitToolPolicy() string {
|
||||
if a == nil || a.Platform != PlatformOpenAI || a.Extra == nil {
|
||||
return codexImageGenerationExplicitToolPolicyAllow
|
||||
}
|
||||
if policy, ok := stringOverrideFromMap(a.Extra, featureKeyCodexImageGenerationExplicitToolPolicy); ok {
|
||||
return normalizeCodexImageGenerationExplicitToolPolicy(policy)
|
||||
}
|
||||
openaiConfig, _ := a.Extra[PlatformOpenAI].(map[string]any)
|
||||
if policy, ok := stringOverrideFromMap(openaiConfig, featureKeyCodexImageGenerationExplicitToolPolicy); ok {
|
||||
return normalizeCodexImageGenerationExplicitToolPolicy(policy)
|
||||
}
|
||||
return codexImageGenerationExplicitToolPolicyAllow
|
||||
}
|
||||
|
||||
@@ -607,18 +607,21 @@ func hasOpenAIImageGenerationTool(reqBody map[string]any) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// stripCodexSparkImageGenerationTools removes image_generation tool entries from
|
||||
// reqBody["tools"]. gpt-5.3-codex-spark rejects that tool upstream with HTTP 400
|
||||
// (invalid_request_error, param=tools), and Codex CLI advertises it by default, so
|
||||
// it must be dropped for spark. When the tools list becomes empty the key is removed.
|
||||
// Returns true when the body was modified.
|
||||
func stripCodexSparkImageGenerationTools(reqBody map[string]any) bool {
|
||||
func stripOpenAIImageGenerationTools(reqBody map[string]any) bool {
|
||||
rawTools, ok := reqBody["tools"]
|
||||
if !ok || rawTools == nil {
|
||||
if openAIAnyToolChoiceSelectsImageGeneration(reqBody["tool_choice"]) {
|
||||
delete(reqBody, "tool_choice")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
tools, ok := rawTools.([]any)
|
||||
if !ok {
|
||||
if openAIAnyToolChoiceSelectsImageGeneration(reqBody["tool_choice"]) {
|
||||
delete(reqBody, "tool_choice")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
filtered := make([]any, 0, len(tools))
|
||||
@@ -631,17 +634,31 @@ func stripCodexSparkImageGenerationTools(reqBody map[string]any) bool {
|
||||
}
|
||||
filtered = append(filtered, rawTool)
|
||||
}
|
||||
if !removed {
|
||||
if !removed && !openAIAnyToolChoiceSelectsImageGeneration(reqBody["tool_choice"]) {
|
||||
return false
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
delete(reqBody, "tools")
|
||||
} else {
|
||||
reqBody["tools"] = filtered
|
||||
if removed {
|
||||
if len(filtered) == 0 {
|
||||
delete(reqBody, "tools")
|
||||
} else {
|
||||
reqBody["tools"] = filtered
|
||||
}
|
||||
}
|
||||
if openAIAnyToolChoiceSelectsImageGeneration(reqBody["tool_choice"]) {
|
||||
delete(reqBody, "tool_choice")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// stripCodexSparkImageGenerationTools removes image_generation tool entries from
|
||||
// reqBody["tools"]. gpt-5.3-codex-spark rejects that tool upstream with HTTP 400
|
||||
// (invalid_request_error, param=tools), and Codex CLI advertises it by default, so
|
||||
// it must be dropped for spark. When the tools list becomes empty the key is removed.
|
||||
// Returns true when the body was modified.
|
||||
func stripCodexSparkImageGenerationTools(reqBody map[string]any) bool {
|
||||
return stripOpenAIImageGenerationTools(reqBody)
|
||||
}
|
||||
|
||||
func hasOpenAIInputImage(reqBody map[string]any) bool {
|
||||
if reqBody == nil {
|
||||
return false
|
||||
|
||||
@@ -2738,8 +2738,25 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
if apiKey != nil {
|
||||
imageGenerationAllowed = GroupAllowsImageGeneration(apiKey.Group)
|
||||
}
|
||||
codexImageGenerationBridgeEnabled := isCodexCLI && imageGenerationAllowed && s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
|
||||
imageIntent := IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body)
|
||||
codexImageGenerationExplicitToolPolicy := codexImageGenerationExplicitToolPolicyAllow
|
||||
if isCodexCLI {
|
||||
codexImageGenerationExplicitToolPolicy = account.CodexImageGenerationExplicitToolPolicy()
|
||||
}
|
||||
codexImageGenerationBridgeEnabled := isCodexCLI && imageGenerationAllowed && codexImageGenerationExplicitToolPolicy != codexImageGenerationExplicitToolPolicyStrip && s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
|
||||
var imageIntent bool
|
||||
if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
|
||||
decoded, decodeErr := ensureReqBody()
|
||||
if decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
if stripOpenAIImageGenerationTools(decoded) {
|
||||
markDecodedModified()
|
||||
logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Stripped /responses image_generation tool for Codex client by account policy")
|
||||
}
|
||||
imageIntent = IsImageGenerationIntentMap(openAIResponsesEndpoint, reqModel, decoded)
|
||||
} else {
|
||||
imageIntent = IsImageGenerationIntent(openAIResponsesEndpoint, reqModel, body)
|
||||
}
|
||||
if imageIntent && !imageGenerationAllowed {
|
||||
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": gin.H{"type": "permission_error", "message": ImageGenerationPermissionMessage()}})
|
||||
|
||||
@@ -152,6 +152,45 @@ func TestOpenAIGatewayServiceForward_ExplicitImageToolWorksWithBridgeDisabled(t
|
||||
require.NotContains(t, instructions, "image_generation")
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceForward_AccountPolicyStripsExplicitImageTool(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
upstream := &httpUpstreamRecorder{
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"id":"resp_stripped_image","model":"gpt-5.4","usage":{"input_tokens":2,"output_tokens":1}}`)),
|
||||
},
|
||||
}
|
||||
svc := newOpenAIImageGenerationControlTestService(upstream)
|
||||
c, _ := newOpenAIImageGenerationControlTestContext(true, "codex_cli_rs/0.98.0")
|
||||
account := newOpenAIImageGenerationControlTestAccount()
|
||||
account.Extra = map[string]any{
|
||||
featureKeyCodexImageGenerationExplicitToolPolicy: codexImageGenerationExplicitToolPolicyStrip,
|
||||
}
|
||||
body := []byte(`{
|
||||
"model":"gpt-5.4",
|
||||
"input":"draw",
|
||||
"stream":false,
|
||||
"tools":[
|
||||
{"type":"function","name":"shell","parameters":{"type":"object"}},
|
||||
{"type":"image_generation","format":"jpeg"}
|
||||
],
|
||||
"tool_choice":{"type":"image_generation"}
|
||||
}`)
|
||||
|
||||
result, err := svc.Forward(context.Background(), c, account, body)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, upstream.lastReq)
|
||||
require.False(t, gjson.GetBytes(upstream.lastBody, `tools.#(type=="image_generation")`).Exists())
|
||||
require.True(t, gjson.GetBytes(upstream.lastBody, `tools.#(type=="function")`).Exists())
|
||||
require.False(t, gjson.GetBytes(upstream.lastBody, "tool_choice").Exists())
|
||||
instructions := gjson.GetBytes(upstream.lastBody, "instructions").String()
|
||||
require.NotContains(t, instructions, "image_generation")
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceForward_ChannelBridgeOverrideEnablesCodexInjection(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
@@ -2448,11 +2448,15 @@ func stripCodexSparkImageGenerationToolFromRawPayload(payload []byte, model stri
|
||||
if !isCodexSparkModel(model) || !openAIRequestBodyHasImageGenerationTool(payload) {
|
||||
return payload, false, nil
|
||||
}
|
||||
return stripOpenAIImageGenerationToolFromRawPayload(payload)
|
||||
}
|
||||
|
||||
func stripOpenAIImageGenerationToolFromRawPayload(payload []byte) ([]byte, bool, error) {
|
||||
payloadMap := make(map[string]any)
|
||||
if err := json.Unmarshal(payload, &payloadMap); err != nil {
|
||||
return payload, false, err
|
||||
}
|
||||
if !stripCodexSparkImageGenerationTools(payloadMap) {
|
||||
if !stripOpenAIImageGenerationTools(payloadMap) {
|
||||
return payload, false, nil
|
||||
}
|
||||
rebuilt, err := json.Marshal(payloadMap)
|
||||
@@ -2671,7 +2675,11 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
|
||||
}
|
||||
apiKey := getAPIKeyFromContext(c)
|
||||
imageGenerationAllowed := GroupAllowsImageGeneration(apiKeyGroup(apiKey))
|
||||
codexBridgeEnabled := isCodexCLI && imageGenerationAllowed && s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
|
||||
codexImageGenerationExplicitToolPolicy := codexImageGenerationExplicitToolPolicyAllow
|
||||
if isCodexCLI {
|
||||
codexImageGenerationExplicitToolPolicy = account.CodexImageGenerationExplicitToolPolicy()
|
||||
}
|
||||
codexBridgeEnabled := isCodexCLI && imageGenerationAllowed && codexImageGenerationExplicitToolPolicy != codexImageGenerationExplicitToolPolicyStrip && s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
|
||||
if codexBridgeEnabled {
|
||||
payloadMap := make(map[string]any)
|
||||
if err := json.Unmarshal(normalized, &payloadMap); err != nil {
|
||||
@@ -2709,6 +2717,14 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
|
||||
}
|
||||
normalized = next
|
||||
}
|
||||
if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
|
||||
if stripped, changed, stripErr := stripOpenAIImageGenerationToolFromRawPayload(normalized); stripErr != nil {
|
||||
return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
|
||||
} else if changed {
|
||||
normalized = stripped
|
||||
logOpenAIWSModeInfo("ingress_ws_codex_image_tool_stripped_by_policy account_id=%d", account.ID)
|
||||
}
|
||||
}
|
||||
if stripped, changed, stripErr := stripCodexSparkImageGenerationToolFromRawPayload(normalized, upstreamModel); stripErr != nil {
|
||||
return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
|
||||
} else if changed {
|
||||
|
||||
@@ -169,6 +169,26 @@ func TestStripCodexSparkImageGenerationToolFromRawPayload(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStripOpenAIImageGenerationToolFromRawPayload(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"type":"response.create",
|
||||
"model":"gpt-5.4",
|
||||
"tools":[
|
||||
{"type":"function","name":"shell"},
|
||||
{"type":"image_generation","output_format":"png"}
|
||||
],
|
||||
"tool_choice":{"type":"image_generation"}
|
||||
}`)
|
||||
|
||||
updated, changed, err := stripOpenAIImageGenerationToolFromRawPayload(payload)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, changed)
|
||||
require.False(t, gjson.GetBytes(updated, `tools.#(type=="image_generation")`).Exists())
|
||||
require.True(t, gjson.GetBytes(updated, `tools.#(type=="function")`).Exists())
|
||||
require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
|
||||
}
|
||||
|
||||
func TestAlignStoreDisabledPreviousResponseID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 强制排除。',
|
||||
|
||||
Reference in New Issue
Block a user