feat(channels): 模型定价支持一键同步最新模型

从 LiteLLM 定价目录中读取指定平台的最新模型列表,
将尚未录入的模型以新定价条目(价格留空)的形式追加,
管理员只需点击同步最新模型按钮即可完成操作。

- backend/service: PricingService 新增 ListModelNamesByProvider
- backend/handler: ChannelHandler 新增 SyncPricingModels (GET /api/v1/admin/channels/pricing/sync-models)
- backend/routes: 注册新路由(在 /:id 通配符之前)
- backend/wire_gen: 手动更新 NewChannelHandler 调用
- frontend/api: channels.ts 新增 syncPricingModels
- frontend/i18n: zh.ts / en.ts 新增 5 个 key
- frontend/view: ChannelsView 定价区域标题行新增「同步最新模型」按钮
- tests: pricing_service_test + channel_handler_test 新增单元测试
This commit is contained in:
wucm667 committed 2026-05-19 20:32:32 +08:00
1 parent 8927ab091e
commit 92ad68a314
10 files changed
+252 -12

No files matched your search

+51 -3
View File
@@ -406,9 +406,19 @@
<div>
<div class="mb-1 flex items-center justify-between">
<label class="input-label text-xs mb-0">{{ t('admin.channels.form.modelPricing', 'Model Pricing') }}</label>
<button type="button" @click="addPricingEntry(sIdx)" class="text-xs text-primary-600 hover:text-primary-700">
+ {{ t('common.add', 'Add') }}
</button>
<div class="flex items-center gap-2">
<button
type="button"
@click="syncLatestModels(sIdx)"
:disabled="syncingPlatform === section.platform"
class="text-xs text-gray-500 hover:text-primary-600 disabled:opacity-50"
>
{{ syncingPlatform === section.platform ? t('admin.channels.form.syncingModels') : t('admin.channels.form.syncLatestModels') }}
</button>
<button type="button" @click="addPricingEntry(sIdx)" class="text-xs text-primary-600 hover:text-primary-700">
+ {{ t('common.add', 'Add') }}
</button>
</div>
</div>
<div
v-if="section.model_pricing.length === 0"
@@ -834,6 +844,44 @@ function addPricingEntry(sectionIdx: number) {
})
}
const syncingPlatform = ref<string | null>(null)
async function syncLatestModels(sectionIdx: number) {
const platform = form.platforms[sectionIdx].platform
if (syncingPlatform.value) return
syncingPlatform.value = platform
try {
const result = await adminAPI.channels.syncPricingModels(platform)
// Collect all model names already present in this platform's pricing entries
const existingModels = new Set<string>()
for (const entry of form.platforms[sectionIdx].model_pricing) {
for (const m of entry.models) existingModels.add(m)
}
const newModels = result.models.filter(m => !existingModels.has(m))
if (newModels.length === 0) {
appStore.showSuccess(t('admin.channels.form.syncModelsAlreadyUpToDate'))
return
}
// Add new models as a single new pricing entry (user fills in prices)
form.platforms[sectionIdx].model_pricing.push({
models: newModels,
billing_mode: 'token',
input_price: null,
output_price: null,
cache_write_price: null,
cache_read_price: null,
image_output_price: null,
per_request_price: null,
intervals: []
})
appStore.showSuccess(t('admin.channels.form.syncModelsSuccess', { count: newModels.length }))
} catch (error) {
appStore.showError(extractApiErrorMessage(error, t('admin.channels.form.syncModelsError')))
} finally {
syncingPlatform.value = null
}
}
function updatePricingEntry(sectionIdx: number, idx: number, updated: PricingFormEntry) {
form.platforms[sectionIdx].model_pricing.splice(idx, 1, updated)
}