feat: image output token billing, channel-mapped billing source, credits balance precheck
- Parse candidatesTokensDetails from Gemini API to separate image/text output tokens
- Add image_output_tokens and image_output_cost to usage_log (migration 089)
- Support per-image-token pricing via output_cost_per_image_token from model pricing data
- Channel pricing ImageOutputPrice override works in token billing mode
- Auto-fill image_output_price in channel pricing form from model defaults
- Add "channel_mapped" billing model source as new default (migration 088)
- Bills by model name after channel mapping, before account mapping
- Fix channel cache error TTL sign error (115s → 5s)
- Fix Update channel only invalidating new groups, not removed groups
- Fix frontend model_mapping clearing sending undefined instead of {}
- Credits balance precheck via shared AccountUsageService cache before injection
- Skip credits injection for accounts with insufficient balance
- Don't mark credits exhausted for "exhausted your capacity on this model" 429s
This commit is contained in:
@@ -134,6 +134,7 @@ export interface ModelDefaultPricing {
|
||||
output_price?: number
|
||||
cache_write_price?: number
|
||||
cache_read_price?: number
|
||||
image_output_price?: number
|
||||
}
|
||||
|
||||
export async function getModelDefaultPricing(model: string): Promise<ModelDefaultPricing> {
|
||||
|
||||
@@ -328,6 +328,7 @@ async function onModelsUpdate(newModels: string[]) {
|
||||
output_price: perTokenToMTok(result.output_price ?? null),
|
||||
cache_write_price: perTokenToMTok(result.cache_write_price ?? null),
|
||||
cache_read_price: perTokenToMTok(result.cache_read_price ?? null),
|
||||
image_output_price: perTokenToMTok(result.image_output_price ?? null),
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1800,6 +1800,7 @@ export default {
|
||||
mappingSource: 'Source model',
|
||||
mappingTarget: 'Target model',
|
||||
billingModelSource: 'Billing Model',
|
||||
billingModelSourceChannelMapped: 'Bill by channel-mapped model',
|
||||
billingModelSourceRequested: 'Bill by requested model',
|
||||
billingModelSourceUpstream: 'Bill by final upstream model',
|
||||
billingModelSourceHint: 'Controls which model name is used for pricing lookup',
|
||||
|
||||
@@ -1880,6 +1880,7 @@ export default {
|
||||
mappingSource: '源模型',
|
||||
mappingTarget: '目标模型',
|
||||
billingModelSource: '计费基准',
|
||||
billingModelSourceChannelMapped: '以渠道映射后的模型计费',
|
||||
billingModelSourceRequested: '以请求模型计费',
|
||||
billingModelSourceUpstream: '以最终模型计费',
|
||||
billingModelSourceHint: '控制使用哪个模型名称进行定价查找',
|
||||
|
||||
@@ -471,6 +471,7 @@ const statusEditOptions = computed(() => [
|
||||
])
|
||||
|
||||
const billingModelSourceOptions = computed(() => [
|
||||
{ value: 'channel_mapped', label: t('admin.channels.form.billingModelSourceChannelMapped', 'Bill by channel-mapped model') },
|
||||
{ value: 'requested', label: t('admin.channels.form.billingModelSourceRequested', 'Bill by requested model') },
|
||||
{ value: 'upstream', label: t('admin.channels.form.billingModelSourceUpstream', 'Bill by final upstream model') }
|
||||
])
|
||||
@@ -504,7 +505,7 @@ const form = reactive({
|
||||
description: '',
|
||||
status: 'active',
|
||||
restrict_models: false,
|
||||
billing_model_source: 'requested' as string,
|
||||
billing_model_source: 'channel_mapped' as string,
|
||||
platforms: [] as PlatformSection[]
|
||||
})
|
||||
|
||||
@@ -819,7 +820,7 @@ function resetForm() {
|
||||
form.description = ''
|
||||
form.status = 'active'
|
||||
form.restrict_models = false
|
||||
form.billing_model_source = 'requested'
|
||||
form.billing_model_source = 'channel_mapped'
|
||||
form.platforms = []
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
@@ -837,7 +838,7 @@ async function openEditDialog(channel: Channel) {
|
||||
form.description = channel.description || ''
|
||||
form.status = channel.status
|
||||
form.restrict_models = channel.restrict_models || false
|
||||
form.billing_model_source = channel.billing_model_source || 'requested'
|
||||
form.billing_model_source = channel.billing_model_source || 'channel_mapped'
|
||||
// Must load groups first so apiToForm can map groupID → platform
|
||||
await loadGroups()
|
||||
form.platforms = apiToForm(channel)
|
||||
@@ -932,7 +933,7 @@ async function handleSubmit() {
|
||||
status: form.status,
|
||||
group_ids,
|
||||
model_pricing,
|
||||
model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : undefined,
|
||||
model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : {},
|
||||
billing_model_source: form.billing_model_source,
|
||||
restrict_models: form.restrict_models
|
||||
}
|
||||
@@ -944,7 +945,7 @@ async function handleSubmit() {
|
||||
description: form.description.trim() || undefined,
|
||||
group_ids,
|
||||
model_pricing,
|
||||
model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : undefined,
|
||||
model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : {},
|
||||
billing_model_source: form.billing_model_source,
|
||||
restrict_models: form.restrict_models
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user