mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-31 03:09:04 +08:00
Compare commits
52 Commits
v3
...
feat-redes
Author | SHA1 | Date | |
---|---|---|---|
|
3fcf0513d2 | ||
|
8de8acdce8 | ||
|
77e321c7cb | ||
|
8093d1ffba | ||
|
74a6e1260e | ||
|
f55f04ab4f | ||
|
0aa807df19 | ||
|
48d44ece58 | ||
|
bffd9d9173 | ||
|
8688842984 | ||
|
a0e4a468d6 | ||
|
1e00c89988 | ||
|
0eccb547b5 | ||
|
4789a7f6a9 | ||
|
0bf758afd4 | ||
|
6612550c06 | ||
|
cf635a5e6f | ||
|
3a007e4f3d | ||
|
9faab960f6 | ||
|
5df8b1d183 | ||
|
ef5f910f19 | ||
|
fffbee80e8 | ||
|
6b30e167e1 | ||
|
8ec721259a | ||
|
9d7ce207b6 | ||
|
2d1f0c9f57 | ||
|
d3131d2f55 | ||
|
c10447df79 | ||
|
212ae76d76 | ||
|
cd48f7eff4 | ||
|
3513c6801e | ||
|
864529cbf6 | ||
|
58c0d3e12d | ||
|
a1493bfb4e | ||
|
b3e856df1d | ||
|
8ef2617eec | ||
|
1da7d81122 | ||
|
a103582346 | ||
|
7b61d05e88 | ||
|
6fc7c50f19 | ||
|
9d728ec3c5 | ||
|
9cd3358e4e | ||
|
4cd94370e8 | ||
|
52312dbd23 | ||
|
b2e8a1eaa2 | ||
|
506c17a093 | ||
|
69642fba52 | ||
|
7d647c981f | ||
|
dd4648ed9a | ||
|
1cd0beb231 | ||
|
b7aab3c102 | ||
|
fcb1a657e3 |
@@ -245,13 +245,17 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
|
|||||||
|
|
||||||
User `-all` to disable all default models, `+all` to enable all default models.
|
User `-all` to disable all default models, `+all` to enable all default models.
|
||||||
|
|
||||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
|
### `WHITE_WEBDEV_ENDPOINTS` (optional)
|
||||||
|
|
||||||
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
|
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
|
||||||
- Each address must be a complete endpoint
|
- Each address must be a complete endpoint
|
||||||
> `https://xxxx/yyy`
|
> `https://xxxx/yyy`
|
||||||
- Multiple addresses are connected by ', '
|
- Multiple addresses are connected by ', '
|
||||||
|
|
||||||
|
### `DEFAULT_INPUT_TEMPLATE` (optional)
|
||||||
|
|
||||||
|
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS >= 18, Docker >= 20
|
NodeJS >= 18, Docker >= 20
|
||||||
|
@@ -156,6 +156,9 @@ anthropic claude Api Url.
|
|||||||
|
|
||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
|
### `DEFAULT_INPUT_TEMPLATE` (可选)
|
||||||
|
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
点击下方按钮,开始二次开发:
|
点击下方按钮,开始二次开发:
|
||||||
|
93
app/api/provider/[...path]/route.ts
Normal file
93
app/api/provider/[...path]/route.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import * as ProviderTemplates from "@/app/client/providers";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
import {
|
||||||
|
disableSystemApiKey,
|
||||||
|
makeUrlsUsable,
|
||||||
|
modelNameRequestHeader,
|
||||||
|
} from "@/app/client/common";
|
||||||
|
import { collectModelTable } from "@/app/utils/model";
|
||||||
|
|
||||||
|
async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
const [providerName] = params.path;
|
||||||
|
const { headers } = req;
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
const modelName = headers.get(modelNameRequestHeader);
|
||||||
|
|
||||||
|
const ProviderTemplate = Object.values(ProviderTemplates).find(
|
||||||
|
(t) => t.prototype.name === providerName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ProviderTemplate) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: "No provider found: " + providerName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #1815 try to refuse gpt4 request
|
||||||
|
if (modelName && serverConfig.customModels) {
|
||||||
|
try {
|
||||||
|
const modelTable = collectModelTable([], serverConfig.customModels);
|
||||||
|
|
||||||
|
// not undefined and is false
|
||||||
|
if (modelTable[modelName]?.available === false) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${modelName} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("models filter", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = disableSystemApiKey(
|
||||||
|
makeUrlsUsable(cloneDeep(serverConfig), [
|
||||||
|
"anthropicUrl",
|
||||||
|
"azureUrl",
|
||||||
|
"googleUrl",
|
||||||
|
"baseUrl",
|
||||||
|
]),
|
||||||
|
["anthropicApiKey", "azureApiKey", "googleApiKey", "apiKey"],
|
||||||
|
serverConfig.needCode &&
|
||||||
|
ProviderTemplate !== ProviderTemplates.NextChatProvider, // if it must take a access code in the req, do not provide system-keys for Non-nextchat providers
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = Object.assign({}, req, {
|
||||||
|
subpath: params.path.join("/"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ProviderTemplate().serverSideRequestHandler(request, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = handle;
|
||||||
|
export const POST = handle;
|
||||||
|
export const PUT = handle;
|
||||||
|
export const PATCH = handle;
|
||||||
|
export const DELETE = handle;
|
||||||
|
export const OPTIONS = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
||||||
|
export const preferredRegion = Array.from(
|
||||||
|
new Set(
|
||||||
|
Object.values(ProviderTemplates).reduce(
|
||||||
|
(arr, t) => [...arr, ...(t.prototype.preferredRegion ?? [])],
|
||||||
|
[] as string[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
@@ -1,12 +1,12 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant";
|
import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
|
||||||
import { getServerSideConfig } from "@/app/config/server";
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
|
||||||
const config = getServerSideConfig();
|
const config = getServerSideConfig();
|
||||||
|
|
||||||
const mergedWhiteWebDavEndpoints = [
|
const mergedAllowedWebDavEndpoints = [
|
||||||
...internalWhiteWebDavEndpoints,
|
...internalAllowedWebDavEndpoints,
|
||||||
...config.whiteWebDevEndpoints,
|
...config.allowedWebDevEndpoints,
|
||||||
].filter((domain) => Boolean(domain.trim()));
|
].filter((domain) => Boolean(domain.trim()));
|
||||||
|
|
||||||
async function handle(
|
async function handle(
|
||||||
@@ -24,7 +24,9 @@ async function handle(
|
|||||||
|
|
||||||
// Validate the endpoint to prevent potential SSRF attacks
|
// Validate the endpoint to prevent potential SSRF attacks
|
||||||
if (
|
if (
|
||||||
!mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white))
|
!mergedAllowedWebDavEndpoints.some(
|
||||||
|
(allowedEndpoint) => endpoint?.startsWith(allowedEndpoint),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
7
app/client/common/index.ts
Normal file
7
app/client/common/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from "./types";
|
||||||
|
|
||||||
|
export * from "./locale";
|
||||||
|
|
||||||
|
export * from "./utils";
|
||||||
|
|
||||||
|
export const modelNameRequestHeader = "x-nextchat-model-name";
|
19
app/client/common/locale.ts
Normal file
19
app/client/common/locale.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Lang, getLang } from "@/app/locales";
|
||||||
|
|
||||||
|
interface PlainConfig {
|
||||||
|
[k: string]: PlainConfig | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocaleMap<
|
||||||
|
TextPlainConfig extends PlainConfig,
|
||||||
|
Default extends Lang,
|
||||||
|
> = Partial<Record<Lang, TextPlainConfig>> & {
|
||||||
|
[name in Default]: TextPlainConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocaleText<
|
||||||
|
TextPlainConfig extends PlainConfig,
|
||||||
|
DefaultLang extends Lang,
|
||||||
|
>(textMap: LocaleMap<TextPlainConfig, DefaultLang>, defaultLang: DefaultLang) {
|
||||||
|
return textMap[getLang()] || textMap[defaultLang];
|
||||||
|
}
|
211
app/client/common/types.ts
Normal file
211
app/client/common/types.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { RequestMessage } from "../api";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export { type RequestMessage };
|
||||||
|
|
||||||
|
// ===================================== LLM Types start ======================================
|
||||||
|
|
||||||
|
export interface ModelConfig {
|
||||||
|
temperature: number;
|
||||||
|
top_p: number;
|
||||||
|
presence_penalty: number;
|
||||||
|
frequency_penalty: number;
|
||||||
|
max_tokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelSettings extends Omit<ModelConfig, "max_tokens"> {
|
||||||
|
global_max_tokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelTemplate = {
|
||||||
|
name: string; // id of model in a provider
|
||||||
|
displayName: string;
|
||||||
|
isVisionModel?: boolean;
|
||||||
|
isDefaultActive: boolean; // model is initialized to be active
|
||||||
|
isDefaultSelected?: boolean; // model is initialized to be as default used model
|
||||||
|
max_tokens?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Model extends Omit<ModelTemplate, "isDefaultActive"> {
|
||||||
|
providerTemplateName: string;
|
||||||
|
isActive: boolean;
|
||||||
|
providerName: string;
|
||||||
|
available: boolean;
|
||||||
|
customized: boolean; // Only customized model is allowed to be modified
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelInfo extends Pick<ModelTemplate, "name"> {
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================== LLM Types end ======================================
|
||||||
|
|
||||||
|
// ===================================== Chat Request Types start ======================================
|
||||||
|
|
||||||
|
export interface ChatRequestPayload {
|
||||||
|
messages: RequestMessage[];
|
||||||
|
context: {
|
||||||
|
isApp: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StandChatRequestPayload extends ChatRequestPayload {
|
||||||
|
modelConfig: ModelConfig;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalChatRequestPayload<SettingKeys extends string = "">
|
||||||
|
extends StandChatRequestPayload {
|
||||||
|
providerConfig: Partial<Record<SettingKeys, string>>;
|
||||||
|
isVisionModel: Model["isVisionModel"];
|
||||||
|
stream: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderRequestPayload {
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body: string;
|
||||||
|
url: string;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalChatHandlers {
|
||||||
|
onProgress: (message: string, chunk: string) => void;
|
||||||
|
onFinish: (message: string) => void;
|
||||||
|
onError: (err: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatHandlers extends InternalChatHandlers {
|
||||||
|
onProgress: (chunk: string) => void;
|
||||||
|
onFinish: () => void;
|
||||||
|
onFlash: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================== Chat Request Types end ======================================
|
||||||
|
|
||||||
|
// ===================================== Chat Response Types start ======================================
|
||||||
|
|
||||||
|
export interface StandChatReponseMessage {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================== Chat Request Types end ======================================
|
||||||
|
|
||||||
|
// ===================================== Provider Settings Types start ======================================
|
||||||
|
|
||||||
|
type NumberRange = [number, number];
|
||||||
|
|
||||||
|
export type Validator =
|
||||||
|
| "required"
|
||||||
|
| "number"
|
||||||
|
| "string"
|
||||||
|
| NumberRange
|
||||||
|
| NumberRange[]
|
||||||
|
| ((v: any) => Promise<string | void>);
|
||||||
|
|
||||||
|
export type CommonSettingItem<SettingKeys extends string> = {
|
||||||
|
name: SettingKeys;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
validators?: Validator[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InputSettingItem = {
|
||||||
|
type: "input";
|
||||||
|
placeholder?: string;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
inputType?: "password" | "normal";
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType?: "number";
|
||||||
|
defaultValue?: number;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SelectSettingItem = {
|
||||||
|
type: "select";
|
||||||
|
options: {
|
||||||
|
name: string;
|
||||||
|
value: "number" | "string" | "boolean";
|
||||||
|
}[];
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RangeSettingItem = {
|
||||||
|
type: "range";
|
||||||
|
range: NumberRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SwitchSettingItem = {
|
||||||
|
type: "switch";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingItem<SettingKeys extends string = ""> =
|
||||||
|
CommonSettingItem<SettingKeys> &
|
||||||
|
(
|
||||||
|
| InputSettingItem
|
||||||
|
| SelectSettingItem
|
||||||
|
| RangeSettingItem
|
||||||
|
| SwitchSettingItem
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===================================== Provider Settings Types end ======================================
|
||||||
|
|
||||||
|
// ===================================== Provider Template Types start ======================================
|
||||||
|
|
||||||
|
export type ServerConfig = ReturnType<typeof getServerSideConfig>;
|
||||||
|
|
||||||
|
export interface IProviderTemplate<
|
||||||
|
SettingKeys extends string,
|
||||||
|
NAME extends string,
|
||||||
|
Meta extends Record<string, any>,
|
||||||
|
> {
|
||||||
|
readonly name: NAME;
|
||||||
|
|
||||||
|
readonly apiRouteRootName: `/api/provider/${NAME}`;
|
||||||
|
|
||||||
|
readonly allowedApiMethods: Array<
|
||||||
|
"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS"
|
||||||
|
>;
|
||||||
|
|
||||||
|
readonly metas: Meta;
|
||||||
|
|
||||||
|
readonly providerMeta: {
|
||||||
|
displayName: string;
|
||||||
|
settingItems: SettingItem<SettingKeys>[];
|
||||||
|
};
|
||||||
|
readonly defaultModels: ModelTemplate[];
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
): AbortController;
|
||||||
|
|
||||||
|
chat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
): Promise<StandChatReponseMessage>;
|
||||||
|
|
||||||
|
getAvailableModels?(
|
||||||
|
providerConfig: InternalChatRequestPayload<SettingKeys>["providerConfig"],
|
||||||
|
): Promise<ModelInfo[]>;
|
||||||
|
|
||||||
|
readonly runtime: "edge";
|
||||||
|
readonly preferredRegion: "auto" | "global" | "home" | string | string[];
|
||||||
|
|
||||||
|
serverSideRequestHandler(
|
||||||
|
req: NextRequest & {
|
||||||
|
subpath: string;
|
||||||
|
},
|
||||||
|
serverConfig: ServerConfig,
|
||||||
|
): Promise<NextResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderTemplate = IProviderTemplate<any, any, any>;
|
||||||
|
|
||||||
|
export interface Serializable<Snapshot> {
|
||||||
|
serialize(): Snapshot;
|
||||||
|
}
|
88
app/client/common/utils.ts
Normal file
88
app/client/common/utils.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { RequestMessage, ServerConfig } from "./types";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
|
||||||
|
export function getMessageTextContent(message: RequestMessage) {
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
return message.content;
|
||||||
|
}
|
||||||
|
for (const c of message.content) {
|
||||||
|
if (c.type === "text") {
|
||||||
|
return c.text ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageImages(message: RequestMessage): string[] {
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const urls: string[] = [];
|
||||||
|
for (const c of message.content) {
|
||||||
|
if (c.type === "image_url") {
|
||||||
|
urls.push(c.image_url?.url ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIP(req: NextRequest) {
|
||||||
|
let ip = req.ip ?? req.headers.get("x-real-ip");
|
||||||
|
const forwardedFor = req.headers.get("x-forwarded-for");
|
||||||
|
|
||||||
|
if (!ip && forwardedFor) {
|
||||||
|
ip = forwardedFor.split(",").at(0) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUrl(baseUrl?: string) {
|
||||||
|
if (baseUrl && !baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
if (baseUrl?.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function travel(
|
||||||
|
config: ServerConfig,
|
||||||
|
keys: Array<keyof ServerConfig>,
|
||||||
|
handle: (prop: any) => any,
|
||||||
|
): ServerConfig {
|
||||||
|
const copiedConfig = cloneDeep(config);
|
||||||
|
keys.forEach((k) => {
|
||||||
|
copiedConfig[k] = handle(copiedConfig[k] as string) as never;
|
||||||
|
});
|
||||||
|
return copiedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeUrlsUsable = (
|
||||||
|
config: ServerConfig,
|
||||||
|
keys: Array<keyof ServerConfig>,
|
||||||
|
) => travel(config, keys, formatUrl);
|
||||||
|
|
||||||
|
export const disableSystemApiKey = (
|
||||||
|
config: ServerConfig,
|
||||||
|
keys: Array<keyof ServerConfig>,
|
||||||
|
forbidden: boolean,
|
||||||
|
) =>
|
||||||
|
travel(config, keys, (p) => {
|
||||||
|
return forbidden ? undefined : p;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function isSameOrigin(requestUrl: string) {
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = requestUrl;
|
||||||
|
|
||||||
|
// 检查协议、主机名和端口号是否与当前页面相同
|
||||||
|
return (
|
||||||
|
a.protocol === window.location.protocol &&
|
||||||
|
a.hostname === window.location.hostname &&
|
||||||
|
a.port === window.location.port
|
||||||
|
);
|
||||||
|
}
|
9
app/client/core/index.ts
Normal file
9
app/client/core/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from "./shim";
|
||||||
|
|
||||||
|
export * from "../common/types";
|
||||||
|
|
||||||
|
export * from "./providerClient";
|
||||||
|
|
||||||
|
export * from "./modelClient";
|
||||||
|
|
||||||
|
export * from "../common/locale";
|
98
app/client/core/modelClient.ts
Normal file
98
app/client/core/modelClient.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
ChatRequestPayload,
|
||||||
|
Model,
|
||||||
|
ModelSettings,
|
||||||
|
InternalChatHandlers,
|
||||||
|
} from "../common";
|
||||||
|
import { Provider, ProviderClient } from "./providerClient";
|
||||||
|
|
||||||
|
export class ModelClient {
|
||||||
|
constructor(
|
||||||
|
private model: Model,
|
||||||
|
private modelSettings: ModelSettings,
|
||||||
|
private providerClient: ProviderClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
chat(payload: ChatRequestPayload, handlers: InternalChatHandlers) {
|
||||||
|
try {
|
||||||
|
return this.providerClient.streamChat(
|
||||||
|
{
|
||||||
|
...payload,
|
||||||
|
modelConfig: {
|
||||||
|
...this.modelSettings,
|
||||||
|
max_tokens:
|
||||||
|
this.model.max_tokens ?? this.modelSettings.global_max_tokens,
|
||||||
|
},
|
||||||
|
model: this.model.name,
|
||||||
|
},
|
||||||
|
handlers,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
handlers.onError(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summerize(payload: ChatRequestPayload) {
|
||||||
|
try {
|
||||||
|
return this.providerClient.chat({
|
||||||
|
...payload,
|
||||||
|
modelConfig: {
|
||||||
|
...this.modelSettings,
|
||||||
|
max_tokens:
|
||||||
|
this.model.max_tokens ?? this.modelSettings.global_max_tokens,
|
||||||
|
},
|
||||||
|
model: this.model.name,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must generate new ModelClient during every chat
|
||||||
|
export function ModelClientFactory(
|
||||||
|
model: Model,
|
||||||
|
provider: Provider,
|
||||||
|
modelSettings: ModelSettings,
|
||||||
|
) {
|
||||||
|
const providerClient = new ProviderClient(provider);
|
||||||
|
return new ModelClient(model, modelSettings, providerClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFiltertModels(
|
||||||
|
models: readonly Model[],
|
||||||
|
customModels: string,
|
||||||
|
) {
|
||||||
|
const modelTable: Record<string, Model> = {};
|
||||||
|
|
||||||
|
// default models
|
||||||
|
models.forEach((m) => {
|
||||||
|
modelTable[m.name] = m;
|
||||||
|
});
|
||||||
|
|
||||||
|
// server custom models
|
||||||
|
customModels
|
||||||
|
.split(",")
|
||||||
|
.filter((v) => !!v && v.length > 0)
|
||||||
|
.forEach((m) => {
|
||||||
|
const available = !m.startsWith("-");
|
||||||
|
const nameConfig =
|
||||||
|
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
|
||||||
|
const [name, displayName] = nameConfig.split("=");
|
||||||
|
|
||||||
|
// enable or disable all models
|
||||||
|
if (name === "all") {
|
||||||
|
Object.values(modelTable).forEach(
|
||||||
|
(model) => (model.available = available),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
modelTable[name] = {
|
||||||
|
...modelTable[name],
|
||||||
|
displayName,
|
||||||
|
available,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return modelTable;
|
||||||
|
}
|
256
app/client/core/providerClient.ts
Normal file
256
app/client/core/providerClient.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
IProviderTemplate,
|
||||||
|
InternalChatHandlers,
|
||||||
|
Model,
|
||||||
|
ModelTemplate,
|
||||||
|
ProviderTemplate,
|
||||||
|
StandChatReponseMessage,
|
||||||
|
StandChatRequestPayload,
|
||||||
|
isSameOrigin,
|
||||||
|
modelNameRequestHeader,
|
||||||
|
} from "../common";
|
||||||
|
import * as ProviderTemplates from "@/app/client/providers";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export type ProviderTemplateName =
|
||||||
|
(typeof ProviderTemplates)[keyof typeof ProviderTemplates]["prototype"]["name"];
|
||||||
|
|
||||||
|
export interface Provider<
|
||||||
|
Providerconfig extends Record<string, any> = Record<string, any>,
|
||||||
|
> {
|
||||||
|
name: string; // id of provider
|
||||||
|
isActive: boolean;
|
||||||
|
providerTemplateName: ProviderTemplateName;
|
||||||
|
providerConfig: Providerconfig;
|
||||||
|
isDefault: boolean; // Not allow to modify models of default provider
|
||||||
|
updated: boolean; // provider initial is finished
|
||||||
|
|
||||||
|
displayName: string;
|
||||||
|
models: Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerTemplates = Object.values(ProviderTemplates).reduce(
|
||||||
|
(r, t) => ({
|
||||||
|
...r,
|
||||||
|
[t.prototype.name]: new t(),
|
||||||
|
}),
|
||||||
|
{} as Record<ProviderTemplateName, ProviderTemplate>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export class ProviderClient {
|
||||||
|
providerTemplate: IProviderTemplate<any, any, any>;
|
||||||
|
genFetch: (modelName: string) => typeof window.fetch;
|
||||||
|
|
||||||
|
static ProviderTemplates = providerTemplates;
|
||||||
|
|
||||||
|
static getAllProviderTemplates = () => {
|
||||||
|
return Object.values(providerTemplates).reduce(
|
||||||
|
(r, t) => ({
|
||||||
|
...r,
|
||||||
|
[t.name]: t,
|
||||||
|
}),
|
||||||
|
{} as Record<ProviderTemplateName, ProviderTemplate>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
static getProviderTemplateMetaList = () => {
|
||||||
|
return Object.values(providerTemplates).map((t) => ({
|
||||||
|
...t.providerMeta,
|
||||||
|
name: t.name,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private provider: Provider) {
|
||||||
|
const { providerTemplateName } = provider;
|
||||||
|
this.providerTemplate = this.getProviderTemplate(providerTemplateName);
|
||||||
|
this.genFetch =
|
||||||
|
(modelName: string) =>
|
||||||
|
(...args) => {
|
||||||
|
const req = new Request(...args);
|
||||||
|
const headers: Record<string, any> = {
|
||||||
|
...req.headers,
|
||||||
|
};
|
||||||
|
if (isSameOrigin(req.url)) {
|
||||||
|
headers[modelNameRequestHeader] = modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.fetch(req.url, {
|
||||||
|
method: req.method,
|
||||||
|
keepalive: req.keepalive,
|
||||||
|
headers,
|
||||||
|
body: req.body,
|
||||||
|
redirect: req.redirect,
|
||||||
|
integrity: req.integrity,
|
||||||
|
signal: req.signal,
|
||||||
|
credentials: req.credentials,
|
||||||
|
mode: req.mode,
|
||||||
|
referrer: req.referrer,
|
||||||
|
referrerPolicy: req.referrerPolicy,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProviderTemplate(providerTemplateName: string) {
|
||||||
|
const providerTemplate = Object.values(providerTemplates).find(
|
||||||
|
(template) => template.name === providerTemplateName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return providerTemplate || providerTemplates.openai;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModelConfig(modelName: string) {
|
||||||
|
const { models } = this.provider;
|
||||||
|
return (
|
||||||
|
models.find((m) => m.name === modelName) ||
|
||||||
|
models.find((m) => m.isDefaultSelected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableModels() {
|
||||||
|
return Promise.resolve(
|
||||||
|
this.providerTemplate.getAvailableModels?.(this.provider.providerConfig),
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
const { defaultModels } = this.providerTemplate;
|
||||||
|
const availableModelsSet = new Set(
|
||||||
|
(res ?? defaultModels).map((o) => o.name),
|
||||||
|
);
|
||||||
|
return defaultModels.filter((m) => availableModelsSet.has(m.name));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return this.providerTemplate.defaultModels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: StandChatRequestPayload,
|
||||||
|
): Promise<StandChatReponseMessage> {
|
||||||
|
return this.providerTemplate.chat(
|
||||||
|
{
|
||||||
|
...payload,
|
||||||
|
stream: false,
|
||||||
|
isVisionModel: this.getModelConfig(payload.model)?.isVisionModel,
|
||||||
|
providerConfig: this.provider.providerConfig,
|
||||||
|
},
|
||||||
|
this.genFetch(payload.model),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(payload: StandChatRequestPayload, handlers: InternalChatHandlers) {
|
||||||
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
|
|
||||||
|
const timer = this.providerTemplate.streamChat(
|
||||||
|
{
|
||||||
|
...payload,
|
||||||
|
stream: true,
|
||||||
|
isVisionModel: this.getModelConfig(payload.model)?.isVisionModel,
|
||||||
|
providerConfig: this.provider.providerConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onProgress: (chunk) => {
|
||||||
|
remainText += chunk;
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
handlers.onError(err);
|
||||||
|
},
|
||||||
|
onFinish: () => {},
|
||||||
|
onFlash: (message: string) => {
|
||||||
|
handlers.onFinish(message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.genFetch(payload.model),
|
||||||
|
);
|
||||||
|
|
||||||
|
timer.signal.onabort = () => {
|
||||||
|
const message = responseText + remainText;
|
||||||
|
remainText = "";
|
||||||
|
handlers.onFinish(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateResponseText = () => {
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
handlers.onProgress(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
};
|
||||||
|
|
||||||
|
// start animaion
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params = Omit<Provider, "providerTemplateName" | "name" | "isDefault">;
|
||||||
|
|
||||||
|
function createProvider(
|
||||||
|
provider: ProviderTemplateName,
|
||||||
|
isDefault: true,
|
||||||
|
): Provider;
|
||||||
|
function createProvider(provider: ProviderTemplate, isDefault: true): Provider;
|
||||||
|
function createProvider(
|
||||||
|
provider: ProviderTemplateName,
|
||||||
|
isDefault: false,
|
||||||
|
params: Params,
|
||||||
|
): Provider;
|
||||||
|
function createProvider(
|
||||||
|
provider: ProviderTemplate,
|
||||||
|
isDefault: false,
|
||||||
|
params: Params,
|
||||||
|
): Provider;
|
||||||
|
function createProvider(
|
||||||
|
provider: ProviderTemplate | ProviderTemplateName,
|
||||||
|
isDefault: boolean,
|
||||||
|
params?: Params,
|
||||||
|
): Provider {
|
||||||
|
let providerTemplate: ProviderTemplate;
|
||||||
|
if (typeof provider === "string") {
|
||||||
|
providerTemplate = ProviderClient.getAllProviderTemplates()[provider];
|
||||||
|
} else {
|
||||||
|
providerTemplate = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = `${providerTemplate.name}__${nanoid()}`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayName = providerTemplate.providerMeta.displayName,
|
||||||
|
models = providerTemplate.defaultModels.map((m) =>
|
||||||
|
createModelFromModelTemplate(m, providerTemplate, name),
|
||||||
|
),
|
||||||
|
providerConfig,
|
||||||
|
} = params ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
isActive: true,
|
||||||
|
models,
|
||||||
|
providerTemplateName: providerTemplate.name,
|
||||||
|
providerConfig: isDefault ? {} : providerConfig!,
|
||||||
|
isDefault,
|
||||||
|
updated: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModelFromModelTemplate(
|
||||||
|
m: ModelTemplate,
|
||||||
|
p: ProviderTemplate,
|
||||||
|
providerName: string,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
providerTemplateName: p.name,
|
||||||
|
providerName,
|
||||||
|
isActive: m.isDefaultActive,
|
||||||
|
available: true,
|
||||||
|
customized: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createProvider };
|
25
app/client/core/shim.ts
Normal file
25
app/client/core/shim.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
|
||||||
|
if (!(window.fetch as any).__hijacked__) {
|
||||||
|
let _fetch = window.fetch;
|
||||||
|
|
||||||
|
function fetch(...args: Parameters<typeof _fetch>) {
|
||||||
|
const { isApp } = getClientConfig() || {};
|
||||||
|
|
||||||
|
let fetch: typeof _fetch = _fetch;
|
||||||
|
|
||||||
|
if (isApp) {
|
||||||
|
try {
|
||||||
|
fetch = window.__TAURI__!.http.fetch;
|
||||||
|
} catch (e) {
|
||||||
|
fetch = _fetch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch.__hijacked__ = true;
|
||||||
|
|
||||||
|
window.fetch = fetch;
|
||||||
|
}
|
3
app/client/index.ts
Normal file
3
app/client/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./core";
|
||||||
|
|
||||||
|
export * from "./providers";
|
@@ -161,6 +161,13 @@ export class ClaudeApi implements LLMApi {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (prompt[0]?.role === "assistant") {
|
||||||
|
prompt.unshift({
|
||||||
|
role: "user",
|
||||||
|
content: ";",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const requestBody: AnthropicChatRequest = {
|
const requestBody: AnthropicChatRequest = {
|
||||||
messages: prompt,
|
messages: prompt,
|
||||||
stream: shouldStream,
|
stream: shouldStream,
|
||||||
|
@@ -21,11 +21,10 @@ export class GeminiProApi implements LLMApi {
|
|||||||
}
|
}
|
||||||
async chat(options: ChatOptions): Promise<void> {
|
async chat(options: ChatOptions): Promise<void> {
|
||||||
// const apiClient = this;
|
// const apiClient = this;
|
||||||
const visionModel = isVisionModel(options.config.model);
|
|
||||||
let multimodal = false;
|
let multimodal = false;
|
||||||
const messages = options.messages.map((v) => {
|
const messages = options.messages.map((v) => {
|
||||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||||
if (visionModel) {
|
if (isVisionModel(options.config.model)) {
|
||||||
const images = getMessageImages(v);
|
const images = getMessageImages(v);
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
multimodal = true;
|
multimodal = true;
|
||||||
@@ -117,17 +116,14 @@ export class GeminiProApi implements LLMApi {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
options.onController?.(controller);
|
options.onController?.(controller);
|
||||||
try {
|
try {
|
||||||
let googleChatPath = visionModel
|
|
||||||
? Google.VisionChatPath(modelConfig.model)
|
|
||||||
: Google.ChatPath(modelConfig.model);
|
|
||||||
let chatPath = this.path(googleChatPath);
|
|
||||||
|
|
||||||
// let baseUrl = accessStore.googleUrl;
|
// let baseUrl = accessStore.googleUrl;
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
baseUrl = isApp
|
baseUrl = isApp
|
||||||
? DEFAULT_API_HOST + "/api/proxy/google/" + googleChatPath
|
? DEFAULT_API_HOST +
|
||||||
: chatPath;
|
"/api/proxy/google/" +
|
||||||
|
Google.ChatPath(modelConfig.model)
|
||||||
|
: this.path(Google.ChatPath(modelConfig.model));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isApp) {
|
if (isApp) {
|
||||||
@@ -145,6 +141,7 @@ export class GeminiProApi implements LLMApi {
|
|||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
let remainText = "";
|
let remainText = "";
|
||||||
|
@@ -129,7 +129,7 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// add max_tokens to vision model
|
// add max_tokens to vision model
|
||||||
if (visionModel) {
|
if (visionModel && modelConfig.model.includes("preview")) {
|
||||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
131
app/client/providers/anthropic/config.ts
Normal file
131
app/client/providers/anthropic/config.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { SettingItem } from "../../common";
|
||||||
|
import Locale from "./locale";
|
||||||
|
|
||||||
|
export type SettingKeys =
|
||||||
|
| "anthropicUrl"
|
||||||
|
| "anthropicApiKey"
|
||||||
|
| "anthropicApiVersion";
|
||||||
|
|
||||||
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
|
export const AnthropicMetas = {
|
||||||
|
ChatPath: "v1/messages",
|
||||||
|
ExampleEndpoint: ANTHROPIC_BASE_URL,
|
||||||
|
Vision: "2023-06-01",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClaudeMapper = {
|
||||||
|
assistant: "assistant",
|
||||||
|
user: "user",
|
||||||
|
system: "user",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const modelConfigs = [
|
||||||
|
{
|
||||||
|
name: "claude-instant-1.2",
|
||||||
|
displayName: "claude-instant-1.2",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-2.0",
|
||||||
|
displayName: "claude-2.0",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-2.1",
|
||||||
|
displayName: "claude-2.1",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-3-sonnet-20240229",
|
||||||
|
displayName: "claude-3-sonnet-20240229",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-3-opus-20240229",
|
||||||
|
displayName: "claude-3-opus-20240229",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-3-haiku-20240307",
|
||||||
|
displayName: "claude-3-haiku-20240307",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const preferredRegion: string | string[] = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const settingItems: (
|
||||||
|
defaultEndpoint: string,
|
||||||
|
) => SettingItem<SettingKeys>[] = (defaultEndpoint) => [
|
||||||
|
{
|
||||||
|
name: "anthropicUrl",
|
||||||
|
title: Locale.Endpoint.Title,
|
||||||
|
description: Locale.Endpoint.SubTitle + AnthropicMetas.ExampleEndpoint,
|
||||||
|
placeholder: AnthropicMetas.ExampleEndpoint,
|
||||||
|
type: "input",
|
||||||
|
defaultValue: defaultEndpoint,
|
||||||
|
validators: [
|
||||||
|
"required",
|
||||||
|
async (v: any) => {
|
||||||
|
if (typeof v === "string" && !v.startsWith(defaultEndpoint)) {
|
||||||
|
try {
|
||||||
|
new URL(v);
|
||||||
|
} catch (e) {
|
||||||
|
return Locale.Endpoint.Error.IllegalURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof v === "string" && v.endsWith("/")) {
|
||||||
|
return Locale.Endpoint.Error.EndWithBackslash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anthropicApiKey",
|
||||||
|
title: Locale.ApiKey.Title,
|
||||||
|
description: Locale.ApiKey.SubTitle,
|
||||||
|
placeholder: Locale.ApiKey.Placeholder,
|
||||||
|
type: "input",
|
||||||
|
inputType: "password",
|
||||||
|
// validators: ["required"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anthropicApiVersion",
|
||||||
|
title: Locale.ApiVerion.Title,
|
||||||
|
description: Locale.ApiVerion.SubTitle,
|
||||||
|
defaultValue: AnthropicMetas.Vision,
|
||||||
|
type: "input",
|
||||||
|
// validators: ["required"],
|
||||||
|
},
|
||||||
|
];
|
356
app/client/providers/anthropic/index.ts
Normal file
356
app/client/providers/anthropic/index.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import {
|
||||||
|
ANTHROPIC_BASE_URL,
|
||||||
|
AnthropicMetas,
|
||||||
|
ClaudeMapper,
|
||||||
|
SettingKeys,
|
||||||
|
modelConfigs,
|
||||||
|
preferredRegion,
|
||||||
|
settingItems,
|
||||||
|
} from "./config";
|
||||||
|
import {
|
||||||
|
ChatHandlers,
|
||||||
|
InternalChatRequestPayload,
|
||||||
|
IProviderTemplate,
|
||||||
|
ServerConfig,
|
||||||
|
} from "../../common";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import {
|
||||||
|
prettyObject,
|
||||||
|
getTimer,
|
||||||
|
authHeaderName,
|
||||||
|
auth,
|
||||||
|
parseResp,
|
||||||
|
formatMessage,
|
||||||
|
} from "./utils";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export type AnthropicProviderSettingKeys = SettingKeys;
|
||||||
|
|
||||||
|
export type MultiBlockContent = {
|
||||||
|
type: "image" | "text";
|
||||||
|
source?: {
|
||||||
|
type: string;
|
||||||
|
media_type: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnthropicMessage = {
|
||||||
|
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
|
||||||
|
content: string | MultiBlockContent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AnthropicChatRequest {
|
||||||
|
model: string; // The model that will complete your prompt.
|
||||||
|
messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
|
||||||
|
max_tokens: number; // The maximum number of tokens to generate before stopping.
|
||||||
|
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||||
|
temperature?: number; // Amount of randomness injected into the response.
|
||||||
|
top_p?: number; // Use nucleus sampling.
|
||||||
|
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||||
|
metadata?: object; // An object describing metadata about the request.
|
||||||
|
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatRequest {
|
||||||
|
model: string; // The model that will complete your prompt.
|
||||||
|
prompt: string; // The prompt that you want Claude to complete.
|
||||||
|
max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
|
||||||
|
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||||
|
temperature?: number; // Amount of randomness injected into the response.
|
||||||
|
top_p?: number; // Use nucleus sampling.
|
||||||
|
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||||
|
metadata?: object; // An object describing metadata about the request.
|
||||||
|
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderTemplate = IProviderTemplate<
|
||||||
|
SettingKeys,
|
||||||
|
"anthropic",
|
||||||
|
typeof AnthropicMetas
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class AnthropicProvider implements ProviderTemplate {
|
||||||
|
apiRouteRootName = "/api/provider/anthropic" as const;
|
||||||
|
allowedApiMethods: ["GET", "POST"] = ["GET", "POST"];
|
||||||
|
|
||||||
|
runtime = "edge" as const;
|
||||||
|
preferredRegion = preferredRegion;
|
||||||
|
|
||||||
|
name = "anthropic" as const;
|
||||||
|
|
||||||
|
metas = AnthropicMetas;
|
||||||
|
|
||||||
|
providerMeta = {
|
||||||
|
displayName: "Anthropic",
|
||||||
|
settingItems: settingItems(
|
||||||
|
`${this.apiRouteRootName}//${AnthropicMetas.ChatPath}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultModels = modelConfigs;
|
||||||
|
|
||||||
|
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||||
|
const {
|
||||||
|
messages: outsideMessages,
|
||||||
|
model,
|
||||||
|
stream,
|
||||||
|
modelConfig,
|
||||||
|
providerConfig,
|
||||||
|
} = payload;
|
||||||
|
const { anthropicApiKey, anthropicApiVersion, anthropicUrl } =
|
||||||
|
providerConfig;
|
||||||
|
const { temperature, top_p, max_tokens } = modelConfig;
|
||||||
|
|
||||||
|
const keys = ["system", "user"];
|
||||||
|
|
||||||
|
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||||
|
const messages = cloneDeep(outsideMessages);
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length - 1; i++) {
|
||||||
|
const message = messages[i];
|
||||||
|
const nextMessage = messages[i + 1];
|
||||||
|
|
||||||
|
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||||
|
messages[i] = [
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: ";",
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = formatMessage(messages, payload.isVisionModel);
|
||||||
|
|
||||||
|
const requestBody: AnthropicChatRequest = {
|
||||||
|
messages: prompt,
|
||||||
|
stream,
|
||||||
|
model,
|
||||||
|
max_tokens,
|
||||||
|
temperature,
|
||||||
|
top_p,
|
||||||
|
top_k: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
[authHeaderName]: anthropicApiKey ?? "",
|
||||||
|
"anthropic-version": anthropicApiVersion ?? "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
method: "POST",
|
||||||
|
url: anthropicUrl!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const authValue = req.headers.get(authHeaderName) ?? "";
|
||||||
|
|
||||||
|
const path = `${req.nextUrl.pathname}`.replaceAll(
|
||||||
|
this.apiRouteRootName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseUrl = serverConfig.anthropicUrl || ANTHROPIC_BASE_URL;
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}${path}`;
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
[authHeaderName]: authValue,
|
||||||
|
"anthropic-version":
|
||||||
|
req.headers.get("anthropic-version") ||
|
||||||
|
serverConfig.anthropicApiVersion ||
|
||||||
|
AnthropicMetas.Vision,
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Anthropic request]", fetchOptions.headers, req.method);
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new NextResponse(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
const res = await fetch(requestPayload.url, {
|
||||||
|
headers: {
|
||||||
|
...requestPayload.headers,
|
||||||
|
},
|
||||||
|
body: requestPayload.body,
|
||||||
|
method: requestPayload.method,
|
||||||
|
signal: timer.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
timer.clear();
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = parseResp(resJson);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
fetchEventSource(requestPayload.url, {
|
||||||
|
...requestPayload,
|
||||||
|
fetch,
|
||||||
|
async onopen(res) {
|
||||||
|
timer.clear();
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log("[OpenAI] request response content type: ", contentType);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
const responseText = await res.clone().text();
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [];
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
handlers.onProgress(delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
handlers.onFinish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
handlers.onError(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSideRequestHandler: ProviderTemplate["serverSideRequestHandler"] =
|
||||||
|
async (req, config) => {
|
||||||
|
const { subpath } = req;
|
||||||
|
const ALLOWD_PATH = [AnthropicMetas.ChatPath];
|
||||||
|
|
||||||
|
if (!ALLOWD_PATH.includes(subpath)) {
|
||||||
|
console.log("[Anthropic Route] forbidden path ", subpath);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to request " + subpath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, config);
|
||||||
|
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.request(req, config);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Anthropic] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
134
app/client/providers/anthropic/locale.ts
Normal file
134
app/client/providers/anthropic/locale.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { getLocaleText } from "../../common";
|
||||||
|
|
||||||
|
export default getLocaleText<
|
||||||
|
{
|
||||||
|
ApiKey: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Placeholder: string;
|
||||||
|
};
|
||||||
|
Endpoint: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: string;
|
||||||
|
IllegalURL: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ApiVerion: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"en"
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
cn: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制",
|
||||||
|
Placeholder: "Anthropic API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」结尾",
|
||||||
|
IllegalURL: "请输入一个完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "接口版本 (claude api version)",
|
||||||
|
SubTitle: "选择一个特定的 API 版本输入",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Anthropic API Key",
|
||||||
|
SubTitle:
|
||||||
|
"Use a custom Anthropic Key to bypass password access restrictions",
|
||||||
|
Placeholder: "Anthropic API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Cannot end with '/'",
|
||||||
|
IllegalURL: "Please enter a complete available url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "API Version (claude api version)",
|
||||||
|
SubTitle: "Select and input a specific API version",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Chave API Anthropic",
|
||||||
|
SubTitle: "Verifique sua chave API do console Anthropic",
|
||||||
|
Placeholder: "Chave API Anthropic",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Exemplo: ",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Não é possível terminar com '/'",
|
||||||
|
IllegalURL: "Insira um URL completo disponível",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Versão API (Versão api claude)",
|
||||||
|
SubTitle: "Verifique sua versão API do console Anthropic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API kľúč Anthropic",
|
||||||
|
SubTitle: "Skontrolujte svoj API kľúč v Anthropic konzole",
|
||||||
|
Placeholder: "API kľúč Anthropic",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Adresa koncového bodu",
|
||||||
|
SubTitle: "Príklad:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Nemôže končiť znakom „/“",
|
||||||
|
IllegalURL: "Zadajte úplnú dostupnú adresu URL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Verzia API (claude verzia API)",
|
||||||
|
SubTitle: "Vyberte špecifickú verziu časti",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API 金鑰",
|
||||||
|
SubTitle: "從 Anthropic AI 取得您的 API 金鑰",
|
||||||
|
Placeholder: "Anthropic API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "終端地址",
|
||||||
|
SubTitle: "範例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」結尾",
|
||||||
|
IllegalURL: "請輸入一個完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "API 版本 (claude api version)",
|
||||||
|
SubTitle: "選擇一個特定的 API 版本輸入",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"en",
|
||||||
|
);
|
151
app/client/providers/anthropic/utils.ts
Normal file
151
app/client/providers/anthropic/utils.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import {
|
||||||
|
RequestMessage,
|
||||||
|
ServerConfig,
|
||||||
|
getIP,
|
||||||
|
getMessageTextContent,
|
||||||
|
} from "../../common";
|
||||||
|
import { ClaudeMapper } from "./config";
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
export const authHeaderName = "x-api-key";
|
||||||
|
|
||||||
|
export function trimEnd(s: string, end = " ") {
|
||||||
|
if (end.length === 0) return s;
|
||||||
|
|
||||||
|
while (s.endsWith(end)) {
|
||||||
|
s = s.slice(0, -end.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bearer(value: string) {
|
||||||
|
return `Bearer ${value.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyObject(msg: any) {
|
||||||
|
const obj = msg;
|
||||||
|
if (typeof msg !== "string") {
|
||||||
|
msg = JSON.stringify(msg, null, " ");
|
||||||
|
}
|
||||||
|
if (msg === "{}") {
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
if (msg.startsWith("```json")) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
return ["```json", msg, "```"].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimer() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...controller,
|
||||||
|
clear: () => {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const apiKey = req.headers.get(authHeaderName);
|
||||||
|
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
|
if (serverConfig.hideUserApiKey && apiKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to access with your own api key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
console.log("[Auth] use user api key");
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user does not provide an api key, inject system api key
|
||||||
|
const systemApiKey = serverConfig.anthropicApiKey;
|
||||||
|
|
||||||
|
if (systemApiKey) {
|
||||||
|
console.log("[Auth] use system api key");
|
||||||
|
req.headers.set(authHeaderName, systemApiKey);
|
||||||
|
} else {
|
||||||
|
console.log("[Auth] admin did not provide an api key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResp(res: any) {
|
||||||
|
return {
|
||||||
|
message: res?.content?.[0]?.text ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMessage(
|
||||||
|
messages: RequestMessage[],
|
||||||
|
isVisionModel?: boolean,
|
||||||
|
) {
|
||||||
|
return messages
|
||||||
|
.flat()
|
||||||
|
.filter((v) => {
|
||||||
|
if (!v.content) return false;
|
||||||
|
if (typeof v.content === "string" && !v.content.trim()) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((v) => {
|
||||||
|
const { role, content } = v;
|
||||||
|
const insideRole = ClaudeMapper[role] ?? "user";
|
||||||
|
|
||||||
|
if (!isVisionModel || typeof content === "string") {
|
||||||
|
return {
|
||||||
|
role: insideRole,
|
||||||
|
content: getMessageTextContent(v),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: insideRole,
|
||||||
|
content: content
|
||||||
|
.filter((v) => v.image_url || v.text)
|
||||||
|
.map(({ type, text, image_url }) => {
|
||||||
|
if (type === "text") {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
text: text!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { url = "" } = image_url || {};
|
||||||
|
const colonIndex = url.indexOf(":");
|
||||||
|
const semicolonIndex = url.indexOf(";");
|
||||||
|
const comma = url.indexOf(",");
|
||||||
|
|
||||||
|
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
|
||||||
|
const encodeType = url.slice(semicolonIndex + 1, comma);
|
||||||
|
const data = url.slice(comma + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "image" as const,
|
||||||
|
source: {
|
||||||
|
type: encodeType,
|
||||||
|
media_type: mimeType,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
79
app/client/providers/azure/config.ts
Normal file
79
app/client/providers/azure/config.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import Locale from "./locale";
|
||||||
|
|
||||||
|
import { SettingItem } from "../../common";
|
||||||
|
import { modelConfigs as openaiModelConfigs } from "../openai/config";
|
||||||
|
|
||||||
|
export const AzureMetas = {
|
||||||
|
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
||||||
|
ChatPath: "chat/completions",
|
||||||
|
ListModelPath: "v1/models",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingKeys = "azureUrl" | "azureApiKey" | "azureApiVersion";
|
||||||
|
|
||||||
|
export const preferredRegion: string | string[] = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const modelConfigs = openaiModelConfigs;
|
||||||
|
|
||||||
|
export const settingItems: (
|
||||||
|
defaultEndpoint: string,
|
||||||
|
) => SettingItem<SettingKeys>[] = (defaultEndpoint) => [
|
||||||
|
{
|
||||||
|
name: "azureUrl",
|
||||||
|
title: Locale.Endpoint.Title,
|
||||||
|
description: Locale.Endpoint.SubTitle + AzureMetas.ExampleEndpoint,
|
||||||
|
placeholder: AzureMetas.ExampleEndpoint,
|
||||||
|
type: "input",
|
||||||
|
defaultValue: defaultEndpoint,
|
||||||
|
validators: [
|
||||||
|
async (v: any) => {
|
||||||
|
if (typeof v === "string") {
|
||||||
|
try {
|
||||||
|
new URL(v);
|
||||||
|
} catch (e) {
|
||||||
|
return Locale.Endpoint.Error.IllegalURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof v === "string" && v.endsWith("/")) {
|
||||||
|
return Locale.Endpoint.Error.EndWithBackslash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "azureApiKey",
|
||||||
|
title: Locale.ApiKey.Title,
|
||||||
|
description: Locale.ApiKey.SubTitle,
|
||||||
|
placeholder: Locale.ApiKey.Placeholder,
|
||||||
|
type: "input",
|
||||||
|
inputType: "password",
|
||||||
|
validators: ["required"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "azureApiVersion",
|
||||||
|
title: Locale.ApiVerion.Title,
|
||||||
|
description: Locale.ApiVerion.SubTitle,
|
||||||
|
placeholder: "2023-08-01-preview",
|
||||||
|
type: "input",
|
||||||
|
validators: ["required"],
|
||||||
|
},
|
||||||
|
];
|
408
app/client/providers/azure/index.ts
Normal file
408
app/client/providers/azure/index.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import {
|
||||||
|
settingItems,
|
||||||
|
SettingKeys,
|
||||||
|
modelConfigs,
|
||||||
|
AzureMetas,
|
||||||
|
preferredRegion,
|
||||||
|
} from "./config";
|
||||||
|
import {
|
||||||
|
ChatHandlers,
|
||||||
|
InternalChatRequestPayload,
|
||||||
|
IProviderTemplate,
|
||||||
|
ModelInfo,
|
||||||
|
getMessageTextContent,
|
||||||
|
ServerConfig,
|
||||||
|
} from "../../common";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import {
|
||||||
|
auth,
|
||||||
|
authHeaderName,
|
||||||
|
getHeaders,
|
||||||
|
getTimer,
|
||||||
|
makeAzurePath,
|
||||||
|
parseResp,
|
||||||
|
prettyObject,
|
||||||
|
} from "./utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export type AzureProviderSettingKeys = SettingKeys;
|
||||||
|
|
||||||
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
|
|
||||||
|
export interface MultimodalContent {
|
||||||
|
type: "text" | "image_url";
|
||||||
|
text?: string;
|
||||||
|
image_url?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestMessage {
|
||||||
|
role: MessageRole;
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestPayload {
|
||||||
|
messages: {
|
||||||
|
role: "system" | "user" | "assistant";
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}[];
|
||||||
|
stream?: boolean;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
presence_penalty: number;
|
||||||
|
frequency_penalty: number;
|
||||||
|
top_p: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelList {
|
||||||
|
object: "list";
|
||||||
|
data: Array<{
|
||||||
|
capabilities: {
|
||||||
|
fine_tune: boolean;
|
||||||
|
inference: boolean;
|
||||||
|
completion: boolean;
|
||||||
|
chat_completion: boolean;
|
||||||
|
embeddings: boolean;
|
||||||
|
};
|
||||||
|
lifecycle_status: "generally-available";
|
||||||
|
id: string;
|
||||||
|
created_at: number;
|
||||||
|
object: "model";
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIListModelResponse {
|
||||||
|
object: string;
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
root: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderTemplate = IProviderTemplate<
|
||||||
|
SettingKeys,
|
||||||
|
"azure",
|
||||||
|
typeof AzureMetas
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class Azure implements ProviderTemplate {
|
||||||
|
apiRouteRootName: "/api/provider/azure" = "/api/provider/azure";
|
||||||
|
allowedApiMethods: (
|
||||||
|
| "POST"
|
||||||
|
| "GET"
|
||||||
|
| "OPTIONS"
|
||||||
|
| "PUT"
|
||||||
|
| "PATCH"
|
||||||
|
| "DELETE"
|
||||||
|
)[] = ["POST", "GET"];
|
||||||
|
runtime = "edge" as const;
|
||||||
|
|
||||||
|
preferredRegion = preferredRegion;
|
||||||
|
|
||||||
|
name = "azure" as const;
|
||||||
|
metas = AzureMetas;
|
||||||
|
|
||||||
|
defaultModels = modelConfigs;
|
||||||
|
|
||||||
|
providerMeta = {
|
||||||
|
displayName: "Azure",
|
||||||
|
settingItems: settingItems(
|
||||||
|
`${this.apiRouteRootName}/${AzureMetas.ChatPath}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isVisionModel,
|
||||||
|
model,
|
||||||
|
stream,
|
||||||
|
modelConfig: {
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
max_tokens,
|
||||||
|
},
|
||||||
|
providerConfig: { azureUrl, azureApiVersion },
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const openAiMessages = messages.map((v) => ({
|
||||||
|
role: v.role,
|
||||||
|
content: isVisionModel ? v.content : getMessageTextContent(v),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages: openAiMessages,
|
||||||
|
stream,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
};
|
||||||
|
|
||||||
|
// add max_tokens to vision model
|
||||||
|
if (isVisionModel) {
|
||||||
|
requestPayload["max_tokens"] = Math.max(max_tokens, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: getHeaders(payload.providerConfig.azureApiKey),
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
method: "POST",
|
||||||
|
url: `${azureUrl}?api-version=${azureApiVersion!}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestAzure(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const authValue =
|
||||||
|
req.headers
|
||||||
|
.get("Authorization")
|
||||||
|
?.trim()
|
||||||
|
.replaceAll("Bearer ", "")
|
||||||
|
.trim() ?? "";
|
||||||
|
|
||||||
|
const { azureUrl, azureApiVersion } = serverConfig;
|
||||||
|
|
||||||
|
if (!azureUrl) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: true,
|
||||||
|
message: `missing AZURE_URL in server env vars`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!azureApiVersion) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: true,
|
||||||
|
message: `missing AZURE_API_VERSION in server env vars`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||||
|
this.apiRouteRootName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
path = makeAzurePath(path, azureApiVersion);
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", azureUrl);
|
||||||
|
|
||||||
|
const fetchUrl = `${azureUrl}/${path}`;
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
[authHeaderName]: authValue,
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||||
|
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||||
|
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||||
|
// The browser will try to decode the response with brotli and fail
|
||||||
|
newHeaders.delete("content-encoding");
|
||||||
|
|
||||||
|
return new NextResponse(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
const res = await fetch(requestPayload.url, {
|
||||||
|
headers: {
|
||||||
|
...requestPayload.headers,
|
||||||
|
},
|
||||||
|
body: requestPayload.body,
|
||||||
|
method: requestPayload.method,
|
||||||
|
signal: timer.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
timer.clear();
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = parseResp(resJson);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
fetchEventSource(requestPayload.url, {
|
||||||
|
...requestPayload,
|
||||||
|
fetch,
|
||||||
|
async onopen(res) {
|
||||||
|
timer.clear();
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log("[OpenAI] request response content type: ", contentType);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
const responseText = await res.clone().text();
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [];
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
handlers.onProgress(delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
handlers.onFinish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
handlers.onError(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableModels(
|
||||||
|
providerConfig: Record<SettingKeys, string>,
|
||||||
|
): Promise<ModelInfo[]> {
|
||||||
|
const { azureApiKey, azureUrl } = providerConfig;
|
||||||
|
const res = await fetch(`${azureUrl}/${AzureMetas.ListModelPath}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${azureApiKey}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data: ModelList = await res.json();
|
||||||
|
|
||||||
|
return data.data.map((o) => ({
|
||||||
|
name: o.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSideRequestHandler: ProviderTemplate["serverSideRequestHandler"] =
|
||||||
|
async (req, config) => {
|
||||||
|
const { subpath } = req;
|
||||||
|
const ALLOWD_PATH = [AzureMetas.ChatPath];
|
||||||
|
|
||||||
|
if (!ALLOWD_PATH.includes(subpath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to request " + subpath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, config);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.requestAzure(req, config);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
133
app/client/providers/azure/locale.ts
Normal file
133
app/client/providers/azure/locale.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { getLocaleText } from "../../common";
|
||||||
|
|
||||||
|
export default getLocaleText<
|
||||||
|
{
|
||||||
|
ApiKey: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Placeholder: string;
|
||||||
|
};
|
||||||
|
Endpoint: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: string;
|
||||||
|
IllegalURL: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ApiVerion: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"en"
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
cn: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 Azure Key 绕过密码访问限制",
|
||||||
|
Placeholder: "Azure API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」结尾",
|
||||||
|
IllegalURL: "请输入一个完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "接口版本 (azure api version)",
|
||||||
|
SubTitle: "选择指定的部分版本",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Azure Api Key",
|
||||||
|
SubTitle: "Check your api key from Azure console",
|
||||||
|
Placeholder: "Azure Api Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Azure Endpoint",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Cannot end with '/'",
|
||||||
|
IllegalURL: "Please enter a complete available url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Azure Api Version",
|
||||||
|
SubTitle: "Check your api version from azure console",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Chave API Azure",
|
||||||
|
SubTitle: "Verifique sua chave API do console Azure",
|
||||||
|
Placeholder: "Chave API Azure",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Azure",
|
||||||
|
SubTitle: "Exemplo: ",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Não é possível terminar com '/'",
|
||||||
|
IllegalURL: "Insira um URL completo disponível",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Versão API Azure",
|
||||||
|
SubTitle: "Verifique sua versão API do console Azure",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API kľúč Azure",
|
||||||
|
SubTitle: "Skontrolujte svoj API kľúč v Azure konzole",
|
||||||
|
Placeholder: "API kľúč Azure",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Koncový bod Azure",
|
||||||
|
SubTitle: "Príklad: ",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Nemôže končiť znakom „/“",
|
||||||
|
IllegalURL: "Zadajte úplnú dostupnú adresu URL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "Verzia API Azure",
|
||||||
|
SubTitle: "Skontrolujte svoju verziu API v Azure konzole",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "介面金鑰",
|
||||||
|
SubTitle: "使用自定義 Azure Key 繞過密碼存取限制",
|
||||||
|
Placeholder: "Azure API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "介面(Endpoint) 地址",
|
||||||
|
SubTitle: "樣例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」結尾",
|
||||||
|
IllegalURL: "請輸入一個完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "介面版本 (azure api version)",
|
||||||
|
SubTitle: "選擇指定的部分版本",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"en",
|
||||||
|
);
|
110
app/client/providers/azure/utils.ts
Normal file
110
app/client/providers/azure/utils.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { ServerConfig, getIP } from "../../common";
|
||||||
|
|
||||||
|
export const authHeaderName = "api-key";
|
||||||
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
export function getHeaders(azureApiKey?: string) {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validString(azureApiKey)) {
|
||||||
|
headers[authHeaderName] = makeBearer(azureApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResp(res: any) {
|
||||||
|
return {
|
||||||
|
message: res.choices?.at(0)?.message?.content ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAzurePath(path: string, apiVersion: string) {
|
||||||
|
// should add api-key to query string
|
||||||
|
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyObject(msg: any) {
|
||||||
|
const obj = msg;
|
||||||
|
if (typeof msg !== "string") {
|
||||||
|
msg = JSON.stringify(msg, null, " ");
|
||||||
|
}
|
||||||
|
if (msg === "{}") {
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
if (msg.startsWith("```json")) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
return ["```json", msg, "```"].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeBearer = (s: string) => `Bearer ${s.trim()}`;
|
||||||
|
export const validString = (x?: string): x is string =>
|
||||||
|
Boolean(x && x.length > 0);
|
||||||
|
|
||||||
|
export function parseApiKey(bearToken: string) {
|
||||||
|
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimer() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...controller,
|
||||||
|
clear: () => {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const authToken = req.headers.get(authHeaderName) ?? "";
|
||||||
|
|
||||||
|
const { hideUserApiKey, apiKey: systemApiKey } = serverConfig;
|
||||||
|
|
||||||
|
const { apiKey } = parseApiKey(authToken);
|
||||||
|
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
|
if (hideUserApiKey && apiKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to access with your own api key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
console.log("[Auth] use user api key");
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemApiKey) {
|
||||||
|
console.log("[Auth] use system api key");
|
||||||
|
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||||
|
} else {
|
||||||
|
console.log("[Auth] admin did not provide an api key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
95
app/client/providers/google/config.ts
Normal file
95
app/client/providers/google/config.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { SettingItem } from "../../common";
|
||||||
|
import Locale from "./locale";
|
||||||
|
|
||||||
|
export const preferredRegion: string | string[] = [
|
||||||
|
"bom1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||||
|
|
||||||
|
export const GoogleMetas = {
|
||||||
|
ExampleEndpoint: GEMINI_BASE_URL,
|
||||||
|
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingKeys = "googleUrl" | "googleApiKey" | "googleApiVersion";
|
||||||
|
|
||||||
|
export const modelConfigs = [
|
||||||
|
{
|
||||||
|
name: "gemini-1.0-pro",
|
||||||
|
displayName: "gemini-1.0-pro",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gemini-1.5-pro-latest",
|
||||||
|
displayName: "gemini-1.5-pro-latest",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gemini-pro-vision",
|
||||||
|
displayName: "gemini-pro-vision",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const settingItems: (
|
||||||
|
defaultEndpoint: string,
|
||||||
|
) => SettingItem<SettingKeys>[] = (defaultEndpoint) => [
|
||||||
|
{
|
||||||
|
name: "googleUrl",
|
||||||
|
title: Locale.Endpoint.Title,
|
||||||
|
description: Locale.Endpoint.SubTitle + GoogleMetas.ExampleEndpoint,
|
||||||
|
placeholder: GoogleMetas.ExampleEndpoint,
|
||||||
|
type: "input",
|
||||||
|
defaultValue: defaultEndpoint,
|
||||||
|
validators: [
|
||||||
|
async (v: any) => {
|
||||||
|
if (typeof v === "string") {
|
||||||
|
try {
|
||||||
|
new URL(v);
|
||||||
|
} catch (e) {
|
||||||
|
return Locale.Endpoint.Error.IllegalURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof v === "string" && v.endsWith("/")) {
|
||||||
|
return Locale.Endpoint.Error.EndWithBackslash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "googleApiKey",
|
||||||
|
title: Locale.ApiKey.Title,
|
||||||
|
description: Locale.ApiKey.SubTitle,
|
||||||
|
placeholder: Locale.ApiKey.Placeholder,
|
||||||
|
type: "input",
|
||||||
|
inputType: "password",
|
||||||
|
// validators: ["required"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "googleApiVersion",
|
||||||
|
title: Locale.ApiVersion.Title,
|
||||||
|
description: Locale.ApiVersion.SubTitle,
|
||||||
|
placeholder: "2023-08-01-preview",
|
||||||
|
type: "input",
|
||||||
|
// validators: ["required"],
|
||||||
|
},
|
||||||
|
];
|
353
app/client/providers/google/index.ts
Normal file
353
app/client/providers/google/index.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import {
|
||||||
|
SettingKeys,
|
||||||
|
modelConfigs,
|
||||||
|
settingItems,
|
||||||
|
GoogleMetas,
|
||||||
|
GEMINI_BASE_URL,
|
||||||
|
preferredRegion,
|
||||||
|
} from "./config";
|
||||||
|
import {
|
||||||
|
ChatHandlers,
|
||||||
|
InternalChatRequestPayload,
|
||||||
|
IProviderTemplate,
|
||||||
|
ModelInfo,
|
||||||
|
StandChatReponseMessage,
|
||||||
|
getMessageTextContent,
|
||||||
|
getMessageImages,
|
||||||
|
} from "../../common";
|
||||||
|
import {
|
||||||
|
auth,
|
||||||
|
ensureProperEnding,
|
||||||
|
getTimer,
|
||||||
|
parseResp,
|
||||||
|
urlParamApikeyName,
|
||||||
|
} from "./utils";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export type GoogleProviderSettingKeys = SettingKeys;
|
||||||
|
|
||||||
|
interface ModelList {
|
||||||
|
models: Array<{
|
||||||
|
name: string;
|
||||||
|
baseModelId: string;
|
||||||
|
version: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
inputTokenLimit: number; // Integer
|
||||||
|
outputTokenLimit: number; // Integer
|
||||||
|
supportedGenerationMethods: [string];
|
||||||
|
temperature: number;
|
||||||
|
topP: number;
|
||||||
|
topK: number; // Integer
|
||||||
|
}>;
|
||||||
|
nextPageToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderTemplate = IProviderTemplate<
|
||||||
|
SettingKeys,
|
||||||
|
"azure",
|
||||||
|
typeof GoogleMetas
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class GoogleProvider
|
||||||
|
implements IProviderTemplate<SettingKeys, "google", typeof GoogleMetas>
|
||||||
|
{
|
||||||
|
allowedApiMethods: (
|
||||||
|
| "POST"
|
||||||
|
| "GET"
|
||||||
|
| "OPTIONS"
|
||||||
|
| "PUT"
|
||||||
|
| "PATCH"
|
||||||
|
| "DELETE"
|
||||||
|
)[] = ["GET", "POST"];
|
||||||
|
runtime = "edge" as const;
|
||||||
|
|
||||||
|
apiRouteRootName: "/api/provider/google" = "/api/provider/google";
|
||||||
|
|
||||||
|
preferredRegion = preferredRegion;
|
||||||
|
|
||||||
|
name = "google" as const;
|
||||||
|
metas = GoogleMetas;
|
||||||
|
|
||||||
|
providerMeta = {
|
||||||
|
displayName: "Google",
|
||||||
|
settingItems: settingItems(this.apiRouteRootName),
|
||||||
|
};
|
||||||
|
defaultModels = modelConfigs;
|
||||||
|
|
||||||
|
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isVisionModel,
|
||||||
|
model,
|
||||||
|
stream,
|
||||||
|
modelConfig,
|
||||||
|
providerConfig,
|
||||||
|
} = payload;
|
||||||
|
const { googleUrl, googleApiKey } = providerConfig;
|
||||||
|
const { temperature, top_p, max_tokens } = modelConfig;
|
||||||
|
|
||||||
|
const internalMessages = messages.map((v) => {
|
||||||
|
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||||
|
|
||||||
|
if (isVisionModel) {
|
||||||
|
const images = getMessageImages(v);
|
||||||
|
if (images.length > 0) {
|
||||||
|
parts = parts.concat(
|
||||||
|
images.map((image) => {
|
||||||
|
const imageType = image.split(";")[0].split(":")[1];
|
||||||
|
const imageData = image.split(",")[1];
|
||||||
|
return {
|
||||||
|
inline_data: {
|
||||||
|
mime_type: imageType,
|
||||||
|
data: imageData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: v.role.replace("assistant", "model").replace("system", "user"),
|
||||||
|
parts: parts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// google requires that role in neighboring messages must not be the same
|
||||||
|
for (let i = 0; i < internalMessages.length - 1; ) {
|
||||||
|
// Check if current and next item both have the role "model"
|
||||||
|
if (internalMessages[i].role === internalMessages[i + 1].role) {
|
||||||
|
// Concatenate the 'parts' of the current and next item
|
||||||
|
internalMessages[i].parts = internalMessages[i].parts.concat(
|
||||||
|
internalMessages[i + 1].parts,
|
||||||
|
);
|
||||||
|
// Remove the next item
|
||||||
|
internalMessages.splice(i + 1, 1);
|
||||||
|
} else {
|
||||||
|
// Move to the next item
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPayload = {
|
||||||
|
contents: internalMessages,
|
||||||
|
generationConfig: {
|
||||||
|
temperature,
|
||||||
|
maxOutputTokens: max_tokens,
|
||||||
|
topP: top_p,
|
||||||
|
},
|
||||||
|
safetySettings: [
|
||||||
|
{
|
||||||
|
category: "HARM_CATEGORY_HARASSMENT",
|
||||||
|
threshold: "BLOCK_ONLY_HIGH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||||
|
threshold: "BLOCK_ONLY_HIGH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
|
threshold: "BLOCK_ONLY_HIGH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
|
threshold: "BLOCK_ONLY_HIGH",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = `${googleUrl}/${GoogleMetas.ChatPath(
|
||||||
|
model,
|
||||||
|
)}?${urlParamApikeyName}=${googleApiKey}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
method: "POST",
|
||||||
|
url: stream
|
||||||
|
? baseUrl.replace("generateContent", "streamGenerateContent")
|
||||||
|
: baseUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
let existingTexts: string[] = [];
|
||||||
|
|
||||||
|
fetch(requestPayload.url, {
|
||||||
|
...requestPayload,
|
||||||
|
signal: timer.signal,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const reader = response?.body?.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let partialData = "";
|
||||||
|
|
||||||
|
return reader?.read().then(function processText({
|
||||||
|
done,
|
||||||
|
value,
|
||||||
|
}): Promise<any> {
|
||||||
|
if (done) {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(ensureProperEnding(partialData));
|
||||||
|
if (data && data[0].error) {
|
||||||
|
handlers.onError(new Error(data[0].error.message));
|
||||||
|
} else {
|
||||||
|
handlers.onError(new Error("Request failed"));
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
handlers.onError(new Error("Request failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Stream complete");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
partialData += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(ensureProperEnding(partialData));
|
||||||
|
|
||||||
|
const textArray = data.reduce(
|
||||||
|
(acc: string[], item: { candidates: any[] }) => {
|
||||||
|
const texts = item.candidates.map((candidate) =>
|
||||||
|
candidate.content.parts
|
||||||
|
.map((part: { text: any }) => part.text)
|
||||||
|
.join(""),
|
||||||
|
);
|
||||||
|
return acc.concat(texts);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (textArray.length > existingTexts.length) {
|
||||||
|
const deltaArray = textArray.slice(existingTexts.length);
|
||||||
|
existingTexts = textArray;
|
||||||
|
handlers.onProgress(deltaArray.join(""));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// console.log("[Response Animation] error: ", error,partialData);
|
||||||
|
// skip error message when parsing json
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.read().then(processText);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
): Promise<StandChatReponseMessage> {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
const res = await fetch(requestPayload.url, {
|
||||||
|
headers: {
|
||||||
|
...requestPayload.headers,
|
||||||
|
},
|
||||||
|
body: requestPayload.body,
|
||||||
|
method: requestPayload.method,
|
||||||
|
signal: timer.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
timer.clear();
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = parseResp(resJson);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableModels(
|
||||||
|
providerConfig: Record<SettingKeys, string>,
|
||||||
|
): Promise<ModelInfo[]> {
|
||||||
|
const { googleApiKey, googleUrl } = providerConfig;
|
||||||
|
const res = await fetch(`${googleUrl}/v1beta/models?key=${googleApiKey}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${googleApiKey}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data: ModelList = await res.json();
|
||||||
|
|
||||||
|
return data.models;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSideRequestHandler: ProviderTemplate["serverSideRequestHandler"] =
|
||||||
|
async (req, serverConfig) => {
|
||||||
|
const { googleUrl = GEMINI_BASE_URL } = serverConfig;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const path = `${req.nextUrl.pathname}`.replaceAll(
|
||||||
|
this.apiRouteRootName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", googleUrl);
|
||||||
|
|
||||||
|
const authResult = auth(req, serverConfig);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUrl = `${googleUrl}/${path}?key=${authResult.apiKey}`;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new NextResponse(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
113
app/client/providers/google/locale.ts
Normal file
113
app/client/providers/google/locale.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { getLocaleText } from "../../common";
|
||||||
|
|
||||||
|
export default getLocaleText<
|
||||||
|
{
|
||||||
|
ApiKey: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Placeholder: string;
|
||||||
|
};
|
||||||
|
Endpoint: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: string;
|
||||||
|
IllegalURL: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ApiVersion: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"en"
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
cn: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API 密钥",
|
||||||
|
SubTitle: "从 Google AI 获取您的 API 密钥",
|
||||||
|
Placeholder: "输入您的 Google AI Studio API 密钥",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "终端地址",
|
||||||
|
SubTitle: "示例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」结尾",
|
||||||
|
IllegalURL: "请输入一个完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVersion: {
|
||||||
|
Title: "API 版本(仅适用于 gemini-pro)",
|
||||||
|
SubTitle: "选择一个特定的 API 版本",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API Key",
|
||||||
|
SubTitle: "Obtain your API Key from Google AI",
|
||||||
|
Placeholder: "Enter your Google AI Studio API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Cannot end with '/'",
|
||||||
|
IllegalURL: "Please enter a complete available url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVersion: {
|
||||||
|
Title: "API Version (specific to gemini-pro)",
|
||||||
|
SubTitle: "Select a specific API version",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API kľúč",
|
||||||
|
SubTitle:
|
||||||
|
"Obísť obmedzenia prístupu heslom pomocou vlastného API kľúča Google AI Studio",
|
||||||
|
Placeholder: "API kľúč Google AI Studio",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Adresa koncového bodu",
|
||||||
|
SubTitle: "Príklad:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Nemôže končiť znakom „/“",
|
||||||
|
IllegalURL: "Zadajte úplnú dostupnú adresu URL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVersion: {
|
||||||
|
Title: "Verzia API (gemini-pro verzia API)",
|
||||||
|
SubTitle: "Vyberte špecifickú verziu časti",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API 金鑰",
|
||||||
|
SubTitle: "從 Google AI 取得您的 API 金鑰",
|
||||||
|
Placeholder: "輸入您的 Google AI Studio API 金鑰",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "終端地址",
|
||||||
|
SubTitle: "範例:",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」結尾",
|
||||||
|
IllegalURL: "請輸入一個完整可用的url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVersion: {
|
||||||
|
Title: "API 版本(僅適用於 gemini-pro)",
|
||||||
|
SubTitle: "選擇一個特定的 API 版本",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"en",
|
||||||
|
);
|
87
app/client/providers/google/utils.ts
Normal file
87
app/client/providers/google/utils.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { ServerConfig, getIP } from "../../common";
|
||||||
|
|
||||||
|
export const urlParamApikeyName = "key";
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
export const makeBearer = (s: string) => `Bearer ${s.trim()}`;
|
||||||
|
export const validString = (x?: string): x is string =>
|
||||||
|
Boolean(x && x.length > 0);
|
||||||
|
|
||||||
|
export function ensureProperEnding(str: string) {
|
||||||
|
if (str.startsWith("[") && !str.endsWith("]")) {
|
||||||
|
return str + "]";
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
let apiKey = req.nextUrl.searchParams.get(urlParamApikeyName);
|
||||||
|
|
||||||
|
const { hideUserApiKey, googleApiKey } = serverConfig;
|
||||||
|
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
|
if (hideUserApiKey && apiKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to access with your own api key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
console.log("[Auth] use user api key");
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (googleApiKey) {
|
||||||
|
console.log("[Auth] use system api key");
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
apiKey: googleApiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Auth] admin did not provide an api key");
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: `missing api key`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimer() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...controller,
|
||||||
|
clear: () => {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResp(res: any) {
|
||||||
|
if (res?.promptFeedback?.blockReason) {
|
||||||
|
// being blocked
|
||||||
|
throw new Error(
|
||||||
|
"Message is being blocked for reason: " + res.promptFeedback.blockReason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
res.candidates?.at(0)?.content?.parts?.at(0)?.text ||
|
||||||
|
res.error?.message ||
|
||||||
|
"",
|
||||||
|
};
|
||||||
|
}
|
20
app/client/providers/index.ts
Normal file
20
app/client/providers/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export {
|
||||||
|
default as NextChatProvider,
|
||||||
|
type NextChatProviderSettingKeys,
|
||||||
|
} from "@/app/client/providers/nextchat";
|
||||||
|
export {
|
||||||
|
default as GoogleProvider,
|
||||||
|
type GoogleProviderSettingKeys,
|
||||||
|
} from "@/app/client/providers/google";
|
||||||
|
export {
|
||||||
|
default as OpenAIProvider,
|
||||||
|
type OpenAIProviderSettingKeys,
|
||||||
|
} from "@/app/client/providers/openai";
|
||||||
|
export {
|
||||||
|
default as AnthropicProvider,
|
||||||
|
type AnthropicProviderSettingKeys,
|
||||||
|
} from "@/app/client/providers/anthropic";
|
||||||
|
export {
|
||||||
|
default as AzureProvider,
|
||||||
|
type AzureProviderSettingKeys,
|
||||||
|
} from "@/app/client/providers/azure";
|
89
app/client/providers/nextchat/config.ts
Normal file
89
app/client/providers/nextchat/config.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { SettingItem } from "../../common";
|
||||||
|
import { isVisionModel } from "@/app/utils";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
|
||||||
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
|
|
||||||
|
export const NextChatMetas = {
|
||||||
|
ChatPath: "v1/chat/completions",
|
||||||
|
UsagePath: "dashboard/billing/usage",
|
||||||
|
SubsPath: "dashboard/billing/subscription",
|
||||||
|
ListModelPath: "v1/models",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const preferredRegion: string | string[] = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export type SettingKeys = "accessCode";
|
||||||
|
|
||||||
|
export const defaultModal = "gpt-3.5-turbo";
|
||||||
|
|
||||||
|
export const models = [
|
||||||
|
defaultModal,
|
||||||
|
"gpt-3.5-turbo-0301",
|
||||||
|
"gpt-3.5-turbo-0613",
|
||||||
|
"gpt-3.5-turbo-1106",
|
||||||
|
"gpt-3.5-turbo-0125",
|
||||||
|
"gpt-3.5-turbo-16k",
|
||||||
|
"gpt-3.5-turbo-16k-0613",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-0314",
|
||||||
|
"gpt-4-0613",
|
||||||
|
"gpt-4-1106-preview",
|
||||||
|
"gpt-4-0125-preview",
|
||||||
|
"gpt-4-32k",
|
||||||
|
"gpt-4-32k-0314",
|
||||||
|
"gpt-4-32k-0613",
|
||||||
|
"gpt-4-turbo",
|
||||||
|
"gpt-4-turbo-preview",
|
||||||
|
"gpt-4-vision-preview",
|
||||||
|
"gpt-4-turbo-2024-04-09",
|
||||||
|
|
||||||
|
"gemini-1.0-pro",
|
||||||
|
"gemini-1.5-pro-latest",
|
||||||
|
"gemini-pro-vision",
|
||||||
|
|
||||||
|
"claude-instant-1.2",
|
||||||
|
"claude-2.0",
|
||||||
|
"claude-2.1",
|
||||||
|
"claude-3-sonnet-20240229",
|
||||||
|
"claude-3-opus-20240229",
|
||||||
|
"claude-3-haiku-20240307",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const modelConfigs = models.map((name) => ({
|
||||||
|
name,
|
||||||
|
displayName: name,
|
||||||
|
isVision: isVisionModel(name),
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: name === defaultModal,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const settingItems: SettingItem<SettingKeys>[] = [
|
||||||
|
{
|
||||||
|
name: "accessCode",
|
||||||
|
title: Locale.Auth.Title,
|
||||||
|
description: Locale.Auth.Tips,
|
||||||
|
placeholder: Locale.Auth.Input,
|
||||||
|
type: "input",
|
||||||
|
inputType: "password",
|
||||||
|
validators: ["required"],
|
||||||
|
},
|
||||||
|
];
|
348
app/client/providers/nextchat/index.ts
Normal file
348
app/client/providers/nextchat/index.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import {
|
||||||
|
modelConfigs,
|
||||||
|
settingItems,
|
||||||
|
SettingKeys,
|
||||||
|
NextChatMetas,
|
||||||
|
preferredRegion,
|
||||||
|
OPENAI_BASE_URL,
|
||||||
|
} from "./config";
|
||||||
|
import {
|
||||||
|
ChatHandlers,
|
||||||
|
getMessageTextContent,
|
||||||
|
InternalChatRequestPayload,
|
||||||
|
IProviderTemplate,
|
||||||
|
ServerConfig,
|
||||||
|
StandChatReponseMessage,
|
||||||
|
} from "../../common";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import { auth, authHeaderName, getHeaders, getTimer, parseResp } from "./utils";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export type NextChatProviderSettingKeys = SettingKeys;
|
||||||
|
|
||||||
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
|
|
||||||
|
export interface MultimodalContent {
|
||||||
|
type: "text" | "image_url";
|
||||||
|
text?: string;
|
||||||
|
image_url?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestMessage {
|
||||||
|
role: MessageRole;
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestPayload {
|
||||||
|
messages: {
|
||||||
|
role: "system" | "user" | "assistant";
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}[];
|
||||||
|
stream?: boolean;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
presence_penalty: number;
|
||||||
|
frequency_penalty: number;
|
||||||
|
top_p: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderTemplate = IProviderTemplate<
|
||||||
|
SettingKeys,
|
||||||
|
"azure",
|
||||||
|
typeof NextChatMetas
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default class NextChatProvider
|
||||||
|
implements IProviderTemplate<SettingKeys, "nextchat", typeof NextChatMetas>
|
||||||
|
{
|
||||||
|
apiRouteRootName: "/api/provider/nextchat" = "/api/provider/nextchat";
|
||||||
|
allowedApiMethods: (
|
||||||
|
| "POST"
|
||||||
|
| "GET"
|
||||||
|
| "OPTIONS"
|
||||||
|
| "PUT"
|
||||||
|
| "PATCH"
|
||||||
|
| "DELETE"
|
||||||
|
)[] = ["GET", "POST"];
|
||||||
|
|
||||||
|
runtime = "edge" as const;
|
||||||
|
preferredRegion = preferredRegion;
|
||||||
|
name = "nextchat" as const;
|
||||||
|
metas = NextChatMetas;
|
||||||
|
|
||||||
|
defaultModels = modelConfigs;
|
||||||
|
|
||||||
|
providerMeta = {
|
||||||
|
displayName: "NextChat",
|
||||||
|
settingItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||||
|
const { messages, isVisionModel, model, stream, modelConfig } = payload;
|
||||||
|
const {
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
max_tokens,
|
||||||
|
} = modelConfig;
|
||||||
|
|
||||||
|
const openAiMessages = messages.map((v) => ({
|
||||||
|
role: v.role,
|
||||||
|
content: isVisionModel ? v.content : getMessageTextContent(v),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages: openAiMessages,
|
||||||
|
stream,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
};
|
||||||
|
|
||||||
|
// add max_tokens to vision model
|
||||||
|
if (isVisionModel) {
|
||||||
|
requestPayload["max_tokens"] = Math.max(max_tokens, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: getHeaders(payload.providerConfig.accessCode!),
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
method: "POST",
|
||||||
|
url: [this.apiRouteRootName, NextChatMetas.ChatPath].join("/"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestOpenai(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const { baseUrl = OPENAI_BASE_URL, openaiOrgId } = serverConfig;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const authValue = req.headers.get(authHeaderName) ?? "";
|
||||||
|
|
||||||
|
const path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||||
|
this.apiRouteRootName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}/${path}`;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
[authHeaderName]: authValue,
|
||||||
|
...(openaiOrgId && {
|
||||||
|
"OpenAI-Organization": openaiOrgId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// Extract the OpenAI-Organization header from the response
|
||||||
|
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
|
||||||
|
|
||||||
|
// Check if serverConfig.openaiOrgId is defined and not an empty string
|
||||||
|
if (openaiOrgId && openaiOrgId.trim() !== "") {
|
||||||
|
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
|
||||||
|
console.log("[Org ID]", openaiOrganizationHeader);
|
||||||
|
} else {
|
||||||
|
console.log("[Org ID] is not set up.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
|
||||||
|
// Also, this is to prevent the header from being sent to the client
|
||||||
|
if (!openaiOrgId || openaiOrgId.trim() === "") {
|
||||||
|
newHeaders.delete("OpenAI-Organization");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||||
|
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||||
|
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||||
|
// The browser will try to decode the response with brotli and fail
|
||||||
|
newHeaders.delete("content-encoding");
|
||||||
|
|
||||||
|
return new NextResponse(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
fetchEventSource(requestPayload.url, {
|
||||||
|
...requestPayload,
|
||||||
|
fetch,
|
||||||
|
async onopen(res) {
|
||||||
|
timer.clear();
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log("[OpenAI] request response content type: ", contentType);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
const responseText = await res.clone().text();
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [];
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
handlers.onProgress(delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
handlers.onFinish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
handlers.onError(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: InternalChatRequestPayload<"accessCode">,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
): Promise<StandChatReponseMessage> {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
const res = await fetch(requestPayload.url, {
|
||||||
|
headers: {
|
||||||
|
...requestPayload.headers,
|
||||||
|
},
|
||||||
|
body: requestPayload.body,
|
||||||
|
method: requestPayload.method,
|
||||||
|
signal: timer.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
timer.clear();
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = parseResp(resJson);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSideRequestHandler: ProviderTemplate["serverSideRequestHandler"] =
|
||||||
|
async (req, config) => {
|
||||||
|
const { subpath } = req;
|
||||||
|
const ALLOWD_PATH = new Set(Object.values(NextChatMetas));
|
||||||
|
|
||||||
|
if (!ALLOWD_PATH.has(subpath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to request " + subpath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, config);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.requestOpenai(req, config);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
112
app/client/providers/nextchat/utils.ts
Normal file
112
app/client/providers/nextchat/utils.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { ServerConfig, getIP } from "../../common";
|
||||||
|
import md5 from "spark-md5";
|
||||||
|
|
||||||
|
export const ACCESS_CODE_PREFIX = "nk-";
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
export const authHeaderName = "Authorization";
|
||||||
|
|
||||||
|
export const makeBearer = (s: string) => `Bearer ${s.trim()}`;
|
||||||
|
|
||||||
|
export const validString = (x?: string): x is string =>
|
||||||
|
Boolean(x && x.length > 0);
|
||||||
|
|
||||||
|
export function prettyObject(msg: any) {
|
||||||
|
const obj = msg;
|
||||||
|
if (typeof msg !== "string") {
|
||||||
|
msg = JSON.stringify(msg, null, " ");
|
||||||
|
}
|
||||||
|
if (msg === "{}") {
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
if (msg.startsWith("```json")) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
return ["```json", msg, "```"].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimer() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...controller,
|
||||||
|
clear: () => {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeaders(accessCode: string) {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
[authHeaderName]: makeBearer(ACCESS_CODE_PREFIX + accessCode),
|
||||||
|
};
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResp(res: { choices: { message: { content: any } }[] }) {
|
||||||
|
return {
|
||||||
|
message: res.choices?.[0]?.message?.content ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseApiKey(req: NextRequest) {
|
||||||
|
const authToken = req.headers.get("Authorization") ?? "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessCode:
|
||||||
|
authToken.startsWith(ACCESS_CODE_PREFIX) &&
|
||||||
|
authToken.slice(ACCESS_CODE_PREFIX.length),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
// check if it is openai api key or user token
|
||||||
|
const { accessCode } = parseApiKey(req);
|
||||||
|
const { googleApiKey, apiKey, anthropicApiKey, azureApiKey, codes } =
|
||||||
|
serverConfig;
|
||||||
|
|
||||||
|
const hashedCode = md5.hash(accessCode || "").trim();
|
||||||
|
|
||||||
|
console.log("[Auth] allowed hashed codes: ", [...codes]);
|
||||||
|
console.log("[Auth] got access code:", accessCode);
|
||||||
|
console.log("[Auth] hashed access code:", hashedCode);
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
|
if (!codes.has(hashedCode)) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: !accessCode ? "empty access code" : "wrong access code",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemApiKey = googleApiKey || apiKey || anthropicApiKey || azureApiKey;
|
||||||
|
|
||||||
|
if (systemApiKey) {
|
||||||
|
console.log("[Auth] use system api key");
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
accessCode,
|
||||||
|
systemApiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Auth] admin did not provide an api key");
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: `Server internal error`,
|
||||||
|
};
|
||||||
|
}
|
214
app/client/providers/openai/config.ts
Normal file
214
app/client/providers/openai/config.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { SettingItem } from "../../common";
|
||||||
|
import Locale from "./locale";
|
||||||
|
|
||||||
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
|
|
||||||
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
|
|
||||||
|
export const preferredRegion: string | string[] = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OpenaiMetas = {
|
||||||
|
ChatPath: "v1/chat/completions",
|
||||||
|
UsagePath: "dashboard/billing/usage",
|
||||||
|
SubsPath: "dashboard/billing/subscription",
|
||||||
|
ListModelPath: "v1/models",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingKeys = "openaiUrl" | "openaiApiKey";
|
||||||
|
|
||||||
|
export const modelConfigs = [
|
||||||
|
{
|
||||||
|
name: "gpt-4o",
|
||||||
|
displayName: "gpt-4o",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo",
|
||||||
|
displayName: "gpt-3.5-turbo",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-0301",
|
||||||
|
displayName: "gpt-3.5-turbo-0301",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-0613",
|
||||||
|
displayName: "gpt-3.5-turbo-0613",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-1106",
|
||||||
|
displayName: "gpt-3.5-turbo-1106",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-0125",
|
||||||
|
displayName: "gpt-3.5-turbo-0125",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-16k",
|
||||||
|
displayName: "gpt-3.5-turbo-16k",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-3.5-turbo-16k-0613",
|
||||||
|
displayName: "gpt-3.5-turbo-16k-0613",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4",
|
||||||
|
displayName: "gpt-4",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-0314",
|
||||||
|
displayName: "gpt-4-0314",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-0613",
|
||||||
|
displayName: "gpt-4-0613",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-1106-preview",
|
||||||
|
displayName: "gpt-4-1106-preview",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-0125-preview",
|
||||||
|
displayName: "gpt-4-0125-preview",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-32k",
|
||||||
|
displayName: "gpt-4-32k",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-32k-0314",
|
||||||
|
displayName: "gpt-4-32k-0314",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-32k-0613",
|
||||||
|
displayName: "gpt-4-32k-0613",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-turbo",
|
||||||
|
displayName: "gpt-4-turbo",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: true,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-turbo-preview",
|
||||||
|
displayName: "gpt-4-turbo-preview",
|
||||||
|
isVision: false,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-vision-preview",
|
||||||
|
displayName: "gpt-4-vision-preview",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpt-4-turbo-2024-04-09",
|
||||||
|
displayName: "gpt-4-turbo-2024-04-09",
|
||||||
|
isVision: true,
|
||||||
|
isDefaultActive: false,
|
||||||
|
isDefaultSelected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const settingItems: (
|
||||||
|
defaultEndpoint: string,
|
||||||
|
) => SettingItem<SettingKeys>[] = (defaultEndpoint) => [
|
||||||
|
{
|
||||||
|
name: "openaiUrl",
|
||||||
|
title: Locale.Endpoint.Title,
|
||||||
|
description: Locale.Endpoint.SubTitle,
|
||||||
|
defaultValue: defaultEndpoint,
|
||||||
|
type: "input",
|
||||||
|
validators: [
|
||||||
|
"required",
|
||||||
|
async (v: any) => {
|
||||||
|
if (typeof v === "string" && v.endsWith("/")) {
|
||||||
|
return Locale.Endpoint.Error.EndWithBackslash;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof v === "string" &&
|
||||||
|
!v.startsWith(defaultEndpoint) &&
|
||||||
|
!v.startsWith("http")
|
||||||
|
) {
|
||||||
|
return Locale.Endpoint.SubTitle;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openaiApiKey",
|
||||||
|
title: Locale.ApiKey.Title,
|
||||||
|
description: Locale.ApiKey.SubTitle,
|
||||||
|
placeholder: Locale.ApiKey.Placeholder,
|
||||||
|
type: "input",
|
||||||
|
inputType: "password",
|
||||||
|
// validators: ["required"],
|
||||||
|
},
|
||||||
|
];
|
381
app/client/providers/openai/index.ts
Normal file
381
app/client/providers/openai/index.ts
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import {
|
||||||
|
ChatHandlers,
|
||||||
|
InternalChatRequestPayload,
|
||||||
|
IProviderTemplate,
|
||||||
|
ModelInfo,
|
||||||
|
getMessageTextContent,
|
||||||
|
ServerConfig,
|
||||||
|
} from "../../common";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import {
|
||||||
|
authHeaderName,
|
||||||
|
prettyObject,
|
||||||
|
parseResp,
|
||||||
|
auth,
|
||||||
|
getTimer,
|
||||||
|
getHeaders,
|
||||||
|
} from "./utils";
|
||||||
|
import {
|
||||||
|
modelConfigs,
|
||||||
|
settingItems,
|
||||||
|
SettingKeys,
|
||||||
|
OpenaiMetas,
|
||||||
|
ROLES,
|
||||||
|
OPENAI_BASE_URL,
|
||||||
|
preferredRegion,
|
||||||
|
} from "./config";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { ModelList } from "./type";
|
||||||
|
|
||||||
|
export type OpenAIProviderSettingKeys = SettingKeys;
|
||||||
|
|
||||||
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
|
|
||||||
|
export interface MultimodalContent {
|
||||||
|
type: "text" | "image_url";
|
||||||
|
text?: string;
|
||||||
|
image_url?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestMessage {
|
||||||
|
role: MessageRole;
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}
|
||||||
|
interface RequestPayload {
|
||||||
|
messages: {
|
||||||
|
role: "system" | "user" | "assistant";
|
||||||
|
content: string | MultimodalContent[];
|
||||||
|
}[];
|
||||||
|
stream?: boolean;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
presence_penalty: number;
|
||||||
|
frequency_penalty: number;
|
||||||
|
top_p: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderTemplate = IProviderTemplate<
|
||||||
|
SettingKeys,
|
||||||
|
"azure",
|
||||||
|
typeof OpenaiMetas
|
||||||
|
>;
|
||||||
|
|
||||||
|
class OpenAIProvider
|
||||||
|
implements IProviderTemplate<SettingKeys, "openai", typeof OpenaiMetas>
|
||||||
|
{
|
||||||
|
apiRouteRootName: "/api/provider/openai" = "/api/provider/openai";
|
||||||
|
allowedApiMethods: (
|
||||||
|
| "POST"
|
||||||
|
| "GET"
|
||||||
|
| "OPTIONS"
|
||||||
|
| "PUT"
|
||||||
|
| "PATCH"
|
||||||
|
| "DELETE"
|
||||||
|
)[] = ["GET", "POST"];
|
||||||
|
runtime = "edge" as const;
|
||||||
|
preferredRegion = preferredRegion;
|
||||||
|
|
||||||
|
name = "openai" as const;
|
||||||
|
metas = OpenaiMetas;
|
||||||
|
|
||||||
|
defaultModels = modelConfigs;
|
||||||
|
|
||||||
|
providerMeta = {
|
||||||
|
displayName: "OpenAI",
|
||||||
|
settingItems: settingItems(
|
||||||
|
`${this.apiRouteRootName}/${OpenaiMetas.ChatPath}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isVisionModel,
|
||||||
|
model,
|
||||||
|
stream,
|
||||||
|
modelConfig: {
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
max_tokens,
|
||||||
|
},
|
||||||
|
providerConfig: { openaiUrl },
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const openAiMessages = messages.map((v) => ({
|
||||||
|
role: v.role,
|
||||||
|
content: isVisionModel ? v.content : getMessageTextContent(v),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages: openAiMessages,
|
||||||
|
stream,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
presence_penalty,
|
||||||
|
frequency_penalty,
|
||||||
|
top_p,
|
||||||
|
};
|
||||||
|
|
||||||
|
// add max_tokens to vision model
|
||||||
|
if (isVisionModel) {
|
||||||
|
requestPayload["max_tokens"] = Math.max(max_tokens, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: getHeaders(payload.providerConfig.openaiApiKey),
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
method: "POST",
|
||||||
|
url: openaiUrl!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestOpenai(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const { baseUrl = OPENAI_BASE_URL, openaiOrgId } = serverConfig;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const authValue = req.headers.get(authHeaderName) ?? "";
|
||||||
|
|
||||||
|
const path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||||
|
this.apiRouteRootName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}/${path}`;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
[authHeaderName]: authValue,
|
||||||
|
...(openaiOrgId && {
|
||||||
|
"OpenAI-Organization": openaiOrgId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// Extract the OpenAI-Organization header from the response
|
||||||
|
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
|
||||||
|
|
||||||
|
// Check if serverConfig.openaiOrgId is defined and not an empty string
|
||||||
|
if (openaiOrgId && openaiOrgId.trim() !== "") {
|
||||||
|
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
|
||||||
|
console.log("[Org ID]", openaiOrganizationHeader);
|
||||||
|
} else {
|
||||||
|
console.log("[Org ID] is not set up.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
|
||||||
|
// Also, this is to prevent the header from being sent to the client
|
||||||
|
if (!openaiOrgId || openaiOrgId.trim() === "") {
|
||||||
|
newHeaders.delete("OpenAI-Organization");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||||
|
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||||
|
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||||
|
// The browser will try to decode the response with brotli and fail
|
||||||
|
newHeaders.delete("content-encoding");
|
||||||
|
|
||||||
|
return new NextResponse(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
const res = await fetch(requestPayload.url, {
|
||||||
|
headers: {
|
||||||
|
...requestPayload.headers,
|
||||||
|
},
|
||||||
|
body: requestPayload.body,
|
||||||
|
method: requestPayload.method,
|
||||||
|
signal: timer.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
timer.clear();
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = parseResp(resJson);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamChat(
|
||||||
|
payload: InternalChatRequestPayload<SettingKeys>,
|
||||||
|
handlers: ChatHandlers,
|
||||||
|
fetch: typeof window.fetch,
|
||||||
|
) {
|
||||||
|
const requestPayload = this.formatChatPayload(payload);
|
||||||
|
|
||||||
|
const timer = getTimer();
|
||||||
|
|
||||||
|
fetchEventSource(requestPayload.url, {
|
||||||
|
...requestPayload,
|
||||||
|
fetch,
|
||||||
|
async onopen(res) {
|
||||||
|
timer.clear();
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log("[OpenAI] request response content type: ", contentType);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
const responseText = await res.clone().text();
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [];
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return handlers.onFlash(responseText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
handlers.onProgress(delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
handlers.onFinish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
handlers.onError(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableModels(
|
||||||
|
providerConfig: Record<SettingKeys, string>,
|
||||||
|
): Promise<ModelInfo[]> {
|
||||||
|
const { openaiApiKey, openaiUrl } = providerConfig;
|
||||||
|
const res = await fetch(`${openaiUrl}/v1/models`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${openaiApiKey}`,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data: ModelList = await res.json();
|
||||||
|
|
||||||
|
return data.data.map((o) => ({
|
||||||
|
name: o.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSideRequestHandler: ProviderTemplate["serverSideRequestHandler"] =
|
||||||
|
async (req, config) => {
|
||||||
|
const { subpath } = req;
|
||||||
|
const ALLOWD_PATH = new Set(Object.values(OpenaiMetas));
|
||||||
|
|
||||||
|
if (!ALLOWD_PATH.has(subpath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to request " + subpath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, config);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.requestOpenai(req, config);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAIProvider;
|
100
app/client/providers/openai/locale.ts
Normal file
100
app/client/providers/openai/locale.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { getLocaleText } from "../../common/locale";
|
||||||
|
|
||||||
|
export default getLocaleText<
|
||||||
|
{
|
||||||
|
ApiKey: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Placeholder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: string;
|
||||||
|
SubTitle: string;
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"en"
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
cn: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API Key",
|
||||||
|
SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制",
|
||||||
|
Placeholder: "OpenAI API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」结尾",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "OpenAI API Key",
|
||||||
|
SubTitle: "User custom OpenAI Api Key",
|
||||||
|
Placeholder: "sk-xxx",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "OpenAI Endpoint",
|
||||||
|
SubTitle: "Must starts with http(s):// or use /api/openai as default",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Cannot end with '/'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Chave API OpenAI",
|
||||||
|
SubTitle: "Usar Chave API OpenAI personalizada",
|
||||||
|
Placeholder: "sk-xxx",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint OpenAI",
|
||||||
|
SubTitle: "Deve começar com http(s):// ou usar /api/openai como padrão",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Não é possível terminar com '/'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API kľúč OpenAI",
|
||||||
|
SubTitle: "Použiť vlastný API kľúč OpenAI",
|
||||||
|
Placeholder: "sk-xxx",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Koncový bod OpenAI",
|
||||||
|
SubTitle:
|
||||||
|
"Musí začínať http(s):// alebo použiť /api/openai ako predvolený",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "Nemôže končiť znakom „/“",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API Key",
|
||||||
|
SubTitle: "使用自定義 OpenAI Key 繞過密碼存取限制",
|
||||||
|
Placeholder: "OpenAI API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "介面(Endpoint) 地址",
|
||||||
|
SubTitle: "除預設地址外,必須包含 http(s)://",
|
||||||
|
Error: {
|
||||||
|
EndWithBackslash: "不能以「/」結尾",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"en",
|
||||||
|
);
|
18
app/client/providers/openai/type.ts
Normal file
18
app/client/providers/openai/type.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export interface ModelList {
|
||||||
|
object: "list";
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
object: "model";
|
||||||
|
created: number;
|
||||||
|
owned_by: "system" | "openai-internal";
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAIListModelResponse {
|
||||||
|
object: string;
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
root: string;
|
||||||
|
}>;
|
||||||
|
}
|
103
app/client/providers/openai/utils.ts
Normal file
103
app/client/providers/openai/utils.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { ServerConfig, getIP } from "../../common";
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
export const authHeaderName = "Authorization";
|
||||||
|
|
||||||
|
const makeBearer = (s: string) => `Bearer ${s.trim()}`;
|
||||||
|
|
||||||
|
const validString = (x?: string): x is string => Boolean(x && x.length > 0);
|
||||||
|
|
||||||
|
function parseApiKey(bearToken: string) {
|
||||||
|
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyObject(msg: any) {
|
||||||
|
const obj = msg;
|
||||||
|
if (typeof msg !== "string") {
|
||||||
|
msg = JSON.stringify(msg, null, " ");
|
||||||
|
}
|
||||||
|
if (msg === "{}") {
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
if (msg.startsWith("```json")) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
return ["```json", msg, "```"].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseResp(res: { choices: { message: { content: any } }[] }) {
|
||||||
|
return {
|
||||||
|
message: res.choices?.[0]?.message?.content ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(req: NextRequest, serverConfig: ServerConfig) {
|
||||||
|
const { hideUserApiKey, apiKey: systemApiKey } = serverConfig;
|
||||||
|
const authToken = req.headers.get(authHeaderName) ?? "";
|
||||||
|
|
||||||
|
const { apiKey } = parseApiKey(authToken);
|
||||||
|
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
|
if (hideUserApiKey && apiKey) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: "you are not allowed to access with your own api key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
console.log("[Auth] use user api key");
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemApiKey) {
|
||||||
|
console.log("[Auth] use system api key");
|
||||||
|
req.headers.set(authHeaderName, `Bearer ${systemApiKey}`);
|
||||||
|
} else {
|
||||||
|
console.log("[Auth] admin did not provide an api key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimer() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...controller,
|
||||||
|
clear: () => {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeaders(openaiApiKey?: string) {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validString(openaiApiKey)) {
|
||||||
|
headers[authHeaderName] = makeBearer(openaiApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
@@ -21,7 +21,7 @@ type Groups = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ActionsBarProps {
|
export interface ActionsBarProps {
|
||||||
actionsShema: Action[];
|
actionsSchema: Action[];
|
||||||
onSelect?: (id: string) => void;
|
onSelect?: (id: string) => void;
|
||||||
selected?: string;
|
selected?: string;
|
||||||
groups: string[][] | Groups;
|
groups: string[][] | Groups;
|
||||||
@@ -30,7 +30,7 @@ export interface ActionsBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionsBar(props: ActionsBarProps) {
|
export default function ActionsBar(props: ActionsBarProps) {
|
||||||
const { actionsShema, onSelect, selected, groups, className, inMobile } =
|
const { actionsSchema, onSelect, selected, groups, className, inMobile } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
const handlerClick =
|
const handlerClick =
|
||||||
@@ -53,7 +53,7 @@ export default function ActionsBar(props: ActionsBarProps) {
|
|||||||
const content = internalGroup.reduce((res, group, ind, arr) => {
|
const content = internalGroup.reduce((res, group, ind, arr) => {
|
||||||
res.push(
|
res.push(
|
||||||
...group.map((i) => {
|
...group.map((i) => {
|
||||||
const action = actionsShema.find((a) => a.id === i);
|
const action = actionsSchema.find((a) => a.id === i);
|
||||||
if (!action) {
|
if (!action) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,8 @@ type Error =
|
|||||||
error: false;
|
error: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Validate = (v: any) => Error | Promise<Error>;
|
||||||
|
|
||||||
export interface ListItemProps {
|
export interface ListItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
@@ -44,7 +46,7 @@ export interface ListItemProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
nextline?: boolean;
|
nextline?: boolean;
|
||||||
validator?: (v: any) => Error | Promise<Error>;
|
validator?: Validate | Validate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListContext = createContext<
|
export const ListContext = createContext<
|
||||||
@@ -92,7 +94,15 @@ export function ListItem(props: ListItemProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleValidate = useCallback((v: any) => {
|
const handleValidate = useCallback((v: any) => {
|
||||||
const insideValidator = validator || (() => {});
|
let insideValidator;
|
||||||
|
if (!validator) {
|
||||||
|
insideValidator = () => {};
|
||||||
|
} else if (Array.isArray(validator)) {
|
||||||
|
insideValidator = (v: any) =>
|
||||||
|
Promise.race(validator.map((validate) => validate(v)));
|
||||||
|
} else {
|
||||||
|
insideValidator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
Promise.resolve(insideValidator(v)).then((result) => {
|
Promise.resolve(insideValidator(v)).then((result) => {
|
||||||
if (result && result.error) {
|
if (result && result.error) {
|
||||||
|
@@ -59,9 +59,10 @@ import {
|
|||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
compressImage,
|
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
import { compressImage } from "@/app/utils/chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import { ChatControllerPool } from "../client/controller";
|
import { ChatControllerPool } from "../client/controller";
|
||||||
@@ -1088,6 +1089,7 @@ function _Chat() {
|
|||||||
if (payload.url) {
|
if (payload.url) {
|
||||||
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
||||||
}
|
}
|
||||||
|
accessStore.update((access) => (access.useCustomConfig = true));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import tauriConfig from "../../src-tauri/tauri.conf.json";
|
import tauriConfig from "../../src-tauri/tauri.conf.json";
|
||||||
|
import { DEFAULT_INPUT_TEMPLATE } from "../constant";
|
||||||
|
|
||||||
export const getBuildConfig = () => {
|
export const getBuildConfig = () => {
|
||||||
if (typeof process === "undefined") {
|
if (typeof process === "undefined") {
|
||||||
@@ -38,6 +39,7 @@ export const getBuildConfig = () => {
|
|||||||
...commitInfo,
|
...commitInfo,
|
||||||
buildMode,
|
buildMode,
|
||||||
isApp,
|
isApp,
|
||||||
|
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -34,6 +34,9 @@ declare global {
|
|||||||
|
|
||||||
// google tag manager
|
// google tag manager
|
||||||
GTM_ID?: string;
|
GTM_ID?: string;
|
||||||
|
|
||||||
|
// custom template for preprocessing user input
|
||||||
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,25 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function getApiKey(keys?: string) {
|
||||||
|
if (!keys) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const apiKeyEnvVar = keys;
|
||||||
|
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
|
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||||
|
const apiKey = apiKeys[randomIndex];
|
||||||
|
if (apiKey) {
|
||||||
|
console.log(
|
||||||
|
`[Server Config] using ${randomIndex + 1} of ${
|
||||||
|
apiKeys.length
|
||||||
|
} api key - ${apiKey}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
export const getServerSideConfig = () => {
|
export const getServerSideConfig = () => {
|
||||||
if (typeof process === "undefined") {
|
if (typeof process === "undefined") {
|
||||||
throw Error(
|
throw Error(
|
||||||
@@ -74,34 +96,34 @@ export const getServerSideConfig = () => {
|
|||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||||
const apiKey = apiKeys[randomIndex];
|
// const apiKey = apiKeys[randomIndex];
|
||||||
console.log(
|
// console.log(
|
||||||
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||||
);
|
// );
|
||||||
|
|
||||||
const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split(
|
const allowedWebDevEndpoints = (
|
||||||
",",
|
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
|
||||||
);
|
).split(",");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseUrl: process.env.BASE_URL,
|
baseUrl: process.env.BASE_URL,
|
||||||
apiKey,
|
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
isAzure,
|
isAzure,
|
||||||
azureUrl: process.env.AZURE_URL,
|
azureUrl: process.env.AZURE_URL,
|
||||||
azureApiKey: process.env.AZURE_API_KEY,
|
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
||||||
azureApiVersion: process.env.AZURE_API_VERSION,
|
azureApiVersion: process.env.AZURE_API_VERSION,
|
||||||
|
|
||||||
isGoogle,
|
isGoogle,
|
||||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
googleApiKey: getApiKey(process.env.GOOGLE_API_KEY),
|
||||||
googleUrl: process.env.GOOGLE_URL,
|
googleUrl: process.env.GOOGLE_URL,
|
||||||
|
|
||||||
isAnthropic,
|
isAnthropic,
|
||||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
anthropicApiKey: getApiKey(process.env.ANTHROPIC_API_KEY),
|
||||||
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
|
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
|
||||||
anthropicUrl: process.env.ANTHROPIC_URL,
|
anthropicUrl: process.env.ANTHROPIC_URL,
|
||||||
|
|
||||||
@@ -120,6 +142,6 @@ export const getServerSideConfig = () => {
|
|||||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||||
customModels,
|
customModels,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
whiteWebDevEndpoints,
|
allowedWebDevEndpoints,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -47,6 +47,7 @@ export enum StoreKey {
|
|||||||
Prompt = "prompt-store",
|
Prompt = "prompt-store",
|
||||||
Update = "chat-update",
|
Update = "chat-update",
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
|
Provider = "provider",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||||
@@ -106,7 +107,6 @@ export const Azure = {
|
|||||||
export const Google = {
|
export const Google = {
|
||||||
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
||||||
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
||||||
VisionChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||||
@@ -135,8 +135,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
|||||||
"gpt-4-turbo": "2023-12",
|
"gpt-4-turbo": "2023-12",
|
||||||
"gpt-4-turbo-2024-04-09": "2023-12",
|
"gpt-4-turbo-2024-04-09": "2023-12",
|
||||||
"gpt-4-turbo-preview": "2023-12",
|
"gpt-4-turbo-preview": "2023-12",
|
||||||
"gpt-4-1106-preview": "2023-04",
|
"gpt-4o": "2023-10",
|
||||||
"gpt-4-0125-preview": "2023-12",
|
"gpt-4o-2024-05-13": "2023-10",
|
||||||
"gpt-4-vision-preview": "2023-04",
|
"gpt-4-vision-preview": "2023-04",
|
||||||
// After improvements,
|
// After improvements,
|
||||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||||
@@ -146,22 +146,16 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
|||||||
|
|
||||||
const openaiModels = [
|
const openaiModels = [
|
||||||
"gpt-3.5-turbo",
|
"gpt-3.5-turbo",
|
||||||
"gpt-3.5-turbo-0301",
|
|
||||||
"gpt-3.5-turbo-0613",
|
|
||||||
"gpt-3.5-turbo-1106",
|
"gpt-3.5-turbo-1106",
|
||||||
"gpt-3.5-turbo-0125",
|
"gpt-3.5-turbo-0125",
|
||||||
"gpt-3.5-turbo-16k",
|
|
||||||
"gpt-3.5-turbo-16k-0613",
|
|
||||||
"gpt-4",
|
"gpt-4",
|
||||||
"gpt-4-0314",
|
|
||||||
"gpt-4-0613",
|
"gpt-4-0613",
|
||||||
"gpt-4-1106-preview",
|
|
||||||
"gpt-4-0125-preview",
|
|
||||||
"gpt-4-32k",
|
"gpt-4-32k",
|
||||||
"gpt-4-32k-0314",
|
|
||||||
"gpt-4-32k-0613",
|
"gpt-4-32k-0613",
|
||||||
"gpt-4-turbo",
|
"gpt-4-turbo",
|
||||||
"gpt-4-turbo-preview",
|
"gpt-4-turbo-preview",
|
||||||
|
"gpt-4o",
|
||||||
|
"gpt-4o-2024-05-13",
|
||||||
"gpt-4-vision-preview",
|
"gpt-4-vision-preview",
|
||||||
"gpt-4-turbo-2024-04-09",
|
"gpt-4-turbo-2024-04-09",
|
||||||
];
|
];
|
||||||
@@ -169,6 +163,7 @@ const openaiModels = [
|
|||||||
const googleModels = [
|
const googleModels = [
|
||||||
"gemini-1.0-pro",
|
"gemini-1.0-pro",
|
||||||
"gemini-1.5-pro-latest",
|
"gemini-1.5-pro-latest",
|
||||||
|
"gemini-1.5-flash-latest",
|
||||||
"gemini-pro-vision",
|
"gemini-pro-vision",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -215,7 +210,7 @@ export const CHAT_PAGE_SIZE = 15;
|
|||||||
export const MAX_RENDER_MSG_COUNT = 45;
|
export const MAX_RENDER_MSG_COUNT = 45;
|
||||||
|
|
||||||
// some famous webdav endpoints
|
// some famous webdav endpoints
|
||||||
export const internalWhiteWebDavEndpoints = [
|
export const internalAllowedWebDavEndpoints = [
|
||||||
"https://dav.jianguoyun.com/dav/",
|
"https://dav.jianguoyun.com/dav/",
|
||||||
"https://dav.dropdav.com/",
|
"https://dav.dropdav.com/",
|
||||||
"https://dav.box.com/dav",
|
"https://dav.box.com/dav",
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
ModelType,
|
ModelType,
|
||||||
} from "@/app/store";
|
} from "@/app/store";
|
||||||
import Locale from "@/app/locales";
|
import Locale from "@/app/locales";
|
||||||
import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
|
import { showConfirm } from "@/app/components/ui-lib";
|
||||||
import {
|
import {
|
||||||
CHAT_PAGE_SIZE,
|
CHAT_PAGE_SIZE,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
@@ -25,7 +25,6 @@ import ChatInputPanel, {
|
|||||||
ChatInputPanelInstance,
|
ChatInputPanelInstance,
|
||||||
} from "./components/ChatInputPanel";
|
} from "./components/ChatInputPanel";
|
||||||
import ChatMessagePanel, { RenderMessage } from "./components/ChatMessagePanel";
|
import ChatMessagePanel, { RenderMessage } from "./components/ChatMessagePanel";
|
||||||
import { useAllModels } from "@/app/utils/hooks";
|
|
||||||
import useRows from "@/app/hooks/useRows";
|
import useRows from "@/app/hooks/useRows";
|
||||||
import SessionConfigModel from "./components/SessionConfigModal";
|
import SessionConfigModel from "./components/SessionConfigModal";
|
||||||
import useScrollToBottom from "@/app/hooks/useScrollToBottom";
|
import useScrollToBottom from "@/app/hooks/useScrollToBottom";
|
||||||
|
@@ -31,7 +31,7 @@ export interface MessageActionsProps {
|
|||||||
setShowPromptModal?: (value: boolean) => void;
|
setShowPromptModal?: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const genActionsShema = (
|
const genActionsSchema = (
|
||||||
message: RenderMessage,
|
message: RenderMessage,
|
||||||
{
|
{
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -272,7 +272,7 @@ export default function MessageActions(props: MessageActionsProps) {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<ActionsBar
|
<ActionsBar
|
||||||
actionsShema={genActionsShema(message, {
|
actionsSchema={genActionsSchema(message, {
|
||||||
onCopy,
|
onCopy,
|
||||||
onDelete,
|
onDelete,
|
||||||
onPinMessage,
|
onPinMessage,
|
||||||
|
@@ -51,7 +51,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
>
|
>
|
||||||
<ActionsBar
|
<ActionsBar
|
||||||
inMobile={isMobileScreen}
|
inMobile={isMobileScreen}
|
||||||
actionsShema={[
|
actionsSchema={[
|
||||||
{
|
{
|
||||||
id: Path.Masks,
|
id: Path.Masks,
|
||||||
icons: {
|
icons: {
|
||||||
|
5
app/global.d.ts
vendored
5
app/global.d.ts
vendored
@@ -21,10 +21,13 @@ declare interface Window {
|
|||||||
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
||||||
writeTextFile(path: string, data: string): Promise<void>;
|
writeTextFile(path: string, data: string): Promise<void>;
|
||||||
};
|
};
|
||||||
notification:{
|
notification: {
|
||||||
requestPermission(): Promise<Permission>;
|
requestPermission(): Promise<Permission>;
|
||||||
isPermissionGranted(): Promise<boolean>;
|
isPermissionGranted(): Promise<boolean>;
|
||||||
sendNotification(options: string | Options): void;
|
sendNotification(options: string | Options): void;
|
||||||
};
|
};
|
||||||
|
http: {
|
||||||
|
fetch: typeof window.fetch;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { compressImage, isVisionModel } from "@/app/utils";
|
import { isVisionModel } from "@/app/utils";
|
||||||
|
import { compressImage } from "@/app/utils/chat";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useChatStore } from "../store/chat";
|
import { useChatStore } from "../store/chat";
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { compressImage } from "@/app/utils";
|
import { compressImage } from "@/app/utils/chat";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
|
|
||||||
interface UseUploadImageOptions {
|
interface UseUploadImageOptions {
|
||||||
|
@@ -4,6 +4,9 @@ import { SubmitKey } from "../store/config";
|
|||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
|
||||||
const cn = {
|
const cn = {
|
||||||
|
Provider: {
|
||||||
|
// OPENAI_DISPLAY_NAME: 'OpenAI'
|
||||||
|
},
|
||||||
WIP: "该功能仍在开发中……",
|
WIP: "该功能仍在开发中……",
|
||||||
Error: {
|
Error: {
|
||||||
Unauthorized: isApp
|
Unauthorized: isApp
|
||||||
|
@@ -317,7 +317,7 @@ const en: LocaleType = {
|
|||||||
|
|
||||||
Endpoint: {
|
Endpoint: {
|
||||||
Title: "OpenAI Endpoint",
|
Title: "OpenAI Endpoint",
|
||||||
SubTitle: "Must starts with http(s):// or use /api/openai as default",
|
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Azure: {
|
Azure: {
|
||||||
|
@@ -21,6 +21,8 @@ import { estimateTokenLength } from "../utils/token";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
||||||
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
|
import { useAccessStore } from "./access";
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -104,9 +106,19 @@ function createEmptySession(): ChatSession {
|
|||||||
function getSummarizeModel(currentModel: string) {
|
function getSummarizeModel(currentModel: string) {
|
||||||
// if it is using gpt-* models, force to use 3.5 to summarize
|
// if it is using gpt-* models, force to use 3.5 to summarize
|
||||||
if (currentModel.startsWith("gpt")) {
|
if (currentModel.startsWith("gpt")) {
|
||||||
return SUMMARIZE_MODEL;
|
const configStore = useAppConfig.getState();
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
const allModel = collectModelsWithDefaultModel(
|
||||||
|
configStore.models,
|
||||||
|
[configStore.customModels, accessStore.customModels].join(","),
|
||||||
|
accessStore.defaultModel,
|
||||||
|
);
|
||||||
|
const summarizeModel = allModel.find(
|
||||||
|
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||||
|
);
|
||||||
|
return summarizeModel?.name ?? currentModel;
|
||||||
}
|
}
|
||||||
if (currentModel.startsWith("gemini-pro")) {
|
if (currentModel.startsWith("gemini")) {
|
||||||
return GEMINI_SUMMARIZE_MODEL;
|
return GEMINI_SUMMARIZE_MODEL;
|
||||||
}
|
}
|
||||||
return currentModel;
|
return currentModel;
|
||||||
@@ -433,14 +445,13 @@ export const useChatStore = createPersistStore(
|
|||||||
getMemoryPrompt() {
|
getMemoryPrompt() {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
|
|
||||||
return {
|
if (session.memoryPrompt.length) {
|
||||||
role: "system",
|
return {
|
||||||
content:
|
role: "system",
|
||||||
session.memoryPrompt.length > 0
|
content: Locale.Store.Prompt.History(session.memoryPrompt),
|
||||||
? Locale.Store.Prompt.History(session.memoryPrompt)
|
date: "",
|
||||||
: "",
|
} as ChatMessage;
|
||||||
date: "",
|
}
|
||||||
} as ChatMessage;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getMessagesWithMemory() {
|
getMessagesWithMemory() {
|
||||||
@@ -476,16 +487,15 @@ export const useChatStore = createPersistStore(
|
|||||||
systemPrompts.at(0)?.content ?? "empty",
|
systemPrompts.at(0)?.content ?? "empty",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const memoryPrompt = get().getMemoryPrompt();
|
||||||
// long term memory
|
// long term memory
|
||||||
const shouldSendLongTermMemory =
|
const shouldSendLongTermMemory =
|
||||||
modelConfig.sendMemory &&
|
modelConfig.sendMemory &&
|
||||||
session.memoryPrompt &&
|
session.memoryPrompt &&
|
||||||
session.memoryPrompt.length > 0 &&
|
session.memoryPrompt.length > 0 &&
|
||||||
session.lastSummarizeIndex > clearContextIndex;
|
session.lastSummarizeIndex > clearContextIndex;
|
||||||
const longTermMemoryPrompts = shouldSendLongTermMemory
|
const longTermMemoryPrompts =
|
||||||
? [get().getMemoryPrompt()]
|
shouldSendLongTermMemory && memoryPrompt ? [memoryPrompt] : [];
|
||||||
: [];
|
|
||||||
const longTermMemoryStartIndex = session.lastSummarizeIndex;
|
const longTermMemoryStartIndex = session.lastSummarizeIndex;
|
||||||
|
|
||||||
// short term memory
|
// short term memory
|
||||||
@@ -610,9 +620,11 @@ export const useChatStore = createPersistStore(
|
|||||||
Math.max(0, n - modelConfig.historyMessageCount),
|
Math.max(0, n - modelConfig.historyMessageCount),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const memoryPrompt = get().getMemoryPrompt();
|
||||||
// add memory prompt
|
if (memoryPrompt) {
|
||||||
toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
|
// add memory prompt
|
||||||
|
toBeSummarizedMsgs.unshift(memoryPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
const lastSummarizeIndex = session.messages.length;
|
const lastSummarizeIndex = session.messages.length;
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@ export const ThemeConfig = {
|
|||||||
title: "Dark model",
|
title: "Dark model",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const config = getClientConfig();
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
lastUpdate: Date.now(), // timestamp, to merge state
|
lastUpdate: Date.now(), // timestamp, to merge state
|
||||||
@@ -49,7 +50,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
avatar: "1f603",
|
avatar: "1f603",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: !!getClientConfig()?.isApp,
|
tightBorder: !!config?.isApp,
|
||||||
sendPreviewBubble: true,
|
sendPreviewBubble: true,
|
||||||
enableAutoGenerateTitle: true,
|
enableAutoGenerateTitle: true,
|
||||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||||
@@ -75,7 +76,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
historyMessageCount: 4,
|
historyMessageCount: 4,
|
||||||
compressMessageLengthThreshold: 1000,
|
compressMessageLengthThreshold: 1000,
|
||||||
enableInjectSystemPrompts: true,
|
enableInjectSystemPrompts: true,
|
||||||
template: DEFAULT_INPUT_TEMPLATE,
|
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ export const useAppConfig = createPersistStore(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Config,
|
name: StoreKey.Config,
|
||||||
version: 3.8,
|
version: 3.9,
|
||||||
migrate(persistedState, version) {
|
migrate(persistedState, version) {
|
||||||
const state = persistedState as ChatConfig;
|
const state = persistedState as ChatConfig;
|
||||||
|
|
||||||
@@ -182,6 +183,13 @@ export const useAppConfig = createPersistStore(
|
|||||||
state.lastUpdate = Date.now();
|
state.lastUpdate = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 3.9) {
|
||||||
|
state.modelConfig.template =
|
||||||
|
state.modelConfig.template !== DEFAULT_INPUT_TEMPLATE
|
||||||
|
? state.modelConfig.template
|
||||||
|
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||||
|
}
|
||||||
|
|
||||||
return state as any;
|
return state as any;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
137
app/store/provider.ts
Normal file
137
app/store/provider.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
ProviderClient,
|
||||||
|
NextChatProvider,
|
||||||
|
createProvider,
|
||||||
|
Provider,
|
||||||
|
Model,
|
||||||
|
} from "@/app/client";
|
||||||
|
// import { getClientConfig } from "../config/client";
|
||||||
|
import { StoreKey } from "../constant";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
|
const firstUpdate = Date.now();
|
||||||
|
|
||||||
|
function getDefaultConfig() {
|
||||||
|
const providers = Object.values(ProviderClient.ProviderTemplates)
|
||||||
|
.filter((t) => !(t instanceof NextChatProvider))
|
||||||
|
.map((t) => createProvider(t, true));
|
||||||
|
|
||||||
|
const initProvider = providers[0];
|
||||||
|
|
||||||
|
const currentModel =
|
||||||
|
initProvider.models.find((m) => m.isDefaultSelected) ||
|
||||||
|
initProvider.models[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastUpdate: firstUpdate, // timestamp, to merge state
|
||||||
|
|
||||||
|
currentModel: currentModel.name,
|
||||||
|
currentProvider: initProvider.name,
|
||||||
|
|
||||||
|
providers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProvidersConfig = ReturnType<typeof getDefaultConfig>;
|
||||||
|
|
||||||
|
export const useProviders = createPersistStore(
|
||||||
|
{ ...getDefaultConfig() },
|
||||||
|
(set, get) => {
|
||||||
|
const methods = {
|
||||||
|
reset() {
|
||||||
|
set(() => getDefaultConfig());
|
||||||
|
},
|
||||||
|
|
||||||
|
addProvider(provider: Provider) {
|
||||||
|
set(() => ({
|
||||||
|
providers: [...get().providers, provider],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProvider(provider: Provider) {
|
||||||
|
set(() => ({
|
||||||
|
providers: [
|
||||||
|
...get().providers.filter((p) => p.name !== provider.name),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProvider(provider: Provider) {
|
||||||
|
set(() => ({
|
||||||
|
providers: get().providers.map((p) =>
|
||||||
|
p.name === provider.name ? provider : p,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getProvider(providerName: string) {
|
||||||
|
return get().providers.find((p) => p.name === providerName);
|
||||||
|
},
|
||||||
|
|
||||||
|
addModel(
|
||||||
|
model: Omit<Model, "providerTemplateName" | "customized">,
|
||||||
|
provider: Provider,
|
||||||
|
) {
|
||||||
|
const newModel: Model = {
|
||||||
|
...model,
|
||||||
|
providerTemplateName: provider.providerTemplateName,
|
||||||
|
customized: true,
|
||||||
|
};
|
||||||
|
return methods.updateProvider({
|
||||||
|
...provider,
|
||||||
|
models: [...provider.models, newModel],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteModel(model: Model, provider: Provider) {
|
||||||
|
return methods.updateProvider({
|
||||||
|
...provider,
|
||||||
|
models: provider.models.filter((m) => m.name !== model.name),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateModel(model: Model, provider: Provider) {
|
||||||
|
return methods.updateProvider({
|
||||||
|
...provider,
|
||||||
|
models: provider.models.map((m) =>
|
||||||
|
m.name === model.name ? model : m,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
switchModel(model: Model, provider: Provider) {
|
||||||
|
set(() => ({
|
||||||
|
currentModel: model.name,
|
||||||
|
currentProvider: provider.name,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getModel(
|
||||||
|
modelName: string,
|
||||||
|
providerName: string,
|
||||||
|
): (Model & { providerName: string }) | undefined {
|
||||||
|
const provider = methods.getProvider(providerName);
|
||||||
|
const model = provider?.models.find((m) => m.name === modelName);
|
||||||
|
return model
|
||||||
|
? {
|
||||||
|
...model,
|
||||||
|
providerName: provider!.name,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
allModels() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return methods;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: StoreKey.Provider,
|
||||||
|
version: 1.0,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
const state = persistedState as ProvidersConfig;
|
||||||
|
|
||||||
|
return state as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
@@ -97,11 +97,20 @@ export const useSyncStore = createPersistStore(
|
|||||||
const client = this.getClient();
|
const client = this.getClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const remoteState = JSON.parse(
|
const remoteState = await client.get(config.username);
|
||||||
await client.get(config.username),
|
if (!remoteState || remoteState === "") {
|
||||||
) as AppState;
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
mergeAppState(localState, remoteState);
|
console.log(
|
||||||
setLocalAppState(localState);
|
"[Sync] Remote state is empty, using local state instead.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const parsedRemoteState = JSON.parse(
|
||||||
|
await client.get(config.username),
|
||||||
|
) as AppState;
|
||||||
|
mergeAppState(localState, parsedRemoteState);
|
||||||
|
setLocalAppState(localState);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[Sync] failed to get remote state", e);
|
console.log("[Sync] failed to get remote state", e);
|
||||||
throw e;
|
throw e;
|
||||||
|
@@ -82,6 +82,7 @@
|
|||||||
@include dark;
|
@include dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: var(--full-height);
|
height: var(--full-height);
|
||||||
|
|
||||||
@@ -106,6 +107,10 @@ body {
|
|||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
background-color: var(--second);
|
background-color: var(--second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
53
app/utils.ts
53
app/utils.ts
@@ -84,48 +84,6 @@ export async function downloadAs(text: string, filename: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compressImage(file: File, maxSize: number): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (readerEvent: any) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = () => {
|
|
||||||
let canvas = document.createElement("canvas");
|
|
||||||
let ctx = canvas.getContext("2d");
|
|
||||||
let width = image.width;
|
|
||||||
let height = image.height;
|
|
||||||
let quality = 0.9;
|
|
||||||
let dataUrl;
|
|
||||||
|
|
||||||
do {
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
ctx?.drawImage(image, 0, 0, width, height);
|
|
||||||
dataUrl = canvas.toDataURL("image/jpeg", quality);
|
|
||||||
|
|
||||||
if (dataUrl.length < maxSize) break;
|
|
||||||
|
|
||||||
if (quality > 0.5) {
|
|
||||||
// Prioritize quality reduction
|
|
||||||
quality -= 0.1;
|
|
||||||
} else {
|
|
||||||
// Then reduce the size
|
|
||||||
width *= 0.9;
|
|
||||||
height *= 0.9;
|
|
||||||
}
|
|
||||||
} while (dataUrl.length > maxSize);
|
|
||||||
|
|
||||||
resolve(dataUrl);
|
|
||||||
};
|
|
||||||
image.onerror = reject;
|
|
||||||
image.src = readerEvent.target.result;
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readFromFile() {
|
export function readFromFile() {
|
||||||
return new Promise<string>((res, rej) => {
|
return new Promise<string>((res, rej) => {
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
@@ -291,18 +249,21 @@ export function getMessageImages(message: RequestMessage): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isVisionModel(model: string) {
|
export function isVisionModel(model: string) {
|
||||||
|
|
||||||
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)
|
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)
|
||||||
|
|
||||||
const visionKeywords = [
|
const visionKeywords = [
|
||||||
"vision",
|
"vision",
|
||||||
"claude-3",
|
"claude-3",
|
||||||
"gemini-1.5-pro",
|
"gemini-1.5-pro",
|
||||||
|
"gemini-1.5-flash",
|
||||||
|
"gpt-4o",
|
||||||
];
|
];
|
||||||
|
const isGpt4Turbo =
|
||||||
|
model.includes("gpt-4-turbo") && !model.includes("preview");
|
||||||
|
|
||||||
const isGpt4Turbo = model.includes("gpt-4-turbo") && !model.includes("preview");
|
return (
|
||||||
|
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
|
||||||
return visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTime(dateTime: string) {
|
export function getTime(dateTime: string) {
|
||||||
|
54
app/utils/chat.ts
Normal file
54
app/utils/chat.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import heic2any from "heic2any";
|
||||||
|
|
||||||
|
export function compressImage(file: File, maxSize: number): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (readerEvent: any) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
let canvas = document.createElement("canvas");
|
||||||
|
let ctx = canvas.getContext("2d");
|
||||||
|
let width = image.width;
|
||||||
|
let height = image.height;
|
||||||
|
let quality = 0.9;
|
||||||
|
let dataUrl;
|
||||||
|
|
||||||
|
do {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx?.drawImage(image, 0, 0, width, height);
|
||||||
|
dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||||
|
|
||||||
|
if (dataUrl.length < maxSize) break;
|
||||||
|
|
||||||
|
if (quality > 0.5) {
|
||||||
|
// Prioritize quality reduction
|
||||||
|
quality -= 0.1;
|
||||||
|
} else {
|
||||||
|
// Then reduce the size
|
||||||
|
width *= 0.9;
|
||||||
|
height *= 0.9;
|
||||||
|
}
|
||||||
|
} while (dataUrl.length > maxSize);
|
||||||
|
|
||||||
|
resolve(dataUrl);
|
||||||
|
};
|
||||||
|
image.onerror = reject;
|
||||||
|
image.src = readerEvent.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
|
||||||
|
if (file.type.includes("heic")) {
|
||||||
|
heic2any({ blob: file, toType: "image/jpeg" })
|
||||||
|
.then((blob) => {
|
||||||
|
reader.readAsDataURL(blob as Blob);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
@@ -32,3 +32,9 @@ export function updateGlobalCSSVars(nextSidebar: number) {
|
|||||||
|
|
||||||
return { menuWidth };
|
return { menuWidth };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
export function getUid() {
|
||||||
|
return count++;
|
||||||
|
}
|
||||||
|
@@ -93,14 +93,17 @@ export function createUpstashClient(store: SyncStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
if (proxyUrl.length > 0 || proxyUrl === "/") {
|
const pathPrefix = "/api/upstash/";
|
||||||
let u = new URL(proxyUrl + "/api/upstash/" + path);
|
|
||||||
|
try {
|
||||||
|
let u = new URL(proxyUrl + pathPrefix + path);
|
||||||
// add query params
|
// add query params
|
||||||
u.searchParams.append("endpoint", config.endpoint);
|
u.searchParams.append("endpoint", config.endpoint);
|
||||||
url = u.toString();
|
url = u.toString();
|
||||||
} else {
|
} catch (e) {
|
||||||
url = "/api/upstash/" + path + "?endpoint=" + config.endpoint;
|
url = pathPrefix + path + "?endpoint=" + config.endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useAccessStore, useAppConfig } from "../store";
|
import { useAccessStore, useAppConfig } from "../store";
|
||||||
import { collectModels, collectModelsWithDefaultModel } from "./model";
|
import { collectModelsWithDefaultModel } from "./model";
|
||||||
|
|
||||||
export function useAllModels() {
|
export function useAllModels() {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
|
@@ -64,13 +64,10 @@ export function collectModelTableWithDefaultModel(
|
|||||||
) {
|
) {
|
||||||
let modelTable = collectModelTable(models, customModels);
|
let modelTable = collectModelTable(models, customModels);
|
||||||
if (defaultModel && defaultModel !== "") {
|
if (defaultModel && defaultModel !== "") {
|
||||||
delete modelTable[defaultModel];
|
|
||||||
modelTable[defaultModel] = {
|
modelTable[defaultModel] = {
|
||||||
|
...modelTable[defaultModel],
|
||||||
name: defaultModel,
|
name: defaultModel,
|
||||||
displayName: defaultModel,
|
|
||||||
available: true,
|
available: true,
|
||||||
provider:
|
|
||||||
modelTable[defaultModel]?.provider ?? customProvider(defaultModel),
|
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -29,11 +29,13 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"emoji-picker-react": "^4.9.2",
|
"emoji-picker-react": "^4.9.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^10.6.1",
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^13.4.9",
|
"next": "^14.1.1",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react": "^18.2.70",
|
"@types/react": "^18.2.70",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
@@ -50,6 +50,10 @@
|
|||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"all": true
|
"all": true
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"all": true,
|
||||||
|
"request": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@@ -112,4 +116,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
178
yarn.lock
178
yarn.lock
@@ -1269,10 +1269,10 @@
|
|||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||||
|
|
||||||
"@next/env@13.4.9":
|
"@next/env@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac"
|
||||||
integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw==
|
integrity sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@13.4.19":
|
"@next/eslint-plugin-next@13.4.19":
|
||||||
version "13.4.19"
|
version "13.4.19"
|
||||||
@@ -1281,50 +1281,50 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@13.4.9":
|
"@next/swc-darwin-arm64@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz#b74ba7c14af7d05fa2848bdeb8ee87716c939b64"
|
||||||
integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ==
|
integrity sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@13.4.9":
|
"@next/swc-darwin-x64@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz#82c3e67775e40094c66e76845d1a36cc29c9e78b"
|
||||||
integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A==
|
integrity sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@13.4.9":
|
"@next/swc-linux-arm64-gnu@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz#4f4134457b90adc5c3d167d07dfb713c632c0caa"
|
||||||
integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg==
|
integrity sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@13.4.9":
|
"@next/swc-linux-arm64-musl@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz#594bedafaeba4a56db23a48ffed2cef7cd09c31a"
|
||||||
integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw==
|
integrity sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@13.4.9":
|
"@next/swc-linux-x64-gnu@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz#cb4e75f1ff2b9bcadf2a50684605928ddfc58528"
|
||||||
integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg==
|
integrity sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@13.4.9":
|
"@next/swc-linux-x64-musl@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz#15f26800df941b94d06327f674819ab64b272e25"
|
||||||
integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A==
|
integrity sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@13.4.9":
|
"@next/swc-win32-arm64-msvc@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz#060c134fa7fa843666e3e8574972b2b723773dd9"
|
||||||
integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA==
|
integrity sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@13.4.9":
|
"@next/swc-win32-ia32-msvc@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz#5c06889352b1f77e3807834a0d0afd7e2d2d1da2"
|
||||||
integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA==
|
integrity sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@13.4.9":
|
"@next/swc-win32-x64-msvc@14.1.1":
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz#d38c63a8f9b7f36c1470872797d3735b4a9c5c52"
|
||||||
integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw==
|
integrity sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A==
|
||||||
|
|
||||||
"@next/third-parties@^14.1.0":
|
"@next/third-parties@^14.1.0":
|
||||||
version "14.1.0"
|
version "14.1.0"
|
||||||
@@ -1720,10 +1720,10 @@
|
|||||||
"@svgr/plugin-jsx" "^6.5.1"
|
"@svgr/plugin-jsx" "^6.5.1"
|
||||||
"@svgr/plugin-svgo" "^6.5.1"
|
"@svgr/plugin-svgo" "^6.5.1"
|
||||||
|
|
||||||
"@swc/helpers@0.5.1":
|
"@swc/helpers@0.5.2":
|
||||||
version "0.5.1"
|
version "0.5.2"
|
||||||
resolved "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a"
|
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||||
integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==
|
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
@@ -1878,6 +1878,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
|
||||||
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
|
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
|
||||||
|
|
||||||
|
"@types/lodash-es@^4.17.12":
|
||||||
|
version "4.17.12"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||||
|
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.17.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8"
|
||||||
|
integrity sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
"@types/mdast@^3.0.0":
|
||||||
version "3.0.11"
|
version "3.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
||||||
@@ -2482,10 +2494,10 @@ camelcase@^6.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503:
|
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579:
|
||||||
version "1.0.30001509"
|
version "1.0.30001617"
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz#2b7ad5265392d6d2de25cd8776d1ab3899570d14"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb"
|
||||||
integrity sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==
|
integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
||||||
version "1.0.30001608"
|
version "1.0.30001608"
|
||||||
@@ -3987,7 +3999,7 @@ gopd@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
@@ -4138,6 +4150,11 @@ heap@^0.2.6:
|
|||||||
resolved "https://registry.npmmirror.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
|
resolved "https://registry.npmmirror.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
|
||||||
integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
|
integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
|
||||||
|
|
||||||
|
heic2any@^0.0.4:
|
||||||
|
version "0.0.4"
|
||||||
|
resolved "https://registry.npmmirror.com/heic2any/-/heic2any-0.0.4.tgz#eddb8e6fec53c8583a6e18b65069bb5e8d19028a"
|
||||||
|
integrity sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==
|
||||||
|
|
||||||
highlight.js@~11.7.0:
|
highlight.js@~11.7.0:
|
||||||
version "11.7.0"
|
version "11.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
||||||
@@ -5295,14 +5312,9 @@ mz@^2.7.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nanoid@^3.3.4:
|
nanoid@^3.3.6, nanoid@^3.3.7:
|
||||||
version "3.3.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
|
||||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
|
||||||
|
|
||||||
nanoid@^3.3.7:
|
|
||||||
version "3.3.7"
|
version "3.3.7"
|
||||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
nanoid@^5.0.3:
|
nanoid@^5.0.3:
|
||||||
@@ -5320,29 +5332,28 @@ neo-async@^2.6.2:
|
|||||||
resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
next@^13.4.9:
|
next@^14.1.1:
|
||||||
version "13.4.9"
|
version "14.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171"
|
||||||
integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA==
|
integrity sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "13.4.9"
|
"@next/env" "14.1.1"
|
||||||
"@swc/helpers" "0.5.1"
|
"@swc/helpers" "0.5.2"
|
||||||
busboy "1.6.0"
|
busboy "1.6.0"
|
||||||
caniuse-lite "^1.0.30001406"
|
caniuse-lite "^1.0.30001579"
|
||||||
postcss "8.4.14"
|
graceful-fs "^4.2.11"
|
||||||
|
postcss "8.4.31"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
watchpack "2.4.0"
|
|
||||||
zod "3.21.4"
|
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-darwin-arm64" "13.4.9"
|
"@next/swc-darwin-arm64" "14.1.1"
|
||||||
"@next/swc-darwin-x64" "13.4.9"
|
"@next/swc-darwin-x64" "14.1.1"
|
||||||
"@next/swc-linux-arm64-gnu" "13.4.9"
|
"@next/swc-linux-arm64-gnu" "14.1.1"
|
||||||
"@next/swc-linux-arm64-musl" "13.4.9"
|
"@next/swc-linux-arm64-musl" "14.1.1"
|
||||||
"@next/swc-linux-x64-gnu" "13.4.9"
|
"@next/swc-linux-x64-gnu" "14.1.1"
|
||||||
"@next/swc-linux-x64-musl" "13.4.9"
|
"@next/swc-linux-x64-musl" "14.1.1"
|
||||||
"@next/swc-win32-arm64-msvc" "13.4.9"
|
"@next/swc-win32-arm64-msvc" "14.1.1"
|
||||||
"@next/swc-win32-ia32-msvc" "13.4.9"
|
"@next/swc-win32-ia32-msvc" "14.1.1"
|
||||||
"@next/swc-win32-x64-msvc" "13.4.9"
|
"@next/swc-win32-x64-msvc" "14.1.1"
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -5660,12 +5671,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@8.4.14:
|
postcss@8.4.31:
|
||||||
version "8.4.14"
|
version "8.4.31"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.4"
|
nanoid "^3.3.6"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
@@ -6846,7 +6857,7 @@ vfile@^5.0.0:
|
|||||||
unist-util-stringify-position "^3.0.0"
|
unist-util-stringify-position "^3.0.0"
|
||||||
vfile-message "^3.0.0"
|
vfile-message "^3.0.0"
|
||||||
|
|
||||||
watchpack@2.4.0, watchpack@^2.4.0:
|
watchpack@^2.4.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
@@ -7015,11 +7026,6 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
zod@3.21.4:
|
|
||||||
version "3.21.4"
|
|
||||||
resolved "https://registry.npmmirror.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
|
||||||
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
|
||||||
|
|
||||||
zustand@^4.3.8:
|
zustand@^4.3.8:
|
||||||
version "4.3.8"
|
version "4.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4"
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4"
|
||||||
|
Reference in New Issue
Block a user