diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 008bdd0bb1..b31d295b3a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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}})", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 0b5dc6d49f..87953447e7 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "このテキストを要約してください", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 4089bf8e29..f9beb63310 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Суммируйте этот текст", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index d30afea1ab..98ee5763d4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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}})", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b05c1a5391..51d236c2da 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "幫我總結一下這段話", diff --git a/src/renderer/src/pages/home/Messages/MessageTokens.tsx b/src/renderer/src/pages/home/Messages/MessageTokens.tsx index c4818c64ab..3326e061de 100644 --- a/src/renderer/src/pages/home/Messages/MessageTokens.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTokens.tsx @@ -18,6 +18,29 @@ const MessgeTokens: React.FC = ({ 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
} @@ -49,6 +72,7 @@ const MessgeTokens: React.FC = ({ message }) => { {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens} + {getPriceString()} ) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx index 2325a3825a..d1cb1d66a1 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelEditContent.tsx @@ -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 = ({ 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 ( = ({ model, onUpdateModel, ope if (visible) { form.getFieldInstance('id')?.focus() } else { - setShowModelTypes(false) + setShowMoreSettings(false) } }}>
= ({ 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}> = ({ model, onUpdateModel, ope - - setShowModelTypes(!showModelTypes)}> + + setShowMoreSettings(!showMoreSettings)} + style={{ position: 'absolute', right: 0 }}> {t('settings.moresetting')} - {showModelTypes ? : } + {showMoreSettings ? : } - {showModelTypes && ( + {showMoreSettings && (
- {t('models.type.select')}: + {t('models.type.select')} {(() => { const defaultTypes = [ ...(isVisionModel(model) ? ['vision'] : []), @@ -193,6 +220,59 @@ const ModelEditContent: FC = ({ model, onUpdateModel, ope /> ) })()} + {t('models.price.price')} + + setCurrencySymbol(e.target.value)} + /> + + )} + + + + + + +
)}
@@ -201,6 +281,7 @@ const ModelEditContent: FC = ({ model, onUpdateModel, ope } const TypeTitle = styled.div` + margin-top: 16px; margin-bottom: 12px; font-size: 14px; font-weight: 600; diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c9e5204a5a..e60f374ba7 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -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 = {