mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 16:29:29 +00:00
feat(billing): 渠道自定义定价支持图片输入 token 单价 image_input_price
渠道 token 计费模式此前无法为图片输入 token 单独定价,gpt-image-2 图片编辑等请求的图像输入被按文本 input_price 计费。新增 channel_model_pricing.image_input_price 列及全链路支持。 后端: - 迁移 178:channel_model_pricing 新增 image_input_price 列 - ChannelModelPricing 新增 ImageInputPrice 字段,repo 读写、校验补齐 - model_pricing_resolver / GetModelPricingWithChannel 映射到 ImageInputPricePerToken;未配置时归零,由 computeTokenBreakdown 回退文本输入价(向后兼容,与 image_output_price 的渠道权威规则一致) - admin / 用户侧定价 DTO 与 model-pricing 自动填充接口补充该字段 前端: - 渠道定价表单新增「图片输入」价格输入(token 模式) - API 类型、表单模型、form↔API 换算、自动填充、用户侧模型定价卡展示 - zh/en i18n 标签 相关 #4386。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wcJTKDddxXSQrepSs3wrU
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7c43b7327d
commit
06e03f467a
@@ -30,6 +30,7 @@ export interface ChannelModelPricing {
|
||||
output_price: number | null
|
||||
cache_write_price: number | null
|
||||
cache_read_price: number | null
|
||||
image_input_price: number | null
|
||||
image_output_price: number | null
|
||||
per_request_price: number | null
|
||||
intervals: PricingInterval[]
|
||||
@@ -154,6 +155,7 @@ export interface ModelDefaultPricing {
|
||||
output_price?: number
|
||||
cache_write_price?: number
|
||||
cache_read_price?: number
|
||||
image_input_price?: number
|
||||
image_output_price?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface UserSupportedModelPricing {
|
||||
output_price: number | null
|
||||
cache_write_price: number | null
|
||||
cache_read_price: number | null
|
||||
image_input_price: number | null
|
||||
image_output_price: number | null
|
||||
per_request_price: number | null
|
||||
intervals: UserPricingInterval[]
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
{{ t('admin.channels.form.defaultPrices') }}
|
||||
<span class="ml-1 font-normal text-gray-400">$/MTok</span>
|
||||
</label>
|
||||
<div class="mt-1 grid grid-cols-2 gap-2 sm:grid-cols-5">
|
||||
<div class="mt-1 grid grid-cols-2 gap-2 sm:grid-cols-6">
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">{{ t('admin.channels.form.inputPrice') }}</label>
|
||||
<input :value="entry.input_price" @input="emitField('input_price', ($event.target as HTMLInputElement).value)"
|
||||
@@ -122,6 +122,11 @@
|
||||
<input :value="entry.cache_read_price" @input="emitField('cache_read_price', ($event.target as HTMLInputElement).value)"
|
||||
type="number" step="any" min="0" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">{{ t('admin.channels.form.imageInputPrice') }}</label>
|
||||
<input :value="entry.image_input_price" @input="emitField('image_input_price', ($event.target as HTMLInputElement).value)"
|
||||
type="number" step="any" min="0" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">{{ t('admin.channels.form.imageTokenPrice') }}</label>
|
||||
<input :value="entry.image_output_price" @input="emitField('image_output_price', ($event.target as HTMLInputElement).value)"
|
||||
@@ -328,6 +333,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_input_price: perTokenToMTok(result.image_input_price ?? null),
|
||||
image_output_price: perTokenToMTok(result.image_output_price ?? null),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface PricingFormEntry {
|
||||
output_price: number | string | null
|
||||
cache_write_price: number | string | null
|
||||
cache_read_price: number | string | null
|
||||
image_input_price: number | string | null
|
||||
image_output_price: number | string | null
|
||||
per_request_price: number | string | null
|
||||
intervals: IntervalFormEntry[]
|
||||
|
||||
@@ -90,6 +90,13 @@
|
||||
:unit="t(prefixKey('unitPerMillion'))"
|
||||
:scale="perMillionScale"
|
||||
/>
|
||||
<PricingRow
|
||||
v-if="model.pricing.image_input_price != null && model.pricing.image_input_price > 0"
|
||||
:label="t(prefixKey('imageInputPrice'))"
|
||||
:value="model.pricing.image_input_price"
|
||||
:unit="t(prefixKey('unitPerMillion'))"
|
||||
:scale="perMillionScale"
|
||||
/>
|
||||
<PricingRow
|
||||
v-if="model.pricing.image_output_price != null && model.pricing.image_output_price > 0"
|
||||
:label="t(prefixKey('imageOutputPrice'))"
|
||||
|
||||
@@ -116,6 +116,7 @@ export default {
|
||||
cacheReadPrice: 'Cache Read',
|
||||
cacheWritePriceShort: 'Cache W',
|
||||
cacheReadPriceShort: 'Cache R',
|
||||
imageInputPrice: 'Image Input',
|
||||
imageTokenPrice: 'Image Output',
|
||||
imageOutputPrice: 'Image Output Price',
|
||||
pricePlaceholder: 'Default',
|
||||
|
||||
@@ -499,6 +499,7 @@ export default {
|
||||
outputPrice: 'Output',
|
||||
cacheWritePrice: 'Cache Write',
|
||||
cacheReadPrice: 'Cache Read',
|
||||
imageInputPrice: 'Image Input',
|
||||
imageOutputPrice: 'Image Output',
|
||||
perRequestPrice: 'Per Request',
|
||||
intervals: 'Tiered Pricing',
|
||||
|
||||
@@ -116,6 +116,7 @@ export default {
|
||||
cacheReadPrice: '缓存读取',
|
||||
cacheWritePriceShort: '缓存写',
|
||||
cacheReadPriceShort: '缓存读',
|
||||
imageInputPrice: '图片输入',
|
||||
imageTokenPrice: '图片输出',
|
||||
imageOutputPrice: '图片输出价格',
|
||||
pricePlaceholder: '默认',
|
||||
|
||||
@@ -504,6 +504,7 @@ export default {
|
||||
outputPrice: '输出',
|
||||
cacheWritePrice: '缓存写入',
|
||||
cacheReadPrice: '缓存读取',
|
||||
imageInputPrice: '图片输入',
|
||||
imageOutputPrice: '图片输出',
|
||||
perRequestPrice: '每次请求',
|
||||
intervals: '阶梯定价',
|
||||
|
||||
@@ -855,6 +855,7 @@ function addPricingEntry(sectionIdx: number) {
|
||||
output_price: null,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
image_input_price: null,
|
||||
image_output_price: null,
|
||||
per_request_price: null,
|
||||
intervals: []
|
||||
@@ -887,6 +888,7 @@ async function syncLatestModels(sectionIdx: number) {
|
||||
output_price: null,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
image_input_price: null,
|
||||
image_output_price: null,
|
||||
per_request_price: null,
|
||||
intervals: []
|
||||
@@ -951,6 +953,7 @@ function addRulePricingEntry(sectionIdx: number, ruleIndex: number) {
|
||||
output_price: null,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
image_input_price: null,
|
||||
image_output_price: null,
|
||||
per_request_price: null,
|
||||
intervals: []
|
||||
@@ -1066,6 +1069,7 @@ function accountStatsRulesToAPI(): AccountStatsPricingRule[] {
|
||||
output_price: mTokToPerToken(p.output_price),
|
||||
cache_write_price: mTokToPerToken(p.cache_write_price),
|
||||
cache_read_price: mTokToPerToken(p.cache_read_price),
|
||||
image_input_price: mTokToPerToken(p.image_input_price),
|
||||
image_output_price: mTokToPerToken(p.image_output_price),
|
||||
per_request_price: p.per_request_price != null && p.per_request_price !== '' ? Number(p.per_request_price) : null,
|
||||
intervals: formIntervalsToAPI(p.intervals || [])
|
||||
@@ -1106,6 +1110,7 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[
|
||||
output_price: mTokToPerToken(entry.output_price),
|
||||
cache_write_price: mTokToPerToken(entry.cache_write_price),
|
||||
cache_read_price: mTokToPerToken(entry.cache_read_price),
|
||||
image_input_price: mTokToPerToken(entry.image_input_price),
|
||||
image_output_price: mTokToPerToken(entry.image_output_price),
|
||||
per_request_price: entry.per_request_price != null && entry.per_request_price !== '' ? Number(entry.per_request_price) : null,
|
||||
intervals: formIntervalsToAPI(entry.intervals || [])
|
||||
@@ -1194,6 +1199,7 @@ function apiToForm(channel: Channel): PlatformSection[] {
|
||||
output_price: perTokenToMTok(p.output_price),
|
||||
cache_write_price: perTokenToMTok(p.cache_write_price),
|
||||
cache_read_price: perTokenToMTok(p.cache_read_price),
|
||||
image_input_price: perTokenToMTok(p.image_input_price),
|
||||
image_output_price: perTokenToMTok(p.image_output_price),
|
||||
per_request_price: p.per_request_price,
|
||||
intervals: apiIntervalsToForm(p.intervals || [])
|
||||
@@ -1382,6 +1388,7 @@ function distributeRulesToPlatforms(apiRules: AccountStatsPricingRule[]) {
|
||||
output_price: perTokenToMTok(p.output_price),
|
||||
cache_write_price: perTokenToMTok(p.cache_write_price),
|
||||
cache_read_price: perTokenToMTok(p.cache_read_price),
|
||||
image_input_price: perTokenToMTok(p.image_input_price),
|
||||
image_output_price: perTokenToMTok(p.image_output_price),
|
||||
per_request_price: p.per_request_price,
|
||||
intervals: apiIntervalsToForm(p.intervals || [])
|
||||
|
||||
Reference in New Issue
Block a user