mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-14 23:20:19 +08:00
fix(frontend): hide pricing source from model list
This commit is contained in:
@@ -88,9 +88,6 @@
|
|||||||
<TableHead class="w-[160px] text-center">
|
<TableHead class="w-[160px] text-center">
|
||||||
价格 ($/M)
|
价格 ($/M)
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[170px] text-center">
|
|
||||||
{{ t('models.pricingSource.label') }}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="w-[80px] text-center">
|
<TableHead class="w-[80px] text-center">
|
||||||
提供商
|
提供商
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -108,7 +105,7 @@
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="loading">
|
<TableRow v-if="loading">
|
||||||
<TableCell
|
<TableCell
|
||||||
colspan="7"
|
colspan="6"
|
||||||
class="text-center py-8"
|
class="text-center py-8"
|
||||||
>
|
>
|
||||||
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
|
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
|
||||||
@@ -116,7 +113,7 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-else-if="filteredGlobalModels.length === 0">
|
<TableRow v-else-if="filteredGlobalModels.length === 0">
|
||||||
<TableCell
|
<TableCell
|
||||||
colspan="7"
|
colspan="6"
|
||||||
class="text-center py-8 text-muted-foreground"
|
class="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
没有找到匹配的模型
|
没有找到匹配的模型
|
||||||
@@ -196,24 +193,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
|
||||||
class="text-center"
|
|
||||||
@mousedown.stop
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<GlobalModelPricingSourceSelect
|
|
||||||
class="mx-auto max-w-[165px]"
|
|
||||||
:model-id="model.id"
|
|
||||||
:source="getModelPricingSource(model)"
|
|
||||||
:candidates="getModelPricingCandidates(model)"
|
|
||||||
:loading="batchManageOnlineLoading"
|
|
||||||
:syncing="isModelPricingSourceSyncing(model.id)"
|
|
||||||
:local-only="isModelPricingSourceLocalOnly(model)"
|
|
||||||
@open="handleModelPricingSourceOpen(model, $event)"
|
|
||||||
@select="syncModelPricingSource(model, $event)"
|
|
||||||
@resync="resyncModelPricingSource(model)"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="text-center">
|
<TableCell class="text-center">
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{{ model.active_provider_count || 0 }}/{{ model.provider_count || 0 }}
|
{{ model.active_provider_count || 0 }}/{{ model.provider_count || 0 }}
|
||||||
@@ -358,26 +337,6 @@
|
|||||||
${{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}/${{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}
|
${{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}/${{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
@mousedown.stop
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span class="shrink-0 text-xs text-muted-foreground">{{ t('models.pricingSource.label') }}</span>
|
|
||||||
<GlobalModelPricingSourceSelect
|
|
||||||
class="min-w-0 flex-1"
|
|
||||||
:model-id="model.id"
|
|
||||||
:source="getModelPricingSource(model)"
|
|
||||||
:candidates="getModelPricingCandidates(model)"
|
|
||||||
:loading="batchManageOnlineLoading"
|
|
||||||
:syncing="isModelPricingSourceSyncing(model.id)"
|
|
||||||
:local-only="isModelPricingSourceLocalOnly(model)"
|
|
||||||
@open="handleModelPricingSourceOpen(model, $event)"
|
|
||||||
@select="syncModelPricingSource(model, $event)"
|
|
||||||
@resync="resyncModelPricingSource(model)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -782,7 +741,6 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
|
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
|
||||||
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
|
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
|
||||||
import GlobalModelPricingSourceSelect from '@/features/models/components/GlobalModelPricingSourceSelect.vue'
|
|
||||||
import ExternalModelsAccessControl from '@/features/models/components/ExternalModelsAccessControl.vue'
|
import ExternalModelsAccessControl from '@/features/models/components/ExternalModelsAccessControl.vue'
|
||||||
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
||||||
import type { Model } from '@/api/endpoints'
|
import type { Model } from '@/api/endpoints'
|
||||||
@@ -889,7 +847,6 @@ const batchManageModels = ref<GlobalModelResponse[]>([])
|
|||||||
const batchManageLoading = ref(false)
|
const batchManageLoading = ref(false)
|
||||||
const batchManageOnlineModels = ref<ModelsDevModelItem[]>([])
|
const batchManageOnlineModels = ref<ModelsDevModelItem[]>([])
|
||||||
const batchManageOnlineLoading = ref(false)
|
const batchManageOnlineLoading = ref(false)
|
||||||
const modelPricingSourceSyncingIds = ref<Set<string>>(new Set())
|
|
||||||
const GLOBAL_MODELS_BATCH_FETCH_PAGE_SIZE = 1000
|
const GLOBAL_MODELS_BATCH_FETCH_PAGE_SIZE = 1000
|
||||||
let globalModelsRequestId = 0
|
let globalModelsRequestId = 0
|
||||||
let modelSelectionRequestId = 0
|
let modelSelectionRequestId = 0
|
||||||
@@ -1508,53 +1465,6 @@ const filteredBatchManageModels = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const onlinePricingCandidatesByModelName = computed(() => {
|
|
||||||
const candidatesByName = new Map<string, ModelsDevModelItem[]>()
|
|
||||||
for (const onlineModel of batchManageOnlineModels.value) {
|
|
||||||
const normalizedName = onlineModel.modelId.trim().toLowerCase()
|
|
||||||
const existing = candidatesByName.get(normalizedName) ?? []
|
|
||||||
const normalizedProviderId = onlineModel.providerId.trim().toLowerCase()
|
|
||||||
if (!existing.some(candidate => (
|
|
||||||
candidate.providerId.trim().toLowerCase() === normalizedProviderId
|
|
||||||
))) {
|
|
||||||
existing.push(onlineModel)
|
|
||||||
candidatesByName.set(normalizedName, existing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const candidates of candidatesByName.values()) {
|
|
||||||
candidates.sort((left, right) => (
|
|
||||||
Number(right.official === true) - Number(left.official === true)
|
|
||||||
|| left.providerName.localeCompare(right.providerName)
|
|
||||||
|| left.providerId.localeCompare(right.providerId)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return candidatesByName
|
|
||||||
})
|
|
||||||
|
|
||||||
function getModelPricingCandidates(model: GlobalModelResponse): ModelsDevModelItem[] {
|
|
||||||
return onlinePricingCandidatesByModelName.value.get(model.name.trim().toLowerCase()) ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelPricingSource(model: GlobalModelResponse) {
|
|
||||||
return getModelsDevPricingSource(model.id, model.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isModelPricingSourceLocalOnly(model: GlobalModelResponse): boolean {
|
|
||||||
return !getModelsDevPricingSourceFromConfig(model.config)
|
|
||||||
&& !!getLocalModelsDevPricingSource(model.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isModelPricingSourceSyncing(modelId: string): boolean {
|
|
||||||
return modelPricingSourceSyncingIds.value.has(modelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModelPricingSourceSyncing(modelId: string, syncing: boolean) {
|
|
||||||
const nextIds = new Set(modelPricingSourceSyncingIds.value)
|
|
||||||
if (syncing) nextIds.add(modelId)
|
|
||||||
else nextIds.delete(modelId)
|
|
||||||
modelPricingSourceSyncingIds.value = nextIds
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyGlobalModelUpdate(updatedModel: GlobalModelResponse) {
|
function applyGlobalModelUpdate(updatedModel: GlobalModelResponse) {
|
||||||
for (const models of [globalModels.value, batchManageModels.value]) {
|
for (const models of [globalModels.value, batchManageModels.value]) {
|
||||||
const current = models.find(model => model.id === updatedModel.id)
|
const current = models.find(model => model.id === updatedModel.id)
|
||||||
@@ -1580,7 +1490,6 @@ function migrateLegacyModelsDevPricingSources(models: GlobalModelResponse[]) {
|
|||||||
}
|
}
|
||||||
pricingSourceMigrationAttemptedIds.add(model.id)
|
pricingSourceMigrationAttemptedIds.add(model.id)
|
||||||
return [async () => {
|
return [async () => {
|
||||||
setModelPricingSourceSyncing(model.id, true)
|
|
||||||
const nextConfig = withModelsDevPricingSource(model.config, localSource)
|
const nextConfig = withModelsDevPricingSource(model.config, localSource)
|
||||||
try {
|
try {
|
||||||
const updatedModel = await updateGlobalModel(model.id, { config: nextConfig })
|
const updatedModel = await updateGlobalModel(model.id, { config: nextConfig })
|
||||||
@@ -1588,84 +1497,12 @@ function migrateLegacyModelsDevPricingSources(models: GlobalModelResponse[]) {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
pricingSourceMigrationAttemptedIds.delete(model.id)
|
pricingSourceMigrationAttemptedIds.delete(model.id)
|
||||||
log.warn('迁移本地模型价格来源失败:', err)
|
log.warn('迁移本地模型价格来源失败:', err)
|
||||||
} finally {
|
|
||||||
setModelPricingSourceSyncing(model.id, false)
|
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
if (tasks.length > 0) void runBatchTasksWithConcurrency(tasks, 4)
|
if (tasks.length > 0) void runBatchTasksWithConcurrency(tasks, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleModelPricingSourceOpen(_model: GlobalModelResponse, open: boolean) {
|
|
||||||
if (open) await loadBatchManageOnlineModels()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncModelPricingSource(model: GlobalModelResponse, providerId: string) {
|
|
||||||
if (!providerId || providerId.startsWith('__') || isModelPricingSourceSyncing(model.id)) return
|
|
||||||
if (batchManageOnlineModels.value.length === 0) await loadBatchManageOnlineModels()
|
|
||||||
const normalizedProviderId = providerId.trim().toLowerCase()
|
|
||||||
const candidate = getModelPricingCandidates(model).find(item => (
|
|
||||||
item.providerId.trim().toLowerCase() === normalizedProviderId
|
|
||||||
))
|
|
||||||
if (!candidate) {
|
|
||||||
showError(
|
|
||||||
t('models.pricingSource.sourceGone'),
|
|
||||||
t('models.pricingSource.cannotSync'),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (candidate.pricingUnsupportedFields?.length) {
|
|
||||||
showError(
|
|
||||||
t('models.pricingSource.unsupported', { fields: formatBatchUnsupportedPricingFields(candidate) }),
|
|
||||||
t('models.pricingSource.incompatible'),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!candidate.tieredPricing?.tiers?.length) {
|
|
||||||
showError(
|
|
||||||
t('models.pricingSource.noUsablePrice'),
|
|
||||||
t('models.pricingSource.cannotSync'),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = {
|
|
||||||
provider_id: candidate.providerId,
|
|
||||||
provider_name: candidate.providerName,
|
|
||||||
}
|
|
||||||
const pricing = cloneTieredPricingConfig(candidate.tieredPricing)
|
|
||||||
const nextConfig = withModelsDevPricingSource(model.config, source)
|
|
||||||
setModelPricingSourceSyncing(model.id, true)
|
|
||||||
try {
|
|
||||||
const updatedModel = await updateGlobalModel(model.id, {
|
|
||||||
default_tiered_pricing: pricing,
|
|
||||||
config: nextConfig,
|
|
||||||
})
|
|
||||||
setModelsDevPricingSource(model.id, source)
|
|
||||||
applyGlobalModelUpdate({
|
|
||||||
...updatedModel,
|
|
||||||
default_tiered_pricing: pricing,
|
|
||||||
config: nextConfig,
|
|
||||||
})
|
|
||||||
success(t('models.pricingSource.selectedAndSynced', { provider: candidate.providerName }))
|
|
||||||
} catch (err: unknown) {
|
|
||||||
log.error('更新模型价格来源失败:', err)
|
|
||||||
showError(
|
|
||||||
parseApiError(err, t('models.pricingSource.updateFailed')),
|
|
||||||
t('models.pricingSource.syncFailed'),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
setModelPricingSourceSyncing(model.id, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resyncModelPricingSource(model: GlobalModelResponse) {
|
|
||||||
const source = getModelPricingSource(model)
|
|
||||||
if (!source) return
|
|
||||||
await loadBatchManageOnlineModels()
|
|
||||||
await syncModelPricingSource(model, source.provider_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchPricingProviderOptions = computed(() => {
|
const batchPricingProviderOptions = computed(() => {
|
||||||
const existingModelNames = new Set(batchManageModels.value.map(model => model.name.trim().toLowerCase()))
|
const existingModelNames = new Set(batchManageModels.value.map(model => model.name.trim().toLowerCase()))
|
||||||
const providers = new Map<string, {
|
const providers = new Map<string, {
|
||||||
|
|||||||
@@ -248,20 +248,20 @@ describe('ModelManagement pricing-source workflow', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the new source and selection controls in English', async () => {
|
it('does not render the pricing source in the model list', async () => {
|
||||||
mountView()
|
mountView()
|
||||||
await settle()
|
await settle()
|
||||||
setI18nLocale('en-US')
|
setI18nLocale('en-US')
|
||||||
await settle()
|
await settle()
|
||||||
|
|
||||||
expect(document.body.textContent).toContain('Online pricing source')
|
expect(document.body.textContent).not.toContain('Online pricing source')
|
||||||
expect(document.body.textContent).toContain('Batch manage')
|
expect(document.body.textContent).toContain('Batch manage')
|
||||||
expect(document.body.querySelector(
|
expect(document.body.querySelector(
|
||||||
'[aria-label="Select model Test Model"]',
|
'[aria-label="Select model Test Model"]',
|
||||||
)).not.toBeNull()
|
)).not.toBeNull()
|
||||||
expect(document.body.querySelector(
|
expect(document.body.querySelector(
|
||||||
'[data-testid="model-pricing-source-model-1"]',
|
'[data-testid="model-pricing-source-model-1"]',
|
||||||
)?.getAttribute('aria-label')).toContain('Current source: Old OpenAI label')
|
)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('migrates a legacy browser-only source into the model database config', async () => {
|
it('migrates a legacy browser-only source into the model database config', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user