@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
255 lines (223 loc) • 7.79 kB
text/typescript
/**
* @see https://github.com/lobehub/lobe-chat/discussions/6563
*/
import type { ChatModelCard } from '@lobechat/types';
import OpenAI, { ClientOptions } from 'openai';
import { Stream } from 'openai/streaming';
import { LobeOpenAI } from '../../providers/openai';
import {
CreateImagePayload,
CreateImageResponse,
GenerateObjectOptions,
GenerateObjectPayload,
ILobeAgentRuntimeErrorType,
} from '../../types';
import {
type ChatCompletionErrorPayload,
ChatMethodOptions,
ChatStreamCallbacks,
ChatStreamPayload,
EmbeddingsOptions,
EmbeddingsPayload,
TextToImagePayload,
TextToSpeechPayload,
} from '../../types';
import { postProcessModelList } from '../../utils/postProcessModelList';
import { LobeRuntimeAI } from '../BaseAI';
import { CreateImageOptions, CustomClientOptions } from '../openaiCompatibleFactory';
import { baseRuntimeMap } from './baseRuntimeMap';
export interface RuntimeItem {
id: string;
models?: string[];
runtime: LobeRuntimeAI;
}
interface ProviderIniOptions extends Record<string, any> {
accessKeyId?: string;
accessKeySecret?: string;
apiKey?: string;
apiVersion?: string;
baseURL?: string;
baseURLOrAccountID?: string;
dangerouslyAllowBrowser?: boolean;
region?: string;
sessionToken?: string;
}
export type RuntimeClass = typeof LobeOpenAI;
interface RouterInstance {
apiType: keyof typeof baseRuntimeMap;
models?: string[];
options: ProviderIniOptions;
runtime?: RuntimeClass;
}
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
type Routers =
| RouterInstance[]
| ((
options: ClientOptions & Record<string, any>,
runtimeContext: {
model?: string;
},
) => RouterInstance[] | Promise<RouterInstance[]>);
export interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
apiKey?: string;
chatCompletion?: {
excludeUsage?: boolean;
handleError?: (
error: any,
options: ConstructorOptions<T>,
) => Omit<ChatCompletionErrorPayload, 'provider'> | undefined;
handlePayload?: (
payload: ChatStreamPayload,
options: ConstructorOptions<T>,
) => OpenAI.ChatCompletionCreateParamsStreaming;
handleStream?: (
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number },
) => ReadableStream;
handleStreamBizErrorType?: (error: {
message: string;
name: string;
}) => ILobeAgentRuntimeErrorType | undefined;
handleTransformResponseToStream?: (
data: OpenAI.ChatCompletion,
) => ReadableStream<OpenAI.ChatCompletionChunk>;
noUserId?: boolean;
};
constructorOptions?: ConstructorOptions<T>;
createImage?: (
payload: CreateImagePayload,
options: CreateImageOptions,
) => Promise<CreateImageResponse>;
customClient?: CustomClientOptions<T>;
debug?: {
chatCompletion: () => boolean;
responses?: () => boolean;
};
defaultHeaders?: Record<string, any>;
errorType?: {
bizError: ILobeAgentRuntimeErrorType;
invalidAPIKey: ILobeAgentRuntimeErrorType;
};
id: string;
models?:
| ((params: { client: OpenAI }) => Promise<ChatModelCard[]>)
| {
transformModel?: (model: OpenAI.Model) => ChatModelCard;
};
responses?: {
handlePayload?: (
payload: ChatStreamPayload,
options: ConstructorOptions<T>,
) => ChatStreamPayload;
};
routers: Routers;
}
export const createRouterRuntime = ({
id,
routers,
apiKey: DEFAULT_API_LEY,
models: modelsOption,
...params
}: CreateRouterRuntimeOptions) => {
return class UniformRuntime implements LobeRuntimeAI {
public _options: ClientOptions & Record<string, any>;
private _routers: Routers;
private _params: any;
private _id: string;
constructor(options: ClientOptions & Record<string, any> = {}) {
this._options = {
...options,
apiKey: options.apiKey?.trim() || DEFAULT_API_LEY,
baseURL: options.baseURL?.trim(),
};
// 保存配置但不创建 runtimes
this._routers = routers;
this._params = params;
this._id = id;
}
/**
* TODO: 考虑添加缓存机制,避免重复创建相同配置的 runtimes
*/
private async createRuntimesByRouters(model?: string): Promise<RuntimeItem[]> {
// 动态获取 routers,支持传入 model
const resolvedRouters =
typeof this._routers === 'function'
? await this._routers(this._options, { model })
: this._routers;
if (resolvedRouters.length === 0) {
throw new Error('empty providers');
}
return resolvedRouters.map((router) => {
const providerAI = router.runtime ?? baseRuntimeMap[router.apiType] ?? LobeOpenAI;
const finalOptions = { ...this._params, ...this._options, ...router.options };
const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id: this._id });
return {
id: router.apiType,
models: router.models,
runtime,
};
});
}
// Check if it can match a specific model, otherwise default to using the last runtime
async getRuntimeByModel(model: string) {
const runtimes = await this.createRuntimesByRouters(model);
for (const runtimeItem of runtimes) {
const models = runtimeItem.models || [];
if (models.includes(model)) {
return runtimeItem.runtime;
}
}
return runtimes.at(-1)!.runtime;
}
async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
try {
const runtime = await this.getRuntimeByModel(payload.model);
return await runtime.chat!(payload, options);
} catch (e) {
if (params.chatCompletion?.handleError) {
const error = params.chatCompletion.handleError(e, this._options);
if (error) {
throw error;
}
}
throw e;
}
}
async generateObject(payload: GenerateObjectPayload, options?: GenerateObjectOptions) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.generateObject!(payload, options);
}
async createImage(payload: CreateImagePayload) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.createImage!(payload);
}
async textToImage(payload: TextToImagePayload) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.textToImage!(payload);
}
async models() {
if (modelsOption && typeof modelsOption === 'function') {
// 延迟创建 runtimes
const runtimes = await this.createRuntimesByRouters();
// 如果是函数式配置,使用最后一个运行时的客户端来调用函数
const lastRuntime = runtimes.at(-1)?.runtime;
if (lastRuntime && 'client' in lastRuntime) {
const modelList = await modelsOption({ client: (lastRuntime as any).client });
return await postProcessModelList(modelList);
}
}
// 延迟创建 runtimes
const runtimes = await this.createRuntimesByRouters();
return runtimes.at(-1)?.runtime.models?.();
}
async embeddings(payload: EmbeddingsPayload, options?: EmbeddingsOptions) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.embeddings!(payload, options);
}
async textToSpeech(payload: TextToSpeechPayload, options?: EmbeddingsOptions) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.textToSpeech!(payload, options);
}
};
};
export type UniformRuntime = InstanceType<ReturnType<typeof createRouterRuntime>>;