feat(openai): port /responses/compact account support flow (PR #1555)

vansour/sub2api#1555 的 OpenAI compact 能力建模手工移植到当前 main:账号
级 compact 状态/auto-force_on-force_off 模式、compact-only 模型映射、调度器
tier 分层(已支持 > 未知 > 已知不支持)、管理后台 compact 主动探测,以及对应
i18n/状态徽章。普通 /responses 流量行为不变,无数据库迁移。
This commit is contained in:
shaw
2026-04-25 14:40:03 +08:00
parent b95ffce244
commit 095f457c57
32 changed files with 2534 additions and 189 deletions

View File

@@ -1306,6 +1306,64 @@
</div>
</div>
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.openai.compactMode') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.compactModeDesc') }}
</p>
</div>
<div class="w-44">
<Select v-model="openAICompactMode" :options="openAICompactModeOptions" />
</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(openAICompactStatusKey) }}</span>
<span
v-if="account?.extra?.openai_compact_checked_at"
class="ml-2 text-gray-500 dark:text-gray-400"
>
{{ t('admin.accounts.openai.compactLastChecked') }}:
{{ formatDateTime(new Date(String(account.extra.openai_compact_checked_at))) }}
</span>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.openai.compactModelMapping') }}</label>
<p class="input-hint">{{ t('admin.accounts.openai.compactModelMappingDesc') }}</p>
<div v-if="openAICompactModelMappings.length > 0" class="mb-3 space-y-2">
<div
v-for="(mapping, index) in openAICompactModelMappings"
:key="getOpenAICompactModelMappingKey(mapping)"
class="flex items-center gap-2"
>
<input
v-model="mapping.from"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.fromModel')"
/>
<span class="text-gray-400"></span>
<input
v-model="mapping.to"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.toModel')"
/>
<button type="button" @click="removeOpenAICompactModelMapping(index)" class="text-red-500 hover:text-red-700">
<Icon name="trash" size="sm" />
</button>
</div>
</div>
<button type="button" @click="addOpenAICompactModelMapping" class="btn btn-secondary text-sm">
+ {{ t('admin.accounts.addMapping') }}
</button>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<div>
@@ -1849,7 +1907,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 } from '@/types'
import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse, OpenAICompactMode } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
@@ -1859,7 +1917,7 @@ import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
import {
OPENAI_WS_MODE_CTX_POOL,
@@ -1934,6 +1992,7 @@ const isBedrockAPIKeyMode = computed(() =>
(props.account?.credentials as Record<string, unknown>)?.auth_mode === 'apikey'
)
const modelMappings = ref<ModelMapping[]>([])
const openAICompactModelMappings = ref<ModelMapping[]>([])
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const allowedModels = ref<string[]>([])
const DEFAULT_POOL_MODE_RETRY_COUNT = 3
@@ -1953,6 +2012,7 @@ const antigravityModelMappings = ref<ModelMapping[]>([])
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-model-mapping')
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-openai-compact-model-mapping')
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('edit-temp-unsched-rule')
@@ -1992,6 +2052,7 @@ const customBaseUrl = ref('')
// OpenAI 自动透传开关OAuth/API Key
const openaiPassthroughEnabled = ref(false)
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false)
@@ -2045,9 +2106,27 @@ const openaiResponsesWebSocketV2Mode = computed({
const openAIWSModeConcurrencyHintKey = computed(() =>
resolveOpenAIWSModeConcurrencyHintKey(openaiResponsesWebSocketV2Mode.value)
)
const openAICompactModeOptions = computed(() => [
{ value: 'auto', label: t('admin.accounts.openai.compactModeAuto') },
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
{ value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') }
])
const isOpenAIModelRestrictionDisabled = computed(() =>
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
)
const openAICompactStatusKey = computed(() => {
const extra = props.account?.extra as Record<string, unknown> | undefined
if (!props.account || props.account.platform !== 'openai') return ''
const mode = typeof extra?.openai_compact_mode === 'string' ? extra.openai_compact_mode : 'auto'
if (mode === 'force_on') return 'admin.accounts.openai.compactSupported'
if (mode === 'force_off') return 'admin.accounts.openai.compactUnsupported'
if (typeof extra?.openai_compact_supported === 'boolean') {
return extra.openai_compact_supported
? 'admin.accounts.openai.compactSupported'
: 'admin.accounts.openai.compactUnsupported'
}
return 'admin.accounts.openai.compactUnknown'
})
// Computed: current preset mappings based on platform
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
@@ -2177,6 +2256,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
openaiPassthroughEnabled.value = false
openAICompactMode.value = 'auto'
openAICompactModelMappings.value = []
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
@@ -2184,6 +2265,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
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'
openaiOAuthResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, {
modeKey: 'openai_oauth_responses_websockets_v2_mode',
enabledKey: 'openai_oauth_responses_websockets_v2_enabled',
@@ -2199,6 +2281,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
if (newAccount.type === 'oauth') {
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
}
const credentials = newAccount.credentials as Record<string, unknown> | undefined
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
if (compactMappings && typeof compactMappings === 'object') {
openAICompactModelMappings.value = Object.entries(compactMappings).map(([from, to]) => ({ from, to }))
}
}
if (newAccount.platform === 'anthropic' && newAccount.type === 'apikey') {
anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true
@@ -2423,6 +2510,15 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editApiKey.value = ''
}
async function loadTLSProfiles() {
try {
const profiles = await adminAPI.tlsFingerprintProfiles.list()
tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name }))
} catch {
tlsFingerprintProfiles.value = []
}
}
watch(
[() => props.show, () => props.account],
([show, newAccount], [wasShow, previousAccount]) => {
@@ -2437,15 +2533,6 @@ watch(
{ immediate: true }
)
const loadTLSProfiles = async () => {
try {
const profiles = await adminAPI.tlsFingerprintProfiles.list()
tlsFingerprintProfiles.value = profiles.map(p => ({ id: p.id, name: p.name }))
} catch {
tlsFingerprintProfiles.value = []
}
}
// Model mapping helpers
const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' })
@@ -2468,6 +2555,14 @@ const addAntigravityModelMapping = () => {
antigravityModelMappings.value.push({ from: '', to: '' })
}
const addOpenAICompactModelMapping = () => {
openAICompactModelMappings.value.push({ from: '', to: '' })
}
const removeOpenAICompactModelMapping = (index: number) => {
openAICompactModelMappings.value.splice(index, 1)
}
const removeAntigravityModelMapping = (index: number) => {
antigravityModelMappings.value.splice(index, 1)
}
@@ -2911,6 +3006,14 @@ const handleSubmit = async () => {
} else if (currentCredentials.model_mapping) {
newCredentials.model_mapping = currentCredentials.model_mapping
}
if (props.account.platform === 'openai') {
const compactModelMapping = buildModelMappingObject('mapping', [], openAICompactModelMappings.value)
if (compactModelMapping) {
newCredentials.compact_model_mapping = compactModelMapping
} else {
delete newCredentials.compact_model_mapping
}
}
// Add pool mode if enabled
if (poolModeEnabled.value) {
@@ -3036,6 +3139,12 @@ const handleSubmit = async () => {
// 透传模式保留现有映射
newCredentials.model_mapping = currentCredentials.model_mapping
}
const compactModelMapping = buildModelMappingObject('mapping', [], openAICompactModelMappings.value)
if (compactModelMapping) {
newCredentials.compact_model_mapping = compactModelMapping
} else {
delete newCredentials.compact_model_mapping
}
updatePayload.credentials = newCredentials
}
@@ -3208,6 +3317,11 @@ const handleSubmit = async () => {
delete newExtra.openai_passthrough
delete newExtra.openai_oauth_passthrough
}
if (openAICompactMode.value === 'auto') {
delete newExtra.openai_compact_mode
} else {
newExtra.openai_compact_mode = openAICompactMode.value
}
if (props.account.type === 'oauth') {
if (codexCLIOnlyEnabled.value) {