mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(billing): support image output pricing in model forms and details
This commit is contained in:
@@ -844,18 +844,24 @@ function loadModelData() {
|
|||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
expandedProvider.value = null
|
expandedProvider.value = null
|
||||||
|
|
||||||
|
const modelTieredPricing = props.model.default_tiered_pricing
|
||||||
|
? JSON.parse(JSON.stringify(props.model.default_tiered_pricing))
|
||||||
|
: null
|
||||||
|
const supportedCapabilities = new Set(props.model.supported_capabilities || [])
|
||||||
|
if (tieredPricingHasImageOutputPricing(modelTieredPricing)) {
|
||||||
|
supportedCapabilities.add('image_generation')
|
||||||
|
}
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
name: props.model.name,
|
name: props.model.name,
|
||||||
display_name: props.model.display_name,
|
display_name: props.model.display_name,
|
||||||
default_price_per_request: props.model.default_price_per_request,
|
default_price_per_request: props.model.default_price_per_request,
|
||||||
supported_capabilities: [...(props.model.supported_capabilities || [])],
|
supported_capabilities: [...supportedCapabilities],
|
||||||
config: props.model.config ? { ...props.model.config } : { streaming: true },
|
config: props.model.config ? { ...props.model.config } : { streaming: true },
|
||||||
is_active: props.model.is_active,
|
is_active: props.model.is_active,
|
||||||
}
|
}
|
||||||
// 确保 tieredPricing 也被正确设置或重置
|
// 确保 tieredPricing 也被正确设置或重置
|
||||||
tieredPricing.value = props.model.default_tiered_pricing
|
tieredPricing.value = modelTieredPricing
|
||||||
? JSON.parse(JSON.stringify(props.model.default_tiered_pricing))
|
|
||||||
: null
|
|
||||||
loadVideoPricingFromConfig()
|
loadVideoPricingFromConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,6 +902,9 @@ async function handleSubmit() {
|
|||||||
} else {
|
} else {
|
||||||
caps.delete('cache_1h')
|
caps.delete('cache_1h')
|
||||||
}
|
}
|
||||||
|
if (tieredPricingHasImageOutputPricing(finalTieredPricing)) {
|
||||||
|
caps.add('image_generation')
|
||||||
|
}
|
||||||
form.value.supported_capabilities = caps.size > 0 ? [...caps] : []
|
form.value.supported_capabilities = caps.size > 0 ? [...caps] : []
|
||||||
|
|
||||||
// 清理空的 config
|
// 清理空的 config
|
||||||
@@ -925,4 +934,13 @@ async function handleSubmit() {
|
|||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tieredPricingHasImageOutputPricing(pricing: TieredPricingConfig | null | undefined): boolean {
|
||||||
|
if (!pricing) return false
|
||||||
|
if (pricing.image_output_price_default != null) return true
|
||||||
|
return Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||||
|
if (!prices || typeof prices !== 'object') return false
|
||||||
|
return Object.values(prices).some((price) => typeof price === 'number' && Number.isFinite(price))
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 默认定价 -->
|
<!-- 默认定价 -->
|
||||||
@@ -145,6 +146,68 @@
|
|||||||
默认定价
|
默认定价
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
<!-- 图片输出计费 -->
|
||||||
|
<div
|
||||||
|
v-if="hasImagePricing"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-sm text-muted-foreground">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>图片输出计费</span>
|
||||||
|
<Badge
|
||||||
|
v-if="imagePricingEntries.length > 0"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] h-5 px-1.5"
|
||||||
|
>
|
||||||
|
矩阵
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="imageOutputDefaultPrice !== null"
|
||||||
|
class="text-xs font-mono"
|
||||||
|
>默认 ${{ imageOutputDefaultPrice.toFixed(6) }}/张</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="imagePricingEntries.length > 0"
|
||||||
|
class="border rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow class="bg-muted/30">
|
||||||
|
<TableHead class="text-xs h-9">
|
||||||
|
分辨率
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||||
|
:key="quality"
|
||||||
|
class="text-xs h-9 text-right"
|
||||||
|
>
|
||||||
|
{{ quality }}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow
|
||||||
|
v-for="entry in imagePricingEntries"
|
||||||
|
:key="entry.size"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
<TableCell class="py-2 font-mono">
|
||||||
|
{{ formatImageSize(entry.size) }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||||
|
:key="`${entry.size}-${quality}`"
|
||||||
|
class="py-2 text-right font-mono"
|
||||||
|
>
|
||||||
|
{{ formatImagePrice(entry.prices[quality]) }}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 单阶梯(固定价格)展示 -->
|
<!-- 单阶梯(固定价格)展示 -->
|
||||||
<div
|
<div
|
||||||
v-if="getTierCount(model.default_tiered_pricing) <= 1"
|
v-if="getTierCount(model.default_tiered_pricing) <= 1"
|
||||||
@@ -561,6 +624,47 @@ const videoPricingEntries = computed(() => {
|
|||||||
return sortResolutionEntries(Object.entries(priceByResolution))
|
return sortResolutionEntries(Object.entries(priceByResolution))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const IMAGE_OUTPUT_QUALITIES = ['low', 'medium', 'high'] as const
|
||||||
|
|
||||||
|
const imageOutputDefaultPrice = computed(() => {
|
||||||
|
const value = props.model?.default_tiered_pricing?.image_output_price_default
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const imagePricingEntries = computed(() => {
|
||||||
|
const prices = props.model?.default_tiered_pricing?.image_output_prices
|
||||||
|
if (!prices || typeof prices !== 'object') return []
|
||||||
|
return sortResolutionEntries(Object.entries(prices)).map(([size, qualityPrices]) => ({
|
||||||
|
size,
|
||||||
|
prices: normalizeImageQualityPrices(qualityPrices),
|
||||||
|
})).filter(entry => Object.values(entry.prices).some(price => price !== null))
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasImagePricing = computed(() =>
|
||||||
|
imageOutputDefaultPrice.value !== null || imagePricingEntries.value.length > 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
function normalizeImageQualityPrices(value: unknown): Record<typeof IMAGE_OUTPUT_QUALITIES[number], number | null> {
|
||||||
|
const object = value && typeof value === 'object' ? value as Record<string, unknown> : {}
|
||||||
|
return {
|
||||||
|
low: toFiniteNumber(object.low),
|
||||||
|
medium: toFiniteNumber(object.medium),
|
||||||
|
high: toFiniteNumber(object.high),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFiniteNumber(value: unknown): number | null {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatImagePrice(value: number | null): string {
|
||||||
|
return value === null ? '-' : `$${value.toFixed(6)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatImageSize(value: string): string {
|
||||||
|
return value.replace(/\s*[xX×]\s*/g, ' x ')
|
||||||
|
}
|
||||||
|
|
||||||
const detailTab = ref('basic')
|
const detailTab = ref('basic')
|
||||||
|
|
||||||
// 处理背景点击
|
// 处理背景点击
|
||||||
|
|||||||
@@ -158,30 +158,55 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="grid grid-cols-[96px_repeat(3,minmax(0,1fr))] gap-2 text-xs text-muted-foreground">
|
<div class="grid grid-cols-[minmax(120px,1.1fr)_repeat(3,minmax(0,1fr))_32px] gap-2 text-xs text-muted-foreground">
|
||||||
<span />
|
<span>分辨率</span>
|
||||||
<span>low</span>
|
<span>low</span>
|
||||||
<span>medium</span>
|
<span>medium</span>
|
||||||
<span>high</span>
|
<span>high</span>
|
||||||
|
<span />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="size in IMAGE_OUTPUT_SIZES"
|
v-for="row in imageOutputPriceRows"
|
||||||
:key="size.value"
|
:key="row.id"
|
||||||
class="grid grid-cols-[96px_repeat(3,minmax(0,1fr))] gap-2 items-center"
|
class="grid grid-cols-[minmax(120px,1.1fr)_repeat(3,minmax(0,1fr))_32px] gap-2 items-center"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-muted-foreground">{{ size.label }}</span>
|
<Input
|
||||||
|
:model-value="row.size"
|
||||||
|
class="h-8 font-mono text-xs"
|
||||||
|
placeholder="1024x1024"
|
||||||
|
@update:model-value="(v) => updateImageOutputSize(row.id, v)"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||||
:key="`${size.value}-${quality}`"
|
:key="`${row.id}-${quality}`"
|
||||||
:model-value="getImageOutputPrice(size.value, quality)"
|
:model-value="getImageOutputPrice(row, quality)"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.001"
|
step="0.001"
|
||||||
min="0"
|
min="0"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
@update:model-value="(v) => updateImageOutputPrice(size.value, quality, v)"
|
@update:model-value="(v) => updateImageOutputPrice(row.id, quality, v)"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
@click="removeImageOutputSizeRow(row.id)"
|
||||||
|
>
|
||||||
|
<X class="w-4 h-4 text-muted-foreground hover:text-destructive" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="w-full"
|
||||||
|
@click="addImageOutputSizeRow"
|
||||||
|
>
|
||||||
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
|
添加分辨率
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,13 +227,13 @@ import { Button, Input, Label } from '@/components/ui'
|
|||||||
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
||||||
|
|
||||||
type ImageOutputQuality = 'low' | 'medium' | 'high'
|
type ImageOutputQuality = 'low' | 'medium' | 'high'
|
||||||
|
type ImageOutputPriceRow = {
|
||||||
|
id: string
|
||||||
|
size: string
|
||||||
|
prices: Partial<Record<ImageOutputQuality, number>>
|
||||||
|
}
|
||||||
|
|
||||||
const IMAGE_OUTPUT_SIZES = [
|
const DEFAULT_IMAGE_OUTPUT_SIZES = ['1024x1024', '1536x1024', '1024x1536']
|
||||||
{ value: '1024x1024', label: '1024 x 1024' },
|
|
||||||
{ value: '1536x1024', label: '1536 x 1024' },
|
|
||||||
{ value: '1024x1536', label: '1024 x 1536' },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const IMAGE_OUTPUT_QUALITIES: ImageOutputQuality[] = ['low', 'medium', 'high']
|
const IMAGE_OUTPUT_QUALITIES: ImageOutputQuality[] = ['low', 'medium', 'high']
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -223,8 +248,10 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
// 本地状态
|
// 本地状态
|
||||||
const localTiers = ref<PricingTier[]>([])
|
const localTiers = ref<PricingTier[]>([])
|
||||||
const imageOutputPrices = ref<Record<string, Record<string, number | undefined>>>({})
|
const imageOutputPriceRows = ref<ImageOutputPriceRow[]>([])
|
||||||
const imageOutputPriceDefault = ref<string>('')
|
const imageOutputPriceDefault = ref<string>('')
|
||||||
|
const lastEmittedPricingJson = ref<string>('')
|
||||||
|
let imageOutputPriceRowId = 0
|
||||||
|
|
||||||
// 跟踪每个阶梯的缓存价格是否被手动设置
|
// 跟踪每个阶梯的缓存价格是否被手动设置
|
||||||
const cacheManuallySet = reactive<Record<number, { creation: boolean; read: boolean; cache1h: boolean }>>({})
|
const cacheManuallySet = reactive<Record<number, { creation: boolean; read: boolean; cache1h: boolean }>>({})
|
||||||
@@ -247,9 +274,12 @@ const customInputValue = reactive<Record<number, string>>({})
|
|||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
|
if (lastEmittedPricingJson.value && JSON.stringify(newValue ?? null) === lastEmittedPricingJson.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (newValue?.tiers) {
|
if (newValue?.tiers) {
|
||||||
localTiers.value = newValue.tiers.map(t => ({ ...t }))
|
localTiers.value = newValue.tiers.map(t => ({ ...t }))
|
||||||
imageOutputPrices.value = cloneImageOutputPrices(newValue.image_output_prices)
|
imageOutputPriceRows.value = createImageOutputPriceRows(newValue.image_output_prices)
|
||||||
imageOutputPriceDefault.value = newValue.image_output_price_default != null
|
imageOutputPriceDefault.value = newValue.image_output_price_default != null
|
||||||
? String(newValue.image_output_price_default)
|
? String(newValue.image_output_price_default)
|
||||||
: ''
|
: ''
|
||||||
@@ -268,7 +298,7 @@ watch(
|
|||||||
input_price_per_1m: 0,
|
input_price_per_1m: 0,
|
||||||
output_price_per_1m: 0,
|
output_price_per_1m: 0,
|
||||||
}]
|
}]
|
||||||
imageOutputPrices.value = {}
|
imageOutputPriceRows.value = createImageOutputPriceRows(null)
|
||||||
imageOutputPriceDefault.value = ''
|
imageOutputPriceDefault.value = ''
|
||||||
cacheManuallySet[0] = { creation: false, read: false, cache1h: false }
|
cacheManuallySet[0] = { creation: false, read: false, cache1h: false }
|
||||||
}
|
}
|
||||||
@@ -434,7 +464,9 @@ function syncToParent() {
|
|||||||
return tier
|
return tier
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', buildPricingConfig(tiers))
|
const value = buildPricingConfig(tiers)
|
||||||
|
lastEmittedPricingJson.value = JSON.stringify(value ?? null)
|
||||||
|
emit('update:modelValue', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最终提交的数据(包含自动计算的缓存价格)
|
// 获取最终提交的数据(包含自动计算的缓存价格)
|
||||||
@@ -499,26 +531,45 @@ function buildPricingConfig(tiers: PricingTier[]): TieredPricingConfig {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneImageOutputPrices(value: TieredPricingConfig['image_output_prices']): Record<string, Record<string, number | undefined>> {
|
function createImageOutputPriceRows(value: TieredPricingConfig['image_output_prices']): ImageOutputPriceRow[] {
|
||||||
const out: Record<string, Record<string, number | undefined>> = {}
|
const rows: ImageOutputPriceRow[] = []
|
||||||
if (!value || typeof value !== 'object') return out
|
if (!value || typeof value !== 'object') {
|
||||||
|
return DEFAULT_IMAGE_OUTPUT_SIZES.map(size => createImageOutputPriceRow(size))
|
||||||
|
}
|
||||||
for (const [size, prices] of Object.entries(value)) {
|
for (const [size, prices] of Object.entries(value)) {
|
||||||
if (!prices || typeof prices !== 'object') continue
|
if (!prices || typeof prices !== 'object') continue
|
||||||
|
const rowPrices: Partial<Record<ImageOutputQuality, number>> = {}
|
||||||
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
||||||
const price = (prices as Record<string, unknown>)[quality]
|
const price = (prices as Record<string, unknown>)[quality]
|
||||||
if (typeof price === 'number' && Number.isFinite(price)) {
|
if (typeof price === 'number' && Number.isFinite(price)) {
|
||||||
out[size] = { ...(out[size] || {}), [quality]: price }
|
rowPrices[quality] = price
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rows.push(createImageOutputPriceRow(size, rowPrices))
|
||||||
|
}
|
||||||
|
if (rows.length > 0) return rows
|
||||||
|
return DEFAULT_IMAGE_OUTPUT_SIZES.map(size => createImageOutputPriceRow(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImageOutputPriceRow(
|
||||||
|
size = '',
|
||||||
|
prices: Partial<Record<ImageOutputQuality, number>> = {},
|
||||||
|
): ImageOutputPriceRow {
|
||||||
|
imageOutputPriceRowId += 1
|
||||||
|
return {
|
||||||
|
id: `image-output-size-${imageOutputPriceRowId}`,
|
||||||
|
size,
|
||||||
|
prices: { ...prices },
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizedImageOutputPrices(): Record<string, Record<string, number>> {
|
function normalizedImageOutputPrices(): Record<string, Record<string, number>> {
|
||||||
const out: Record<string, Record<string, number>> = {}
|
const out: Record<string, Record<string, number>> = {}
|
||||||
for (const [size, prices] of Object.entries(imageOutputPrices.value)) {
|
for (const row of imageOutputPriceRows.value) {
|
||||||
|
const size = normalizeImageOutputSize(row.size)
|
||||||
|
if (!size) continue
|
||||||
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
||||||
const price = prices[quality]
|
const price = row.prices[quality]
|
||||||
if (price != null && Number.isFinite(price)) {
|
if (price != null && Number.isFinite(price)) {
|
||||||
out[size] = { ...(out[size] || {}), [quality]: price }
|
out[size] = { ...(out[size] || {}), [quality]: price }
|
||||||
}
|
}
|
||||||
@@ -533,25 +584,44 @@ function parseOptionalFloat(value: string | number): number | null {
|
|||||||
return Number.isFinite(number) ? number : null
|
return Number.isFinite(number) ? number : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageOutputPrice(size: string, quality: ImageOutputQuality): string | number {
|
function normalizeImageOutputSize(size: string): string {
|
||||||
return imageOutputPrices.value[size]?.[quality] ?? ''
|
return String(size || '').trim().replace(/\s*[xX×]\s*/g, 'x')
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateImageOutputPrice(size: string, quality: ImageOutputQuality, value: string | number) {
|
function getImageOutputPrice(row: ImageOutputPriceRow, quality: ImageOutputQuality): string | number {
|
||||||
|
return row.prices[quality] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImageOutputSize(rowId: string, value: string | number) {
|
||||||
|
const row = imageOutputPriceRows.value.find(item => item.id === rowId)
|
||||||
|
if (!row) return
|
||||||
|
row.size = normalizeImageOutputSize(String(value ?? ''))
|
||||||
|
imageOutputPriceRows.value = [...imageOutputPriceRows.value]
|
||||||
|
syncToParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImageOutputPrice(rowId: string, quality: ImageOutputQuality, value: string | number) {
|
||||||
|
const row = imageOutputPriceRows.value.find(item => item.id === rowId)
|
||||||
|
if (!row) return
|
||||||
const price = parseOptionalFloat(value)
|
const price = parseOptionalFloat(value)
|
||||||
const current = { ...(imageOutputPrices.value[size] || {}) }
|
|
||||||
if (price == null) {
|
if (price == null) {
|
||||||
delete current[quality]
|
delete row.prices[quality]
|
||||||
} else {
|
} else {
|
||||||
current[quality] = price
|
row.prices[quality] = price
|
||||||
}
|
|
||||||
if (Object.values(current).some(v => v != null)) {
|
|
||||||
imageOutputPrices.value = { ...imageOutputPrices.value, [size]: current }
|
|
||||||
} else {
|
|
||||||
const next = { ...imageOutputPrices.value }
|
|
||||||
delete next[size]
|
|
||||||
imageOutputPrices.value = next
|
|
||||||
}
|
}
|
||||||
|
imageOutputPriceRows.value = [...imageOutputPriceRows.value]
|
||||||
|
syncToParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addImageOutputSizeRow() {
|
||||||
|
const usedSizes = new Set(imageOutputPriceRows.value.map(row => normalizeImageOutputSize(row.size)).filter(Boolean))
|
||||||
|
const suggestedSize = DEFAULT_IMAGE_OUTPUT_SIZES.find(size => !usedSizes.has(size)) || ''
|
||||||
|
imageOutputPriceRows.value = [...imageOutputPriceRows.value, createImageOutputPriceRow(suggestedSize)]
|
||||||
|
syncToParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImageOutputSizeRow(rowId: string) {
|
||||||
|
imageOutputPriceRows.value = imageOutputPriceRows.value.filter(row => row.id !== rowId)
|
||||||
syncToParent()
|
syncToParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ watch(() => props.open, async (newOpen) => {
|
|||||||
// 编辑模式:填充表单
|
// 编辑模式:填充表单
|
||||||
// 使用有效配置(合并全局模型的默认值)供用户查看和编辑
|
// 使用有效配置(合并全局模型的默认值)供用户查看和编辑
|
||||||
const effectiveConfig = props.editingModel.effective_config || props.editingModel.config || {}
|
const effectiveConfig = props.editingModel.effective_config || props.editingModel.config || {}
|
||||||
|
const supportsImageGeneration = modelSupportsImageGeneration(props.editingModel)
|
||||||
form.value = {
|
form.value = {
|
||||||
global_model_id: props.editingModel.global_model_id || '',
|
global_model_id: props.editingModel.global_model_id || '',
|
||||||
provider_model_name: props.editingModel.provider_model_name || '',
|
provider_model_name: props.editingModel.provider_model_name || '',
|
||||||
@@ -450,7 +451,7 @@ watch(() => props.open, async (newOpen) => {
|
|||||||
supports_function_calling: props.editingModel.supports_function_calling ?? undefined,
|
supports_function_calling: props.editingModel.supports_function_calling ?? undefined,
|
||||||
supports_streaming: props.editingModel.supports_streaming ?? undefined,
|
supports_streaming: props.editingModel.supports_streaming ?? undefined,
|
||||||
supports_extended_thinking: props.editingModel.supports_extended_thinking ?? undefined,
|
supports_extended_thinking: props.editingModel.supports_extended_thinking ?? undefined,
|
||||||
supports_image_generation: props.editingModel.supports_image_generation ?? undefined,
|
supports_image_generation: supportsImageGeneration ? true : props.editingModel.supports_image_generation ?? undefined,
|
||||||
is_active: props.editingModel.is_active
|
is_active: props.editingModel.is_active
|
||||||
}
|
}
|
||||||
// 从有效配置中加载视频费用
|
// 从有效配置中加载视频费用
|
||||||
@@ -544,6 +545,9 @@ function modelSupportsImageGeneration(model: {
|
|||||||
supported_capabilities?: string[] | null
|
supported_capabilities?: string[] | null
|
||||||
supports_image_generation?: boolean | null
|
supports_image_generation?: boolean | null
|
||||||
effective_supports_image_generation?: boolean | null
|
effective_supports_image_generation?: boolean | null
|
||||||
|
default_tiered_pricing?: TieredPricingConfig | null
|
||||||
|
tiered_pricing?: TieredPricingConfig | null
|
||||||
|
effective_tiered_pricing?: TieredPricingConfig | null
|
||||||
config?: Record<string, unknown> | null
|
config?: Record<string, unknown> | null
|
||||||
} | null | undefined): boolean {
|
} | null | undefined): boolean {
|
||||||
if (!model) return false
|
if (!model) return false
|
||||||
@@ -554,6 +558,18 @@ function modelSupportsImageGeneration(model: {
|
|||||||
|| config.image_generation === true
|
|| config.image_generation === true
|
||||||
|| config.model_type === 'image'
|
|| config.model_type === 'image'
|
||||||
|| (Array.isArray(config.api_formats) && config.api_formats.some((format) => String(format).endsWith(':image')))
|
|| (Array.isArray(config.api_formats) && config.api_formats.some((format) => String(format).endsWith(':image')))
|
||||||
|
|| tieredPricingHasImageOutputPricing(model.default_tiered_pricing)
|
||||||
|
|| tieredPricingHasImageOutputPricing(model.tiered_pricing)
|
||||||
|
|| tieredPricingHasImageOutputPricing(model.effective_tiered_pricing)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tieredPricingHasImageOutputPricing(pricing: TieredPricingConfig | null | undefined): boolean {
|
||||||
|
if (!pricing) return false
|
||||||
|
if (pricing.image_output_price_default != null) return true
|
||||||
|
return Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||||
|
if (!prices || typeof prices !== 'object') return false
|
||||||
|
return Object.values(prices).some((price) => typeof price === 'number' && Number.isFinite(price))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setImageGenerationEnabled(value: boolean | 'indeterminate') {
|
function setImageGenerationEnabled(value: boolean | 'indeterminate') {
|
||||||
@@ -696,7 +712,11 @@ function _copyVideoPricingFromSelectedGlobal() {
|
|||||||
configTouched.value = true
|
configTouched.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createManualGlobalModel(finalTieredPricing: TieredPricingConfig | null, cleanConfig: Record<string, unknown> | undefined): Promise<GlobalModelResponse> {
|
async function createManualGlobalModel(
|
||||||
|
finalTieredPricing: TieredPricingConfig | null,
|
||||||
|
cleanConfig: Record<string, unknown> | undefined,
|
||||||
|
supportsImageGeneration: boolean,
|
||||||
|
): Promise<GlobalModelResponse> {
|
||||||
const modelName = form.value.manual_global_model_name.trim()
|
const modelName = form.value.manual_global_model_name.trim()
|
||||||
const displayName = form.value.manual_global_model_display_name.trim() || modelName
|
const displayName = form.value.manual_global_model_display_name.trim() || modelName
|
||||||
const supportedCapabilities = [
|
const supportedCapabilities = [
|
||||||
@@ -704,7 +724,7 @@ async function createManualGlobalModel(finalTieredPricing: TieredPricingConfig |
|
|||||||
form.value.supports_function_calling === true ? 'function_calling' : null,
|
form.value.supports_function_calling === true ? 'function_calling' : null,
|
||||||
form.value.supports_streaming === true ? 'streaming' : null,
|
form.value.supports_streaming === true ? 'streaming' : null,
|
||||||
form.value.supports_extended_thinking === true ? 'extended_thinking' : null,
|
form.value.supports_extended_thinking === true ? 'extended_thinking' : null,
|
||||||
form.value.supports_image_generation === true ? 'image_generation' : null,
|
supportsImageGeneration ? 'image_generation' : null,
|
||||||
].filter((capability): capability is string => capability !== null)
|
].filter((capability): capability is string => capability !== null)
|
||||||
|
|
||||||
return createGlobalModel({
|
return createGlobalModel({
|
||||||
@@ -763,6 +783,8 @@ async function handleSubmit() {
|
|||||||
try {
|
try {
|
||||||
// 获取包含自动计算缓存价格的最终数据
|
// 获取包含自动计算缓存价格的最终数据
|
||||||
const finalTieredPricing = tieredPricingEditorRef.value?.getFinalPricing() ?? tieredPricing.value
|
const finalTieredPricing = tieredPricingEditorRef.value?.getFinalPricing() ?? tieredPricing.value
|
||||||
|
const supportsImageGeneration = isImageGenerationEnabled.value
|
||||||
|
|| tieredPricingHasImageOutputPricing(finalTieredPricing)
|
||||||
|
|
||||||
// Apply billing (video) pricing into config.
|
// Apply billing (video) pricing into config.
|
||||||
applyVideoPricingToConfig(form.value.config)
|
applyVideoPricingToConfig(form.value.config)
|
||||||
@@ -781,14 +803,14 @@ async function handleSubmit() {
|
|||||||
supportsFunctionCalling: form.value.supports_function_calling,
|
supportsFunctionCalling: form.value.supports_function_calling,
|
||||||
supportsStreaming: form.value.supports_streaming,
|
supportsStreaming: form.value.supports_streaming,
|
||||||
supportsExtendedThinking: form.value.supports_extended_thinking,
|
supportsExtendedThinking: form.value.supports_extended_thinking,
|
||||||
supportsImageGeneration: form.value.supports_image_generation,
|
supportsImageGeneration,
|
||||||
isActive: form.value.is_active
|
isActive: form.value.is_active
|
||||||
}))
|
}))
|
||||||
showSuccess('模型配置已更新')
|
showSuccess('模型配置已更新')
|
||||||
} else {
|
} else {
|
||||||
// 添加模式:只有用户修改了配置才提交 tiered_pricing,否则保持继承关系
|
// 添加模式:只有用户修改了配置才提交 tiered_pricing,否则保持继承关系
|
||||||
const selectedModel = manualGlobalModelMode.value
|
const selectedModel = manualGlobalModelMode.value
|
||||||
? await createManualGlobalModel(finalTieredPricing, cleanConfig)
|
? await createManualGlobalModel(finalTieredPricing, cleanConfig, supportsImageGeneration)
|
||||||
: availableGlobalModels.value.find(m => m.id === form.value.global_model_id)
|
: availableGlobalModels.value.find(m => m.id === form.value.global_model_id)
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
showError('请选择模型,或切换到手动添加后填写模型ID', '错误')
|
showError('请选择模型,或切换到手动添加后填写模型ID', '错误')
|
||||||
@@ -806,7 +828,7 @@ async function handleSubmit() {
|
|||||||
supportsFunctionCalling: form.value.supports_function_calling,
|
supportsFunctionCalling: form.value.supports_function_calling,
|
||||||
supportsStreaming: form.value.supports_streaming,
|
supportsStreaming: form.value.supports_streaming,
|
||||||
supportsExtendedThinking: form.value.supports_extended_thinking,
|
supportsExtendedThinking: form.value.supports_extended_thinking,
|
||||||
supportsImageGeneration: form.value.supports_image_generation,
|
supportsImageGeneration,
|
||||||
isActive: form.value.is_active
|
isActive: form.value.is_active
|
||||||
}))
|
}))
|
||||||
showSuccess('模型已添加')
|
showSuccess('模型已添加')
|
||||||
|
|||||||
Reference in New Issue
Block a user