feat: 1) Present 'maxtokens' as properties tied to a single model. 2) Remove the original author's implementation of the send verification logic and replace it with a user input validator. Pre-verification 3) Provides the ability to pull the 'User Visible modellist' provided by 'provider' 4) Provider-related parameters are passed in the constructor of 'providerClient'. Not passed in the 'chat' method

This commit is contained in:
Dean-YZG
2024-05-17 21:11:21 +08:00
parent 74a6e1260e
commit 8093d1ffba
30 changed files with 883 additions and 581 deletions

View File

@@ -1,9 +1,9 @@
export * from "./types";
export * from "../common/types";
export * from "./providerClient";
export * from "./modelClient";
export * from "./locale";
export * from "../common/locale";
export * from "./shim";

View File

@@ -1,19 +0,0 @@
import { Lang, getLang } from "@/app/locales";
interface PlainConfig {
[k: string]: PlainConfig | string;
}
export type LocaleMap<
TextPlainConfig extends PlainConfig,
Default extends Lang,
> = Partial<Record<Lang, TextPlainConfig>> & {
[name in Default]: TextPlainConfig;
};
export function getLocaleText<
TextPlainConfig extends PlainConfig,
DefaultLang extends Lang,
>(textMap: LocaleMap<TextPlainConfig, DefaultLang>, defaultLang: DefaultLang) {
return textMap[getLang()] || textMap[defaultLang];
}

View File

@@ -1,23 +1,28 @@
import { ChatRequestPayload, Model, ModelConfig, ChatHandlers } from "./types";
import { ProviderClient, ProviderTemplateName } from "./providerClient";
import {
ChatRequestPayload,
Model,
ModelSettings,
InternalChatHandlers,
} from "../common";
import { Provider, ProviderClient } from "./providerClient";
export class ModelClient {
static getAllProvidersDefaultModels = () => {
return ProviderClient.getAllProvidersDefaultModels();
};
constructor(
private model: Model,
private modelConfig: ModelConfig,
private modelSettings: ModelSettings,
private providerClient: ProviderClient,
) {}
chat(payload: ChatRequestPayload, handlers: ChatHandlers) {
chat(payload: ChatRequestPayload, handlers: InternalChatHandlers) {
try {
return this.providerClient.streamChat(
{
...payload,
modelConfig: this.modelConfig,
modelConfig: {
...this.modelSettings,
max_tokens:
this.model.max_tokens ?? this.modelSettings.global_max_tokens,
},
model: this.model.name,
},
handlers,
@@ -31,7 +36,11 @@ export class ModelClient {
try {
return this.providerClient.chat({
...payload,
modelConfig: this.modelConfig,
modelConfig: {
...this.modelSettings,
max_tokens:
this.model.max_tokens ?? this.modelSettings.global_max_tokens,
},
model: this.model.name,
});
} catch (e) {
@@ -40,7 +49,50 @@ export class ModelClient {
}
}
export function ModelClientFactory(model: Model, modelConfig: ModelConfig) {
const providerClient = new ProviderClient(model.providerTemplateName);
return new ModelClient(model, modelConfig, providerClient);
// must generate new ModelClient during every chat
export function ModelClientFactory(
model: Model,
provider: Provider,
modelSettings: ModelSettings,
) {
const providerClient = new ProviderClient(provider);
return new ModelClient(model, modelSettings, providerClient);
}
export function getFiltertModels(
models: readonly Model[],
customModels: string,
) {
const modelTable: Record<string, Model> = {};
// default models
models.forEach((m) => {
modelTable[m.name] = m;
});
// server custom models
customModels
.split(",")
.filter((v) => !!v && v.length > 0)
.forEach((m) => {
const available = !m.startsWith("-");
const nameConfig =
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
const [name, displayName] = nameConfig.split("=");
// enable or disable all models
if (name === "all") {
Object.values(modelTable).forEach(
(model) => (model.available = available),
);
} else {
modelTable[name] = {
...modelTable[name],
displayName,
available,
};
}
});
return modelTable;
}

View File

@@ -1,118 +1,182 @@
import {
ChatHandlers,
IProviderTemplate,
InternalChatHandlers,
Model,
ModelTemplate,
StandChatReponseMessage,
StandChatRequestPayload,
} from "./types";
} from "../common";
import * as ProviderTemplates from "@/app/client/providers";
import { cloneDeep } from "lodash-es";
import { nanoid } from "nanoid";
export type ProviderTemplate =
(typeof ProviderTemplates)[keyof typeof ProviderTemplates];
export type ProviderTemplate = IProviderTemplate<any, any, any>;
export type ProviderTemplateName =
(typeof ProviderTemplates)[keyof typeof ProviderTemplates]["prototype"]["name"];
export interface Provider<
Providerconfig extends Record<string, any> = Record<string, any>,
> {
name: string; // id of provider
isActive: boolean;
providerTemplateName: ProviderTemplateName;
providerConfig: Providerconfig;
isDefault: boolean; // Not allow to modify models of default provider
updated: boolean; // provider initial is finished
displayName: string;
models: Model[];
}
const providerTemplates = Object.values(ProviderTemplates).reduce(
(r, t) => ({
...r,
[t.prototype.name]: new t(),
}),
{} as Record<ProviderTemplateName, ProviderTemplate>,
);
export class ProviderClient {
provider: IProviderTemplate<any, any, any>;
providerTemplate: IProviderTemplate<any, any, any>;
static ProviderTemplates = ProviderTemplates;
static getAllProvidersDefaultModels = () => {
return Object.values(ProviderClient.ProviderTemplates).reduce(
(r, p) => ({
...r,
[p.prototype.name]: cloneDeep(p.prototype.models),
}),
{} as Record<ProviderTemplateName, Model[]>,
);
};
static ProviderTemplates = providerTemplates;
static getAllProviderTemplates = () => {
return Object.values(ProviderClient.ProviderTemplates).reduce(
(r, p) => ({
return Object.values(providerTemplates).reduce(
(r, t) => ({
...r,
[p.prototype.name]: p,
[t.name]: t,
}),
{} as Record<ProviderTemplateName, ProviderTemplate>,
);
};
static getProviderTemplateList = () => {
return Object.values(ProviderClient.ProviderTemplates);
static getProviderTemplateMetaList = () => {
return Object.values(providerTemplates).map((t) => ({
...t.providerMeta,
name: t.name,
}));
};
constructor(providerTemplateName: string) {
this.provider = this.getProviderTemplate(providerTemplateName);
}
get settingItems() {
const { providerMeta } = this.provider;
const { settingItems } = providerMeta;
return settingItems;
constructor(private provider: Provider) {
const { providerTemplateName } = provider;
this.providerTemplate = this.getProviderTemplate(providerTemplateName);
}
private getProviderTemplate(providerTemplateName: string) {
const providerTemplate =
Object.values(ProviderTemplates).find(
(template) => template.prototype.name === providerTemplateName,
) || ProviderTemplates.NextChatProvider;
const providerTemplate = Object.values(providerTemplates).find(
(template) => template.name === providerTemplateName,
);
return new providerTemplate();
return providerTemplate || providerTemplates.openai;
}
getModelConfig(modelName: string) {
private getModelConfig(modelName: string) {
const { models } = this.provider;
return (
models.find((config) => config.name === modelName) ||
models.find((config) => config.isDefaultSelected)
models.find((m) => m.name === modelName) ||
models.find((m) => m.isDefaultSelected)
);
}
getAvailableModels() {
return Promise.resolve(
this.providerTemplate.getAvailableModels?.(this.provider.providerConfig),
)
.then((res) => {
const { defaultModels } = this.providerTemplate;
const availableModelsSet = new Set(
(res ?? defaultModels).map((o) => o.name),
);
return defaultModels.filter((m) => availableModelsSet.has(m.name));
})
.catch(() => {
return this.providerTemplate.defaultModels;
});
}
async chat(
payload: StandChatRequestPayload<string>,
payload: StandChatRequestPayload,
): Promise<StandChatReponseMessage> {
return this.provider.chat({
return this.providerTemplate.chat({
...payload,
stream: false,
isVisionModel: this.getModelConfig(payload.model)?.isVisionModel,
providerConfig: this.provider.providerConfig,
});
}
streamChat(payload: StandChatRequestPayload<string>, handlers: ChatHandlers) {
return this.provider.streamChat(
streamChat(payload: StandChatRequestPayload, handlers: InternalChatHandlers) {
let responseText = "";
let remainText = "";
const timer = this.providerTemplate.streamChat(
{
...payload,
stream: true,
isVisionModel: this.getModelConfig(payload.model)?.isVisionModel,
providerConfig: this.provider.providerConfig,
},
{
onProgress: (chunk) => {
remainText += chunk;
},
onError: (err) => {
handlers.onError(err);
},
onFinish: () => {},
onFlash: (message: string) => {
handlers.onFinish(message);
},
},
handlers.onProgress,
handlers.onFinish,
handlers.onError,
);
timer.signal.onabort = () => {
const message = responseText + remainText;
remainText = "";
handlers.onFinish(message);
};
const animateResponseText = () => {
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
handlers.onProgress(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
};
// start animaion
animateResponseText();
return timer;
}
}
export interface Provider {
name: string; // id of provider
displayName: string;
isActive: boolean;
providerTemplateName: ProviderTemplateName;
models: Model[];
}
type Params = Omit<Provider, "providerTemplateName" | "name" | "isDefault">;
function createProvider(
provider: ProviderTemplateName,
params?: Omit<Provider, "providerTemplateName">,
isDefault: true,
): Provider;
function createProvider(provider: ProviderTemplate, isDefault: true): Provider;
function createProvider(
provider: ProviderTemplateName,
isDefault: false,
params: Params,
): Provider;
function createProvider(
provider: ProviderTemplate,
params?: Omit<Provider, "providerTemplateName">,
isDefault: false,
params: Params,
): Provider;
function createProvider(
provider: ProviderTemplate | ProviderTemplateName,
params?: Omit<Provider, "providerTemplateName">,
isDefault: boolean,
params?: Params,
): Provider {
let providerTemplate: ProviderTemplate;
if (typeof provider === "string") {
@@ -120,17 +184,41 @@ function createProvider(
} else {
providerTemplate = provider;
}
const name = `${providerTemplate.name}__${nanoid()}`;
const {
name = providerTemplate.prototype.name,
displayName = providerTemplate.prototype.providerMeta.displayName,
models = providerTemplate.prototype.models,
displayName = providerTemplate.providerMeta.displayName,
models = providerTemplate.defaultModels.map((m) =>
createModelFromModelTemplate(m, providerTemplate, name),
),
providerConfig,
} = params ?? {};
return {
name,
displayName,
isActive: true,
models,
providerTemplateName: providerTemplate.prototype.name,
providerTemplateName: providerTemplate.name,
providerConfig: isDefault ? {} : providerConfig!,
isDefault,
updated: true,
};
}
function createModelFromModelTemplate(
m: ModelTemplate,
p: ProviderTemplate,
providerName: string,
) {
return {
...m,
providerTemplateName: p.name,
providerName,
isActive: m.isDefaultActive,
available: true,
customized: false,
};
}

View File

@@ -1,164 +0,0 @@
import { RequestMessage } from "../api";
// ===================================== LLM Types start ======================================
export interface ModelConfig {
temperature: number;
top_p: number;
presence_penalty: number;
frequency_penalty: number;
max_tokens: number;
}
export type Model = {
name: string; // id of model in a provider
displayName: string;
isVisionModel?: boolean;
isDefaultActive: boolean; // model is initialized to be active
isDefaultSelected?: boolean; // model is initialized to be as default used model
providerTemplateName: string;
};
// ===================================== LLM Types end ======================================
// ===================================== Chat Request Types start ======================================
export interface ChatRequestPayload<SettingKeys extends string = ""> {
messages: RequestMessage[];
providerConfig: Record<SettingKeys, string>;
context: {
isApp: boolean;
};
}
export interface StandChatRequestPayload<SettingKeys extends string = "">
extends ChatRequestPayload<SettingKeys> {
modelConfig: ModelConfig;
model: string;
}
export interface InternalChatRequestPayload<SettingKeys extends string = "">
extends StandChatRequestPayload<SettingKeys> {
isVisionModel: Model["isVisionModel"];
stream: boolean;
}
export interface ProviderRequestPayload {
headers: Record<string, string>;
body: string;
url: string;
method: string;
}
export interface ChatHandlers {
onProgress: (message: string, chunk: string) => void;
onFinish: (message: string) => void;
onError: (err: Error) => void;
}
// ===================================== Chat Request Types end ======================================
// ===================================== Chat Response Types start ======================================
export interface StandChatReponseMessage {
message: string;
}
// ===================================== Chat Request Types end ======================================
// ===================================== Provider Settings Types start ======================================
type NumberRange = [number, number];
export type Validator =
| "required"
| "number"
| "string"
| NumberRange
| NumberRange[];
export type CommonSettingItem<SettingKeys extends string> = {
name: SettingKeys;
title?: string;
description?: string;
validators?: Validator[];
};
export type InputSettingItem = {
type: "input";
placeholder?: string;
} & (
| {
inputType?: "password" | "normal";
defaultValue?: string;
}
| {
inputType?: "number";
defaultValue?: number;
}
);
export type SelectSettingItem = {
type: "select";
options: {
name: string;
value: "number" | "string" | "boolean";
}[];
placeholder?: string;
};
export type RangeSettingItem = {
type: "range";
range: NumberRange;
};
export type SwitchSettingItem = {
type: "switch";
};
export type SettingItem<SettingKeys extends string = ""> =
CommonSettingItem<SettingKeys> &
(
| InputSettingItem
| SelectSettingItem
| RangeSettingItem
| SwitchSettingItem
);
// ===================================== Provider Settings Types end ======================================
// ===================================== Provider Template Types start ======================================
export interface IProviderTemplate<
SettingKeys extends string,
NAME extends string,
Meta extends Record<string, any>,
> {
readonly name: NAME;
readonly metas: Meta;
readonly providerMeta: {
displayName: string;
settingItems: SettingItem<SettingKeys>[];
};
readonly models: Model[];
// formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>): ProviderRequestPayload;
// readWholeMessageResponseBody(res: WholeMessageResponseBody): StandChatReponseMessage;
streamChat(
payload: InternalChatRequestPayload<SettingKeys>,
onProgress?: (message: string, chunk: string) => void,
onFinish?: (message: string) => void,
onError?: (err: Error) => void,
): AbortController;
chat(
payload: InternalChatRequestPayload<SettingKeys>,
): Promise<StandChatReponseMessage>;
}
export interface Serializable<Snapshot> {
serialize(): Snapshot;
}