init
This commit is contained in:
14
src/provider/chat-adapter/custom.ts
Normal file
14
src/provider/chat-adapter/custom.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
|
||||
export type OllamaOptions = BaseChatOptions;
|
||||
|
||||
/**
|
||||
* 自定义模型
|
||||
*/
|
||||
export class Custom extends BaseChat {
|
||||
static BASE_URL = 'https://api.deepseek.com/v1/';
|
||||
constructor(options: OllamaOptions) {
|
||||
const baseURL = options.baseURL || Custom.BASE_URL;
|
||||
super({ ...(options as BaseChatOptions), baseURL: baseURL });
|
||||
}
|
||||
}
|
||||
10
src/provider/chat-adapter/deepseek.ts
Normal file
10
src/provider/chat-adapter/deepseek.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
|
||||
export type DeepSeekOptions = Partial<BaseChatOptions>;
|
||||
export class DeepSeek extends BaseChat {
|
||||
static BASE_URL = 'https://api.deepseek.com/v1/';
|
||||
constructor(options: DeepSeekOptions) {
|
||||
const baseURL = options.baseURL || DeepSeek.BASE_URL;
|
||||
super({ ...(options as BaseChatOptions), baseURL: baseURL });
|
||||
}
|
||||
}
|
||||
11
src/provider/chat-adapter/model-scope.ts
Normal file
11
src/provider/chat-adapter/model-scope.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// https://api-inference.modelscope.cn/v1/
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
|
||||
export type ModelScopeOptions = Partial<BaseChatOptions>;
|
||||
export class ModelScope extends BaseChat {
|
||||
static BASE_URL = 'https://api-inference.modelscope.cn/v1/';
|
||||
constructor(options: ModelScopeOptions) {
|
||||
const baseURL = options.baseURL || ModelScope.BASE_URL;
|
||||
super({ ...options, baseURL: baseURL } as any);
|
||||
}
|
||||
}
|
||||
47
src/provider/chat-adapter/ollama.ts
Normal file
47
src/provider/chat-adapter/ollama.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/index.ts';
|
||||
import type { ChatMessage, ChatMessageOptions } from '../core/index.ts';
|
||||
|
||||
export type OllamaOptions = Partial<BaseChatOptions>;
|
||||
|
||||
type OllamaModel = {
|
||||
name: string;
|
||||
model: string;
|
||||
modified_at: string;
|
||||
|
||||
size: number;
|
||||
digest: string;
|
||||
details: {
|
||||
parent_model: string;
|
||||
format: string; // example: gguf
|
||||
family: string; // example qwen
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string; // example: Q4_K_M Q4_0
|
||||
};
|
||||
};
|
||||
export class Ollama extends BaseChat {
|
||||
static BASE_URL = 'http://localhost:11434/v1';
|
||||
constructor(options: OllamaOptions) {
|
||||
const baseURL = options.baseURL || Ollama.BASE_URL;
|
||||
super({ ...(options as BaseChatOptions), baseURL: baseURL });
|
||||
}
|
||||
async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
|
||||
const res = await super.chat(messages, options);
|
||||
console.log('thunk', this.getChatUsage());
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 获取模型列表
|
||||
* @returns
|
||||
*/
|
||||
async listModels(): Promise<{ models: OllamaModel[] }> {
|
||||
const _url = new URL(this.baseURL);
|
||||
const tagsURL = new URL('/api/tags', _url);
|
||||
return this.openai.get(tagsURL.toString());
|
||||
}
|
||||
async listRunModels(): Promise<{ models: OllamaModel[] }> {
|
||||
const _url = new URL(this.baseURL);
|
||||
const tagsURL = new URL('/api/ps', _url);
|
||||
return this.openai.get(tagsURL.toString());
|
||||
}
|
||||
}
|
||||
39
src/provider/chat-adapter/siliconflow.ts
Normal file
39
src/provider/chat-adapter/siliconflow.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
export type SiliconFlowOptions = Partial<BaseChatOptions>;
|
||||
|
||||
type SiliconFlowUsageData = {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
balance: string;
|
||||
status: 'normal' | 'suspended' | 'expired' | string; // 状态
|
||||
introduce: string;
|
||||
role: string;
|
||||
chargeBalance: string;
|
||||
totalBalance: string;
|
||||
category: string;
|
||||
};
|
||||
type SiliconFlowUsageResponse = {
|
||||
code: number;
|
||||
message: string;
|
||||
status: boolean;
|
||||
data: SiliconFlowUsageData;
|
||||
};
|
||||
export class SiliconFlow extends BaseChat {
|
||||
static BASE_URL = 'https://api.siliconflow.cn/v1';
|
||||
constructor(options: SiliconFlowOptions) {
|
||||
const baseURL = options.baseURL || SiliconFlow.BASE_URL;
|
||||
super({ ...(options as BaseChatOptions), baseURL: baseURL });
|
||||
}
|
||||
async getUsageInfo(): Promise<SiliconFlowUsageResponse> {
|
||||
return this.openai.get('/user/info');
|
||||
}
|
||||
async chat(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], options?: Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>) {
|
||||
const res = await super.chat(messages, options);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
10
src/provider/chat-adapter/volces.ts
Normal file
10
src/provider/chat-adapter/volces.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
|
||||
export type VolcesOptions = Partial<BaseChatOptions>;
|
||||
export class Volces extends BaseChat {
|
||||
static BASE_URL = 'https://ark.cn-beijing.volces.com/api/v3/';
|
||||
constructor(options: VolcesOptions) {
|
||||
const baseURL = options.baseURL || Volces.BASE_URL;
|
||||
super({ ...(options as BaseChatOptions), baseURL: baseURL });
|
||||
}
|
||||
}
|
||||
143
src/provider/core/chat.ts
Normal file
143
src/provider/core/chat.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { OpenAI } from 'openai';
|
||||
import type {
|
||||
BaseChatInterface,
|
||||
ChatMessageComplete,
|
||||
ChatMessage,
|
||||
ChatMessageOptions,
|
||||
BaseChatUsageInterface,
|
||||
ChatStream,
|
||||
EmbeddingMessage,
|
||||
EmbeddingMessageComplete,
|
||||
} from './type.ts';
|
||||
|
||||
export type BaseChatOptions<T = Record<string, any>> = {
|
||||
/**
|
||||
* 默认baseURL
|
||||
*/
|
||||
baseURL: string;
|
||||
/**
|
||||
* 默认模型
|
||||
*/
|
||||
model?: string;
|
||||
/**
|
||||
* 默认apiKey
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* 是否在浏览器中使用
|
||||
*/
|
||||
isBrowser?: boolean;
|
||||
/**
|
||||
* 是否流式输出, 默认 false
|
||||
*/
|
||||
stream?: boolean;
|
||||
} & T;
|
||||
|
||||
export class BaseChat implements BaseChatInterface, BaseChatUsageInterface {
|
||||
/**
|
||||
* 默认baseURL
|
||||
*/
|
||||
baseURL: string;
|
||||
/**
|
||||
* 默认模型
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* 默认apiKey
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* 是否在浏览器中使用
|
||||
*/
|
||||
isBrowser: boolean;
|
||||
/**
|
||||
* openai实例
|
||||
*/
|
||||
openai: OpenAI;
|
||||
|
||||
prompt_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens: number;
|
||||
|
||||
constructor(options: BaseChatOptions) {
|
||||
this.baseURL = options.baseURL;
|
||||
this.model = options.model;
|
||||
this.apiKey = options.apiKey;
|
||||
this.isBrowser = options.isBrowser ?? false;
|
||||
this.openai = new OpenAI({
|
||||
apiKey: this.apiKey,
|
||||
baseURL: this.baseURL,
|
||||
dangerouslyAllowBrowser: this.isBrowser,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 聊天
|
||||
*/
|
||||
async chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete> {
|
||||
const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
|
||||
model: this.model,
|
||||
messages,
|
||||
...options,
|
||||
stream: false,
|
||||
};
|
||||
const res = (await this.openai.chat.completions.create(createParams)) as ChatMessageComplete;
|
||||
this.prompt_tokens = res.usage?.prompt_tokens ?? 0;
|
||||
this.total_tokens = res.usage?.total_tokens ?? 0;
|
||||
this.completion_tokens = res.usage?.completion_tokens ?? 0;
|
||||
return res;
|
||||
}
|
||||
async chatStream(messages: ChatMessage[], options?: ChatMessageOptions) {
|
||||
const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
|
||||
model: this.model,
|
||||
messages,
|
||||
...options,
|
||||
stream: true,
|
||||
};
|
||||
if (createParams.response_format) {
|
||||
throw new Error('response_format is not supported in stream mode');
|
||||
}
|
||||
return this.openai.chat.completions.create(createParams) as unknown as ChatStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试
|
||||
*/
|
||||
test() {
|
||||
return this.chat([{ role: 'user', content: 'Hello, world!' }]);
|
||||
}
|
||||
/**
|
||||
* 获取聊天使用情况
|
||||
* @returns
|
||||
*/
|
||||
getChatUsage() {
|
||||
return {
|
||||
prompt_tokens: this.prompt_tokens,
|
||||
total_tokens: this.total_tokens,
|
||||
completion_tokens: this.completion_tokens,
|
||||
};
|
||||
}
|
||||
getHeaders(headers?: Record<string, string>) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
...headers,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 生成embedding 内部
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async generateEmbeddingCore(text: string | string[], options?: EmbeddingMessage): Promise<EmbeddingMessageComplete> {
|
||||
const embeddingModel = options?.model || this.model;
|
||||
const res = await this.openai.embeddings.create({
|
||||
model: embeddingModel,
|
||||
input: text,
|
||||
encoding_format: 'float',
|
||||
...options,
|
||||
});
|
||||
this.prompt_tokens += res.usage.prompt_tokens;
|
||||
this.total_tokens += res.usage.total_tokens;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
27
src/provider/core/index.ts
Normal file
27
src/provider/core/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ChatStream } from './type.ts';
|
||||
|
||||
// export type { BaseChat, BaseChatOptions } from './chat.ts';
|
||||
export * from './chat.ts'
|
||||
// export {
|
||||
// ChatMessage,
|
||||
// ChatMessageOptions, //
|
||||
// ChatMessageComplete,
|
||||
// ChatMessageStream,
|
||||
// BaseChatInterface,
|
||||
// BaseChatUsageInterface,
|
||||
// ChatStream,
|
||||
// EmbeddingMessage,
|
||||
// EmbeddingMessageComplete,
|
||||
// } from './type.ts';
|
||||
export * from './type.ts'
|
||||
/**
|
||||
* for await (const chunk of chatStream) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* @param chatStream
|
||||
*/
|
||||
export const readStream = async (chatStream: ChatStream) => {
|
||||
for await (const chunk of chatStream) {
|
||||
console.log(chunk);
|
||||
}
|
||||
};
|
||||
105
src/provider/core/text-regex.ts
Normal file
105
src/provider/core/text-regex.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// Updated: Aug. 20, 2024
|
||||
// Live demo: https://jina.ai/tokenizer
|
||||
// LICENSE: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
||||
// COPYRIGHT: Jina AI
|
||||
|
||||
// Define variables for magic numbers
|
||||
const MAX_HEADING_LENGTH = 7;
|
||||
const MAX_HEADING_CONTENT_LENGTH = 200;
|
||||
const MAX_HEADING_UNDERLINE_LENGTH = 200;
|
||||
const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100;
|
||||
const MAX_LIST_ITEM_LENGTH = 200;
|
||||
const MAX_NESTED_LIST_ITEMS = 6;
|
||||
const MAX_LIST_INDENT_SPACES = 7;
|
||||
const MAX_BLOCKQUOTE_LINE_LENGTH = 200;
|
||||
const MAX_BLOCKQUOTE_LINES = 15;
|
||||
const MAX_CODE_BLOCK_LENGTH = 1500;
|
||||
const MAX_CODE_LANGUAGE_LENGTH = 20;
|
||||
const MAX_INDENTED_CODE_LINES = 20;
|
||||
const MAX_TABLE_CELL_LENGTH = 200;
|
||||
const MAX_TABLE_ROWS = 20;
|
||||
const MAX_HTML_TABLE_LENGTH = 2000;
|
||||
const MIN_HORIZONTAL_RULE_LENGTH = 3;
|
||||
const MAX_SENTENCE_LENGTH = 400;
|
||||
const MAX_QUOTED_TEXT_LENGTH = 300;
|
||||
const MAX_PARENTHETICAL_CONTENT_LENGTH = 200;
|
||||
const MAX_NESTED_PARENTHESES = 5;
|
||||
const MAX_MATH_INLINE_LENGTH = 100;
|
||||
const MAX_MATH_BLOCK_LENGTH = 500;
|
||||
const MAX_PARAGRAPH_LENGTH = 1000;
|
||||
const MAX_STANDALONE_LINE_LENGTH = 800;
|
||||
const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100;
|
||||
const MAX_HTML_TAG_CONTENT_LENGTH = 1000;
|
||||
const LOOKAHEAD_RANGE = 100; // Number of characters to look ahead for a sentence boundary
|
||||
|
||||
const AVOID_AT_START = `[\\s\\]})>,']`;
|
||||
const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`;
|
||||
const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`;
|
||||
const SENTENCE_END = `(?:${PUNCTUATION}(?<!${AVOID_AT_START}(?=${PUNCTUATION}))|${QUOTE_END})(?=\\S|$)`;
|
||||
const SENTENCE_BOUNDARY = `(?:${SENTENCE_END}|(?=[\\r\\n]|$))`;
|
||||
const LOOKAHEAD_PATTERN = `(?:(?!${SENTENCE_END}).){1,${LOOKAHEAD_RANGE}}${SENTENCE_END}`;
|
||||
const NOT_PUNCTUATION_SPACE = `(?!${PUNCTUATION}\\s)`;
|
||||
const SENTENCE_PATTERN = `${NOT_PUNCTUATION_SPACE}(?:[^\\r\\n]{1,{MAX_LENGTH}}${SENTENCE_BOUNDARY}|[^\\r\\n]{1,{MAX_LENGTH}}(?=${PUNCTUATION}|${QUOTE_END})(?:${LOOKAHEAD_PATTERN})?)${AVOID_AT_START}*`;
|
||||
|
||||
|
||||
export const textSplitter = new RegExp(
|
||||
"(" +
|
||||
// 1. Headings (Setext-style, Markdown, and HTML-style, with length constraints)
|
||||
`(?:^(?:[#*=-]{1,${MAX_HEADING_LENGTH}}|\\w[^\\r\\n]{0,${MAX_HEADING_CONTENT_LENGTH}}\\r?\\n[-=]{2,${MAX_HEADING_UNDERLINE_LENGTH}}|<h[1-6][^>]{0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:</h[1-6]>)?(?:\\r?\\n|$))` +
|
||||
"|" +
|
||||
// New pattern for citations
|
||||
`(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` +
|
||||
"|" +
|
||||
// 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}` +
|
||||
`(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` +
|
||||
`(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` +
|
||||
"|" +
|
||||
// 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints)
|
||||
`(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,${MAX_BLOCKQUOTE_LINES}})` +
|
||||
"|" +
|
||||
// 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` +
|
||||
`|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,${MAX_INDENTED_CODE_LINES}}\\r?\\n?)` +
|
||||
`|(?:<pre>(?:<code>)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:</code>)?</pre>))` +
|
||||
"|" +
|
||||
// 5. Tables (Markdown, grid tables, and HTML tables, with length constraints)
|
||||
`(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` +
|
||||
`|<table>[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?</table>))` +
|
||||
"|" +
|
||||
// 6. Horizontal rules (Markdown and HTML hr tag)
|
||||
`(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|<hr\\s*/?>)` +
|
||||
"|" +
|
||||
// 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints)
|
||||
`(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}(?:</[a-zA-Z]+>)?(?:\\r?\\n|$))` +
|
||||
"|" +
|
||||
// 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation)
|
||||
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` +
|
||||
"|" +
|
||||
// 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints)
|
||||
"(?:" +
|
||||
`(?<!\\w)\"\"\"[^\"]{0,${MAX_QUOTED_TEXT_LENGTH}}\"\"\"(?!\\w)` +
|
||||
`|(?<!\\w)(?:['\"\`'"])[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}\\1(?!\\w)` +
|
||||
`|(?<!\\w)\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}'(?!\\w)` +
|
||||
`|(?<!\\w)\`\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}''(?!\\w)` +
|
||||
`|\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\)[^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\)` +
|
||||
`|\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\][^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\]` +
|
||||
`|\\$[^\\r\\n$]{0,${MAX_MATH_INLINE_LENGTH}}\\$` +
|
||||
`|\`[^\`\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\`` +
|
||||
")" +
|
||||
"|" +
|
||||
// 9. Paragraphs (with length constraints)
|
||||
`(?!${AVOID_AT_START})(?:(?:^|\\r?\\n\\r?\\n)(?:<p>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:</p>)?(?=\\r?\\n\\r?\\n|$))` +
|
||||
"|" +
|
||||
// 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints)
|
||||
`(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?</[a-zA-Z]+>|\\s*/>))` +
|
||||
"|" +
|
||||
// 12. LaTeX-style math expressions (inline and block, with length constraints)
|
||||
`(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` +
|
||||
"|" +
|
||||
// 14. Fallback for any remaining content (with length constraints)
|
||||
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` +
|
||||
")",
|
||||
"gmu"
|
||||
);
|
||||
|
||||
29
src/provider/core/type.ts
Normal file
29
src/provider/core/type.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import OpenAI from 'openai';
|
||||
|
||||
export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
||||
export type ChatMessageOptions = Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>;
|
||||
export type ChatMessageComplete = OpenAI.Chat.Completions.ChatCompletion;
|
||||
export type ChatMessageStream = OpenAI.Chat.Completions.ChatCompletion;
|
||||
|
||||
export type EmbeddingMessage = Partial<OpenAI.Embeddings.EmbeddingCreateParams>;
|
||||
export type EmbeddingMessageComplete = OpenAI.Embeddings.CreateEmbeddingResponse;
|
||||
export interface BaseChatInterface {
|
||||
chat(messages: ChatMessage[], options?: ChatMessageOptions): Promise<ChatMessageComplete>;
|
||||
}
|
||||
|
||||
export interface BaseChatUsageInterface {
|
||||
/**
|
||||
* 提示词令牌
|
||||
*/
|
||||
prompt_tokens: number;
|
||||
/**
|
||||
* 总令牌
|
||||
*/
|
||||
total_tokens: number;
|
||||
/**
|
||||
* 完成令牌
|
||||
*/
|
||||
completion_tokens: number;
|
||||
}
|
||||
|
||||
export type ChatStream = AsyncGenerator<ChatMessageComplete, void, unknown>;
|
||||
63
src/provider/index.ts
Normal file
63
src/provider/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export * from './core/index.ts';
|
||||
import { BaseChat } from './core/chat.ts';
|
||||
|
||||
import { Ollama } from './chat-adapter/ollama.ts';
|
||||
import { SiliconFlow } from './chat-adapter/siliconflow.ts';
|
||||
import { Custom } from './chat-adapter/custom.ts';
|
||||
import { Volces } from './chat-adapter/volces.ts';
|
||||
import { DeepSeek } from './chat-adapter/deepseek.ts';
|
||||
import { ModelScope } from './chat-adapter/model-scope.ts';
|
||||
import { ChatMessage } from './core/type.ts';
|
||||
|
||||
export const OllamaProvider = Ollama;
|
||||
export const SiliconFlowProvider = SiliconFlow;
|
||||
export const CustomProvider = Custom;
|
||||
export const VolcesProvider = Volces;
|
||||
export const DeepSeekProvider = DeepSeek;
|
||||
export const ModelScopeProvider = ModelScope;
|
||||
|
||||
export const ProviderMap = {
|
||||
Ollama: OllamaProvider,
|
||||
SiliconFlow: SiliconFlowProvider,
|
||||
Custom: CustomProvider,
|
||||
Volces: VolcesProvider,
|
||||
DeepSeek: DeepSeekProvider,
|
||||
ModelScope: ModelScopeProvider,
|
||||
BaseChat: BaseChat,
|
||||
};
|
||||
|
||||
type ProviderManagerConfig = {
|
||||
provider: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
baseURL?: string;
|
||||
};
|
||||
export class ProviderManager {
|
||||
provider: BaseChat;
|
||||
constructor(config: ProviderManagerConfig) {
|
||||
const { provider, model, apiKey, baseURL } = config;
|
||||
const Provider = ProviderMap[provider] as typeof BaseChat;
|
||||
if (!Provider) {
|
||||
throw new Error(`Provider ${provider} not found`);
|
||||
}
|
||||
const providerConfig = {
|
||||
model,
|
||||
apiKey,
|
||||
baseURL,
|
||||
};
|
||||
if (!providerConfig.baseURL) {
|
||||
delete providerConfig.baseURL;
|
||||
}
|
||||
this.provider = new Provider(providerConfig);
|
||||
}
|
||||
static async createProvider(config: ProviderManagerConfig) {
|
||||
if (!config.baseURL) {
|
||||
delete config.baseURL;
|
||||
}
|
||||
const pm = new ProviderManager(config);
|
||||
return pm.provider;
|
||||
}
|
||||
async chat(messages: ChatMessage[]) {
|
||||
return this.provider.chat(messages);
|
||||
}
|
||||
}
|
||||
107
src/provider/knowledge/knowledge-base.ts
Normal file
107
src/provider/knowledge/knowledge-base.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { BaseChat, BaseChatOptions } from '../core/chat.ts';
|
||||
import { EmbeddingMessage } from '../core/type.ts';
|
||||
|
||||
export type KnowledgeOptions<T = Record<string, string>> = BaseChatOptions<
|
||||
{
|
||||
embeddingModel: string;
|
||||
splitSize?: number; // 分块大小 默认 2000
|
||||
splitOverlap?: number; // 分块重叠 默认 200
|
||||
batchSize?: number; // 批量大小 默认 4, 4*2000=8000
|
||||
} & T
|
||||
>;
|
||||
/**
|
||||
* 知识库构建
|
||||
* 1. Embedding generate
|
||||
* 2. retriever
|
||||
* 3. reranker
|
||||
*/
|
||||
export class KnowledgeBase extends BaseChat {
|
||||
embeddingModel: string;
|
||||
splitSize: number;
|
||||
splitOverlap: number;
|
||||
batchSize: number;
|
||||
constructor(options: KnowledgeOptions) {
|
||||
super(options);
|
||||
this.embeddingModel = options.embeddingModel;
|
||||
this.splitSize = options.splitSize || 2000;
|
||||
this.splitOverlap = options.splitOverlap || 200;
|
||||
this.prompt_tokens = 0;
|
||||
this.total_tokens = 0;
|
||||
this.batchSize = options.batchSize || 4;
|
||||
}
|
||||
/**
|
||||
* 生成embedding
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async generateEmbedding(text: string | string[]) {
|
||||
try {
|
||||
const res = await this.generateEmbeddingCore(text, { model: this.embeddingModel });
|
||||
return { code: 200, data: res.data };
|
||||
} catch (error) {
|
||||
const has413 = error?.message?.includes('413');
|
||||
if (has413) {
|
||||
return {
|
||||
code: 413,
|
||||
message: '请求过大,请分割文本',
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: error?.code || 500,
|
||||
message: '生成embedding失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 批量生成embedding
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async generateEmbeddingBatch(textArray: string[]) {
|
||||
const batchSize = this.batchSize || 4;
|
||||
const embeddings: number[][] = [];
|
||||
for (let i = 0; i < textArray.length; i += batchSize) {
|
||||
const batch = textArray.slice(i, i + batchSize);
|
||||
const res = await this.generateEmbedding(batch);
|
||||
if (res.code === 200) {
|
||||
embeddings.push(...res.data.map((item) => item.embedding));
|
||||
}
|
||||
}
|
||||
return embeddings;
|
||||
}
|
||||
/**
|
||||
* 分割长文本, 生成对应的embedding
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async splitLongText(text: string) {
|
||||
// 分割文本
|
||||
const chunks: string[] = [];
|
||||
let startIndex = 0;
|
||||
|
||||
while (startIndex < text.length) {
|
||||
// 计算当前chunk的结束位置
|
||||
const endIndex = Math.min(startIndex + this.splitSize, text.length);
|
||||
|
||||
// 提取当前chunk
|
||||
const chunk = text.substring(startIndex, endIndex);
|
||||
chunks.push(chunk);
|
||||
|
||||
// 移动到下一个起始位置,考虑重叠
|
||||
startIndex = endIndex - this.splitOverlap;
|
||||
|
||||
// 如果下一个起始位置已经超出或者太接近文本结尾,就结束循环
|
||||
if (startIndex >= text.length - this.splitOverlap) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个chunk生成embedding
|
||||
const embeddings = await this.generateEmbeddingBatch(chunks);
|
||||
// 返回文本片段和对应的embedding
|
||||
return chunks.map((chunk, index) => ({
|
||||
text: chunk,
|
||||
embedding: embeddings[index],
|
||||
}));
|
||||
}
|
||||
}
|
||||
7
src/provider/knowledge/knowledge.ts
Normal file
7
src/provider/knowledge/knowledge.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { KnowledgeBase, KnowledgeOptions } from './knowledge-base.ts';
|
||||
|
||||
export class Knowledge extends KnowledgeBase {
|
||||
constructor(options: KnowledgeOptions) {
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
1
src/provider/media/index.ts
Normal file
1
src/provider/media/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './video/siliconflow.ts';
|
||||
37
src/provider/media/video/siliconflow.ts
Normal file
37
src/provider/media/video/siliconflow.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { SiliconFlow } from '../../chat-adapter/siliconflow.ts';
|
||||
export class VideoSiliconFlow extends SiliconFlow {
|
||||
constructor(opts: any) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
async uploadAudioVoice(audioBase64: string | Blob | File) {
|
||||
const pathname = 'uploads/audio/voice';
|
||||
const url = `${this.baseURL}/${pathname}`;
|
||||
const headers = {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
const formData = new FormData();
|
||||
// formData.append('audio', 'data:audio/mpeg;base64,aGVsbG93b3JsZA==');
|
||||
// formData.append('audio', audioBase64);
|
||||
formData.append('file', audioBase64);
|
||||
formData.append('model', 'FunAudioLLM/CosyVoice2-0.5B');
|
||||
formData.append('customName', 'test_name');
|
||||
formData.append('text', '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
}).then((res) => res.json());
|
||||
console.log('uploadAudioVoice', res);
|
||||
}
|
||||
async audioSpeech() {
|
||||
this.openai.audio.speech.create({
|
||||
model: 'FunAudioLLM/CosyVoice2-0.5B',
|
||||
voice: 'alloy',
|
||||
input: '在一无所知中, 梦里的一天结束了,一个新的轮回便会开始',
|
||||
response_format: 'mp3',
|
||||
});
|
||||
}
|
||||
}
|
||||
52
src/provider/utils/ai-config-type.ts
Normal file
52
src/provider/utils/ai-config-type.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Permission } from '@kevisual/permission';
|
||||
|
||||
export type AIModel = {
|
||||
/**
|
||||
* 提供商
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* 模型组
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* 每日请求频率限制
|
||||
*/
|
||||
dayLimit?: number;
|
||||
/**
|
||||
* 总的token限制
|
||||
*/
|
||||
tokenLimit?: number;
|
||||
};
|
||||
export type SecretKey = {
|
||||
/**
|
||||
* 组
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* 解密密钥
|
||||
*/
|
||||
decryptKey?: string;
|
||||
};
|
||||
|
||||
export type AIConfig = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
models: AIModel[];
|
||||
secretKeys: SecretKey[];
|
||||
permission?: Permission;
|
||||
filter?: {
|
||||
objectKey: string;
|
||||
type: 'array' | 'object';
|
||||
operate: 'removeAttribute' | 'remove';
|
||||
attribute: string[];
|
||||
}[];
|
||||
};
|
||||
86
src/provider/utils/chunk.ts
Normal file
86
src/provider/utils/chunk.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { numTokensFromString } from './token.ts';
|
||||
|
||||
// 常量定义
|
||||
const CHUNK_SIZE = 512; // 每个chunk的最大token数
|
||||
const MAGIC_SEPARATOR = '🦛';
|
||||
const DELIMITER = [',', '.', '!', '?', '\n', ',', '。', '!', '?'];
|
||||
const PARAGRAPH_DELIMITER = '\n\n';
|
||||
|
||||
export interface Chunk {
|
||||
chunkId: number;
|
||||
text: string;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保每个chunk的大小不超过最大token数
|
||||
* @param chunk 输入的文本块
|
||||
* @returns 分割后的文本块及其token数的数组
|
||||
*/
|
||||
function ensureChunkSize(chunk: string): Array<[string, number]> {
|
||||
const tokens = numTokensFromString(chunk);
|
||||
if (tokens <= CHUNK_SIZE) {
|
||||
return [[chunk, tokens]];
|
||||
}
|
||||
|
||||
// 在分隔符后添加魔法分隔符
|
||||
let processedChunk = chunk;
|
||||
for (const delimiter of DELIMITER) {
|
||||
// 转义特殊字符
|
||||
const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
processedChunk = processedChunk.replace(new RegExp(escapedDelimiter, 'g'), delimiter + MAGIC_SEPARATOR);
|
||||
}
|
||||
|
||||
const chunks: Array<[string, number]> = [];
|
||||
let tail = '';
|
||||
|
||||
// 按CHUNK_SIZE分割文本
|
||||
for (let i = 0; i < processedChunk.length; i += CHUNK_SIZE) {
|
||||
const sentences = (processedChunk.slice(i, i + CHUNK_SIZE) + ' ').split(MAGIC_SEPARATOR);
|
||||
const currentChunk = tail + sentences.slice(0, -1).join('');
|
||||
if (currentChunk.trim()) {
|
||||
const tokenCount = numTokensFromString(currentChunk);
|
||||
chunks.push([currentChunk, tokenCount]);
|
||||
}
|
||||
tail = sentences[sentences.length - 1].trim();
|
||||
}
|
||||
|
||||
// 处理最后剩余的tail
|
||||
if (tail) {
|
||||
const tokenCount = numTokensFromString(tail);
|
||||
chunks.push([tail, tokenCount]);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本分割成chunks
|
||||
* @param text 输入文本
|
||||
* @returns 分割后的chunks数组
|
||||
*/
|
||||
export async function getChunks(text: string): Promise<Chunk[]> {
|
||||
// 按段落分割文本
|
||||
const paragraphs = text
|
||||
.split(PARAGRAPH_DELIMITER)
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p);
|
||||
|
||||
const chunks: Chunk[] = [];
|
||||
let currentIndex = 0;
|
||||
|
||||
// 处理每个段落
|
||||
for (const paragraph of paragraphs) {
|
||||
const splittedParagraph = ensureChunkSize(paragraph);
|
||||
for (const [text, tokens] of splittedParagraph) {
|
||||
chunks.push({
|
||||
chunkId: currentIndex,
|
||||
text,
|
||||
tokens,
|
||||
});
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
206
src/provider/utils/parse-config.ts
Normal file
206
src/provider/utils/parse-config.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Permission } from '@kevisual/permission';
|
||||
import AES from 'crypto-js/aes.js';
|
||||
import Utf8 from 'crypto-js/enc-utf8.js';
|
||||
|
||||
const CryptoJS = { AES, enc: { Utf8 } };
|
||||
// 加密函数
|
||||
export function encryptAES(plainText: string, secretKey: string) {
|
||||
return CryptoJS.AES.encrypt(plainText, secretKey).toString();
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
export function decryptAES(cipherText: string, secretKey: string) {
|
||||
const bytes = CryptoJS.AES.decrypt(cipherText, secretKey);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
type AIModel = {
|
||||
/**
|
||||
* 提供商
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* 模型组
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* 每日请求频率限制
|
||||
*/
|
||||
dayLimit?: number;
|
||||
/**
|
||||
* 总的token限制
|
||||
*/
|
||||
tokenLimit?: number;
|
||||
};
|
||||
|
||||
type SecretKey = {
|
||||
/**
|
||||
* 组
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* 解密密钥
|
||||
*/
|
||||
decryptKey?: string;
|
||||
};
|
||||
|
||||
export type GetProviderOpts = {
|
||||
model: string;
|
||||
group: string;
|
||||
decryptKey?: string;
|
||||
};
|
||||
export type ProviderResult = {
|
||||
provider: string;
|
||||
model: string;
|
||||
group: string;
|
||||
apiKey: string;
|
||||
dayLimit?: number;
|
||||
tokenLimit?: number;
|
||||
baseURL?: string;
|
||||
/**
|
||||
* 解密密钥
|
||||
*/
|
||||
decryptKey?: string;
|
||||
};
|
||||
|
||||
export type AIConfig = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
models: AIModel[];
|
||||
secretKeys: SecretKey[];
|
||||
permission?: Permission;
|
||||
filter?: {
|
||||
objectKey: string;
|
||||
type: 'array' | 'object';
|
||||
operate: 'removeAttribute' | 'remove';
|
||||
attribute: string[];
|
||||
}[];
|
||||
};
|
||||
export class AIConfigParser {
|
||||
private config: AIConfig;
|
||||
result: ProviderResult;
|
||||
constructor(config: AIConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* 获取模型配置
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
getProvider(opts: GetProviderOpts): ProviderResult {
|
||||
const { model, group, decryptKey } = opts;
|
||||
const modelConfig = this.config.models.find((m) => m.model === model && m.group === group);
|
||||
const groupConfig = this.config.secretKeys.find((m) => m.group === group);
|
||||
if (!modelConfig) {
|
||||
throw new Error(`在模型组 ${group} 中未找到模型 ${model}`);
|
||||
}
|
||||
const mergeConfig = {
|
||||
...modelConfig,
|
||||
...groupConfig,
|
||||
decryptKey: decryptKey || groupConfig?.decryptKey,
|
||||
};
|
||||
// 验证模型配置
|
||||
if (!mergeConfig.provider) {
|
||||
throw new Error(`模型 ${model} 未配置提供商`);
|
||||
}
|
||||
if (!mergeConfig.model) {
|
||||
throw new Error(`模型 ${model} 未配置模型名称`);
|
||||
}
|
||||
if (!mergeConfig.apiKey) {
|
||||
throw new Error(`组 ${group} 未配置 API 密钥`);
|
||||
}
|
||||
if (!mergeConfig.group) {
|
||||
throw new Error(`组 ${group} 未配置`);
|
||||
}
|
||||
this.result = mergeConfig;
|
||||
return mergeConfig;
|
||||
}
|
||||
/**
|
||||
* 获取解密密钥
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
async getSecretKey(opts?: {
|
||||
getCache?: (key: string) => Promise<string>;
|
||||
setCache?: (key: string, value: string) => Promise<void>;
|
||||
providerResult?: ProviderResult;
|
||||
}) {
|
||||
const { getCache, setCache, providerResult } = opts || {};
|
||||
const { apiKey, decryptKey, group = '', model } = providerResult || this.result;
|
||||
const cacheKey = `${group}--${model}`;
|
||||
if (!decryptKey) {
|
||||
return apiKey;
|
||||
}
|
||||
if (getCache) {
|
||||
const cache = await getCache(cacheKey);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
const secretKey = decryptAES(apiKey, decryptKey);
|
||||
if (setCache) {
|
||||
await setCache(cacheKey, secretKey);
|
||||
}
|
||||
return secretKey;
|
||||
}
|
||||
/**
|
||||
* 加密
|
||||
* @param plainText
|
||||
* @param secretKey
|
||||
* @returns
|
||||
*/
|
||||
encrypt(plainText: string, secretKey: string) {
|
||||
return encryptAES(plainText, secretKey);
|
||||
}
|
||||
/**
|
||||
* 解密
|
||||
* @param cipherText
|
||||
* @param secretKey
|
||||
* @returns
|
||||
*/
|
||||
decrypt(cipherText: string, secretKey: string) {
|
||||
return decryptAES(cipherText, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型配置
|
||||
* @returns
|
||||
*/
|
||||
getSelectOpts() {
|
||||
const { models, secretKeys = [] } = this.config;
|
||||
|
||||
return models.map((model) => {
|
||||
const selectOpts = secretKeys.find((m) => m.group === model.group);
|
||||
return {
|
||||
...model,
|
||||
...selectOpts,
|
||||
};
|
||||
});
|
||||
}
|
||||
getConfig(keepSecret?: boolean, config?: AIConfig) {
|
||||
const chatConfig = config ?? this.config;
|
||||
if (keepSecret) {
|
||||
return chatConfig;
|
||||
}
|
||||
// 过滤掉secret中的所有apiKey,移除掉并返回chatConfig
|
||||
const { secretKeys = [], ...rest } = chatConfig || {};
|
||||
return {
|
||||
...rest,
|
||||
secretKeys: secretKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
apiKey: undefined,
|
||||
decryptKey: undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
34
src/provider/utils/token.ts
Normal file
34
src/provider/utils/token.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { encoding_for_model, get_encoding } from 'tiktoken';
|
||||
|
||||
|
||||
const MODEL_TO_ENCODING = {
|
||||
'gpt-4': 'cl100k_base',
|
||||
'gpt-4-turbo': 'cl100k_base',
|
||||
'gpt-3.5-turbo': 'cl100k_base',
|
||||
'text-embedding-ada-002': 'cl100k_base',
|
||||
'text-davinci-002': 'p50k_base',
|
||||
'text-davinci-003': 'p50k_base',
|
||||
} as const;
|
||||
|
||||
export function numTokensFromString(text: string, model: keyof typeof MODEL_TO_ENCODING = 'gpt-3.5-turbo'): number {
|
||||
try {
|
||||
// 对于特定模型使用专门的编码器
|
||||
const encoder = encoding_for_model(model);
|
||||
const tokens = encoder.encode(text);
|
||||
const tokenCount = tokens.length;
|
||||
encoder.free(); // 释放编码器
|
||||
return tokenCount;
|
||||
} catch (error) {
|
||||
try {
|
||||
// 如果模型特定的编码器失败,尝试使用基础编码器
|
||||
const encoder = get_encoding(MODEL_TO_ENCODING[model]);
|
||||
const tokens = encoder.encode(text);
|
||||
const tokenCount = tokens.length;
|
||||
encoder.free(); // 释放编码器
|
||||
return tokenCount;
|
||||
} catch (error) {
|
||||
// 如果编码失败,使用一个粗略的估计:平均每个字符0.25个token
|
||||
return Math.ceil(text.length * 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user