mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 08:22:34 +00:00
feat(openai): add Live gateway support
This commit is contained in:
@@ -263,6 +263,7 @@ const groupOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.a
|
||||
const requestTypeOptions = ref<SelectOption[]>([
|
||||
{ value: null, label: t('admin.usage.allTypes') },
|
||||
{ value: 'ws_v2', label: t('usage.ws') },
|
||||
{ value: 'live', label: t('usage.live') },
|
||||
{ value: 'stream', label: t('usage.stream') },
|
||||
{ value: 'sync', label: t('usage.sync') },
|
||||
{ value: 'cyber', label: t('usage.cyber') }
|
||||
|
||||
@@ -583,6 +583,7 @@ const tokenTooltipData = ref<AdminUsageLog | null>(null)
|
||||
const getRequestTypeLabel = (row: AdminUsageLog): string => {
|
||||
const requestType = resolveUsageRequestType(row)
|
||||
if (requestType === 'cyber') return t('usage.cyber')
|
||||
if (requestType === 'live') return t('usage.live')
|
||||
if (requestType === 'ws_v2') return t('usage.ws')
|
||||
if (requestType === 'stream') return t('usage.stream')
|
||||
if (requestType === 'sync') return t('usage.sync')
|
||||
@@ -592,6 +593,7 @@ const getRequestTypeLabel = (row: AdminUsageLog): string => {
|
||||
const getRequestTypeBadgeClass = (row: AdminUsageLog): string => {
|
||||
const requestType = resolveUsageRequestType(row)
|
||||
if (requestType === 'cyber') return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
if (requestType === 'live') return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'
|
||||
if (requestType === 'ws_v2') return 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200'
|
||||
if (requestType === 'stream') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||
if (requestType === 'sync') return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
|
||||
@@ -1095,6 +1095,11 @@ export default {
|
||||
targetModelPlaceholder: 'e.g., gpt-5.4',
|
||||
removeExactMapping: 'Remove Exact Mapping'
|
||||
},
|
||||
openaiLive: {
|
||||
title: 'OpenAI Live',
|
||||
allow: 'Allow Live access',
|
||||
hint: 'When enabled, API keys in this OpenAI group can create and control Live voice sessions. Disabled by default.'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: 'Invalid Request Fallback Group',
|
||||
hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.',
|
||||
|
||||
@@ -317,6 +317,7 @@ export default {
|
||||
stream: 'Stream',
|
||||
sync: 'Sync',
|
||||
cyber: 'Cyber',
|
||||
live: 'Live',
|
||||
unknown: 'Unknown',
|
||||
in: 'In',
|
||||
out: 'Out',
|
||||
|
||||
@@ -1093,6 +1093,11 @@ export default {
|
||||
targetModelPlaceholder: '例如: gpt-5.4',
|
||||
removeExactMapping: '删除精确映射'
|
||||
},
|
||||
openaiLive: {
|
||||
title: 'OpenAI Live',
|
||||
allow: '允许访问 Live',
|
||||
hint: '启用后,此 OpenAI 分组的 API Key 可以创建并控制 Live 语音会话。默认关闭。'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: '无效请求兜底分组',
|
||||
hint: '仅当上游明确返回 prompt too long 时才会触发,留空表示不兜底',
|
||||
|
||||
@@ -322,6 +322,7 @@ export default {
|
||||
stream: '流式',
|
||||
sync: '同步',
|
||||
cyber: '安全策略',
|
||||
live: 'Live',
|
||||
unknown: '未知',
|
||||
in: '输入',
|
||||
out: '输出',
|
||||
|
||||
@@ -551,6 +551,8 @@ export interface Group {
|
||||
fallback_group_id_on_invalid_request: number | null
|
||||
// OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程)
|
||||
allow_messages_dispatch?: boolean
|
||||
// OpenAI Live 接口开关
|
||||
allow_live: boolean
|
||||
default_mapped_model?: string
|
||||
messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig
|
||||
require_oauth_only: boolean
|
||||
@@ -742,6 +744,7 @@ export interface CreateGroupRequest {
|
||||
supported_model_scopes?: string[]
|
||||
models_list_config?: ModelsListConfig
|
||||
allow_messages_dispatch?: boolean
|
||||
allow_live?: boolean
|
||||
default_mapped_model?: string
|
||||
messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig
|
||||
model_routing?: Record<string, number[]> | null
|
||||
@@ -792,6 +795,7 @@ export interface UpdateGroupRequest {
|
||||
supported_model_scopes?: string[]
|
||||
models_list_config?: ModelsListConfig
|
||||
allow_messages_dispatch?: boolean
|
||||
allow_live?: boolean
|
||||
default_mapped_model?: string
|
||||
messages_dispatch_model_config?: OpenAIMessagesDispatchModelConfig
|
||||
model_routing?: Record<string, number[]> | null
|
||||
@@ -1511,7 +1515,7 @@ export interface CodexSessionImportResult {
|
||||
// ==================== Usage & Redeem Types ====================
|
||||
|
||||
export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription' | 'invitation'
|
||||
export type UsageRequestType = 'unknown' | 'sync' | 'stream' | 'ws_v2' | 'cyber'
|
||||
export type UsageRequestType = 'unknown' | 'sync' | 'stream' | 'ws_v2' | 'cyber' | 'live'
|
||||
export type ImageSizeSource = 'output' | 'input' | 'default' | 'legacy'
|
||||
export type ImageSizeBreakdown = Record<string, number>
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ export function statusCodeBadgeClass(code: number): string {
|
||||
return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
|
||||
}
|
||||
|
||||
/** 请求类型徽章配色(cyber 红、ws 紫、stream 蓝、sync 灰、未知琥珀) */
|
||||
/** 请求类型徽章配色(cyber 红、live 绿、ws 紫、stream 蓝、sync 灰、未知琥珀) */
|
||||
export function requestTypeBadgeClass(kind: UsageRequestKind): string {
|
||||
if (kind === 'cyber') return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
if (kind === 'live') return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'
|
||||
if (kind === 'ws_v2') return 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200'
|
||||
if (kind === 'stream') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||
if (kind === 'sync') return 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-200'
|
||||
@@ -27,6 +28,7 @@ export function requestTypeBadgeClass(kind: UsageRequestKind): string {
|
||||
/** 请求类型 i18n 键(展示方自行 t()) */
|
||||
export function requestTypeLabelKey(kind: UsageRequestKind): string {
|
||||
if (kind === 'cyber') return 'usage.cyber'
|
||||
if (kind === 'live') return 'usage.live'
|
||||
if (kind === 'ws_v2') return 'usage.ws'
|
||||
if (kind === 'stream') return 'usage.stream'
|
||||
if (kind === 'sync') return 'usage.sync'
|
||||
@@ -43,6 +45,7 @@ export function numericRequestTypeKind(
|
||||
): UsageRequestKind | null {
|
||||
const rt = requestType ?? (stream == null ? 0 : stream ? 2 : 1)
|
||||
if (rt === 3) return 'ws_v2'
|
||||
if (rt === 5) return 'live'
|
||||
if (rt === 2) return 'stream'
|
||||
if (rt === 1) return 'sync'
|
||||
return null
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface UsageRequestTypeLike {
|
||||
openai_ws_mode?: boolean | null
|
||||
}
|
||||
|
||||
const VALID_REQUEST_TYPES = new Set<UsageRequestType>(['unknown', 'sync', 'stream', 'ws_v2', 'cyber'])
|
||||
const VALID_REQUEST_TYPES = new Set<UsageRequestType>(['unknown', 'sync', 'stream', 'ws_v2', 'cyber', 'live'])
|
||||
|
||||
export const isUsageRequestType = (value: unknown): value is UsageRequestType => {
|
||||
return typeof value === 'string' && VALID_REQUEST_TYPES.has(value as UsageRequestType)
|
||||
@@ -24,7 +24,7 @@ export const resolveUsageRequestType = (value: UsageRequestTypeLike): UsageReque
|
||||
|
||||
export const requestTypeToLegacyStream = (requestType?: UsageRequestType | null): boolean | null | undefined => {
|
||||
// cyber 与 stream 正交(cyber 可发生在 stream 或非 stream 请求),不映射到 legacy stream 维度。
|
||||
if (!requestType || requestType === 'unknown' || requestType === 'cyber') {
|
||||
if (!requestType || requestType === 'unknown' || requestType === 'cyber' || requestType === 'live') {
|
||||
return null
|
||||
}
|
||||
if (requestType === 'sync') {
|
||||
|
||||
@@ -1394,6 +1394,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Live 开关(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="createForm.platform === 'openai'"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ t("admin.groups.openaiLive.title") }}
|
||||
</h4>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t("admin.groups.openaiLive.allow")
|
||||
}}</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="createForm.allow_live = !createForm.allow_live"
|
||||
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="
|
||||
createForm.allow_live
|
||||
? 'bg-primary-500'
|
||||
: 'bg-gray-300 dark:bg-dark-600'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="createForm.allow_live ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ t("admin.groups.openaiLive.hint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="createForm.platform === 'openai'"
|
||||
@@ -2912,6 +2945,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Live 开关(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="editForm.platform === 'openai'"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ t("admin.groups.openaiLive.title") }}
|
||||
</h4>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">{{
|
||||
t("admin.groups.openaiLive.allow")
|
||||
}}</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="editForm.allow_live = !editForm.allow_live"
|
||||
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="
|
||||
editForm.allow_live
|
||||
? 'bg-primary-500'
|
||||
: 'bg-gray-300 dark:bg-dark-600'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="editForm.allow_live ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ t("admin.groups.openaiLive.hint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="editForm.platform === 'openai'"
|
||||
@@ -4535,6 +4601,7 @@ const createForm = reactive({
|
||||
fallback_group_id_on_invalid_request: null as number | null,
|
||||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||||
allow_messages_dispatch: false,
|
||||
allow_live: false,
|
||||
opus_mapped_model: createMessagesDispatchDefaults.opus_mapped_model,
|
||||
sonnet_mapped_model: createMessagesDispatchDefaults.sonnet_mapped_model,
|
||||
haiku_mapped_model: createMessagesDispatchDefaults.haiku_mapped_model,
|
||||
@@ -4884,6 +4951,7 @@ const editForm = reactive({
|
||||
fallback_group_id_on_invalid_request: null as number | null,
|
||||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||||
allow_messages_dispatch: false,
|
||||
allow_live: false,
|
||||
default_mapped_model: '',
|
||||
opus_mapped_model: editMessagesDispatchDefaults.opus_mapped_model,
|
||||
sonnet_mapped_model: editMessagesDispatchDefaults.sonnet_mapped_model,
|
||||
@@ -5287,6 +5355,7 @@ const closeCreateModal = () => {
|
||||
createForm.fallback_group_id = null;
|
||||
createForm.fallback_group_id_on_invalid_request = null;
|
||||
resetMessagesDispatchFormState(createForm);
|
||||
createForm.allow_live = false;
|
||||
createForm.require_oauth_only = false;
|
||||
createForm.require_privacy_set = false;
|
||||
createForm.supported_model_scopes = ["claude", "gemini_text", "gemini_image"];
|
||||
@@ -5474,6 +5543,7 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
editForm.allow_messages_dispatch =
|
||||
group.allow_messages_dispatch ||
|
||||
messagesDispatchFormState.allow_messages_dispatch;
|
||||
editForm.allow_live = group.allow_live ?? false;
|
||||
editForm.opus_mapped_model = messagesDispatchFormState.opus_mapped_model;
|
||||
editForm.sonnet_mapped_model = messagesDispatchFormState.sonnet_mapped_model;
|
||||
editForm.haiku_mapped_model = messagesDispatchFormState.haiku_mapped_model;
|
||||
@@ -5530,6 +5600,7 @@ const closeEditModal = () => {
|
||||
editForm.video_price_1080p = null;
|
||||
editForm.web_search_price_per_call = null;
|
||||
resetMessagesDispatchFormState(editForm);
|
||||
editForm.allow_live = false;
|
||||
resetModelsListState(editModelsListState);
|
||||
};
|
||||
|
||||
@@ -5934,6 +6005,7 @@ watch(
|
||||
}
|
||||
if (newVal !== "openai") {
|
||||
resetMessagesDispatchFormState(createForm);
|
||||
createForm.allow_live = false;
|
||||
}
|
||||
createForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
|
||||
newVal,
|
||||
@@ -5976,6 +6048,7 @@ watch(
|
||||
}
|
||||
if (newVal !== "openai") {
|
||||
resetMessagesDispatchFormState(editForm);
|
||||
editForm.allow_live = false;
|
||||
}
|
||||
editForm.max_reasoning_effort = normalizeReasoningEffortForPlatform(
|
||||
newVal,
|
||||
@@ -6020,6 +6093,7 @@ watch(
|
||||
}
|
||||
if (newVal !== 'openai') {
|
||||
editForm.allow_messages_dispatch = false
|
||||
editForm.allow_live = false
|
||||
editForm.default_mapped_model = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,6 +538,7 @@ const openCleanupDialog = () => { cleanupDialogVisible.value = true }
|
||||
const getRequestTypeLabel = (log: AdminUsageLog): string => {
|
||||
const requestType = resolveUsageRequestType(log)
|
||||
if (requestType === 'cyber') return t('usage.cyber')
|
||||
if (requestType === 'live') return t('usage.live')
|
||||
if (requestType === 'ws_v2') return t('usage.ws')
|
||||
if (requestType === 'stream') return t('usage.stream')
|
||||
if (requestType === 'sync') return t('usage.sync')
|
||||
|
||||
@@ -376,6 +376,7 @@ const granularityOptions = computed<SelectOption[]>(() => [
|
||||
const requestTypeOptions = computed<SelectOption[]>(() => [
|
||||
{ value: null, label: t('admin.usage.allTypes') },
|
||||
{ value: 'ws_v2', label: t('usage.ws') },
|
||||
{ value: 'live', label: t('usage.live') },
|
||||
{ value: 'stream', label: t('usage.stream') },
|
||||
{ value: 'sync', label: t('usage.sync') },
|
||||
])
|
||||
@@ -595,6 +596,7 @@ const handleIpGeoBatchFailed = () => {
|
||||
const getRequestTypeExportText = (log: UsageLog): string => {
|
||||
const requestType = resolveUsageRequestType(log)
|
||||
if (requestType === 'cyber') return 'Cyber'
|
||||
if (requestType === 'live') return 'Live'
|
||||
if (requestType === 'ws_v2') return 'WS'
|
||||
if (requestType === 'stream') return 'Stream'
|
||||
if (requestType === 'sync') return 'Sync'
|
||||
|
||||
Reference in New Issue
Block a user