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,12 +3,13 @@
[Git 仓库](https://github.com/c121914yu/FastGPT)
### 交流群/问题反馈
扫码满了,加个小号,定时拉
wx号: fastgpt123
wx 号: fastgpt123
![](/imgs/wx300.jpg)
### 快速开始
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. 进入模型编辑页

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,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 || '请求异常');
}
});
}
);

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

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