feat: Add pricing configuration and display for models (#3125)

* feat: Add pricing configuration and display for models

- Introduce model pricing fields in ModelEditContent
- Add price calculation and display in MessageTokens
- Update localization files with price-related translations
- Extend Model type with optional pricing information

* fix: Correct currency symbol placement in message token pricing display

* feat: Add custom currency support in model pricing configuration

- Introduce custom currency option in ModelEditContent
- Update localization files with custom currency translations
- Enhance currency symbol selection with custom input
- Improve input styling for pricing configuration

* fix(OpenAIProvider): ensure messages.content of the request is string

* Update ModelEditContent.tsx

* fix(model-price): remove duplicate button

* fix: build error

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
This commit is contained in:
shiquda 2025-06-17 22:53:47 +08:00 committed by GitHub
parent eeb504d447
commit 41f495904f
8 changed files with 193 additions and 12 deletions

View File

@ -786,6 +786,18 @@
"string": "Text"
},
"pinned": "Pinned",
"price": {
"cost": "Cost",
"currency": "Currency",
"custom": "Custom",
"custom_currency": "Custom Currency",
"custom_currency_placeholder": "Enter Custom Currency",
"input": "Input Price",
"million_tokens": "M Tokens",
"output": "Output Price",
"price": "Price"
},
"reasoning": "Reasoning",
"rerank_model": "Reranker",
"rerank_model_support_provider": "Currently, the reranker model only supports some providers ({{provider}})",
"rerank_model_not_support_provider": "Currently, the reranker model does not support this provider ({{provider}})",

View File

@ -964,6 +964,17 @@
"required_field": "必須項目",
"uploaded_input": "アップロード済みの入力"
},
"price": {
"cost": "コスト",
"currency": "通貨",
"custom": "カスタム",
"custom_currency": "カスタム通貨",
"custom_currency_placeholder": "カスタム通貨を入力してください",
"input": "入力価格",
"million_tokens": "百万トークン",
"output": "出力価格",
"price": "価格"
},
"prompts": {
"explanation": "この概念を説明してください",
"summarize": "このテキストを要約してください",

View File

@ -963,6 +963,29 @@
"required_field": "Обязательное поле",
"uploaded_input": "Загруженный ввод"
},
"plantuml": {
"download": {
"failed": "下载失败,请检查网络",
"png": "下载 PNG",
"svg": "下载 SVG"
},
"tabs": {
"preview": "Предпросмотр",
"source": "Исходный код"
},
"title": "PlantUML 图表"
},
"price": {
"cost": "Стоимость",
"currency": "Валюта",
"custom": "Пользовательский",
"custom_currency": "Пользовательская валюта",
"custom_currency_placeholder": "Введите пользовательскую валюту",
"input": "Цена ввода",
"million_tokens": "M Tokens",
"output": "Цена вывода",
"price": "Цена"
},
"prompts": {
"explanation": "Объясните мне этот концепт",
"summarize": "Суммируйте этот текст",

View File

@ -786,6 +786,18 @@
"string": "文本"
},
"pinned": "已固定",
"price": {
"cost": "花费",
"currency": "币种",
"custom": "自定义",
"custom_currency": "自定义币种",
"custom_currency_placeholder": "请输入自定义币种",
"input": "输入价格",
"million_tokens": "百万 Token",
"output": "输出价格",
"price": "价格"
},
"reasoning": "推理",
"rerank_model": "重排模型",
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
"rerank_model_not_support_provider": "目前重排序模型不支持该服务商 ({{provider}})",

View File

@ -964,6 +964,17 @@
"required_field": "必填欄位",
"uploaded_input": "已上傳輸入"
},
"price": {
"cost": "花費",
"currency": "幣種",
"custom": "自訂",
"custom_currency": "自訂幣種",
"custom_currency_placeholder": "請輸入自訂幣種",
"input": "輸入價格",
"million_tokens": "M Tokens",
"output": "輸出價格",
"price": "價格"
},
"prompts": {
"explanation": "幫我解釋一下這個概念",
"summarize": "幫我總結一下這段話",

View File

@ -18,6 +18,29 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
}
const getPrice = () => {
const inputTokens = message?.usage?.prompt_tokens ?? 0
const outputTokens = message?.usage?.completion_tokens ?? 0
const model = message.model
if (!model || model.pricing?.input_per_million_tokens === 0 || model.pricing?.output_per_million_tokens === 0) {
return 0
}
return (
(inputTokens * (model.pricing?.input_per_million_tokens ?? 0) +
outputTokens * (model.pricing?.output_per_million_tokens ?? 0)) /
1000000
)
}
const getPriceString = () => {
const price = getPrice()
if (price === 0) {
return ''
}
const currencySymbol = message.model?.pricing?.currencySymbol || '$'
return `| ${t('models.price.cost')}: ${currencySymbol}${price}`
}
if (!message.usage) {
return <div />
}
@ -49,6 +72,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
<span>{message?.usage?.total_tokens}</span>
<span>{message?.usage?.prompt_tokens}</span>
<span>{message?.usage?.completion_tokens}</span>
<span>{getPriceString()}</span>
</span>
)

View File

@ -9,7 +9,7 @@ import {
} from '@renderer/config/models'
import { Model, ModelType } from '@renderer/types'
import { getDefaultGroupName } from '@renderer/utils'
import { Button, Checkbox, Divider, Flex, Form, Input, message, Modal } from 'antd'
import { Button, Checkbox, Divider, Flex, Form, Input, InputNumber, message, Modal, Select } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -20,25 +20,42 @@ interface ModelEditContentProps {
onClose: () => void
}
const symbols = ['$', '¥', '€', '£']
const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, open, onClose }) => {
const [form] = Form.useForm()
const { t } = useTranslation()
const [showModelTypes, setShowModelTypes] = useState(false)
const [showMoreSettings, setShowMoreSettings] = useState(false)
const [currencySymbol, setCurrencySymbol] = useState(model.pricing?.currencySymbol || '$')
const [isCustomCurrency, setIsCustomCurrency] = useState(!symbols.includes(model.pricing?.currencySymbol || '$'))
const onFinish = (values: any) => {
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
const updatedModel = {
...model,
id: values.id || model.id,
name: values.name || model.name,
group: values.group || model.group
group: values.group || model.group,
pricing: {
input_per_million_tokens: Number(values.input_per_million_tokens) || 0,
output_per_million_tokens: Number(values.output_per_million_tokens) || 0,
currencySymbol: finalCurrencySymbol || '$'
}
}
onUpdateModel(updatedModel)
setShowModelTypes(false)
setShowMoreSettings(false)
onClose()
}
const handleClose = () => {
setShowModelTypes(false)
setShowMoreSettings(false)
onClose()
}
const currencyOptions = [
...symbols.map((symbol) => ({ label: symbol, value: symbol })),
{ label: t('models.price.custom'), value: 'custom' }
]
return (
<Modal
title={t('models.edit')}
@ -52,7 +69,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
if (visible) {
form.getFieldInstance('id')?.focus()
} else {
setShowModelTypes(false)
setShowMoreSettings(false)
}
}}>
<Form
@ -64,7 +81,15 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
initialValues={{
id: model.id,
name: model.name,
group: model.group
group: model.group,
input_per_million_tokens: model.pricing?.input_per_million_tokens ?? 0,
output_per_million_tokens: model.pricing?.output_per_million_tokens ?? 0,
currencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
? model.pricing?.currencySymbol || '$'
: 'custom',
customCurrencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
? ''
: model.pricing?.currencySymbol || ''
}}
onFinish={onFinish}>
<Form.Item
@ -109,20 +134,22 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
<MoreSettingsRow onClick={() => setShowModelTypes(!showModelTypes)}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<MoreSettingsRow
onClick={() => setShowMoreSettings(!showMoreSettings)}
style={{ position: 'absolute', right: 0 }}>
{t('settings.moresetting')}
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
<ExpandIcon>{showMoreSettings ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</Flex>
</Form.Item>
{showModelTypes && (
{showMoreSettings && (
<div>
<Divider style={{ margin: '0 0 15px 0' }} />
<TypeTitle>{t('models.type.select')}:</TypeTitle>
<TypeTitle>{t('models.type.select')}</TypeTitle>
{(() => {
const defaultTypes = [
...(isVisionModel(model) ? ['vision'] : []),
@ -193,6 +220,59 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
/>
)
})()}
<TypeTitle>{t('models.price.price')}</TypeTitle>
<Form.Item name="currencySymbol" label={t('models.price.currency')} style={{ marginBottom: 10 }}>
<Select
style={{ width: '100px' }}
options={currencyOptions}
onChange={(value) => {
if (value === 'custom') {
setIsCustomCurrency(true)
setCurrencySymbol(form.getFieldValue('customCurrencySymbol') || '')
} else {
setIsCustomCurrency(false)
setCurrencySymbol(value)
}
}}
dropdownMatchSelectWidth={false}
/>
</Form.Item>
{isCustomCurrency && (
<Form.Item
name="customCurrencySymbol"
label={t('models.price.custom_currency')}
style={{ marginBottom: 10 }}
rules={[{ required: isCustomCurrency }]}>
<Input
style={{ width: '100px' }}
placeholder={t('models.price.custom_currency_placeholder')}
maxLength={5}
onChange={(e) => setCurrencySymbol(e.target.value)}
/>
</Form.Item>
)}
<Form.Item label={t('models.price.input')} name="input_per_million_tokens">
<InputNumber
placeholder="0.00"
min={0}
step={0.01}
precision={2}
style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
/>
</Form.Item>
<Form.Item label={t('models.price.output')} name="output_per_million_tokens">
<InputNumber
placeholder="0.00"
min={0}
step={0.01}
precision={2}
style={{ width: '240px' }}
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
/>
</Form.Item>
</div>
)}
</Form>
@ -201,6 +281,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
}
const TypeTitle = styled.div`
margin-top: 16px;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;

View File

@ -174,6 +174,12 @@ export type ProviderType =
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search'
export type ModelPricing = {
input_per_million_tokens: number
output_per_million_tokens: number
currencySymbol?: string
}
export type Model = {
id: string
provider: string
@ -182,6 +188,7 @@ export type Model = {
owned_by?: string
description?: string
type?: ModelType[]
pricing?: ModelPricing
}
export type Suggestion = {