mirror of
https://github.com/cs-lazy-tools/ChatGPT-On-CS.git
synced 2026-08-28 18:17:40 +08:00
add: 添加多平台的支持
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
PY_HOSTNAME=localhost
|
||||
PY_PORT=9999
|
||||
VAR=1234
|
||||
BKEXE_PATH=./backend/__main__.exe
|
||||
BKEXE_PATH=./backend/__main__.exe
|
||||
PKG_VERSION=0.0.1
|
||||
@@ -30,6 +30,10 @@ module.exports = {
|
||||
'max-classes-per-file': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
camelcase: 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'no-dupe-class-members': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { BroadcastService } from './services/broadcastService';
|
||||
import { SystemService } from './services/systemService';
|
||||
import { ConfigService } from './services/configService';
|
||||
import { PlatformConfigController } from './controllers/platformConfigController';
|
||||
import { AutoReplyController } from './controllers/autoReplyController';
|
||||
import { AutoReplyController } from './controllers/keywordReplyController';
|
||||
|
||||
const sessionController = new SessionController();
|
||||
const configController = new ConfigController();
|
||||
@@ -93,7 +93,6 @@ class BKServer {
|
||||
console.log('Client connected registerHandlers');
|
||||
this.messageService.registerHandlers(socket);
|
||||
this.sessionService.registerHandlers(socket);
|
||||
this.configService.registerHandlers(socket);
|
||||
this.broadcastService.registerHandlers(socket);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
export const ALL_PLATFORMS = [
|
||||
{
|
||||
id: 'bilibili',
|
||||
name: 'bilibili',
|
||||
},
|
||||
{
|
||||
id: 'douyin',
|
||||
name: '抖音',
|
||||
},
|
||||
{
|
||||
id: 'douyin_mp',
|
||||
name: '抖音企业号',
|
||||
},
|
||||
{
|
||||
id: 'win_jinmai',
|
||||
name: '京卖',
|
||||
},
|
||||
{
|
||||
id: 'jinritemai',
|
||||
name: '抖店',
|
||||
},
|
||||
{
|
||||
id: 'win_qianniu',
|
||||
name: '千牛',
|
||||
},
|
||||
{
|
||||
id: 'win_wechat',
|
||||
name: '微信',
|
||||
},
|
||||
{
|
||||
id: 'win_wecom',
|
||||
name: '企微(Bate版)',
|
||||
},
|
||||
{
|
||||
id: 'weibo',
|
||||
name: '微博私信',
|
||||
},
|
||||
{
|
||||
id: 'xiaohongshu',
|
||||
name: '小红书评论',
|
||||
},
|
||||
{
|
||||
id: 'xiaohongshu_pro',
|
||||
name: '小红书私信',
|
||||
},
|
||||
{
|
||||
id: 'zhihu',
|
||||
name: '知乎',
|
||||
},
|
||||
];
|
||||
|
||||
// # 固定会传递的上下文参数
|
||||
// CTX_APP_NAME = "app_name"
|
||||
// CTX_APP_ID = "app_id"
|
||||
// CTX_INSTANCE_ID = "instance_id"
|
||||
|
||||
// CTX_USERNAME = "username"
|
||||
// CTX_PLATFORM = "platform"
|
||||
// CTX_HAS_NEW_MESSAGE = "has_new_message"
|
||||
// CTX_HAS_GROUP_MESSAGE = "has_group_message"
|
||||
|
||||
// # 千牛平台特有的上下文参数
|
||||
// CTX_CURRENT_GOODS = "CTX_CURRENT_GOODS" # 当前商品
|
||||
// CTX_CURRENT_GOODS_ID = "CTX_CURRENT_GOODS_ID" # 当前商品 ID
|
||||
// CTX_MEMBER_TAG = "CTX_MEMBER_TAG" # 会员标签
|
||||
// CTX_FAN_TAG = "CTX_FAN_TAG" # 粉丝标签
|
||||
// CTX_NEW_CUSTOMER_TAG = "CTX_NEW_CUSTOMER_TAG" # 新客标签
|
||||
|
||||
export const CTX_APP_NAME = 'app_name';
|
||||
export const CTX_APP_ID = 'app_id';
|
||||
export const CTX_INSTANCE_ID = 'instance_id';
|
||||
|
||||
@@ -1,44 +1,37 @@
|
||||
import { Config } from '../entities/config';
|
||||
import { Plugin } from '../entities/plugin';
|
||||
|
||||
export class ConfigController {
|
||||
async getConfig(): Promise<Config> {
|
||||
try {
|
||||
const config = await Config.findByPk(1);
|
||||
if (!config) {
|
||||
throw new Error('Config not found');
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
const newConfig = await Config.create({
|
||||
extract_phone: true,
|
||||
extract_product: true,
|
||||
save_path: '',
|
||||
reply_speed: 0,
|
||||
reply_random_speed: 0,
|
||||
default_reply: '',
|
||||
wait_humans_time: 60,
|
||||
context_count: 1,
|
||||
gpt_base_url: 'https://api.openai.com/v1',
|
||||
gpt_key: 'your-key',
|
||||
gpt_model: 'gpt-3.5-turbo',
|
||||
gpt_temperature: 0.7,
|
||||
gpt_top_p: 0.9,
|
||||
stream: true,
|
||||
use_lazy: false,
|
||||
lazy_key: 'your-key',
|
||||
});
|
||||
return newConfig;
|
||||
}
|
||||
async getByAppId(appId: string): Promise<Config | null> {
|
||||
const config = await Config.findOne({
|
||||
where: { platform_id: appId },
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
async updateConfig(id: number, configData: Partial<Config>): Promise<Config> {
|
||||
const config = await Config.findByPk(id);
|
||||
async getByInstanceId(instanceId: string): Promise<Config | null> {
|
||||
const config = await Config.findOne({
|
||||
where: { instance_id: instanceId },
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
async getGlobalConfig(): Promise<Config> {
|
||||
let config = await Config.findOne({
|
||||
where: { global: true },
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Config not found');
|
||||
config = await Config.create({
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await config.update(configData);
|
||||
return data;
|
||||
return config;
|
||||
}
|
||||
|
||||
async getPluginConfig(pluginId: number): Promise<Plugin | null> {
|
||||
const plugin = await Plugin.findByPk(pluginId);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { GlobalParam } from '../entities/globalParam';
|
||||
|
||||
export class GlobalParamController {
|
||||
async create(globalParamData: any) {
|
||||
return GlobalParam.create(globalParamData);
|
||||
}
|
||||
|
||||
async update(id: number, globalParamData: any) {
|
||||
const globalParam = await GlobalParam.findByPk(id);
|
||||
if (!globalParam) {
|
||||
throw new Error('GlobalParam not found');
|
||||
}
|
||||
return globalParam.update(globalParamData);
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
const globalParam = await GlobalParam.findByPk(id);
|
||||
if (!globalParam) {
|
||||
throw new Error('GlobalParam not found');
|
||||
}
|
||||
return globalParam.destroy();
|
||||
}
|
||||
|
||||
async list() {
|
||||
return GlobalParam.findAll();
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -1,17 +1,17 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import fs from 'fs';
|
||||
import { Op } from 'sequelize';
|
||||
import { AutoReply } from '../entities/autoReply';
|
||||
import { Keyword } from '../entities/keyword';
|
||||
import { ALL_PLATFORMS } from '../constants';
|
||||
import { getTempPath } from '../../utils';
|
||||
|
||||
export class AutoReplyController {
|
||||
async create(autoReplyData: any) {
|
||||
return AutoReply.create(autoReplyData);
|
||||
return Keyword.create(autoReplyData);
|
||||
}
|
||||
|
||||
async update(id: number, autoReplyData: any) {
|
||||
const autoReply = await AutoReply.findByPk(id);
|
||||
const autoReply = await Keyword.findByPk(id);
|
||||
if (!autoReply) {
|
||||
throw new Error('AutoReply not found');
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export class AutoReplyController {
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
const autoReply = await AutoReply.findByPk(id);
|
||||
const autoReply = await Keyword.findByPk(id);
|
||||
if (!autoReply) {
|
||||
throw new Error('AutoReply not found');
|
||||
}
|
||||
@@ -68,22 +68,22 @@ export class AutoReplyController {
|
||||
}
|
||||
});
|
||||
|
||||
const originalAutoReplies = await AutoReply.findAll();
|
||||
const originalAutoReplies = await Keyword.findAll();
|
||||
|
||||
try {
|
||||
// 先删除所有数据
|
||||
await AutoReply.destroy({ where: {} });
|
||||
await AutoReply.bulkCreate(autoReplies);
|
||||
await Keyword.destroy({ where: {} });
|
||||
await Keyword.bulkCreate(autoReplies);
|
||||
} catch (error) {
|
||||
// 如果插入失败,回滚数据
|
||||
// @ts-ignore
|
||||
await AutoReply.bulkCreate(originalAutoReplies);
|
||||
await Keyword.bulkCreate(originalAutoReplies);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exportExcel() {
|
||||
const autoReplies = await AutoReply.findAll();
|
||||
const autoReplies = await Keyword.findAll();
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('自动回复');
|
||||
@@ -144,13 +144,13 @@ export class AutoReplyController {
|
||||
}
|
||||
|
||||
async getKeywords(platformId: string) {
|
||||
const autoReplies = await AutoReply.findAll({
|
||||
const autoReplies = await Keyword.findAll({
|
||||
where: {
|
||||
platform_id: platformId,
|
||||
},
|
||||
});
|
||||
|
||||
const globalKeywords = await AutoReply.findAll({
|
||||
const globalKeywords = await Keyword.findAll({
|
||||
where: {
|
||||
platform_id: {
|
||||
[Op.or]: [null, ''],
|
||||
@@ -172,7 +172,7 @@ export class AutoReplyController {
|
||||
}) {
|
||||
try {
|
||||
const { rows: autoReplies, count: total } =
|
||||
await AutoReply.findAndCountAll({
|
||||
await Keyword.findAndCountAll({
|
||||
where: platformId
|
||||
? {
|
||||
platform_id: platformId,
|
||||
@@ -1,68 +0,0 @@
|
||||
import { PlatformConfig } from '../entities/platformConfig';
|
||||
|
||||
export class PlatformConfigController {
|
||||
async updateByPlatformId(
|
||||
platformId: string,
|
||||
settings: any,
|
||||
): Promise<PlatformConfig> {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { platform_id: platformId },
|
||||
});
|
||||
|
||||
const active = settings && settings.active ? settings.active : false;
|
||||
|
||||
if (platformConfig) {
|
||||
// @ts-ignore
|
||||
return this.update(platformConfig.id, { settings, active });
|
||||
}
|
||||
|
||||
return this.create({
|
||||
platform_id: platformId,
|
||||
settings,
|
||||
active,
|
||||
});
|
||||
}
|
||||
|
||||
async getByPlatformId(platformId: string) {
|
||||
let data = await PlatformConfig.findOne({
|
||||
where: { platform_id: platformId },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
// 创建一个对象
|
||||
data = await PlatformConfig.create({
|
||||
platform_id: platformId,
|
||||
active: false,
|
||||
settings: {},
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async create(platformConfigData: any): Promise<PlatformConfig> {
|
||||
const platformConfig = await PlatformConfig.create(platformConfigData);
|
||||
return platformConfig;
|
||||
}
|
||||
|
||||
async update(id: number, platformConfigData: Partial<PlatformConfig>) {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { id },
|
||||
});
|
||||
if (!platformConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
await platformConfig.update(platformConfigData);
|
||||
}
|
||||
|
||||
async delete(id: number): Promise<void> {
|
||||
const platformConfig = await PlatformConfig.findOne({
|
||||
where: { id },
|
||||
});
|
||||
if (!platformConfig) {
|
||||
return;
|
||||
}
|
||||
await platformConfig.destroy();
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,22 @@ export class SessionCreate {
|
||||
}
|
||||
|
||||
export class SessionController {
|
||||
async create(sessionData: SessionCreate) {
|
||||
// @ts-ignore
|
||||
return Session.create(sessionData);
|
||||
async createSession(
|
||||
platformId: string,
|
||||
platformName: string,
|
||||
username?: string,
|
||||
goodsName?: string,
|
||||
goodsAvatar?: string,
|
||||
): Promise<Session> {
|
||||
return Session.create({
|
||||
platform_id: platformId,
|
||||
platform: platformName,
|
||||
username: username || '',
|
||||
last_active: new Date(),
|
||||
goods_name: goodsName || '',
|
||||
goods_avatar: goodsAvatar || '',
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: number, sessionData: Session) {
|
||||
|
||||
@@ -4,6 +4,20 @@ import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
export class Config extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare global: boolean;
|
||||
|
||||
declare active: boolean;
|
||||
|
||||
declare platform: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare instance_id: string;
|
||||
|
||||
declare use_plugin: boolean;
|
||||
|
||||
declare plugin_id: number;
|
||||
|
||||
declare extract_phone: boolean;
|
||||
|
||||
declare extract_product: boolean;
|
||||
@@ -24,48 +38,7 @@ export class Config extends Model {
|
||||
|
||||
declare gpt_key: string;
|
||||
|
||||
declare use_dify: boolean;
|
||||
|
||||
declare gpt_model: string;
|
||||
|
||||
declare gpt_temperature: number;
|
||||
|
||||
declare gpt_top_p: number;
|
||||
|
||||
declare stream: boolean;
|
||||
}
|
||||
|
||||
export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
const tableDescription = await Config.describe();
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.reply_random_speed) {
|
||||
await sequelize
|
||||
.getQueryInterface()
|
||||
.addColumn('Config', 'reply_random_speed', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.context_count) {
|
||||
await sequelize.getQueryInterface().addColumn('Config', 'context_count', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.default_reply) {
|
||||
await sequelize.getQueryInterface().addColumn('Config', 'default_reply', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
declare llm_type: string;
|
||||
}
|
||||
|
||||
export function initConfig(sequelize: Sequelize) {
|
||||
@@ -76,6 +49,37 @@ export function initConfig(sequelize: Sequelize) {
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
global: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: true,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
instance_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
use_plugin: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: true,
|
||||
},
|
||||
plugin_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
extract_phone: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
@@ -110,43 +114,25 @@ export function initConfig(sequelize: Sequelize) {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true,
|
||||
},
|
||||
gpt_base_url: {
|
||||
base_url: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
gpt_key: {
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
use_dify: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: true,
|
||||
},
|
||||
gpt_model: {
|
||||
llm_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
gpt_temperature: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true,
|
||||
},
|
||||
gpt_top_p: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true,
|
||||
},
|
||||
stream: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: 'chatgpt',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Config',
|
||||
tableName: 'config',
|
||||
tableName: 'n_config',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
|
||||
checkAndAddFields(sequelize);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class GlobalParam extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare key: string;
|
||||
|
||||
declare value: string;
|
||||
}
|
||||
|
||||
export function initGlobalParam(sequelize: Sequelize) {
|
||||
GlobalParam.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'GlobalParam',
|
||||
tableName: 'global_params',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class AutoReply extends Model {
|
||||
export class Keyword extends Model {
|
||||
declare id: number; // Note that the `null assertion` `!` is required in strict mode.
|
||||
|
||||
declare keyword: string;
|
||||
@@ -13,8 +13,8 @@ export class AutoReply extends Model {
|
||||
declare platform_id: string;
|
||||
}
|
||||
|
||||
export function initAutoReply(sequelize: Sequelize) {
|
||||
AutoReply.init(
|
||||
export function initKeyword(sequelize: Sequelize) {
|
||||
Keyword.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
@@ -40,8 +40,8 @@ export function initAutoReply(sequelize: Sequelize) {
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'AutoReply',
|
||||
tableName: 'auto_reply',
|
||||
modelName: 'Keyword',
|
||||
tableName: 'keyword',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
@@ -10,7 +10,7 @@ export class Message extends Model {
|
||||
|
||||
declare content: string;
|
||||
|
||||
declare unique: string;
|
||||
declare sender: string;
|
||||
|
||||
declare msg_type: string;
|
||||
|
||||
@@ -31,19 +31,19 @@ export function initMessage(sequelize: Sequelize) {
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
sender: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
unique: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
msg_type: {
|
||||
type: DataTypes.STRING(55),
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
@@ -53,7 +53,7 @@ export function initMessage(sequelize: Sequelize) {
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Message',
|
||||
tableName: 'messages',
|
||||
tableName: 'n_messages',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
// Extend the Model class with the attributes interface
|
||||
export class PlatformConfig extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare settings: any;
|
||||
|
||||
declare active: boolean;
|
||||
}
|
||||
|
||||
export function initPlatformConfig(sequelize: Sequelize) {
|
||||
PlatformConfig.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
settings: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'PlatformConfig',
|
||||
tableName: 'platform_cfg', // 创建一个新的表,用于存储 JSON 格式的配置
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
export class Plugin extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare name: string;
|
||||
|
||||
declare enabled: boolean;
|
||||
|
||||
declare code: string;
|
||||
|
||||
declare platform: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare instance_id: string; // 可能是作用于单个实例的插件
|
||||
|
||||
declare created_at: Date;
|
||||
}
|
||||
|
||||
export function initPlugin(sequelize: Sequelize) {
|
||||
Plugin.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
instance_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Plugin',
|
||||
tableName: 'plugins',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -3,17 +3,13 @@ import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
export class Session extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare username: string;
|
||||
|
||||
declare last_active: Date;
|
||||
|
||||
declare platform: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare goods_name: string;
|
||||
declare instance_id: string; // 可能是作用于单个实例的插件
|
||||
|
||||
declare goods_avatar: string;
|
||||
declare context: string;
|
||||
|
||||
declare created_at: Date;
|
||||
}
|
||||
@@ -26,14 +22,6 @@ export function initSession(sequelize: Sequelize) {
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
last_active: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
@@ -42,12 +30,8 @@ export function initSession(sequelize: Sequelize) {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
goods_name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
goods_avatar: {
|
||||
type: DataTypes.STRING(255),
|
||||
context: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
@@ -58,7 +42,7 @@ export function initSession(sequelize: Sequelize) {
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'Session',
|
||||
tableName: 'sessions',
|
||||
tableName: 'n_sessions',
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,11 +6,10 @@ import * as path from 'path';
|
||||
import sqlite from 'sqlite3';
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { initConfig } from './entities/config';
|
||||
import { initPlatformConfig } from './entities/platformConfig';
|
||||
import { initSession } from './entities/session';
|
||||
import { initMessage } from './entities/message';
|
||||
import { AutoReply, initAutoReply } from './entities/autoReply';
|
||||
import { initGlobalParam } from './entities/globalParam';
|
||||
import { initPlugin } from './entities/plugin';
|
||||
import { Keyword, initKeyword } from './entities/keyword';
|
||||
|
||||
// Get user's documents directory path
|
||||
const DOCUMENTS_DIR = path.join(os.homedir(), 'Documents');
|
||||
@@ -35,15 +34,14 @@ const sequelize = new Sequelize({
|
||||
|
||||
// 初始化模型
|
||||
initConfig(sequelize);
|
||||
initPlatformConfig(sequelize);
|
||||
initSession(sequelize);
|
||||
initMessage(sequelize);
|
||||
initAutoReply(sequelize);
|
||||
initGlobalParam(sequelize);
|
||||
initKeyword(sequelize);
|
||||
initPlugin(sequelize);
|
||||
|
||||
// 异步初始化和数据填充函数
|
||||
async function initDb(): Promise<void> {
|
||||
const count = await AutoReply.count();
|
||||
const count = await Keyword.count();
|
||||
if (count === 0) {
|
||||
const replies = [
|
||||
{
|
||||
@@ -153,7 +151,7 @@ async function initDb(): Promise<void> {
|
||||
},
|
||||
];
|
||||
|
||||
await AutoReply.bulkCreate(replies);
|
||||
await Keyword.bulkCreate(replies);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
export class BroadcastService {
|
||||
private mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('broadcastService-sendBroadcast', (msg: any, callback) => {
|
||||
const { event_id: eventId, message } = msg;
|
||||
this.receiveBroadcast(msg);
|
||||
callback({
|
||||
event_id: eventId,
|
||||
event_type: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public receiveBroadcast(msg: any): void {
|
||||
this.mainWindow.webContents.send('broadcast', msg);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,29 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { PlatformConfigController } from '../controllers/platformConfigController';
|
||||
import { Config } from '../entities/config';
|
||||
import { CTX_APP_ID, CTX_INSTANCE_ID } from '../constants';
|
||||
|
||||
export class ConfigService {
|
||||
private configController: ConfigController;
|
||||
|
||||
private platformConfigController: PlatformConfigController;
|
||||
|
||||
constructor(
|
||||
configController: ConfigController,
|
||||
platformConfigController: PlatformConfigController,
|
||||
) {
|
||||
constructor(configController: ConfigController) {
|
||||
this.configController = configController;
|
||||
this.platformConfigController = platformConfigController;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('configService-getConfig', async (arg, callback) => {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
callback(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to get config', error);
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
public async get(ctx: any): Promise<Config> {
|
||||
const appId = ctx.get(CTX_APP_ID);
|
||||
const instanceId = ctx.get(CTX_INSTANCE_ID);
|
||||
|
||||
public async getConfigByPlatformId(platformId: string): Promise<Config> {
|
||||
const platformConfig =
|
||||
await this.platformConfigController.getByPlatformId(platformId);
|
||||
|
||||
const { settings } = platformConfig;
|
||||
const settingsVal =
|
||||
typeof settings === 'string' ? JSON.parse(settings) : settings;
|
||||
if (!settingsVal || !settingsVal.active) {
|
||||
return this.configController.getConfig();
|
||||
const instanceConfig =
|
||||
await this.configController.getByInstanceId(instanceId);
|
||||
if (instanceConfig) {
|
||||
return instanceConfig;
|
||||
}
|
||||
|
||||
const config = await this.configController.getConfig();
|
||||
const config = await this.configController.getByAppId(appId);
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
|
||||
return {
|
||||
id: config.id,
|
||||
extract_phone: config.extract_phone,
|
||||
extract_product: config.extract_product,
|
||||
save_path: config.save_path,
|
||||
reply_speed: config.reply_speed,
|
||||
reply_random_speed: config.reply_random_speed,
|
||||
wait_humans_time: config.wait_humans_time,
|
||||
context_count: settingsVal.contextCount ?? config.context_count,
|
||||
gpt_base_url: settingsVal.proxyAddress ?? config.gpt_base_url,
|
||||
gpt_key: settingsVal.apiKey ?? config.gpt_key,
|
||||
use_dify: settingsVal.useDify ?? config.use_dify,
|
||||
gpt_model: settingsVal.model ?? config.gpt_model,
|
||||
default_reply: settingsVal.defaultReply ?? config.default_reply,
|
||||
gpt_temperature: config.gpt_temperature,
|
||||
gpt_top_p: config.gpt_top_p,
|
||||
stream: config.stream,
|
||||
} as any;
|
||||
}
|
||||
|
||||
public async getConfig(): Promise<Config> {
|
||||
return this.configController.getConfig();
|
||||
return this.configController.getGlobalConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { Platform, StrategyServiceStatusEnum } from '../types';
|
||||
import { emitAndWait } from '../../utils';
|
||||
|
||||
export class DispatchService {
|
||||
private mainWindow: BrowserWindow;
|
||||
|
||||
private io: socketIo.Server;
|
||||
|
||||
constructor(mainWindow: BrowserWindow, io: socketIo.Server) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('messageService-broadcast', (msg: any, callback) => {
|
||||
const { event_id: eventId, message } = msg;
|
||||
this.receiveBroadcast(msg);
|
||||
callback({
|
||||
event_id: eventId,
|
||||
event_type: message,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('messageService-getMessages', async (data, callback) => {
|
||||
const { ctx, messages } = data;
|
||||
});
|
||||
}
|
||||
|
||||
public receiveBroadcast(msg: any): void {
|
||||
this.mainWindow.webContents.send('broadcast', msg);
|
||||
}
|
||||
|
||||
public async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
return await this.io.timeout(5000).emitWithAck('systemService-health');
|
||||
} catch (error) {
|
||||
console.error('Failed to check health', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
|
||||
try {
|
||||
return await emitAndWait(this.io, 'strategyService-updateStatus', {
|
||||
status,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update status', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllPlatforms(): Promise<Platform[]> {
|
||||
const maxRetries = 10;
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const data = await emitAndWait<Platform[]>(
|
||||
this.io,
|
||||
'strategyService-getAppsInfo',
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
attempt++;
|
||||
console.error(`Attempt ${attempt} failed to update strategies`, error);
|
||||
if (attempt >= maxRetries) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
import * as vm from 'vm';
|
||||
|
||||
// 静态导入预加载模块
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { MessageDTO, ReplyDTO } from '../types';
|
||||
|
||||
interface PreloadedModules {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 创建一个包含所有预加载模块的对象
|
||||
const preloadedModules: PreloadedModules = {
|
||||
fs,
|
||||
path,
|
||||
axios,
|
||||
};
|
||||
|
||||
class PluginSystem {
|
||||
private configController: ConfigController;
|
||||
|
||||
constructor(configController: ConfigController) {
|
||||
this.configController = configController;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行插件函数
|
||||
* @param plugin_id 插件的ID
|
||||
* @param ctx 上下文信息
|
||||
* @param messages 消息数组
|
||||
* @returns 插件函数的执行结果
|
||||
*/
|
||||
async executePlugin(
|
||||
plugin_id: number,
|
||||
ctx: any,
|
||||
messages: MessageDTO[],
|
||||
): Promise<ReplyDTO> {
|
||||
// 从配置控制器中获取插件配置
|
||||
const plugin = await this.configController.getPluginConfig(plugin_id);
|
||||
if (!plugin) {
|
||||
throw new Error('Plugin not found');
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建沙盒环境
|
||||
const sandbox = {
|
||||
module: {} as any,
|
||||
exports: {},
|
||||
require: (module: string) => {
|
||||
// 只允许加载预加载的模块
|
||||
if (preloadedModules[module]) {
|
||||
return preloadedModules[module];
|
||||
}
|
||||
throw new Error(`Module ${module} is not available`);
|
||||
},
|
||||
};
|
||||
|
||||
// =================== 插件代码示例 ===================
|
||||
// module.exports = function(ctx, messages) {
|
||||
// do something...
|
||||
// return {
|
||||
// content: 'Hello, world!',
|
||||
// type: 'TEXT',
|
||||
// };
|
||||
// };
|
||||
// =================== HTTP请求示例 ===================
|
||||
// module.exports = async function(ctx, messages) {
|
||||
// const response = await axios.get('https://api.example.com');
|
||||
// return {
|
||||
// content: response.data,
|
||||
// type: 'TEXT',
|
||||
// };
|
||||
// };
|
||||
// =================== 文件读取示例 ===================
|
||||
// module.exports = function(ctx, messages) {
|
||||
// const content = fs.readFileSync('example.txt', 'utf8');
|
||||
// return {
|
||||
// content,
|
||||
// type: 'TEXT',
|
||||
// };
|
||||
// };
|
||||
// ===================================================
|
||||
|
||||
// 插件代码包装模板,确保插件函数存在并导出
|
||||
const pluginCode = `
|
||||
const pluginFunction = ${plugin.code};
|
||||
if (typeof pluginFunction !== 'function') {
|
||||
throw new Error('Plugin function is not defined or not a function');
|
||||
}
|
||||
module.exports = pluginFunction;
|
||||
`;
|
||||
|
||||
// 创建并运行沙盒上下文
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(pluginCode, sandbox);
|
||||
|
||||
// 检查导出的是否为函数
|
||||
if (typeof sandbox.module.exports !== 'function') {
|
||||
throw new Error('Plugin does not export a function');
|
||||
}
|
||||
|
||||
// 执行插件函数并传递 ctx 和 messages
|
||||
const data = sandbox.module.exports(ctx, messages);
|
||||
|
||||
// data 的返回类型应该是 ReplyDTO,这里做个数据校验
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'content' in data &&
|
||||
'type' in data
|
||||
) {
|
||||
return data as ReplyDTO;
|
||||
}
|
||||
|
||||
throw new Error('Plugin function did not return a valid response');
|
||||
} catch (error) {
|
||||
// 捕获并返回错误信息
|
||||
console.error('Plugin execution error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginSystem;
|
||||
@@ -1,101 +0,0 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { SessionController } from '../controllers/sessionController';
|
||||
import { Session } from '../entities/session';
|
||||
import { StrategyService } from './strategyService';
|
||||
|
||||
export class SessionService {
|
||||
private sessionController: SessionController;
|
||||
|
||||
private strategyService: StrategyService;
|
||||
|
||||
constructor(
|
||||
sessionController: SessionController,
|
||||
strategyService: StrategyService,
|
||||
) {
|
||||
this.sessionController = sessionController;
|
||||
this.strategyService = strategyService;
|
||||
}
|
||||
|
||||
public registerHandlers(socket: socketIo.Socket): void {
|
||||
socket.on('sessionService-getSession', async (data: any, callback) => {
|
||||
const { platformId, username, goodsName, goodsAvatar } = data;
|
||||
|
||||
const ptf = await this.strategyService.getPlatformInfoById(platformId);
|
||||
const startTime = new Date().getTime();
|
||||
try {
|
||||
if (!username) {
|
||||
// 直接新建一个 session
|
||||
const session = await this.createSession(
|
||||
platformId,
|
||||
ptf?.name || '',
|
||||
goodsName,
|
||||
goodsAvatar,
|
||||
);
|
||||
|
||||
callback(session);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await this.getSessionOrCreate(
|
||||
platformId,
|
||||
username,
|
||||
goodsName,
|
||||
goodsAvatar,
|
||||
);
|
||||
callback(session);
|
||||
} catch (error) {
|
||||
console.error('Failed to get session', error);
|
||||
callback(null);
|
||||
} finally {
|
||||
console.log('getSession time:', new Date().getTime() - startTime, 'ms');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
platformId: string,
|
||||
platformName: string,
|
||||
goodsName?: string,
|
||||
goodsAvatar?: string,
|
||||
): Promise<Session> {
|
||||
return this.sessionController.create({
|
||||
platform_id: platformId,
|
||||
platform: platformName,
|
||||
username: '',
|
||||
last_active: new Date(),
|
||||
goods_name: goodsName || '',
|
||||
goods_avatar: goodsAvatar || '',
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getSessionOrCreate(
|
||||
platformId: string,
|
||||
username: string,
|
||||
goodsName?: string,
|
||||
goodsAvatar?: string,
|
||||
): Promise<Session> {
|
||||
const sessions = await this.sessionController.search(platformId, username);
|
||||
if (sessions.length > 0) {
|
||||
const session = sessions[0];
|
||||
session.last_active = new Date();
|
||||
if (goodsName && goodsAvatar) {
|
||||
session.goods_name = goodsName;
|
||||
session.goods_avatar = goodsAvatar;
|
||||
}
|
||||
return this.sessionController.update(session.id, session);
|
||||
}
|
||||
|
||||
const ptf = await this.strategyService.getPlatformInfoById(platformId);
|
||||
|
||||
return this.sessionController.create({
|
||||
platform_id: platformId,
|
||||
platform: ptf?.name || '',
|
||||
username,
|
||||
last_active: new Date(),
|
||||
goods_name: goodsName || '',
|
||||
goods_avatar: goodsAvatar || '',
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import socketIo from 'socket.io';
|
||||
import { Platform, StrategyServiceStatusEnum } from '../types';
|
||||
|
||||
export class StrategyService {
|
||||
private io: socketIo.Server;
|
||||
|
||||
constructor(io: socketIo.Server) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async emitAndWait<T>(
|
||||
event: string,
|
||||
data?: any,
|
||||
timeout: number = 5000,
|
||||
): Promise<T> {
|
||||
let response;
|
||||
if (data === undefined) {
|
||||
response = await this.io.timeout(timeout).emitWithAck(event);
|
||||
} else {
|
||||
response = await this.io.timeout(timeout).emitWithAck(event, data);
|
||||
}
|
||||
|
||||
// 判断是否是 Array
|
||||
if (Array.isArray(response) && response.length === 1) {
|
||||
if (typeof response[0] === 'undefined') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
if (response[0] === 'string') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
// 如果 response[0] 也是数组,那么直接返回
|
||||
if (Array.isArray(response[0])) {
|
||||
return response[0] as T;
|
||||
}
|
||||
|
||||
return JSON.parse(response[0]);
|
||||
}
|
||||
|
||||
// 判断是否是字符串
|
||||
if (typeof response === 'string') {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async updateStatus(status: StrategyServiceStatusEnum): Promise<any> {
|
||||
try {
|
||||
return await this.emitAndWait('strategyService-updateStatus', { status });
|
||||
} catch (error) {
|
||||
console.error('Failed to update status', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllPlatforms(): Promise<Platform[]> {
|
||||
const maxRetries = 10;
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const data = await this.emitAndWait<Platform[]>(
|
||||
'strategyService-getAppsInfo',
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
attempt++;
|
||||
console.error(`Attempt ${attempt} failed to update strategies`, error);
|
||||
if (attempt >= maxRetries) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import socketIo from 'socket.io';
|
||||
|
||||
export class SystemService {
|
||||
private io: socketIo.Server;
|
||||
|
||||
constructor(io: socketIo.Server) {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
return await this.io.timeout(5000).emitWithAck('systemService-health');
|
||||
} catch (error) {
|
||||
console.error('Failed to check health', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,19 @@ export enum PlatformTypeEnum {
|
||||
ME_MEDIA = 'ME_MEDIA',
|
||||
}
|
||||
|
||||
export type RoleType = 'assistant' | 'user' | 'system';
|
||||
export type MessageType =
|
||||
| 'text'
|
||||
| 'image'
|
||||
| 'video'
|
||||
| 'file'
|
||||
| 'mention'
|
||||
| 'goods';
|
||||
export type RoleType = 'SELF' | 'OTHER' | 'SYSTEM';
|
||||
export type MessageType = 'TEXT' | 'IMAGE' | 'VIDEO' | 'FILE';
|
||||
|
||||
export interface MessageDTO {
|
||||
session_id: number;
|
||||
platform_id: string;
|
||||
unique: string;
|
||||
content: string; // If it's an image, this is the URL of the image
|
||||
sender: string;
|
||||
content: string;
|
||||
role: RoleType; // assistant, user
|
||||
msg_type: MessageType;
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
export interface ReplyDTO {
|
||||
content: string;
|
||||
msg_type: MessageType;
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
fork from https://github.com/zhengxs2018/ai
|
||||
@@ -0,0 +1,212 @@
|
||||
import axios from 'axios';
|
||||
import OpenAI from 'openai';
|
||||
import { Stream } from 'openai/streaming';
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
|
||||
const { stream } = params;
|
||||
const body = this.buildCreateParams(params);
|
||||
const path = '/chat-messages';
|
||||
|
||||
const response: Response = await this._client.post(path, {
|
||||
...options,
|
||||
body: body as unknown as Record<string, unknown>,
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return this.afterSSEResponse(response, controller);
|
||||
}
|
||||
|
||||
// if ('stream' in params && params.stream) {
|
||||
// // Streaming request
|
||||
// const stream = new Stream<OpenAI.ChatCompletionChunk>();
|
||||
// try {
|
||||
// const resp = await axios.post(
|
||||
// `${gpt_base_url}/chat-messages`,
|
||||
// {
|
||||
// inputs: {},
|
||||
// query: rest.query,
|
||||
// response_mode: 'streaming',
|
||||
// user: 'apiuser',
|
||||
// auto_generate_name: false,
|
||||
// },
|
||||
// {
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: `Bearer ${gpt_key}`,
|
||||
// },
|
||||
// responseType: 'stream',
|
||||
// },
|
||||
// );
|
||||
|
||||
// let buffer = '';
|
||||
|
||||
// resp.data.on('data', (chunk: any) => {
|
||||
// buffer += chunk.toString();
|
||||
// const lines = buffer.split('\n');
|
||||
|
||||
// for (let i = 0; i < lines.length - 1; i++) {
|
||||
// const line = lines[i].trim();
|
||||
// if (line === '') continue;
|
||||
// let chunkObj;
|
||||
// try {
|
||||
// const cleanedLine = line.replace(/^data: /, '').trim();
|
||||
// if (cleanedLine.startsWith('{') && cleanedLine.endsWith('}')) {
|
||||
// chunkObj = JSON.parse(cleanedLine);
|
||||
// } else {
|
||||
// continue;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error parsing JSON:', error);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (
|
||||
// chunkObj.event === 'message' ||
|
||||
// chunkObj.event === 'agent_message'
|
||||
// ) {
|
||||
// stream.push(chunkObj);
|
||||
// }
|
||||
// }
|
||||
|
||||
// buffer = lines[lines.length - 1];
|
||||
// });
|
||||
|
||||
// resp.data.on('end', () => {
|
||||
// stream.push(null);
|
||||
// });
|
||||
|
||||
// resp.data.on('error', (error: Error) => {
|
||||
// stream.emit('error', error);
|
||||
// });
|
||||
// } catch (error) {
|
||||
// stream.emit('error', error);
|
||||
// }
|
||||
|
||||
// return stream;
|
||||
// }
|
||||
|
||||
// // Non-streaming request
|
||||
// const response = await axios.post(
|
||||
// `${gpt_base_url}/chat-messages`,
|
||||
// {
|
||||
// inputs: {},
|
||||
// query: rest.query,
|
||||
// response_mode: 'non-streaming',
|
||||
// user: 'apiuser',
|
||||
// auto_generate_name: false,
|
||||
// },
|
||||
// {
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: `Bearer ${gpt_key}`,
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
|
||||
return response.data as OpenAI.ChatCompletion;
|
||||
}
|
||||
|
||||
protected buildCreateParams(params: ChatCompletionCreateParams) {
|
||||
const { model, top_k, ...rest } = params;
|
||||
return {
|
||||
...rest,
|
||||
model,
|
||||
top_k,
|
||||
response_mode: 'streaming',
|
||||
};
|
||||
}
|
||||
|
||||
protected afterSSEResponse(
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
const stream = Stream.fromSSEResponse<DifyChat.GenerateContentResponse>(
|
||||
response,
|
||||
controller,
|
||||
);
|
||||
|
||||
// response.body.on('data', (chunk: any) => {
|
||||
// const chunkObj = JSON.parse(chunk.toString());
|
||||
|
||||
// if (chunkObj.event === 'message' || chunkObj.event === 'agent_message') {
|
||||
// stream.push(chunkObj);
|
||||
// }
|
||||
// });
|
||||
|
||||
// response.body.on('end', () => {
|
||||
// stream.push(null);
|
||||
// });
|
||||
|
||||
// response.body.on('error', (error: Error) => {
|
||||
// stream.emit('error', error);
|
||||
// });
|
||||
|
||||
// return stream;
|
||||
|
||||
const toChoices = (chunk: DifyChat.GenerateContentResponse) => {};
|
||||
}
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
Chat.ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatCompletionCreateParams = Chat.ChatCompletionCreateParams;
|
||||
|
||||
export namespace Chat {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type ChatModel = (string & NonNullable<unknown>) | 'gemini-pro';
|
||||
// 支持的有点问题
|
||||
// | 'gemini-pro-vision';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number | null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
}
|
||||
|
||||
export namespace DifyChat {
|
||||
export interface GenerateContentResponse {
|
||||
event: 'message' | 'agent_message';
|
||||
answer: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
import * as API from './resources';
|
||||
|
||||
export interface DifyAIOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
defaultHeaders?: Headers;
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class DifyAI extends APIClient {
|
||||
protected apiKey: string;
|
||||
|
||||
private _options: DifyAIOptions;
|
||||
|
||||
constructor(options: DifyAIOptions = {}) {
|
||||
const {
|
||||
apiKey = process.env.DIFY_API_KEY || '',
|
||||
baseURL = 'https://api.dify.ai/v1/',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return {
|
||||
...this._options.defaultQuery,
|
||||
key: this.apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './chat/index';
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface ErnieAIOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
|
||||
/**
|
||||
* Default headers to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* header to `undefined` or `null` in request options.
|
||||
*/
|
||||
defaultHeaders?: Headers;
|
||||
|
||||
/**
|
||||
* Default query parameters to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* param to `undefined` in request options.
|
||||
*/
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class ErnieAI extends APIClient {
|
||||
protected apiKey: string;
|
||||
|
||||
private _options: ErnieAIOptions;
|
||||
|
||||
constructor(options: ErnieAIOptions = {}) {
|
||||
const {
|
||||
apiKey = process.env.EB_API_KEY || '',
|
||||
baseURL = 'https://aistudio.baidu.com/llm/lmapi/v1',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
embeddings = new API.Embeddings(this);
|
||||
|
||||
protected override authHeaders() {
|
||||
return {
|
||||
Authorization: `token ${this.apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace ErnieAI {
|
||||
export type Chat = API.Chat;
|
||||
export type ChatModel = API.ChatModel;
|
||||
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
API.ChatCompletionCreateParamsNonStreaming;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
API.ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
|
||||
}
|
||||
|
||||
export default ErnieAI;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { APIResource } from '../../../resource';
|
||||
import { Completions } from './completions';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import OpenAI, { APIError, OpenAIError } from 'openai';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../../resource';
|
||||
import { ensureArray } from '../../../util';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
protected endpoints: Record<ChatModel, string> = {
|
||||
'ernie-bot': '/chat/completions',
|
||||
'ernie-bot-turbo': '/chat/eb-instant',
|
||||
'ernie-bot-4': '/chat/completions_pro',
|
||||
'ernie-bot-8k': '/chat/ernie_bot_8k',
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*
|
||||
* 文心一言 由于分发在不同的平台,所以有不同的文档
|
||||
* 百度云的响应和 OpenAI 的比较类似,但授权没有 AI Studio 方便
|
||||
* 之前 AI Studio 的文档是有文档的,但现在不知道去哪了
|
||||
* 参考:
|
||||
* - https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
|
||||
* - https://github.com/PaddlePaddle/ERNIE-Bot-SDK/blob/develop/erniebot/backends/aistudio.py
|
||||
*/
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
) {
|
||||
const { model = 'ernie-bot', ...body } =
|
||||
Completions.buildCreateParams(params);
|
||||
|
||||
const endpoint = this.endpoints[model];
|
||||
|
||||
if (!endpoint) {
|
||||
throw new OpenAIError(`Invalid model: ${model}`);
|
||||
}
|
||||
|
||||
const { stream } = body;
|
||||
|
||||
const headers = {
|
||||
...options?.headers,
|
||||
// Note: 如果是 stream 的话,需要设置 Accept 为 text/event-stream
|
||||
Accept: stream ? 'text/event-stream' : 'application/json',
|
||||
};
|
||||
|
||||
const response: Response = await this._client.post(endpoint, {
|
||||
...options,
|
||||
body,
|
||||
headers,
|
||||
// 文心一言的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
|
||||
// 设置 __binaryResponse 为 true, 是为了让 client 返回原始的 response
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return Completions.fromOpenAIStream(
|
||||
model,
|
||||
Stream.fromSSEResponse(response, controller),
|
||||
controller,
|
||||
);
|
||||
}
|
||||
|
||||
return Completions.fromResponse(model, await response.json());
|
||||
}
|
||||
|
||||
static buildCreateParams(
|
||||
params: ChatCompletionCreateParams,
|
||||
): ChatCompletions.ChatCompletionCreateParams {
|
||||
const { messages = [], presence_penalty, user, stop, ...rest } = params;
|
||||
|
||||
const head = messages[0];
|
||||
|
||||
// 文心一言的 system 是独立字段
|
||||
// (1)长度限制1024个字符
|
||||
// (2)如果使用functions参数,不支持设定人设system
|
||||
const system = head && head.role === 'system' ? head.content : undefined;
|
||||
|
||||
// 移除 system 角色的消息
|
||||
if (system) {
|
||||
messages.splice(0, 1);
|
||||
}
|
||||
|
||||
const data: ChatCompletions.ChatCompletionCreateParams = {
|
||||
...rest,
|
||||
messages,
|
||||
};
|
||||
|
||||
if (system) {
|
||||
data.system = system;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
data.user_id = user;
|
||||
}
|
||||
|
||||
if (presence_penalty) {
|
||||
data.penalty_score = presence_penalty;
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
data.stop = ensureArray(stop);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static fromResponse(
|
||||
model: string,
|
||||
data: ChatCompletions.APIResponse,
|
||||
): OpenAI.ChatCompletion {
|
||||
Completions.assert(data);
|
||||
|
||||
const { result } = data;
|
||||
|
||||
const choice: OpenAI.ChatCompletion.Choice = {
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: result.result,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop',
|
||||
};
|
||||
|
||||
// TODO 需要确认 is_truncated 是否和 is_end 互斥
|
||||
// TODO 需要确认 functions 是否响应式不一样
|
||||
if (result.is_end) {
|
||||
choice.finish_reason = 'stop';
|
||||
} else if (result.is_truncated) {
|
||||
choice.finish_reason = 'length';
|
||||
} else if (result.need_clear_history) {
|
||||
choice.finish_reason = 'content_filter';
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
model,
|
||||
choices: [choice],
|
||||
created: parseInt(result.created, 10),
|
||||
object: 'chat.completion',
|
||||
usage: result.usage,
|
||||
};
|
||||
}
|
||||
|
||||
static fromOpenAIStream(
|
||||
model: string,
|
||||
stream: Stream<ChatCompletions.APIResponse>,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
async function* iterator(): AsyncIterator<
|
||||
OpenAI.ChatCompletionChunk,
|
||||
any,
|
||||
undefined
|
||||
> {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const chunk of stream) {
|
||||
Completions.assert(chunk);
|
||||
|
||||
// TODO 某些情况下,文心一言的 result 只有 id,需要排查情况
|
||||
const data = chunk.result;
|
||||
|
||||
const choice: OpenAI.ChatCompletionChunk.Choice = {
|
||||
index: 0,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: data.result || '',
|
||||
},
|
||||
finish_reason: null,
|
||||
};
|
||||
|
||||
// TODO 需要确认 is_truncated 是否和 is_end 互斥
|
||||
// TODO 需要确认 functions 是否响应式不一样
|
||||
if (data.is_end) {
|
||||
choice.finish_reason = 'stop';
|
||||
} else if (data.is_truncated) {
|
||||
choice.finish_reason = 'length';
|
||||
} else if (data.need_clear_history) {
|
||||
choice.finish_reason = 'content_filter';
|
||||
}
|
||||
|
||||
yield {
|
||||
id: data.id,
|
||||
model,
|
||||
choices: [choice],
|
||||
object: 'chat.completion.chunk',
|
||||
created: parseInt(data.created, 10),
|
||||
// openai-node 上 已经有讨论添加 usage 的问题
|
||||
// 文心一言是有提供的,这里主要是为了向前兼容
|
||||
// @ts-ignore
|
||||
usage: data.usage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建错误
|
||||
*
|
||||
* @param code -
|
||||
* @param message -
|
||||
* @returns 错误
|
||||
*/
|
||||
static makeAPIError(code: number, message: string) {
|
||||
const error = { code, message };
|
||||
|
||||
switch (code) {
|
||||
case 2:
|
||||
return APIError.generate(500, error, message, {});
|
||||
case 6: // permission error
|
||||
case 111: // token expired
|
||||
return APIError.generate(403, error, message, {});
|
||||
case 17:
|
||||
case 18:
|
||||
case 19:
|
||||
case 40407:
|
||||
return APIError.generate(429, error, message, {});
|
||||
case 110: // invalid token
|
||||
case 40401: // invalid token
|
||||
return APIError.generate(401, error, message, {});
|
||||
case 336003: // invalid parameter
|
||||
return APIError.generate(400, error, message, {});
|
||||
case 336100: // try again
|
||||
return APIError.generate(500, error, message, {});
|
||||
default:
|
||||
return APIError.generate(undefined, error, message, {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果 code 不为 0,抛出 APIError
|
||||
*
|
||||
* @param code -
|
||||
* @param message -
|
||||
*/
|
||||
static assert(resp: ChatCompletions.APIResponse) {
|
||||
if (resp.errorCode === 0) return;
|
||||
|
||||
throw Completions.makeAPIError(resp.errorCode, resp.errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends Pick<
|
||||
OpenAI.ChatCompletionCreateParamsNonStreaming,
|
||||
| 'messages'
|
||||
| 'functions'
|
||||
| 'temperature'
|
||||
| 'top_p'
|
||||
| 'presence_penalty'
|
||||
| 'stream'
|
||||
| 'stop'
|
||||
| 'user'
|
||||
> {
|
||||
model: ChatModel;
|
||||
disable_search?: boolean | null;
|
||||
enable_citation?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends Pick<
|
||||
OpenAI.ChatCompletionCreateParamsStreaming,
|
||||
| 'messages'
|
||||
| 'functions'
|
||||
| 'temperature'
|
||||
| 'top_p'
|
||||
| 'presence_penalty'
|
||||
| 'stream'
|
||||
| 'stop'
|
||||
| 'user'
|
||||
> {
|
||||
model: ChatModel;
|
||||
disable_search?: boolean | null;
|
||||
enable_citation?: boolean | null;
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatModel =
|
||||
| 'ernie-bot'
|
||||
| 'ernie-bot-turbo'
|
||||
| 'ernie-bot-4'
|
||||
| 'ernie-bot-8k';
|
||||
|
||||
export namespace ChatCompletions {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParams {
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
model: ChatModel;
|
||||
|
||||
/**
|
||||
* 是否强制关闭实时搜索功能,默认 false,表示不关闭
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disable_search?: boolean | null;
|
||||
|
||||
/**
|
||||
* 是否开启上角标返回,说明:
|
||||
* (1)开启后,有概率触发搜索溯源信息search_info,search_info内容见响应参数介绍
|
||||
* (2)默认false,不开启
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
enable_citation?: boolean | null;
|
||||
|
||||
/**
|
||||
* 模型人设,主要用于人设设定,例如,你是xxx公司制作的AI助手,说明:
|
||||
* (1)长度限制1024个字符
|
||||
* (2)如果使用 functions 参数,不支持设定人设 system
|
||||
*
|
||||
* @remarks OpenAI 是通过 messages 的 role 来区分的
|
||||
*/
|
||||
system?: string | null;
|
||||
|
||||
/**
|
||||
* 聊天上下文信息
|
||||
*
|
||||
* @remarks 不支持 system 角色
|
||||
*/
|
||||
messages: OpenAI.ChatCompletionCreateParams['messages'];
|
||||
|
||||
/**
|
||||
* 一个可触发函数的描述列表
|
||||
*/
|
||||
functions?: OpenAI.ChatCompletionCreateParams['functions'];
|
||||
|
||||
/**
|
||||
* 内容随机性
|
||||
*
|
||||
* 说明:
|
||||
* (1)较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定
|
||||
* (2)默认0.8,范围 (0, 1.0],不能为0
|
||||
* (3)建议该参数和 top_p 只设置1个
|
||||
* (4)建议 top_p 和 temperature 不要同时更改
|
||||
*/
|
||||
temperature?: number | null;
|
||||
|
||||
/**
|
||||
* 生成文本的多样性
|
||||
*
|
||||
* 说明:
|
||||
* (1)影响输出文本的多样性,取值越大,生成文本的多样性越强
|
||||
* (2)默认0.8,取值范围 [0, 1.0]
|
||||
* (3)建议该参数和 temperature 只设置1个
|
||||
* (4)建议 top_p 和 temperature 不要同时更改
|
||||
*/
|
||||
top_p?: number | null;
|
||||
|
||||
/**
|
||||
*
|
||||
* 通过对已生成的token增加惩罚,减少重复生成的现象。说明:
|
||||
* (1)值越大表示惩罚越大
|
||||
* (2)默认1.0,取值范围:[1.0, 2.0]
|
||||
*
|
||||
* @remarks 在 OpenAI 中,参数名为 presence_penalty
|
||||
*/
|
||||
penalty_score?: number | null;
|
||||
|
||||
/**
|
||||
* 是否以流式接口的形式返回数据,默认 false
|
||||
*/
|
||||
stream?: boolean | null;
|
||||
|
||||
/**
|
||||
* 生成停止标识,当模型生成结果以stop中某个元素结尾时,停止文本生成。说明:
|
||||
* (1)每个元素长度不超过20字符
|
||||
* (2)最多4个元素
|
||||
*/
|
||||
stop?: string | string[] | undefined;
|
||||
|
||||
/**
|
||||
* 表示最终用户的唯一标识符,可以监视和检测滥用行为,防止接口恶意调用
|
||||
*
|
||||
* @remarks OpenAI 中是通过 user 区分
|
||||
*/
|
||||
user_id?: string | undefined;
|
||||
}
|
||||
|
||||
export type ChatCompletion = {
|
||||
id: string;
|
||||
result: string;
|
||||
created: string;
|
||||
is_end: boolean;
|
||||
is_truncated: boolean;
|
||||
need_clear_history: boolean;
|
||||
usage: OpenAI.CompletionUsage;
|
||||
};
|
||||
|
||||
export type APIResponse = {
|
||||
errorCode: number;
|
||||
errorMsg: string;
|
||||
result: ChatCompletion;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Chat } from './chat';
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1,95 @@
|
||||
import OpenAI, { APIError, OpenAIError } from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Embeddings extends APIResource {
|
||||
protected endpoints: Record<EmbeddingModel, string> = {
|
||||
'ernie-text-embedding': '/embeddings/embedding-v1',
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an embedding vector representing the input text.
|
||||
*
|
||||
* See https://cloud.baidu.com/doc/WENXINWORKSHOP/s/alj562vvu
|
||||
*/
|
||||
async create(
|
||||
params: EmbeddingCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<OpenAI.CreateEmbeddingResponse> {
|
||||
const { model, user, input } = params;
|
||||
const endpoint = this.endpoints[model];
|
||||
|
||||
if (!endpoint) {
|
||||
throw new OpenAIError(`Invalid model: ${model}`);
|
||||
}
|
||||
|
||||
const body = {
|
||||
input,
|
||||
user_id: user,
|
||||
};
|
||||
|
||||
const response: Response = await this._client.post(endpoint, {
|
||||
body,
|
||||
...options,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
return Embeddings.fromResponse(model, await response.json());
|
||||
}
|
||||
|
||||
static fromResponse(
|
||||
model: EmbeddingModel,
|
||||
data: CreateEmbeddingResponse,
|
||||
): OpenAI.CreateEmbeddingResponse {
|
||||
Embeddings.assert(data);
|
||||
|
||||
const { result } = data;
|
||||
|
||||
return {
|
||||
data: result.data,
|
||||
model,
|
||||
object: 'list',
|
||||
usage: result.usage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果 code 不为 0,抛出 APIError
|
||||
*
|
||||
* @param code -
|
||||
* @param message -
|
||||
*/
|
||||
static assert(resp: CreateEmbeddingResponse) {
|
||||
if (resp.errorCode === 0) return;
|
||||
|
||||
const error = { code: resp.errorCode, message: resp.errorMsg };
|
||||
|
||||
throw APIError.generate(undefined, error, undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export type EmbeddingModel = 'ernie-text-embedding';
|
||||
|
||||
export interface EmbeddingCreateParams {
|
||||
/**
|
||||
* 输入文本
|
||||
*/
|
||||
input: string | Array<string> | Array<number> | Array<Array<number>>;
|
||||
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
model: EmbeddingModel;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
user?: string;
|
||||
}
|
||||
|
||||
type CreateEmbeddingResponse = {
|
||||
errorCode: number;
|
||||
errorMsg: string;
|
||||
result: OpenAI.CreateEmbeddingResponse;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './chat/index';
|
||||
|
||||
export { Embeddings, type EmbeddingCreateParams } from './embeddings';
|
||||
@@ -0,0 +1,46 @@
|
||||
import { APIError } from 'openai';
|
||||
|
||||
/**
|
||||
* 构建错误
|
||||
*
|
||||
* @param code -
|
||||
* @param message -
|
||||
* @returns 错误
|
||||
*/
|
||||
export function makeAPIError(code: number, message: string) {
|
||||
const error = { code, message };
|
||||
|
||||
switch (code) {
|
||||
case 2:
|
||||
return APIError.generate(500, error, message, {});
|
||||
case 6: // permission error
|
||||
case 111: // token expired
|
||||
return APIError.generate(403, error, message, {});
|
||||
case 17:
|
||||
case 18:
|
||||
case 19:
|
||||
case 40407:
|
||||
return APIError.generate(429, error, message, {});
|
||||
case 110: // invalid token
|
||||
case 40401: // invalid token
|
||||
return APIError.generate(401, error, message, {});
|
||||
case 336003: // invalid parameter
|
||||
return APIError.generate(400, error, message, {});
|
||||
case 336100: // try again
|
||||
return APIError.generate(500, error, message, {});
|
||||
default:
|
||||
return APIError.generate(undefined, error, message, {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果 code 不为 0,抛出 APIError
|
||||
*
|
||||
* @param code -
|
||||
* @param message -
|
||||
*/
|
||||
export function assertNonZero(code: number, message: string) {
|
||||
if (code === 0) return;
|
||||
|
||||
throw makeAPIError(code, message);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
const BASE_URL = 'https://generativelanguage.googleapis.com/v1';
|
||||
|
||||
export interface GeminiAIOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
defaultHeaders?: Headers;
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class GeminiAI extends APIClient {
|
||||
apiKey: string;
|
||||
|
||||
private _options: GeminiAIOptions;
|
||||
|
||||
constructor(options: GeminiAIOptions = {}) {
|
||||
const {
|
||||
apiKey = process.env.GEMINI_API_KEY || '',
|
||||
baseURL = process.env.GEMINI_BASE_URL || BASE_URL,
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
models = new API.Models(this);
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return {
|
||||
...this._options.defaultQuery,
|
||||
key: this.apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace GeminiAI {
|
||||
export type ChatModel = API.ChatModel;
|
||||
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
API.ChatCompletionCreateParamsStreaming;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
API.ChatCompletionCreateParamsNonStreaming;
|
||||
}
|
||||
|
||||
export default GeminiAI;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { GeminiAI } from './index';
|
||||
|
||||
export class APIResource {
|
||||
protected _client: GeminiAI;
|
||||
|
||||
constructor(client: GeminiAI) {
|
||||
this._client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Completions } from './completions';
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import OpenAI from 'openai';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { ensureArray } from '../../../util';
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*/
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
|
||||
const { stream, model } = params;
|
||||
const body = this.buildCreateParams(params);
|
||||
const path = `/models/${model}:generateContent`;
|
||||
|
||||
const response: Response = await this._client.post(path, {
|
||||
...options,
|
||||
query: stream ? { alt: 'sse' } : {},
|
||||
body: body as unknown as Record<string, unknown>,
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return this.afterSSEResponse(model, response, controller);
|
||||
}
|
||||
|
||||
return this.afterResponse(model, response);
|
||||
}
|
||||
|
||||
protected buildCreateParams(params: ChatCompletionCreateParams) {
|
||||
const {
|
||||
messages = [],
|
||||
max_tokens,
|
||||
top_p,
|
||||
top_k,
|
||||
stop,
|
||||
temperature,
|
||||
} = params;
|
||||
|
||||
function formatContentParts(
|
||||
content: string | OpenAI.ChatCompletionContentPart[],
|
||||
) {
|
||||
const parts: GeminiChat.Part[] = [];
|
||||
|
||||
if (typeof content === 'string') {
|
||||
parts.push({ text: content });
|
||||
return parts;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const part of content) {
|
||||
if (part.type === 'text') {
|
||||
parts.push({ text: part.text });
|
||||
} else {
|
||||
// TODO: Handle images
|
||||
// parts.push({
|
||||
// inline_data: {
|
||||
// "mime_type": "image/jpeg",
|
||||
// "data": "'$(base64 -w0 image.jpg)'"
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function formatRole(role: string): 'user' | 'model' {
|
||||
return role === 'user' ? 'user' : 'model';
|
||||
}
|
||||
|
||||
const generationConfig: GeminiChat.GenerationConfig = {};
|
||||
|
||||
const data: GeminiChat.GenerateContentRequest = {
|
||||
contents: messages.map((item) => {
|
||||
return {
|
||||
role: formatRole(item.role),
|
||||
parts: formatContentParts(item.content!),
|
||||
};
|
||||
}),
|
||||
generationConfig,
|
||||
};
|
||||
|
||||
if (temperature != null) {
|
||||
generationConfig.temperature = temperature;
|
||||
}
|
||||
|
||||
if (top_k != null) {
|
||||
generationConfig.topK = top_k;
|
||||
}
|
||||
|
||||
if (top_p != null) {
|
||||
generationConfig.topP = top_p;
|
||||
}
|
||||
|
||||
if (stop != null) {
|
||||
generationConfig.stopSequences = ensureArray(stop);
|
||||
}
|
||||
|
||||
if (max_tokens != null) {
|
||||
generationConfig.maxOutputTokens = max_tokens;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected async afterResponse(
|
||||
model: string,
|
||||
response: Response,
|
||||
): Promise<OpenAI.ChatCompletion> {
|
||||
const data: GeminiChat.GenerateContentResponse = await response.json();
|
||||
const choices: OpenAI.ChatCompletion.Choice[] = data.candidates!.map(
|
||||
(item) => {
|
||||
const [part] = item.content.parts;
|
||||
|
||||
const choice: OpenAI.ChatCompletion.Choice = {
|
||||
index: item.index,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: part.text!,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop',
|
||||
};
|
||||
|
||||
switch (item.finishReason) {
|
||||
case 'MAX_TOKENS':
|
||||
choice.finish_reason = 'length';
|
||||
break;
|
||||
case 'SAFETY':
|
||||
case 'RECITATION':
|
||||
choice.finish_reason = 'content_filter';
|
||||
break;
|
||||
default:
|
||||
choice.finish_reason = 'stop';
|
||||
}
|
||||
|
||||
return choice;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: randomUUID(),
|
||||
model,
|
||||
choices,
|
||||
object: 'chat.completion',
|
||||
created: Date.now() / 10,
|
||||
// TODO 需要支持 usage
|
||||
usage: {
|
||||
completion_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected afterSSEResponse(
|
||||
model: string,
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
const stream = Stream.fromSSEResponse<GeminiChat.GenerateContentResponse>(
|
||||
response,
|
||||
controller,
|
||||
);
|
||||
|
||||
const toChoices = (data: GeminiChat.GenerateContentResponse) => {
|
||||
return data.candidates!.map((item) => {
|
||||
const [part] = item.content.parts;
|
||||
|
||||
const choice: OpenAI.ChatCompletionChunk.Choice = {
|
||||
index: item.index,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: part.text || '',
|
||||
},
|
||||
finish_reason: null,
|
||||
};
|
||||
|
||||
switch (item.finishReason) {
|
||||
case 'MAX_TOKENS':
|
||||
choice.finish_reason = 'length';
|
||||
break;
|
||||
case 'SAFETY':
|
||||
case 'RECITATION':
|
||||
choice.finish_reason = 'content_filter';
|
||||
break;
|
||||
default:
|
||||
choice.finish_reason = 'stop';
|
||||
}
|
||||
|
||||
return choice;
|
||||
});
|
||||
};
|
||||
|
||||
async function* iterator(): AsyncIterator<
|
||||
OpenAI.ChatCompletionChunk,
|
||||
any,
|
||||
undefined
|
||||
> {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const chunk of stream) {
|
||||
yield {
|
||||
id: randomUUID(),
|
||||
model,
|
||||
choices: toChoices(chunk),
|
||||
object: 'chat.completion.chunk',
|
||||
created: Date.now() / 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
Chat.ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatCompletionCreateParams = Chat.ChatCompletionCreateParams;
|
||||
|
||||
export type ChatModel = Chat.ChatModel;
|
||||
|
||||
export namespace Chat {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type ChatModel = (string & NonNullable<unknown>) | 'gemini-pro';
|
||||
// 支持的有点问题
|
||||
// | 'gemini-pro-vision';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number | null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
}
|
||||
|
||||
namespace GeminiChat {
|
||||
export interface GenerationConfig {
|
||||
candidateCount?: number;
|
||||
stopSequences?: string[];
|
||||
maxOutputTokens?: number;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
topK?: number;
|
||||
}
|
||||
|
||||
export interface GenerateContentCandidate {
|
||||
index: number;
|
||||
content: Content;
|
||||
finishReason?:
|
||||
| 'FINISH_REASON_UNSPECIFIED'
|
||||
| 'STOP'
|
||||
| 'MAX_TOKENS'
|
||||
| 'SAFETY'
|
||||
| 'RECITATION'
|
||||
| 'OTHER';
|
||||
finishMessage?: string;
|
||||
citationMetadata?: CitationMetadata;
|
||||
}
|
||||
|
||||
export interface GenerateContentResponse {
|
||||
candidates?: GenerateContentCandidate[];
|
||||
// promptFeedback?: PromptFeedback;
|
||||
}
|
||||
|
||||
export interface CitationMetadata {
|
||||
citationSources: CitationSource[];
|
||||
}
|
||||
|
||||
export interface CitationSource {
|
||||
startIndex?: number;
|
||||
endIndex?: number;
|
||||
uri?: string;
|
||||
license?: string;
|
||||
}
|
||||
|
||||
export interface InputContent {
|
||||
parts: string | Array<string | Part>;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface Content extends InputContent {
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
export type Part = TextPart | InlineDataPart;
|
||||
|
||||
export interface TextPart {
|
||||
text: string;
|
||||
inlineData?: never;
|
||||
}
|
||||
|
||||
export interface InlineDataPart {
|
||||
text?: never;
|
||||
inlineData: GeminiContentBlob;
|
||||
}
|
||||
|
||||
export interface GeminiContentBlob {
|
||||
mimeType: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface BaseParams {
|
||||
generationConfig?: GenerationConfig;
|
||||
}
|
||||
|
||||
export interface GenerateContentRequest extends BaseParams {
|
||||
contents: Content[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Chat } from './chat';
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './chat/index';
|
||||
|
||||
export { Models, type Model, ModelsPage } from './models';
|
||||
@@ -0,0 +1,85 @@
|
||||
import OpenAI from 'openai';
|
||||
import {
|
||||
type FinalRequestOptions,
|
||||
type PagePromise,
|
||||
type RequestOptions,
|
||||
} from 'openai/core';
|
||||
import { Page } from 'openai/pagination';
|
||||
|
||||
import { type GeminiAI } from '../../index';
|
||||
import { APIResource } from '../resource';
|
||||
|
||||
// TODO 输出原始对象
|
||||
export class Models extends APIResource {
|
||||
/**
|
||||
* Retrieves a model instance, providing basic information about the model such as
|
||||
* the owner and permissioning.
|
||||
*/
|
||||
async retrieve(model: string, options?: RequestOptions): Promise<Model> {
|
||||
const item: GeminiModel = await this._client.get(
|
||||
`/models/${model}`,
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
id: item.name,
|
||||
created: 0,
|
||||
object: 'model',
|
||||
owned_by: 'google',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the currently available models, and provides basic information about each
|
||||
* one such as the owner and availability.
|
||||
*/
|
||||
list(options?: RequestOptions): PagePromise<ModelsPage, Model> {
|
||||
return this._client.getAPIList('/models', ModelsPage, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelsPage extends Page<Model> {
|
||||
constructor(
|
||||
client: GeminiAI,
|
||||
response: Response,
|
||||
body: GeminiPageResponse,
|
||||
options: FinalRequestOptions,
|
||||
) {
|
||||
const data: Model[] = body.models.map((item) => {
|
||||
return {
|
||||
id: item.name,
|
||||
created: 0,
|
||||
object: 'model',
|
||||
owned_by: 'google',
|
||||
};
|
||||
});
|
||||
|
||||
super(client, response, { data, object: 'list' }, options);
|
||||
}
|
||||
}
|
||||
|
||||
interface GeminiModel {
|
||||
name: string;
|
||||
version: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
inputTokenLimit: string;
|
||||
outputTokenLimit: string;
|
||||
supportedGenerationMethods: string[];
|
||||
}
|
||||
|
||||
interface GeminiPageResponse {
|
||||
models: GeminiModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an OpenAI model offering that can be used with the API.
|
||||
*/
|
||||
export type Model = OpenAI.Models.Model;
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace Models {
|
||||
export import Model = OpenAI.Models.Model;
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export import ModelsPage = OpenAI.Models.ModelsPage;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createHmac } from 'node:crypto';
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface HunYuanAIOptions {
|
||||
baseURL?: string;
|
||||
appId?: string;
|
||||
secretId?: string;
|
||||
secretKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
/**
|
||||
* Default headers to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* header to `undefined` or `null` in request options.
|
||||
*/
|
||||
defaultHeaders?: Headers;
|
||||
|
||||
/**
|
||||
* Default query parameters to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* param to `undefined` in request options.
|
||||
*/
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class HunYuanAI extends APIClient {
|
||||
appId: number;
|
||||
|
||||
secretId: string;
|
||||
|
||||
secretKey: string;
|
||||
|
||||
private _options: HunYuanAIOptions;
|
||||
|
||||
constructor(options: HunYuanAIOptions = {}) {
|
||||
const {
|
||||
appId = process.env.HUNYUAN_APP_ID || '',
|
||||
secretId = process.env.HUNYUAN_SECRET_ID || '',
|
||||
secretKey = process.env.HUNYUAN_SECRET_KEY || '',
|
||||
baseURL = 'https://hunyuan.cloud.tencent.com/hyllm/v1',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.appId = parseInt(appId, 10);
|
||||
this.secretKey = secretKey;
|
||||
this.secretId = secretId;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
|
||||
generateAuthorization(path: string, data: Record<string, any>) {
|
||||
const rawSessionKey = this.buildURL(path, {}).replace('https://', '');
|
||||
|
||||
const rawSignature: string[] = [];
|
||||
|
||||
Object.keys(data)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
const value = data[key];
|
||||
|
||||
if (value == null) return;
|
||||
|
||||
if (typeof value === 'object') {
|
||||
rawSignature.push(`${key}=${JSON.stringify(value)}`);
|
||||
} else {
|
||||
rawSignature.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
return this.hash(`${rawSessionKey}?${rawSignature.join('&')}`);
|
||||
}
|
||||
|
||||
protected hash(data: string) {
|
||||
const hash = createHmac('sha1', this.secretKey);
|
||||
return hash.update(Buffer.from(data, 'utf8')).digest('base64');
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace HunYuanAI {
|
||||
export type ChatModel = API.ChatModel;
|
||||
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
API.ChatCompletionCreateParamsStreaming;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
API.ChatCompletionCreateParamsNonStreaming;
|
||||
}
|
||||
|
||||
export default HunYuanAI;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { HunYuanAI } from './index';
|
||||
|
||||
export class APIResource {
|
||||
protected _client: HunYuanAI;
|
||||
|
||||
constructor(client: HunYuanAI) {
|
||||
this._client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { APIResource } from '../../resource';
|
||||
import { Completions } from './completions';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
import OpenAI, { APIError } from 'openai';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*
|
||||
* See https://cloud.tencent.com/document/product/1729/97732
|
||||
*/
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion | Stream<OpenAI.ChatCompletionChunk>> {
|
||||
const client = this._client;
|
||||
const { model, messages, temperature = 0.8, top_p, stream } = params;
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const body: ChatCompletions.ChatCompletionCreateParams = {
|
||||
app_id: client.appId,
|
||||
secret_id: client.secretId,
|
||||
timestamp,
|
||||
expired: timestamp + 7200,
|
||||
temperature,
|
||||
top_p,
|
||||
stream: stream ? 1 : 0,
|
||||
messages,
|
||||
};
|
||||
|
||||
const path = '/chat/completions';
|
||||
|
||||
const signature = client.generateAuthorization(path, body);
|
||||
|
||||
const response: Response = await this._client.post(path, {
|
||||
...options,
|
||||
body,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
Authorization: signature,
|
||||
},
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (params.stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return Completions.fromSSEResponse(
|
||||
model,
|
||||
Stream.fromSSEResponse(response, controller),
|
||||
controller,
|
||||
);
|
||||
}
|
||||
|
||||
return Completions.fromResponse(model, await response.json());
|
||||
}
|
||||
|
||||
static fromSSEResponse(
|
||||
model: string,
|
||||
stream: Stream<ChatCompletions.ChatCompletion>,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
async function* iterator(): AsyncIterator<
|
||||
OpenAI.ChatCompletionChunk,
|
||||
any,
|
||||
undefined
|
||||
> {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.error) {
|
||||
throw new APIError(undefined, chunk.error, undefined, undefined);
|
||||
}
|
||||
|
||||
const message = chunk.choices[0];
|
||||
|
||||
const choice: OpenAI.ChatCompletionChunk.Choice = {
|
||||
index: 0,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: message.delta.content || '',
|
||||
},
|
||||
finish_reason: null,
|
||||
};
|
||||
|
||||
yield {
|
||||
id: chunk.id,
|
||||
model,
|
||||
choices: [choice],
|
||||
object: 'chat.completion.chunk',
|
||||
created: parseInt(chunk.created, 10),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
|
||||
static fromResponse(
|
||||
model: string,
|
||||
data: ChatCompletions.ChatCompletion,
|
||||
): OpenAI.ChatCompletion {
|
||||
if (data.error) {
|
||||
throw new APIError(undefined, data.error, undefined, undefined);
|
||||
}
|
||||
|
||||
const message = data.choices[0];
|
||||
|
||||
const choice: OpenAI.ChatCompletion.Choice = {
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: message.messages.content,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: message.finish_reason,
|
||||
};
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
model,
|
||||
choices: [choice],
|
||||
created: parseInt(data.created, 10),
|
||||
object: 'chat.completion',
|
||||
usage: data.usage,
|
||||
};
|
||||
}
|
||||
|
||||
protected hash(data: string) {
|
||||
const hash = createHmac('sha1', this._client.secretKey);
|
||||
return hash.update(Buffer.from(data, 'utf8')).digest('base64');
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
|
||||
model: ChatModel;
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsStreaming {
|
||||
model: ChatModel;
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatModel = 'hunyuan';
|
||||
|
||||
export namespace ChatCompletions {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParams {
|
||||
/**
|
||||
* 腾讯云账号的 APPID
|
||||
*/
|
||||
app_id: number;
|
||||
|
||||
/**
|
||||
* API 密钥
|
||||
*/
|
||||
secret_id: string;
|
||||
|
||||
/**
|
||||
* 当前 UNIX 时间戳,单位为秒,可记录发起 API 请求的时间。
|
||||
*/
|
||||
timestamp: number;
|
||||
|
||||
/**
|
||||
* 签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值,单位为秒;Expired 必须与 Timestamp 的差值小于90天
|
||||
*/
|
||||
expired: number;
|
||||
|
||||
/**
|
||||
* 请求 ID,用于问题排查
|
||||
*/
|
||||
query_id?: string;
|
||||
|
||||
/**
|
||||
* 内容随机性
|
||||
*/
|
||||
temperature?: number | null;
|
||||
|
||||
/**
|
||||
* 生成结果的多样性
|
||||
*/
|
||||
top_p?: number | null;
|
||||
|
||||
/**
|
||||
* 是否返回流式结果
|
||||
*
|
||||
* 0:同步,1:流式 (默认,协议:SSE)
|
||||
*
|
||||
* 同步请求超时:60s,如果内容较长建议使用流式
|
||||
*/
|
||||
stream?: number | null;
|
||||
|
||||
/**
|
||||
* 会话内容, 按对话时间序排列,长度最多为40
|
||||
* 最大支持16k tokens上下文
|
||||
*/
|
||||
messages: OpenAI.ChatCompletionMessageParam[];
|
||||
}
|
||||
|
||||
export type CompletionChoicesDelta = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type CompletionChoice = {
|
||||
finish_reason: 'stop';
|
||||
/**
|
||||
* 内容,同步模式返回内容,流模式为 null
|
||||
*/
|
||||
messages: OpenAI.ChatCompletionMessage;
|
||||
/**
|
||||
* 内容,流模式返回内容,同步模式为 null
|
||||
*/
|
||||
delta: CompletionChoicesDelta;
|
||||
};
|
||||
|
||||
export interface ChatCompletion {
|
||||
choices: CompletionChoice[];
|
||||
created: string;
|
||||
note: string;
|
||||
id: string;
|
||||
usage: OpenAI.CompletionUsage;
|
||||
|
||||
error?: {
|
||||
message: string;
|
||||
code: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Chat } from './chat';
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './chat/index';
|
||||
@@ -0,0 +1,51 @@
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import ErnieAI, { ErnieAIOptions } from './ernie';
|
||||
import GeminiAI, { GeminiAIOptions } from './gemini';
|
||||
import HunYuanAI, { HunYuanAIOptions } from './hunyuan';
|
||||
import MinimaxAI, { MinimaxAIOptions } from './minimax';
|
||||
import QWenAI, { QWenAIOptions } from './qwen';
|
||||
import SparkAI, { SparkAIOptions } from './spark';
|
||||
import VYroAI, { VYroAIOptions } from './vyro';
|
||||
|
||||
export {
|
||||
ErnieAI,
|
||||
type ErnieAIOptions,
|
||||
GeminiAI,
|
||||
type GeminiAIOptions,
|
||||
HunYuanAI,
|
||||
type HunYuanAIOptions,
|
||||
MinimaxAI,
|
||||
type MinimaxAIOptions,
|
||||
OpenAI,
|
||||
QWenAI,
|
||||
type QWenAIOptions,
|
||||
SparkAI,
|
||||
type SparkAIOptions,
|
||||
VYroAI,
|
||||
type VYroAIOptions,
|
||||
};
|
||||
|
||||
export {
|
||||
OpenAIError,
|
||||
APIError,
|
||||
APIConnectionError,
|
||||
APIConnectionTimeoutError,
|
||||
APIUserAbortError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
RateLimitError,
|
||||
BadRequestError,
|
||||
AuthenticationError,
|
||||
InternalServerError,
|
||||
PermissionDeniedError,
|
||||
UnprocessableEntityError,
|
||||
} from 'openai';
|
||||
|
||||
export * from './resource';
|
||||
export * from './streaming';
|
||||
export * from './util';
|
||||
|
||||
export default {
|
||||
version: process.env.PKG_VERSION,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { APIError } from 'openai';
|
||||
|
||||
export type MinimaxAPIResponse = {
|
||||
base_resp: {
|
||||
status_code: number;
|
||||
status_msg: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function assertStatusCode(data: MinimaxAPIResponse) {
|
||||
if (data.base_resp.status_code === 0) return;
|
||||
|
||||
const error = {
|
||||
code: data.base_resp.status_code,
|
||||
message: data.base_resp.status_msg,
|
||||
};
|
||||
|
||||
throw new APIError(undefined, error, undefined, undefined);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface MinimaxAIOptions {
|
||||
baseURL?: string;
|
||||
orgId?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
defaultHeaders?: Headers;
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class MinimaxAI extends APIClient {
|
||||
protected orgId: string;
|
||||
|
||||
protected apiKey: string;
|
||||
|
||||
private _options: MinimaxAIOptions;
|
||||
|
||||
constructor(options: MinimaxAIOptions = {}) {
|
||||
const {
|
||||
orgId = process.env.MINIMAX_API_ORG || '',
|
||||
apiKey = process.env.MINIMAX_API_KEY || '',
|
||||
baseURL = 'https://api.minimax.chat/v1',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
this.orgId = orgId;
|
||||
}
|
||||
|
||||
audio = new API.Audio(this);
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
embeddings = new API.Embeddings(this);
|
||||
|
||||
protected authHeaders(): Headers {
|
||||
return {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return {
|
||||
GroupId: this.orgId,
|
||||
...this._options.defaultQuery,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace MinimaxAI {
|
||||
export type Chat = API.Chat;
|
||||
export type ChatModel = API.ChatModel;
|
||||
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
API.ChatCompletionCreateParamsNonStreaming;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
API.ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type Embeddings = API.Embeddings;
|
||||
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
|
||||
|
||||
export type Audio = API.Audio;
|
||||
}
|
||||
|
||||
export default MinimaxAI;
|
||||
@@ -0,0 +1,7 @@
|
||||
// File generated from our OpenAPI spec by Stainless.
|
||||
import { APIResource } from '../../../resource';
|
||||
import { Speech } from './speech';
|
||||
|
||||
export class Audio extends APIResource {
|
||||
speech = new Speech(this._client);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Audio } from './audio';
|
||||
export { type SpeechCreateParams, type SpeechModel, Speech } from './speech';
|
||||
@@ -0,0 +1,234 @@
|
||||
import { OpenAIError } from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../../../resource';
|
||||
import { assertStatusCode, type MinimaxAPIResponse } from '../../error';
|
||||
|
||||
export class Speech extends APIResource {
|
||||
protected resources: Record<
|
||||
SpeechModel,
|
||||
{
|
||||
model: string;
|
||||
endpoint: string;
|
||||
resposne_type: 'json' | 'binary' | 'stream';
|
||||
}
|
||||
> = {
|
||||
'speech-01': {
|
||||
model: 'speech-01',
|
||||
endpoint: '/text_to_speech',
|
||||
resposne_type: 'binary',
|
||||
},
|
||||
'speech-01-pro': {
|
||||
model: 'speech-01',
|
||||
endpoint: '/t2a_pro',
|
||||
resposne_type: 'json',
|
||||
},
|
||||
// Note: 返回的是 SSE 流数据
|
||||
// 'speech-01-stream': {
|
||||
// model: 'speech-01',
|
||||
// endpoint: '/tts/stream',
|
||||
// resposne_type: 'stream',
|
||||
// },
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates audio from the input text.
|
||||
*
|
||||
* See https://api.minimax.chat/document/guides/T2A-model/tts
|
||||
*/
|
||||
create(
|
||||
params: Speech.SpeechCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<Response>;
|
||||
|
||||
create(
|
||||
params: Speech.SpeechCreateParams,
|
||||
options: RequestOptions & {
|
||||
__binaryResponse: false;
|
||||
},
|
||||
): Promise<Speech.AudioCreateResponse>;
|
||||
|
||||
async create(
|
||||
params: Speech.SpeechCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<Response | Speech.AudioCreateResponse> {
|
||||
const { input, voice, ...rest } = params;
|
||||
|
||||
const resource = this.resources[params.model];
|
||||
if (!resource) {
|
||||
throw new OpenAIError(`Invalid model: ${params.model}`);
|
||||
}
|
||||
|
||||
const body: Record<string, any> = {
|
||||
...rest,
|
||||
text: input,
|
||||
model: resource.model,
|
||||
};
|
||||
|
||||
if (voice) {
|
||||
body.voice_id = voice;
|
||||
}
|
||||
|
||||
const response: Response = await this._client.post(resource.endpoint, {
|
||||
...options,
|
||||
body,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
// Note: pro 模型返回 json
|
||||
if (
|
||||
options?.__binaryResponse ||
|
||||
resource.resposne_type === 'binary' ||
|
||||
resource.resposne_type === 'stream'
|
||||
)
|
||||
return response;
|
||||
|
||||
return response.json().then((data: Speech.AudioCreateResponse) => {
|
||||
assertStatusCode(data);
|
||||
|
||||
return fetch(data.audio_file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type SpeechModel = Speech.SpeechModel;
|
||||
|
||||
export type SpeechCreateParams = Speech.SpeechCreateParams;
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace Speech {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type SpeechModel =
|
||||
| (string & NonNullable<unknown>)
|
||||
| 'speech-01'
|
||||
| 'speech-01-pro';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface SpeechCreateParams {
|
||||
/**
|
||||
* One of the available [TTS models](https://api.minimax.chat/document/guides/T2A-model/tts)
|
||||
*/
|
||||
model: SpeechModel;
|
||||
|
||||
/**
|
||||
* The text to generate audio for.
|
||||
*/
|
||||
input: string;
|
||||
|
||||
/**
|
||||
* The voice to use when generating the audio.
|
||||
*
|
||||
* - 青涩青年音色(male-qn-qingse)
|
||||
* - 精英青年音色(male-qn-jingying)
|
||||
* - 霸道青年音色(male-qn-badao)
|
||||
* - 青年大学生音色(male-qn-daxuesheng)
|
||||
* - 少女音色(female-shaonv)
|
||||
* - 御姐音色(female-yujie)
|
||||
* - 成熟女性音色(female-chengshu)
|
||||
* - 甜美女性音色(female-tianmei)
|
||||
* - 男性主持人(presenter_male)
|
||||
* - 女性主持人(presenter_female)
|
||||
* - 男性有声书1(audiobook_male_1)
|
||||
* - 男性有声书2(audiobook_male_2)
|
||||
* - 女性有声书1(audiobook_female_1)
|
||||
* - 女性有声书2(audiobook_female_2)
|
||||
* - 青涩青年音色-beta(male-qn-qingse-jingpin)
|
||||
* - 精英青年音色-beta(male-qn-jingying-jingpin)
|
||||
* - 霸道青年音色-beta(male-qn-badao-jingpin)
|
||||
* - 青年大学生音色-beta(male-qn-daxuesheng-jingpin)
|
||||
* - 少女音色-beta(female-shaonv-jingpin)
|
||||
* - 御姐音色-beta(female-yujie-jingpin)
|
||||
* - 成熟女性音色-beta(female-chengshu-jingpin)
|
||||
* - 甜美女性音色-beta(female-tianmei-jingpin)
|
||||
*/
|
||||
voice:
|
||||
| (string & NonNullable<unknown>)
|
||||
| 'male-qn-qingse'
|
||||
| 'male-qn-jingying'
|
||||
| 'male-qn-badao'
|
||||
| 'male-qn-daxuesheng'
|
||||
| 'female-shaonv'
|
||||
| 'female-yujie'
|
||||
| 'female-chengshu'
|
||||
| 'female-tianmei'
|
||||
| 'presenter_male'
|
||||
| 'presenter_female'
|
||||
| 'audiobook_male_1'
|
||||
| 'audiobook_male_2'
|
||||
| 'audiobook_female_1'
|
||||
| 'audiobook_female_2'
|
||||
| 'male-qn-qingse-jingpin'
|
||||
| 'male-qn-jingying-jingpin'
|
||||
| 'male-qn-badao-jingpin'
|
||||
| 'male-qn-daxuesheng-jingpin'
|
||||
| 'female-shaonv-jingpin'
|
||||
| 'female-yujie-jingpin'
|
||||
| 'female-chengshu-jingpin'
|
||||
| 'female-tianmei-jingpin';
|
||||
|
||||
/**
|
||||
* The speed of the generated audio.
|
||||
*
|
||||
* Range: 0.5 - 2.0
|
||||
*
|
||||
* @defaultValue 1.0
|
||||
*/
|
||||
speed?: number;
|
||||
|
||||
/**
|
||||
* The vol of the generated audio.
|
||||
*
|
||||
*
|
||||
* Range: 0~1
|
||||
*
|
||||
* @defaultValue 1.0
|
||||
*/
|
||||
vol?: number;
|
||||
|
||||
/**
|
||||
* The pitch of the generated audio.
|
||||
*
|
||||
* Range: 0~1
|
||||
*
|
||||
* @defaultValue 0
|
||||
*/
|
||||
pitch?: number;
|
||||
|
||||
/**
|
||||
* 生成声音的采样率。t2a_pro 可用
|
||||
*
|
||||
* Range: [16000, 24000]
|
||||
*
|
||||
* @defaultValue 24000
|
||||
*/
|
||||
audio_sample_rate?: number;
|
||||
|
||||
/**
|
||||
* 生成声音的比特率. t2a_pro 可用
|
||||
*
|
||||
* Range: [32000, 64000,128000]
|
||||
*
|
||||
* @defaultValue 128000
|
||||
*/
|
||||
bitrate?: number;
|
||||
|
||||
/**
|
||||
* The format to audio in. Supported formats are `mp3`, `opus`, `aac`, and `flac`.
|
||||
*/
|
||||
response_format?: 'mp3' | 'opus' | 'aac' | 'flac';
|
||||
}
|
||||
|
||||
export interface AudioCreateResponse extends MinimaxAPIResponse {
|
||||
audio_file: string;
|
||||
subtitle_file: string;
|
||||
trace_id: string;
|
||||
extra_info: {
|
||||
audio_length: number;
|
||||
audio_sample_rate: number;
|
||||
audio_size: number;
|
||||
bitrate: number;
|
||||
word_count: number;
|
||||
invisible_character_ratio: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { APIResource } from '../../../resource';
|
||||
import { Completions } from './completions';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
import OpenAI, { APIError, OpenAIError } from 'openai';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../../resource';
|
||||
import { iterMessages, SSEDecoder } from '../../../streaming';
|
||||
import { assertStatusCode } from '../../error';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
protected resources: Record<
|
||||
ChatModel,
|
||||
{
|
||||
model: ChatModel;
|
||||
endpoint: string;
|
||||
}
|
||||
> = {
|
||||
'abab5-chat': {
|
||||
model: 'abab5-chat',
|
||||
endpoint: '/text/chatcompletion',
|
||||
},
|
||||
'abab5.5-chat': {
|
||||
model: 'abab5.5-chat',
|
||||
endpoint: '/text/chatcompletion',
|
||||
},
|
||||
'abab5.5-chat-pro': {
|
||||
model: 'abab5.5-chat',
|
||||
endpoint: '/text/chatcompletion_pro',
|
||||
},
|
||||
};
|
||||
|
||||
protected system =
|
||||
'MM智能助理是一款由MiniMax自研的,没有调用其他产品的接口的大型语言模型。MiniMax是一家中国科技公司,一直致力于进行大模型相关的研究。';
|
||||
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*
|
||||
* See https://api.minimax.chat/document/guides/chat-model/chat/api
|
||||
*/
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
) {
|
||||
const resource = this.resources[params.model];
|
||||
|
||||
if (!resource) {
|
||||
throw new OpenAIError(`Invalid model: ${params.model}`);
|
||||
}
|
||||
|
||||
const body = this.buildCreateParams(params);
|
||||
|
||||
const response: Response = await this._client.post(resource.endpoint, {
|
||||
...options,
|
||||
body: { ...body, model: resource.model },
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (body.stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return Completions.fromSSEResponse(params.model, response, controller);
|
||||
}
|
||||
|
||||
return Completions.fromResponse(params.model, await response.json());
|
||||
}
|
||||
|
||||
protected buildCreateParams(
|
||||
params: ChatCompletionCreateParams,
|
||||
): ChatCompletions.ChatCompletionCreateParams {
|
||||
const { model, messages = [], max_tokens, ...rest } = params;
|
||||
|
||||
const data: ChatCompletions.ChatCompletionCreateParams = {
|
||||
model,
|
||||
messages: [],
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (max_tokens) {
|
||||
data.tokens_to_generate = max_tokens;
|
||||
}
|
||||
|
||||
const head = messages[0];
|
||||
|
||||
// minimax 的 system 是独立字段
|
||||
const system = head && head.role === 'system' ? head.content : null;
|
||||
|
||||
// 移除 system 角色的消息
|
||||
if (system) {
|
||||
messages.splice(0, 1);
|
||||
}
|
||||
|
||||
if (model === 'abab5.5-chat-pro') {
|
||||
data.bot_setting = [
|
||||
{
|
||||
bot_name: 'MM智能助理',
|
||||
content: system || this.system,
|
||||
},
|
||||
];
|
||||
data.reply_constraints = {
|
||||
sender_type: 'BOT',
|
||||
sender_name: 'MM智能助理',
|
||||
};
|
||||
} else {
|
||||
data.role_meta = {
|
||||
bot_name: 'MM智能助理',
|
||||
user_name: '用户',
|
||||
};
|
||||
data.prompt = system || this.system;
|
||||
}
|
||||
|
||||
data.messages = messages.map((item) => {
|
||||
switch (item.role) {
|
||||
case 'assistant':
|
||||
return {
|
||||
sender_type: 'BOT',
|
||||
text: item.content as string,
|
||||
};
|
||||
default: {
|
||||
const message: ChatCompletions.ChatMessage = {
|
||||
sender_type: 'USER',
|
||||
text: item.content as string,
|
||||
};
|
||||
|
||||
if (model === 'abab5.5-chat-pro') {
|
||||
message.sender_name = '用户';
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (params.stream) {
|
||||
data.use_standard_sse = true;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static fromResponse(
|
||||
model: ChatModel,
|
||||
data: ChatCompletions.ChatCompletion,
|
||||
): OpenAI.ChatCompletion {
|
||||
assertStatusCode(data);
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
model: data.model,
|
||||
choices: data.choices.map((choice, index) => {
|
||||
const { finish_reason } = choice;
|
||||
|
||||
if (model === 'abab5.5-chat-pro') {
|
||||
return {
|
||||
index,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: choice.messages[0].text,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: choice.text,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason,
|
||||
};
|
||||
}),
|
||||
created: data.created,
|
||||
object: 'chat.completion',
|
||||
usage: data.usage,
|
||||
};
|
||||
}
|
||||
|
||||
static fromSSEResponse(
|
||||
model: ChatModel,
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
let consumed = false;
|
||||
const decoder = new SSEDecoder();
|
||||
|
||||
function transform(
|
||||
data: ChatCompletions.ChatCompletionChunk,
|
||||
): OpenAI.ChatCompletionChunk {
|
||||
return {
|
||||
id: data.request_id,
|
||||
model,
|
||||
choices: data.choices.map((choice, index) => {
|
||||
const { finish_reason = null } = choice;
|
||||
|
||||
if (model === 'abab5.5-chat-pro') {
|
||||
const content = choice.messages[0].text;
|
||||
|
||||
return {
|
||||
index,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: finish_reason === 'stop' ? '' : content,
|
||||
},
|
||||
finish_reason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: choice.delta,
|
||||
},
|
||||
finish_reason,
|
||||
};
|
||||
}),
|
||||
object: 'chat.completion.chunk',
|
||||
created: data.created,
|
||||
};
|
||||
}
|
||||
|
||||
async function* iterator(): AsyncIterator<
|
||||
OpenAI.ChatCompletionChunk,
|
||||
any,
|
||||
undefined
|
||||
> {
|
||||
if (consumed) {
|
||||
throw new Error(
|
||||
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
|
||||
);
|
||||
}
|
||||
consumed = true;
|
||||
let done = false;
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const sse of iterMessages(response, decoder, controller)) {
|
||||
if (done) continue;
|
||||
|
||||
if (sse.data.startsWith('[DONE]')) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sse.event === null) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(sse.data);
|
||||
} catch (e) {
|
||||
console.error(`Could not parse message into JSON:`, sse.data);
|
||||
console.error(`From chunk:`, sse.raw);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (data && data.code) {
|
||||
throw new APIError(undefined, data, undefined, undefined);
|
||||
}
|
||||
|
||||
yield transform(data);
|
||||
}
|
||||
}
|
||||
done = true;
|
||||
} catch (e) {
|
||||
// If the user calls `stream.controller.abort()`, we should exit without throwing.
|
||||
if (e instanceof Error && e.name === 'AbortError') return;
|
||||
throw e;
|
||||
} finally {
|
||||
// If the user `break`s, abort the ongoing request.
|
||||
if (!done) controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
|
||||
model: ChatModel;
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsStreaming {
|
||||
model: ChatModel;
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatModel = 'abab5-chat' | 'abab5.5-chat' | 'abab5.5-chat-pro';
|
||||
|
||||
export namespace ChatCompletions {
|
||||
export type ChatMessage = {
|
||||
sender_type: 'USER' | 'BOT' | 'FUNCTION';
|
||||
sender_name?: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ChatCompletionCreateParams {
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
model: ChatModel;
|
||||
|
||||
/**
|
||||
* 对话背景、人物或功能设定
|
||||
*
|
||||
* 和 bot_setting 互斥
|
||||
*/
|
||||
prompt?: string | null;
|
||||
|
||||
/**
|
||||
* 对话 meta 信息
|
||||
*
|
||||
* 和 bot_setting 互斥
|
||||
*/
|
||||
role_meta?: {
|
||||
/**
|
||||
* 用户代称
|
||||
*/
|
||||
user_name: string;
|
||||
/**
|
||||
* AI 代称
|
||||
*/
|
||||
bot_name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* pro 模式下,可以设置 bot 的名称和内容
|
||||
*
|
||||
* 和 prompt 互斥
|
||||
*/
|
||||
bot_setting?: {
|
||||
bot_name: string;
|
||||
content: string;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* pro 模式下,设置模型回复要求
|
||||
*/
|
||||
reply_constraints?: {
|
||||
sender_type: string;
|
||||
sender_name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 对话内容
|
||||
*/
|
||||
messages: ChatMessage[];
|
||||
|
||||
/**
|
||||
* 如果为 true,则表明设置当前请求为续写模式,回复内容为传入 messages 的最后一句话的续写;
|
||||
*
|
||||
* 此时最后一句发送者不限制 USER,也可以为 BOT。
|
||||
*/
|
||||
continue_last_message?: boolean | null;
|
||||
|
||||
/**
|
||||
* 内容随机性
|
||||
*/
|
||||
temperature?: number | null;
|
||||
|
||||
/**
|
||||
* 生成文本的多样性
|
||||
*/
|
||||
top_p?: number | null;
|
||||
|
||||
/**
|
||||
* 最大生成token数,需要注意的是,这个参数并不会影响模型本身的生成效果,
|
||||
*
|
||||
* 而是仅仅通过以截断超出的 token 的方式来实现功能需要保证输入上文的 token 个数和这个值加一起小于 6144 或者 16384,否则请求会失败
|
||||
*/
|
||||
tokens_to_generate?: number | null;
|
||||
|
||||
/**
|
||||
* 对输出中易涉及隐私问题的文本信息进行脱敏,
|
||||
*
|
||||
* 目前包括但不限于邮箱、域名、链接、证件号、家庭住址等,默认 false,即开启脱敏
|
||||
*/
|
||||
skip_info_mask?: boolean | null;
|
||||
|
||||
/**
|
||||
* 对输出中易涉及隐私问题的文本信息进行打码,
|
||||
*
|
||||
* 目前包括但不限于邮箱、域名、链接、证件号、家庭住址等,默认true,即开启打码
|
||||
*/
|
||||
mask_sensitive_info?: boolean | null;
|
||||
|
||||
/**
|
||||
* 生成多少个结果;不设置默认为1,最大不超过4。
|
||||
*
|
||||
* 由于 beam_width 生成多个结果,会消耗更多 token。
|
||||
*/
|
||||
beam_width?: number | null;
|
||||
|
||||
/**
|
||||
* 是否以流式接口的形式返回数据,默认 false
|
||||
*/
|
||||
stream?: boolean | null;
|
||||
|
||||
/**
|
||||
* 是否使用标准 SSE 格式,设置为 true 时,
|
||||
* 流式返回的结果将以两个换行为分隔符。
|
||||
*
|
||||
* 只有在 stream=true 时,此参数才会生效。
|
||||
*/
|
||||
use_standard_sse?: boolean | null;
|
||||
}
|
||||
|
||||
export type ChatCompletionChoice = {
|
||||
index?: number;
|
||||
text: string;
|
||||
messages: {
|
||||
sender_type: 'BOT';
|
||||
sender_name: string;
|
||||
text: string;
|
||||
}[];
|
||||
finish_reason:
|
||||
| 'stop'
|
||||
| 'length'
|
||||
| 'tool_calls'
|
||||
| 'content_filter'
|
||||
| 'function_call';
|
||||
};
|
||||
|
||||
export interface ChatCompletion {
|
||||
id: string;
|
||||
created: number;
|
||||
model: ChatModel;
|
||||
reply: string;
|
||||
choices: ChatCompletionChoice[];
|
||||
usage: {
|
||||
/**
|
||||
* Number of tokens in the generated completion.
|
||||
*/
|
||||
completion_tokens: number;
|
||||
|
||||
/**
|
||||
* Number of tokens in the prompt.
|
||||
*/
|
||||
prompt_tokens: number;
|
||||
|
||||
/**
|
||||
* Total number of tokens used in the request (prompt + completion).
|
||||
*/
|
||||
total_tokens: number;
|
||||
};
|
||||
input_sensitive: boolean;
|
||||
output_sensitive: boolean;
|
||||
base_resp: {
|
||||
status_code: number;
|
||||
status_msg: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type ChatCompletionChunkChoice = {
|
||||
index: number;
|
||||
delta: string;
|
||||
messages: {
|
||||
sender_type: 'BOT';
|
||||
sender_name: string;
|
||||
text: string;
|
||||
}[];
|
||||
finish_reason:
|
||||
| 'stop'
|
||||
| 'length'
|
||||
| 'content_filter'
|
||||
| 'function_call'
|
||||
| null;
|
||||
};
|
||||
|
||||
export interface ChatCompletionChunk {
|
||||
request_id: string;
|
||||
created: number;
|
||||
model: ChatModel;
|
||||
reply: string;
|
||||
choices: ChatCompletionChunkChoice[];
|
||||
usage: {
|
||||
total_tokens: number;
|
||||
};
|
||||
input_sensitive: false;
|
||||
output_sensitive: false;
|
||||
base_resp: {
|
||||
status_code: number;
|
||||
status_msg: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Chat } from './chat';
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1,70 @@
|
||||
import OpenAI from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
import { assertStatusCode } from '../error';
|
||||
|
||||
export class Embeddings extends APIResource {
|
||||
/**
|
||||
* Creates an embedding vector representing the input text.
|
||||
*
|
||||
* See https://api.minimax.chat/document/guides/Embeddings
|
||||
*/
|
||||
async create(
|
||||
params: EmbeddingCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<OpenAI.CreateEmbeddingResponse> {
|
||||
const { model, input, type = 'query' } = params;
|
||||
|
||||
const response: Response = await this._client.post('/embeddings', {
|
||||
body: {
|
||||
model,
|
||||
texts: input,
|
||||
type,
|
||||
},
|
||||
...options,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
const data: CreateEmbeddingResponse = await response.json();
|
||||
|
||||
assertStatusCode(data);
|
||||
|
||||
return {
|
||||
data: data.vectors.map((embedding, index) => {
|
||||
return {
|
||||
embedding,
|
||||
index,
|
||||
object: 'embedding',
|
||||
};
|
||||
}),
|
||||
model,
|
||||
object: 'list',
|
||||
usage: {
|
||||
prompt_tokens: data.total_tokens,
|
||||
total_tokens: data.total_tokens,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface EmbeddingCreateParams extends OpenAI.EmbeddingCreateParams {
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
model: 'embo-01';
|
||||
|
||||
/**
|
||||
* 首先通过db生成目标内容的向量并存储到向量数据库中,之后通过query生成检索文本的向量。
|
||||
*/
|
||||
type?: 'db' | 'query';
|
||||
}
|
||||
|
||||
type CreateEmbeddingResponse = {
|
||||
vectors: number[][];
|
||||
total_tokens: number;
|
||||
base_resp: {
|
||||
status_code: number;
|
||||
status_msg: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './chat/index';
|
||||
export * from './audio/audio';
|
||||
|
||||
export { Embeddings, type EmbeddingCreateParams } from './embeddings';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './resolvers';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,262 @@
|
||||
import OpenAI, { APIError } from 'openai';
|
||||
import { _iterSSEMessages, Stream } from 'openai/streaming';
|
||||
|
||||
import { DashscopeChat, OpenAIChatCompatibility } from '../types';
|
||||
import { isMultiModal, toCompletionUsage } from './completions';
|
||||
|
||||
export function fromChatCompletionMessages(
|
||||
messages: OpenAI.ChatCompletionMessageParam[],
|
||||
): OpenAI.ChatCompletionMessageParam[] {
|
||||
return messages.map((message) => {
|
||||
if (Array.isArray(message.content)) {
|
||||
message.content.forEach((part) => {
|
||||
if (part.type === 'image_url') {
|
||||
// @ts-expect-error
|
||||
part.image = part.image_url.url;
|
||||
|
||||
// @ts-expect-error
|
||||
delete part.image_url;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
delete part.type;
|
||||
});
|
||||
} else {
|
||||
message.content = [
|
||||
// @ts-expect-error
|
||||
{ text: message.content! },
|
||||
];
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
export function fromChatCompletionTextMessages(
|
||||
messages: OpenAI.ChatCompletionMessageParam[],
|
||||
): OpenAI.ChatCompletionMessageParam[] {
|
||||
return messages.map((message) => {
|
||||
if (Array.isArray(message.content)) {
|
||||
const part = message.content.find(
|
||||
(c) => c.type === 'text',
|
||||
) as OpenAI.ChatCompletionContentPartText;
|
||||
return {
|
||||
role: message.role,
|
||||
content: part.text,
|
||||
} as OpenAI.ChatCompletionMessageParam;
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
export function fromChatCompletionCreateParams(
|
||||
params: OpenAIChatCompatibility.ChatCompletionCreateParams,
|
||||
): DashscopeChat.ChatCompletionCreateParams {
|
||||
const {
|
||||
model,
|
||||
messages,
|
||||
raw,
|
||||
response_format,
|
||||
stream_options = {},
|
||||
...parameters
|
||||
} = params;
|
||||
|
||||
const result: DashscopeChat.ChatCompletionCreateParams = {
|
||||
model,
|
||||
input: {
|
||||
messages: [],
|
||||
},
|
||||
parameters,
|
||||
};
|
||||
|
||||
if (raw === true) {
|
||||
result.input.messages = messages;
|
||||
} else if (isMultiModal(model)) {
|
||||
result.input.messages = fromChatCompletionMessages(messages);
|
||||
} else {
|
||||
result.input.messages = fromChatCompletionTextMessages(messages);
|
||||
}
|
||||
|
||||
if (params.tools) {
|
||||
result.parameters!.result_format = 'message';
|
||||
} else {
|
||||
if (response_format && response_format.type) {
|
||||
result.parameters!.result_format = response_format.type;
|
||||
}
|
||||
|
||||
if (params.stream) {
|
||||
const incremental_output = stream_options?.incremental_output ?? true;
|
||||
result.parameters!.incremental_output = incremental_output;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toChatCompletionFinishReason(
|
||||
reason?: DashscopeChat.ResponseFinish | null,
|
||||
stream?: boolean,
|
||||
) {
|
||||
if (reason === 'null' || !reason) {
|
||||
return (stream ? null : 'stop') as 'stop';
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
export function toChatCompletion(
|
||||
params: DashscopeChat.ChatCompletionCreateParams,
|
||||
response: DashscopeChat.ChatCompletion,
|
||||
): OpenAI.ChatCompletion {
|
||||
const { model } = params;
|
||||
const { output, usage } = response;
|
||||
|
||||
const choice: OpenAI.ChatCompletion.Choice = {
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop',
|
||||
};
|
||||
|
||||
// Note: `params.parameters.result_format=message`
|
||||
if (output.choices) {
|
||||
const { message, finish_reason } = output.choices[0];
|
||||
|
||||
choice.message = {
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
};
|
||||
|
||||
if (finish_reason === 'tool_calls') {
|
||||
choice.finish_reason = 'tool_calls';
|
||||
|
||||
choice.message.tool_calls = message.tool_calls;
|
||||
} else {
|
||||
choice.finish_reason = toChatCompletionFinishReason(finish_reason, true);
|
||||
}
|
||||
} else {
|
||||
choice.message.content = output.text;
|
||||
choice.finish_reason = toChatCompletionFinishReason(output.finish_reason);
|
||||
}
|
||||
|
||||
return {
|
||||
id: response.request_id,
|
||||
model,
|
||||
choices: [choice],
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
object: 'chat.completion',
|
||||
usage: toCompletionUsage(usage),
|
||||
};
|
||||
}
|
||||
|
||||
function toCompletionChunk(
|
||||
params: DashscopeChat.ChatCompletionCreateParams,
|
||||
chunk: DashscopeChat.ChatCompletion,
|
||||
): OpenAI.ChatCompletionChunk {
|
||||
const { output } = chunk;
|
||||
|
||||
const choice: OpenAI.ChatCompletionChunk.Choice = {
|
||||
index: 0,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
},
|
||||
finish_reason: null,
|
||||
};
|
||||
|
||||
// Note: work in `params.parameters.result_format=message`
|
||||
if (output.choices) {
|
||||
const { message, finish_reason } = output.choices[0];
|
||||
|
||||
choice.delta = {
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
};
|
||||
|
||||
if (finish_reason === 'tool_calls') {
|
||||
choice.finish_reason = 'tool_calls';
|
||||
choice.delta.tool_calls =
|
||||
message.tool_calls as OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall[];
|
||||
} else {
|
||||
choice.finish_reason = toChatCompletionFinishReason(finish_reason, true);
|
||||
}
|
||||
} else {
|
||||
choice.delta.content = output.text;
|
||||
choice.finish_reason = toChatCompletionFinishReason(
|
||||
output.finish_reason,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: chunk.request_id,
|
||||
model: params.model,
|
||||
choices: [choice],
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
export function toChatCompletionStream(
|
||||
params: DashscopeChat.ChatCompletionCreateParams,
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.ChatCompletionChunk> {
|
||||
let consumed = false;
|
||||
async function* iterator(): AsyncIterator<
|
||||
OpenAI.ChatCompletionChunk,
|
||||
any,
|
||||
undefined
|
||||
> {
|
||||
if (consumed) {
|
||||
throw new Error(
|
||||
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
|
||||
);
|
||||
}
|
||||
consumed = true;
|
||||
let done = false;
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const sse of _iterSSEMessages(response, controller)) {
|
||||
if (done) continue;
|
||||
|
||||
if (sse.data.startsWith('[DONE]')) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sse.event === 'result') {
|
||||
let message;
|
||||
|
||||
try {
|
||||
message = JSON.parse(sse.data);
|
||||
} catch (e) {
|
||||
console.error(`Could not parse message into JSON:`, sse.data);
|
||||
console.error(`From chunk:`, sse.raw);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (message && message.code) {
|
||||
throw new APIError(undefined, message, undefined, undefined);
|
||||
}
|
||||
|
||||
yield toCompletionChunk(params, message);
|
||||
}
|
||||
}
|
||||
done = true;
|
||||
} catch (e) {
|
||||
// If the user calls `stream.controller.abort()`, we should exit without throwing.
|
||||
if (e instanceof Error && e.name === 'AbortError') return;
|
||||
throw e;
|
||||
} finally {
|
||||
// If the user `break`s, abort the ongoing request.
|
||||
if (!done) controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import OpenAI, { APIError } from 'openai';
|
||||
import { _iterSSEMessages, Stream } from 'openai/streaming';
|
||||
|
||||
import type {
|
||||
DashscopeCompletions,
|
||||
OpenAICompletionsCompatibility,
|
||||
} from '../types';
|
||||
|
||||
export function isMultiModal(model: string): boolean {
|
||||
return model.startsWith('qwen-vl');
|
||||
}
|
||||
|
||||
export function getCompletionCreateEndpoint(model: string) {
|
||||
return isMultiModal(model)
|
||||
? '/services/aigc/multimodal-generation/generation'
|
||||
: '/services/aigc/text-generation/generation';
|
||||
}
|
||||
|
||||
export function fromCompletionCreateParams(
|
||||
params: OpenAICompletionsCompatibility.CompletionCreateParams,
|
||||
): DashscopeCompletions.CompletionCreateParams {
|
||||
const { model, prompt, response_format, stream_options, ...parameters } =
|
||||
params;
|
||||
|
||||
const result: DashscopeCompletions.CompletionCreateParams = {
|
||||
model,
|
||||
input: { prompt },
|
||||
parameters,
|
||||
};
|
||||
|
||||
if (response_format && response_format.type) {
|
||||
result.parameters!.result_format = response_format.type;
|
||||
}
|
||||
|
||||
if (params.stream) {
|
||||
const { incremental_output } = stream_options || {};
|
||||
result.parameters!.incremental_output = incremental_output ?? true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toCompletionFinishReason(
|
||||
reason?: DashscopeCompletions.ResponseFinish | null,
|
||||
stream?: boolean,
|
||||
) {
|
||||
if (reason === 'null' || !reason) {
|
||||
return (stream ? null : 'stop') as 'stop';
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
export function toCompletionUsage(
|
||||
usage: DashscopeCompletions.CompletionUsage,
|
||||
): OpenAI.CompletionUsage {
|
||||
// hack: 部分模型不存在 total tokens?
|
||||
// 如:llama2-7b-chat-v2
|
||||
const {
|
||||
output_tokens,
|
||||
input_tokens,
|
||||
total_tokens = output_tokens + input_tokens,
|
||||
} = usage;
|
||||
|
||||
return {
|
||||
completion_tokens: output_tokens,
|
||||
prompt_tokens: input_tokens,
|
||||
total_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function toCompletion(
|
||||
params: DashscopeCompletions.CompletionCreateParams,
|
||||
response: DashscopeCompletions.Completion,
|
||||
stream?: boolean,
|
||||
): OpenAI.Completion {
|
||||
const { model } = params;
|
||||
const { output, usage } = response;
|
||||
|
||||
const choice: OpenAI.CompletionChoice = {
|
||||
index: 0,
|
||||
text: output.text,
|
||||
logprobs: null,
|
||||
finish_reason: toCompletionFinishReason(output.finish_reason, stream),
|
||||
};
|
||||
|
||||
return {
|
||||
id: response.request_id,
|
||||
model,
|
||||
choices: [choice],
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
object: 'text_completion',
|
||||
usage: toCompletionUsage(usage),
|
||||
};
|
||||
}
|
||||
|
||||
export function toCompletionStream(
|
||||
params: DashscopeCompletions.CompletionCreateParams,
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Stream<OpenAI.Completion> {
|
||||
let consumed = false;
|
||||
async function* iterator(): AsyncIterator<OpenAI.Completion, any, undefined> {
|
||||
if (consumed) {
|
||||
throw new Error(
|
||||
'Cannot iterate over a consumed stream, use `.tee()` to split the stream.',
|
||||
);
|
||||
}
|
||||
|
||||
consumed = true;
|
||||
let done = false;
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const sse of _iterSSEMessages(response, controller)) {
|
||||
if (done) continue;
|
||||
|
||||
if (sse.data.startsWith('[DONE]')) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sse.event === 'result') {
|
||||
let message;
|
||||
|
||||
try {
|
||||
message = JSON.parse(sse.data);
|
||||
} catch (e) {
|
||||
console.error(`Could not parse message into JSON:`, sse.data);
|
||||
console.error(`From chunk:`, sse.raw);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (message && message.code) {
|
||||
throw new APIError(undefined, message, undefined, undefined);
|
||||
}
|
||||
|
||||
yield toCompletion(params, message, true);
|
||||
}
|
||||
}
|
||||
done = true;
|
||||
} catch (e) {
|
||||
// If the user calls `stream.controller.abort()`, we should exit without throwing.
|
||||
if (e instanceof Error && e.name === 'AbortError') return;
|
||||
throw e;
|
||||
} finally {
|
||||
// If the user `break`s, abort the ongoing request.
|
||||
if (!done) controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
return new Stream(iterator, controller);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
import { DashscopeEmbeddings, OpenAIEmbeddingsCompatibility } from '../types';
|
||||
|
||||
export function fromEmbeddingCreatePrams(
|
||||
params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams,
|
||||
): DashscopeEmbeddings.EmbeddingCreateParams {
|
||||
return {
|
||||
model: params.model,
|
||||
input: {
|
||||
texts: params.input,
|
||||
},
|
||||
parameters: {
|
||||
text_type: params.type || 'query',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function toEmbedding(
|
||||
params: OpenAIEmbeddingsCompatibility.EmbeddingCreateParams,
|
||||
response: DashscopeEmbeddings.CreateEmbeddingResponse,
|
||||
): OpenAI.CreateEmbeddingResponse {
|
||||
const { output, usage } = response;
|
||||
|
||||
return {
|
||||
object: 'list',
|
||||
model: params.model,
|
||||
data: output.embeddings.map(({ text_index, embedding }) => ({
|
||||
index: text_index,
|
||||
embedding,
|
||||
object: 'embedding',
|
||||
})),
|
||||
usage: {
|
||||
prompt_tokens: usage.total_tokens,
|
||||
total_tokens: usage.total_tokens,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './chat';
|
||||
export * from './completions';
|
||||
@@ -0,0 +1,58 @@
|
||||
import type OpenAI from 'openai';
|
||||
|
||||
import type { DashscopeCompletions } from './completions';
|
||||
|
||||
export namespace DashscopeChat {
|
||||
/**
|
||||
* https://help.aliyun.com/zh/dashscope/developer-reference/model-square
|
||||
*/
|
||||
export type ChatModel = DashscopeCompletions.CompletionModel;
|
||||
|
||||
export type ResponseFinish =
|
||||
| 'stop'
|
||||
| 'length'
|
||||
| 'tool_calls'
|
||||
| 'content_filter'
|
||||
| 'function_call'
|
||||
| 'null';
|
||||
|
||||
export interface ChatCompletionParametersParam
|
||||
extends DashscopeCompletions.CompletionParametersParam {
|
||||
/**
|
||||
* 指定可供模型调用的工具列表
|
||||
*
|
||||
* 当输入多个工具时,模型会选择其中一个生成结果。
|
||||
*
|
||||
* 警告:
|
||||
*
|
||||
* - tools 暂时无法和 incremental_output 参数同时使用
|
||||
* - 使用 tools 时需要同时指定 result_format 为 message
|
||||
*/
|
||||
tools?: OpenAI.ChatCompletionTool[];
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParams {
|
||||
model: ({} & string) | ChatModel;
|
||||
input: {
|
||||
messages: OpenAI.ChatCompletionMessageParam[];
|
||||
};
|
||||
parameters?: ChatCompletionParametersParam;
|
||||
}
|
||||
|
||||
export namespace ChatCompletion {
|
||||
export interface Output {
|
||||
text: string;
|
||||
finish_reason?: ResponseFinish;
|
||||
choices: OpenAI.ChatCompletion.Choice[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 详见 [输入参数配置](https://help.aliyun.com/zh/dashscope/developer-reference/api-details)
|
||||
*/
|
||||
export interface ChatCompletion {
|
||||
request_id: string;
|
||||
usage: DashscopeCompletions.CompletionUsage;
|
||||
output: ChatCompletion.Output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
export namespace DashscopeCompletions {
|
||||
/**
|
||||
* https://help.aliyun.com/zh/dashscope/developer-reference/model-square
|
||||
*/
|
||||
export type CompletionModel =
|
||||
// 通义千问
|
||||
| 'qwen-long'
|
||||
| 'qwen-turbo'
|
||||
| 'qwen-plus'
|
||||
| 'qwen-max'
|
||||
| 'qwen-max-0428'
|
||||
| 'qwen-max-0403'
|
||||
| 'qwen-max-0107'
|
||||
| 'qwen-max-1201'
|
||||
| 'qwen-max-longcontext'
|
||||
// 通义千问开源系列
|
||||
| 'qwen-7b-chat'
|
||||
| 'qwen-14b-chat'
|
||||
| 'qwen-72b-chat'
|
||||
// 多模型
|
||||
| 'qwen-vl-v1'
|
||||
| 'qwen-vl-chat-v1'
|
||||
| 'qwen-vl-plus'
|
||||
// LLAMA2;
|
||||
| 'llama2-7b-chat-v2'
|
||||
| 'llama2-13b-chat-v2'
|
||||
// 百川
|
||||
| 'baichuan-7b-v1'
|
||||
| 'baichuan2-13b-chat-v1'
|
||||
| 'baichuan2-7b-chat-v1'
|
||||
// ChatGLM
|
||||
| 'chatglm3-6b'
|
||||
| 'chatglm-6b-v2';
|
||||
|
||||
export type ResponseFinish = 'stop' | 'length' | 'null';
|
||||
|
||||
/**
|
||||
* - text 旧版本的 text
|
||||
* - message 兼容 openai 的 message
|
||||
*
|
||||
* @defaultValue "text"
|
||||
*/
|
||||
export type ResponseFormat = 'text' | 'message';
|
||||
|
||||
export type CompletionParametersParam = {
|
||||
/**
|
||||
* 启用流式输出
|
||||
*
|
||||
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
|
||||
*
|
||||
* 通过 {@link CompletionParametersParam.incremental_output incremental_output} 参数关闭。
|
||||
*/
|
||||
stream?: boolean | null;
|
||||
|
||||
/**
|
||||
* 启用增量输出
|
||||
*
|
||||
* 启用 {@link ChatCompletionCreateParamsBase.stream stream} 参数时,每次输出是否每次都包含前面输出的内容。
|
||||
*
|
||||
* Warning: Function call 信息暂时不支持增量输出,开启时需要注意。
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
incremental_output?: boolean | null;
|
||||
|
||||
/**
|
||||
* 生成结果的格式
|
||||
*
|
||||
* @defaultValue "text"
|
||||
*/
|
||||
result_format?: ResponseFormat;
|
||||
|
||||
/**
|
||||
* 生成时,随机数的种子,用于控制模型生成的随机性。
|
||||
*
|
||||
* 如果使用相同的种子,每次运行生成的结果都将相同;
|
||||
* 当需要复现模型的生成结果时,可以使用相同的种子。
|
||||
* seed参数支持无符号64位整数类型。
|
||||
*
|
||||
* @defaultValue 1234
|
||||
*/
|
||||
seed?: number | null;
|
||||
|
||||
/**
|
||||
* 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量。最大值和默认值均为1500
|
||||
*
|
||||
* @defaultValue 1500
|
||||
*/
|
||||
max_tokens?: number | null;
|
||||
|
||||
/**
|
||||
* 生成文本的多样性
|
||||
*
|
||||
* @defaultValue 0.8
|
||||
*/
|
||||
top_p?: number | null;
|
||||
|
||||
/**
|
||||
* 生成时,采样候选集的大小。
|
||||
*
|
||||
* 例如,
|
||||
* 取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。
|
||||
* 取值越大,生成的随机性越高;取值越小,生成的确定性越高。
|
||||
*
|
||||
* 注意:如果top_k参数为空或者top_k的值大于100,表示不启用top_k策略,此时仅有top_p策略生效,默认是空。
|
||||
*
|
||||
* @defaultValue 80
|
||||
*/
|
||||
top_k?: number | null;
|
||||
|
||||
/**
|
||||
* 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。默认为1.1。
|
||||
*/
|
||||
repetition_penalty?: number | null;
|
||||
|
||||
/**
|
||||
* 用户控制模型生成时整个序列中的重复度。
|
||||
*
|
||||
* 提高时可以降低模型生成的重复度,取值范围[-2.0, 2.0]。
|
||||
*/
|
||||
presence_penalty?: number | null;
|
||||
|
||||
/**
|
||||
* 内容随机性
|
||||
*
|
||||
* @defaultValue 1.0
|
||||
*/
|
||||
temperature?: number | null;
|
||||
|
||||
/**
|
||||
* 生成停止标识符
|
||||
*/
|
||||
stop?: string | string[] | null;
|
||||
|
||||
/**
|
||||
* 生成时,是否参考搜索的结果。
|
||||
*
|
||||
* 注意:打开搜索并不意味着一定会使用搜索结果;
|
||||
* 如果打开搜索,模型会将搜索结果作为prompt,进而“自行判断”是否生成结合搜索结果的文本,默认为false
|
||||
*/
|
||||
enable_search?: boolean | null;
|
||||
};
|
||||
|
||||
export interface CompletionCreateParams {
|
||||
model: ({} & string) | CompletionModel;
|
||||
input: {
|
||||
prompt: string;
|
||||
};
|
||||
parameters?: DashscopeCompletions.CompletionParametersParam;
|
||||
}
|
||||
|
||||
export interface CompletionUsage {
|
||||
output_tokens: number;
|
||||
input_tokens: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export namespace Completion {
|
||||
export interface Output {
|
||||
text: string;
|
||||
finish_reason: DashscopeCompletions.ResponseFinish;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Completion {
|
||||
request_id: string;
|
||||
usage: CompletionUsage;
|
||||
output: Completion.Output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export namespace DashscopeEmbeddings {
|
||||
export type EmbeddingModel =
|
||||
| 'text-embedding-v1'
|
||||
| 'text-embedding-async-v1'
|
||||
| 'text-embedding-v2'
|
||||
| 'text-embedding-async-v2';
|
||||
|
||||
export interface EmbeddingCreateParams {
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
model: ({} & string) | EmbeddingModel;
|
||||
input: {
|
||||
/**
|
||||
* 输入文本
|
||||
*/
|
||||
texts: string | Array<string> | Array<number> | Array<Array<number>>;
|
||||
};
|
||||
parameters: {
|
||||
/**
|
||||
* 文本转换为向量后可以应用于检索、聚类、分类等下游任务,对检索这类非对称任务为了达到更好的检索效果
|
||||
* 建议区分查询文本(query)和底库文本(document)类型,
|
||||
* 聚类、分类等对称任务可以不用特殊指定,采用系统默认值"document"即可
|
||||
*
|
||||
* @defaultValue 'query'
|
||||
*/
|
||||
text_type?: 'query' | 'document';
|
||||
};
|
||||
}
|
||||
|
||||
export type Embedding = {
|
||||
text_index: number;
|
||||
embedding: number[];
|
||||
};
|
||||
|
||||
export type CreateEmbeddingResponse = {
|
||||
request_id: string;
|
||||
code: string;
|
||||
message: string;
|
||||
output: {
|
||||
embeddings: Embedding[];
|
||||
};
|
||||
usage: {
|
||||
total_tokens: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './chat';
|
||||
export * from './completions';
|
||||
export * from './embeddings';
|
||||
export * from './openai';
|
||||
@@ -0,0 +1,191 @@
|
||||
import type OpenAI from 'openai';
|
||||
|
||||
import type { DashscopeChat } from './chat';
|
||||
import type { DashscopeCompletions } from './completions';
|
||||
import { DashscopeEmbeddings } from './embeddings';
|
||||
|
||||
export namespace OpenAICompletionsCompatibility {
|
||||
export type CompletionModel = DashscopeCompletions.CompletionModel;
|
||||
|
||||
export interface StreamOptions {
|
||||
/**
|
||||
* 启用增量输出
|
||||
*
|
||||
* 在启用流输出参数后,是否每次输出是否每次都包含前面输出的内容。
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
incremental_output?: boolean | null;
|
||||
}
|
||||
|
||||
export interface CompletionCreateParamsBase
|
||||
extends Pick<
|
||||
DashscopeCompletions.CompletionParametersParam,
|
||||
| 'enable_search'
|
||||
| 'temperature'
|
||||
| 'presence_penalty'
|
||||
| 'repetition_penalty'
|
||||
| 'top_k'
|
||||
| 'top_p'
|
||||
| 'seed'
|
||||
| 'stop'
|
||||
| 'max_tokens'
|
||||
| 'stream'
|
||||
> {
|
||||
/**
|
||||
* 生成模型
|
||||
*
|
||||
* 内置的 {@link CompletionModel} 是经过测试的,但你可以通过 [模型列表](https://help.aliyun.com/zh/dashscope/developer-reference/model-square) 测试其他支持的模型。
|
||||
*/
|
||||
model: ({} & string) | CompletionModel;
|
||||
|
||||
/**
|
||||
* 用户输入的指令,用于指导模型生成回复
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* 流输出额外参数
|
||||
*/
|
||||
stream_options?: StreamOptions | null;
|
||||
|
||||
/**
|
||||
* 响应格式
|
||||
*/
|
||||
response_format?: {
|
||||
type?: 'text';
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompletionCreateParamsNonStreaming
|
||||
extends CompletionCreateParamsBase {
|
||||
/**
|
||||
* 启用流式输出
|
||||
*
|
||||
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
|
||||
* 可以使用 {@link StreamOptions stream_options} 参数关闭。
|
||||
*/
|
||||
stream?: false | null;
|
||||
}
|
||||
|
||||
export interface CompletionCreateParamsStreaming
|
||||
extends CompletionCreateParamsBase {
|
||||
/**
|
||||
* 启用流式输出
|
||||
*
|
||||
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
|
||||
* 可以使用 {@link StreamOptions stream_options} 参数关闭。
|
||||
*/
|
||||
stream: true;
|
||||
}
|
||||
|
||||
export type CompletionCreateParams =
|
||||
| CompletionCreateParamsNonStreaming
|
||||
| CompletionCreateParamsStreaming;
|
||||
}
|
||||
|
||||
export namespace OpenAIChatCompatibility {
|
||||
export type ChatModel = DashscopeChat.ChatModel;
|
||||
|
||||
export interface ChatCompletionCreateParamsBase
|
||||
extends Pick<
|
||||
DashscopeCompletions.CompletionParametersParam,
|
||||
| 'enable_search'
|
||||
| 'temperature'
|
||||
| 'presence_penalty'
|
||||
| 'repetition_penalty'
|
||||
| 'top_k'
|
||||
| 'top_p'
|
||||
| 'seed'
|
||||
| 'stop'
|
||||
| 'max_tokens'
|
||||
| 'stream'
|
||||
> {
|
||||
/**
|
||||
* 聊天模型
|
||||
*
|
||||
* 内置的 {@link ChatModel} 是经过测试的,但你可以通过 [模型列表](https://help.aliyun.com/zh/dashscope/developer-reference/model-square) 测试其他支持的模型。
|
||||
*/
|
||||
model: ({} & string) | ChatModel;
|
||||
|
||||
/**
|
||||
* 聊天上下文信息
|
||||
*/
|
||||
messages: OpenAI.ChatCompletionMessageParam[];
|
||||
|
||||
/**
|
||||
* 指定可供模型调用的工具列表
|
||||
*
|
||||
* 当输入多个工具时,模型会选择其中一个生成结果。
|
||||
*/
|
||||
tools?: OpenAI.ChatCompletionTool[];
|
||||
|
||||
/**
|
||||
* SDK 内部有特殊的多模型消息适配机制
|
||||
*
|
||||
* 设置为 true 可以直接采用外部传递的消息格式
|
||||
*/
|
||||
raw?: boolean | null;
|
||||
|
||||
/**
|
||||
* 流输出额外参数
|
||||
*/
|
||||
stream_options?: OpenAICompletionsCompatibility.StreamOptions | null;
|
||||
|
||||
/**
|
||||
* 响应格式
|
||||
*/
|
||||
response_format?: {
|
||||
type?: 'text';
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends ChatCompletionCreateParamsBase {
|
||||
/**
|
||||
* 启用流式输出
|
||||
*
|
||||
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
|
||||
* 可以使用 {@link ChatCompletionCreateParamsBase.stream_options stream_options} 参数关闭。
|
||||
*/
|
||||
stream?: false | null;
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends ChatCompletionCreateParamsBase {
|
||||
/**
|
||||
* 启用流式输出
|
||||
*
|
||||
* 默认每次输出为当前生成的整个序列,最后一次输出为最终全部生成结果
|
||||
* 可以使用 {@link ChatCompletionCreateParamsBase.stream_options stream_options} 参数关闭。
|
||||
*/
|
||||
stream: true;
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
}
|
||||
|
||||
export namespace OpenAIEmbeddingsCompatibility {
|
||||
export interface EmbeddingCreateParams {
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
model: ({} & string) | DashscopeEmbeddings.EmbeddingModel;
|
||||
|
||||
/**
|
||||
* 输入文本
|
||||
*/
|
||||
input: string | Array<string> | Array<number> | Array<Array<number>>;
|
||||
|
||||
/**
|
||||
* 文本转换为向量后可以应用于检索、聚类、分类等下游任务,对检索这类非对称任务为了达到更好的检索效果
|
||||
* 建议区分查询文本(query)和底库文本(document)类型,
|
||||
* 聚类、分类等对称任务可以不用特殊指定,采用系统默认值"document"即可
|
||||
*
|
||||
* @defaultValue 'query'
|
||||
*/
|
||||
type?: 'query' | 'document';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import { APIError } from 'openai';
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type FinalRequestOptions,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface QWenAIOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
fetch?: Fetch | undefined;
|
||||
/**
|
||||
* Default headers to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* header to `undefined` or `null` in request options.
|
||||
*/
|
||||
defaultHeaders?: Headers;
|
||||
|
||||
/**
|
||||
* Default query parameters to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* param to `undefined` in request options.
|
||||
*/
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于阿里云 [DashScope 灵积模型服务](https://help.aliyun.com/zh/dashscope/product-overview/product-introduction) 的接口封装
|
||||
*
|
||||
* @deprecated 请重点关注阿里云的 [OpenAI接口兼容](https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope/) 计划。
|
||||
*/
|
||||
export class QWenAI extends APIClient {
|
||||
protected apiKey: string;
|
||||
|
||||
private _options: QWenAIOptions;
|
||||
|
||||
constructor(options: QWenAIOptions = {}) {
|
||||
const {
|
||||
apiKey = process.env.QWEN_API_KEY || '',
|
||||
baseURL = 'https://dashscope.aliyuncs.com/api/v1/',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
completions = new API.Completions(this);
|
||||
|
||||
embeddings = new API.Embeddings(this);
|
||||
|
||||
images = new API.Images(this);
|
||||
|
||||
protected override authHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultHeaders(opts: FinalRequestOptions): Headers {
|
||||
return {
|
||||
...super.defaultHeaders(opts),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
|
||||
protected override makeStatusError(
|
||||
status: number | undefined,
|
||||
error: Record<string, any> | undefined,
|
||||
message: string | undefined,
|
||||
headers: Headers | undefined,
|
||||
) {
|
||||
return APIError.generate(status, { error }, message, headers);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace QWenAI {
|
||||
export import Chat = API.Chat;
|
||||
export import ChatModel = API.ChatModel;
|
||||
export import ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export import ChatCompletionCreateParamsNonStreaming = API.ChatCompletionCreateParamsNonStreaming;
|
||||
export import ChatCompletionCreateParamsStreaming = API.ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export import Completions = API.Completions;
|
||||
export import CompletionModel = API.CompletionModel;
|
||||
export type CompletionCreateParams = API.CompletionCreateParams;
|
||||
export type CompletionCreateParamsStreaming =
|
||||
API.CompletionCreateParamsStreaming;
|
||||
export type CompletionCreateParamsNonStreaming =
|
||||
API.CompletionCreateParamsNonStreaming;
|
||||
|
||||
export import Embeddings = API.Embeddings;
|
||||
export type EmbeddingModel = API.EmbeddingModel;
|
||||
export type EmbeddingCreateParams = API.EmbeddingCreateParams;
|
||||
|
||||
export import Images = API.Images;
|
||||
export type ImageModel = API.ImageModel;
|
||||
export type ImageGenerateParams = API.ImageGenerateParams;
|
||||
}
|
||||
|
||||
export default QWenAI;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { APIResource } from '../../../resource';
|
||||
import { OpenAIChatCompatibility } from '../../dashscope';
|
||||
import { Completions } from './completions';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
|
||||
export type ChatModel = OpenAIChatCompatibility.ChatModel;
|
||||
export type ChatCompletionCreateParams =
|
||||
OpenAIChatCompatibility.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
OpenAIChatCompatibility.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming;
|
||||
@@ -0,0 +1,67 @@
|
||||
import OpenAI from 'openai';
|
||||
import { type Headers } from 'openai/core';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../../resource';
|
||||
import {
|
||||
fromChatCompletionCreateParams,
|
||||
getCompletionCreateEndpoint,
|
||||
type OpenAIChatCompatibility,
|
||||
toChatCompletion,
|
||||
toChatCompletionStream,
|
||||
} from '../../dashscope';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*
|
||||
* See https://help.aliyun.com/zh/dashscope/developer-reference/api-details
|
||||
*/
|
||||
create(
|
||||
body: OpenAIChatCompatibility.ChatCompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: OpenAIChatCompatibility.ChatCompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
body: OpenAIChatCompatibility.ChatCompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
) {
|
||||
const headers: Headers = {
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
if (body.stream) {
|
||||
headers.Accept = 'text/event-stream';
|
||||
}
|
||||
|
||||
const path = getCompletionCreateEndpoint(body.model);
|
||||
const params = fromChatCompletionCreateParams(body);
|
||||
|
||||
const response: Response = await this._client.post(path, {
|
||||
...options,
|
||||
body: params,
|
||||
headers,
|
||||
// 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
|
||||
// 设置 __binaryResponse 为 true, 是为了让 client 返回原始的 response
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (body.stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return toChatCompletionStream(params, response, controller);
|
||||
}
|
||||
|
||||
return toChatCompletion(params, await response.json());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './chat';
|
||||
@@ -0,0 +1,78 @@
|
||||
import OpenAI from 'openai';
|
||||
import { type Headers } from 'openai/core';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
import {
|
||||
fromCompletionCreateParams,
|
||||
getCompletionCreateEndpoint,
|
||||
type OpenAICompletionsCompatibility,
|
||||
toCompletion,
|
||||
toCompletionStream,
|
||||
} from '../dashscope';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
/**
|
||||
* Creates a completion for the provided prompt and parameters.
|
||||
*/
|
||||
create(
|
||||
body: OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.Completion>;
|
||||
|
||||
create(
|
||||
body: OpenAICompletionsCompatibility.CompletionCreateParamsStreaming,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.Completion>>;
|
||||
|
||||
create(
|
||||
body: OpenAICompletionsCompatibility.CompletionCreateParamsBase,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<Stream<OpenAI.Completion> | OpenAI.Completion>;
|
||||
|
||||
async create(
|
||||
body: OpenAICompletionsCompatibility.CompletionCreateParams,
|
||||
options?: OpenAI.RequestOptions,
|
||||
): Promise<OpenAI.Completion | Stream<OpenAI.Completion>> {
|
||||
const headers: Headers = {
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
if (body.stream) {
|
||||
headers.Accept = 'text/event-stream';
|
||||
}
|
||||
|
||||
const path = getCompletionCreateEndpoint(body.model);
|
||||
const params = fromCompletionCreateParams(body);
|
||||
|
||||
const response: Response = await this._client.post(path, {
|
||||
...options,
|
||||
body: params,
|
||||
headers,
|
||||
// 通义千问的响应内容被包裹了一层,需要解构并转换为 OpenAI 的格式
|
||||
// 设置 __binaryResponse 为 true, 是为了让 client 返回原始的 response
|
||||
stream: false,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
if (body.stream) {
|
||||
const controller = new AbortController();
|
||||
|
||||
options?.signal?.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return toCompletionStream(params, response, controller);
|
||||
}
|
||||
|
||||
return toCompletion(params, await response.json());
|
||||
}
|
||||
}
|
||||
|
||||
export type CompletionModel = OpenAICompletionsCompatibility.CompletionModel;
|
||||
export type CompletionCreateParams =
|
||||
OpenAICompletionsCompatibility.CompletionCreateParams;
|
||||
export type CompletionCreateParamsStreaming =
|
||||
OpenAICompletionsCompatibility.CompletionCreateParamsStreaming;
|
||||
export type CompletionCreateParamsNonStreaming =
|
||||
OpenAICompletionsCompatibility.CompletionCreateParamsNonStreaming;
|
||||
@@ -0,0 +1,46 @@
|
||||
import OpenAI from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
import { DashscopeEmbeddings } from '../dashscope';
|
||||
import {
|
||||
fromEmbeddingCreatePrams,
|
||||
toEmbedding,
|
||||
} from '../dashscope/resolvers/embeddings';
|
||||
|
||||
export class Embeddings extends APIResource {
|
||||
/**
|
||||
* Creates an embedding vector representing the input text.
|
||||
*
|
||||
* See https://help.aliyun.com/zh/dashscope/developer-reference/generic-text-vector
|
||||
*/
|
||||
async create(
|
||||
params: OpenAI.EmbeddingCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<OpenAI.CreateEmbeddingResponse> {
|
||||
const body = fromEmbeddingCreatePrams(params);
|
||||
|
||||
const response: Response = await this._client.post(
|
||||
'/services/embeddings/text-embedding/text-embedding',
|
||||
{
|
||||
...options,
|
||||
body,
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return toEmbedding(params, await response.json());
|
||||
}
|
||||
}
|
||||
|
||||
export type EmbeddingModel = Embeddings.EmbeddingModel;
|
||||
|
||||
export type EmbeddingCreateParams = Embeddings.EmbeddingCreateParams;
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace Embeddings {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type EmbeddingModel = DashscopeEmbeddings.EmbeddingModel;
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type EmbeddingCreateParams = DashscopeEmbeddings.EmbeddingCreateParams;
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import OpenAI, { OpenAIError } from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Images extends APIResource {
|
||||
/**
|
||||
* Creates an image given a prompt.
|
||||
*/
|
||||
async generate(
|
||||
params: ImageGenerateParams,
|
||||
options: RequestOptions = {},
|
||||
): Promise<OpenAI.ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const { headers, ...config } = options;
|
||||
const { model = 'wanx-v1', prompt, n = 1, cfg, ...rest } = params;
|
||||
|
||||
const taskId = await client
|
||||
.post<any, Response>('/services/aigc/text2image/image-synthesis', {
|
||||
...config,
|
||||
headers: { 'X-DashScope-Async': 'enable', ...headers },
|
||||
body: {
|
||||
model,
|
||||
input: {
|
||||
prompt,
|
||||
},
|
||||
parameters: {
|
||||
...rest,
|
||||
scale: cfg,
|
||||
n,
|
||||
},
|
||||
},
|
||||
__binaryResponse: true,
|
||||
})
|
||||
.then<ImageCreateTaskResponse>((res) => res.json())
|
||||
.then((res) => res.output.task_id);
|
||||
|
||||
return this.waitTask(taskId, options).then((images) => {
|
||||
return {
|
||||
created: Date.now() / 1000,
|
||||
data: images,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected async waitTask(
|
||||
taskId: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImageTask.Image[]> {
|
||||
const response = await this._client
|
||||
.get<any, Response>(`/tasks/${taskId}`, {
|
||||
...options,
|
||||
__binaryResponse: true,
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
.then<ImageTaskQueryResponse>((response) => response.json());
|
||||
|
||||
const { task_status, message } = response.output;
|
||||
|
||||
if (task_status === 'PENDING' || task_status === 'RUNNING') {
|
||||
return new Promise<ImageTask.Image[]>((resolve) => {
|
||||
setTimeout(() => resolve(this.waitTask(taskId, options)), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
if (task_status === 'SUCCEEDED') {
|
||||
return response.output.results.filter(
|
||||
(result) => 'url' in result,
|
||||
) as ImageTask.Image[];
|
||||
}
|
||||
|
||||
if (task_status === 'FAILED') {
|
||||
throw new OpenAIError(message);
|
||||
}
|
||||
|
||||
throw new OpenAIError('Unknown task status');
|
||||
}
|
||||
}
|
||||
|
||||
type ImageCreateTaskResponse = {
|
||||
request_id: string;
|
||||
output: {
|
||||
task_id: string;
|
||||
task_status: ImageTask.Status;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ImageTaskQueryResponse =
|
||||
| ImageTaskPendingResponse
|
||||
| ImageTaskRunningResponse
|
||||
| ImageTaskFinishedResponse
|
||||
| ImageTaskFailedResponse
|
||||
| ImageTaskUnknownResponse;
|
||||
|
||||
type ImageTaskPendingResponse = {
|
||||
request_id: string;
|
||||
output: {
|
||||
task_id: string;
|
||||
task_status: 'PENDING';
|
||||
task_metrics: ImageTask.Metrics;
|
||||
submit_time: string;
|
||||
scheduled_time: string;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ImageTaskRunningResponse = {
|
||||
request_id: string;
|
||||
output: {
|
||||
task_id: string;
|
||||
task_status: 'RUNNING';
|
||||
task_metrics: ImageTask.Metrics;
|
||||
submit_time: string;
|
||||
scheduled_time: string;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ImageTaskFinishedResponse = {
|
||||
request_id: string;
|
||||
output: {
|
||||
task_id: string;
|
||||
task_status: 'SUCCEEDED';
|
||||
task_metrics: ImageTask.Metrics;
|
||||
results: (ImageTask.Image | ImageTask.FailedError)[];
|
||||
submit_time: string;
|
||||
scheduled_time: string;
|
||||
end_time: string;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
usage: {
|
||||
image_count: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ImageTaskFailedResponse = {
|
||||
request_id: string;
|
||||
code: string;
|
||||
message: string;
|
||||
output: {
|
||||
task_status: 'FAILED';
|
||||
task_metrics: ImageTask.Metrics;
|
||||
submit_time: string;
|
||||
scheduled_time: string;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ImageTaskUnknownResponse = {
|
||||
request_id: string;
|
||||
output: {
|
||||
task_status: 'UNKNOWN';
|
||||
task_metrics: ImageTask.Metrics;
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
namespace ImageTask {
|
||||
export type Image = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type FailedError = {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type Status =
|
||||
| 'PENDING'
|
||||
| 'RUNNING'
|
||||
| 'SUCCEEDED'
|
||||
| 'FAILED'
|
||||
| 'UNKNOWN';
|
||||
|
||||
export type Metrics = {
|
||||
TOTAL: number;
|
||||
SUCCEEDED: number;
|
||||
FAILED: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type ImageModel = Images.ImageModel;
|
||||
|
||||
export type ImageGenerateParams = Images.ImageGenerateParams;
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace Images {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export type ImageModel =
|
||||
| (string & NonNullable<unknown>)
|
||||
// 通义万相
|
||||
| 'wanx-v1'
|
||||
// Stable Diffusion
|
||||
| 'stable-diffusion-v1.5'
|
||||
| 'stable-diffusion-xl';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export interface ImageGenerateParams {
|
||||
/**
|
||||
* The model to use for image generation.
|
||||
*
|
||||
* @defaultValue wanx-v1
|
||||
*/
|
||||
model?: ImageModel | null;
|
||||
|
||||
/**
|
||||
* A prompt is the text input that guides the AI in generating visual content.
|
||||
* It defines the textual description or concept for the image you wish to generate.
|
||||
* Think of it as the creative vision you want the AI to bring to life.
|
||||
* Crafting clear and creative prompts is crucial for achieving the desired results with Imagine's API.
|
||||
* For example, A serene forest with a river under the moonlight, can be a prompt.
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* The negative_prompt parameter empowers you to provide additional
|
||||
* guidance to the AI by specifying what you don't want in the image.
|
||||
* It helps refine the creative direction, ensuring that the generated
|
||||
* content aligns with your intentions.
|
||||
*/
|
||||
negative_prompt?: string | null;
|
||||
|
||||
/**
|
||||
* The size of the generated images.
|
||||
*
|
||||
* @defaultValue 1024*1024
|
||||
*/
|
||||
size?: (string & NonNullable<unknown>) | '1024*1024' | null;
|
||||
|
||||
/**
|
||||
* The style of the generated images.
|
||||
*
|
||||
* - \<photography\> 摄影
|
||||
* - \<portrait\> 人像写真
|
||||
* - \<3d cartoon\> 3D卡通
|
||||
* - \<anime\> 动画
|
||||
* - \<oil painting\> 油画
|
||||
* - \<watercolor\>水彩
|
||||
* - \<sketch\> 素描
|
||||
* - \<chinese painting\> 中国画
|
||||
* - \<flat illustration\> 扁平插画
|
||||
* - \<auto\> 默认
|
||||
*
|
||||
* 仅 wanx-v1 模型支持
|
||||
*
|
||||
* @defaultValue <auto>
|
||||
*/
|
||||
style?:
|
||||
| '<photography>'
|
||||
| '<portrait>'
|
||||
| '<3d cartoon>'
|
||||
| '<anime>'
|
||||
| '<oil painting>'
|
||||
| '<watercolor>'
|
||||
| '<sketch>'
|
||||
| '<chinese painting>'
|
||||
| '<flat illustration>'
|
||||
| '<auto>'
|
||||
| null;
|
||||
|
||||
/**
|
||||
* The number of images to generate. Must be between 1 and 4.
|
||||
*
|
||||
* @defaultValue 1
|
||||
*/
|
||||
n?: number | null;
|
||||
|
||||
/**
|
||||
* The steps parameter defines the number of operations or iterations that the
|
||||
* generator will perform during image creation. It can impact the complexity
|
||||
* and detail of the generated image.
|
||||
*
|
||||
* Range: 30-50
|
||||
*
|
||||
* 仅 StableDiffusion 模型支持
|
||||
*
|
||||
* @defaultValue 40
|
||||
*/
|
||||
steps?: number | null;
|
||||
|
||||
/**
|
||||
* The cfg parameter acts as a creative control knob.
|
||||
* You can adjust it to fine-tune the level of artistic innovation in the image.
|
||||
* Lower values encourage faithful execution of the prompt,
|
||||
* while higher values introduce more creative and imaginative variations.
|
||||
*
|
||||
* Range: 1 - 15
|
||||
*
|
||||
* @defaultValue 10
|
||||
*/
|
||||
cfg?: number | null;
|
||||
|
||||
/**
|
||||
* The seed parameter serves as the initial value for the random number generator.
|
||||
* By setting a specific seed value, you can ensure that the AI generates the same
|
||||
* image or outcome each time you use that exact seed.
|
||||
*
|
||||
* range: 1-Infinity
|
||||
*/
|
||||
seed?: number | null;
|
||||
|
||||
/**
|
||||
* The format in which the generated images are returned.
|
||||
*/
|
||||
response_format?: 'url' | null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './chat/index';
|
||||
export * from './completions';
|
||||
export { Images, type ImageModel, type ImageGenerateParams } from './images';
|
||||
export {
|
||||
Embeddings,
|
||||
type EmbeddingModel,
|
||||
type EmbeddingCreateParams,
|
||||
} from './embeddings';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { APIClient } from 'openai/core';
|
||||
|
||||
export class APIResource<Client extends APIClient = APIClient> {
|
||||
protected _client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this._client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface SparkAIOptions {
|
||||
baseURL?: string;
|
||||
appId?: string;
|
||||
apiKey?: string;
|
||||
apiSecret?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: unknown;
|
||||
fetch?: Fetch | undefined;
|
||||
/**
|
||||
* Default headers to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* header to `undefined` or `null` in request options.
|
||||
*/
|
||||
defaultHeaders?: Headers;
|
||||
|
||||
/**
|
||||
* Default query parameters to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* param to `undefined` in request options.
|
||||
*/
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class SparkAI extends APIClient {
|
||||
appId: string;
|
||||
|
||||
protected apiKey: string;
|
||||
|
||||
protected apiSecret: string;
|
||||
|
||||
private _options: SparkAIOptions;
|
||||
|
||||
constructor(options: SparkAIOptions = {}) {
|
||||
const {
|
||||
appId = process.env.SPARK_APP_ID || '',
|
||||
apiKey = process.env.SPARK_API_KEY || '',
|
||||
apiSecret = process.env.SPARK_API_SECRET || '',
|
||||
baseURL = 'https://spark-api.xf-yun.com',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.appId = appId;
|
||||
this.apiKey = apiKey;
|
||||
this.apiSecret = apiSecret;
|
||||
}
|
||||
|
||||
chat = new API.Chat(this);
|
||||
|
||||
images = new API.Images(this);
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url - 需要签名的 URL
|
||||
* @param method - HTTP method
|
||||
* @returns 签名后的 URL
|
||||
*/
|
||||
generateAuthorizationURL(url: string | URL, method: string = 'GET'): string {
|
||||
const target = new URL(url, this.baseURL);
|
||||
|
||||
const date = new Date().toUTCString();
|
||||
|
||||
const authorization = this.generateAuthorization({
|
||||
method,
|
||||
path: target.pathname,
|
||||
host: target.host,
|
||||
date,
|
||||
});
|
||||
|
||||
target.searchParams.set('authorization', authorization);
|
||||
target.searchParams.set('host', target.host);
|
||||
target.searchParams.set('date', date);
|
||||
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成鉴权信息
|
||||
*
|
||||
* See https://www.xfyun.cn/doc/spark/general_url_authentication.html
|
||||
*/
|
||||
generateAuthorization({
|
||||
method,
|
||||
host,
|
||||
path,
|
||||
date,
|
||||
}: {
|
||||
method: string;
|
||||
host: string;
|
||||
path: string;
|
||||
date: string;
|
||||
}) {
|
||||
// 生成签名原文
|
||||
const rawSignature = `host: ${host}\ndate: ${date}\n${method} ${path} HTTP/1.1`;
|
||||
|
||||
// 生成签名,需要转为 base64 编码
|
||||
const signature = this.hash(rawSignature);
|
||||
|
||||
return btoa(
|
||||
`api_key="${this.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`,
|
||||
);
|
||||
}
|
||||
|
||||
protected hash(data: string) {
|
||||
const sha256Hmac = createHmac('sha256', this.apiSecret);
|
||||
sha256Hmac.update(data);
|
||||
return sha256Hmac.digest('base64');
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace SparkAI {
|
||||
export type Chat = API.Chat;
|
||||
export type ChatModel = API.ChatModel;
|
||||
export type ChatCompletionCreateParams = API.ChatCompletionCreateParams;
|
||||
export type ChatCompletionCreateParamsNonStreaming =
|
||||
API.ChatCompletionCreateParamsNonStreaming;
|
||||
export type ChatCompletionCreateParamsStreaming =
|
||||
API.ChatCompletionCreateParamsStreaming;
|
||||
}
|
||||
|
||||
export default SparkAI;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { SparkAI } from './index';
|
||||
|
||||
export class APIResource {
|
||||
protected _client: SparkAI;
|
||||
|
||||
constructor(client: SparkAI) {
|
||||
this._client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { APIResource } from '../../resource';
|
||||
import { Completions } from './completions';
|
||||
|
||||
export class Chat extends APIResource {
|
||||
completions = new Completions(this._client);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import OpenAI, { APIError } from 'openai';
|
||||
import { RequestOptions } from 'openai/core';
|
||||
import { Stream } from 'openai/streaming';
|
||||
|
||||
import { APIResource } from '../../resource';
|
||||
|
||||
export class Completions extends APIResource {
|
||||
protected resources: Record<
|
||||
ChatModel,
|
||||
{
|
||||
domain: string;
|
||||
url: string;
|
||||
}
|
||||
> = {
|
||||
'spark-1.5': {
|
||||
domain: 'general',
|
||||
url: 'wss://spark-api.xf-yun.com/v1.1/chat',
|
||||
},
|
||||
'spark-2': {
|
||||
domain: 'generalv2',
|
||||
url: 'wss://spark-api.xf-yun.com/v2.1/chat',
|
||||
},
|
||||
'spark-3': {
|
||||
domain: 'generalv3',
|
||||
url: 'wss://spark-api.xf-yun.com/v3.1/chat',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a model response for the given chat conversation.
|
||||
*
|
||||
* See https://help.aliyun.com/zh/dashscope/developer-reference/api-details
|
||||
*/
|
||||
create(
|
||||
body: ChatCompletionCreateParamsNonStreaming,
|
||||
options?: RequestOptions,
|
||||
): Promise<OpenAI.ChatCompletion>;
|
||||
|
||||
create(
|
||||
body: ChatCompletionCreateParamsStreaming,
|
||||
options?: RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk>>;
|
||||
|
||||
async create(
|
||||
params: ChatCompletionCreateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<Stream<OpenAI.ChatCompletionChunk> | OpenAI.ChatCompletion> {
|
||||
const { model, messages, functions, user, ...rest } = params;
|
||||
|
||||
const resource = this.resources[model];
|
||||
|
||||
const url = this._client.generateAuthorizationURL(resource.url, 'GET');
|
||||
|
||||
const body: ChatCompletions.ChatCompletionParameters = {
|
||||
header: {
|
||||
app_id: this._client.appId,
|
||||
},
|
||||
parameter: {
|
||||
chat: {
|
||||
...rest,
|
||||
domain: resource.domain,
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
message: {
|
||||
text: messages,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (functions) {
|
||||
body.payload.functions = { text: functions };
|
||||
}
|
||||
|
||||
if (user) {
|
||||
body.header.uid = user;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
controller.abort();
|
||||
});
|
||||
}
|
||||
|
||||
const ws: WebSocket = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify(body));
|
||||
};
|
||||
|
||||
if (params.stream) {
|
||||
const readableStream = new ReadableStream({
|
||||
pull(ctrl) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data: ChatCompletions.ChatCompletionResponse = JSON.parse(
|
||||
event.data,
|
||||
);
|
||||
|
||||
const { header, payload } = data;
|
||||
|
||||
if (header.code !== 0) {
|
||||
ctrl.error(
|
||||
new APIError(undefined, data.header, undefined, undefined),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const choices = payload.choices.text;
|
||||
|
||||
const [message] = choices;
|
||||
|
||||
const choice: OpenAI.ChatCompletionChunk.Choice = {
|
||||
index: 0,
|
||||
delta: {
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
},
|
||||
finish_reason: null,
|
||||
};
|
||||
|
||||
if (header.status === 2) {
|
||||
choice.finish_reason = 'stop';
|
||||
}
|
||||
|
||||
if (message.function_call) {
|
||||
choice.delta.function_call = message.function_call;
|
||||
}
|
||||
|
||||
const completion: OpenAI.ChatCompletionChunk = {
|
||||
id: header.sid,
|
||||
model,
|
||||
choices: [choice],
|
||||
object: 'chat.completion.chunk',
|
||||
created: Date.now() / 1000,
|
||||
};
|
||||
|
||||
ctrl.enqueue(encoder.encode(`${JSON.stringify(completion)}\n`));
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
ctrl.error(error);
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
ws.close();
|
||||
},
|
||||
});
|
||||
|
||||
controller.signal.addEventListener('abort', () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
return Stream.fromReadableStream(readableStream, controller);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data: ChatCompletions.ChatCompletionResponse = JSON.parse(
|
||||
event.data,
|
||||
);
|
||||
|
||||
const { header, payload } = data;
|
||||
|
||||
// 2 代表完成
|
||||
if (header.status !== 2) return;
|
||||
|
||||
const usage = payload.usage.text;
|
||||
const choices = payload.choices.text;
|
||||
|
||||
const [message] = choices;
|
||||
|
||||
const choice: OpenAI.ChatCompletion.Choice = {
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: message.content,
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop',
|
||||
};
|
||||
|
||||
const completion: OpenAI.ChatCompletion = {
|
||||
id: header.sid,
|
||||
object: 'chat.completion',
|
||||
created: Date.now() / 1000,
|
||||
model,
|
||||
choices: [choice],
|
||||
usage: {
|
||||
completion_tokens: usage.completion_tokens,
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: usage.prompt_tokens,
|
||||
},
|
||||
};
|
||||
|
||||
resolve(completion);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsNonStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsNonStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number | null;
|
||||
chat_id?: string | null;
|
||||
}
|
||||
|
||||
export interface ChatCompletionCreateParamsStreaming
|
||||
extends OpenAI.ChatCompletionCreateParamsStreaming {
|
||||
model: ChatModel;
|
||||
top_k?: number | null;
|
||||
chat_id?: string | null;
|
||||
}
|
||||
|
||||
export type ChatCompletionCreateParams =
|
||||
| ChatCompletionCreateParamsNonStreaming
|
||||
| ChatCompletionCreateParamsStreaming;
|
||||
|
||||
export type ChatModel = 'spark-1.5' | 'spark-2' | 'spark-3';
|
||||
|
||||
export namespace ChatCompletions {
|
||||
export type ChatCompletionParameters = {
|
||||
header: {
|
||||
app_id: string;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
parameter: {
|
||||
chat: {
|
||||
domain: string;
|
||||
temperature?: number | null;
|
||||
max_tokens?: number | null;
|
||||
top_k?: number | null;
|
||||
chat_id?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
payload: {
|
||||
message: {
|
||||
text: OpenAI.ChatCompletionMessageParam[];
|
||||
};
|
||||
|
||||
functions?: {
|
||||
text: OpenAI.ChatCompletionCreateParams.Function[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type ChatCompletionResponse = {
|
||||
header: {
|
||||
code: number;
|
||||
message: string;
|
||||
sid: string;
|
||||
status: number;
|
||||
};
|
||||
payload: {
|
||||
choices: {
|
||||
status: number;
|
||||
seq: number;
|
||||
text: OpenAI.ChatCompletionMessage[];
|
||||
};
|
||||
usage: {
|
||||
text: {
|
||||
question_tokens: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Chat } from './chat';
|
||||
export {
|
||||
type ChatModel,
|
||||
type ChatCompletionCreateParams,
|
||||
type ChatCompletionCreateParamsNonStreaming,
|
||||
type ChatCompletionCreateParamsStreaming,
|
||||
Completions,
|
||||
} from './completions';
|
||||
@@ -0,0 +1,117 @@
|
||||
import OpenAI, { APIError } from 'openai';
|
||||
import { type RequestOptions } from 'openai/core';
|
||||
|
||||
import { APIResource } from '../resource';
|
||||
|
||||
// TODO: 没有权限,暂未测试
|
||||
export class Images extends APIResource {
|
||||
/**
|
||||
* See https://www.xfyun.cn/doc/spark/ImageGeneration.html
|
||||
*/
|
||||
async generate(
|
||||
params: OpenAI.ImageGenerateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<OpenAI.ImagesResponse> {
|
||||
const { prompt, user } = params;
|
||||
|
||||
const body: ImagesAPI.ImageGenerateParams = {
|
||||
header: {
|
||||
app_id: this._client.appId,
|
||||
uid: user,
|
||||
},
|
||||
parameter: {
|
||||
chat: {
|
||||
max_tokens: 4096,
|
||||
domain: 'general',
|
||||
temperature: 0.5,
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
message: {
|
||||
text: [{ role: 'user', content: prompt }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const url = this._client.generateAuthorizationURL(
|
||||
'https://spark-api.cn-huabei-1.xf-yun.com/v2.1/tti',
|
||||
'POST',
|
||||
);
|
||||
|
||||
const response: Response = await this._client.post(url, {
|
||||
...options,
|
||||
body,
|
||||
__binaryResponse: true,
|
||||
});
|
||||
|
||||
const resp: ImagesAPI.ImageGenerateResponse = await response.json();
|
||||
|
||||
if (resp.header.code > 0) {
|
||||
throw new APIError(undefined, resp.header, undefined, undefined);
|
||||
}
|
||||
|
||||
return {
|
||||
created: Date.now() / 1000,
|
||||
data: [
|
||||
{
|
||||
// base64 encoded image
|
||||
url: resp.payload.choices.text[0].content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace ImagesAPI {
|
||||
export type ImageGenerateMessageParam = {
|
||||
role: 'user';
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ImageGenerateParams = {
|
||||
header: {
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
app_id: string;
|
||||
/**
|
||||
* 用户唯一标识
|
||||
*/
|
||||
uid?: string;
|
||||
};
|
||||
parameter: {
|
||||
chat: {
|
||||
max_tokens: number;
|
||||
domain: string;
|
||||
temperature: number;
|
||||
};
|
||||
};
|
||||
payload: {
|
||||
message: {
|
||||
text: ImageGenerateMessageParam[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type ImageGenerateAssistantMessage = {
|
||||
index: 0;
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ImageGenerateResponse = {
|
||||
header: {
|
||||
code: number;
|
||||
message: string;
|
||||
sid: string;
|
||||
status: number;
|
||||
};
|
||||
payload: {
|
||||
choices: {
|
||||
status: number;
|
||||
seq: number;
|
||||
text: ImageGenerateAssistantMessage[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './chat/index';
|
||||
|
||||
export { Images } from './images';
|
||||
@@ -0,0 +1,262 @@
|
||||
import { OpenAIError } from 'openai';
|
||||
|
||||
export type Bytes =
|
||||
| string
|
||||
| ArrayBuffer
|
||||
| Uint8Array
|
||||
| Buffer
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type ServerSentEvent = {
|
||||
event: string | null;
|
||||
data: string;
|
||||
raw: string[];
|
||||
};
|
||||
|
||||
export async function* iterMessages(
|
||||
response: Response,
|
||||
decoder: SSEDecoder,
|
||||
controller: AbortController,
|
||||
): AsyncGenerator<ServerSentEvent, void, unknown> {
|
||||
if (!response.body) {
|
||||
controller.abort();
|
||||
throw new OpenAIError(`Attempted to iterate over a response with no body`);
|
||||
}
|
||||
|
||||
const lineDecoder = new LineDecoder();
|
||||
|
||||
const iter = readableStreamAsyncIterable<Bytes>(response.body);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for await (const chunk of iter) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const line of lineDecoder.decode(chunk)) {
|
||||
const sse = decoder.decode(line);
|
||||
if (sse) yield sse;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const line of lineDecoder.flush()) {
|
||||
const sse = decoder.decode(line);
|
||||
if (sse) yield sse;
|
||||
}
|
||||
}
|
||||
|
||||
export class SSEDecoder {
|
||||
private data: string[];
|
||||
|
||||
private event: string | null;
|
||||
|
||||
private chunks: string[];
|
||||
|
||||
constructor() {
|
||||
this.event = null;
|
||||
this.data = [];
|
||||
this.chunks = [];
|
||||
}
|
||||
|
||||
decode(line: string) {
|
||||
if (line.endsWith('\r')) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
line = line.substring(0, line.length - 1);
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
// empty line and we didn't previously encounter any messages
|
||||
if (!this.event && !this.data.length) return null;
|
||||
|
||||
const sse: ServerSentEvent = {
|
||||
event: this.event,
|
||||
data: this.data.join('\n'),
|
||||
raw: this.chunks,
|
||||
};
|
||||
|
||||
this.event = null;
|
||||
this.data = [];
|
||||
this.chunks = [];
|
||||
|
||||
return sse;
|
||||
}
|
||||
|
||||
this.chunks.push(line);
|
||||
|
||||
if (line.startsWith(':')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, prefer-const
|
||||
let [fieldname, _, value] = partition(line, ':');
|
||||
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.substring(1);
|
||||
}
|
||||
|
||||
if (fieldname === 'event') {
|
||||
this.event = value;
|
||||
} else if (fieldname === 'data') {
|
||||
this.data.push(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
|
||||
* reading lines from text.
|
||||
*
|
||||
* https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
|
||||
*/
|
||||
export class LineDecoder {
|
||||
// prettier-ignore
|
||||
static NEWLINE_CHARS = new Set(['\n', '\r', '\x0b', '\x0c', '\x1c', '\x1d', '\x1e', '\x85', '\u2028', '\u2029']);
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
static NEWLINE_REGEXP = /\r\n|[\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029]/g;
|
||||
|
||||
buffer: string[];
|
||||
|
||||
trailingCR: boolean;
|
||||
|
||||
textDecoder: any; // TextDecoder found in browsers; not typed to avoid pulling in either "dom" or "node" types.
|
||||
|
||||
constructor() {
|
||||
this.buffer = [];
|
||||
this.trailingCR = false;
|
||||
}
|
||||
|
||||
decode(chunk: Bytes): string[] {
|
||||
let text = this.decodeText(chunk);
|
||||
|
||||
if (this.trailingCR) {
|
||||
text = `\r${text}`;
|
||||
this.trailingCR = false;
|
||||
}
|
||||
if (text.endsWith('\r')) {
|
||||
this.trailingCR = true;
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const trailingNewline = LineDecoder.NEWLINE_CHARS.has(
|
||||
text[text.length - 1] || '',
|
||||
);
|
||||
let lines = text.split(LineDecoder.NEWLINE_REGEXP);
|
||||
|
||||
if (lines.length === 1 && !trailingNewline) {
|
||||
this.buffer.push(lines[0]!);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.buffer.length > 0) {
|
||||
lines = [this.buffer.join('') + lines[0], ...lines.slice(1)];
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
if (!trailingNewline) {
|
||||
this.buffer = [lines.pop() || ''];
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
decodeText(bytes: Bytes): string {
|
||||
if (bytes == null) return '';
|
||||
if (typeof bytes === 'string') return bytes;
|
||||
|
||||
// Node:
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
if (bytes instanceof Buffer) {
|
||||
return bytes.toString();
|
||||
}
|
||||
if (bytes instanceof Uint8Array) {
|
||||
return Buffer.from(bytes).toString();
|
||||
}
|
||||
|
||||
throw new OpenAIError(
|
||||
`Unexpected: received non-Uint8Array (${bytes.constructor.name}) stream chunk in an environment with a global "Buffer" defined, which this library assumes to be Node. Please report this error.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Browser
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
if (bytes instanceof Uint8Array || bytes instanceof ArrayBuffer) {
|
||||
this.textDecoder ??= new TextDecoder('utf8');
|
||||
return this.textDecoder.decode(bytes);
|
||||
}
|
||||
|
||||
throw new OpenAIError(
|
||||
`Unexpected: received non-Uint8Array/ArrayBuffer (${
|
||||
(bytes as any).constructor.name
|
||||
}) in a web platform. Please report this error.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new OpenAIError(
|
||||
`Unexpected: neither Buffer nor TextDecoder are available as globals. Please report this error.`,
|
||||
);
|
||||
}
|
||||
|
||||
flush(): string[] {
|
||||
if (!this.buffer.length && !this.trailingCR) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = [this.buffer.join('')];
|
||||
this.buffer = [];
|
||||
this.trailingCR = false;
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
function partition(str: string, delimiter: string): [string, string, string] {
|
||||
const index = str.indexOf(delimiter);
|
||||
if (index !== -1) {
|
||||
return [
|
||||
str.substring(0, index),
|
||||
delimiter,
|
||||
str.substring(index + delimiter.length),
|
||||
];
|
||||
}
|
||||
|
||||
return [str, '', ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Most browsers don't yet have async iterable support for ReadableStream,
|
||||
* and Node has a very different way of reading bytes from its "ReadableStream".
|
||||
*
|
||||
* This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
|
||||
*/
|
||||
export function readableStreamAsyncIterable<T>(
|
||||
stream: any,
|
||||
): AsyncIterableIterator<T> {
|
||||
if (stream[Symbol.asyncIterator]) return stream;
|
||||
|
||||
const reader = stream.getReader();
|
||||
return {
|
||||
async next() {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
|
||||
return result;
|
||||
} catch (e) {
|
||||
reader.releaseLock(); // release lock when stream becomes errored
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async return() {
|
||||
const cancelPromise = reader.cancel();
|
||||
reader.releaseLock();
|
||||
await cancelPromise;
|
||||
return { done: true, value: undefined };
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const castToError = (err: any): Error => {
|
||||
if (err instanceof Error) return err;
|
||||
return new Error(err);
|
||||
};
|
||||
|
||||
export function ensureArray<T>(value: T | T[]): T[] {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
export function random(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Agent } from 'node:http';
|
||||
|
||||
import {
|
||||
APIClient,
|
||||
type DefaultQuery,
|
||||
type Fetch,
|
||||
type Headers,
|
||||
} from 'openai/core';
|
||||
|
||||
import * as API from './resources';
|
||||
|
||||
export interface VYroAIOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
timeout?: number | undefined;
|
||||
httpAgent?: Agent;
|
||||
apiType?: (string & NonNullable<unknown>) | 'api';
|
||||
fetch?: Fetch | undefined;
|
||||
defaultHeaders?: Headers;
|
||||
defaultQuery?: DefaultQuery;
|
||||
}
|
||||
|
||||
export class VYroAI extends APIClient {
|
||||
public apiType: (string & NonNullable<unknown>) | 'api';
|
||||
|
||||
protected apiKey: string;
|
||||
|
||||
private _options: VYroAIOptions;
|
||||
|
||||
constructor(options: VYroAIOptions = {}) {
|
||||
const {
|
||||
apiKey = process.env.VYRO_API_KEY || '',
|
||||
apiType = process.env.VYRO_API_TYPE || 'api',
|
||||
baseURL = 'https://api.vyro.ai/v1',
|
||||
timeout = 30000,
|
||||
httpAgent = undefined,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
super({
|
||||
baseURL,
|
||||
timeout,
|
||||
fetch,
|
||||
httpAgent,
|
||||
...rest,
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
|
||||
this.apiKey = apiKey;
|
||||
this.apiType = apiType;
|
||||
}
|
||||
|
||||
images = new API.Images(this);
|
||||
|
||||
protected override authHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultHeaders(): Headers {
|
||||
return {
|
||||
...this.authHeaders(),
|
||||
...this._options.defaultHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
protected override defaultQuery(): DefaultQuery | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export namespace VYroAI {
|
||||
export type Images = API.Images;
|
||||
export type ImageGenerateParams = API.ImageGenerateParams;
|
||||
}
|
||||
|
||||
export default VYroAI;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { VYroAI } from './index';
|
||||
|
||||
export class APIResource {
|
||||
protected _client: VYroAI;
|
||||
|
||||
constructor(client: VYroAI) {
|
||||
this._client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
import { ReadableStream } from 'node:stream/web';
|
||||
|
||||
import { type RequestOptions, type Uploadable } from 'openai/core';
|
||||
import { toFile } from 'openai/uploads';
|
||||
|
||||
import { random } from '../../util';
|
||||
import { APIResource } from '../resource';
|
||||
|
||||
export class Images extends APIResource {
|
||||
protected models: Record<ImageModel, number> = {
|
||||
'imagine-v5': 33,
|
||||
'anime-v5': 34,
|
||||
'imagine-v4.1': 32,
|
||||
'imagine-v4': 31,
|
||||
'imagine-v3': 30,
|
||||
'imagine-v1': 28,
|
||||
realistic: 29,
|
||||
anime: 21,
|
||||
portrait: 26,
|
||||
'sdxl-1.0': 122,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a variation of a given image.
|
||||
*/
|
||||
async createVariation(
|
||||
params: ImageCreateVariationParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const { model, style = this.models[model ?? 'realistic'] } = params;
|
||||
|
||||
// @ts-expect-error
|
||||
formData.append('image', await toFile(params.image));
|
||||
formData.append('style_id', (style || 29).toString());
|
||||
formData.append('prompt', params.prompt);
|
||||
formData.append('negative_prompt', params.negative_prompt || '');
|
||||
formData.append('strength', (params.strength || 0).toString());
|
||||
formData.append('steps', (params.steps || 30).toString());
|
||||
formData.append('cfg', (params.cfg || 7.5).toString());
|
||||
formData.append('seed', (params.seed || random(1, 1000000)).toString());
|
||||
|
||||
const response: Response = await client.post(
|
||||
`/imagine/${client.apiType}/generations/variations`,
|
||||
{
|
||||
...options,
|
||||
body: {
|
||||
body: formData,
|
||||
[Symbol.toStringTag]: 'MultipartBody',
|
||||
},
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
binary: response.body as unknown as ReadableStream,
|
||||
},
|
||||
],
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Experience the magic of Imagine's Image Remix feature, designed to breathe new life into your existing images.
|
||||
*/
|
||||
async edit(
|
||||
params: ImageEditParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const { model, style = this.models[model ?? 'realistic'] } = params;
|
||||
|
||||
// @ts-expect-error
|
||||
formData.append('image', await toFile(params.image));
|
||||
formData.append('style_id', (style || 29).toString());
|
||||
formData.append('prompt', params.prompt);
|
||||
formData.append('negative_prompt', params.negative_prompt || '');
|
||||
formData.append('strength', (params.strength || 0).toString());
|
||||
formData.append('control', params.control || 'openpose');
|
||||
formData.append('steps', (params.steps || 30).toString());
|
||||
formData.append('cfg', (params.cfg || 7.5).toString());
|
||||
formData.append('seed', (params.seed || random(1, 1000000)).toString());
|
||||
|
||||
const response: Response = await client.post(
|
||||
`/imagine/${client.apiType}/edits/remix`,
|
||||
{
|
||||
...options,
|
||||
body: {
|
||||
body: formData,
|
||||
[Symbol.toStringTag]: 'MultipartBody',
|
||||
},
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
binary: response.body as unknown as ReadableStream,
|
||||
},
|
||||
],
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an image given a prompt.
|
||||
*/
|
||||
async generate(
|
||||
params: ImageGenerateParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const { model, style = this.models[model ?? 'imagine-v4'] } = params;
|
||||
|
||||
formData.append('style_id', (style || 30).toString());
|
||||
formData.append('prompt', params.prompt);
|
||||
formData.append('negative_prompt', params.negative_prompt || '');
|
||||
formData.append('aspect_ratio', params.aspect_ratio || '1:1');
|
||||
formData.append('steps', (params.steps || 30).toString());
|
||||
formData.append('cfg', (params.cfg || 7.5).toString());
|
||||
formData.append('seed', (params.seed || random(1, 1000000)).toString());
|
||||
formData.append('high_res_results', params.quality === 'hd' ? '1' : '0');
|
||||
|
||||
const response: Response = await client.post(
|
||||
`/imagine/${client.apiType}/generations`,
|
||||
{
|
||||
...options,
|
||||
body: {
|
||||
body: formData,
|
||||
[Symbol.toStringTag]: 'MultipartBody',
|
||||
},
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [
|
||||
{
|
||||
binary: response.body as unknown as ReadableStream,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The image upscale feature provides a better image to the user by increasing its resolution.
|
||||
*/
|
||||
async upscale(
|
||||
params: ImageUpscaleParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// @ts-expect-error
|
||||
formData.append('image', await toFile(params.image));
|
||||
|
||||
const response: Response = await client.post(
|
||||
`/imagine/${client.apiType}/upscale`,
|
||||
{
|
||||
...options,
|
||||
body: {
|
||||
body: formData,
|
||||
[Symbol.toStringTag]: 'MultipartBody',
|
||||
},
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [
|
||||
{
|
||||
binary: response.body as unknown as ReadableStream,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inpaint is an advanced feature of the Text-to-Image Stable Diffusion pipeline.
|
||||
* It allows users to remove unwanted objects or elements from an image by intelligently filling in the missing areas.
|
||||
*/
|
||||
async restoration(
|
||||
params: ImageRestorationParams,
|
||||
options?: RequestOptions,
|
||||
): Promise<ImagesResponse> {
|
||||
const client = this._client;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// @ts-expect-error
|
||||
formData.append('image', await toFile(params.image));
|
||||
// @ts-expect-error
|
||||
formData.append('mask', await toFile(params.mask));
|
||||
formData.append('style_id', '1');
|
||||
formData.append('prompt', params.prompt);
|
||||
formData.append('neg_prompt', params.negative_prompt || '');
|
||||
formData.append('inpaint_strength', (params.strength || 0).toString());
|
||||
formData.append('cfg', (params.cfg || 7.5).toString());
|
||||
|
||||
const response: Response = await client.post(
|
||||
`/imagine/${client.apiType}/generations/variations`,
|
||||
{
|
||||
...options,
|
||||
body: {
|
||||
body: formData,
|
||||
[Symbol.toStringTag]: 'MultipartBody',
|
||||
},
|
||||
__binaryResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
binary: response.body as unknown as ReadableStream,
|
||||
},
|
||||
],
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type ImageModel =
|
||||
| 'imagine-v5'
|
||||
| 'anime-v5'
|
||||
| 'imagine-v4.1'
|
||||
| 'imagine-v4'
|
||||
| 'imagine-v3'
|
||||
| 'imagine-v1'
|
||||
| 'realistic'
|
||||
| 'anime'
|
||||
| 'portrait'
|
||||
| 'sdxl-1.0';
|
||||
|
||||
export interface ImageRestorationParams {
|
||||
/**
|
||||
* The image to use as the basis for the variation(s). Must be a valid PNG file,
|
||||
* less than 4MB, and square.
|
||||
*/
|
||||
image: Uploadable;
|
||||
|
||||
/**
|
||||
* The mask indicating the areas to be inpainted.
|
||||
*/
|
||||
mask: Uploadable;
|
||||
|
||||
/**
|
||||
* The text guides the image generation.
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* The model to use for image generation.
|
||||
*/
|
||||
model?: 'vyro-inpaint' | null;
|
||||
|
||||
/**
|
||||
* The negative_prompt parameter empowers you to provide additional
|
||||
* guidance to the AI by specifying what you don't want in the image.
|
||||
* It helps refine the creative direction, ensuring that the generated
|
||||
* content aligns with your intentions.
|
||||
*/
|
||||
negative_prompt?: string | null;
|
||||
|
||||
/**
|
||||
* Specifies the model to be used. Currently supports only 1 for realism.
|
||||
*
|
||||
* @defaultValue 1
|
||||
*/
|
||||
style?: 1 | null;
|
||||
|
||||
/**
|
||||
* Weightage to be given to text
|
||||
*
|
||||
* Range: 3 - 15
|
||||
*
|
||||
* @defaultValue 7.5
|
||||
*/
|
||||
cfg?: number | null;
|
||||
|
||||
/**
|
||||
* Weightage given to initial image. Greater this parameter more the output will be close to starting image and far from prompt.
|
||||
*
|
||||
* Range: 0 - 1
|
||||
*
|
||||
* @defaultValue 0.5
|
||||
*/
|
||||
strength?: number | null;
|
||||
|
||||
/**
|
||||
* 目前仅支持 binary 格式
|
||||
*/
|
||||
response_format?: 'binary' | null;
|
||||
}
|
||||
|
||||
export interface ImageCreateVariationParams {
|
||||
/**
|
||||
* The image to use as the basis for the variation(s). Must be a valid PNG file,
|
||||
* less than 4MB, and square.
|
||||
*/
|
||||
image: Uploadable;
|
||||
|
||||
/**
|
||||
* The text guides the image generation.
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* The model to use for image generation.
|
||||
*/
|
||||
model?: ImageModel | null;
|
||||
|
||||
/**
|
||||
* The negative_prompt parameter empowers you to provide additional
|
||||
* guidance to the AI by specifying what you don't want in the image.
|
||||
* It helps refine the creative direction, ensuring that the generated
|
||||
* content aligns with your intentions.
|
||||
*/
|
||||
negative_prompt?: string | null;
|
||||
|
||||
/**
|
||||
* The style_id parameter is like choosing an artistic palette for your image.
|
||||
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
|
||||
* Style IDs range from 1 to N, each representing a unique artistic style.
|
||||
*
|
||||
* @defaultValue 30
|
||||
*/
|
||||
style?: number | null;
|
||||
|
||||
/**
|
||||
* The steps parameter defines the number of operations or iterations that the
|
||||
* generator will perform during image creation. It can impact the complexity
|
||||
* and detail of the generated image.
|
||||
*
|
||||
* Range: 30-50
|
||||
*
|
||||
* @defaultValue 30
|
||||
*/
|
||||
steps?: number | null;
|
||||
|
||||
/**
|
||||
* The cfg parameter acts as a creative control knob.
|
||||
* You can adjust it to fine-tune the level of artistic innovation in the image.
|
||||
* Lower values encourage faithful execution of the prompt,
|
||||
* while higher values introduce more creative and imaginative variations.
|
||||
*
|
||||
* Range: 3 - 15
|
||||
*
|
||||
* @defaultValue 7.5
|
||||
*/
|
||||
cfg?: number | null;
|
||||
|
||||
/**
|
||||
* The seed parameter serves as the initial value for the random number generator.
|
||||
* By setting a specific seed value, you can ensure that the AI generates the same
|
||||
* image or outcome each time you use that exact seed.
|
||||
*
|
||||
* range: 1-Infinity
|
||||
*/
|
||||
seed?: number | null;
|
||||
|
||||
/**
|
||||
* Influences the impact of the control image on output.
|
||||
*
|
||||
* Range: 0 - 1
|
||||
*
|
||||
* @defaultValue 0
|
||||
*/
|
||||
strength?: number | null;
|
||||
|
||||
/**
|
||||
* 目前仅支持 binary 格式
|
||||
*/
|
||||
response_format?: 'binary' | null;
|
||||
}
|
||||
|
||||
export interface ImageEditParams {
|
||||
/**
|
||||
* The image to use as the basis for the variation(s). Must be a valid PNG file,
|
||||
* less than 4MB, and square.
|
||||
*/
|
||||
image: Uploadable;
|
||||
|
||||
/**
|
||||
* The text guides the image generation.
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* The model to use for image generation.
|
||||
*/
|
||||
model?: ImageModel | null;
|
||||
|
||||
/**
|
||||
* The negative_prompt parameter empowers you to provide additional
|
||||
* guidance to the AI by specifying what you don't want in the image.
|
||||
* It helps refine the creative direction, ensuring that the generated
|
||||
* content aligns with your intentions.
|
||||
*/
|
||||
negative_prompt?: string | null;
|
||||
|
||||
/**
|
||||
* The style_id parameter is like choosing an artistic palette for your image.
|
||||
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
|
||||
* Style IDs range from 1 to N, each representing a unique artistic style.
|
||||
*
|
||||
* @defaultValue 29
|
||||
*/
|
||||
style?: number | null;
|
||||
|
||||
/**
|
||||
* The steps parameter defines the number of operations or iterations that the
|
||||
* generator will perform during image creation. It can impact the complexity
|
||||
* and detail of the generated image.
|
||||
*
|
||||
* Range: 30-50
|
||||
*
|
||||
* @defaultValue 30
|
||||
*/
|
||||
steps?: number | null;
|
||||
|
||||
/**
|
||||
* The cfg parameter acts as a creative control knob.
|
||||
* You can adjust it to fine-tune the level of artistic innovation in the image.
|
||||
* Lower values encourage faithful execution of the prompt,
|
||||
* while higher values introduce more creative and imaginative variations.
|
||||
*
|
||||
* Range: 3 - 15
|
||||
*
|
||||
* @defaultValue 7.5
|
||||
*/
|
||||
cfg?: number | null;
|
||||
|
||||
/**
|
||||
* The seed parameter serves as the initial value for the random number generator.
|
||||
* By setting a specific seed value, you can ensure that the AI generates the same
|
||||
* image or outcome each time you use that exact seed.
|
||||
*
|
||||
* range: 1-Infinity
|
||||
*/
|
||||
seed?: number | null;
|
||||
|
||||
/**
|
||||
* Influences the impact of the control image on output.
|
||||
*
|
||||
* Range: 0 - 1
|
||||
*
|
||||
* @defaultValue 0
|
||||
*/
|
||||
strength?: number | null;
|
||||
|
||||
/**
|
||||
* The method/control used to guide image generation.
|
||||
*
|
||||
* @defaultValue openpose
|
||||
*/
|
||||
control?: 'openpose' | 'scribble' | 'canny' | 'lineart' | 'depth' | null;
|
||||
|
||||
/**
|
||||
* 目前仅支持 binary 格式
|
||||
*/
|
||||
response_format?: 'binary' | null;
|
||||
}
|
||||
|
||||
export interface ImageGenerateParams {
|
||||
/**
|
||||
* A prompt is the text input that guides the AI in generating visual content.
|
||||
* It defines the textual description or concept for the image you wish to generate.
|
||||
* Think of it as the creative vision you want the AI to bring to life.
|
||||
* Crafting clear and creative prompts is crucial for achieving the desired results with Imagine's API.
|
||||
* For example, A serene forest with a river under the moonlight, can be a prompt.
|
||||
*/
|
||||
prompt: string;
|
||||
|
||||
/**
|
||||
* The model to use for image generation.
|
||||
*/
|
||||
model?: ImageModel | null;
|
||||
|
||||
/**
|
||||
* The negative_prompt parameter empowers you to provide additional
|
||||
* guidance to the AI by specifying what you don't want in the image.
|
||||
* It helps refine the creative direction, ensuring that the generated
|
||||
* content aligns with your intentions.
|
||||
*/
|
||||
negative_prompt?: string | null;
|
||||
|
||||
/**
|
||||
* The aspect_ratio parameter allows you to specify the proportions and dimensions of the generated image.
|
||||
* You can set it to different ratios like 1:1 for square images, 16:9 for widescreen, or 3:4 for vertical formats,
|
||||
* shaping the visual composition to your liking.
|
||||
*
|
||||
* @defaultValue 1:1
|
||||
*/
|
||||
aspect_ratio?: '1:1' | '3:2' | '4:3' | '3:4' | '16:9' | '9:16' | null;
|
||||
|
||||
/**
|
||||
* The quality parameter is a flag that, when set to hd,
|
||||
* requests high-resolution results from the AI.
|
||||
*
|
||||
* @defaultValue standard
|
||||
*/
|
||||
quality?: 'standard' | 'hd';
|
||||
|
||||
/**
|
||||
* The style_id parameter is like choosing an artistic palette for your image.
|
||||
* By selecting a style id, you guide the AI in crafting the image with a particular visual aesthetic.
|
||||
* Style IDs range from 1 to N, each representing a unique artistic style.
|
||||
*
|
||||
* @defaultValue 30
|
||||
*/
|
||||
style?: number | null;
|
||||
|
||||
/**
|
||||
* The steps parameter defines the number of operations or iterations that the
|
||||
* generator will perform during image creation. It can impact the complexity
|
||||
* and detail of the generated image.
|
||||
*
|
||||
* Range: 30-50
|
||||
*
|
||||
* @defaultValue 30
|
||||
*/
|
||||
steps?: number | null;
|
||||
|
||||
/**
|
||||
* The cfg parameter acts as a creative control knob.
|
||||
* You can adjust it to fine-tune the level of artistic innovation in the image.
|
||||
* Lower values encourage faithful execution of the prompt,
|
||||
* while higher values introduce more creative and imaginative variations.
|
||||
*
|
||||
* Range: 3 - 15
|
||||
*
|
||||
* @defaultValue 7.5
|
||||
*/
|
||||
cfg?: number | null;
|
||||
|
||||
/**
|
||||
* The seed parameter serves as the initial value for the random number generator.
|
||||
* By setting a specific seed value, you can ensure that the AI generates the same
|
||||
* image or outcome each time you use that exact seed.
|
||||
*
|
||||
* range: 1-Infinity
|
||||
*/
|
||||
seed?: number | null;
|
||||
|
||||
/**
|
||||
* 目前仅支持 binary 格式
|
||||
*/
|
||||
response_format?: 'binary' | null;
|
||||
}
|
||||
|
||||
export interface ImageUpscaleParams {
|
||||
/**
|
||||
* The image to use as the basis for the variation(s). Must be a valid PNG file,
|
||||
* less than 4MB, and square.
|
||||
*/
|
||||
image: Uploadable;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
/**
|
||||
* The binary of the generated image.
|
||||
*/
|
||||
binary?: ReadableStream;
|
||||
|
||||
/**
|
||||
* The base64-encoded JSON of the generated image, if `response_format` is
|
||||
* `b64_json`.
|
||||
*/
|
||||
b64_json?: string;
|
||||
|
||||
/**
|
||||
* The prompt that was used to generate the image, if there was any revision to the
|
||||
* prompt.
|
||||
*/
|
||||
revised_prompt?: string;
|
||||
|
||||
/**
|
||||
* The URL of the generated image, if `response_format` is `url` (default).
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface ImagesResponse {
|
||||
/**
|
||||
* When the request was made.
|
||||
*/
|
||||
created: number;
|
||||
|
||||
/**
|
||||
* The generated images.
|
||||
*/
|
||||
data: Image[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Images, type ImageGenerateParams } from './images';
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import socketIo from 'socket.io';
|
||||
|
||||
export const getTempPath = () => {
|
||||
const tempDir = os.tmpdir();
|
||||
@@ -10,3 +11,42 @@ export const getTempPath = () => {
|
||||
}
|
||||
return logDir;
|
||||
};
|
||||
|
||||
export async function emitAndWait<T>(
|
||||
io: socketIo.Server,
|
||||
event: string,
|
||||
data?: any,
|
||||
timeout: number = 5000,
|
||||
): Promise<any> {
|
||||
let response;
|
||||
if (data === undefined) {
|
||||
response = await io.timeout(timeout).emitWithAck(event);
|
||||
} else {
|
||||
response = await io.timeout(timeout).emitWithAck(event, data);
|
||||
}
|
||||
|
||||
// 判断是否是 Array
|
||||
if (Array.isArray(response) && response.length === 1) {
|
||||
if (typeof response[0] === 'undefined') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
if (response[0] === 'string') {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
// 如果 response[0] 也是数组,那么直接返回
|
||||
if (Array.isArray(response[0])) {
|
||||
return response[0] as T;
|
||||
}
|
||||
|
||||
return JSON.parse(response[0]);
|
||||
}
|
||||
|
||||
// 判断是否是字符串
|
||||
if (typeof response === 'string') {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 范围匹配
|
||||
* @param ptt 范围查询关键词 e.g. 'hello [and] world'
|
||||
* @param msg 消息
|
||||
* @returns
|
||||
*/
|
||||
export function rangeMatch(ptt: string, msg: string): boolean {
|
||||
if (ptt.includes('[and]')) {
|
||||
const keywords = ptt.split('[and]');
|
||||
return keywords.every((keyword) => matchKeyword(keyword.trim(), msg));
|
||||
}
|
||||
return matchKeyword(ptt, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配关键词
|
||||
* @param ptt 匹配模式
|
||||
* @param msg 消息
|
||||
* @returns
|
||||
*/
|
||||
export function matchKeyword(ptt: string, msg: string): boolean {
|
||||
let pattern = ptt.trim();
|
||||
|
||||
// 如果模式只是一个星号,它应该匹配任何消息。
|
||||
if (pattern === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 合并连续的 '*' 字符为一个 '*'
|
||||
pattern = pattern.replace(/\*+/g, '*');
|
||||
|
||||
// 如果模式不包含 '*',则直接比较是否相等
|
||||
if (!pattern.includes('*')) {
|
||||
return pattern === msg;
|
||||
}
|
||||
|
||||
const parts = pattern.split('*');
|
||||
let lastIndex = 0;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const part of parts) {
|
||||
// 跳过空字符串(它们来自模式开始、结束或连续 '*')
|
||||
if (part === '') continue;
|
||||
|
||||
const index = msg.indexOf(part, lastIndex);
|
||||
// 如果找不到部分或部分不是按顺序出现,则匹配失败
|
||||
if (index === -1 || index < lastIndex) {
|
||||
return false;
|
||||
}
|
||||
lastIndex = index + part.length;
|
||||
}
|
||||
|
||||
// 确保消息的剩余部分可以被模式尾部的 '*' 匹配
|
||||
return parts[parts.length - 1] === '' || lastIndex <= msg.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换文本中的 token
|
||||
* @param text 原始文本
|
||||
* @param replacements 替换文本
|
||||
* @returns
|
||||
*/
|
||||
export function tokenReplace(
|
||||
text: string,
|
||||
replacements: Record<string, string>,
|
||||
): string {
|
||||
return text.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return replacements[key] || match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换特殊 token
|
||||
* @param text 原始文本
|
||||
* @returns
|
||||
*/
|
||||
export function specialTokenReplace(text: string): string {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return text.replace(/\[\~\]/g, () => {
|
||||
const randomChoices = [
|
||||
...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'🌸',
|
||||
'😊',
|
||||
'🌷',
|
||||
'🌹',
|
||||
'💖',
|
||||
'🪷',
|
||||
'💐',
|
||||
'🌺',
|
||||
'🌼',
|
||||
'🌻',
|
||||
];
|
||||
return randomChoices[Math.floor(Math.random() * randomChoices.length)];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user