mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 20:37:48 +00:00
feat: 修改计费模式为tokens
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^9.0.6",
|
||||
"gpt-token-utils": "^1.2.0",
|
||||
"hyperdown": "^2.4.29",
|
||||
"immer": "^9.0.19",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -27,6 +27,7 @@ specifiers:
|
||||
eventsource-parser: ^0.1.0
|
||||
formidable: ^2.1.1
|
||||
framer-motion: ^9.0.6
|
||||
gpt-token-utils: ^1.2.0
|
||||
husky: ^8.0.3
|
||||
hyperdown: ^2.4.29
|
||||
immer: ^9.0.19
|
||||
@@ -69,6 +70,7 @@ dependencies:
|
||||
eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0
|
||||
formidable: registry.npmmirror.com/formidable/2.1.1
|
||||
framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y
|
||||
gpt-token-utils: registry.npmmirror.com/gpt-token-utils/1.2.0
|
||||
hyperdown: registry.npmmirror.com/hyperdown/2.4.29
|
||||
immer: registry.npmmirror.com/immer/9.0.19
|
||||
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
||||
@@ -6964,6 +6966,12 @@ packages:
|
||||
get-intrinsic: registry.npmmirror.com/get-intrinsic/1.2.0
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/gpt-token-utils/1.2.0:
|
||||
resolution: {integrity: sha512-s8twaU38UE2Vp65JhQEjz8qvWhWY8KZYvmvYHapxlPT03Ok35Clq+gm9eE27wQILdFisseMVRSiC5lJR9GBklA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gpt-token-utils/-/gpt-token-utils-1.2.0.tgz}
|
||||
name: gpt-token-utils
|
||||
version: 1.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz}
|
||||
name: graceful-fs
|
||||
|
@@ -2,15 +2,25 @@ import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { throttle } from 'lodash';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
|
||||
interface Props extends BoxProps {
|
||||
nextPage: () => void;
|
||||
isLoadAll: boolean;
|
||||
requesting: boolean;
|
||||
children: React.ReactNode;
|
||||
initRequesting?: boolean;
|
||||
}
|
||||
|
||||
const ScrollData = ({ children, nextPage, isLoadAll, requesting, ...props }: Props) => {
|
||||
const ScrollData = ({
|
||||
children,
|
||||
nextPage,
|
||||
isLoadAll,
|
||||
requesting,
|
||||
initRequesting,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { Loading } = useLoading({ defaultLoading: true });
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const loadText = useMemo(() => {
|
||||
if (requesting) return '请求中……';
|
||||
@@ -43,7 +53,7 @@ const ScrollData = ({ children, nextPage, isLoadAll, requesting, ...props }: Pro
|
||||
}, [elementRef, nextPage]);
|
||||
|
||||
return (
|
||||
<Box {...props} ref={elementRef} overflow={'auto'}>
|
||||
<Box {...props} ref={elementRef} overflow={'auto'} position={'relative'}>
|
||||
{children}
|
||||
<Box
|
||||
mt={2}
|
||||
@@ -58,6 +68,7 @@ const ScrollData = ({ children, nextPage, isLoadAll, requesting, ...props }: Pro
|
||||
>
|
||||
{loadText}
|
||||
</Box>
|
||||
{initRequesting && <Loading fixed={false} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -13,7 +13,7 @@ export type ModelConstantsData = {
|
||||
trainName: string; // 空字符串代表不能训练
|
||||
maxToken: number;
|
||||
maxTemperature: number;
|
||||
price: number; // 多少钱 / 1字,单位: 0.00001元
|
||||
price: number; // 多少钱 / 1token,单位: 0.00001元
|
||||
};
|
||||
|
||||
export const modelList: ModelConstantsData[] = [
|
||||
@@ -21,10 +21,10 @@ export const modelList: ModelConstantsData[] = [
|
||||
serviceCompany: 'openai',
|
||||
name: 'chatGPT',
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
trainName: 'turbo',
|
||||
trainName: '',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2,
|
||||
price: 5
|
||||
price: 3
|
||||
},
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
@@ -33,7 +33,7 @@ export const modelList: ModelConstantsData[] = [
|
||||
trainName: 'davinci',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2,
|
||||
price: 50
|
||||
price: 30
|
||||
}
|
||||
];
|
||||
|
||||
|
@@ -18,11 +18,15 @@ export const usePaging = <T = any>({
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoadAll, setIsLoadAll] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const [initRequesting, setInitRequesting] = useState(false);
|
||||
|
||||
const getData = useCallback(
|
||||
async (num: number, init = false) => {
|
||||
if (requesting) return;
|
||||
if (!init && isLoadAll) return;
|
||||
if (init) {
|
||||
setInitRequesting(true);
|
||||
}
|
||||
setRequesting(true);
|
||||
|
||||
try {
|
||||
@@ -49,6 +53,7 @@ export const usePaging = <T = any>({
|
||||
}
|
||||
|
||||
setRequesting(false);
|
||||
setInitRequesting(false);
|
||||
return null;
|
||||
},
|
||||
[api, isLoadAll, pageSize, params, requesting, toast]
|
||||
@@ -66,6 +71,7 @@ export const usePaging = <T = any>({
|
||||
getData,
|
||||
requesting,
|
||||
isLoadAll,
|
||||
nextPage
|
||||
nextPage,
|
||||
initRequesting
|
||||
};
|
||||
};
|
||||
|
@@ -143,15 +143,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
const promptsLen = formatPrompts.reduce((sum, item) => sum + item.content.length, 0);
|
||||
console.log(`responseLen: ${responseContent.length}`, `promptLen: ${promptsLen}`);
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
console.log(`responseLen: ${responseContent.length}`, `promptLen: ${promptsContent.length}`);
|
||||
// 只有使用平台的 key 才计费
|
||||
!userApiKey &&
|
||||
pushBill({
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
textLen: promptsLen + responseContent.length
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
|
@@ -54,21 +54,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
);
|
||||
|
||||
const responseMessage = response.data.choices[0]?.text || '';
|
||||
const responseContent = response.data.choices[0]?.text || '';
|
||||
|
||||
const promptsLen = prompt.reduce((sum, item) => sum + item.value.length, 0);
|
||||
console.log(`responseLen: ${responseMessage.length}`, `promptLen: ${promptsLen}`);
|
||||
console.log(`responseLen: ${responseContent.length}`, `promptLen: ${formatPrompts.length}`);
|
||||
// 只有使用平台的 key 才计费
|
||||
!userApiKey &&
|
||||
pushBill({
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
textLen: promptsLen + responseMessage.length
|
||||
text: formatPrompts + responseContent
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: responseMessage
|
||||
data: responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
jsonRes(res, {
|
||||
|
@@ -21,6 +21,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 根据 userId 获取模型信息
|
||||
const models = await Model.find({
|
||||
userId
|
||||
}).sort({
|
||||
_id: -1
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
|
@@ -12,12 +12,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
await Bill.updateMany(
|
||||
{},
|
||||
{
|
||||
type: 'chat',
|
||||
modelName: 'gpt-3.5-turbo'
|
||||
}
|
||||
const bills = await Bill.find({
|
||||
tokenLen: { $exists: false }
|
||||
});
|
||||
await Promise.all(
|
||||
bills.map((bill) =>
|
||||
Bill.findByIdAndUpdate(bill._id, {
|
||||
tokenLen: bill.textLen
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
|
@@ -5,6 +5,7 @@ import axios from 'axios';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { connectToDatabase, Pay } from '@/service/mongo';
|
||||
import { PRICE_SCALE } from '@/utils/user';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
|
||||
|
||||
@@ -35,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 充值记录 + 1
|
||||
const payOrder = await Pay.create({
|
||||
userId,
|
||||
price: amount * 100000,
|
||||
price: amount * PRICE_SCALE,
|
||||
orderId: id
|
||||
});
|
||||
|
||||
|
@@ -32,7 +32,8 @@ const DataList = () => {
|
||||
isLoadAll,
|
||||
requesting,
|
||||
data: dataList,
|
||||
getData
|
||||
getData,
|
||||
initRequesting
|
||||
} = usePaging<DataListItem>({
|
||||
api: getDataList,
|
||||
pageSize: 20
|
||||
@@ -76,7 +77,13 @@ const DataList = () => {
|
||||
</Card>
|
||||
{/* 数据表 */}
|
||||
<Card mt={3} flex={'1 0 0'} h={['auto', '0']} px={6} py={4}>
|
||||
<ScrollData h={'100%'} nextPage={nextPage} isLoadAll={isLoadAll} requesting={requesting}>
|
||||
<ScrollData
|
||||
h={'100%'}
|
||||
nextPage={nextPage}
|
||||
isLoadAll={isLoadAll}
|
||||
requesting={requesting}
|
||||
initRequesting={initRequesting}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
@@ -96,7 +103,7 @@ const DataList = () => {
|
||||
defaultValue={item.name}
|
||||
size={'sm'}
|
||||
onBlur={(e) => {
|
||||
if (!e.target.value) return;
|
||||
if (!e.target.value || e.target.value === item.name) return;
|
||||
updateDataName(item._id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
@@ -112,7 +112,7 @@ const CreateModel = ({
|
||||
{formatPrice(
|
||||
modelList.find((item) => item.model === getValues('serviceModelName'))?.price || 0
|
||||
) * 1000}
|
||||
元/1000字(包括上下文和标点符号)
|
||||
元/1K tokens(包括上下文和标点符号)
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
|
@@ -195,7 +195,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -246,7 +246,13 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
</Card>
|
||||
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<ModelEditForm formHooks={formHooks} />
|
||||
<Card p={4}>{!!model && <Training model={model} />}</Card>
|
||||
|
||||
{canTrain && (
|
||||
<Card p={4}>
|
||||
<Training model={model} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
神奇操作
|
||||
|
@@ -100,7 +100,7 @@ const PayModal = ({ onClose }: { onClose: () => void }) => {
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>模型类型</Th>
|
||||
<Th>价格(元/1000字符,包含所有上下文)</Th>
|
||||
<Th>价格(元/1K tokens,包含所有上下文)</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
|
@@ -1,41 +1,49 @@
|
||||
import { connectToDatabase, Bill, User } from '../mongo';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
export const pushBill = async ({
|
||||
modelName,
|
||||
userId,
|
||||
chatId,
|
||||
textLen
|
||||
text
|
||||
}: {
|
||||
modelName: string;
|
||||
userId: string;
|
||||
chatId: string;
|
||||
textLen: number;
|
||||
text: string;
|
||||
}) => {
|
||||
await connectToDatabase();
|
||||
|
||||
let billId;
|
||||
|
||||
try {
|
||||
// 获取模型单价格
|
||||
const modelItem = modelList.find((item) => item.model === modelName);
|
||||
const unitPrice = modelItem?.price || 5;
|
||||
|
||||
if (!modelItem) return;
|
||||
// 计算 token 数量
|
||||
const tokens = encode(text);
|
||||
|
||||
const price = modelItem.price * textLen;
|
||||
// 计算价格
|
||||
const price = unitPrice * tokens.length;
|
||||
console.log('token len:', tokens.length, 'price: ', `${formatPrice(price)}元`);
|
||||
|
||||
try {
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type: 'chat',
|
||||
modelName: modelItem.model,
|
||||
modelName,
|
||||
chatId,
|
||||
textLen,
|
||||
textLen: text.length,
|
||||
tokenLen: tokens.length,
|
||||
price
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 扣费
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -price }
|
||||
});
|
||||
|
@@ -31,6 +31,11 @@ const BillSchema = new Schema({
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
tokenLen: {
|
||||
// 折算成 token 的数量
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Schema, model, models } from 'mongoose';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
import { PRICE_SCALE } from '@/utils/user';
|
||||
|
||||
const UserSchema = new Schema({
|
||||
email: {
|
||||
@@ -16,7 +17,7 @@ const UserSchema = new Schema({
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0.5 * 100000
|
||||
default: 0.5 * PRICE_SCALE
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const tokenKey = 'fast-gpt-token';
|
||||
export const PRICE_SCALE = 100000;
|
||||
|
||||
export const setToken = (val: string) => {
|
||||
localStorage.setItem(tokenKey, val);
|
||||
@@ -14,5 +15,5 @@ export const clearToken = () => {
|
||||
* 把数据库读取到的price,转化成元
|
||||
*/
|
||||
export const formatPrice = (val: number) => {
|
||||
return val / 100000;
|
||||
return val / PRICE_SCALE;
|
||||
};
|
||||
|
Reference in New Issue
Block a user