mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
feat: lafClaude
This commit is contained in:
@@ -26,7 +26,6 @@
|
||||
"axios": "^1.3.3",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"delay": "^5.0.0",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^9.0.6",
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -28,7 +28,6 @@ specifiers:
|
||||
axios: ^1.3.3
|
||||
crypto: ^1.0.1
|
||||
dayjs: ^1.11.7
|
||||
delay: ^5.0.0
|
||||
eslint: 8.34.0
|
||||
eslint-config-next: 13.1.6
|
||||
eventsource-parser: ^0.1.0
|
||||
@@ -83,7 +82,6 @@ dependencies:
|
||||
axios: registry.npmmirror.com/axios/1.3.3
|
||||
crypto: registry.npmmirror.com/crypto/1.0.1
|
||||
dayjs: registry.npmmirror.com/dayjs/1.11.7
|
||||
delay: 5.0.0
|
||||
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
|
||||
@@ -288,11 +286,6 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/delay/5.0.0:
|
||||
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/fsevents/2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
@@ -3,12 +3,13 @@
|
||||
[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||
|
||||
### 交流群/问题反馈
|
||||
|
||||
扫码满了,加个小号,定时拉
|
||||
wx 号: fastgpt123
|
||||

|
||||
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. 使用手机号注册账号。
|
||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||
3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
|
||||
@@ -16,15 +17,16 @@ wx号: fastgpt123
|
||||
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||
|
||||
### 价格表
|
||||
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| claude - 对话 | 免费 |
|
||||
| chatgpt - 对话 | 0.03 |
|
||||
| 知识库 - 对话 | 0.03 |
|
||||
| 知识库 - 索引 | 0.004 |
|
||||
| 文件拆分 | 0.03 |
|
||||
|
||||
|
||||
### 定制 prompt
|
||||
|
||||
1. 进入模型编辑页
|
||||
|
@@ -24,6 +24,7 @@ export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${
|
||||
*/
|
||||
export const postSaveChat = (data: {
|
||||
modelId: string;
|
||||
newChatId: '' | string;
|
||||
chatId: '' | string;
|
||||
prompts: ChatItemType[];
|
||||
}) => POST<string>('/chat/saveChat', data);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { getToken } from '../utils/user';
|
||||
import { SYSTEM_PROMPT_PREFIX } from '@/constants/chat';
|
||||
import { SYSTEM_PROMPT_HEADER, NEW_CHATID_HEADER } from '@/constants/chat';
|
||||
|
||||
interface StreamFetchProps {
|
||||
url: string;
|
||||
@@ -8,7 +8,8 @@ interface StreamFetchProps {
|
||||
abortSignal: AbortController;
|
||||
}
|
||||
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||
new Promise<{ responseText: string; systemPrompt: string }>(async (resolve, reject) => {
|
||||
new Promise<{ responseText: string; systemPrompt: string; newChatId: string }>(
|
||||
async (resolve, reject) => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -23,15 +24,18 @@ export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchPr
|
||||
if (!reader) return;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const systemPrompt = decodeURIComponent(res.headers.get(SYSTEM_PROMPT_HEADER) || '');
|
||||
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
|
||||
|
||||
let responseText = '';
|
||||
let systemPrompt = '';
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
const { done, value } = await reader?.read();
|
||||
if (done) {
|
||||
if (res.status === 200) {
|
||||
resolve({ responseText, systemPrompt });
|
||||
resolve({ responseText, systemPrompt, newChatId });
|
||||
} else {
|
||||
const parseError = JSON.parse(responseText);
|
||||
reject(parseError?.message || '请求异常');
|
||||
@@ -39,29 +43,21 @@ export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchPr
|
||||
|
||||
return;
|
||||
}
|
||||
let text = decoder.decode(value).replace(/<br\/>/g, '\n');
|
||||
// check system prompt
|
||||
if (text.includes(SYSTEM_PROMPT_PREFIX)) {
|
||||
const arr = text.split(SYSTEM_PROMPT_PREFIX);
|
||||
systemPrompt = arr.pop() || '';
|
||||
|
||||
text = arr.join('');
|
||||
}
|
||||
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
|
||||
responseText += text;
|
||||
onMessage(text);
|
||||
|
||||
read();
|
||||
} catch (err: any) {
|
||||
if (err?.message === 'The user aborted a request.') {
|
||||
return resolve({ responseText, systemPrompt });
|
||||
return resolve({ responseText, systemPrompt, newChatId });
|
||||
}
|
||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||
}
|
||||
};
|
||||
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err, '====');
|
||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export const SYSTEM_PROMPT_PREFIX = 'SYSTEM_PROMPT:';
|
||||
export const SYSTEM_PROMPT_HEADER = 'System-Prompt-Header';
|
||||
export const NEW_CHATID_HEADER = 'Chat-Id-Header';
|
||||
|
||||
export enum ChatRoleEnum {
|
||||
System = 'System',
|
||||
|
@@ -8,13 +8,17 @@ export enum OpenAiChatEnum {
|
||||
'GPT4' = 'gpt-4',
|
||||
'GPT432k' = 'gpt-4-32k'
|
||||
}
|
||||
export enum ClaudeEnum {
|
||||
'Claude' = 'Claude'
|
||||
}
|
||||
|
||||
export type ChatModelType = `${OpenAiChatEnum}`;
|
||||
export type ChatModelType = `${OpenAiChatEnum}` | `${ClaudeEnum}`;
|
||||
|
||||
export type ChatModelItemType = {
|
||||
chatModel: ChatModelType;
|
||||
name: string;
|
||||
contextMaxToken: number;
|
||||
systemMaxToken: number;
|
||||
maxTemperature: number;
|
||||
price: number;
|
||||
};
|
||||
@@ -24,6 +28,7 @@ export const ChatModelMap = {
|
||||
chatModel: OpenAiChatEnum.GPT35,
|
||||
name: 'ChatGpt',
|
||||
contextMaxToken: 4096,
|
||||
systemMaxToken: 3000,
|
||||
maxTemperature: 1.5,
|
||||
price: 3
|
||||
},
|
||||
@@ -31,6 +36,7 @@ export const ChatModelMap = {
|
||||
chatModel: OpenAiChatEnum.GPT4,
|
||||
name: 'Gpt4',
|
||||
contextMaxToken: 8000,
|
||||
systemMaxToken: 4000,
|
||||
maxTemperature: 1.5,
|
||||
price: 30
|
||||
},
|
||||
@@ -38,12 +44,24 @@ export const ChatModelMap = {
|
||||
chatModel: OpenAiChatEnum.GPT432k,
|
||||
name: 'Gpt4-32k',
|
||||
contextMaxToken: 32000,
|
||||
systemMaxToken: 4000,
|
||||
maxTemperature: 1.5,
|
||||
price: 30
|
||||
},
|
||||
[ClaudeEnum.Claude]: {
|
||||
chatModel: ClaudeEnum.Claude,
|
||||
name: 'Claude(免费体验)',
|
||||
contextMaxToken: 9000,
|
||||
systemMaxToken: 2500,
|
||||
maxTemperature: 1,
|
||||
price: 0
|
||||
}
|
||||
};
|
||||
|
||||
export const chatModelList: ChatModelItemType[] = [ChatModelMap[OpenAiChatEnum.GPT35]];
|
||||
export const chatModelList: ChatModelItemType[] = [
|
||||
ChatModelMap[OpenAiChatEnum.GPT35],
|
||||
ChatModelMap[ClaudeEnum.Claude]
|
||||
];
|
||||
|
||||
export enum ModelStatusEnum {
|
||||
running = 'running',
|
||||
|
@@ -42,7 +42,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { model, showModelDetail, content, userApiKey, systemApiKey, userId } = await authChat({
|
||||
const { model, showModelDetail, content, userOpenAiKey, systemAuthKey, userId } =
|
||||
await authChat({
|
||||
modelId,
|
||||
chatId,
|
||||
authorization
|
||||
@@ -56,8 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 使用了知识库搜索
|
||||
if (model.chat.useKb) {
|
||||
const { code, searchPrompt } = await searchKb({
|
||||
userApiKey,
|
||||
systemApiKey,
|
||||
userOpenAiKey,
|
||||
prompts,
|
||||
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
||||
model,
|
||||
@@ -86,10 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// 发出请求
|
||||
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
|
||||
apiKey: userApiKey || systemApiKey,
|
||||
apiKey: userOpenAiKey || systemAuthKey,
|
||||
temperature: +temperature,
|
||||
messages: prompts,
|
||||
stream: true
|
||||
stream: true,
|
||||
res,
|
||||
chatId
|
||||
});
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
@@ -108,7 +110,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
isPay: !userOpenAiKey,
|
||||
chatModel: model.chat.chatModel,
|
||||
userId,
|
||||
chatId,
|
||||
|
@@ -9,7 +9,8 @@ import mongoose from 'mongoose';
|
||||
/* 聊天内容存存储 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, modelId, prompts } = req.body as {
|
||||
const { chatId, modelId, prompts, newChatId } = req.body as {
|
||||
newChatId: '' | string;
|
||||
chatId: '' | string;
|
||||
modelId: string;
|
||||
prompts: ChatItemType[];
|
||||
@@ -35,6 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 没有 chatId, 创建一个对话
|
||||
if (!chatId) {
|
||||
const { _id } = await Chat.create({
|
||||
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
|
||||
userId,
|
||||
modelId,
|
||||
content,
|
||||
|
@@ -65,7 +65,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22;
|
||||
|
||||
const { code, searchPrompt } = await searchKb({
|
||||
systemApiKey: apiKey,
|
||||
prompts,
|
||||
similarity,
|
||||
model,
|
||||
|
@@ -116,7 +116,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// 获取向量匹配到的提示词
|
||||
const { searchPrompt } = await searchKb({
|
||||
systemApiKey: apiKey,
|
||||
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
||||
prompts,
|
||||
model,
|
||||
|
@@ -206,7 +206,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
||||
};
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, systemPrompt } = await streamFetch({
|
||||
let { responseText, systemPrompt, newChatId } = await streamFetch({
|
||||
url: '/api/chat/chat',
|
||||
data: {
|
||||
prompt,
|
||||
@@ -234,10 +234,10 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let newChatId = '';
|
||||
// save chat record
|
||||
try {
|
||||
newChatId = await postSaveChat({
|
||||
newChatId, // 如果有newChatId,会自动以这个Id创建对话框
|
||||
modelId,
|
||||
chatId,
|
||||
prompts: [
|
||||
|
@@ -45,12 +45,12 @@ export async function generateQA(next = false): Promise<any> {
|
||||
const textList: string[] = dataItem.textList.slice(-5);
|
||||
|
||||
// 获取 openapi Key
|
||||
let userApiKey = '',
|
||||
systemApiKey = '';
|
||||
let userOpenAiKey = '',
|
||||
systemAuthKey = '';
|
||||
try {
|
||||
const key = await getApiKey({ model: OpenAiChatEnum.GPT35, userId: dataItem.userId });
|
||||
userApiKey = key.userApiKey;
|
||||
systemApiKey = key.systemApiKey;
|
||||
userOpenAiKey = key.userOpenAiKey;
|
||||
systemAuthKey = key.systemAuthKey;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 501) {
|
||||
// 余额不够了, 清空该记录
|
||||
@@ -73,7 +73,7 @@ export async function generateQA(next = false): Promise<any> {
|
||||
textList.map((text) =>
|
||||
modelServiceToolMap[OpenAiChatEnum.GPT35]
|
||||
.chatCompletion({
|
||||
apiKey: userApiKey || systemApiKey,
|
||||
apiKey: userOpenAiKey || systemAuthKey,
|
||||
temperature: 0.8,
|
||||
messages: [
|
||||
{
|
||||
@@ -98,7 +98,7 @@ export async function generateQA(next = false): Promise<any> {
|
||||
console.log(`split result length: `, result.length);
|
||||
// 计费
|
||||
pushSplitDataBill({
|
||||
isPay: !userApiKey && result.length > 0,
|
||||
isPay: !userOpenAiKey && result.length > 0,
|
||||
userId: dataItem.userId,
|
||||
type: 'QA',
|
||||
textLen: responseMessages.map((item) => item.value).join('').length,
|
||||
|
@@ -2,7 +2,6 @@ import { openaiCreateEmbedding } from '../utils/chat/openai';
|
||||
import { getApiKey } from '../utils/auth';
|
||||
import { openaiError2 } from '../errorCode';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { embeddingModel } from '@/constants/model';
|
||||
|
||||
export async function generateVector(next = false): Promise<any> {
|
||||
if (process.env.queueTask !== '1') {
|
||||
@@ -42,11 +41,10 @@ export async function generateVector(next = false): Promise<any> {
|
||||
dataId = dataItem.id;
|
||||
|
||||
// 获取 openapi Key
|
||||
let userApiKey, systemApiKey;
|
||||
let userOpenAiKey;
|
||||
try {
|
||||
const res = await getApiKey({ model: embeddingModel, userId: dataItem.userId });
|
||||
userApiKey = res.userApiKey;
|
||||
systemApiKey = res.systemApiKey;
|
||||
const res = await getApiKey({ model: 'gpt-3.5-turbo', userId: dataItem.userId });
|
||||
userOpenAiKey = res.userOpenAiKey;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 501) {
|
||||
await PgClient.delete('modelData', {
|
||||
@@ -63,8 +61,7 @@ export async function generateVector(next = false): Promise<any> {
|
||||
const { vectors } = await openaiCreateEmbedding({
|
||||
textArr: [dataItem.q],
|
||||
userId: dataItem.userId,
|
||||
userApiKey,
|
||||
systemApiKey
|
||||
userOpenAiKey
|
||||
});
|
||||
|
||||
// 更新 pg 向量和状态数据
|
||||
|
@@ -3,22 +3,20 @@ import { ModelDataStatusEnum, ModelVectorSearchModeEnum, ChatModelMap } from '@/
|
||||
import { ModelSchema } from '@/types/mongoSchema';
|
||||
import { openaiCreateEmbedding } from '../utils/chat/openai';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { sliceTextByToken } from '@/utils/chat';
|
||||
import { modelToolMap } from '@/utils/chat';
|
||||
import { ChatItemSimpleType } from '@/types/chat';
|
||||
|
||||
/**
|
||||
* use openai embedding search kb
|
||||
*/
|
||||
export const searchKb = async ({
|
||||
userApiKey,
|
||||
systemApiKey,
|
||||
userOpenAiKey,
|
||||
prompts,
|
||||
similarity = 0.2,
|
||||
model,
|
||||
userId
|
||||
}: {
|
||||
userApiKey?: string;
|
||||
systemApiKey: string;
|
||||
userOpenAiKey?: string;
|
||||
prompts: ChatItemSimpleType[];
|
||||
model: ModelSchema;
|
||||
userId: string;
|
||||
@@ -33,8 +31,7 @@ export const searchKb = async ({
|
||||
async function search(textArr: string[] = []) {
|
||||
// 获取提示词的向量
|
||||
const { vectors: promptVectors } = await openaiCreateEmbedding({
|
||||
userApiKey,
|
||||
systemApiKey,
|
||||
userOpenAiKey,
|
||||
userId,
|
||||
textArr
|
||||
});
|
||||
@@ -81,11 +78,24 @@ export const searchKb = async ({
|
||||
].filter((item) => item);
|
||||
const systemPrompts = await search(searchArr);
|
||||
|
||||
// filter system prompt
|
||||
if (
|
||||
systemPrompts.length === 0 &&
|
||||
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity
|
||||
) {
|
||||
// filter system prompts.
|
||||
const filterRateMap: Record<number, number[]> = {
|
||||
1: [1],
|
||||
2: [0.7, 0.3]
|
||||
};
|
||||
const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0];
|
||||
|
||||
const filterSystemPrompt = filterRate
|
||||
.map((rate, i) =>
|
||||
modelToolMap[model.chat.chatModel].sliceText({
|
||||
text: systemPrompts[i],
|
||||
length: Math.floor(modelConstantsData.systemMaxToken * rate)
|
||||
})
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
/* 高相似度+不回复 */
|
||||
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity) {
|
||||
return {
|
||||
code: 201,
|
||||
searchPrompt: {
|
||||
@@ -95,7 +105,7 @@ export const searchKb = async ({
|
||||
};
|
||||
}
|
||||
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
|
||||
if (systemPrompts.length === 0 && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) {
|
||||
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) {
|
||||
return {
|
||||
code: 200,
|
||||
searchPrompt: model.chat.systemPrompt
|
||||
@@ -107,25 +117,7 @@ export const searchKb = async ({
|
||||
};
|
||||
}
|
||||
|
||||
/* 有匹配情况下,system 添加知识库内容。 */
|
||||
|
||||
// filter system prompts. max 70% tokens
|
||||
const filterRateMap: Record<number, number[]> = {
|
||||
1: [0.7],
|
||||
2: [0.5, 0.2]
|
||||
};
|
||||
const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0];
|
||||
|
||||
const filterSystemPrompt = filterRate
|
||||
.map((rate, i) =>
|
||||
sliceTextByToken({
|
||||
model: model.chat.chatModel,
|
||||
text: systemPrompts[i],
|
||||
length: Math.floor(modelConstantsData.contextMaxToken * rate)
|
||||
})
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
/* 有匹配 */
|
||||
return {
|
||||
code: 200,
|
||||
searchPrompt: {
|
||||
@@ -133,9 +125,9 @@ export const searchKb = async ({
|
||||
value: `
|
||||
${model.chat.systemPrompt}
|
||||
${
|
||||
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? `不回答知识库外的内容.` : ''
|
||||
model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? '不回答知识库外的内容.' : ''
|
||||
}
|
||||
知识库内容为: ${filterSystemPrompt}'
|
||||
知识库内容为: '${filterSystemPrompt}'
|
||||
`
|
||||
}
|
||||
};
|
||||
|
@@ -4,15 +4,10 @@ import { Chat, Model, OpenApi, User } from '../mongo';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { ChatItemSimpleType } from '@/types/chat';
|
||||
import mongoose from 'mongoose';
|
||||
import { defaultModel } from '@/constants/model';
|
||||
import { ClaudeEnum, defaultModel } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import {
|
||||
ChatModelType,
|
||||
OpenAiChatEnum,
|
||||
embeddingModel,
|
||||
EmbeddingModelType
|
||||
} from '@/constants/model';
|
||||
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
|
||||
|
||||
/* 校验 token */
|
||||
export const authToken = (token?: string): Promise<string> => {
|
||||
@@ -34,13 +29,7 @@ export const authToken = (token?: string): Promise<string> => {
|
||||
};
|
||||
|
||||
/* 获取 api 请求的 key */
|
||||
export const getApiKey = async ({
|
||||
model,
|
||||
userId
|
||||
}: {
|
||||
model: ChatModelType | EmbeddingModelType;
|
||||
userId: string;
|
||||
}) => {
|
||||
export const getApiKey = async ({ model, userId }: { model: ChatModelType; userId: string }) => {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return Promise.reject({
|
||||
@@ -51,29 +40,29 @@ export const getApiKey = async ({
|
||||
|
||||
const keyMap = {
|
||||
[OpenAiChatEnum.GPT35]: {
|
||||
userApiKey: user.openaiKey || '',
|
||||
systemApiKey: process.env.OPENAIKEY as string
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: process.env.OPENAIKEY as string
|
||||
},
|
||||
[OpenAiChatEnum.GPT4]: {
|
||||
userApiKey: user.openaiKey || '',
|
||||
systemApiKey: process.env.OPENAIKEY as string
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: process.env.OPENAIKEY as string
|
||||
},
|
||||
[OpenAiChatEnum.GPT432k]: {
|
||||
userApiKey: user.openaiKey || '',
|
||||
systemApiKey: process.env.OPENAIKEY as string
|
||||
userOpenAiKey: user.openaiKey || '',
|
||||
systemAuthKey: process.env.OPENAIKEY as string
|
||||
},
|
||||
[embeddingModel]: {
|
||||
userApiKey: user.openaiKey || '',
|
||||
systemApiKey: process.env.OPENAIKEY as string
|
||||
[ClaudeEnum.Claude]: {
|
||||
userOpenAiKey: '',
|
||||
systemAuthKey: process.env.LAFKEY as string
|
||||
}
|
||||
};
|
||||
|
||||
// 有自己的key
|
||||
if (keyMap[model].userApiKey) {
|
||||
if (keyMap[model].userOpenAiKey) {
|
||||
return {
|
||||
user,
|
||||
userApiKey: keyMap[model].userApiKey,
|
||||
systemApiKey: ''
|
||||
userOpenAiKey: keyMap[model].userOpenAiKey,
|
||||
systemAuthKey: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,8 +76,8 @@ export const getApiKey = async ({
|
||||
|
||||
return {
|
||||
user,
|
||||
userApiKey: '',
|
||||
systemApiKey: keyMap[model].systemApiKey
|
||||
userOpenAiKey: '',
|
||||
systemAuthKey: keyMap[model].systemAuthKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -176,11 +165,11 @@ export const authChat = async ({
|
||||
]);
|
||||
}
|
||||
// 获取 user 的 apiKey
|
||||
const { userApiKey, systemApiKey } = await getApiKey({ model: model.chat.chatModel, userId });
|
||||
const { userOpenAiKey, systemAuthKey } = await getApiKey({ model: model.chat.chatModel, userId });
|
||||
|
||||
return {
|
||||
userApiKey,
|
||||
systemApiKey,
|
||||
userOpenAiKey,
|
||||
systemAuthKey,
|
||||
content,
|
||||
userId,
|
||||
model,
|
||||
|
103
src/service/utils/chat/claude.ts
Normal file
103
src/service/utils/chat/claude.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { modelToolMap } from '@/utils/chat';
|
||||
import { ChatCompletionType, StreamResponseType } from './index';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import axios from 'axios';
|
||||
import mongoose from 'mongoose';
|
||||
import { NEW_CHATID_HEADER } from '@/constants/chat';
|
||||
import { ClaudeEnum } from '@/constants/model';
|
||||
|
||||
/* 模型对话 */
|
||||
export const lafClaudChat = async ({
|
||||
apiKey,
|
||||
messages,
|
||||
stream,
|
||||
chatId,
|
||||
res
|
||||
}: ChatCompletionType) => {
|
||||
const conversationId = chatId || String(new mongoose.Types.ObjectId());
|
||||
// create a new chat
|
||||
!chatId &&
|
||||
messages.filter((item) => item.obj === 'Human').length === 1 &&
|
||||
res?.setHeader(NEW_CHATID_HEADER, conversationId);
|
||||
|
||||
// get system prompt
|
||||
const systemPrompt = messages
|
||||
.filter((item) => item.obj === 'System')
|
||||
.map((item) => item.value)
|
||||
.join('\n');
|
||||
const systemPromptText = systemPrompt ? `这是我的知识:'${systemPrompt}'\n` : '';
|
||||
|
||||
const prompt = systemPromptText + messages[messages.length - 1].value;
|
||||
|
||||
const lafResponse = await axios.post(
|
||||
'https://hnvacz.laf.run/claude-gpt',
|
||||
{
|
||||
prompt,
|
||||
stream,
|
||||
conversationId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: apiKey
|
||||
},
|
||||
timeout: stream ? 40000 : 240000,
|
||||
responseType: stream ? 'stream' : 'json'
|
||||
}
|
||||
);
|
||||
|
||||
let responseText = '';
|
||||
let totalTokens = 0;
|
||||
|
||||
if (!stream) {
|
||||
responseText = lafResponse.data?.text || '';
|
||||
}
|
||||
|
||||
return {
|
||||
streamResponse: lafResponse,
|
||||
responseMessages: messages.concat({ obj: ChatRoleEnum.AI, value: responseText }),
|
||||
responseText,
|
||||
totalTokens
|
||||
};
|
||||
};
|
||||
|
||||
/* openai stream response */
|
||||
export const lafClaudStreamResponse = async ({
|
||||
stream,
|
||||
chatResponse,
|
||||
prompts
|
||||
}: StreamResponseType) => {
|
||||
try {
|
||||
let responseContent = '';
|
||||
|
||||
try {
|
||||
const decoder = new TextDecoder();
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const content = decoder.decode(chunk);
|
||||
responseContent += content;
|
||||
content && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// count tokens
|
||||
const finishMessages = prompts.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: responseContent
|
||||
});
|
||||
const totalTokens = modelToolMap[ClaudeEnum.Claude].countTokens({
|
||||
messages: finishMessages
|
||||
});
|
||||
|
||||
return {
|
||||
responseContent,
|
||||
totalTokens,
|
||||
finishMessages
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
@@ -1,19 +1,19 @@
|
||||
import { ChatItemSimpleType } from '@/types/chat';
|
||||
import { modelToolMap } from '@/utils/chat';
|
||||
import type { ChatModelType } from '@/constants/model';
|
||||
import { ChatRoleEnum, SYSTEM_PROMPT_PREFIX } from '@/constants/chat';
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import { ChatRoleEnum, SYSTEM_PROMPT_HEADER } from '@/constants/chat';
|
||||
import { OpenAiChatEnum, ClaudeEnum } from '@/constants/model';
|
||||
import { chatResponse, openAiStreamResponse } from './openai';
|
||||
import { lafClaudChat, lafClaudStreamResponse } from './claude';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import type { PassThrough } from 'stream';
|
||||
import delay from 'delay';
|
||||
|
||||
export type ChatCompletionType = {
|
||||
apiKey: string;
|
||||
temperature: number;
|
||||
messages: ChatItemSimpleType[];
|
||||
stream: boolean;
|
||||
params?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type ChatCompletionResponseType = {
|
||||
streamResponse: any;
|
||||
@@ -25,6 +25,9 @@ export type StreamResponseType = {
|
||||
stream: PassThrough;
|
||||
chatResponse: any;
|
||||
prompts: ChatItemSimpleType[];
|
||||
res: NextApiResponse;
|
||||
systemPrompt?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type StreamResponseReturnType = {
|
||||
responseContent: string;
|
||||
@@ -65,6 +68,10 @@ export const modelServiceToolMap: Record<
|
||||
model: OpenAiChatEnum.GPT432k,
|
||||
...data
|
||||
})
|
||||
},
|
||||
[ClaudeEnum.Claude]: {
|
||||
chatCompletion: lafClaudChat,
|
||||
streamResponse: lafClaudStreamResponse
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,14 +150,13 @@ export const resStreamResponse = async ({
|
||||
prompts
|
||||
}: StreamResponseType & {
|
||||
model: ChatModelType;
|
||||
res: NextApiResponse;
|
||||
systemPrompt?: string;
|
||||
}) => {
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
systemPrompt && res.setHeader(SYSTEM_PROMPT_HEADER, encodeURIComponent(systemPrompt));
|
||||
stream.pipe(res);
|
||||
|
||||
const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap[
|
||||
@@ -158,16 +164,11 @@ export const resStreamResponse = async ({
|
||||
].streamResponse({
|
||||
chatResponse,
|
||||
stream,
|
||||
prompts
|
||||
prompts,
|
||||
res,
|
||||
systemPrompt
|
||||
});
|
||||
|
||||
await delay(100);
|
||||
|
||||
// push system prompt
|
||||
!stream.destroyed &&
|
||||
systemPrompt &&
|
||||
stream.push(`${SYSTEM_PROMPT_PREFIX}${systemPrompt.replace(/\n/g, '<br/>')}`);
|
||||
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
@@ -19,18 +19,18 @@ export const getOpenAIApi = (apiKey: string) => {
|
||||
|
||||
/* 获取向量 */
|
||||
export const openaiCreateEmbedding = async ({
|
||||
userApiKey,
|
||||
systemApiKey,
|
||||
userOpenAiKey,
|
||||
userId,
|
||||
textArr
|
||||
}: {
|
||||
userApiKey?: string;
|
||||
systemApiKey: string;
|
||||
userOpenAiKey?: string;
|
||||
userId: string;
|
||||
textArr: string[];
|
||||
}) => {
|
||||
const systemAuthKey = process.env.OPENAIKEY as string;
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemApiKey);
|
||||
const chatAPI = getOpenAIApi(userOpenAiKey || systemAuthKey);
|
||||
|
||||
// 把输入的内容转成向量
|
||||
const res = await chatAPI
|
||||
@@ -50,7 +50,7 @@ export const openaiCreateEmbedding = async ({
|
||||
}));
|
||||
|
||||
pushGenerateVectorBill({
|
||||
isPay: !userApiKey,
|
||||
isPay: !userOpenAiKey,
|
||||
userId,
|
||||
text: textArr.join(''),
|
||||
tokenLen: res.tokenLen
|
||||
|
3
src/utils/chat/claude.ts
Normal file
3
src/utils/chat/claude.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ClaudeSliceTextByToken = ({ text, length }: { text: string; length: number }) => {
|
||||
return text.slice(0, length);
|
||||
};
|
@@ -1,46 +1,30 @@
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import { ClaudeEnum, OpenAiChatEnum } from '@/constants/model';
|
||||
import type { ChatModelType } from '@/constants/model';
|
||||
import type { ChatItemSimpleType } from '@/types/chat';
|
||||
import { countOpenAIToken, getOpenAiEncMap, adaptChatItem_openAI } from './openai';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
|
||||
export type CountTokenType = { messages: ChatItemSimpleType[] };
|
||||
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
|
||||
import { ClaudeSliceTextByToken } from './claude';
|
||||
|
||||
export const modelToolMap: Record<
|
||||
ChatModelType,
|
||||
{
|
||||
countTokens: (data: CountTokenType) => number;
|
||||
adaptChatMessages: (data: CountTokenType) => ChatCompletionRequestMessage[];
|
||||
countTokens: (data: { messages: ChatItemSimpleType[] }) => number;
|
||||
sliceText: (data: { text: string; length: number }) => string;
|
||||
}
|
||||
> = {
|
||||
[OpenAiChatEnum.GPT35]: {
|
||||
countTokens: ({ messages }: CountTokenType) =>
|
||||
countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
||||
adaptChatMessages: adaptChatItem_openAI
|
||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data })
|
||||
},
|
||||
[OpenAiChatEnum.GPT4]: {
|
||||
countTokens: ({ messages }: CountTokenType) =>
|
||||
countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
|
||||
adaptChatMessages: adaptChatItem_openAI
|
||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
|
||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data })
|
||||
},
|
||||
[OpenAiChatEnum.GPT432k]: {
|
||||
countTokens: ({ messages }: CountTokenType) =>
|
||||
countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
|
||||
adaptChatMessages: adaptChatItem_openAI
|
||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
|
||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data })
|
||||
},
|
||||
[ClaudeEnum.Claude]: {
|
||||
countTokens: () => 0,
|
||||
sliceText: ClaudeSliceTextByToken
|
||||
}
|
||||
};
|
||||
|
||||
export const sliceTextByToken = ({
|
||||
model = 'gpt-3.5-turbo',
|
||||
text,
|
||||
length
|
||||
}: {
|
||||
model: ChatModelType;
|
||||
text: string;
|
||||
length: number;
|
||||
}) => {
|
||||
const enc = getOpenAiEncMap()[model];
|
||||
const encodeText = enc.encode(text);
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(enc.decode(encodeText.slice(0, length)));
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import { encoding_for_model, type Tiktoken } from '@dqbd/tiktoken';
|
||||
import type { ChatItemSimpleType } from '@/types/chat';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import Graphemer from 'graphemer';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
@@ -52,7 +52,7 @@ export function countOpenAIToken({
|
||||
model
|
||||
}: {
|
||||
messages: ChatItemSimpleType[];
|
||||
model: 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k';
|
||||
model: `${OpenAiChatEnum}`;
|
||||
}) {
|
||||
function getChatGPTEncodingText(
|
||||
messages: { role: 'system' | 'user' | 'assistant'; content: string; name?: string }[],
|
||||
@@ -104,3 +104,18 @@ export function countOpenAIToken({
|
||||
|
||||
return text2TokensLen(getOpenAiEncMap()[model], getChatGPTEncodingText(adaptMessages, model));
|
||||
}
|
||||
|
||||
export const openAiSliceTextByToken = ({
|
||||
model = 'gpt-3.5-turbo',
|
||||
text,
|
||||
length
|
||||
}: {
|
||||
model: `${OpenAiChatEnum}`;
|
||||
text: string;
|
||||
length: number;
|
||||
}) => {
|
||||
const enc = getOpenAiEncMap()[model];
|
||||
const encodeText = enc.encode(text);
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(enc.decode(encodeText.slice(0, length)));
|
||||
};
|
||||
|
Reference in New Issue
Block a user