mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 07:39:06 +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"
|
"string": "Text"
|
||||||
},
|
},
|
||||||
"pinned": "Pinned",
|
"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": "Reranker",
|
||||||
"rerank_model_support_provider": "Currently, the reranker model only supports some providers ({{provider}})",
|
"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}})",
|
"rerank_model_not_support_provider": "Currently, the reranker model does not support this provider ({{provider}})",
|
||||||
|
|||||||
@ -964,6 +964,17 @@
|
|||||||
"required_field": "必須項目",
|
"required_field": "必須項目",
|
||||||
"uploaded_input": "アップロード済みの入力"
|
"uploaded_input": "アップロード済みの入力"
|
||||||
},
|
},
|
||||||
|
"price": {
|
||||||
|
"cost": "コスト",
|
||||||
|
"currency": "通貨",
|
||||||
|
"custom": "カスタム",
|
||||||
|
"custom_currency": "カスタム通貨",
|
||||||
|
"custom_currency_placeholder": "カスタム通貨を入力してください",
|
||||||
|
"input": "入力価格",
|
||||||
|
"million_tokens": "百万トークン",
|
||||||
|
"output": "出力価格",
|
||||||
|
"price": "価格"
|
||||||
|
},
|
||||||
"prompts": {
|
"prompts": {
|
||||||
"explanation": "この概念を説明してください",
|
"explanation": "この概念を説明してください",
|
||||||
"summarize": "このテキストを要約してください",
|
"summarize": "このテキストを要約してください",
|
||||||
|
|||||||
@ -963,6 +963,29 @@
|
|||||||
"required_field": "Обязательное поле",
|
"required_field": "Обязательное поле",
|
||||||
"uploaded_input": "Загруженный ввод"
|
"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": {
|
"prompts": {
|
||||||
"explanation": "Объясните мне этот концепт",
|
"explanation": "Объясните мне этот концепт",
|
||||||
"summarize": "Суммируйте этот текст",
|
"summarize": "Суммируйте этот текст",
|
||||||
|
|||||||
@ -786,6 +786,18 @@
|
|||||||
"string": "文本"
|
"string": "文本"
|
||||||
},
|
},
|
||||||
"pinned": "已固定",
|
"pinned": "已固定",
|
||||||
|
"price": {
|
||||||
|
"cost": "花费",
|
||||||
|
"currency": "币种",
|
||||||
|
"custom": "自定义",
|
||||||
|
"custom_currency": "自定义币种",
|
||||||
|
"custom_currency_placeholder": "请输入自定义币种",
|
||||||
|
"input": "输入价格",
|
||||||
|
"million_tokens": "百万 Token",
|
||||||
|
"output": "输出价格",
|
||||||
|
"price": "价格"
|
||||||
|
},
|
||||||
|
"reasoning": "推理",
|
||||||
"rerank_model": "重排模型",
|
"rerank_model": "重排模型",
|
||||||
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
|
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
|
||||||
"rerank_model_not_support_provider": "目前重排序模型不支持该服务商 ({{provider}})",
|
"rerank_model_not_support_provider": "目前重排序模型不支持该服务商 ({{provider}})",
|
||||||
|
|||||||
@ -964,6 +964,17 @@
|
|||||||
"required_field": "必填欄位",
|
"required_field": "必填欄位",
|
||||||
"uploaded_input": "已上傳輸入"
|
"uploaded_input": "已上傳輸入"
|
||||||
},
|
},
|
||||||
|
"price": {
|
||||||
|
"cost": "花費",
|
||||||
|
"currency": "幣種",
|
||||||
|
"custom": "自訂",
|
||||||
|
"custom_currency": "自訂幣種",
|
||||||
|
"custom_currency_placeholder": "請輸入自訂幣種",
|
||||||
|
"input": "輸入價格",
|
||||||
|
"million_tokens": "M Tokens",
|
||||||
|
"output": "輸出價格",
|
||||||
|
"price": "價格"
|
||||||
|
},
|
||||||
"prompts": {
|
"prompts": {
|
||||||
"explanation": "幫我解釋一下這個概念",
|
"explanation": "幫我解釋一下這個概念",
|
||||||
"summarize": "幫我總結一下這段話",
|
"summarize": "幫我總結一下這段話",
|
||||||
|
|||||||
@ -18,6 +18,29 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
|||||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
|
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) {
|
if (!message.usage) {
|
||||||
return <div />
|
return <div />
|
||||||
}
|
}
|
||||||
@ -49,6 +72,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
|||||||
<span>{message?.usage?.total_tokens}</span>
|
<span>{message?.usage?.total_tokens}</span>
|
||||||
<span>↑{message?.usage?.prompt_tokens}</span>
|
<span>↑{message?.usage?.prompt_tokens}</span>
|
||||||
<span>↓{message?.usage?.completion_tokens}</span>
|
<span>↓{message?.usage?.completion_tokens}</span>
|
||||||
|
<span>{getPriceString()}</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import { Model, ModelType } from '@renderer/types'
|
import { Model, ModelType } from '@renderer/types'
|
||||||
import { getDefaultGroupName } from '@renderer/utils'
|
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 { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -20,25 +20,42 @@ interface ModelEditContentProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const symbols = ['$', '¥', '€', '£']
|
||||||
const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, open, onClose }) => {
|
const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, open, onClose }) => {
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const { t } = useTranslation()
|
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 onFinish = (values: any) => {
|
||||||
|
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
|
||||||
const updatedModel = {
|
const updatedModel = {
|
||||||
...model,
|
...model,
|
||||||
id: values.id || model.id,
|
id: values.id || model.id,
|
||||||
name: values.name || model.name,
|
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)
|
onUpdateModel(updatedModel)
|
||||||
setShowModelTypes(false)
|
setShowMoreSettings(false)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setShowModelTypes(false)
|
setShowMoreSettings(false)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currencyOptions = [
|
||||||
|
...symbols.map((symbol) => ({ label: symbol, value: symbol })),
|
||||||
|
{ label: t('models.price.custom'), value: 'custom' }
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('models.edit')}
|
title={t('models.edit')}
|
||||||
@ -52,7 +69,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
form.getFieldInstance('id')?.focus()
|
form.getFieldInstance('id')?.focus()
|
||||||
} else {
|
} else {
|
||||||
setShowModelTypes(false)
|
setShowMoreSettings(false)
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Form
|
<Form
|
||||||
@ -64,7 +81,15 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
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}>
|
onFinish={onFinish}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -109,20 +134,22 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
|
|||||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
|
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
|
||||||
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
<Flex justify="center" align="center" style={{ position: 'relative' }}>
|
||||||
<MoreSettingsRow onClick={() => setShowModelTypes(!showModelTypes)}>
|
<MoreSettingsRow
|
||||||
|
onClick={() => setShowMoreSettings(!showMoreSettings)}
|
||||||
|
style={{ position: 'absolute', right: 0 }}>
|
||||||
{t('settings.moresetting')}
|
{t('settings.moresetting')}
|
||||||
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
|
<ExpandIcon>{showMoreSettings ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
|
||||||
</MoreSettingsRow>
|
</MoreSettingsRow>
|
||||||
<Button type="primary" htmlType="submit" size="middle">
|
<Button type="primary" htmlType="submit" size="middle">
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{showModelTypes && (
|
{showMoreSettings && (
|
||||||
<div>
|
<div>
|
||||||
<Divider style={{ margin: '0 0 15px 0' }} />
|
<Divider style={{ margin: '0 0 15px 0' }} />
|
||||||
<TypeTitle>{t('models.type.select')}:</TypeTitle>
|
<TypeTitle>{t('models.type.select')}</TypeTitle>
|
||||||
{(() => {
|
{(() => {
|
||||||
const defaultTypes = [
|
const defaultTypes = [
|
||||||
...(isVisionModel(model) ? ['vision'] : []),
|
...(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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
@ -201,6 +281,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TypeTitle = styled.div`
|
const TypeTitle = styled.div`
|
||||||
|
margin-top: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@ -174,6 +174,12 @@ export type ProviderType =
|
|||||||
|
|
||||||
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search'
|
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 = {
|
export type Model = {
|
||||||
id: string
|
id: string
|
||||||
provider: string
|
provider: string
|
||||||
@ -182,6 +188,7 @@ export type Model = {
|
|||||||
owned_by?: string
|
owned_by?: string
|
||||||
description?: string
|
description?: string
|
||||||
type?: ModelType[]
|
type?: ModelType[]
|
||||||
|
pricing?: ModelPricing
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Suggestion = {
|
export type Suggestion = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user