feat: lafClaude

This commit is contained in:
archer
2023-05-04 10:53:55 +08:00
parent 3c8f38799c
commit 0d6897e180
22 changed files with 327 additions and 231 deletions

View File

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

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

View File

@@ -3,39 +3,41 @@
[Git 仓库](https://github.com/c121914yu/FastGPT)
### 交流群/问题反馈
扫码满了,加个小号,定时拉
wx号: fastgpt123
wx 号: fastgpt123
![](/imgs/wx300.jpg)
### 快速开始
1. 使用手机号注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
1. 使用手机号注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
### 价格表
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| --- | --- |
| claude - 对话 | 免费 |
| chatgpt - 对话 | 0.03 |
| 知识库 - 对话 | 0.03 |
| 知识库 - 索引 | 0.004 |
| 文件拆分 | 0.03 |
### 定制 prompt
1. 进入模型编辑页
2. 调整温度和提示词
1. 进入模型编辑页
2. 调整温度和提示词
3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。
### 知识库
1. 创建模型时选择【知识库】
2. 进入模型编辑页
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
4. 使用该模型对话。
1. 创建模型时选择【知识库】
2. 进入模型编辑页
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
4. 使用该模型对话。
注意使用知识库模型对话时tokens 消耗会加快。
注意使用知识库模型对话时tokens 消耗会加快。

View File

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

View File

@@ -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,60 +8,56 @@ interface StreamFetchProps {
abortSignal: AbortController;
}
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<{ responseText: string; systemPrompt: string }>(async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: getToken() || ''
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
new Promise<{ responseText: string; systemPrompt: string; newChatId: string }>(
async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: getToken() || ''
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
let responseText = '';
let systemPrompt = '';
const decoder = new TextDecoder();
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, systemPrompt });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
const systemPrompt = decodeURIComponent(res.headers.get(SYSTEM_PROMPT_HEADER) || '');
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
let responseText = '';
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, systemPrompt, newChatId });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
}
return;
}
return;
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, newChatId });
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
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('');
}
responseText += text;
onMessage(text);
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, systemPrompt });
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
}
});
);

View File

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

View File

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

View File

@@ -42,11 +42,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
let startTime = Date.now();
const { model, showModelDetail, content, userApiKey, systemApiKey, userId } = await authChat({
modelId,
chatId,
authorization
});
const { model, showModelDetail, content, userOpenAiKey, systemAuthKey, userId } =
await authChat({
modelId,
chatId,
authorization
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,18 +73,18 @@ 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: [
{
obj: ChatRoleEnum.System,
value: `你是出题人
${dataItem.prompt || '下面是"一段长文本"'}
从中选出5至20个题目和答案.答案详细.按格式返回: Q1:
A1:
Q2:
A2:
...`
${dataItem.prompt || '下面是"一段长文本"'}
从中选出5至20个题目和答案.答案详细.按格式返回: Q1:
A1:
Q2:
A2:
...`
},
{
obj: 'Human',
@@ -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,

View File

@@ -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 向量和状态数据

View File

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

View File

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

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

View File

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

View File

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

@@ -0,0 +1,3 @@
export const ClaudeSliceTextByToken = ({ text, length }: { text: string; length: number }) => {
return text.slice(0, length);
};

View File

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

View File

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