mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #2450 from wucm667/codex/issue-2431-responses-api-support
feat: 支持后台配置 OpenAI Responses API 路由
This commit is contained in:
@@ -1409,6 +1409,31 @@
|
||||
</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 自动透传开关 -->
|
||||
<div
|
||||
v-if="account?.platform === 'anthropic' && account?.type === 'apikey'"
|
||||
@@ -2193,7 +2218,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
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 ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
@@ -2344,6 +2369,7 @@ const customBaseUrl = ref('')
|
||||
// OpenAI 自动透传开关(OAuth/API Key)
|
||||
const openaiPassthroughEnabled = ref(false)
|
||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
|
||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
@@ -2445,9 +2471,36 @@ const openAICompactModeOptions = computed(() => [
|
||||
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
|
||||
{ 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(() =>
|
||||
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 extra = props.account?.extra as Record<string, unknown> | undefined
|
||||
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)
|
||||
openaiPassthroughEnabled.value = false
|
||||
openAICompactMode.value = 'auto'
|
||||
openAIResponsesMode.value = 'auto'
|
||||
openAICompactModelMappings.value = []
|
||||
openaiOAuthResponsesWebSocketV2Mode.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')) {
|
||||
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
|
||||
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'
|
||||
? extra.codex_image_generation_bridge
|
||||
: extra?.codex_image_generation_bridge_enabled
|
||||
@@ -3767,6 +3824,13 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
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
|
||||
if (codexImageGenerationBridgeMode.value === 'inherit') {
|
||||
|
||||
@@ -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 () => {
|
||||
const account = buildAccount()
|
||||
account.extra = {
|
||||
|
||||
@@ -3167,6 +3167,17 @@ export default {
|
||||
'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.',
|
||||
responsesWebsocketsV2PassthroughHint:
|
||||
'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',
|
||||
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.',
|
||||
|
||||
@@ -3313,6 +3313,17 @@ export default {
|
||||
apiKeyResponsesWebsocketsV2Desc:
|
||||
'仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket 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 官方客户端',
|
||||
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
|
||||
codexImageGenerationBridge: 'Codex 图片生成桥接',
|
||||
|
||||
@@ -970,6 +970,7 @@ export interface CodexUsageSnapshot {
|
||||
}
|
||||
|
||||
export type OpenAICompactMode = 'auto' | 'force_on' | 'force_off'
|
||||
export type OpenAIResponsesMode = 'auto' | 'force_responses' | 'force_chat_completions'
|
||||
|
||||
export interface OpenAICompactState {
|
||||
openai_compact_mode?: OpenAICompactMode
|
||||
@@ -979,6 +980,11 @@ export interface OpenAICompactState {
|
||||
openai_compact_last_error?: string
|
||||
}
|
||||
|
||||
export interface OpenAIResponsesState {
|
||||
openai_responses_mode?: OpenAIResponsesMode
|
||||
openai_responses_supported?: boolean
|
||||
}
|
||||
|
||||
export interface CreateAccountRequest {
|
||||
name: string
|
||||
notes?: string | null
|
||||
|
||||
Reference in New Issue
Block a user