feat: 修改计费模式为tokens

This commit is contained in:
archer
2023-03-25 14:43:32 +08:00
parent 4eaf3a1be0
commit 6bba859060
18 changed files with 97 additions and 38 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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
}
];

View File

@@ -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
};
};

View File

@@ -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) {

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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
});

View File

@@ -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);
}}
/>

View File

@@ -112,7 +112,7 @@ const CreateModel = ({
{formatPrice(
modelList.find((item) => item.model === getValues('serviceModelName'))?.price || 0
) * 1000}
/1000()
/1K tokens()
</Box>
</ModalBody>

View File

@@ -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'}>

View File

@@ -100,7 +100,7 @@ const PayModal = ({ onClose }: { onClose: () => void }) => {
<Thead>
<Tr>
<Th></Th>
<Th>(/1000)</Th>
<Th>(/1K tokens)</Th>
</Tr>
</Thead>
<Tbody>

View File

@@ -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 }
});

View File

@@ -31,6 +31,11 @@ const BillSchema = new Schema({
type: Number,
required: true
},
tokenLen: {
// 折算成 token 的数量
type: Number,
required: true
},
price: {
type: Number,
required: true

View File

@@ -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: [
{

View File

@@ -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;
};