Merge pull request #2450 from wucm667/codex/issue-2431-responses-api-support

feat: 支持后台配置 OpenAI Responses API 路由
This commit is contained in:
Wesley Liddick 2026-05-19 14:47:10 +08:00 committed by GitHub
commit e365aae450
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 234 additions and 10 deletions

View File

@ -294,7 +294,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
// resolveRawCCUpstreamEndpoint returns the actual upstream endpoint for // resolveRawCCUpstreamEndpoint returns the actual upstream endpoint for
// OpenAI Chat Completions requests. For APIKey accounts whose upstream // OpenAI Chat Completions requests. For APIKey accounts whose upstream
// has been probed to not support the Responses API, the request is // is forced or probed to not support the Responses API, the request is
// forwarded directly to /v1/chat/completions — not through the default // forwarded directly to /v1/chat/completions — not through the default
// CC→Responses conversion path. // CC→Responses conversion path.
func resolveRawCCUpstreamEndpoint(c *gin.Context, account *service.Account) string { func resolveRawCCUpstreamEndpoint(c *gin.Context, account *service.Account) string {

View File

@ -17,7 +17,7 @@
// pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems // pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems
package openai_compat package openai_compat
// AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的支持状态。 // AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的有效支持状态。
// //
// 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。 // 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。
type AccountResponsesSupport int type AccountResponsesSupport int
@ -35,11 +35,43 @@ const (
ResponsesSupportNo ResponsesSupportNo
) )
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储探测结果的键名。 // ResponsesSupportMode 描述账号级 Responses API 路由覆盖模式。
type ResponsesSupportMode string
const (
// ResponsesSupportModeAuto 表示跟随自动探测结果。
ResponsesSupportModeAuto ResponsesSupportMode = "auto"
// ResponsesSupportModeForceResponses 强制使用 /v1/responses。
ResponsesSupportModeForceResponses ResponsesSupportMode = "force_responses"
// ResponsesSupportModeForceChatCompletions 强制使用 /v1/chat/completions。
ResponsesSupportModeForceChatCompletions ResponsesSupportMode = "force_chat_completions"
)
// ExtraKeyResponsesMode 是 accounts.extra JSON 中存储手动覆盖模式的键名。
// 值类型为 stringauto=跟随探测force_responses=强制 Responses
// force_chat_completions=强制 Chat Completions。
const ExtraKeyResponsesMode = "openai_responses_mode"
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储自动探测结果的键名。
// 值类型为 booltrue=支持、false=不支持、键缺失=未探测。 // 值类型为 booltrue=支持、false=不支持、键缺失=未探测。
const ExtraKeyResponsesSupported = "openai_responses_supported" const ExtraKeyResponsesSupported = "openai_responses_supported"
// ResolveResponsesSupport 从账号的 extra map 中读取探测标记。 // NormalizeResponsesSupportMode 归一化账号级 Responses API 路由覆盖模式。
// 缺失或非法值按 auto 处理,以保持存量行为。
func NormalizeResponsesSupportMode(mode string) ResponsesSupportMode {
switch ResponsesSupportMode(mode) {
case ResponsesSupportModeForceResponses:
return ResponsesSupportModeForceResponses
case ResponsesSupportModeForceChatCompletions:
return ResponsesSupportModeForceChatCompletions
default:
return ResponsesSupportModeAuto
}
}
// ResolveResponsesSupport 从账号的 extra map 中读取手动覆盖模式与探测标记。
// //
// 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按 // 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
// "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI // "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI
@ -47,6 +79,14 @@ func ResolveResponsesSupport(extra map[string]any) AccountResponsesSupport {
if extra == nil { if extra == nil {
return ResponsesSupportUnknown return ResponsesSupportUnknown
} }
if mode, ok := extra[ExtraKeyResponsesMode].(string); ok {
switch NormalizeResponsesSupportMode(mode) {
case ResponsesSupportModeForceResponses:
return ResponsesSupportYes
case ResponsesSupportModeForceChatCompletions:
return ResponsesSupportNo
}
}
v, ok := extra[ExtraKeyResponsesSupported] v, ok := extra[ExtraKeyResponsesSupported]
if !ok { if !ok {
return ResponsesSupportUnknown return ResponsesSupportUnknown

View File

@ -16,6 +16,12 @@ func TestResolveResponsesSupport(t *testing.T) {
{"value wrong type string", map[string]any{ExtraKeyResponsesSupported: "true"}, ResponsesSupportUnknown}, {"value wrong type string", map[string]any{ExtraKeyResponsesSupported: "true"}, ResponsesSupportUnknown},
{"value wrong type number", map[string]any{ExtraKeyResponsesSupported: 1}, ResponsesSupportUnknown}, {"value wrong type number", map[string]any{ExtraKeyResponsesSupported: 1}, ResponsesSupportUnknown},
{"value nil", map[string]any{ExtraKeyResponsesSupported: nil}, ResponsesSupportUnknown}, {"value nil", map[string]any{ExtraKeyResponsesSupported: nil}, ResponsesSupportUnknown},
{"force responses", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses)}, ResponsesSupportYes},
{"force chat completions", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions)}, ResponsesSupportNo},
{"auto follows probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeAuto), ExtraKeyResponsesSupported: false}, ResponsesSupportNo},
{"invalid mode follows probe", map[string]any{ExtraKeyResponsesMode: "bogus", ExtraKeyResponsesSupported: true}, ResponsesSupportYes},
{"force responses overrides probe false", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, ResponsesSupportYes},
{"force chat completions overrides probe true", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, ResponsesSupportNo},
} }
for _, tc := range tests { for _, tc := range tests {
@ -42,6 +48,10 @@ func TestShouldUseResponsesAPI(t *testing.T) {
// 已探测:标记决定 // 已探测:标记决定
{"explicitly supported", map[string]any{ExtraKeyResponsesSupported: true}, true}, {"explicitly supported", map[string]any{ExtraKeyResponsesSupported: true}, true},
{"explicitly unsupported", map[string]any{ExtraKeyResponsesSupported: false}, false}, {"explicitly unsupported", map[string]any{ExtraKeyResponsesSupported: false}, false},
// 手动覆盖:覆盖自动探测结果
{"force responses overrides unsupported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, true},
{"force chat completions overrides supported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, false},
} }
for _, tc := range tests { for _, tc := range tests {
@ -53,3 +63,26 @@ func TestShouldUseResponsesAPI(t *testing.T) {
}) })
} }
} }
func TestNormalizeResponsesSupportMode(t *testing.T) {
tests := []struct {
name string
mode string
want ResponsesSupportMode
}{
{"empty", "", ResponsesSupportModeAuto},
{"auto", "auto", ResponsesSupportModeAuto},
{"force responses", "force_responses", ResponsesSupportModeForceResponses},
{"force chat completions", "force_chat_completions", ResponsesSupportModeForceChatCompletions},
{"invalid", "enabled", ResponsesSupportModeAuto},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeResponsesSupportMode(tc.mode)
if got != tc.want {
t.Errorf("NormalizeResponsesSupportMode(%q) = %q, want %q", tc.mode, got, tc.want)
}
})
}
}

View File

@ -12,3 +12,14 @@ func TestShouldEnqueueSchedulerOutboxForExtraUpdates_CompactCapabilityKeysAreRel
t.Fatalf("expected compact capability updates to enqueue scheduler outbox") t.Fatalf("expected compact capability updates to enqueue scheduler outbox")
} }
} }
func TestShouldEnqueueSchedulerOutboxForExtraUpdates_OpenAIResponsesCapabilityKeysAreRelevant(t *testing.T) {
updates := map[string]any{
"openai_responses_mode": "force_chat_completions",
"openai_responses_supported": false,
}
if !shouldEnqueueSchedulerOutboxForExtraUpdates(updates) {
t.Fatalf("expected responses capability updates to enqueue scheduler outbox")
}
}

View File

@ -546,6 +546,8 @@ func filterSchedulerExtra(extra map[string]any) map[string]any {
"responses_websockets_v2_enabled", "responses_websockets_v2_enabled",
"openai_ws_enabled", "openai_ws_enabled",
"openai_ws_force_http", "openai_ws_force_http",
"openai_responses_mode",
"openai_responses_supported",
} }
filtered := make(map[string]any) filtered := make(map[string]any)
for _, key := range keys { for _, key := range keys {

View File

@ -18,6 +18,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
"openai_oauth_responses_websockets_v2_enabled": true, "openai_oauth_responses_websockets_v2_enabled": true,
"openai_oauth_responses_websockets_v2_mode": service.OpenAIWSIngressModePassthrough, "openai_oauth_responses_websockets_v2_mode": service.OpenAIWSIngressModePassthrough,
"openai_ws_force_http": true, "openai_ws_force_http": true,
"openai_responses_mode": "force_chat_completions",
"openai_responses_supported": false,
"mixed_scheduling": true, "mixed_scheduling": true,
"unused_large_field": "drop-me", "unused_large_field": "drop-me",
}, },
@ -28,6 +30,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
require.Equal(t, true, got.Extra["openai_oauth_responses_websockets_v2_enabled"]) require.Equal(t, true, got.Extra["openai_oauth_responses_websockets_v2_enabled"])
require.Equal(t, service.OpenAIWSIngressModePassthrough, got.Extra["openai_oauth_responses_websockets_v2_mode"]) require.Equal(t, service.OpenAIWSIngressModePassthrough, got.Extra["openai_oauth_responses_websockets_v2_mode"])
require.Equal(t, true, got.Extra["openai_ws_force_http"]) require.Equal(t, true, got.Extra["openai_ws_force_http"])
require.Equal(t, "force_chat_completions", got.Extra["openai_responses_mode"])
require.Equal(t, false, got.Extra["openai_responses_supported"])
require.Equal(t, true, got.Extra["mixed_scheduling"]) require.Equal(t, true, got.Extra["mixed_scheduling"])
require.Nil(t, got.Extra["unused_large_field"]) require.Nil(t, got.Extra["unused_large_field"])
} }

View File

@ -48,10 +48,10 @@ var cursorResponsesUnsupportedFields = []string{
// 正确的,但 sub2api 接入 DeepSeek/Kimi/GLM 等第三方 OpenAI 兼容上游后假设破裂: // 正确的,但 sub2api 接入 DeepSeek/Kimi/GLM 等第三方 OpenAI 兼容上游后假设破裂:
// 这些上游普遍只支持 /v1/chat/completions无 /v1/responses 端点。 // 这些上游普遍只支持 /v1/chat/completions无 /v1/responses 端点。
// //
// 当前路由策略(基于账号探测标记,详见 openai_compat.ShouldUseResponsesAPI // 当前路由策略(基于账号覆盖模式/探测标记,详见 openai_compat.ShouldUseResponsesAPI
// - APIKey 账号 + 探测确认不支持 Responses → 走 forwardAsRawChatCompletions // - APIKey 账号 + 强制或探测确认不支持 Responses → 走 forwardAsRawChatCompletions
// 直转上游 /v1/chat/completions不做协议转换 // 直转上游 /v1/chat/completions不做协议转换
// - 其他所有情况OAuth、APIKey 探测确认支持、未探测)→ 走原有 CC→Responses // - 其他所有情况OAuth、APIKey 强制/探测确认支持、未探测)→ 走原有 CC→Responses
// 转换路径(保留旧行为,存量未探测账号零兼容破坏) // 转换路径(保留旧行为,存量未探测账号零兼容破坏)
func (s *OpenAIGatewayService) ForwardAsChatCompletions( func (s *OpenAIGatewayService) ForwardAsChatCompletions(
ctx context.Context, ctx context.Context,
@ -61,8 +61,8 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
promptCacheKey string, promptCacheKey string,
defaultMappedModel string, defaultMappedModel string,
) (*OpenAIForwardResult, error) { ) (*OpenAIForwardResult, error) {
// 入口分流APIKey 账号 + 已探测确认上游不支持 Responses走 CC 直转。 // 入口分流APIKey 账号 + 强制或已探测确认上游不支持 Responses走 CC 直转。
// 标记缺失(未探测)按"现状即证据"原则继续走下方原 Responses 转换路径。 // 自动模式下标记缺失(未探测)按"现状即证据"原则继续走下方原 Responses 转换路径。
if account.Type == AccountTypeAPIKey && !openai_compat.ShouldUseResponsesAPI(account.Extra) { if account.Type == AccountTypeAPIKey && !openai_compat.ShouldUseResponsesAPI(account.Extra) {
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel) return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
} }

View File

@ -1409,6 +1409,31 @@
</div> </div>
</div> </div>
<!-- OpenAI APIKey Responses API support mode -->
<div
v-if="account?.platform === 'openai' && account?.type === 'apikey'"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-3"
>
<div class="flex items-center justify-between gap-4">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.openai.responsesMode') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.responsesModeDesc') }}
</p>
</div>
<div class="w-56">
<Select
v-model="openAIResponsesMode"
:options="openAIResponsesModeOptions"
data-testid="openai-responses-mode-select"
/>
</div>
</div>
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300">
<span class="font-medium">{{ t(openAIResponsesStatusKey) }}</span>
</div>
</div>
<!-- Anthropic API Key 自动透传开关 --> <!-- Anthropic API Key 自动透传开关 -->
<div <div
v-if="account?.platform === 'anthropic' && account?.type === 'apikey'" v-if="account?.platform === 'anthropic' && account?.type === 'apikey'"
@ -2193,7 +2218,7 @@ import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import { useQuotaNotifyState } from '@/composables/useQuotaNotifyState' import { useQuotaNotifyState } from '@/composables/useQuotaNotifyState'
import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse, OpenAICompactMode } from '@/types' import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse, OpenAICompactMode, OpenAIResponsesMode } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
@ -2344,6 +2369,7 @@ const customBaseUrl = ref('')
// OpenAI OAuth/API Key // OpenAI OAuth/API Key
const openaiPassthroughEnabled = ref(false) const openaiPassthroughEnabled = ref(false)
const openAICompactMode = ref<OpenAICompactMode>('auto') const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF) const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF) const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false) const codexCLIOnlyEnabled = ref(false)
@ -2445,9 +2471,36 @@ const openAICompactModeOptions = computed(() => [
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') }, { value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
{ value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') } { value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') }
]) ])
const openAIResponsesModeOptions = computed(() => [
{ value: 'auto', label: t('admin.accounts.openai.responsesModeAuto') },
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
])
const normalizeOpenAIResponsesMode = (mode: unknown): OpenAIResponsesMode => {
if (mode === 'force_responses' || mode === 'force_chat_completions') {
return mode
}
return 'auto'
}
const isOpenAIModelRestrictionDisabled = computed(() => const isOpenAIModelRestrictionDisabled = computed(() =>
props.account?.platform === 'openai' && openaiPassthroughEnabled.value props.account?.platform === 'openai' && openaiPassthroughEnabled.value
) )
const openAIResponsesStatusKey = computed(() => {
if (openAIResponsesMode.value === 'force_responses') {
return 'admin.accounts.openai.responsesStatusForcedResponses'
}
if (openAIResponsesMode.value === 'force_chat_completions') {
return 'admin.accounts.openai.responsesStatusForcedChatCompletions'
}
const extra = props.account?.extra as Record<string, unknown> | undefined
if (extra?.openai_responses_supported === true) {
return 'admin.accounts.openai.responsesStatusAutoSupported'
}
if (extra?.openai_responses_supported === false) {
return 'admin.accounts.openai.responsesStatusAutoUnsupported'
}
return 'admin.accounts.openai.responsesStatusAutoUnknown'
})
const openAICompactStatusKey = computed(() => { const openAICompactStatusKey = computed(() => {
const extra = props.account?.extra as Record<string, unknown> | undefined const extra = props.account?.extra as Record<string, unknown> | undefined
if (!props.account || props.account.platform !== 'openai') return '' if (!props.account || props.account.platform !== 'openai') return ''
@ -2594,6 +2647,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key) // Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
openaiPassthroughEnabled.value = false openaiPassthroughEnabled.value = false
openAICompactMode.value = 'auto' openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
openAICompactModelMappings.value = [] openAICompactModelMappings.value = []
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
@ -2604,6 +2658,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) { if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto' openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
if (newAccount.type === 'apikey') {
openAIResponsesMode.value = normalizeOpenAIResponsesMode(extra?.openai_responses_mode)
}
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean' const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
? extra.codex_image_generation_bridge ? extra.codex_image_generation_bridge
: extra?.codex_image_generation_bridge_enabled : extra?.codex_image_generation_bridge_enabled
@ -3767,6 +3824,13 @@ const handleSubmit = async () => {
} else { } else {
newExtra.openai_compact_mode = openAICompactMode.value newExtra.openai_compact_mode = openAICompactMode.value
} }
if (props.account.type === 'apikey') {
if (openAIResponsesMode.value === 'auto') {
delete newExtra.openai_responses_mode
} else {
newExtra.openai_responses_mode = openAIResponsesMode.value
}
}
delete newExtra.codex_image_generation_bridge_enabled delete newExtra.codex_image_generation_bridge_enabled
if (codexImageGenerationBridgeMode.value === 'inherit') { if (codexImageGenerationBridgeMode.value === 'inherit') {

View File

@ -217,6 +217,48 @@ describe('EditAccountModal', () => {
}) })
}) })
it('submits OpenAI APIKey Responses support override mode', async () => {
const account = buildAccount()
account.extra = {
openai_responses_mode: 'force_chat_completions',
openai_responses_supported: false
}
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
await wrapper.get('[data-testid="openai-responses-mode-select"]').setValue('force_responses')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_mode).toBe('force_responses')
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_supported).toBe(false)
})
it('clears OpenAI APIKey Responses override when set back to auto', async () => {
const account = buildAccount()
account.extra = {
openai_responses_mode: 'force_chat_completions',
openai_responses_supported: true
}
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
await wrapper.get('[data-testid="openai-responses-mode-select"]').setValue('auto')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('openai_responses_mode')
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_supported).toBe(true)
})
it('submits account-level Codex image generation bridge override', async () => { it('submits account-level Codex image generation bridge override', async () => {
const account = buildAccount() const account = buildAccount()
account.extra = { account.extra = {

View File

@ -3167,6 +3167,17 @@ export default {
'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.', 'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.',
responsesWebsocketsV2PassthroughHint: responsesWebsocketsV2PassthroughHint:
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.', 'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
responsesMode: 'Responses API support',
responsesModeDesc:
'Only applies to OpenAI API Key accounts. Auto follows probe results; force modes override probing.',
responsesModeAuto: 'Auto',
responsesModeForceResponses: 'Force Responses',
responsesModeForceChatCompletions: 'Force Chat Completions',
responsesStatusAutoSupported: 'Auto probe: Responses',
responsesStatusAutoUnsupported: 'Auto probe: Chat Completions',
responsesStatusAutoUnknown: 'Auto probe: unknown',
responsesStatusForcedResponses: 'Forced Responses',
responsesStatusForcedChatCompletions: 'Forced Chat Completions',
codexCLIOnly: 'Codex official clients only', codexCLIOnly: 'Codex official clients only',
codexCLIOnlyDesc: 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.', '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.',

View File

@ -3313,6 +3313,17 @@ export default {
apiKeyResponsesWebsocketsV2Desc: apiKeyResponsesWebsocketsV2Desc:
'仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', '仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。',
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。', responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
responsesMode: 'Responses API 支持',
responsesModeDesc:
'仅对 OpenAI API Key 生效。自动跟随探测结果,强制模式会覆盖自动探测。',
responsesModeAuto: '自动',
responsesModeForceResponses: '强制 Responses',
responsesModeForceChatCompletions: '强制 Chat Completions',
responsesStatusAutoSupported: '自动探测Responses',
responsesStatusAutoUnsupported: '自动探测Chat Completions',
responsesStatusAutoUnknown: '自动探测:未探测',
responsesStatusForcedResponses: '已强制 Responses',
responsesStatusForcedChatCompletions: '已强制 Chat Completions',
codexCLIOnly: '仅允许 Codex 官方客户端', codexCLIOnly: '仅允许 Codex 官方客户端',
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。', codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
codexImageGenerationBridge: 'Codex 图片生成桥接', codexImageGenerationBridge: 'Codex 图片生成桥接',

View File

@ -970,6 +970,7 @@ export interface CodexUsageSnapshot {
} }
export type OpenAICompactMode = 'auto' | 'force_on' | 'force_off' export type OpenAICompactMode = 'auto' | 'force_on' | 'force_off'
export type OpenAIResponsesMode = 'auto' | 'force_responses' | 'force_chat_completions'
export interface OpenAICompactState { export interface OpenAICompactState {
openai_compact_mode?: OpenAICompactMode openai_compact_mode?: OpenAICompactMode
@ -979,6 +980,11 @@ export interface OpenAICompactState {
openai_compact_last_error?: string openai_compact_last_error?: string
} }
export interface OpenAIResponsesState {
openai_responses_mode?: OpenAIResponsesMode
openai_responses_supported?: boolean
}
export interface CreateAccountRequest { export interface CreateAccountRequest {
name: string name: string
notes?: string | null notes?: string | null