feat(Sora): 完成Sora网关接入与媒体能力
新增 Sora 网关路由、账号调度与同步服务\n补充媒体代理与签名 URL、模型列表动态拉取\n完善计费配置、前端支持与相关测试
This commit is contained in:
@@ -18,6 +18,7 @@ import geminiAPI from './gemini'
|
||||
import antigravityAPI from './antigravity'
|
||||
import userAttributesAPI from './userAttributes'
|
||||
import opsAPI from './ops'
|
||||
import modelsAPI from './models'
|
||||
|
||||
/**
|
||||
* Unified admin API object for convenient access
|
||||
@@ -37,7 +38,8 @@ export const adminAPI = {
|
||||
gemini: geminiAPI,
|
||||
antigravity: antigravityAPI,
|
||||
userAttributes: userAttributesAPI,
|
||||
ops: opsAPI
|
||||
ops: opsAPI,
|
||||
models: modelsAPI
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -55,7 +57,8 @@ export {
|
||||
geminiAPI,
|
||||
antigravityAPI,
|
||||
userAttributesAPI,
|
||||
opsAPI
|
||||
opsAPI,
|
||||
modelsAPI
|
||||
}
|
||||
|
||||
export default adminAPI
|
||||
|
||||
14
frontend/src/api/admin/models.ts
Normal file
14
frontend/src/api/admin/models.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { apiClient } from '@/api/client'
|
||||
|
||||
export async function getPlatformModels(platform: string): Promise<string[]> {
|
||||
const { data } = await apiClient.get<string[]>('/admin/models', {
|
||||
params: { platform }
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export const modelsAPI = {
|
||||
getPlatformModels
|
||||
}
|
||||
|
||||
export default modelsAPI
|
||||
@@ -45,6 +45,19 @@
|
||||
:placeholder="t('admin.accounts.searchModels')"
|
||||
@click.stop
|
||||
/>
|
||||
<div v-if="props.platform === 'sora'" class="mt-2 flex items-center gap-2 text-xs">
|
||||
<span v-if="loadingSoraModels" class="text-gray-500">
|
||||
{{ t('admin.accounts.soraModelsLoading') }}
|
||||
</span>
|
||||
<button
|
||||
v-else-if="soraLoadError"
|
||||
type="button"
|
||||
class="text-primary-600 hover:underline dark:text-primary-400"
|
||||
@click.stop="loadSoraModels"
|
||||
>
|
||||
{{ t('admin.accounts.soraModelsRetry') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-52 overflow-auto">
|
||||
<button
|
||||
@@ -120,12 +133,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ModelIcon from '@/components/common/ModelIcon.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -144,11 +158,24 @@ const showDropdown = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const customModel = ref('')
|
||||
const isComposing = ref(false)
|
||||
const soraModelOptions = ref<{ value: string; label: string }[]>([])
|
||||
const loadingSoraModels = ref(false)
|
||||
const soraLoadError = ref(false)
|
||||
|
||||
const availableOptions = computed(() => {
|
||||
if (props.platform === 'sora') {
|
||||
if (soraModelOptions.value.length > 0) {
|
||||
return soraModelOptions.value
|
||||
}
|
||||
return getModelsByPlatform('sora').map(m => ({ value: m, label: m }))
|
||||
}
|
||||
return allModels
|
||||
})
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
if (!query) return allModels
|
||||
return allModels.filter(
|
||||
if (!query) return availableOptions.value
|
||||
return availableOptions.value.filter(
|
||||
m => m.value.toLowerCase().includes(query) || m.label.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
@@ -186,7 +213,9 @@ const handleEnter = () => {
|
||||
}
|
||||
|
||||
const fillRelated = () => {
|
||||
const models = getModelsByPlatform(props.platform)
|
||||
const models = props.platform === 'sora' && soraModelOptions.value.length > 0
|
||||
? soraModelOptions.value.map(m => m.value)
|
||||
: getModelsByPlatform(props.platform)
|
||||
const newModels = [...props.modelValue]
|
||||
for (const model of models) {
|
||||
if (!newModels.includes(model)) newModels.push(model)
|
||||
@@ -197,4 +226,32 @@ const fillRelated = () => {
|
||||
const clearAll = () => {
|
||||
emit('update:modelValue', [])
|
||||
}
|
||||
|
||||
const loadSoraModels = async () => {
|
||||
if (props.platform !== 'sora') {
|
||||
soraModelOptions.value = []
|
||||
return
|
||||
}
|
||||
if (loadingSoraModels.value) return
|
||||
soraLoadError.value = false
|
||||
loadingSoraModels.value = true
|
||||
try {
|
||||
const models = await adminAPI.models.getPlatformModels('sora')
|
||||
soraModelOptions.value = (models || []).map((m) => ({ value: m, label: m }))
|
||||
} catch (error) {
|
||||
console.warn('加载 Sora 模型列表失败', error)
|
||||
soraLoadError.value = true
|
||||
appStore.showWarning(t('admin.accounts.soraModelsLoadFailed'))
|
||||
} finally {
|
||||
loadingSoraModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.platform,
|
||||
() => {
|
||||
loadSoraModels()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@ const props = defineProps(['searchQuery', 'filters']); const emit = defineEmits(
|
||||
const updatePlatform = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, platform: value }) }
|
||||
const updateType = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, type: value }) }
|
||||
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }, { value: 'sora', label: 'Sora' }])
|
||||
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }])
|
||||
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }])
|
||||
</script>
|
||||
|
||||
@@ -97,6 +97,9 @@ const labelClass = computed(() => {
|
||||
if (props.platform === 'gemini') {
|
||||
return `${base} bg-blue-200/60 text-blue-800 dark:bg-blue-800/40 dark:text-blue-300`
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return `${base} bg-rose-200/60 text-rose-800 dark:bg-rose-800/40 dark:text-rose-300`
|
||||
}
|
||||
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
|
||||
})
|
||||
|
||||
@@ -118,6 +121,11 @@ const badgeClass = computed(() => {
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return isSubscription.value
|
||||
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
|
||||
: 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
|
||||
}
|
||||
// Fallback: original colors
|
||||
return isSubscription.value
|
||||
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
<svg v-else-if="platform === 'antigravity'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
|
||||
</svg>
|
||||
<!-- Sora logo (sparkle) -->
|
||||
<svg v-else-if="platform === 'sora'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2.5l2.1 4.7 5.1.5-3.9 3.4 1.2 5-4.5-2.6-4.5 2.6 1.2-5-3.9-3.4 5.1-.5L12 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Fallback: generic platform icon -->
|
||||
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
||||
@@ -48,6 +48,7 @@ const platformLabel = computed(() => {
|
||||
if (props.platform === 'anthropic') return 'Anthropic'
|
||||
if (props.platform === 'openai') return 'OpenAI'
|
||||
if (props.platform === 'antigravity') return 'Antigravity'
|
||||
if (props.platform === 'sora') return 'Sora'
|
||||
return 'Gemini'
|
||||
})
|
||||
|
||||
@@ -74,6 +75,9 @@ const platformClass = computed(() => {
|
||||
if (props.platform === 'antigravity') {
|
||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
|
||||
@@ -87,6 +91,9 @@ const typeClass = computed(() => {
|
||||
if (props.platform === 'antigravity') {
|
||||
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
}
|
||||
if (props.platform === 'sora') {
|
||||
return 'bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -52,6 +52,22 @@ const geminiModels = [
|
||||
'gemini-3-pro-preview'
|
||||
]
|
||||
|
||||
// Sora (sora2api)
|
||||
const soraModels = [
|
||||
'gpt-image', 'gpt-image-landscape', 'gpt-image-portrait',
|
||||
'sora2-landscape-10s', 'sora2-portrait-10s',
|
||||
'sora2-landscape-15s', 'sora2-portrait-15s',
|
||||
'sora2-landscape-25s', 'sora2-portrait-25s',
|
||||
'sora2pro-landscape-10s', 'sora2pro-portrait-10s',
|
||||
'sora2pro-landscape-15s', 'sora2pro-portrait-15s',
|
||||
'sora2pro-landscape-25s', 'sora2pro-portrait-25s',
|
||||
'sora2pro-hd-landscape-10s', 'sora2pro-hd-portrait-10s',
|
||||
'sora2pro-hd-landscape-15s', 'sora2pro-hd-portrait-15s',
|
||||
'prompt-enhance-short-10s', 'prompt-enhance-short-15s', 'prompt-enhance-short-20s',
|
||||
'prompt-enhance-medium-10s', 'prompt-enhance-medium-15s', 'prompt-enhance-medium-20s',
|
||||
'prompt-enhance-long-10s', 'prompt-enhance-long-15s', 'prompt-enhance-long-20s'
|
||||
]
|
||||
|
||||
// 智谱 GLM
|
||||
const zhipuModels = [
|
||||
'glm-4', 'glm-4v', 'glm-4-plus', 'glm-4-0520',
|
||||
@@ -182,6 +198,7 @@ const allModelsList: string[] = [
|
||||
...openaiModels,
|
||||
...claudeModels,
|
||||
...geminiModels,
|
||||
...soraModels,
|
||||
...zhipuModels,
|
||||
...qwenModels,
|
||||
...deepseekModels,
|
||||
@@ -227,6 +244,8 @@ const openaiPresetMappings = [
|
||||
{ label: 'GPT-5.1 Codex', from: 'gpt-5.1-codex', to: 'gpt-5.1-codex', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }
|
||||
]
|
||||
|
||||
const soraPresetMappings: { label: string; from: string; to: string; color: string }[] = []
|
||||
|
||||
const geminiPresetMappings = [
|
||||
{ label: 'Flash 2.0', from: 'gemini-2.0-flash', to: 'gemini-2.0-flash', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||
{ label: '2.5 Flash', from: 'gemini-2.5-flash', to: 'gemini-2.5-flash', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
@@ -258,6 +277,7 @@ export function getModelsByPlatform(platform: string): string[] {
|
||||
case 'anthropic':
|
||||
case 'claude': return claudeModels
|
||||
case 'gemini': return geminiModels
|
||||
case 'sora': return soraModels
|
||||
case 'zhipu': return zhipuModels
|
||||
case 'qwen': return qwenModels
|
||||
case 'deepseek': return deepseekModels
|
||||
@@ -281,6 +301,7 @@ export function getModelsByPlatform(platform: string): string[] {
|
||||
export function getPresetMappingsByPlatform(platform: string) {
|
||||
if (platform === 'openai') return openaiPresetMappings
|
||||
if (platform === 'gemini') return geminiPresetMappings
|
||||
if (platform === 'sora') return soraPresetMappings
|
||||
return anthropicPresetMappings
|
||||
}
|
||||
|
||||
|
||||
@@ -895,7 +895,8 @@ export default {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
antigravity: 'Antigravity',
|
||||
sora: 'Sora'
|
||||
},
|
||||
deleteConfirm:
|
||||
"Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.",
|
||||
@@ -920,6 +921,14 @@ export default {
|
||||
title: 'Image Generation Pricing',
|
||||
description: 'Configure pricing for gemini-3-pro-image model. Leave empty to use default prices.'
|
||||
},
|
||||
soraPricing: {
|
||||
title: 'Sora Per-Request Pricing',
|
||||
description: 'Configure per-request pricing for Sora image/video generation. Leave empty to disable billing.',
|
||||
image360: 'Image 360px ($)',
|
||||
image540: 'Image 540px ($)',
|
||||
video: 'Video (standard) ($)',
|
||||
videoHd: 'Video (Pro-HD) ($)'
|
||||
},
|
||||
claudeCode: {
|
||||
title: 'Claude Code Client Restriction',
|
||||
tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.',
|
||||
@@ -1079,7 +1088,8 @@ export default {
|
||||
claude: 'Claude',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
antigravity: 'Antigravity',
|
||||
sora: 'Sora'
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
@@ -1257,6 +1267,9 @@ export default {
|
||||
'Map request models to actual models. Left is the requested model, right is the actual model sent to API.',
|
||||
selectedModels: 'Selected {count} model(s)',
|
||||
supportsAllModels: '(supports all models)',
|
||||
soraModelsLoadFailed: 'Failed to load Sora models, fallback to default list',
|
||||
soraModelsLoading: 'Loading Sora models...',
|
||||
soraModelsRetry: 'Load failed, click to retry',
|
||||
requestModel: 'Request model',
|
||||
actualModel: 'Actual model',
|
||||
addMapping: 'Add Mapping',
|
||||
|
||||
@@ -941,7 +941,8 @@ export default {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
antigravity: 'Antigravity',
|
||||
sora: 'Sora'
|
||||
},
|
||||
saving: '保存中...',
|
||||
noGroups: '暂无分组',
|
||||
@@ -995,6 +996,14 @@ export default {
|
||||
title: '图片生成计费',
|
||||
description: '配置 gemini-3-pro-image 模型的图片生成价格,留空则使用默认价格'
|
||||
},
|
||||
soraPricing: {
|
||||
title: 'Sora 按次计费',
|
||||
description: '配置 Sora 图片/视频按次收费价格,留空则默认不计费',
|
||||
image360: '图片 360px ($)',
|
||||
image540: '图片 540px ($)',
|
||||
video: '视频(标准)($)',
|
||||
videoHd: '视频(Pro-HD)($)'
|
||||
},
|
||||
claudeCode: {
|
||||
title: 'Claude Code 客户端限制',
|
||||
tooltip: '启用后,此分组仅允许 Claude Code 官方客户端访问。非 Claude Code 请求将被拒绝或降级到指定分组。',
|
||||
@@ -1199,7 +1208,8 @@ export default {
|
||||
openai: 'OpenAI',
|
||||
anthropic: 'Anthropic',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
antigravity: 'Antigravity',
|
||||
sora: 'Sora'
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
@@ -1391,6 +1401,9 @@ export default {
|
||||
mapRequestModels: '将请求模型映射到实际模型。左边是请求的模型,右边是发送到 API 的实际模型。',
|
||||
selectedModels: '已选择 {count} 个模型',
|
||||
supportsAllModels: '(支持所有模型)',
|
||||
soraModelsLoadFailed: '加载 Sora 模型列表失败,已回退到默认列表',
|
||||
soraModelsLoading: '正在加载 Sora 模型...',
|
||||
soraModelsRetry: '加载失败,点击重试',
|
||||
requestModel: '请求模型',
|
||||
actualModel: '实际模型',
|
||||
addMapping: '添加映射',
|
||||
|
||||
@@ -252,7 +252,7 @@ export interface PaginationConfig {
|
||||
|
||||
// ==================== API Key & Group Types ====================
|
||||
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'
|
||||
|
||||
export type SubscriptionType = 'standard' | 'subscription'
|
||||
|
||||
@@ -272,6 +272,11 @@ export interface Group {
|
||||
image_price_1k: number | null
|
||||
image_price_2k: number | null
|
||||
image_price_4k: number | null
|
||||
// Sora 按次计费配置
|
||||
sora_image_price_360: number | null
|
||||
sora_image_price_540: number | null
|
||||
sora_video_price_per_request: number | null
|
||||
sora_video_price_per_request_hd: number | null
|
||||
// Claude Code 客户端限制
|
||||
claude_code_only: boolean
|
||||
fallback_group_id: number | null
|
||||
@@ -331,6 +336,10 @@ export interface CreateGroupRequest {
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
sora_image_price_360?: number | null
|
||||
sora_image_price_540?: number | null
|
||||
sora_video_price_per_request?: number | null
|
||||
sora_video_price_per_request_hd?: number | null
|
||||
claude_code_only?: boolean
|
||||
fallback_group_id?: number | null
|
||||
}
|
||||
@@ -349,13 +358,17 @@ export interface UpdateGroupRequest {
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
sora_image_price_360?: number | null
|
||||
sora_image_price_540?: number | null
|
||||
sora_video_price_per_request?: number | null
|
||||
sora_video_price_per_request_hd?: number | null
|
||||
claude_code_only?: boolean
|
||||
fallback_group_id?: number | null
|
||||
}
|
||||
|
||||
// ==================== Account & Proxy Types ====================
|
||||
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'sora'
|
||||
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
|
||||
export type OAuthAddMethod = 'oauth' | 'setup-token'
|
||||
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
|
||||
|
||||
@@ -404,6 +404,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sora 按次计费配置 -->
|
||||
<div v-if="createForm.platform === 'sora'" class="border-t pt-4">
|
||||
<label class="block mb-2 font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.soraPricing.title') }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t('admin.groups.soraPricing.description') }}
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.image360') }}</label>
|
||||
<input
|
||||
v-model.number="createForm.sora_image_price_360"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.05"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.image540') }}</label>
|
||||
<input
|
||||
v-model.number="createForm.sora_image_price_540"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.08"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.video') }}</label>
|
||||
<input
|
||||
v-model.number="createForm.sora_video_price_per_request"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.videoHd') }}</label>
|
||||
<input
|
||||
v-model.number="createForm.sora_video_price_per_request_hd"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code 客户端限制(仅 anthropic 平台) -->
|
||||
<div v-if="createForm.platform === 'anthropic'" class="border-t pt-4">
|
||||
<div class="mb-1.5 flex items-center gap-1">
|
||||
@@ -848,6 +906,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sora 按次计费配置 -->
|
||||
<div v-if="editForm.platform === 'sora'" class="border-t pt-4">
|
||||
<label class="block mb-2 font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.soraPricing.title') }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t('admin.groups.soraPricing.description') }}
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.image360') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.sora_image_price_360"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.05"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.image540') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.sora_image_price_540"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.08"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.video') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.sora_video_price_per_request"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.soraPricing.videoHd') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.sora_video_price_per_request_hd"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code 客户端限制(仅 anthropic 平台) -->
|
||||
<div v-if="editForm.platform === 'anthropic'" class="border-t pt-4">
|
||||
<div class="mb-1.5 flex items-center gap-1">
|
||||
@@ -1152,7 +1268,8 @@ const platformOptions = computed(() => [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
{ value: 'antigravity', label: 'Antigravity' },
|
||||
{ value: 'sora', label: 'Sora' }
|
||||
])
|
||||
|
||||
const platformFilterOptions = computed(() => [
|
||||
@@ -1160,7 +1277,8 @@ const platformFilterOptions = computed(() => [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
{ value: 'antigravity', label: 'Antigravity' },
|
||||
{ value: 'sora', label: 'Sora' }
|
||||
])
|
||||
|
||||
const editStatusOptions = computed(() => [
|
||||
@@ -1240,6 +1358,16 @@ const createForm = reactive({
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null,
|
||||
// Sora 按次计费配置
|
||||
sora_image_price_360: null as number | null,
|
||||
sora_image_price_540: null as number | null,
|
||||
sora_video_price_per_request: null as number | null,
|
||||
sora_video_price_per_request_hd: null as number | null,
|
||||
// Sora 按次计费配置
|
||||
sora_image_price_360: null as number | null,
|
||||
sora_image_price_540: null as number | null,
|
||||
sora_video_price_per_request: null as number | null,
|
||||
sora_video_price_per_request_hd: null as number | null,
|
||||
// Claude Code 客户端限制(仅 anthropic 平台使用)
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null as number | null,
|
||||
@@ -1411,6 +1539,11 @@ const editForm = reactive({
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null,
|
||||
// Sora 按次计费配置
|
||||
sora_image_price_360: null as number | null,
|
||||
sora_image_price_540: null as number | null,
|
||||
sora_video_price_per_request: null as number | null,
|
||||
sora_video_price_per_request_hd: null as number | null,
|
||||
// Claude Code 客户端限制(仅 anthropic 平台使用)
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null as number | null,
|
||||
@@ -1495,6 +1628,10 @@ const closeCreateModal = () => {
|
||||
createForm.image_price_1k = null
|
||||
createForm.image_price_2k = null
|
||||
createForm.image_price_4k = null
|
||||
createForm.sora_image_price_360 = null
|
||||
createForm.sora_image_price_540 = null
|
||||
createForm.sora_video_price_per_request = null
|
||||
createForm.sora_video_price_per_request_hd = null
|
||||
createForm.claude_code_only = false
|
||||
createForm.fallback_group_id = null
|
||||
createModelRoutingRules.value = []
|
||||
@@ -1544,6 +1681,10 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
editForm.image_price_1k = group.image_price_1k
|
||||
editForm.image_price_2k = group.image_price_2k
|
||||
editForm.image_price_4k = group.image_price_4k
|
||||
editForm.sora_image_price_360 = group.sora_image_price_360
|
||||
editForm.sora_image_price_540 = group.sora_image_price_540
|
||||
editForm.sora_video_price_per_request = group.sora_video_price_per_request
|
||||
editForm.sora_video_price_per_request_hd = group.sora_video_price_per_request_hd
|
||||
editForm.claude_code_only = group.claude_code_only || false
|
||||
editForm.fallback_group_id = group.fallback_group_id
|
||||
editForm.model_routing_enabled = group.model_routing_enabled || false
|
||||
|
||||
Reference in New Issue
Block a user