mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
eeb504d447
commit
41f495904f
@ -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}})",
|
||||
|
||||
@ -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": "このテキストを要約してください",
|
||||
|
||||
@ -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": "Суммируйте этот текст",
|
||||
|
||||
@ -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}})",
|
||||
|
||||
@ -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": "幫我總結一下這段話",
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user