mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-10-31 06:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			165 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b9d1dca65d | ||
|  | 8e4d26163a | ||
|  | 53c1176cbf | ||
|  | 46d3e7884b | ||
|  | b4ae706914 | ||
|  | 476bdac717 | ||
|  | 831627268d | ||
|  | 9b97dca601 | ||
|  | 4ea8c0802a | ||
|  | 9203870df5 | ||
|  | e8088d6e38 | ||
|  | 59d9bcdd27 | ||
|  | 9d1b13ba73 | ||
|  | 728c38396a | ||
|  | 89049e1a22 | ||
|  | 5e7254e8dc | ||
|  | f8c2732fdc | ||
|  | fec36eb298 | ||
|  | 2299a4156d | ||
|  | 32b82b9cb3 | ||
|  | ba6039fc8b | ||
|  | 6885812d21 | ||
|  | 844025ec14 | ||
|  | 94bc91c554 | ||
|  | 044c16da4c | ||
|  | cd4784c54a | ||
|  | 814aaa4a69 | ||
|  | e3b3a4fefa | ||
|  | 3fcbb3010d | ||
|  | 7573a19dc9 | ||
|  | 3628d68d9a | ||
|  | 23872086fa | ||
|  | bb349a03da | ||
|  | 82be426f78 | ||
|  | 9d2a633f5e | ||
|  | 1149d45589 | ||
|  | 9d7e19cebf | ||
|  | b3023543d6 | ||
|  | c229d2c3ce | ||
|  | 47ea383ddd | ||
|  | f2a35f1114 | ||
|  | 147fc9a35a | ||
|  | 93a03f8fe4 | ||
|  | 230e3823a9 | ||
|  | b14a0f24ae | ||
|  | 5295802720 | ||
|  | fadd7f6eb4 | ||
|  | 011b76e4e7 | ||
|  | f68cd2c5c0 | ||
|  | 6ac9789a1c | ||
|  | 34ab37f31e | ||
|  | 71af2628eb | ||
|  | 15f028abfb | ||
|  | 9bdd37bb63 | ||
|  | 1caa61f4c0 | ||
|  | f3e3f08377 | ||
|  | 2ec8b7a804 | ||
|  | 9f7d137b05 | ||
|  | 7218f13783 | ||
|  | fa31e7802c | ||
|  | 9b3b4494ba | ||
|  | 785d3748e1 | ||
|  | 5e0657ce55 | ||
|  | 700b06f9c5 | ||
|  | b58bbf8eb4 | ||
|  | 2d1f522aaf | ||
|  | 0b2863dfab | ||
|  | 70907ead8a | ||
|  | 6dc4844c12 | ||
|  | 14bc1b6aac | ||
|  | 183ad2a34b | ||
|  | d9758be3ae | ||
|  | 6b1b530443 | ||
|  | 1c20137b0e | ||
|  | c4a6c933f8 | ||
|  | 31d9444264 | ||
|  | 8cb204e22e | ||
|  | 97aa72ec5b | ||
|  | a68341eae6 | ||
|  | aa08183439 | ||
|  | 7a5596b909 | ||
|  | b9ffd50992 | ||
|  | 14f2a8f370 | ||
|  | e7b16bfbc0 | ||
|  | 2803a91673 | ||
|  | cf2fce7666 | ||
|  | 1609abd166 | ||
|  | 88c74ae18d | ||
|  | 78e2b41e0c | ||
|  | 501f8b028b | ||
|  | c4ad66f745 | ||
|  | 69974d5651 | ||
|  | ce3b6a04c2 | ||
|  | 37e2517dac | ||
|  | d65ddead11 | ||
|  | c359b30763 | ||
|  | 95e3b156c0 | ||
|  | b972a0d081 | ||
|  | 20749355da | ||
|  | dad122199a | ||
|  | 9fb8fbcc65 | ||
|  | 78e7ea72dc | ||
|  | 4640060891 | ||
|  | 6efe4fb734 | ||
|  | 9b0a705055 | ||
|  | 163fc9e3a3 | ||
|  | b6735bffe4 | ||
|  | 1d8fd480ca | ||
|  | da2e2372aa | ||
|  | f3b972e573 | ||
|  | bf3bc3c7e9 | ||
|  | 38664487a0 | ||
|  | de1111286c | ||
|  | d89a12aa05 | ||
|  | 754acd7c26 | ||
|  | c3e2f3b714 | ||
|  | 22ef3d3a46 | ||
|  | 7f3516f44f | ||
|  | bfdb47a7ed | ||
|  | f55f04ab4f | ||
|  | 01c9dbc1fd | ||
|  | 0aa807df19 | ||
|  | 48d44ece58 | ||
|  | e58cb2b0db | ||
|  | bffd9d9173 | ||
|  | 8688842984 | ||
|  | cf29a8f2c8 | ||
|  | 1e00c89988 | ||
|  | 0eccb547b5 | ||
|  | 4789a7f6a9 | ||
|  | 0bf758afd4 | ||
|  | 6612550c06 | ||
|  | d411159124 | ||
|  | 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 | ||
|  | 5bf402710f | ||
|  | 2053db4cfc | ||
|  | 754303e7c7 | 
| @@ -43,7 +43,7 @@ COPY --from=builder /app/.next/server ./.next/server | |||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
|  |  | ||||||
| CMD if [ -n "$PROXY_URL" ]; then \ | CMD if [ -n "$PROXY_URL" ]; then \ | ||||||
|     export HOSTNAME="127.0.0.1"; \ |     export HOSTNAME="0.0.0.0"; \ | ||||||
|     protocol=$(echo $PROXY_URL | cut -d: -f1); \ |     protocol=$(echo $PROXY_URL | cut -d: -f1); \ | ||||||
|     host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ |     host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ | ||||||
|     port=$(echo $PROXY_URL | cut -d: -f3); \ |     port=$(echo $PROXY_URL | cut -d: -f3); \ | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| MIT License | MIT License | ||||||
|  |  | ||||||
| Copyright (c) 2023 Zhang Yifei | Copyright (c) 2023-2024 Zhang Yifei | ||||||
|  |  | ||||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							| @@ -18,7 +18,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 | |||||||
|  |  | ||||||
| [网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) | [网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) | ||||||
|  |  | ||||||
| [web-url]: https://chatgpt.nextweb.fun | [web-url]: https://app.nextchat.dev/ | ||||||
| [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases | [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases | ||||||
| [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge | [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge | ||||||
| [Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows | [Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows | ||||||
| @@ -181,6 +181,7 @@ Specify OpenAI organization ID. | |||||||
| ### `AZURE_URL` (optional) | ### `AZURE_URL` (optional) | ||||||
|  |  | ||||||
| > Example: https://{azure-resource-url}/openai/deployments/{deploy-name} | > Example: https://{azure-resource-url}/openai/deployments/{deploy-name} | ||||||
|  | > if you config deployment name in `CUSTOM_MODELS`, you can remove `{deploy-name}` in `AZURE_URL` | ||||||
|  |  | ||||||
| Azure deploy url. | Azure deploy url. | ||||||
|  |  | ||||||
| @@ -212,6 +213,34 @@ anthropic claude Api version. | |||||||
|  |  | ||||||
| anthropic claude Api Url. | anthropic claude Api Url. | ||||||
|  |  | ||||||
|  | ### `BAIDU_API_KEY` (optional) | ||||||
|  |  | ||||||
|  | Baidu Api Key. | ||||||
|  |  | ||||||
|  | ### `BAIDU_SECRET_KEY` (optional) | ||||||
|  |  | ||||||
|  | Baidu Secret Key. | ||||||
|  |  | ||||||
|  | ### `BAIDU_URL` (optional) | ||||||
|  |  | ||||||
|  | Baidu Api Url. | ||||||
|  |  | ||||||
|  | ### `BYTEDANCE_API_KEY` (optional) | ||||||
|  |  | ||||||
|  | ByteDance Api Key. | ||||||
|  |  | ||||||
|  | ### `BYTEDANCE_URL` (optional) | ||||||
|  |  | ||||||
|  | ByteDance Api Url. | ||||||
|  |  | ||||||
|  | ### `ALIBABA_API_KEY` (optional) | ||||||
|  |  | ||||||
|  | Alibaba Cloud Api Key. | ||||||
|  |  | ||||||
|  | ### `ALIBABA_URL` (optional) | ||||||
|  |  | ||||||
|  | Alibaba Cloud Api Url. | ||||||
|  |  | ||||||
| ### `HIDE_USER_API_KEY` (optional) | ### `HIDE_USER_API_KEY` (optional) | ||||||
|  |  | ||||||
| > Default: Empty | > Default: Empty | ||||||
| @@ -245,13 +274,27 @@ 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` (可选) | For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name. | ||||||
|  | > Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list. | ||||||
|  |  | ||||||
|  | For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name. | ||||||
|  | > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. | ||||||
|  |  | ||||||
|  | ### `DEFAULT_MODEL` (optional) | ||||||
|  |  | ||||||
|  | Change default model | ||||||
|  |  | ||||||
|  | ### `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 | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								README_CN.md
									
									
									
									
									
								
							| @@ -95,6 +95,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 | |||||||
| ### `AZURE_URL` (可选) | ### `AZURE_URL` (可选) | ||||||
|  |  | ||||||
| > 形如:https://{azure-resource-url}/openai/deployments/{deploy-name} | > 形如:https://{azure-resource-url}/openai/deployments/{deploy-name} | ||||||
|  | > 如果你已经在`CUSTOM_MODELS`中参考`displayName`的方式配置了{deploy-name},那么可以从`AZURE_URL`中移除`{deploy-name}` | ||||||
|  |  | ||||||
| Azure 部署地址。 | Azure 部署地址。 | ||||||
|  |  | ||||||
| @@ -106,26 +107,54 @@ Azure 密钥。 | |||||||
|  |  | ||||||
| Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 | Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 | ||||||
|  |  | ||||||
| ### `GOOGLE_API_KEY` (optional) | ### `GOOGLE_API_KEY` (可选) | ||||||
|  |  | ||||||
| Google Gemini Pro 密钥. | Google Gemini Pro 密钥. | ||||||
|  |  | ||||||
| ### `GOOGLE_URL` (optional) | ### `GOOGLE_URL` (可选) | ||||||
|  |  | ||||||
| Google Gemini Pro Api Url. | Google Gemini Pro Api Url. | ||||||
|  |  | ||||||
| ### `ANTHROPIC_API_KEY` (optional) | ### `ANTHROPIC_API_KEY` (可选) | ||||||
|  |  | ||||||
| anthropic claude Api Key. | anthropic claude Api Key. | ||||||
|  |  | ||||||
| ### `ANTHROPIC_API_VERSION` (optional) | ### `ANTHROPIC_API_VERSION` (可选) | ||||||
|  |  | ||||||
| anthropic claude Api version. | anthropic claude Api version. | ||||||
|  |  | ||||||
| ### `ANTHROPIC_URL` (optional) | ### `ANTHROPIC_URL` (可选) | ||||||
|  |  | ||||||
| anthropic claude Api Url. | anthropic claude Api Url. | ||||||
|  |  | ||||||
|  | ### `BAIDU_API_KEY` (可选) | ||||||
|  |  | ||||||
|  | Baidu Api Key. | ||||||
|  |  | ||||||
|  | ### `BAIDU_SECRET_KEY` (可选) | ||||||
|  |  | ||||||
|  | Baidu Secret Key. | ||||||
|  |  | ||||||
|  | ### `BAIDU_URL` (可选) | ||||||
|  |  | ||||||
|  | Baidu Api Url. | ||||||
|  |  | ||||||
|  | ### `BYTEDANCE_API_KEY` (可选) | ||||||
|  |  | ||||||
|  | ByteDance Api Key. | ||||||
|  |  | ||||||
|  | ### `BYTEDANCE_URL` (可选) | ||||||
|  |  | ||||||
|  | ByteDance Api Url. | ||||||
|  |  | ||||||
|  | ### `ALIBABA_API_KEY` (可选) | ||||||
|  |  | ||||||
|  | 阿里云(千问)Api Key. | ||||||
|  |  | ||||||
|  | ### `ALIBABA_URL` (可选) | ||||||
|  |  | ||||||
|  | 阿里云(千问)Api Url. | ||||||
|  |  | ||||||
| ### `HIDE_USER_API_KEY` (可选) | ### `HIDE_USER_API_KEY` (可选) | ||||||
|  |  | ||||||
| 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 | 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 | ||||||
| @@ -156,6 +185,21 @@ anthropic claude Api Url. | |||||||
|  |  | ||||||
| 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 | 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 | ||||||
|  |  | ||||||
|  | 在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) | ||||||
|  | > 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项 | ||||||
|  |  | ||||||
|  | 在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) | ||||||
|  | > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### `DEFAULT_MODEL` (可选) | ||||||
|  |  | ||||||
|  | 更改默认模型 | ||||||
|  |  | ||||||
|  | ### `DEFAULT_INPUT_TEMPLATE` (可选) | ||||||
|  |  | ||||||
|  | 自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项 | ||||||
|  |  | ||||||
| ## 开发 | ## 开发 | ||||||
|  |  | ||||||
| 点击下方按钮,开始二次开发: | 点击下方按钮,开始二次开发: | ||||||
|   | |||||||
							
								
								
									
										155
									
								
								app/api/alibaba/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								app/api/alibaba/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | import { getServerSideConfig } from "@/app/config/server"; | ||||||
|  | import { | ||||||
|  |   Alibaba, | ||||||
|  |   ALIBABA_BASE_URL, | ||||||
|  |   ApiPath, | ||||||
|  |   ModelProvider, | ||||||
|  |   ServiceProvider, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { NextRequest, NextResponse } from "next/server"; | ||||||
|  | import { auth } from "@/app/api/auth"; | ||||||
|  | import { isModelAvailableInServer } from "@/app/utils/model"; | ||||||
|  | import type { RequestPayload } from "@/app/client/platforms/openai"; | ||||||
|  |  | ||||||
|  | const serverConfig = getServerSideConfig(); | ||||||
|  |  | ||||||
|  | async function handle( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: { path: string[] } }, | ||||||
|  | ) { | ||||||
|  |   console.log("[Alibaba Route] params ", params); | ||||||
|  |  | ||||||
|  |   if (req.method === "OPTIONS") { | ||||||
|  |     return NextResponse.json({ body: "OK" }, { status: 200 }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const authResult = auth(req, ModelProvider.Qwen); | ||||||
|  |   if (authResult.error) { | ||||||
|  |     return NextResponse.json(authResult, { | ||||||
|  |       status: 401, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const response = await request(req); | ||||||
|  |     return response; | ||||||
|  |   } catch (e) { | ||||||
|  |     console.error("[Alibaba] ", e); | ||||||
|  |     return NextResponse.json(prettyObject(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const GET = handle; | ||||||
|  | export const POST = handle; | ||||||
|  |  | ||||||
|  | export const runtime = "edge"; | ||||||
|  | export const preferredRegion = [ | ||||||
|  |   "arn1", | ||||||
|  |   "bom1", | ||||||
|  |   "cdg1", | ||||||
|  |   "cle1", | ||||||
|  |   "cpt1", | ||||||
|  |   "dub1", | ||||||
|  |   "fra1", | ||||||
|  |   "gru1", | ||||||
|  |   "hnd1", | ||||||
|  |   "iad1", | ||||||
|  |   "icn1", | ||||||
|  |   "kix1", | ||||||
|  |   "lhr1", | ||||||
|  |   "pdx1", | ||||||
|  |   "sfo1", | ||||||
|  |   "sin1", | ||||||
|  |   "syd1", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | async function request(req: NextRequest) { | ||||||
|  |   const controller = new AbortController(); | ||||||
|  |  | ||||||
|  |   // alibaba use base url or just remove the path | ||||||
|  |   let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, ""); | ||||||
|  |  | ||||||
|  |   let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL; | ||||||
|  |  | ||||||
|  |   if (!baseUrl.startsWith("http")) { | ||||||
|  |     baseUrl = `https://${baseUrl}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (baseUrl.endsWith("/")) { | ||||||
|  |     baseUrl = baseUrl.slice(0, -1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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", | ||||||
|  |       Authorization: req.headers.get("Authorization") ?? "", | ||||||
|  |       "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable", | ||||||
|  |     }, | ||||||
|  |     method: req.method, | ||||||
|  |     body: req.body, | ||||||
|  |     redirect: "manual", | ||||||
|  |     // @ts-ignore | ||||||
|  |     duplex: "half", | ||||||
|  |     signal: controller.signal, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // #1815 try to refuse some request to some models | ||||||
|  |   if (serverConfig.customModels && req.body) { | ||||||
|  |     try { | ||||||
|  |       const clonedBody = await req.text(); | ||||||
|  |       fetchOptions.body = clonedBody; | ||||||
|  |  | ||||||
|  |       const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||||||
|  |  | ||||||
|  |       // not undefined and is false | ||||||
|  |       if ( | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.Alibaba as string, | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return NextResponse.json( | ||||||
|  |           { | ||||||
|  |             error: true, | ||||||
|  |             message: `you are not allowed to use ${jsonBody?.model} model`, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             status: 403, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(`[Alibaba] filter`, e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   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 Response(res.body, { | ||||||
|  |       status: res.status, | ||||||
|  |       statusText: res.statusText, | ||||||
|  |       headers: newHeaders, | ||||||
|  |     }); | ||||||
|  |   } finally { | ||||||
|  |     clearTimeout(timeoutId); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -4,12 +4,14 @@ import { | |||||||
|   Anthropic, |   Anthropic, | ||||||
|   ApiPath, |   ApiPath, | ||||||
|   DEFAULT_MODELS, |   DEFAULT_MODELS, | ||||||
|  |   ServiceProvider, | ||||||
|   ModelProvider, |   ModelProvider, | ||||||
| } from "@/app/constant"; | } from "@/app/constant"; | ||||||
| import { prettyObject } from "@/app/utils/format"; | import { prettyObject } from "@/app/utils/format"; | ||||||
| import { NextRequest, NextResponse } from "next/server"; | import { NextRequest, NextResponse } from "next/server"; | ||||||
| import { auth } from "../../auth"; | import { auth } from "../../auth"; | ||||||
| import { collectModelTable } from "@/app/utils/model"; | import { isModelAvailableInServer } from "@/app/utils/model"; | ||||||
|  | import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; | ||||||
|  |  | ||||||
| const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); | const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); | ||||||
|  |  | ||||||
| @@ -113,7 +115,8 @@ async function request(req: NextRequest) { | |||||||
|     10 * 60 * 1000, |     10 * 60 * 1000, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const fetchUrl = `${baseUrl}${path}`; |   // try rebuild url, when using cloudflare ai gateway in server | ||||||
|  |   const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`); | ||||||
|  |  | ||||||
|   const fetchOptions: RequestInit = { |   const fetchOptions: RequestInit = { | ||||||
|     headers: { |     headers: { | ||||||
| @@ -136,17 +139,19 @@ async function request(req: NextRequest) { | |||||||
|   // #1815 try to refuse some request to some models |   // #1815 try to refuse some request to some models | ||||||
|   if (serverConfig.customModels && req.body) { |   if (serverConfig.customModels && req.body) { | ||||||
|     try { |     try { | ||||||
|       const modelTable = collectModelTable( |  | ||||||
|         DEFAULT_MODELS, |  | ||||||
|         serverConfig.customModels, |  | ||||||
|       ); |  | ||||||
|       const clonedBody = await req.text(); |       const clonedBody = await req.text(); | ||||||
|       fetchOptions.body = clonedBody; |       fetchOptions.body = clonedBody; | ||||||
|  |  | ||||||
|       const jsonBody = JSON.parse(clonedBody) as { model?: string }; |       const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||||||
|  |  | ||||||
|       // not undefined and is false |       // not undefined and is false | ||||||
|       if (modelTable[jsonBody?.model ?? ""].available === false) { |       if ( | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.Anthropic as string, | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|         return NextResponse.json( |         return NextResponse.json( | ||||||
|           { |           { | ||||||
|             error: true, |             error: true, | ||||||
| @@ -161,17 +166,17 @@ async function request(req: NextRequest) { | |||||||
|       console.error(`[Anthropic] filter`, e); |       console.error(`[Anthropic] filter`, e); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   console.log("[Anthropic request]", fetchOptions.headers, req.method); |   // console.log("[Anthropic request]", fetchOptions.headers, req.method); | ||||||
|   try { |   try { | ||||||
|     const res = await fetch(fetchUrl, fetchOptions); |     const res = await fetch(fetchUrl, fetchOptions); | ||||||
|  |  | ||||||
|     console.log( |     // console.log( | ||||||
|       "[Anthropic response]", |     //   "[Anthropic response]", | ||||||
|       res.status, |     //   res.status, | ||||||
|       "   ", |     //   "   ", | ||||||
|       res.headers, |     //   res.headers, | ||||||
|       res.url, |     //   res.url, | ||||||
|     ); |     // ); | ||||||
|     // to prevent browser prompt for credentials |     // to prevent browser prompt for credentials | ||||||
|     const newHeaders = new Headers(res.headers); |     const newHeaders = new Headers(res.headers); | ||||||
|     newHeaders.delete("www-authenticate"); |     newHeaders.delete("www-authenticate"); | ||||||
|   | |||||||
| @@ -73,9 +73,18 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { | |||||||
|       case ModelProvider.Claude: |       case ModelProvider.Claude: | ||||||
|         systemApiKey = serverConfig.anthropicApiKey; |         systemApiKey = serverConfig.anthropicApiKey; | ||||||
|         break; |         break; | ||||||
|  |       case ModelProvider.Doubao: | ||||||
|  |         systemApiKey = serverConfig.bytedanceApiKey; | ||||||
|  |         break; | ||||||
|  |       case ModelProvider.Ernie: | ||||||
|  |         systemApiKey = serverConfig.baiduApiKey; | ||||||
|  |         break; | ||||||
|  |       case ModelProvider.Qwen: | ||||||
|  |         systemApiKey = serverConfig.alibabaApiKey; | ||||||
|  |         break; | ||||||
|       case ModelProvider.GPT: |       case ModelProvider.GPT: | ||||||
|       default: |       default: | ||||||
|         if (serverConfig.isAzure) { |         if (req.nextUrl.pathname.includes("azure/deployments")) { | ||||||
|           systemApiKey = serverConfig.azureApiKey; |           systemApiKey = serverConfig.azureApiKey; | ||||||
|         } else { |         } else { | ||||||
|           systemApiKey = serverConfig.apiKey; |           systemApiKey = serverConfig.apiKey; | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								app/api/azure/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/api/azure/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import { getServerSideConfig } from "@/app/config/server"; | ||||||
|  | import { ModelProvider } from "@/app/constant"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { NextRequest, NextResponse } from "next/server"; | ||||||
|  | import { auth } from "../../auth"; | ||||||
|  | import { requestOpenai } from "../../common"; | ||||||
|  |  | ||||||
|  | async function handle( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: { path: string[] } }, | ||||||
|  | ) { | ||||||
|  |   console.log("[Azure Route] params ", params); | ||||||
|  |  | ||||||
|  |   if (req.method === "OPTIONS") { | ||||||
|  |     return NextResponse.json({ body: "OK" }, { status: 200 }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const subpath = params.path.join("/"); | ||||||
|  |  | ||||||
|  |   const authResult = auth(req, ModelProvider.GPT); | ||||||
|  |   if (authResult.error) { | ||||||
|  |     return NextResponse.json(authResult, { | ||||||
|  |       status: 401, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     return await requestOpenai(req); | ||||||
|  |   } catch (e) { | ||||||
|  |     console.error("[Azure] ", e); | ||||||
|  |     return NextResponse.json(prettyObject(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const GET = handle; | ||||||
|  | export const POST = handle; | ||||||
|  |  | ||||||
|  | export const runtime = "edge"; | ||||||
|  | export const preferredRegion = [ | ||||||
|  |   "arn1", | ||||||
|  |   "bom1", | ||||||
|  |   "cdg1", | ||||||
|  |   "cle1", | ||||||
|  |   "cpt1", | ||||||
|  |   "dub1", | ||||||
|  |   "fra1", | ||||||
|  |   "gru1", | ||||||
|  |   "hnd1", | ||||||
|  |   "iad1", | ||||||
|  |   "icn1", | ||||||
|  |   "kix1", | ||||||
|  |   "lhr1", | ||||||
|  |   "pdx1", | ||||||
|  |   "sfo1", | ||||||
|  |   "sin1", | ||||||
|  |   "syd1", | ||||||
|  | ]; | ||||||
							
								
								
									
										169
									
								
								app/api/baidu/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								app/api/baidu/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | |||||||
|  | import { getServerSideConfig } from "@/app/config/server"; | ||||||
|  | import { | ||||||
|  |   BAIDU_BASE_URL, | ||||||
|  |   ApiPath, | ||||||
|  |   ModelProvider, | ||||||
|  |   BAIDU_OATUH_URL, | ||||||
|  |   ServiceProvider, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { NextRequest, NextResponse } from "next/server"; | ||||||
|  | import { auth } from "@/app/api/auth"; | ||||||
|  | import { isModelAvailableInServer } from "@/app/utils/model"; | ||||||
|  | import { getAccessToken } from "@/app/utils/baidu"; | ||||||
|  |  | ||||||
|  | const serverConfig = getServerSideConfig(); | ||||||
|  |  | ||||||
|  | async function handle( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: { path: string[] } }, | ||||||
|  | ) { | ||||||
|  |   console.log("[Baidu Route] params ", params); | ||||||
|  |  | ||||||
|  |   if (req.method === "OPTIONS") { | ||||||
|  |     return NextResponse.json({ body: "OK" }, { status: 200 }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const authResult = auth(req, ModelProvider.Ernie); | ||||||
|  |   if (authResult.error) { | ||||||
|  |     return NextResponse.json(authResult, { | ||||||
|  |       status: 401, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) { | ||||||
|  |     return NextResponse.json( | ||||||
|  |       { | ||||||
|  |         error: true, | ||||||
|  |         message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         status: 401, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const response = await request(req); | ||||||
|  |     return response; | ||||||
|  |   } catch (e) { | ||||||
|  |     console.error("[Baidu] ", e); | ||||||
|  |     return NextResponse.json(prettyObject(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const GET = handle; | ||||||
|  | export const POST = handle; | ||||||
|  |  | ||||||
|  | export const runtime = "edge"; | ||||||
|  | export const preferredRegion = [ | ||||||
|  |   "arn1", | ||||||
|  |   "bom1", | ||||||
|  |   "cdg1", | ||||||
|  |   "cle1", | ||||||
|  |   "cpt1", | ||||||
|  |   "dub1", | ||||||
|  |   "fra1", | ||||||
|  |   "gru1", | ||||||
|  |   "hnd1", | ||||||
|  |   "iad1", | ||||||
|  |   "icn1", | ||||||
|  |   "kix1", | ||||||
|  |   "lhr1", | ||||||
|  |   "pdx1", | ||||||
|  |   "sfo1", | ||||||
|  |   "sin1", | ||||||
|  |   "syd1", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | async function request(req: NextRequest) { | ||||||
|  |   const controller = new AbortController(); | ||||||
|  |  | ||||||
|  |   let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, ""); | ||||||
|  |  | ||||||
|  |   let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL; | ||||||
|  |  | ||||||
|  |   if (!baseUrl.startsWith("http")) { | ||||||
|  |     baseUrl = `https://${baseUrl}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (baseUrl.endsWith("/")) { | ||||||
|  |     baseUrl = baseUrl.slice(0, -1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   console.log("[Proxy] ", path); | ||||||
|  |   console.log("[Base Url]", baseUrl); | ||||||
|  |  | ||||||
|  |   const timeoutId = setTimeout( | ||||||
|  |     () => { | ||||||
|  |       controller.abort(); | ||||||
|  |     }, | ||||||
|  |     10 * 60 * 1000, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const { access_token } = await getAccessToken( | ||||||
|  |     serverConfig.baiduApiKey as string, | ||||||
|  |     serverConfig.baiduSecretKey as string, | ||||||
|  |   ); | ||||||
|  |   const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`; | ||||||
|  |  | ||||||
|  |   const fetchOptions: RequestInit = { | ||||||
|  |     headers: { | ||||||
|  |       "Content-Type": "application/json", | ||||||
|  |     }, | ||||||
|  |     method: req.method, | ||||||
|  |     body: req.body, | ||||||
|  |     redirect: "manual", | ||||||
|  |     // @ts-ignore | ||||||
|  |     duplex: "half", | ||||||
|  |     signal: controller.signal, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // #1815 try to refuse some request to some models | ||||||
|  |   if (serverConfig.customModels && req.body) { | ||||||
|  |     try { | ||||||
|  |       const clonedBody = await req.text(); | ||||||
|  |       fetchOptions.body = clonedBody; | ||||||
|  |  | ||||||
|  |       const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||||||
|  |  | ||||||
|  |       // not undefined and is false | ||||||
|  |       if ( | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.Baidu as string, | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return NextResponse.json( | ||||||
|  |           { | ||||||
|  |             error: true, | ||||||
|  |             message: `you are not allowed to use ${jsonBody?.model} model`, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             status: 403, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(`[Baidu] filter`, e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   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 Response(res.body, { | ||||||
|  |       status: res.status, | ||||||
|  |       statusText: res.statusText, | ||||||
|  |       headers: newHeaders, | ||||||
|  |     }); | ||||||
|  |   } finally { | ||||||
|  |     clearTimeout(timeoutId); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								app/api/bytedance/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								app/api/bytedance/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | import { getServerSideConfig } from "@/app/config/server"; | ||||||
|  | import { | ||||||
|  |   BYTEDANCE_BASE_URL, | ||||||
|  |   ApiPath, | ||||||
|  |   ModelProvider, | ||||||
|  |   ServiceProvider, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { NextRequest, NextResponse } from "next/server"; | ||||||
|  | import { auth } from "@/app/api/auth"; | ||||||
|  | import { isModelAvailableInServer } from "@/app/utils/model"; | ||||||
|  |  | ||||||
|  | const serverConfig = getServerSideConfig(); | ||||||
|  |  | ||||||
|  | async function handle( | ||||||
|  |   req: NextRequest, | ||||||
|  |   { params }: { params: { path: string[] } }, | ||||||
|  | ) { | ||||||
|  |   console.log("[ByteDance Route] params ", params); | ||||||
|  |  | ||||||
|  |   if (req.method === "OPTIONS") { | ||||||
|  |     return NextResponse.json({ body: "OK" }, { status: 200 }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const authResult = auth(req, ModelProvider.Doubao); | ||||||
|  |   if (authResult.error) { | ||||||
|  |     return NextResponse.json(authResult, { | ||||||
|  |       status: 401, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const response = await request(req); | ||||||
|  |     return response; | ||||||
|  |   } catch (e) { | ||||||
|  |     console.error("[ByteDance] ", e); | ||||||
|  |     return NextResponse.json(prettyObject(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const GET = handle; | ||||||
|  | export const POST = handle; | ||||||
|  |  | ||||||
|  | export const runtime = "edge"; | ||||||
|  | export const preferredRegion = [ | ||||||
|  |   "arn1", | ||||||
|  |   "bom1", | ||||||
|  |   "cdg1", | ||||||
|  |   "cle1", | ||||||
|  |   "cpt1", | ||||||
|  |   "dub1", | ||||||
|  |   "fra1", | ||||||
|  |   "gru1", | ||||||
|  |   "hnd1", | ||||||
|  |   "iad1", | ||||||
|  |   "icn1", | ||||||
|  |   "kix1", | ||||||
|  |   "lhr1", | ||||||
|  |   "pdx1", | ||||||
|  |   "sfo1", | ||||||
|  |   "sin1", | ||||||
|  |   "syd1", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | async function request(req: NextRequest) { | ||||||
|  |   const controller = new AbortController(); | ||||||
|  |  | ||||||
|  |   let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, ""); | ||||||
|  |  | ||||||
|  |   let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL; | ||||||
|  |  | ||||||
|  |   if (!baseUrl.startsWith("http")) { | ||||||
|  |     baseUrl = `https://${baseUrl}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (baseUrl.endsWith("/")) { | ||||||
|  |     baseUrl = baseUrl.slice(0, -1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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", | ||||||
|  |       Authorization: req.headers.get("Authorization") ?? "", | ||||||
|  |     }, | ||||||
|  |     method: req.method, | ||||||
|  |     body: req.body, | ||||||
|  |     redirect: "manual", | ||||||
|  |     // @ts-ignore | ||||||
|  |     duplex: "half", | ||||||
|  |     signal: controller.signal, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // #1815 try to refuse some request to some models | ||||||
|  |   if (serverConfig.customModels && req.body) { | ||||||
|  |     try { | ||||||
|  |       const clonedBody = await req.text(); | ||||||
|  |       fetchOptions.body = clonedBody; | ||||||
|  |  | ||||||
|  |       const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||||||
|  |  | ||||||
|  |       // not undefined and is false | ||||||
|  |       if ( | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.ByteDance as string, | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         return NextResponse.json( | ||||||
|  |           { | ||||||
|  |             error: true, | ||||||
|  |             message: `you are not allowed to use ${jsonBody?.model} model`, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             status: 403, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(`[ByteDance] filter`, e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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 Response(res.body, { | ||||||
|  |       status: res.status, | ||||||
|  |       statusText: res.statusText, | ||||||
|  |       headers: newHeaders, | ||||||
|  |     }); | ||||||
|  |   } finally { | ||||||
|  |     clearTimeout(timeoutId); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,17 +1,24 @@ | |||||||
| import { NextRequest, NextResponse } from "next/server"; | import { NextRequest, NextResponse } from "next/server"; | ||||||
| import { getServerSideConfig } from "../config/server"; | import { getServerSideConfig } from "../config/server"; | ||||||
| import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant"; | import { | ||||||
| import { collectModelTable } from "../utils/model"; |   DEFAULT_MODELS, | ||||||
| import { makeAzurePath } from "../azure"; |   OPENAI_BASE_URL, | ||||||
|  |   GEMINI_BASE_URL, | ||||||
|  |   ServiceProvider, | ||||||
|  | } from "../constant"; | ||||||
|  | import { isModelAvailableInServer } from "../utils/model"; | ||||||
|  | import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; | ||||||
|  |  | ||||||
| const serverConfig = getServerSideConfig(); | const serverConfig = getServerSideConfig(); | ||||||
|  |  | ||||||
| export async function requestOpenai(req: NextRequest) { | export async function requestOpenai(req: NextRequest) { | ||||||
|   const controller = new AbortController(); |   const controller = new AbortController(); | ||||||
|  |  | ||||||
|  |   const isAzure = req.nextUrl.pathname.includes("azure/deployments"); | ||||||
|  |  | ||||||
|   var authValue, |   var authValue, | ||||||
|     authHeaderName = ""; |     authHeaderName = ""; | ||||||
|   if (serverConfig.isAzure) { |   if (isAzure) { | ||||||
|     authValue = |     authValue = | ||||||
|       req.headers |       req.headers | ||||||
|         .get("Authorization") |         .get("Authorization") | ||||||
| @@ -31,7 +38,7 @@ export async function requestOpenai(req: NextRequest) { | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   let baseUrl = |   let baseUrl = | ||||||
|     serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL; |     (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; | ||||||
|  |  | ||||||
|   if (!baseUrl.startsWith("http")) { |   if (!baseUrl.startsWith("http")) { | ||||||
|     baseUrl = `https://${baseUrl}`; |     baseUrl = `https://${baseUrl}`; | ||||||
| @@ -51,17 +58,46 @@ export async function requestOpenai(req: NextRequest) { | |||||||
|     10 * 60 * 1000, |     10 * 60 * 1000, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   if (serverConfig.isAzure) { |   if (isAzure) { | ||||||
|     if (!serverConfig.azureApiVersion) { |     const azureApiVersion = | ||||||
|       return NextResponse.json({ |       req?.nextUrl?.searchParams?.get("api-version") || | ||||||
|         error: true, |       serverConfig.azureApiVersion; | ||||||
|         message: `missing AZURE_API_VERSION in server env vars`, |     baseUrl = baseUrl.split("/deployments").shift() as string; | ||||||
|       }); |     path = `${req.nextUrl.pathname.replaceAll( | ||||||
|  |       "/api/azure/", | ||||||
|  |       "", | ||||||
|  |     )}?api-version=${azureApiVersion}`; | ||||||
|  |  | ||||||
|  |     // Forward compatibility: | ||||||
|  |     // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL | ||||||
|  |     // then using default '{deploy-id}' | ||||||
|  |     if (serverConfig.customModels && serverConfig.azureUrl) { | ||||||
|  |       const modelName = path.split("/")[1]; | ||||||
|  |       let realDeployName = ""; | ||||||
|  |       serverConfig.customModels | ||||||
|  |         .split(",") | ||||||
|  |         .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName)) | ||||||
|  |         .forEach((m) => { | ||||||
|  |           const [fullName, displayName] = m.split("="); | ||||||
|  |           const [_, providerName] = fullName.split("@"); | ||||||
|  |           if (providerName === "azure" && !displayName) { | ||||||
|  |             const [_, deployId] = (serverConfig?.azureUrl ?? "").split( | ||||||
|  |               "deployments/", | ||||||
|  |             ); | ||||||
|  |             if (deployId) { | ||||||
|  |               realDeployName = deployId; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       if (realDeployName) { | ||||||
|  |         console.log("[Replace with DeployId", realDeployName); | ||||||
|  |         path = path.replaceAll(modelName, realDeployName); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     path = makeAzurePath(path, serverConfig.azureApiVersion); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const fetchUrl = `${baseUrl}/${path}`; |   const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`); | ||||||
|  |   console.log("fetchUrl", fetchUrl); | ||||||
|   const fetchOptions: RequestInit = { |   const fetchOptions: RequestInit = { | ||||||
|     headers: { |     headers: { | ||||||
|       "Content-Type": "application/json", |       "Content-Type": "application/json", | ||||||
| @@ -83,17 +119,24 @@ export async function requestOpenai(req: NextRequest) { | |||||||
|   // #1815 try to refuse gpt4 request |   // #1815 try to refuse gpt4 request | ||||||
|   if (serverConfig.customModels && req.body) { |   if (serverConfig.customModels && req.body) { | ||||||
|     try { |     try { | ||||||
|       const modelTable = collectModelTable( |  | ||||||
|         DEFAULT_MODELS, |  | ||||||
|         serverConfig.customModels, |  | ||||||
|       ); |  | ||||||
|       const clonedBody = await req.text(); |       const clonedBody = await req.text(); | ||||||
|       fetchOptions.body = clonedBody; |       fetchOptions.body = clonedBody; | ||||||
|  |  | ||||||
|       const jsonBody = JSON.parse(clonedBody) as { model?: string }; |       const jsonBody = JSON.parse(clonedBody) as { model?: string }; | ||||||
|  |  | ||||||
|       // not undefined and is false |       // not undefined and is false | ||||||
|       if (modelTable[jsonBody?.model ?? ""].available === false) { |       if ( | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.OpenAI as string, | ||||||
|  |         ) || | ||||||
|  |         isModelAvailableInServer( | ||||||
|  |           serverConfig.customModels, | ||||||
|  |           jsonBody?.model as string, | ||||||
|  |           ServiceProvider.Azure as string, | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|         return NextResponse.json( |         return NextResponse.json( | ||||||
|           { |           { | ||||||
|             error: true, |             error: true, | ||||||
| @@ -129,7 +172,6 @@ export async function requestOpenai(req: NextRequest) { | |||||||
|     // to disable nginx buffering |     // to disable nginx buffering | ||||||
|     newHeaders.set("X-Accel-Buffering", "no"); |     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) |     // 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 |     // Also, this is to prevent the header from being sent to the client | ||||||
|     if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { |     if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { | ||||||
| @@ -142,7 +184,6 @@ export async function requestOpenai(req: NextRequest) { | |||||||
|     // The browser will try to decode the response with brotli and fail |     // The browser will try to decode the response with brotli and fail | ||||||
|     newHeaders.delete("content-encoding"); |     newHeaders.delete("content-encoding"); | ||||||
|  |  | ||||||
|  |  | ||||||
|     return new Response(res.body, { |     return new Response(res.body, { | ||||||
|       status: res.status, |       status: res.status, | ||||||
|       statusText: res.statusText, |       statusText: res.statusText, | ||||||
|   | |||||||
| @@ -63,7 +63,9 @@ async function handle( | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const fetchUrl = `${baseUrl}/${path}?key=${key}`; |   const fetchUrl = `${baseUrl}/${path}?key=${key}${ | ||||||
|  |     req?.nextUrl?.searchParams?.get("alt") == "sse" ? "&alt=sse" : "" | ||||||
|  |   }`; | ||||||
|   const fetchOptions: RequestInit = { |   const fetchOptions: RequestInit = { | ||||||
|     headers: { |     headers: { | ||||||
|       "Content-Type": "application/json", |       "Content-Type": "application/json", | ||||||
|   | |||||||
| @@ -1,14 +1,22 @@ | |||||||
| 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())); | ||||||
|  |  | ||||||
|  | const normalizeUrl = (url: string) => { | ||||||
|  |   try { | ||||||
|  |     return new URL(url); | ||||||
|  |   } catch (err) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| async function handle( | async function handle( | ||||||
|   req: NextRequest, |   req: NextRequest, | ||||||
|   { params }: { params: { path: string[] } }, |   { params }: { params: { path: string[] } }, | ||||||
| @@ -24,7 +32,15 @@ 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)) |     !endpoint || | ||||||
|  |     !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => { | ||||||
|  |       const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); | ||||||
|  |       const normalizedEndpoint = normalizeUrl(endpoint as string); | ||||||
|  |  | ||||||
|  |       return normalizedEndpoint && | ||||||
|  |         normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && | ||||||
|  |         normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname); | ||||||
|  |     }) | ||||||
|   ) { |   ) { | ||||||
|     return NextResponse.json( |     return NextResponse.json( | ||||||
|       { |       { | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| export function makeAzurePath(path: string, apiVersion: string) { |  | ||||||
|   // should omit /v1 prefix |  | ||||||
|   path = path.replaceAll("v1/", ""); |  | ||||||
|  |  | ||||||
|   // should add api-key to query string |  | ||||||
|   path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`; |  | ||||||
|  |  | ||||||
|   return path; |  | ||||||
| } |  | ||||||
| @@ -9,6 +9,10 @@ import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; | |||||||
| import { ChatGPTApi } from "./platforms/openai"; | import { ChatGPTApi } from "./platforms/openai"; | ||||||
| import { GeminiProApi } from "./platforms/google"; | import { GeminiProApi } from "./platforms/google"; | ||||||
| import { ClaudeApi } from "./platforms/anthropic"; | import { ClaudeApi } from "./platforms/anthropic"; | ||||||
|  | import { ErnieApi } from "./platforms/baidu"; | ||||||
|  | import { DoubaoApi } from "./platforms/bytedance"; | ||||||
|  | import { QwenApi } from "./platforms/alibaba"; | ||||||
|  |  | ||||||
| export const ROLES = ["system", "user", "assistant"] as const; | export const ROLES = ["system", "user", "assistant"] as const; | ||||||
| export type MessageRole = (typeof ROLES)[number]; | export type MessageRole = (typeof ROLES)[number]; | ||||||
|  |  | ||||||
| @@ -30,6 +34,7 @@ export interface RequestMessage { | |||||||
|  |  | ||||||
| export interface LLMConfig { | export interface LLMConfig { | ||||||
|   model: string; |   model: string; | ||||||
|  |   providerName?: string; | ||||||
|   temperature?: number; |   temperature?: number; | ||||||
|   top_p?: number; |   top_p?: number; | ||||||
|   stream?: boolean; |   stream?: boolean; | ||||||
| @@ -54,6 +59,7 @@ export interface LLMUsage { | |||||||
|  |  | ||||||
| export interface LLMModel { | export interface LLMModel { | ||||||
|   name: string; |   name: string; | ||||||
|  |   displayName?: string; | ||||||
|   available: boolean; |   available: boolean; | ||||||
|   provider: LLMModelProvider; |   provider: LLMModelProvider; | ||||||
| } | } | ||||||
| @@ -102,6 +108,15 @@ export class ClientApi { | |||||||
|       case ModelProvider.Claude: |       case ModelProvider.Claude: | ||||||
|         this.llm = new ClaudeApi(); |         this.llm = new ClaudeApi(); | ||||||
|         break; |         break; | ||||||
|  |       case ModelProvider.Ernie: | ||||||
|  |         this.llm = new ErnieApi(); | ||||||
|  |         break; | ||||||
|  |       case ModelProvider.Doubao: | ||||||
|  |         this.llm = new DoubaoApi(); | ||||||
|  |         break; | ||||||
|  |       case ModelProvider.Qwen: | ||||||
|  |         this.llm = new QwenApi(); | ||||||
|  |         break; | ||||||
|       default: |       default: | ||||||
|         this.llm = new ChatGPTApi(); |         this.llm = new ChatGPTApi(); | ||||||
|     } |     } | ||||||
| @@ -155,37 +170,100 @@ export class ClientApi { | |||||||
|  |  | ||||||
| export function getHeaders() { | export function getHeaders() { | ||||||
|   const accessStore = useAccessStore.getState(); |   const accessStore = useAccessStore.getState(); | ||||||
|  |   const chatStore = useChatStore.getState(); | ||||||
|   const headers: Record<string, string> = { |   const headers: Record<string, string> = { | ||||||
|     "Content-Type": "application/json", |     "Content-Type": "application/json", | ||||||
|     Accept: "application/json", |     Accept: "application/json", | ||||||
|   }; |   }; | ||||||
|   const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; |  | ||||||
|   const isGoogle = modelConfig.model.startsWith("gemini"); |   const clientConfig = getClientConfig(); | ||||||
|   const isAzure = accessStore.provider === ServiceProvider.Azure; |  | ||||||
|   const authHeader = isAzure ? "api-key" : "Authorization"; |   function getConfig() { | ||||||
|  |     const modelConfig = chatStore.currentSession().mask.modelConfig; | ||||||
|  |     const isGoogle = modelConfig.providerName == ServiceProvider.Google; | ||||||
|  |     const isAzure = modelConfig.providerName === ServiceProvider.Azure; | ||||||
|  |     const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; | ||||||
|  |     const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; | ||||||
|  |     const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance; | ||||||
|  |     const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba; | ||||||
|  |     const isEnabledAccessControl = accessStore.enabledAccessControl(); | ||||||
|     const apiKey = isGoogle |     const apiKey = isGoogle | ||||||
|       ? accessStore.googleApiKey |       ? accessStore.googleApiKey | ||||||
|       : isAzure |       : isAzure | ||||||
|       ? accessStore.azureApiKey |       ? accessStore.azureApiKey | ||||||
|  |       : isAnthropic | ||||||
|  |       ? accessStore.anthropicApiKey | ||||||
|  |       : isByteDance | ||||||
|  |       ? accessStore.bytedanceApiKey | ||||||
|  |       : isAlibaba | ||||||
|  |       ? accessStore.alibabaApiKey | ||||||
|       : accessStore.openaiApiKey; |       : accessStore.openaiApiKey; | ||||||
|   const clientConfig = getClientConfig(); |     return { | ||||||
|   const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`; |       isGoogle, | ||||||
|   const validString = (x: string) => x && x.length > 0; |       isAzure, | ||||||
|  |       isAnthropic, | ||||||
|  |       isBaidu, | ||||||
|  |       isByteDance, | ||||||
|  |       isAlibaba, | ||||||
|  |       apiKey, | ||||||
|  |       isEnabledAccessControl, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getAuthHeader(): string { | ||||||
|  |     return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getBearerToken(apiKey: string, noBearer: boolean = false): string { | ||||||
|  |     return validString(apiKey) | ||||||
|  |       ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}` | ||||||
|  |       : ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function validString(x: string): boolean { | ||||||
|  |     return x?.length > 0; | ||||||
|  |   } | ||||||
|  |   const { | ||||||
|  |     isGoogle, | ||||||
|  |     isAzure, | ||||||
|  |     isAnthropic, | ||||||
|  |     isBaidu, | ||||||
|  |     apiKey, | ||||||
|  |     isEnabledAccessControl, | ||||||
|  |   } = getConfig(); | ||||||
|   // when using google api in app, not set auth header |   // when using google api in app, not set auth header | ||||||
|   if (!(isGoogle && clientConfig?.isApp)) { |   if (isGoogle && clientConfig?.isApp) return headers; | ||||||
|     // use user's api key first |   // when using baidu api in app, not set auth header | ||||||
|     if (validString(apiKey)) { |   if (isBaidu && clientConfig?.isApp) return headers; | ||||||
|       headers[authHeader] = makeBearer(apiKey); |  | ||||||
|     } else if ( |   const authHeader = getAuthHeader(); | ||||||
|       accessStore.enabledAccessControl() && |  | ||||||
|       validString(accessStore.accessCode) |   const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic); | ||||||
|     ) { |  | ||||||
|       headers[authHeader] = makeBearer( |   if (bearerToken) { | ||||||
|  |     headers[authHeader] = bearerToken; | ||||||
|  |   } else if (isEnabledAccessControl && validString(accessStore.accessCode)) { | ||||||
|  |     headers["Authorization"] = getBearerToken( | ||||||
|       ACCESS_CODE_PREFIX + accessStore.accessCode, |       ACCESS_CODE_PREFIX + accessStore.accessCode, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return headers; |   return headers; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function getClientApi(provider: ServiceProvider): ClientApi { | ||||||
|  |   switch (provider) { | ||||||
|  |     case ServiceProvider.Google: | ||||||
|  |       return new ClientApi(ModelProvider.GeminiPro); | ||||||
|  |     case ServiceProvider.Anthropic: | ||||||
|  |       return new ClientApi(ModelProvider.Claude); | ||||||
|  |     case ServiceProvider.Baidu: | ||||||
|  |       return new ClientApi(ModelProvider.Ernie); | ||||||
|  |     case ServiceProvider.ByteDance: | ||||||
|  |       return new ClientApi(ModelProvider.Doubao); | ||||||
|  |     case ServiceProvider.Alibaba: | ||||||
|  |       return new ClientApi(ModelProvider.Qwen); | ||||||
|  |     default: | ||||||
|  |       return new ClientApi(ModelProvider.GPT); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										268
									
								
								app/client/platforms/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								app/client/platforms/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | |||||||
|  | "use client"; | ||||||
|  | import { | ||||||
|  |   ApiPath, | ||||||
|  |   Alibaba, | ||||||
|  |   ALIBABA_BASE_URL, | ||||||
|  |   REQUEST_TIMEOUT_MS, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   ChatOptions, | ||||||
|  |   getHeaders, | ||||||
|  |   LLMApi, | ||||||
|  |   LLMModel, | ||||||
|  |   MultimodalContent, | ||||||
|  | } from "../api"; | ||||||
|  | import Locale from "../../locales"; | ||||||
|  | import { | ||||||
|  |   EventStreamContentType, | ||||||
|  |   fetchEventSource, | ||||||
|  | } from "@fortaine/fetch-event-source"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { getClientConfig } from "@/app/config/client"; | ||||||
|  | import { getMessageTextContent, isVisionModel } from "@/app/utils"; | ||||||
|  |  | ||||||
|  | export interface OpenAIListModelResponse { | ||||||
|  |   object: string; | ||||||
|  |   data: Array<{ | ||||||
|  |     id: string; | ||||||
|  |     object: string; | ||||||
|  |     root: string; | ||||||
|  |   }>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface RequestInput { | ||||||
|  |   messages: { | ||||||
|  |     role: "system" | "user" | "assistant"; | ||||||
|  |     content: string | MultimodalContent[]; | ||||||
|  |   }[]; | ||||||
|  | } | ||||||
|  | interface RequestParam { | ||||||
|  |   result_format: string; | ||||||
|  |   incremental_output?: boolean; | ||||||
|  |   temperature: number; | ||||||
|  |   repetition_penalty?: number; | ||||||
|  |   top_p: number; | ||||||
|  |   max_tokens?: number; | ||||||
|  | } | ||||||
|  | interface RequestPayload { | ||||||
|  |   model: string; | ||||||
|  |   input: RequestInput; | ||||||
|  |   parameters: RequestParam; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class QwenApi implements LLMApi { | ||||||
|  |   path(path: string): string { | ||||||
|  |     const accessStore = useAccessStore.getState(); | ||||||
|  |  | ||||||
|  |     let baseUrl = ""; | ||||||
|  |  | ||||||
|  |     if (accessStore.useCustomConfig) { | ||||||
|  |       baseUrl = accessStore.alibabaUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.length === 0) { | ||||||
|  |       const isApp = !!getClientConfig()?.isApp; | ||||||
|  |       baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.endsWith("/")) { | ||||||
|  |       baseUrl = baseUrl.slice(0, baseUrl.length - 1); | ||||||
|  |     } | ||||||
|  |     if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) { | ||||||
|  |       baseUrl = "https://" + baseUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log("[Proxy Endpoint] ", baseUrl, path); | ||||||
|  |  | ||||||
|  |     return [baseUrl, path].join("/"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   extractMessage(res: any) { | ||||||
|  |     return res?.output?.choices?.at(0)?.message?.content ?? ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async chat(options: ChatOptions) { | ||||||
|  |     const messages = options.messages.map((v) => ({ | ||||||
|  |       role: v.role, | ||||||
|  |       content: getMessageTextContent(v), | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     const modelConfig = { | ||||||
|  |       ...useAppConfig.getState().modelConfig, | ||||||
|  |       ...useChatStore.getState().currentSession().mask.modelConfig, | ||||||
|  |       ...{ | ||||||
|  |         model: options.config.model, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const shouldStream = !!options.config.stream; | ||||||
|  |     const requestPayload: RequestPayload = { | ||||||
|  |       model: modelConfig.model, | ||||||
|  |       input: { | ||||||
|  |         messages, | ||||||
|  |       }, | ||||||
|  |       parameters: { | ||||||
|  |         result_format: "message", | ||||||
|  |         incremental_output: shouldStream, | ||||||
|  |         temperature: modelConfig.temperature, | ||||||
|  |         // max_tokens: modelConfig.max_tokens, | ||||||
|  |         top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1 | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const controller = new AbortController(); | ||||||
|  |     options.onController?.(controller); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const chatPath = this.path(Alibaba.ChatPath); | ||||||
|  |       const chatPayload = { | ||||||
|  |         method: "POST", | ||||||
|  |         body: JSON.stringify(requestPayload), | ||||||
|  |         signal: controller.signal, | ||||||
|  |         headers: { | ||||||
|  |           ...getHeaders(), | ||||||
|  |           "X-DashScope-SSE": shouldStream ? "enable" : "disable", | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // make a fetch request | ||||||
|  |       const requestTimeoutId = setTimeout( | ||||||
|  |         () => controller.abort(), | ||||||
|  |         REQUEST_TIMEOUT_MS, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (shouldStream) { | ||||||
|  |         let responseText = ""; | ||||||
|  |         let remainText = ""; | ||||||
|  |         let finished = false; | ||||||
|  |  | ||||||
|  |         // animate response to make it looks smooth | ||||||
|  |         function animateResponseText() { | ||||||
|  |           if (finished || controller.signal.aborted) { | ||||||
|  |             responseText += remainText; | ||||||
|  |             console.log("[Response Animation] finished"); | ||||||
|  |             if (responseText?.length === 0) { | ||||||
|  |               options.onError?.(new Error("empty response from server")); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           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); | ||||||
|  |             options.onUpdate?.(responseText, fetchText); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           requestAnimationFrame(animateResponseText); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // start animaion | ||||||
|  |         animateResponseText(); | ||||||
|  |  | ||||||
|  |         const finish = () => { | ||||||
|  |           if (!finished) { | ||||||
|  |             finished = true; | ||||||
|  |             options.onFinish(responseText + remainText); | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         controller.signal.onabort = finish; | ||||||
|  |  | ||||||
|  |         fetchEventSource(chatPath, { | ||||||
|  |           ...chatPayload, | ||||||
|  |           async onopen(res) { | ||||||
|  |             clearTimeout(requestTimeoutId); | ||||||
|  |             const contentType = res.headers.get("content-type"); | ||||||
|  |             console.log( | ||||||
|  |               "[Alibaba] request response content type: ", | ||||||
|  |               contentType, | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             if (contentType?.startsWith("text/plain")) { | ||||||
|  |               responseText = await res.clone().text(); | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |               !res.ok || | ||||||
|  |               !res.headers | ||||||
|  |                 .get("content-type") | ||||||
|  |                 ?.startsWith(EventStreamContentType) || | ||||||
|  |               res.status !== 200 | ||||||
|  |             ) { | ||||||
|  |               const responseTexts = [responseText]; | ||||||
|  |               let extraInfo = await res.clone().text(); | ||||||
|  |               try { | ||||||
|  |                 const resJson = await res.clone().json(); | ||||||
|  |                 extraInfo = prettyObject(resJson); | ||||||
|  |               } catch {} | ||||||
|  |  | ||||||
|  |               if (res.status === 401) { | ||||||
|  |                 responseTexts.push(Locale.Error.Unauthorized); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               if (extraInfo) { | ||||||
|  |                 responseTexts.push(extraInfo); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               responseText = responseTexts.join("\n\n"); | ||||||
|  |  | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onmessage(msg) { | ||||||
|  |             if (msg.data === "[DONE]" || finished) { | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |             const text = msg.data; | ||||||
|  |             try { | ||||||
|  |               const json = JSON.parse(text); | ||||||
|  |               const choices = json.output.choices as Array<{ | ||||||
|  |                 message: { content: string }; | ||||||
|  |               }>; | ||||||
|  |               const delta = choices[0]?.message?.content; | ||||||
|  |               if (delta) { | ||||||
|  |                 remainText += delta; | ||||||
|  |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               console.error("[Request] parse error", text, msg); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onclose() { | ||||||
|  |             finish(); | ||||||
|  |           }, | ||||||
|  |           onerror(e) { | ||||||
|  |             options.onError?.(e); | ||||||
|  |             throw e; | ||||||
|  |           }, | ||||||
|  |           openWhenHidden: true, | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         const res = await fetch(chatPath, chatPayload); | ||||||
|  |         clearTimeout(requestTimeoutId); | ||||||
|  |  | ||||||
|  |         const resJson = await res.json(); | ||||||
|  |         const message = this.extractMessage(resJson); | ||||||
|  |         options.onFinish(message); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.log("[Request] failed to make a chat request", e); | ||||||
|  |       options.onError?.(e as Error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   async usage() { | ||||||
|  |     return { | ||||||
|  |       used: 0, | ||||||
|  |       total: 0, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async models(): Promise<LLMModel[]> { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export { Alibaba }; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; | import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; | ||||||
| import { ChatOptions, LLMApi, MultimodalContent } from "../api"; | import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api"; | ||||||
| import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
| import { getClientConfig } from "@/app/config/client"; | import { getClientConfig } from "@/app/config/client"; | ||||||
| import { DEFAULT_API_HOST } from "@/app/constant"; | import { DEFAULT_API_HOST } from "@/app/constant"; | ||||||
| @@ -12,6 +12,7 @@ import { | |||||||
| import Locale from "../../locales"; | import Locale from "../../locales"; | ||||||
| import { prettyObject } from "@/app/utils/format"; | import { prettyObject } from "@/app/utils/format"; | ||||||
| import { getMessageTextContent, isVisionModel } from "@/app/utils"; | import { getMessageTextContent, isVisionModel } from "@/app/utils"; | ||||||
|  | import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; | ||||||
|  |  | ||||||
| export type MultiBlockContent = { | export type MultiBlockContent = { | ||||||
|   type: "image" | "text"; |   type: "image" | "text"; | ||||||
| @@ -161,6 +162,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, | ||||||
| @@ -183,11 +191,10 @@ export class ClaudeApi implements LLMApi { | |||||||
|       body: JSON.stringify(requestBody), |       body: JSON.stringify(requestBody), | ||||||
|       signal: controller.signal, |       signal: controller.signal, | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": "application/json", |         ...getHeaders(), // get common headers | ||||||
|         Accept: "application/json", |  | ||||||
|         "x-api-key": accessStore.anthropicApiKey, |  | ||||||
|         "anthropic-version": accessStore.anthropicApiVersion, |         "anthropic-version": accessStore.anthropicApiVersion, | ||||||
|         Authorization: getAuthKey(accessStore.anthropicApiKey), |         // do not send `anthropicApiKey` in browser!!! | ||||||
|  |         // Authorization: getAuthKey(accessStore.anthropicApiKey), | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -369,7 +376,8 @@ export class ClaudeApi implements LLMApi { | |||||||
|  |  | ||||||
|     baseUrl = trimEnd(baseUrl, "/"); |     baseUrl = trimEnd(baseUrl, "/"); | ||||||
|  |  | ||||||
|     return `${baseUrl}/${path}`; |     // try rebuild url, when using cloudflare ai gateway in client | ||||||
|  |     return cloudflareAIGatewayUrl(`${baseUrl}/${path}`); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -382,27 +390,3 @@ function trimEnd(s: string, end = " ") { | |||||||
|  |  | ||||||
|   return s; |   return s; | ||||||
| } | } | ||||||
|  |  | ||||||
| function bearer(value: string) { |  | ||||||
|   return `Bearer ${value.trim()}`; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function getAuthKey(apiKey = "") { |  | ||||||
|   const accessStore = useAccessStore.getState(); |  | ||||||
|   const isApp = !!getClientConfig()?.isApp; |  | ||||||
|   let authKey = ""; |  | ||||||
|  |  | ||||||
|   if (apiKey) { |  | ||||||
|     // use user's api key first |  | ||||||
|     authKey = bearer(apiKey); |  | ||||||
|   } else if ( |  | ||||||
|     accessStore.enabledAccessControl() && |  | ||||||
|     !isApp && |  | ||||||
|     !!accessStore.accessCode |  | ||||||
|   ) { |  | ||||||
|     // or use access code |  | ||||||
|     authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return authKey; |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										273
									
								
								app/client/platforms/baidu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								app/client/platforms/baidu.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | |||||||
|  | "use client"; | ||||||
|  | import { | ||||||
|  |   ApiPath, | ||||||
|  |   Baidu, | ||||||
|  |   BAIDU_BASE_URL, | ||||||
|  |   REQUEST_TIMEOUT_MS, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
|  | import { getAccessToken } from "@/app/utils/baidu"; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   ChatOptions, | ||||||
|  |   getHeaders, | ||||||
|  |   LLMApi, | ||||||
|  |   LLMModel, | ||||||
|  |   MultimodalContent, | ||||||
|  | } from "../api"; | ||||||
|  | import Locale from "../../locales"; | ||||||
|  | import { | ||||||
|  |   EventStreamContentType, | ||||||
|  |   fetchEventSource, | ||||||
|  | } from "@fortaine/fetch-event-source"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { getClientConfig } from "@/app/config/client"; | ||||||
|  | import { getMessageTextContent } from "@/app/utils"; | ||||||
|  |  | ||||||
|  | export interface OpenAIListModelResponse { | ||||||
|  |   object: string; | ||||||
|  |   data: Array<{ | ||||||
|  |     id: string; | ||||||
|  |     object: string; | ||||||
|  |     root: string; | ||||||
|  |   }>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class ErnieApi implements LLMApi { | ||||||
|  |   path(path: string): string { | ||||||
|  |     const accessStore = useAccessStore.getState(); | ||||||
|  |  | ||||||
|  |     let baseUrl = ""; | ||||||
|  |  | ||||||
|  |     if (accessStore.useCustomConfig) { | ||||||
|  |       baseUrl = accessStore.baiduUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.length === 0) { | ||||||
|  |       const isApp = !!getClientConfig()?.isApp; | ||||||
|  |       // do not use proxy for baidubce api | ||||||
|  |       baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.endsWith("/")) { | ||||||
|  |       baseUrl = baseUrl.slice(0, baseUrl.length - 1); | ||||||
|  |     } | ||||||
|  |     if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) { | ||||||
|  |       baseUrl = "https://" + baseUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log("[Proxy Endpoint] ", baseUrl, path); | ||||||
|  |  | ||||||
|  |     return [baseUrl, path].join("/"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async chat(options: ChatOptions) { | ||||||
|  |     const messages = options.messages.map((v) => ({ | ||||||
|  |       role: v.role, | ||||||
|  |       content: getMessageTextContent(v), | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     // "error_code": 336006, "error_msg": "the length of messages must be an odd number", | ||||||
|  |     if (messages.length % 2 === 0) { | ||||||
|  |       messages.unshift({ | ||||||
|  |         role: "user", | ||||||
|  |         content: " ", | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const modelConfig = { | ||||||
|  |       ...useAppConfig.getState().modelConfig, | ||||||
|  |       ...useChatStore.getState().currentSession().mask.modelConfig, | ||||||
|  |       ...{ | ||||||
|  |         model: options.config.model, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const shouldStream = !!options.config.stream; | ||||||
|  |     const requestPayload: RequestPayload = { | ||||||
|  |       messages, | ||||||
|  |       stream: shouldStream, | ||||||
|  |       model: modelConfig.model, | ||||||
|  |       temperature: modelConfig.temperature, | ||||||
|  |       presence_penalty: modelConfig.presence_penalty, | ||||||
|  |       frequency_penalty: modelConfig.frequency_penalty, | ||||||
|  |       top_p: modelConfig.top_p, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     console.log("[Request] Baidu payload: ", requestPayload); | ||||||
|  |  | ||||||
|  |     const controller = new AbortController(); | ||||||
|  |     options.onController?.(controller); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       let chatPath = this.path(Baidu.ChatPath(modelConfig.model)); | ||||||
|  |  | ||||||
|  |       // getAccessToken can not run in browser, because cors error | ||||||
|  |       if (!!getClientConfig()?.isApp) { | ||||||
|  |         const accessStore = useAccessStore.getState(); | ||||||
|  |         if (accessStore.useCustomConfig) { | ||||||
|  |           if (accessStore.isValidBaidu()) { | ||||||
|  |             const { access_token } = await getAccessToken( | ||||||
|  |               accessStore.baiduApiKey, | ||||||
|  |               accessStore.baiduSecretKey, | ||||||
|  |             ); | ||||||
|  |             chatPath = `${chatPath}${ | ||||||
|  |               chatPath.includes("?") ? "&" : "?" | ||||||
|  |             }access_token=${access_token}`; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       const chatPayload = { | ||||||
|  |         method: "POST", | ||||||
|  |         body: JSON.stringify(requestPayload), | ||||||
|  |         signal: controller.signal, | ||||||
|  |         headers: getHeaders(), | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // make a fetch request | ||||||
|  |       const requestTimeoutId = setTimeout( | ||||||
|  |         () => controller.abort(), | ||||||
|  |         REQUEST_TIMEOUT_MS, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (shouldStream) { | ||||||
|  |         let responseText = ""; | ||||||
|  |         let remainText = ""; | ||||||
|  |         let finished = false; | ||||||
|  |  | ||||||
|  |         // animate response to make it looks smooth | ||||||
|  |         function animateResponseText() { | ||||||
|  |           if (finished || controller.signal.aborted) { | ||||||
|  |             responseText += remainText; | ||||||
|  |             console.log("[Response Animation] finished"); | ||||||
|  |             if (responseText?.length === 0) { | ||||||
|  |               options.onError?.(new Error("empty response from server")); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           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); | ||||||
|  |             options.onUpdate?.(responseText, fetchText); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           requestAnimationFrame(animateResponseText); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // start animaion | ||||||
|  |         animateResponseText(); | ||||||
|  |  | ||||||
|  |         const finish = () => { | ||||||
|  |           if (!finished) { | ||||||
|  |             finished = true; | ||||||
|  |             options.onFinish(responseText + remainText); | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         controller.signal.onabort = finish; | ||||||
|  |  | ||||||
|  |         fetchEventSource(chatPath, { | ||||||
|  |           ...chatPayload, | ||||||
|  |           async onopen(res) { | ||||||
|  |             clearTimeout(requestTimeoutId); | ||||||
|  |             const contentType = res.headers.get("content-type"); | ||||||
|  |             console.log("[Baidu] request response content type: ", contentType); | ||||||
|  |  | ||||||
|  |             if (contentType?.startsWith("text/plain")) { | ||||||
|  |               responseText = await res.clone().text(); | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |               !res.ok || | ||||||
|  |               !res.headers | ||||||
|  |                 .get("content-type") | ||||||
|  |                 ?.startsWith(EventStreamContentType) || | ||||||
|  |               res.status !== 200 | ||||||
|  |             ) { | ||||||
|  |               const responseTexts = [responseText]; | ||||||
|  |               let extraInfo = await res.clone().text(); | ||||||
|  |               try { | ||||||
|  |                 const resJson = await res.clone().json(); | ||||||
|  |                 extraInfo = prettyObject(resJson); | ||||||
|  |               } catch {} | ||||||
|  |  | ||||||
|  |               if (res.status === 401) { | ||||||
|  |                 responseTexts.push(Locale.Error.Unauthorized); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               if (extraInfo) { | ||||||
|  |                 responseTexts.push(extraInfo); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               responseText = responseTexts.join("\n\n"); | ||||||
|  |  | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onmessage(msg) { | ||||||
|  |             if (msg.data === "[DONE]" || finished) { | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |             const text = msg.data; | ||||||
|  |             try { | ||||||
|  |               const json = JSON.parse(text); | ||||||
|  |               const delta = json?.result; | ||||||
|  |               if (delta) { | ||||||
|  |                 remainText += delta; | ||||||
|  |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               console.error("[Request] parse error", text, msg); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onclose() { | ||||||
|  |             finish(); | ||||||
|  |           }, | ||||||
|  |           onerror(e) { | ||||||
|  |             options.onError?.(e); | ||||||
|  |             throw e; | ||||||
|  |           }, | ||||||
|  |           openWhenHidden: true, | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         const res = await fetch(chatPath, chatPayload); | ||||||
|  |         clearTimeout(requestTimeoutId); | ||||||
|  |  | ||||||
|  |         const resJson = await res.json(); | ||||||
|  |         const message = resJson?.result; | ||||||
|  |         options.onFinish(message); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.log("[Request] failed to make a chat request", e); | ||||||
|  |       options.onError?.(e as Error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   async usage() { | ||||||
|  |     return { | ||||||
|  |       used: 0, | ||||||
|  |       total: 0, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async models(): Promise<LLMModel[]> { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export { Baidu }; | ||||||
							
								
								
									
										255
									
								
								app/client/platforms/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								app/client/platforms/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | "use client"; | ||||||
|  | import { | ||||||
|  |   ApiPath, | ||||||
|  |   ByteDance, | ||||||
|  |   BYTEDANCE_BASE_URL, | ||||||
|  |   REQUEST_TIMEOUT_MS, | ||||||
|  | } from "@/app/constant"; | ||||||
|  | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   ChatOptions, | ||||||
|  |   getHeaders, | ||||||
|  |   LLMApi, | ||||||
|  |   LLMModel, | ||||||
|  |   MultimodalContent, | ||||||
|  | } from "../api"; | ||||||
|  | import Locale from "../../locales"; | ||||||
|  | import { | ||||||
|  |   EventStreamContentType, | ||||||
|  |   fetchEventSource, | ||||||
|  | } from "@fortaine/fetch-event-source"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
|  | import { getClientConfig } from "@/app/config/client"; | ||||||
|  | import { getMessageTextContent } from "@/app/utils"; | ||||||
|  |  | ||||||
|  | export interface OpenAIListModelResponse { | ||||||
|  |   object: string; | ||||||
|  |   data: Array<{ | ||||||
|  |     id: string; | ||||||
|  |     object: string; | ||||||
|  |     root: string; | ||||||
|  |   }>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class DoubaoApi implements LLMApi { | ||||||
|  |   path(path: string): string { | ||||||
|  |     const accessStore = useAccessStore.getState(); | ||||||
|  |  | ||||||
|  |     let baseUrl = ""; | ||||||
|  |  | ||||||
|  |     if (accessStore.useCustomConfig) { | ||||||
|  |       baseUrl = accessStore.bytedanceUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.length === 0) { | ||||||
|  |       const isApp = !!getClientConfig()?.isApp; | ||||||
|  |       baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (baseUrl.endsWith("/")) { | ||||||
|  |       baseUrl = baseUrl.slice(0, baseUrl.length - 1); | ||||||
|  |     } | ||||||
|  |     if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) { | ||||||
|  |       baseUrl = "https://" + baseUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log("[Proxy Endpoint] ", baseUrl, path); | ||||||
|  |  | ||||||
|  |     return [baseUrl, path].join("/"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   extractMessage(res: any) { | ||||||
|  |     return res.choices?.at(0)?.message?.content ?? ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async chat(options: ChatOptions) { | ||||||
|  |     const messages = options.messages.map((v) => ({ | ||||||
|  |       role: v.role, | ||||||
|  |       content: getMessageTextContent(v), | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     const modelConfig = { | ||||||
|  |       ...useAppConfig.getState().modelConfig, | ||||||
|  |       ...useChatStore.getState().currentSession().mask.modelConfig, | ||||||
|  |       ...{ | ||||||
|  |         model: options.config.model, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const shouldStream = !!options.config.stream; | ||||||
|  |     const requestPayload: RequestPayload = { | ||||||
|  |       messages, | ||||||
|  |       stream: shouldStream, | ||||||
|  |       model: modelConfig.model, | ||||||
|  |       temperature: modelConfig.temperature, | ||||||
|  |       presence_penalty: modelConfig.presence_penalty, | ||||||
|  |       frequency_penalty: modelConfig.frequency_penalty, | ||||||
|  |       top_p: modelConfig.top_p, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const controller = new AbortController(); | ||||||
|  |     options.onController?.(controller); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const chatPath = this.path(ByteDance.ChatPath); | ||||||
|  |       const chatPayload = { | ||||||
|  |         method: "POST", | ||||||
|  |         body: JSON.stringify(requestPayload), | ||||||
|  |         signal: controller.signal, | ||||||
|  |         headers: getHeaders(), | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // make a fetch request | ||||||
|  |       const requestTimeoutId = setTimeout( | ||||||
|  |         () => controller.abort(), | ||||||
|  |         REQUEST_TIMEOUT_MS, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (shouldStream) { | ||||||
|  |         let responseText = ""; | ||||||
|  |         let remainText = ""; | ||||||
|  |         let finished = false; | ||||||
|  |  | ||||||
|  |         // animate response to make it looks smooth | ||||||
|  |         function animateResponseText() { | ||||||
|  |           if (finished || controller.signal.aborted) { | ||||||
|  |             responseText += remainText; | ||||||
|  |             console.log("[Response Animation] finished"); | ||||||
|  |             if (responseText?.length === 0) { | ||||||
|  |               options.onError?.(new Error("empty response from server")); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           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); | ||||||
|  |             options.onUpdate?.(responseText, fetchText); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           requestAnimationFrame(animateResponseText); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // start animaion | ||||||
|  |         animateResponseText(); | ||||||
|  |  | ||||||
|  |         const finish = () => { | ||||||
|  |           if (!finished) { | ||||||
|  |             finished = true; | ||||||
|  |             options.onFinish(responseText + remainText); | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         controller.signal.onabort = finish; | ||||||
|  |  | ||||||
|  |         fetchEventSource(chatPath, { | ||||||
|  |           ...chatPayload, | ||||||
|  |           async onopen(res) { | ||||||
|  |             clearTimeout(requestTimeoutId); | ||||||
|  |             const contentType = res.headers.get("content-type"); | ||||||
|  |             console.log( | ||||||
|  |               "[ByteDance] request response content type: ", | ||||||
|  |               contentType, | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             if (contentType?.startsWith("text/plain")) { | ||||||
|  |               responseText = await res.clone().text(); | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |               !res.ok || | ||||||
|  |               !res.headers | ||||||
|  |                 .get("content-type") | ||||||
|  |                 ?.startsWith(EventStreamContentType) || | ||||||
|  |               res.status !== 200 | ||||||
|  |             ) { | ||||||
|  |               const responseTexts = [responseText]; | ||||||
|  |               let extraInfo = await res.clone().text(); | ||||||
|  |               try { | ||||||
|  |                 const resJson = await res.clone().json(); | ||||||
|  |                 extraInfo = prettyObject(resJson); | ||||||
|  |               } catch {} | ||||||
|  |  | ||||||
|  |               if (res.status === 401) { | ||||||
|  |                 responseTexts.push(Locale.Error.Unauthorized); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               if (extraInfo) { | ||||||
|  |                 responseTexts.push(extraInfo); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               responseText = responseTexts.join("\n\n"); | ||||||
|  |  | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onmessage(msg) { | ||||||
|  |             if (msg.data === "[DONE]" || finished) { | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |             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) { | ||||||
|  |                 remainText += delta; | ||||||
|  |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               console.error("[Request] parse error", text, msg); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onclose() { | ||||||
|  |             finish(); | ||||||
|  |           }, | ||||||
|  |           onerror(e) { | ||||||
|  |             options.onError?.(e); | ||||||
|  |             throw e; | ||||||
|  |           }, | ||||||
|  |           openWhenHidden: true, | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         const res = await fetch(chatPath, chatPayload); | ||||||
|  |         clearTimeout(requestTimeoutId); | ||||||
|  |  | ||||||
|  |         const resJson = await res.json(); | ||||||
|  |         const message = this.extractMessage(resJson); | ||||||
|  |         options.onFinish(message); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.log("[Request] failed to make a chat request", e); | ||||||
|  |       options.onError?.(e as Error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   async usage() { | ||||||
|  |     return { | ||||||
|  |       used: 0, | ||||||
|  |       total: 0, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async models(): Promise<LLMModel[]> { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export { ByteDance }; | ||||||
| @@ -3,6 +3,12 @@ import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; | |||||||
| import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
| import { getClientConfig } from "@/app/config/client"; | import { getClientConfig } from "@/app/config/client"; | ||||||
| import { DEFAULT_API_HOST } from "@/app/constant"; | import { DEFAULT_API_HOST } from "@/app/constant"; | ||||||
|  | import Locale from "../../locales"; | ||||||
|  | import { | ||||||
|  |   EventStreamContentType, | ||||||
|  |   fetchEventSource, | ||||||
|  | } from "@fortaine/fetch-event-source"; | ||||||
|  | import { prettyObject } from "@/app/utils/format"; | ||||||
| import { | import { | ||||||
|   getMessageTextContent, |   getMessageTextContent, | ||||||
|   getMessageImages, |   getMessageImages, | ||||||
| @@ -20,7 +26,7 @@ export class GeminiProApi implements LLMApi { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   async chat(options: ChatOptions): Promise<void> { |   async chat(options: ChatOptions): Promise<void> { | ||||||
|     // const apiClient = this; |     const apiClient = this; | ||||||
|     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) }]; | ||||||
| @@ -116,14 +122,13 @@ export class GeminiProApi implements LLMApi { | |||||||
|     const controller = new AbortController(); |     const controller = new AbortController(); | ||||||
|     options.onController?.(controller); |     options.onController?.(controller); | ||||||
|     try { |     try { | ||||||
|       // let baseUrl = accessStore.googleUrl; |       if (!baseUrl && isApp) { | ||||||
|  |         baseUrl = DEFAULT_API_HOST + "/api/proxy/google/"; | ||||||
|       if (!baseUrl) { |  | ||||||
|         baseUrl = isApp |  | ||||||
|           ? DEFAULT_API_HOST + "/api/proxy/google/" + Google.ChatPath(modelConfig.model) |  | ||||||
|           : this.path(Google.ChatPath(modelConfig.model)); |  | ||||||
|       } |       } | ||||||
|  |       baseUrl = `${baseUrl}/${Google.ChatPath(modelConfig.model)}`.replaceAll( | ||||||
|  |         "//", | ||||||
|  |         "/", | ||||||
|  |       ); | ||||||
|       if (isApp) { |       if (isApp) { | ||||||
|         baseUrl += `?key=${accessStore.googleApiKey}`; |         baseUrl += `?key=${accessStore.googleApiKey}`; | ||||||
|       } |       } | ||||||
| @@ -145,10 +150,11 @@ export class GeminiProApi implements LLMApi { | |||||||
|         let remainText = ""; |         let remainText = ""; | ||||||
|         let finished = false; |         let finished = false; | ||||||
|  |  | ||||||
|         let existingTexts: string[] = []; |  | ||||||
|         const finish = () => { |         const finish = () => { | ||||||
|  |           if (!finished) { | ||||||
|             finished = true; |             finished = true; | ||||||
|           options.onFinish(existingTexts.join("")); |             options.onFinish(responseText + remainText); | ||||||
|  |           } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // animate response to make it looks smooth |         // animate response to make it looks smooth | ||||||
| @@ -173,71 +179,84 @@ export class GeminiProApi implements LLMApi { | |||||||
|         // start animaion |         // start animaion | ||||||
|         animateResponseText(); |         animateResponseText(); | ||||||
|  |  | ||||||
|         fetch( |         controller.signal.onabort = finish; | ||||||
|           baseUrl.replace("generateContent", "streamGenerateContent"), |  | ||||||
|           chatPayload, |  | ||||||
|         ) |  | ||||||
|           .then((response) => { |  | ||||||
|             const reader = response?.body?.getReader(); |  | ||||||
|             const decoder = new TextDecoder(); |  | ||||||
|             let partialData = ""; |  | ||||||
|  |  | ||||||
|             return reader?.read().then(function processText({ |         // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb | ||||||
|               done, |         const chatPath = | ||||||
|               value, |           baseUrl.replace("generateContent", "streamGenerateContent") + | ||||||
|             }): Promise<any> { |           (baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse"); | ||||||
|               if (done) { |         fetchEventSource(chatPath, { | ||||||
|                 if (response.status !== 200) { |           ...chatPayload, | ||||||
|                   try { |           async onopen(res) { | ||||||
|                     let data = JSON.parse(ensureProperEnding(partialData)); |             clearTimeout(requestTimeoutId); | ||||||
|                     if (data && data[0].error) { |             const contentType = res.headers.get("content-type"); | ||||||
|                       options.onError?.(new Error(data[0].error.message)); |             console.log( | ||||||
|                     } else { |               "[Gemini] request response content type: ", | ||||||
|                       options.onError?.(new Error("Request failed")); |               contentType, | ||||||
|                     } |  | ||||||
|                   } catch (_) { |  | ||||||
|                     options.onError?.(new Error("Request failed")); |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 console.log("Stream complete"); |  | ||||||
|                 // options.onFinish(responseText + remainText); |  | ||||||
|                 finished = true; |  | ||||||
|                 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 (contentType?.startsWith("text/plain")) { | ||||||
|  |               responseText = await res.clone().text(); | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |               !res.ok || | ||||||
|  |               !res.headers | ||||||
|  |                 .get("content-type") | ||||||
|  |                 ?.startsWith(EventStreamContentType) || | ||||||
|  |               res.status !== 200 | ||||||
|  |             ) { | ||||||
|  |               const responseTexts = [responseText]; | ||||||
|  |               let extraInfo = await res.clone().text(); | ||||||
|  |               try { | ||||||
|  |                 const resJson = await res.clone().json(); | ||||||
|  |                 extraInfo = prettyObject(resJson); | ||||||
|  |               } catch {} | ||||||
|  |  | ||||||
|  |               if (res.status === 401) { | ||||||
|  |                 responseTexts.push(Locale.Error.Unauthorized); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               if (extraInfo) { | ||||||
|  |                 responseTexts.push(extraInfo); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               responseText = responseTexts.join("\n\n"); | ||||||
|  |  | ||||||
|  |               return finish(); | ||||||
|  |             } | ||||||
|           }, |           }, | ||||||
|                   [], |           onmessage(msg) { | ||||||
|                 ); |             if (msg.data === "[DONE]" || finished) { | ||||||
|  |               return finish(); | ||||||
|                 if (textArray.length > existingTexts.length) { |  | ||||||
|                   const deltaArray = textArray.slice(existingTexts.length); |  | ||||||
|                   existingTexts = textArray; |  | ||||||
|                   remainText += deltaArray.join(""); |  | ||||||
|             } |             } | ||||||
|               } catch (error) { |             const text = msg.data; | ||||||
|                 // console.log("[Response Animation] error: ", error,partialData); |             try { | ||||||
|                 // skip error message when parsing json |               const json = JSON.parse(text); | ||||||
|  |               const delta = apiClient.extractMessage(json); | ||||||
|  |  | ||||||
|  |               if (delta) { | ||||||
|  |                 remainText += delta; | ||||||
|               } |               } | ||||||
|  |  | ||||||
|               return reader.read().then(processText); |               const blockReason = json?.promptFeedback?.blockReason; | ||||||
|             }); |               if (blockReason) { | ||||||
|           }) |                 // being blocked | ||||||
|           .catch((error) => { |                 console.log(`[Google] [Safety Ratings] result:`, blockReason); | ||||||
|             console.error("Error:", error); |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               console.error("[Request] parse error", text, msg); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           onclose() { | ||||||
|  |             finish(); | ||||||
|  |           }, | ||||||
|  |           onerror(e) { | ||||||
|  |             options.onError?.(e); | ||||||
|  |             throw e; | ||||||
|  |           }, | ||||||
|  |           openWhenHidden: true, | ||||||
|         }); |         }); | ||||||
|       } else { |       } else { | ||||||
|         const res = await fetch(baseUrl, chatPayload); |         const res = await fetch(baseUrl, chatPayload); | ||||||
| @@ -252,7 +271,7 @@ export class GeminiProApi implements LLMApi { | |||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|         const message = this.extractMessage(resJson); |         const message = apiClient.extractMessage(resJson); | ||||||
|         options.onFinish(message); |         options.onFinish(message); | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|   | |||||||
| @@ -1,13 +1,17 @@ | |||||||
| "use client"; | "use client"; | ||||||
|  | // azure and openai, using same models. so using same LLMApi. | ||||||
| import { | import { | ||||||
|   ApiPath, |   ApiPath, | ||||||
|   DEFAULT_API_HOST, |   DEFAULT_API_HOST, | ||||||
|   DEFAULT_MODELS, |   DEFAULT_MODELS, | ||||||
|   OpenaiPath, |   OpenaiPath, | ||||||
|  |   Azure, | ||||||
|   REQUEST_TIMEOUT_MS, |   REQUEST_TIMEOUT_MS, | ||||||
|   ServiceProvider, |   ServiceProvider, | ||||||
| } from "@/app/constant"; | } from "@/app/constant"; | ||||||
| import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
|  | import { collectModelsWithDefaultModel } from "@/app/utils/model"; | ||||||
|  | import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   ChatOptions, |   ChatOptions, | ||||||
| @@ -24,7 +28,6 @@ import { | |||||||
| } from "@fortaine/fetch-event-source"; | } from "@fortaine/fetch-event-source"; | ||||||
| import { prettyObject } from "@/app/utils/format"; | import { prettyObject } from "@/app/utils/format"; | ||||||
| import { getClientConfig } from "@/app/config/client"; | import { getClientConfig } from "@/app/config/client"; | ||||||
| import { makeAzurePath } from "@/app/azure"; |  | ||||||
| import { | import { | ||||||
|   getMessageTextContent, |   getMessageTextContent, | ||||||
|   getMessageImages, |   getMessageImages, | ||||||
| @@ -40,7 +43,7 @@ export interface OpenAIListModelResponse { | |||||||
|   }>; |   }>; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface RequestPayload { | export interface RequestPayload { | ||||||
|   messages: { |   messages: { | ||||||
|     role: "system" | "user" | "assistant"; |     role: "system" | "user" | "assistant"; | ||||||
|     content: string | MultimodalContent[]; |     content: string | MultimodalContent[]; | ||||||
| @@ -62,39 +65,38 @@ export class ChatGPTApi implements LLMApi { | |||||||
|  |  | ||||||
|     let baseUrl = ""; |     let baseUrl = ""; | ||||||
|  |  | ||||||
|  |     const isAzure = path.includes("deployments"); | ||||||
|     if (accessStore.useCustomConfig) { |     if (accessStore.useCustomConfig) { | ||||||
|       const isAzure = accessStore.provider === ServiceProvider.Azure; |  | ||||||
|  |  | ||||||
|       if (isAzure && !accessStore.isValidAzure()) { |       if (isAzure && !accessStore.isValidAzure()) { | ||||||
|         throw Error( |         throw Error( | ||||||
|           "incomplete azure config, please check it in your settings page", |           "incomplete azure config, please check it in your settings page", | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (isAzure) { |  | ||||||
|         path = makeAzurePath(path, accessStore.azureApiVersion); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl; |       baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (baseUrl.length === 0) { |     if (baseUrl.length === 0) { | ||||||
|       const isApp = !!getClientConfig()?.isApp; |       const isApp = !!getClientConfig()?.isApp; | ||||||
|       baseUrl = isApp |       const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI; | ||||||
|         ? DEFAULT_API_HOST + "/proxy" + ApiPath.OpenAI |       baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; | ||||||
|         : ApiPath.OpenAI; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (baseUrl.endsWith("/")) { |     if (baseUrl.endsWith("/")) { | ||||||
|       baseUrl = baseUrl.slice(0, baseUrl.length - 1); |       baseUrl = baseUrl.slice(0, baseUrl.length - 1); | ||||||
|     } |     } | ||||||
|     if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) { |     if ( | ||||||
|  |       !baseUrl.startsWith("http") && | ||||||
|  |       !isAzure && | ||||||
|  |       !baseUrl.startsWith(ApiPath.OpenAI) | ||||||
|  |     ) { | ||||||
|       baseUrl = "https://" + baseUrl; |       baseUrl = "https://" + baseUrl; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log("[Proxy Endpoint] ", baseUrl, path); |     console.log("[Proxy Endpoint] ", baseUrl, path); | ||||||
|  |  | ||||||
|     return [baseUrl, path].join("/"); |     // try rebuild url, when using cloudflare ai gateway in client | ||||||
|  |     return cloudflareAIGatewayUrl([baseUrl, path].join("/")); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   extractMessage(res: any) { |   extractMessage(res: any) { | ||||||
| @@ -113,6 +115,7 @@ export class ChatGPTApi implements LLMApi { | |||||||
|       ...useChatStore.getState().currentSession().mask.modelConfig, |       ...useChatStore.getState().currentSession().mask.modelConfig, | ||||||
|       ...{ |       ...{ | ||||||
|         model: options.config.model, |         model: options.config.model, | ||||||
|  |         providerName: options.config.providerName, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -140,7 +143,35 @@ export class ChatGPTApi implements LLMApi { | |||||||
|     options.onController?.(controller); |     options.onController?.(controller); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const chatPath = this.path(OpenaiPath.ChatPath); |       let chatPath = ""; | ||||||
|  |       if (modelConfig.providerName === ServiceProvider.Azure) { | ||||||
|  |         // find model, and get displayName as deployName | ||||||
|  |         const { models: configModels, customModels: configCustomModels } = | ||||||
|  |           useAppConfig.getState(); | ||||||
|  |         const { | ||||||
|  |           defaultModel, | ||||||
|  |           customModels: accessCustomModels, | ||||||
|  |           useCustomConfig, | ||||||
|  |         } = useAccessStore.getState(); | ||||||
|  |         const models = collectModelsWithDefaultModel( | ||||||
|  |           configModels, | ||||||
|  |           [configCustomModels, accessCustomModels].join(","), | ||||||
|  |           defaultModel, | ||||||
|  |         ); | ||||||
|  |         const model = models.find( | ||||||
|  |           (model) => | ||||||
|  |             model.name === modelConfig.model && | ||||||
|  |             model?.provider?.providerName === ServiceProvider.Azure, | ||||||
|  |         ); | ||||||
|  |         chatPath = this.path( | ||||||
|  |           Azure.ChatPath( | ||||||
|  |             (model?.displayName ?? model?.name) as string, | ||||||
|  |             useCustomConfig ? useAccessStore.getState().azureApiVersion : "", | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         chatPath = this.path(OpenaiPath.ChatPath); | ||||||
|  |       } | ||||||
|       const chatPayload = { |       const chatPayload = { | ||||||
|         method: "POST", |         method: "POST", | ||||||
|         body: JSON.stringify(requestPayload), |         body: JSON.stringify(requestPayload), | ||||||
|   | |||||||
| @@ -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"; | ||||||
| @@ -87,6 +88,7 @@ import { | |||||||
|   Path, |   Path, | ||||||
|   REQUEST_TIMEOUT_MS, |   REQUEST_TIMEOUT_MS, | ||||||
|   UNFINISHED_INPUT, |   UNFINISHED_INPUT, | ||||||
|  |   ServiceProvider, | ||||||
| } from "../constant"; | } from "../constant"; | ||||||
| import { Avatar } from "./emoji"; | import { Avatar } from "./emoji"; | ||||||
| import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; | import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; | ||||||
| @@ -447,6 +449,9 @@ export function ChatActions(props: { | |||||||
|  |  | ||||||
|   // switch model |   // switch model | ||||||
|   const currentModel = chatStore.currentSession().mask.modelConfig.model; |   const currentModel = chatStore.currentSession().mask.modelConfig.model; | ||||||
|  |   const currentProviderName = | ||||||
|  |     chatStore.currentSession().mask.modelConfig?.providerName || | ||||||
|  |     ServiceProvider.OpenAI; | ||||||
|   const allModels = useAllModels(); |   const allModels = useAllModels(); | ||||||
|   const models = useMemo(() => { |   const models = useMemo(() => { | ||||||
|     const filteredModels = allModels.filter((m) => m.available); |     const filteredModels = allModels.filter((m) => m.available); | ||||||
| @@ -462,6 +467,14 @@ export function ChatActions(props: { | |||||||
|       return filteredModels; |       return filteredModels; | ||||||
|     } |     } | ||||||
|   }, [allModels]); |   }, [allModels]); | ||||||
|  |   const currentModelName = useMemo(() => { | ||||||
|  |     const model = models.find( | ||||||
|  |       (m) => | ||||||
|  |         m.name == currentModel && | ||||||
|  |         m?.provider?.providerName == currentProviderName, | ||||||
|  |     ); | ||||||
|  |     return model?.displayName ?? ""; | ||||||
|  |   }, [models, currentModel, currentProviderName]); | ||||||
|   const [showModelSelector, setShowModelSelector] = useState(false); |   const [showModelSelector, setShowModelSelector] = useState(false); | ||||||
|   const [showUploadImage, setShowUploadImage] = useState(false); |   const [showUploadImage, setShowUploadImage] = useState(false); | ||||||
|  |  | ||||||
| @@ -478,13 +491,17 @@ export function ChatActions(props: { | |||||||
|     const isUnavaliableModel = !models.some((m) => m.name === currentModel); |     const isUnavaliableModel = !models.some((m) => m.name === currentModel); | ||||||
|     if (isUnavaliableModel && models.length > 0) { |     if (isUnavaliableModel && models.length > 0) { | ||||||
|       // show next model to default model if exist |       // show next model to default model if exist | ||||||
|       let nextModel: ModelType = ( |       let nextModel = models.find((model) => model.isDefault) || models[0]; | ||||||
|         models.find((model) => model.isDefault) || models[0] |       chatStore.updateCurrentSession((session) => { | ||||||
|       ).name; |         session.mask.modelConfig.model = nextModel.name; | ||||||
|       chatStore.updateCurrentSession( |         session.mask.modelConfig.providerName = nextModel?.provider | ||||||
|         (session) => (session.mask.modelConfig.model = nextModel), |           ?.providerName as ServiceProvider; | ||||||
|  |       }); | ||||||
|  |       showToast( | ||||||
|  |         nextModel?.provider?.providerName == "ByteDance" | ||||||
|  |           ? nextModel.displayName | ||||||
|  |           : nextModel.name, | ||||||
|       ); |       ); | ||||||
|       showToast(nextModel); |  | ||||||
|     } |     } | ||||||
|   }, [chatStore, currentModel, models]); |   }, [chatStore, currentModel, models]); | ||||||
|  |  | ||||||
| @@ -566,25 +583,40 @@ export function ChatActions(props: { | |||||||
|  |  | ||||||
|       <ChatAction |       <ChatAction | ||||||
|         onClick={() => setShowModelSelector(true)} |         onClick={() => setShowModelSelector(true)} | ||||||
|         text={currentModel} |         text={currentModelName} | ||||||
|         icon={<RobotIcon />} |         icon={<RobotIcon />} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       {showModelSelector && ( |       {showModelSelector && ( | ||||||
|         <Selector |         <Selector | ||||||
|           defaultSelectedValue={currentModel} |           defaultSelectedValue={`${currentModel}@${currentProviderName}`} | ||||||
|           items={models.map((m) => ({ |           items={models.map((m) => ({ | ||||||
|             title: m.displayName, |             title: `${m.displayName}${ | ||||||
|             value: m.name, |               m?.provider?.providerName | ||||||
|  |                 ? "(" + m?.provider?.providerName + ")" | ||||||
|  |                 : "" | ||||||
|  |             }`, | ||||||
|  |             value: `${m.name}@${m?.provider?.providerName}`, | ||||||
|           }))} |           }))} | ||||||
|           onClose={() => setShowModelSelector(false)} |           onClose={() => setShowModelSelector(false)} | ||||||
|           onSelection={(s) => { |           onSelection={(s) => { | ||||||
|             if (s.length === 0) return; |             if (s.length === 0) return; | ||||||
|  |             const [model, providerName] = s[0].split("@"); | ||||||
|             chatStore.updateCurrentSession((session) => { |             chatStore.updateCurrentSession((session) => { | ||||||
|               session.mask.modelConfig.model = s[0] as ModelType; |               session.mask.modelConfig.model = model as ModelType; | ||||||
|  |               session.mask.modelConfig.providerName = | ||||||
|  |                 providerName as ServiceProvider; | ||||||
|               session.mask.syncGlobalConfig = false; |               session.mask.syncGlobalConfig = false; | ||||||
|             }); |             }); | ||||||
|             showToast(s[0]); |             if (providerName == "ByteDance") { | ||||||
|  |               const selectedModel = models.find( | ||||||
|  |                 (m) => | ||||||
|  |                   m.name == model && m?.provider?.providerName == providerName, | ||||||
|  |               ); | ||||||
|  |               showToast(selectedModel?.displayName ?? ""); | ||||||
|  |             } else { | ||||||
|  |               showToast(model); | ||||||
|  |             } | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
| @@ -1088,6 +1120,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 { | ||||||
|   | |||||||
| @@ -36,11 +36,10 @@ import { toBlob, toPng } from "html-to-image"; | |||||||
| import { DEFAULT_MASK_AVATAR } from "../store/mask"; | import { DEFAULT_MASK_AVATAR } from "../store/mask"; | ||||||
|  |  | ||||||
| import { prettyObject } from "../utils/format"; | import { prettyObject } from "../utils/format"; | ||||||
| import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; | import { EXPORT_MESSAGE_CLASS_NAME } from "../constant"; | ||||||
| import { getClientConfig } from "../config/client"; | import { getClientConfig } from "../config/client"; | ||||||
| import { ClientApi } from "../client/api"; | import { type ClientApi, getClientApi } from "../client/api"; | ||||||
| import { getMessageTextContent } from "../utils"; | import { getMessageTextContent } from "../utils"; | ||||||
| import { identifyDefaultClaudeModel } from "../utils/checkers"; |  | ||||||
|  |  | ||||||
| const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | ||||||
|   loading: () => <LoadingIcon />, |   loading: () => <LoadingIcon />, | ||||||
| @@ -313,14 +312,7 @@ export function PreviewActions(props: { | |||||||
|   const onRenderMsgs = (msgs: ChatMessage[]) => { |   const onRenderMsgs = (msgs: ChatMessage[]) => { | ||||||
|     setShouldExport(false); |     setShouldExport(false); | ||||||
|  |  | ||||||
|     var api: ClientApi; |     const api: ClientApi = getClientApi(config.modelConfig.providerName); | ||||||
|     if (config.modelConfig.model.startsWith("gemini")) { |  | ||||||
|       api = new ClientApi(ModelProvider.GeminiPro); |  | ||||||
|     } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { |  | ||||||
|       api = new ClientApi(ModelProvider.Claude); |  | ||||||
|     } else { |  | ||||||
|       api = new ClientApi(ModelProvider.GPT); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     api |     api | ||||||
|       .share(msgs) |       .share(msgs) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg"; | |||||||
| import { getCSSVar, useMobileScreen } from "../utils"; | import { getCSSVar, useMobileScreen } from "../utils"; | ||||||
|  |  | ||||||
| import dynamic from "next/dynamic"; | import dynamic from "next/dynamic"; | ||||||
| import { ModelProvider, Path, SlotID } from "../constant"; | import { Path, SlotID } from "../constant"; | ||||||
| import { ErrorBoundary } from "./error"; | import { ErrorBoundary } from "./error"; | ||||||
|  |  | ||||||
| import { getISOLang, getLang } from "../locales"; | import { getISOLang, getLang } from "../locales"; | ||||||
| @@ -27,9 +27,8 @@ import { SideBar } from "./sidebar"; | |||||||
| import { useAppConfig } from "../store/config"; | import { useAppConfig } from "../store/config"; | ||||||
| import { AuthPage } from "./auth"; | import { AuthPage } from "./auth"; | ||||||
| import { getClientConfig } from "../config/client"; | import { getClientConfig } from "../config/client"; | ||||||
| import { ClientApi } from "../client/api"; | import { type ClientApi, getClientApi } from "../client/api"; | ||||||
| import { useAccessStore } from "../store"; | import { useAccessStore } from "../store"; | ||||||
| import { identifyDefaultClaudeModel } from "../utils/checkers"; |  | ||||||
|  |  | ||||||
| export function Loading(props: { noLogo?: boolean }) { | export function Loading(props: { noLogo?: boolean }) { | ||||||
|   return ( |   return ( | ||||||
| @@ -171,14 +170,8 @@ function Screen() { | |||||||
| export function useLoadData() { | export function useLoadData() { | ||||||
|   const config = useAppConfig(); |   const config = useAppConfig(); | ||||||
|  |  | ||||||
|   var api: ClientApi; |   const api: ClientApi = getClientApi(config.modelConfig.providerName); | ||||||
|   if (config.modelConfig.model.startsWith("gemini")) { |  | ||||||
|     api = new ClientApi(ModelProvider.GeminiPro); |  | ||||||
|   } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { |  | ||||||
|     api = new ClientApi(ModelProvider.Claude); |  | ||||||
|   } else { |  | ||||||
|     api = new ClientApi(ModelProvider.GPT); |  | ||||||
|   } |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     (async () => { |     (async () => { | ||||||
|       const models = await api.llm.models(); |       const models = await api.llm.models(); | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { ServiceProvider } from "@/app/constant"; | ||||||
| import { ModalConfigValidator, ModelConfig } from "../store"; | import { ModalConfigValidator, ModelConfig } from "../store"; | ||||||
|  |  | ||||||
| import Locale from "../locales"; | import Locale from "../locales"; | ||||||
| @@ -10,25 +11,25 @@ export function ModelConfigList(props: { | |||||||
|   updateConfig: (updater: (config: ModelConfig) => void) => void; |   updateConfig: (updater: (config: ModelConfig) => void) => void; | ||||||
| }) { | }) { | ||||||
|   const allModels = useAllModels(); |   const allModels = useAllModels(); | ||||||
|  |   const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <ListItem title={Locale.Settings.Model}> |       <ListItem title={Locale.Settings.Model}> | ||||||
|         <Select |         <Select | ||||||
|           value={props.modelConfig.model} |           value={value} | ||||||
|           onChange={(e) => { |           onChange={(e) => { | ||||||
|             props.updateConfig( |             const [model, providerName] = e.currentTarget.value.split("@"); | ||||||
|               (config) => |             props.updateConfig((config) => { | ||||||
|                 (config.model = ModalConfigValidator.model( |               config.model = ModalConfigValidator.model(model); | ||||||
|                   e.currentTarget.value, |               config.providerName = providerName as ServiceProvider; | ||||||
|                 )), |             }); | ||||||
|             ); |  | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|           {allModels |           {allModels | ||||||
|             .filter((v) => v.available) |             .filter((v) => v.available) | ||||||
|             .map((v, i) => ( |             .map((v, i) => ( | ||||||
|               <option value={v.name} key={i}> |               <option value={`${v.name}@${v.provider?.providerName}`} key={i}> | ||||||
|                 {v.displayName}({v.provider?.providerName}) |                 {v.displayName}({v.provider?.providerName}) | ||||||
|               </option> |               </option> | ||||||
|             ))} |             ))} | ||||||
| @@ -92,7 +93,7 @@ export function ModelConfigList(props: { | |||||||
|         ></input> |         ></input> | ||||||
|       </ListItem> |       </ListItem> | ||||||
|  |  | ||||||
|       {props.modelConfig.model.startsWith("gemini") ? null : ( |       {props.modelConfig?.providerName == ServiceProvider.Google ? null : ( | ||||||
|         <> |         <> | ||||||
|           <ListItem |           <ListItem | ||||||
|             title={Locale.Settings.PresencePenalty.Title} |             title={Locale.Settings.PresencePenalty.Title} | ||||||
|   | |||||||
| @@ -53,6 +53,9 @@ import Link from "next/link"; | |||||||
| import { | import { | ||||||
|   Anthropic, |   Anthropic, | ||||||
|   Azure, |   Azure, | ||||||
|  |   Baidu, | ||||||
|  |   ByteDance, | ||||||
|  |   Alibaba, | ||||||
|   Google, |   Google, | ||||||
|   OPENAI_BASE_URL, |   OPENAI_BASE_URL, | ||||||
|   Path, |   Path, | ||||||
| @@ -1187,6 +1190,155 @@ export function Settings() { | |||||||
|                       </ListItem> |                       </ListItem> | ||||||
|                     </> |                     </> | ||||||
|                   )} |                   )} | ||||||
|  |                   {accessStore.provider === ServiceProvider.Baidu && ( | ||||||
|  |                     <> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.Baidu.Endpoint.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.Baidu.Endpoint.SubTitle | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <input | ||||||
|  |                           type="text" | ||||||
|  |                           value={accessStore.baiduUrl} | ||||||
|  |                           placeholder={Baidu.ExampleEndpoint} | ||||||
|  |                           onChange={(e) => | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.baiduUrl = e.currentTarget.value), | ||||||
|  |                             ) | ||||||
|  |                           } | ||||||
|  |                         ></input> | ||||||
|  |                       </ListItem> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.Baidu.ApiKey.Title} | ||||||
|  |                         subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle} | ||||||
|  |                       > | ||||||
|  |                         <PasswordInput | ||||||
|  |                           value={accessStore.baiduApiKey} | ||||||
|  |                           type="text" | ||||||
|  |                           placeholder={ | ||||||
|  |                             Locale.Settings.Access.Baidu.ApiKey.Placeholder | ||||||
|  |                           } | ||||||
|  |                           onChange={(e) => { | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.baiduApiKey = e.currentTarget.value), | ||||||
|  |                             ); | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |                       </ListItem> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.Baidu.SecretKey.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.Baidu.SecretKey.SubTitle | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <PasswordInput | ||||||
|  |                           value={accessStore.baiduSecretKey} | ||||||
|  |                           type="text" | ||||||
|  |                           placeholder={ | ||||||
|  |                             Locale.Settings.Access.Baidu.SecretKey.Placeholder | ||||||
|  |                           } | ||||||
|  |                           onChange={(e) => { | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.baiduSecretKey = e.currentTarget.value), | ||||||
|  |                             ); | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |                       </ListItem> | ||||||
|  |                     </> | ||||||
|  |                   )} | ||||||
|  |  | ||||||
|  |                   {accessStore.provider === ServiceProvider.ByteDance && ( | ||||||
|  |                     <> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.ByteDance.Endpoint.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.ByteDance.Endpoint.SubTitle + | ||||||
|  |                           ByteDance.ExampleEndpoint | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <input | ||||||
|  |                           type="text" | ||||||
|  |                           value={accessStore.bytedanceUrl} | ||||||
|  |                           placeholder={ByteDance.ExampleEndpoint} | ||||||
|  |                           onChange={(e) => | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.bytedanceUrl = e.currentTarget.value), | ||||||
|  |                             ) | ||||||
|  |                           } | ||||||
|  |                         ></input> | ||||||
|  |                       </ListItem> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.ByteDance.ApiKey.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.ByteDance.ApiKey.SubTitle | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <PasswordInput | ||||||
|  |                           value={accessStore.bytedanceApiKey} | ||||||
|  |                           type="text" | ||||||
|  |                           placeholder={ | ||||||
|  |                             Locale.Settings.Access.ByteDance.ApiKey.Placeholder | ||||||
|  |                           } | ||||||
|  |                           onChange={(e) => { | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.bytedanceApiKey = | ||||||
|  |                                   e.currentTarget.value), | ||||||
|  |                             ); | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |                       </ListItem> | ||||||
|  |                     </> | ||||||
|  |                   )} | ||||||
|  |  | ||||||
|  |                   {accessStore.provider === ServiceProvider.Alibaba && ( | ||||||
|  |                     <> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.Alibaba.Endpoint.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.Alibaba.Endpoint.SubTitle + | ||||||
|  |                           Alibaba.ExampleEndpoint | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <input | ||||||
|  |                           type="text" | ||||||
|  |                           value={accessStore.alibabaUrl} | ||||||
|  |                           placeholder={Alibaba.ExampleEndpoint} | ||||||
|  |                           onChange={(e) => | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.alibabaUrl = e.currentTarget.value), | ||||||
|  |                             ) | ||||||
|  |                           } | ||||||
|  |                         ></input> | ||||||
|  |                       </ListItem> | ||||||
|  |                       <ListItem | ||||||
|  |                         title={Locale.Settings.Access.Alibaba.ApiKey.Title} | ||||||
|  |                         subTitle={ | ||||||
|  |                           Locale.Settings.Access.Alibaba.ApiKey.SubTitle | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         <PasswordInput | ||||||
|  |                           value={accessStore.alibabaApiKey} | ||||||
|  |                           type="text" | ||||||
|  |                           placeholder={ | ||||||
|  |                             Locale.Settings.Access.Alibaba.ApiKey.Placeholder | ||||||
|  |                           } | ||||||
|  |                           onChange={(e) => { | ||||||
|  |                             accessStore.update( | ||||||
|  |                               (access) => | ||||||
|  |                                 (access.alibabaApiKey = e.currentTarget.value), | ||||||
|  |                             ); | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |                       </ListItem> | ||||||
|  |                     </> | ||||||
|  |                   )} | ||||||
|                 </> |                 </> | ||||||
|               )} |               )} | ||||||
|             </> |             </> | ||||||
|   | |||||||
| @@ -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,27 @@ declare global { | |||||||
|  |  | ||||||
|       // google tag manager |       // google tag manager | ||||||
|       GTM_ID?: string; |       GTM_ID?: string; | ||||||
|  |  | ||||||
|  |       // anthropic only | ||||||
|  |       ANTHROPIC_URL?: string; | ||||||
|  |       ANTHROPIC_API_KEY?: string; | ||||||
|  |       ANTHROPIC_API_VERSION?: string; | ||||||
|  |  | ||||||
|  |       // baidu only | ||||||
|  |       BAIDU_URL?: string; | ||||||
|  |       BAIDU_API_KEY?: string; | ||||||
|  |       BAIDU_SECRET_KEY?: string; | ||||||
|  |  | ||||||
|  |       // bytedance only | ||||||
|  |       BYTEDANCE_URL?: string; | ||||||
|  |       BYTEDANCE_API_KEY?: string; | ||||||
|  |  | ||||||
|  |       // alibaba only | ||||||
|  |       ALIBABA_URL?: string; | ||||||
|  |       ALIBABA_API_KEY?: string; | ||||||
|  |  | ||||||
|  |       // custom template for preprocessing user input | ||||||
|  |       DEFAULT_INPUT_TEMPLATE?: string; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -51,6 +72,22 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> { | |||||||
|   } |   } | ||||||
| })(); | })(); | ||||||
|  |  | ||||||
|  | function getApiKey(keys?: string) { | ||||||
|  |   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,37 +111,53 @@ 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 isBaidu = !!process.env.BAIDU_API_KEY; | ||||||
|   const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); |   const isBytedance = !!process.env.BYTEDANCE_API_KEY; | ||||||
|   const randomIndex = Math.floor(Math.random() * apiKeys.length); |   const isAlibaba = !!process.env.ALIBABA_API_KEY; | ||||||
|   const apiKey = apiKeys[randomIndex]; |   // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; | ||||||
|   console.log( |   // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); | ||||||
|     `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, |   // const randomIndex = Math.floor(Math.random() * apiKeys.length); | ||||||
|   ); |   // const apiKey = apiKeys[randomIndex]; | ||||||
|  |   // console.log( | ||||||
|  |   //   `[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, | ||||||
|  |  | ||||||
|  |     isBaidu, | ||||||
|  |     baiduUrl: process.env.BAIDU_URL, | ||||||
|  |     baiduApiKey: getApiKey(process.env.BAIDU_API_KEY), | ||||||
|  |     baiduSecretKey: process.env.BAIDU_SECRET_KEY, | ||||||
|  |  | ||||||
|  |     isBytedance, | ||||||
|  |     bytedanceApiKey: getApiKey(process.env.BYTEDANCE_API_KEY), | ||||||
|  |     bytedanceUrl: process.env.BYTEDANCE_URL, | ||||||
|  |  | ||||||
|  |     isAlibaba, | ||||||
|  |     alibabaUrl: process.env.ALIBABA_URL, | ||||||
|  |     alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), | ||||||
|  |  | ||||||
|     gtmId: process.env.GTM_ID, |     gtmId: process.env.GTM_ID, | ||||||
|  |  | ||||||
|     needCode: ACCESS_CODES.size > 0, |     needCode: ACCESS_CODES.size > 0, | ||||||
| @@ -120,6 +173,6 @@ export const getServerSideConfig = () => { | |||||||
|     disableFastLink: !!process.env.DISABLE_FAST_LINK, |     disableFastLink: !!process.env.DISABLE_FAST_LINK, | ||||||
|     customModels, |     customModels, | ||||||
|     defaultModel, |     defaultModel, | ||||||
|     whiteWebDevEndpoints, |     allowedWebDevEndpoints, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								app/constant.ts
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								app/constant.ts
									
									
									
									
									
								
							| @@ -14,6 +14,13 @@ export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; | |||||||
|  |  | ||||||
| export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; | export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; | ||||||
|  |  | ||||||
|  | export const BAIDU_BASE_URL = "https://aip.baidubce.com"; | ||||||
|  | export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`; | ||||||
|  |  | ||||||
|  | export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com"; | ||||||
|  |  | ||||||
|  | export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; | ||||||
|  |  | ||||||
| export enum Path { | export enum Path { | ||||||
|   Home = "/", |   Home = "/", | ||||||
|   Chat = "/chat", |   Chat = "/chat", | ||||||
| @@ -25,8 +32,13 @@ export enum Path { | |||||||
|  |  | ||||||
| export enum ApiPath { | export enum ApiPath { | ||||||
|   Cors = "", |   Cors = "", | ||||||
|  |   Azure = "/api/azure", | ||||||
|   OpenAI = "/api/openai", |   OpenAI = "/api/openai", | ||||||
|   Anthropic = "/api/anthropic", |   Anthropic = "/api/anthropic", | ||||||
|  |   Google = "/api/google", | ||||||
|  |   Baidu = "/api/baidu", | ||||||
|  |   ByteDance = "/api/bytedance", | ||||||
|  |   Alibaba = "/api/alibaba", | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum SlotID { | export enum SlotID { | ||||||
| @@ -70,12 +82,18 @@ export enum ServiceProvider { | |||||||
|   Azure = "Azure", |   Azure = "Azure", | ||||||
|   Google = "Google", |   Google = "Google", | ||||||
|   Anthropic = "Anthropic", |   Anthropic = "Anthropic", | ||||||
|  |   Baidu = "Baidu", | ||||||
|  |   ByteDance = "ByteDance", | ||||||
|  |   Alibaba = "Alibaba", | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum ModelProvider { | export enum ModelProvider { | ||||||
|   GPT = "GPT", |   GPT = "GPT", | ||||||
|   GeminiPro = "GeminiPro", |   GeminiPro = "GeminiPro", | ||||||
|   Claude = "Claude", |   Claude = "Claude", | ||||||
|  |   Ernie = "Ernie", | ||||||
|  |   Doubao = "Doubao", | ||||||
|  |   Qwen = "Qwen", | ||||||
| } | } | ||||||
|  |  | ||||||
| export const Anthropic = { | export const Anthropic = { | ||||||
| @@ -93,6 +111,8 @@ export const OpenaiPath = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const Azure = { | export const Azure = { | ||||||
|  |   ChatPath: (deployName: string, apiVersion: string) => | ||||||
|  |     `deployments/${deployName}/chat/completions?api-version=${apiVersion}`, | ||||||
|   ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}", |   ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -101,6 +121,33 @@ export const Google = { | |||||||
|   ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, |   ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const Baidu = { | ||||||
|  |   ExampleEndpoint: BAIDU_BASE_URL, | ||||||
|  |   ChatPath: (modelName: string) => { | ||||||
|  |     let endpoint = modelName; | ||||||
|  |     if (modelName === "ernie-4.0-8k") { | ||||||
|  |       endpoint = "completions_pro"; | ||||||
|  |     } | ||||||
|  |     if (modelName === "ernie-4.0-8k-preview-0518") { | ||||||
|  |       endpoint = "completions_adv_pro"; | ||||||
|  |     } | ||||||
|  |     if (modelName === "ernie-3.5-8k") { | ||||||
|  |       endpoint = "completions"; | ||||||
|  |     } | ||||||
|  |     return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const ByteDance = { | ||||||
|  |   ExampleEndpoint: "https://ark.cn-beijing.volces.com/api/", | ||||||
|  |   ChatPath: "api/v3/chat/completions", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const Alibaba = { | ||||||
|  |   ExampleEndpoint: ALIBABA_BASE_URL, | ||||||
|  |   ChatPath: "v1/services/aigc/text-generation/generation", | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang | export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang | ||||||
| // export const DEFAULT_SYSTEM_TEMPLATE = ` | // export const DEFAULT_SYSTEM_TEMPLATE = ` | ||||||
| // You are ChatGPT, a large language model trained by {{ServiceProvider}}. | // You are ChatGPT, a large language model trained by {{ServiceProvider}}. | ||||||
| @@ -127,6 +174,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-4o": "2023-10", | ||||||
|  |   "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. | ||||||
| @@ -144,13 +193,17 @@ const openaiModels = [ | |||||||
|   "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", | ||||||
|  |   "gpt-4-1106-preview", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| 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", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @@ -161,6 +214,36 @@ const anthropicModels = [ | |||||||
|   "claude-3-sonnet-20240229", |   "claude-3-sonnet-20240229", | ||||||
|   "claude-3-opus-20240229", |   "claude-3-opus-20240229", | ||||||
|   "claude-3-haiku-20240307", |   "claude-3-haiku-20240307", | ||||||
|  |   "claude-3-5-sonnet-20240620", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const baiduModels = [ | ||||||
|  |   "ernie-4.0-turbo-8k", | ||||||
|  |   "ernie-4.0-8k", | ||||||
|  |   "ernie-4.0-8k-preview", | ||||||
|  |   "ernie-4.0-8k-preview-0518", | ||||||
|  |   "ernie-4.0-8k-latest", | ||||||
|  |   "ernie-3.5-8k", | ||||||
|  |   "ernie-3.5-8k-0205", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const bytedanceModels = [ | ||||||
|  |   "Doubao-lite-4k", | ||||||
|  |   "Doubao-lite-32k", | ||||||
|  |   "Doubao-lite-128k", | ||||||
|  |   "Doubao-pro-4k", | ||||||
|  |   "Doubao-pro-32k", | ||||||
|  |   "Doubao-pro-128k", | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const alibabaModes = [ | ||||||
|  |   "qwen-turbo", | ||||||
|  |   "qwen-plus", | ||||||
|  |   "qwen-max", | ||||||
|  |   "qwen-max-0428", | ||||||
|  |   "qwen-max-0403", | ||||||
|  |   "qwen-max-0107", | ||||||
|  |   "qwen-max-longcontext", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export const DEFAULT_MODELS = [ | export const DEFAULT_MODELS = [ | ||||||
| @@ -173,6 +256,15 @@ export const DEFAULT_MODELS = [ | |||||||
|       providerType: "openai", |       providerType: "openai", | ||||||
|     }, |     }, | ||||||
|   })), |   })), | ||||||
|  |   ...openaiModels.map((name) => ({ | ||||||
|  |     name, | ||||||
|  |     available: true, | ||||||
|  |     provider: { | ||||||
|  |       id: "azure", | ||||||
|  |       providerName: "Azure", | ||||||
|  |       providerType: "azure", | ||||||
|  |     }, | ||||||
|  |   })), | ||||||
|   ...googleModels.map((name) => ({ |   ...googleModels.map((name) => ({ | ||||||
|     name, |     name, | ||||||
|     available: true, |     available: true, | ||||||
| @@ -191,17 +283,45 @@ export const DEFAULT_MODELS = [ | |||||||
|       providerType: "anthropic", |       providerType: "anthropic", | ||||||
|     }, |     }, | ||||||
|   })), |   })), | ||||||
|  |   ...baiduModels.map((name) => ({ | ||||||
|  |     name, | ||||||
|  |     available: true, | ||||||
|  |     provider: { | ||||||
|  |       id: "baidu", | ||||||
|  |       providerName: "Baidu", | ||||||
|  |       providerType: "baidu", | ||||||
|  |     }, | ||||||
|  |   })), | ||||||
|  |   ...bytedanceModels.map((name) => ({ | ||||||
|  |     name, | ||||||
|  |     available: true, | ||||||
|  |     provider: { | ||||||
|  |       id: "bytedance", | ||||||
|  |       providerName: "ByteDance", | ||||||
|  |       providerType: "bytedance", | ||||||
|  |     }, | ||||||
|  |   })), | ||||||
|  |   ...alibabaModes.map((name) => ({ | ||||||
|  |     name, | ||||||
|  |     available: true, | ||||||
|  |     provider: { | ||||||
|  |       id: "alibaba", | ||||||
|  |       providerName: "Alibaba", | ||||||
|  |       providerType: "alibaba", | ||||||
|  |     }, | ||||||
|  |   })), | ||||||
| ] as const; | ] as const; | ||||||
|  |  | ||||||
| export const CHAT_PAGE_SIZE = 15; | 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", | ||||||
|   "https://nanao.teracloud.jp/dav/", |   "https://nanao.teracloud.jp/dav/", | ||||||
|  |   "https://bora.teracloud.jp/dav/", | ||||||
|   "https://webdav.4shared.com/", |   "https://webdav.4shared.com/", | ||||||
|   "https://dav.idrivesync.com", |   "https://dav.idrivesync.com", | ||||||
|   "https://webdav.yandex.com", |   "https://webdav.yandex.com", | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import "./styles/globals.scss"; | |||||||
| import "./styles/markdown.scss"; | import "./styles/markdown.scss"; | ||||||
| import "./styles/highlight.scss"; | import "./styles/highlight.scss"; | ||||||
| import { getClientConfig } from "./config/client"; | import { getClientConfig } from "./config/client"; | ||||||
| import { type Metadata } from "next"; | import type { Metadata, Viewport } from "next"; | ||||||
| import { SpeedInsights } from "@vercel/speed-insights/next"; | import { SpeedInsights } from "@vercel/speed-insights/next"; | ||||||
| import { getServerSideConfig } from "./config/server"; | import { getServerSideConfig } from "./config/server"; | ||||||
| import { GoogleTagManager } from "@next/third-parties/google"; | import { GoogleTagManager } from "@next/third-parties/google"; | ||||||
| @@ -12,21 +12,22 @@ const serverConfig = getServerSideConfig(); | |||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
|   title: "NextChat", |   title: "NextChat", | ||||||
|   description: "Your personal ChatGPT Chat Bot.", |   description: "Your personal ChatGPT Chat Bot.", | ||||||
|   viewport: { |  | ||||||
|     width: "device-width", |  | ||||||
|     initialScale: 1, |  | ||||||
|     maximumScale: 1, |  | ||||||
|   }, |  | ||||||
|   themeColor: [ |  | ||||||
|     { media: "(prefers-color-scheme: light)", color: "#fafafa" }, |  | ||||||
|     { media: "(prefers-color-scheme: dark)", color: "#151515" }, |  | ||||||
|   ], |  | ||||||
|   appleWebApp: { |   appleWebApp: { | ||||||
|     title: "NextChat", |     title: "NextChat", | ||||||
|     statusBarStyle: "default", |     statusBarStyle: "default", | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const viewport: Viewport = { | ||||||
|  |   width: "device-width", | ||||||
|  |   initialScale: 1, | ||||||
|  |   maximumScale: 1, | ||||||
|  |   themeColor: [ | ||||||
|  |     { media: "(prefers-color-scheme: light)", color: "#fafafa" }, | ||||||
|  |     { media: "(prefers-color-scheme: dark)", color: "#151515" }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
|  |  | ||||||
| export default function RootLayout({ | export default function RootLayout({ | ||||||
|   children, |   children, | ||||||
| }: { | }: { | ||||||
|   | |||||||
| @@ -347,6 +347,44 @@ const cn = { | |||||||
|           SubTitle: "选择一个特定的 API 版本", |           SubTitle: "选择一个特定的 API 版本", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|  |       Baidu: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "API Key", | ||||||
|  |           SubTitle: "使用自定义 Baidu API Key", | ||||||
|  |           Placeholder: "Baidu API Key", | ||||||
|  |         }, | ||||||
|  |         SecretKey: { | ||||||
|  |           Title: "Secret Key", | ||||||
|  |           SubTitle: "使用自定义 Baidu Secret Key", | ||||||
|  |           Placeholder: "Baidu Secret Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "接口地址", | ||||||
|  |           SubTitle: "不支持自定义前往.env配置", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       ByteDance: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "接口密钥", | ||||||
|  |           SubTitle: "使用自定义 ByteDance API Key", | ||||||
|  |           Placeholder: "ByteDance API Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "接口地址", | ||||||
|  |           SubTitle: "样例:", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       Alibaba: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "接口密钥", | ||||||
|  |           SubTitle: "使用自定义阿里云API Key", | ||||||
|  |           Placeholder: "Alibaba Cloud API Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "接口地址", | ||||||
|  |           SubTitle: "样例:", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|       CustomModel: { |       CustomModel: { | ||||||
|         Title: "自定义模型名", |         Title: "自定义模型名", | ||||||
|         SubTitle: "增加自定义模型可选项,使用英文逗号隔开", |         SubTitle: "增加自定义模型可选项,使用英文逗号隔开", | ||||||
|   | |||||||
| @@ -296,7 +296,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: { | ||||||
| @@ -334,6 +334,44 @@ const en: LocaleType = { | |||||||
|           SubTitle: "Select and input a specific API version", |           SubTitle: "Select and input a specific API version", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|  |       Baidu: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "Baidu API Key", | ||||||
|  |           SubTitle: "Use a custom Baidu API Key", | ||||||
|  |           Placeholder: "Baidu API Key", | ||||||
|  |         }, | ||||||
|  |         SecretKey: { | ||||||
|  |           Title: "Baidu Secret Key", | ||||||
|  |           SubTitle: "Use a custom Baidu Secret Key", | ||||||
|  |           Placeholder: "Baidu Secret Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "Endpoint Address", | ||||||
|  |           SubTitle: "not supported, configure in .env", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       ByteDance: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "ByteDance API Key", | ||||||
|  |           SubTitle: "Use a custom ByteDance API Key", | ||||||
|  |           Placeholder: "ByteDance API Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "Endpoint Address", | ||||||
|  |           SubTitle: "Example: ", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       Alibaba: { | ||||||
|  |         ApiKey: { | ||||||
|  |           Title: "Alibaba API Key", | ||||||
|  |           SubTitle: "Use a custom Alibaba Cloud API Key", | ||||||
|  |           Placeholder: "Alibaba Cloud API Key", | ||||||
|  |         }, | ||||||
|  |         Endpoint: { | ||||||
|  |           Title: "Endpoint Address", | ||||||
|  |           SubTitle: "Example: ", | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|       CustomModel: { |       CustomModel: { | ||||||
|         Title: "Custom Models", |         Title: "Custom Models", | ||||||
|         SubTitle: "Custom model options, seperated by comma", |         SubTitle: "Custom model options, seperated by comma", | ||||||
|   | |||||||
| @@ -97,7 +97,17 @@ function setItem(key: string, value: string) { | |||||||
|  |  | ||||||
| function getLanguage() { | function getLanguage() { | ||||||
|   try { |   try { | ||||||
|     return navigator.language.toLowerCase(); |     const locale = new Intl.Locale(navigator.language).maximize(); | ||||||
|  |     const region = locale?.region?.toLowerCase(); | ||||||
|  |     // 1. check region code in ALL_LANGS | ||||||
|  |     if (AllLangs.includes(region as Lang)) { | ||||||
|  |       return region as Lang; | ||||||
|  |     } | ||||||
|  |     // 2. check language code in ALL_LANGS | ||||||
|  |     if (AllLangs.includes(locale.language as Lang)) { | ||||||
|  |       return locale.language as Lang; | ||||||
|  |     } | ||||||
|  |     return DEFAULT_LANG; | ||||||
|   } catch { |   } catch { | ||||||
|     return DEFAULT_LANG; |     return DEFAULT_LANG; | ||||||
|   } |   } | ||||||
| @@ -110,15 +120,7 @@ export function getLang(): Lang { | |||||||
|     return savedLang as Lang; |     return savedLang as Lang; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const lang = getLanguage(); |   return getLanguage(); | ||||||
|  |  | ||||||
|   for (const option of AllLangs) { |  | ||||||
|     if (lang.includes(option)) { |  | ||||||
|       return option; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return DEFAULT_LANG; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function changeLang(lang: Lang) { | export function changeLang(lang: Lang) { | ||||||
|   | |||||||
| @@ -4,11 +4,11 @@ import { SubmitKey } from "../store/config"; | |||||||
| const isApp = !!getClientConfig()?.isApp; | const isApp = !!getClientConfig()?.isApp; | ||||||
|  |  | ||||||
| const tw = { | const tw = { | ||||||
|   WIP: "該功能仍在開發中……", |   WIP: "此功能仍在開發中……", | ||||||
|   Error: { |   Error: { | ||||||
|     Unauthorized: isApp |     Unauthorized: isApp | ||||||
|       ? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否設定正確。" |       ? "偵測到無效的 API Key,請前往[設定](/#/settings)頁面檢查 API Key 是否設定正確。" | ||||||
|       : "存取密碼不正確或未填寫,請前往[登入](/#/auth)頁輸入正確的存取密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。", |       : "存取密碼不正確或尚未填寫,請前往[登入](/#/auth)頁面輸入正確的存取密碼,或者在[設定](/#/settings)頁面填入你自己的 OpenAI API Key。", | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   Auth: { |   Auth: { | ||||||
| @@ -159,7 +159,7 @@ const tw = { | |||||||
|     }, |     }, | ||||||
|     InputTemplate: { |     InputTemplate: { | ||||||
|       Title: "使用者輸入預處理", |       Title: "使用者輸入預處理", | ||||||
|       SubTitle: "使用者最新的一條訊息會填充到此範本", |       SubTitle: "使用者最新的一則訊息會填充到此範本", | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     Update: { |     Update: { | ||||||
| @@ -194,19 +194,19 @@ const tw = { | |||||||
|         }, |         }, | ||||||
|         SyncType: { |         SyncType: { | ||||||
|           Title: "同步類型", |           Title: "同步類型", | ||||||
|           SubTitle: "選擇喜愛的同步伺服器", |           SubTitle: "選擇偏好的同步伺服器", | ||||||
|         }, |         }, | ||||||
|         Proxy: { |         Proxy: { | ||||||
|           Title: "啟用代理", |           Title: "啟用代理伺服器", | ||||||
|           SubTitle: "在瀏覽器中同步時,必須啟用代理以避免跨域限制", |           SubTitle: "在瀏覽器中同步時,啟用代理伺服器以避免跨域限制", | ||||||
|         }, |         }, | ||||||
|         ProxyUrl: { |         ProxyUrl: { | ||||||
|           Title: "代理地址", |           Title: "代理伺服器位置", | ||||||
|           SubTitle: "僅適用於本專案自帶的跨域代理", |           SubTitle: "僅適用於本專案內建的跨域代理", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         WebDav: { |         WebDav: { | ||||||
|           Endpoint: "WebDAV 地址", |           Endpoint: "WebDAV 位置", | ||||||
|           UserName: "使用者名稱", |           UserName: "使用者名稱", | ||||||
|           Password: "密碼", |           Password: "密碼", | ||||||
|         }, |         }, | ||||||
| @@ -218,9 +218,9 @@ const tw = { | |||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|  |  | ||||||
|       LocalState: "本地資料", |       LocalState: "本機資料", | ||||||
|       Overview: (overview: any) => { |       Overview: (overview: any) => { | ||||||
|         return `${overview.chat} 次對話,${overview.message} 條訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`; |         return `${overview.chat} 次對話,${overview.message} 則訊息,${overview.prompt} 條提示詞,${overview.mask} 個角色範本`; | ||||||
|       }, |       }, | ||||||
|       ImportFailed: "匯入失敗", |       ImportFailed: "匯入失敗", | ||||||
|     }, |     }, | ||||||
| @@ -239,13 +239,13 @@ const tw = { | |||||||
|         Title: "停用提示詞自動補齊", |         Title: "停用提示詞自動補齊", | ||||||
|         SubTitle: "在輸入框開頭輸入 / 即可觸發自動補齊", |         SubTitle: "在輸入框開頭輸入 / 即可觸發自動補齊", | ||||||
|       }, |       }, | ||||||
|       List: "自定義提示詞列表", |       List: "自訂提示詞列表", | ||||||
|       ListCount: (builtin: number, custom: number) => |       ListCount: (builtin: number, custom: number) => | ||||||
|         `內建 ${builtin} 條,使用者定義 ${custom} 條`, |       `內建 ${builtin} 條,使用者自訂 ${custom} 條`, | ||||||
|       Edit: "編輯", |       Edit: "編輯", | ||||||
|       Modal: { |       Modal: { | ||||||
|         Title: "提示詞列表", |         Title: "提示詞列表", | ||||||
|         Add: "新增一條", |         Add: "新增一則", | ||||||
|         Search: "搜尋提示詞", |         Search: "搜尋提示詞", | ||||||
|       }, |       }, | ||||||
|       EditModal: { |       EditModal: { | ||||||
| @@ -278,40 +278,40 @@ const tw = { | |||||||
|         Placeholder: "請輸入存取密碼", |         Placeholder: "請輸入存取密碼", | ||||||
|       }, |       }, | ||||||
|       CustomEndpoint: { |       CustomEndpoint: { | ||||||
|         Title: "自定義介面 (Endpoint)", |         Title: "自訂 API 端點 (Endpoint)", | ||||||
|         SubTitle: "是否使用自定義 Azure 或 OpenAI 服務", |         SubTitle: "是否使用自訂 Azure 或 OpenAI 服務", | ||||||
|       }, |       }, | ||||||
|       Provider: { |       Provider: { | ||||||
|         Title: "模型服務商", |         Title: "模型供應商", | ||||||
|         SubTitle: "切換不同的服務商", |         SubTitle: "切換不同的服務供應商", | ||||||
|       }, |       }, | ||||||
|       OpenAI: { |       OpenAI: { | ||||||
|         ApiKey: { |         ApiKey: { | ||||||
|           Title: "API Key", |           Title: "API Key", | ||||||
|           SubTitle: "使用自定義 OpenAI Key 繞過密碼存取限制", |           SubTitle: "使用自訂 OpenAI Key 繞過密碼存取限制", | ||||||
|           Placeholder: "OpenAI API Key", |           Placeholder: "OpenAI API Key", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
|           Title: "介面(Endpoint) 地址", |           Title: "API 端點 (Endpoint) 位址", | ||||||
|           SubTitle: "除預設地址外,必須包含 http(s)://", |           SubTitle: "除預設位址外,必須包含 http(s)://", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       Azure: { |       Azure: { | ||||||
|         ApiKey: { |         ApiKey: { | ||||||
|           Title: "介面金鑰", |           Title: "API 金鑰", | ||||||
|           SubTitle: "使用自定義 Azure Key 繞過密碼存取限制", |           SubTitle: "使用自訂 Azure Key 繞過密碼存取限制", | ||||||
|           Placeholder: "Azure API Key", |           Placeholder: "Azure API Key", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
|           Title: "介面(Endpoint) 地址", |           Title: "API 端點 (Endpoint) 位址", | ||||||
|           SubTitle: "樣例:", |           SubTitle: "範例:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         ApiVerion: { |         ApiVerion: { | ||||||
|           Title: "介面版本 (azure api version)", |           Title: "API 版本 (azure api version)", | ||||||
|           SubTitle: "選擇指定的部分版本", |           SubTitle: "指定一個特定的 API 版本", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       Anthropic: { |       Anthropic: { | ||||||
| @@ -322,13 +322,13 @@ const tw = { | |||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
|           Title: "終端地址", |           Title: "端點位址", | ||||||
|           SubTitle: "範例:", |           SubTitle: "範例:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         ApiVerion: { |         ApiVerion: { | ||||||
|           Title: "API 版本 (claude api version)", |           Title: "API 版本 (claude api version)", | ||||||
|           SubTitle: "選擇一個特定的 API 版本輸入", |           SubTitle: "指定一個特定的 API 版本", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       Google: { |       Google: { | ||||||
| @@ -339,7 +339,7 @@ const tw = { | |||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
|           Title: "終端地址", |           Title: "端點位址", | ||||||
|           SubTitle: "範例:", |           SubTitle: "範例:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -349,8 +349,8 @@ const tw = { | |||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       CustomModel: { |       CustomModel: { | ||||||
|         Title: "自定義模型名", |         Title: "自訂模型名稱", | ||||||
|         SubTitle: "增加自定義模型可選項,使用英文逗號隔開", |         SubTitle: "增加自訂模型可選擇項目,使用英文逗號隔開", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
| @@ -400,7 +400,7 @@ const tw = { | |||||||
|   Context: { |   Context: { | ||||||
|     Toast: (x: any) => `已設定 ${x} 條前置上下文`, |     Toast: (x: any) => `已設定 ${x} 條前置上下文`, | ||||||
|     Edit: "前置上下文和歷史記憶", |     Edit: "前置上下文和歷史記憶", | ||||||
|     Add: "新增一條", |     Add: "新增一則", | ||||||
|     Clear: "上下文已清除", |     Clear: "上下文已清除", | ||||||
|     Revert: "恢復上下文", |     Revert: "恢復上下文", | ||||||
|   }, |   }, | ||||||
| @@ -425,16 +425,16 @@ const tw = { | |||||||
|     EditModal: { |     EditModal: { | ||||||
|       Title: (readonly: boolean) => |       Title: (readonly: boolean) => | ||||||
|         `編輯預設角色範本 ${readonly ? "(唯讀)" : ""}`, |         `編輯預設角色範本 ${readonly ? "(唯讀)" : ""}`, | ||||||
|       Download: "下載預設", |       Download: "下載預設值", | ||||||
|       Clone: "複製預設", |       Clone: "以此預設值建立副本", | ||||||
|     }, |     }, | ||||||
|     Config: { |     Config: { | ||||||
|       Avatar: "角色頭像", |       Avatar: "角色頭像", | ||||||
|       Name: "角色名稱", |       Name: "角色名稱", | ||||||
|       Sync: { |       Sync: { | ||||||
|         Title: "使用全域性設定", |         Title: "使用全域設定", | ||||||
|         SubTitle: "目前對話是否使用全域性模型設定", |         SubTitle: "目前對話是否使用全域模型設定", | ||||||
|         Confirm: "目前對話的自定義設定將會被自動覆蓋,確認啟用全域性設定?", |         Confirm: "目前對話的自訂設定將會被自動覆蓋,確認啟用全域設定?", | ||||||
|       }, |       }, | ||||||
|       HideContext: { |       HideContext: { | ||||||
|         Title: "隱藏預設對話", |         Title: "隱藏預設對話", | ||||||
| @@ -450,15 +450,15 @@ const tw = { | |||||||
|   NewChat: { |   NewChat: { | ||||||
|     Return: "返回", |     Return: "返回", | ||||||
|     Skip: "跳過", |     Skip: "跳過", | ||||||
|     NotShow: "不再呈現", |     NotShow: "不再顯示", | ||||||
|     ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。", |     ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。", | ||||||
|     Title: "挑選一個角色範本", |     Title: "挑選一個角色範本", | ||||||
|     SubTitle: "現在開始,與角色範本背後的靈魂思維碰撞", |     SubTitle: "現在開始,與角色範本背後的靈魂思維碰撞", | ||||||
|     More: "搜尋更多", |     More: "搜尋更多", | ||||||
|   }, |   }, | ||||||
|   URLCommand: { |   URLCommand: { | ||||||
|     Code: "檢測到連結中已經包含存取密碼,是否自動填入?", |     Code: "偵測到連結中已經包含存取密碼,是否自動填入?", | ||||||
|     Settings: "檢測到連結中包含了預設設定,是否自動填入?", |     Settings: "偵測到連結中包含了預設設定,是否自動填入?", | ||||||
|   }, |   }, | ||||||
|   UI: { |   UI: { | ||||||
|     Confirm: "確認", |     Confirm: "確認", | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								app/masks/build.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/masks/build.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import fs from "fs"; | ||||||
|  | import path from "path"; | ||||||
|  | import { CN_MASKS } from "./cn"; | ||||||
|  | import { TW_MASKS } from "./tw"; | ||||||
|  | import { EN_MASKS } from "./en"; | ||||||
|  |  | ||||||
|  | import { type BuiltinMask } from "./typing"; | ||||||
|  |  | ||||||
|  | const BUILTIN_MASKS: Record<string, BuiltinMask[]> = { | ||||||
|  |   cn: CN_MASKS, | ||||||
|  |   tw: TW_MASKS, | ||||||
|  |   en: EN_MASKS, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const dirname = path.dirname(__filename); | ||||||
|  |  | ||||||
|  | fs.writeFile( | ||||||
|  |   dirname + "/../../public/masks.json", | ||||||
|  |   JSON.stringify(BUILTIN_MASKS, null, 4), | ||||||
|  |   function (error) { | ||||||
|  |     if (error) { | ||||||
|  |       console.error("[Build] failed to build masks", error); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | ); | ||||||
| @@ -22,6 +22,20 @@ export const BUILTIN_MASK_STORE = { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...TW_MASKS, ...EN_MASKS].map( | export const BUILTIN_MASKS: BuiltinMask[] = []; | ||||||
|   (m) => BUILTIN_MASK_STORE.add(m), |  | ||||||
| ); | if (typeof window != "undefined") { | ||||||
|  |   // run in browser skip in next server | ||||||
|  |   fetch("/masks.json") | ||||||
|  |     .then((res) => res.json()) | ||||||
|  |     .catch((error) => { | ||||||
|  |       console.error("[Fetch] failed to fetch masks", error); | ||||||
|  |       return { cn: [], tw: [], en: [] }; | ||||||
|  |     }) | ||||||
|  |     .then((masks) => { | ||||||
|  |       const { cn = [], tw = [], en = [] } = masks; | ||||||
|  |       return [...cn, ...tw, ...en].map((m) => { | ||||||
|  |         BUILTIN_MASKS.push(BUILTIN_MASK_STORE.add(m)); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -12,11 +12,34 @@ import { DEFAULT_CONFIG } from "./config"; | |||||||
|  |  | ||||||
| let fetchState = 0; // 0 not fetch, 1 fetching, 2 done | let fetchState = 0; // 0 not fetch, 1 fetching, 2 done | ||||||
|  |  | ||||||
| const DEFAULT_OPENAI_URL = | const isApp = getClientConfig()?.buildMode === "export"; | ||||||
|   getClientConfig()?.buildMode === "export" |  | ||||||
|  | const DEFAULT_OPENAI_URL = isApp | ||||||
|   ? DEFAULT_API_HOST + "/api/proxy/openai" |   ? DEFAULT_API_HOST + "/api/proxy/openai" | ||||||
|   : ApiPath.OpenAI; |   : ApiPath.OpenAI; | ||||||
|  |  | ||||||
|  | const DEFAULT_GOOGLE_URL = isApp | ||||||
|  |   ? DEFAULT_API_HOST + "/api/proxy/google" | ||||||
|  |   : ApiPath.Google; | ||||||
|  |  | ||||||
|  | const DEFAULT_ANTHROPIC_URL = isApp | ||||||
|  |   ? DEFAULT_API_HOST + "/api/proxy/anthropic" | ||||||
|  |   : ApiPath.Anthropic; | ||||||
|  |  | ||||||
|  | const DEFAULT_BAIDU_URL = isApp | ||||||
|  |   ? DEFAULT_API_HOST + "/api/proxy/baidu" | ||||||
|  |   : ApiPath.Baidu; | ||||||
|  |  | ||||||
|  | const DEFAULT_BYTEDANCE_URL = isApp | ||||||
|  |   ? DEFAULT_API_HOST + "/api/proxy/bytedance" | ||||||
|  |   : ApiPath.ByteDance; | ||||||
|  |  | ||||||
|  | const DEFAULT_ALIBABA_URL = isApp | ||||||
|  |   ? DEFAULT_API_HOST + "/api/proxy/alibaba" | ||||||
|  |   : ApiPath.Alibaba; | ||||||
|  |  | ||||||
|  | console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL); | ||||||
|  |  | ||||||
| const DEFAULT_ACCESS_STATE = { | const DEFAULT_ACCESS_STATE = { | ||||||
|   accessCode: "", |   accessCode: "", | ||||||
|   useCustomConfig: false, |   useCustomConfig: false, | ||||||
| @@ -33,14 +56,27 @@ const DEFAULT_ACCESS_STATE = { | |||||||
|   azureApiVersion: "2023-08-01-preview", |   azureApiVersion: "2023-08-01-preview", | ||||||
|  |  | ||||||
|   // google ai studio |   // google ai studio | ||||||
|   googleUrl: "", |   googleUrl: DEFAULT_GOOGLE_URL, | ||||||
|   googleApiKey: "", |   googleApiKey: "", | ||||||
|   googleApiVersion: "v1", |   googleApiVersion: "v1", | ||||||
|  |  | ||||||
|   // anthropic |   // anthropic | ||||||
|  |   anthropicUrl: DEFAULT_ANTHROPIC_URL, | ||||||
|   anthropicApiKey: "", |   anthropicApiKey: "", | ||||||
|   anthropicApiVersion: "2023-06-01", |   anthropicApiVersion: "2023-06-01", | ||||||
|   anthropicUrl: "", |  | ||||||
|  |   // baidu | ||||||
|  |   baiduUrl: DEFAULT_BAIDU_URL, | ||||||
|  |   baiduApiKey: "", | ||||||
|  |   baiduSecretKey: "", | ||||||
|  |  | ||||||
|  |   // bytedance | ||||||
|  |   bytedanceUrl: DEFAULT_BYTEDANCE_URL, | ||||||
|  |   bytedanceApiKey: "", | ||||||
|  |  | ||||||
|  |   // alibaba | ||||||
|  |   alibabaUrl: DEFAULT_ALIBABA_URL, | ||||||
|  |   alibabaApiKey: "", | ||||||
|  |  | ||||||
|   // server config |   // server config | ||||||
|   needCode: true, |   needCode: true, | ||||||
| @@ -78,6 +114,18 @@ export const useAccessStore = createPersistStore( | |||||||
|       return ensure(get(), ["anthropicApiKey"]); |       return ensure(get(), ["anthropicApiKey"]); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     isValidBaidu() { | ||||||
|  |       return ensure(get(), ["baiduApiKey", "baiduSecretKey"]); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     isValidByteDance() { | ||||||
|  |       return ensure(get(), ["bytedanceApiKey"]); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     isValidAlibaba() { | ||||||
|  |       return ensure(get(), ["alibabaApiKey"]); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     isAuthorized() { |     isAuthorized() { | ||||||
|       this.fetch(); |       this.fetch(); | ||||||
|  |  | ||||||
| @@ -87,6 +135,9 @@ export const useAccessStore = createPersistStore( | |||||||
|         this.isValidAzure() || |         this.isValidAzure() || | ||||||
|         this.isValidGoogle() || |         this.isValidGoogle() || | ||||||
|         this.isValidAnthropic() || |         this.isValidAnthropic() || | ||||||
|  |         this.isValidBaidu() || | ||||||
|  |         this.isValidByteDance() || | ||||||
|  |         this.isValidAlibaba() || | ||||||
|         !this.enabledAccessControl() || |         !this.enabledAccessControl() || | ||||||
|         (this.enabledAccessControl() && ensure(get(), ["accessCode"])) |         (this.enabledAccessControl() && ensure(get(), ["accessCode"])) | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -9,18 +9,25 @@ import { | |||||||
|   DEFAULT_MODELS, |   DEFAULT_MODELS, | ||||||
|   DEFAULT_SYSTEM_TEMPLATE, |   DEFAULT_SYSTEM_TEMPLATE, | ||||||
|   KnowledgeCutOffDate, |   KnowledgeCutOffDate, | ||||||
|  |   ServiceProvider, | ||||||
|   ModelProvider, |   ModelProvider, | ||||||
|   StoreKey, |   StoreKey, | ||||||
|   SUMMARIZE_MODEL, |   SUMMARIZE_MODEL, | ||||||
|   GEMINI_SUMMARIZE_MODEL, |   GEMINI_SUMMARIZE_MODEL, | ||||||
| } from "../constant"; | } from "../constant"; | ||||||
| import { ClientApi, RequestMessage, MultimodalContent } from "../client/api"; | import { getClientApi } from "../client/api"; | ||||||
|  | import type { | ||||||
|  |   ClientApi, | ||||||
|  |   RequestMessage, | ||||||
|  |   MultimodalContent, | ||||||
|  | } from "../client/api"; | ||||||
| import { ChatControllerPool } from "../client/controller"; | import { ChatControllerPool } from "../client/controller"; | ||||||
| import { prettyObject } from "../utils/format"; | import { prettyObject } from "../utils/format"; | ||||||
| import { estimateTokenLength } from "../utils/token"; | 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 { collectModelsWithDefaultModel } from "../utils/model"; | ||||||
|  | import { useAccessStore } from "./access"; | ||||||
|  |  | ||||||
| export type ChatMessage = RequestMessage & { | export type ChatMessage = RequestMessage & { | ||||||
|   date: string; |   date: string; | ||||||
| @@ -87,9 +94,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; | ||||||
| @@ -351,15 +368,7 @@ export const useChatStore = createPersistStore( | |||||||
|           ]); |           ]); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         var api: ClientApi; |         const api: ClientApi = getClientApi(modelConfig.providerName); | ||||||
|         if (modelConfig.model.startsWith("gemini")) { |  | ||||||
|           api = new ClientApi(ModelProvider.GeminiPro); |  | ||||||
|         } else if (identifyDefaultClaudeModel(modelConfig.model)) { |  | ||||||
|           api = new ClientApi(ModelProvider.Claude); |  | ||||||
|         } else { |  | ||||||
|           api = new ClientApi(ModelProvider.GPT); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // make request |         // make request | ||||||
|         api.llm.chat({ |         api.llm.chat({ | ||||||
|           messages: sendMessages, |           messages: sendMessages, | ||||||
| @@ -416,14 +425,13 @@ export const useChatStore = createPersistStore( | |||||||
|       getMemoryPrompt() { |       getMemoryPrompt() { | ||||||
|         const session = get().currentSession(); |         const session = get().currentSession(); | ||||||
|  |  | ||||||
|  |         if (session.memoryPrompt.length) { | ||||||
|           return { |           return { | ||||||
|             role: "system", |             role: "system", | ||||||
|           content: |             content: Locale.Store.Prompt.History(session.memoryPrompt), | ||||||
|             session.memoryPrompt.length > 0 |  | ||||||
|               ? Locale.Store.Prompt.History(session.memoryPrompt) |  | ||||||
|               : "", |  | ||||||
|             date: "", |             date: "", | ||||||
|           } as ChatMessage; |           } as ChatMessage; | ||||||
|  |         } | ||||||
|       }, |       }, | ||||||
|  |  | ||||||
|       getMessagesWithMemory() { |       getMessagesWithMemory() { | ||||||
| @@ -459,16 +467,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 | ||||||
| @@ -537,14 +544,7 @@ export const useChatStore = createPersistStore( | |||||||
|         const session = get().currentSession(); |         const session = get().currentSession(); | ||||||
|         const modelConfig = session.mask.modelConfig; |         const modelConfig = session.mask.modelConfig; | ||||||
|  |  | ||||||
|         var api: ClientApi; |         const api: ClientApi = getClientApi(modelConfig.providerName); | ||||||
|         if (modelConfig.model.startsWith("gemini")) { |  | ||||||
|           api = new ClientApi(ModelProvider.GeminiPro); |  | ||||||
|         } else if (identifyDefaultClaudeModel(modelConfig.model)) { |  | ||||||
|           api = new ClientApi(ModelProvider.Claude); |  | ||||||
|         } else { |  | ||||||
|           api = new ClientApi(ModelProvider.GPT); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // remove error messages if any |         // remove error messages if any | ||||||
|         const messages = session.messages; |         const messages = session.messages; | ||||||
| @@ -593,9 +593,11 @@ export const useChatStore = createPersistStore( | |||||||
|             Math.max(0, n - modelConfig.historyMessageCount), |             Math.max(0, n - modelConfig.historyMessageCount), | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |         const memoryPrompt = get().getMemoryPrompt(); | ||||||
|  |         if (memoryPrompt) { | ||||||
|           // add memory prompt |           // add memory prompt | ||||||
|         toBeSummarizedMsgs.unshift(get().getMemoryPrompt()); |           toBeSummarizedMsgs.unshift(memoryPrompt); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const lastSummarizeIndex = session.messages.length; |         const lastSummarizeIndex = session.messages.length; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { LLMModel } from "../client/api"; | import { LLMModel } from "../client/api"; | ||||||
| import { isMacOS } from "../utils"; |  | ||||||
| import { getClientConfig } from "../config/client"; | import { getClientConfig } from "../config/client"; | ||||||
| import { | import { | ||||||
|   DEFAULT_INPUT_TEMPLATE, |   DEFAULT_INPUT_TEMPLATE, | ||||||
|   DEFAULT_MODELS, |   DEFAULT_MODELS, | ||||||
|   DEFAULT_SIDEBAR_WIDTH, |   DEFAULT_SIDEBAR_WIDTH, | ||||||
|   StoreKey, |   StoreKey, | ||||||
|  |   ServiceProvider, | ||||||
| } from "../constant"; | } from "../constant"; | ||||||
| import { createPersistStore } from "../utils/store"; | import { createPersistStore } from "../utils/store"; | ||||||
|  |  | ||||||
| @@ -25,6 +25,8 @@ export enum Theme { | |||||||
|   Light = "light", |   Light = "light", | ||||||
| } | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  |  | ||||||
| @@ -32,7 +34,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, | ||||||
| @@ -47,6 +49,7 @@ export const DEFAULT_CONFIG = { | |||||||
|  |  | ||||||
|   modelConfig: { |   modelConfig: { | ||||||
|     model: "gpt-3.5-turbo" as ModelType, |     model: "gpt-3.5-turbo" as ModelType, | ||||||
|  |     providerName: "OpenAI" as ServiceProvider, | ||||||
|     temperature: 0.5, |     temperature: 0.5, | ||||||
|     top_p: 1, |     top_p: 1, | ||||||
|     max_tokens: 4000, |     max_tokens: 4000, | ||||||
| @@ -56,7 +59,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, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -115,12 +118,12 @@ export const useAppConfig = createPersistStore( | |||||||
|  |  | ||||||
|       for (const model of oldModels) { |       for (const model of oldModels) { | ||||||
|         model.available = false; |         model.available = false; | ||||||
|         modelMap[model.name] = model; |         modelMap[`${model.name}@${model?.provider?.id}`] = model; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       for (const model of newModels) { |       for (const model of newModels) { | ||||||
|         model.available = true; |         model.available = true; | ||||||
|         modelMap[model.name] = model; |         modelMap[`${model.name}@${model?.provider?.id}`] = model; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       set(() => ({ |       set(() => ({ | ||||||
| @@ -132,7 +135,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; | ||||||
|  |  | ||||||
| @@ -163,6 +166,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; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -97,11 +97,18 @@ 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); | ||||||
|  |         if (!remoteState || remoteState === "") { | ||||||
|  |           await client.set(config.username, JSON.stringify(localState)); | ||||||
|  |           console.log("[Sync] Remote state is empty, using local state instead."); | ||||||
|  |           return | ||||||
|  |         } else { | ||||||
|  |           const parsedRemoteState = JSON.parse( | ||||||
|             await client.get(config.username), |             await client.get(config.username), | ||||||
|           ) as AppState; |           ) as AppState; | ||||||
|         mergeAppState(localState, remoteState); |           mergeAppState(localState, parsedRemoteState); | ||||||
|           setLocalAppState(localState); |           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; | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								app/utils.ts
									
									
									
									
									
								
							| @@ -83,48 +83,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"); | ||||||
| @@ -290,16 +248,19 @@ 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; |   ); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								app/utils/baidu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/utils/baidu.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { BAIDU_OATUH_URL } from "../constant"; | ||||||
|  | /** | ||||||
|  |  * 使用 AK,SK 生成鉴权签名(Access Token) | ||||||
|  |  * @return 鉴权签名信息 | ||||||
|  |  */ | ||||||
|  | export async function getAccessToken( | ||||||
|  |   clientId: string, | ||||||
|  |   clientSecret: string, | ||||||
|  | ): Promise<{ | ||||||
|  |   access_token: string; | ||||||
|  |   expires_in: number; | ||||||
|  |   error?: number; | ||||||
|  | }> { | ||||||
|  |   const res = await fetch( | ||||||
|  |     `${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`, | ||||||
|  |     { | ||||||
|  |       method: "POST", | ||||||
|  |       mode: "cors", | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  |   const resJson = await res.json(); | ||||||
|  |   return resJson; | ||||||
|  | } | ||||||
							
								
								
									
										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); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| import { useAccessStore } from "../store/access"; |  | ||||||
| import { useAppConfig } from "../store/config"; |  | ||||||
| import { collectModels } from "./model"; |  | ||||||
|  |  | ||||||
| export function identifyDefaultClaudeModel(modelName: string) { |  | ||||||
|   const accessStore = useAccessStore.getState(); |  | ||||||
|   const configStore = useAppConfig.getState(); |  | ||||||
|  |  | ||||||
|   const allModals = collectModels( |  | ||||||
|     configStore.models, |  | ||||||
|     [configStore.customModels, accessStore.customModels].join(","), |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const modelMeta = allModals.find((m) => m.name === modelName); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     modelName.startsWith("claude") && |  | ||||||
|     modelMeta && |  | ||||||
|     modelMeta.provider?.providerType === "anthropic" |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -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; | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								app/utils/cloudflare.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/utils/cloudflare.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | export function cloudflareAIGatewayUrl(fetchUrl: string) { | ||||||
|  |   // rebuild fetchUrl, if using cloudflare ai gateway | ||||||
|  |   // document: https://developers.cloudflare.com/ai-gateway/providers/openai/ | ||||||
|  |  | ||||||
|  |   const paths = fetchUrl.split("/"); | ||||||
|  |   if ("gateway.ai.cloudflare.com" == paths[2]) { | ||||||
|  |     // is cloudflare.com ai gateway | ||||||
|  |     // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15' | ||||||
|  |     if ("azure-openai" == paths[6]) { | ||||||
|  |       // is azure gateway | ||||||
|  |       return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url | ||||||
|  |     } | ||||||
|  |     // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions | ||||||
|  |     if ("openai" == paths[6]) { | ||||||
|  |       // is openai gateway | ||||||
|  |       return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url | ||||||
|  |     } | ||||||
|  |     // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \ | ||||||
|  |     if ("anthropic" == paths[6]) { | ||||||
|  |       // is anthropic gateway | ||||||
|  |       return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url | ||||||
|  |     } | ||||||
|  |     // TODO: Amazon Bedrock, Groq, HuggingFace... | ||||||
|  |   } | ||||||
|  |   return fetchUrl; | ||||||
|  | } | ||||||
| @@ -11,7 +11,12 @@ export function useAllModels() { | |||||||
|       [configStore.customModels, accessStore.customModels].join(","), |       [configStore.customModels, accessStore.customModels].join(","), | ||||||
|       accessStore.defaultModel, |       accessStore.defaultModel, | ||||||
|     ); |     ); | ||||||
|   }, [accessStore.customModels, configStore.customModels, configStore.models]); |   }, [ | ||||||
|  |     accessStore.customModels, | ||||||
|  |     accessStore.defaultModel, | ||||||
|  |     configStore.customModels, | ||||||
|  |     configStore.models, | ||||||
|  |   ]); | ||||||
|  |  | ||||||
|   return models; |   return models; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
|  | import { DEFAULT_MODELS } from "../constant"; | ||||||
| import { LLMModel } from "../client/api"; | import { LLMModel } from "../client/api"; | ||||||
|  |  | ||||||
| const customProvider = (modelName: string) => ({ | const customProvider = (providerName: string) => ({ | ||||||
|   id: modelName, |   id: providerName.toLowerCase(), | ||||||
|   providerName: "", |   providerName: providerName, | ||||||
|   providerType: "custom", |   providerType: "custom", | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -23,7 +24,8 @@ export function collectModelTable( | |||||||
|  |  | ||||||
|   // default models |   // default models | ||||||
|   models.forEach((m) => { |   models.forEach((m) => { | ||||||
|     modelTable[m.name] = { |     // using <modelName>@<providerId> as fullName | ||||||
|  |     modelTable[`${m.name}@${m?.provider?.id}`] = { | ||||||
|       ...m, |       ...m, | ||||||
|       displayName: m.name, // 'provider' is copied over if it exists |       displayName: m.name, // 'provider' is copied over if it exists | ||||||
|     }; |     }; | ||||||
| @@ -37,7 +39,7 @@ export function collectModelTable( | |||||||
|       const available = !m.startsWith("-"); |       const available = !m.startsWith("-"); | ||||||
|       const nameConfig = |       const nameConfig = | ||||||
|         m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; |         m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; | ||||||
|       const [name, displayName] = nameConfig.split("="); |       let [name, displayName] = nameConfig.split("="); | ||||||
|  |  | ||||||
|       // enable or disable all models |       // enable or disable all models | ||||||
|       if (name === "all") { |       if (name === "all") { | ||||||
| @@ -45,13 +47,46 @@ export function collectModelTable( | |||||||
|           (model) => (model.available = available), |           (model) => (model.available = available), | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         modelTable[name] = { |         // 1. find model by name, and set available value | ||||||
|           name, |         const [customModelName, customProviderName] = name.split("@"); | ||||||
|           displayName: displayName || name, |         let count = 0; | ||||||
|  |         for (const fullName in modelTable) { | ||||||
|  |           const [modelName, providerName] = fullName.split("@"); | ||||||
|  |           if ( | ||||||
|  |             customModelName == modelName && | ||||||
|  |             (customProviderName === undefined || | ||||||
|  |               customProviderName === providerName) | ||||||
|  |           ) { | ||||||
|  |             count += 1; | ||||||
|  |             modelTable[fullName]["available"] = available; | ||||||
|  |             // swap name and displayName for bytedance | ||||||
|  |             if (providerName === "bytedance") { | ||||||
|  |               [name, displayName] = [displayName, modelName]; | ||||||
|  |               modelTable[fullName]["name"] = name; | ||||||
|  |             } | ||||||
|  |             if (displayName) { | ||||||
|  |               modelTable[fullName]["displayName"] = displayName; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         // 2. if model not exists, create new model with available value | ||||||
|  |         if (count === 0) { | ||||||
|  |           let [customModelName, customProviderName] = name.split("@"); | ||||||
|  |           const provider = customProvider( | ||||||
|  |             customProviderName || customModelName, | ||||||
|  |           ); | ||||||
|  |           // swap name and displayName for bytedance | ||||||
|  |           if (displayName && provider.providerName == "ByteDance") { | ||||||
|  |             [customModelName, displayName] = [displayName, customModelName]; | ||||||
|  |           } | ||||||
|  |           modelTable[`${customModelName}@${provider?.id}`] = { | ||||||
|  |             name: customModelName, | ||||||
|  |             displayName: displayName || customModelName, | ||||||
|             available, |             available, | ||||||
|           provider: modelTable[name]?.provider ?? customProvider(name), // Use optional chaining |             provider, // Use optional chaining | ||||||
|           }; |           }; | ||||||
|         } |         } | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   return modelTable; |   return modelTable; | ||||||
| @@ -64,13 +99,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, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| @@ -103,3 +135,13 @@ export function collectModelsWithDefaultModel( | |||||||
|   const allModels = Object.values(modelTable); |   const allModels = Object.values(modelTable); | ||||||
|   return allModels; |   return allModels; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function isModelAvailableInServer( | ||||||
|  |   customModels: string, | ||||||
|  |   modelName: string, | ||||||
|  |   providerName: string, | ||||||
|  | ) { | ||||||
|  |   const fullName = `${modelName}@${providerName}`; | ||||||
|  |   const modelTable = collectModelTable(DEFAULT_MODELS, customModels); | ||||||
|  |   return modelTable[fullName]?.available === false; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
| 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 | 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 | ||||||
| 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: | 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: | ||||||
|    ``` |    ``` | ||||||
|    npx @cloudflare/next-on-pages@1.5.0 |    npx @cloudflare/next-on-pages --experimental-minify | ||||||
|    ``` |    ``` | ||||||
| 9. 对于 "Build output directory",使用默认值并且不要修改。 | 9. 对于 "Build output directory",使用默认值并且不要修改。 | ||||||
| 10. 不要修改 "Root Directory"。 | 10. 不要修改 "Root Directory"。 | ||||||
|   | |||||||
| @@ -12,7 +12,9 @@ Bifurca el proyecto en Github, luego inicia sesión en dash.cloudflare.com y ve | |||||||
| 6.  Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario. | 6.  Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario. | ||||||
| 7.  En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js. | 7.  En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js. | ||||||
| 8.  Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando: | 8.  Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando: | ||||||
|         npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify |     ``` | ||||||
|  |     npx @cloudflare/next-on-pages --experimental-minify | ||||||
|  |     ``` | ||||||
| 9.  Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique. | 9.  Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique. | ||||||
| 10. No modifique el "Directorio raíz". | 10. No modifique el "Directorio raíz". | ||||||
| 11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información: | 11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información: | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ GitHub でこのプロジェクトをフォークし、dash.cloudflare.com に | |||||||
| 7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 | 7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 | ||||||
| 8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: | 8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: | ||||||
|     ``` |     ``` | ||||||
|     npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify |     npx @cloudflare/next-on-pages --experimental-minify | ||||||
|     ``` |     ``` | ||||||
| 9. "Build output directory" はデフォルト値を使用し、変更しない。 | 9. "Build output directory" はデフォルト値を使用し、変更しない。 | ||||||
| 10. "Root Directory" を変更しない。 | 10. "Root Directory" を変更しない。 | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ | |||||||
| 6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. | 6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. | ||||||
| 7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. | 7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. | ||||||
| 8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: | 8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: | ||||||
|    `` |    ``` | ||||||
|    npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental- minify |    npx @cloudflare/next-on-pages --experimental-minify | ||||||
|    ``` |    ``` | ||||||
| 9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. | 9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. | ||||||
| 10. "루트 디렉토리"는 수정하지 마십시오. | 10. "루트 디렉토리"는 수정하지 마십시오. | ||||||
|   | |||||||
| @@ -69,6 +69,11 @@ if (mode !== "export") { | |||||||
|         source: "/api/proxy/v1/:path*", |         source: "/api/proxy/v1/:path*", | ||||||
|         destination: "https://api.openai.com/v1/:path*", |         destination: "https://api.openai.com/v1/:path*", | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions | ||||||
|  |         source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", | ||||||
|  |         destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         source: "/api/proxy/google/:path*", |         source: "/api/proxy/google/:path*", | ||||||
|         destination: "https://generativelanguage.googleapis.com/:path*", |         destination: "https://generativelanguage.googleapis.com/:path*", | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								package.json
									
									
									
									
									
								
							| @@ -3,14 +3,16 @@ | |||||||
|   "private": false, |   "private": false, | ||||||
|   "license": "mit", |   "license": "mit", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "next dev", |     "mask": "npx tsx app/masks/build.ts", | ||||||
|     "build": "cross-env BUILD_MODE=standalone next build", |     "mask:watch": "npx watch 'yarn mask' app/masks", | ||||||
|  |     "dev": "yarn run mask:watch & next dev", | ||||||
|  |     "build": "yarn mask && cross-env BUILD_MODE=standalone next build", | ||||||
|     "start": "next start", |     "start": "next start", | ||||||
|     "lint": "next lint", |     "lint": "next lint", | ||||||
|     "export": "cross-env BUILD_MODE=export BUILD_APP=1 next build", |     "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build", | ||||||
|     "export:dev": "cross-env BUILD_MODE=export BUILD_APP=1 next dev", |     "export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev", | ||||||
|     "app:dev": "yarn tauri dev", |     "app:dev": "yarn mask:watch & yarn tauri dev", | ||||||
|     "app:build": "yarn tauri build", |     "app:build": "yarn mask && yarn tauri build", | ||||||
|     "prompts": "node ./scripts/fetch-prompts.mjs", |     "prompts": "node ./scripts/fetch-prompts.mjs", | ||||||
|     "prepare": "husky install", |     "prepare": "husky install", | ||||||
|     "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" |     "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" | ||||||
| @@ -24,10 +26,11 @@ | |||||||
|     "@vercel/speed-insights": "^1.0.2", |     "@vercel/speed-insights": "^1.0.2", | ||||||
|     "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", | ||||||
|     "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", | ||||||
| @@ -58,7 +61,9 @@ | |||||||
|     "husky": "^8.0.0", |     "husky": "^8.0.0", | ||||||
|     "lint-staged": "^13.2.2", |     "lint-staged": "^13.2.2", | ||||||
|     "prettier": "^3.0.2", |     "prettier": "^3.0.2", | ||||||
|  |     "tsx": "^4.16.0", | ||||||
|     "typescript": "5.2.2", |     "typescript": "5.2.2", | ||||||
|  |     "watch": "^1.0.2", | ||||||
|     "webpack": "^5.88.1" |     "webpack": "^5.88.1" | ||||||
|   }, |   }, | ||||||
|   "resolutions": { |   "resolutions": { | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| @@ -9,7 +9,7 @@ | |||||||
|   }, |   }, | ||||||
|   "package": { |   "package": { | ||||||
|     "productName": "NextChat", |     "productName": "NextChat", | ||||||
|     "version": "2.11.3" |     "version": "2.13.0" | ||||||
|   }, |   }, | ||||||
|   "tauri": { |   "tauri": { | ||||||
|     "allowlist": { |     "allowlist": { | ||||||
|   | |||||||
							
								
								
									
										356
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										356
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1092,6 +1092,121 @@ | |||||||
|   resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" |   resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" | ||||||
|   integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== |   integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== | ||||||
|  |  | ||||||
|  | "@esbuild/aix-ppc64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" | ||||||
|  |   integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== | ||||||
|  |  | ||||||
|  | "@esbuild/android-arm64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" | ||||||
|  |   integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== | ||||||
|  |  | ||||||
|  | "@esbuild/android-arm@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" | ||||||
|  |   integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== | ||||||
|  |  | ||||||
|  | "@esbuild/android-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" | ||||||
|  |   integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== | ||||||
|  |  | ||||||
|  | "@esbuild/darwin-arm64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" | ||||||
|  |   integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== | ||||||
|  |  | ||||||
|  | "@esbuild/darwin-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" | ||||||
|  |   integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== | ||||||
|  |  | ||||||
|  | "@esbuild/freebsd-arm64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" | ||||||
|  |   integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== | ||||||
|  |  | ||||||
|  | "@esbuild/freebsd-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" | ||||||
|  |   integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-arm64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" | ||||||
|  |   integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-arm@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" | ||||||
|  |   integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-ia32@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" | ||||||
|  |   integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-loong64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" | ||||||
|  |   integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-mips64el@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" | ||||||
|  |   integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-ppc64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" | ||||||
|  |   integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-riscv64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" | ||||||
|  |   integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-s390x@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" | ||||||
|  |   integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" | ||||||
|  |   integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== | ||||||
|  |  | ||||||
|  | "@esbuild/netbsd-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" | ||||||
|  |   integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== | ||||||
|  |  | ||||||
|  | "@esbuild/openbsd-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" | ||||||
|  |   integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== | ||||||
|  |  | ||||||
|  | "@esbuild/sunos-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" | ||||||
|  |   integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-arm64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" | ||||||
|  |   integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-ia32@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" | ||||||
|  |   integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-x64@0.21.5": | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" | ||||||
|  |   integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== | ||||||
|  |  | ||||||
| "@eslint-community/eslint-utils@^4.2.0": | "@eslint-community/eslint-utils@^4.2.0": | ||||||
|   version "4.4.0" |   version "4.4.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" |   resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" | ||||||
| @@ -1218,10 +1333,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" | ||||||
| @@ -1230,50 +1345,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" | ||||||
| @@ -1424,10 +1539,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" | ||||||
|  |  | ||||||
| @@ -2130,10 +2245,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== | ||||||
|  |  | ||||||
| ccount@^2.0.0: | ccount@^2.0.0: | ||||||
|   version "2.0.1" |   version "2.0.1" | ||||||
| @@ -2981,6 +3096,35 @@ es-to-primitive@^1.2.1: | |||||||
|     is-date-object "^1.0.1" |     is-date-object "^1.0.1" | ||||||
|     is-symbol "^1.0.2" |     is-symbol "^1.0.2" | ||||||
|  |  | ||||||
|  | esbuild@~0.21.5: | ||||||
|  |   version "0.21.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" | ||||||
|  |   integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== | ||||||
|  |   optionalDependencies: | ||||||
|  |     "@esbuild/aix-ppc64" "0.21.5" | ||||||
|  |     "@esbuild/android-arm" "0.21.5" | ||||||
|  |     "@esbuild/android-arm64" "0.21.5" | ||||||
|  |     "@esbuild/android-x64" "0.21.5" | ||||||
|  |     "@esbuild/darwin-arm64" "0.21.5" | ||||||
|  |     "@esbuild/darwin-x64" "0.21.5" | ||||||
|  |     "@esbuild/freebsd-arm64" "0.21.5" | ||||||
|  |     "@esbuild/freebsd-x64" "0.21.5" | ||||||
|  |     "@esbuild/linux-arm" "0.21.5" | ||||||
|  |     "@esbuild/linux-arm64" "0.21.5" | ||||||
|  |     "@esbuild/linux-ia32" "0.21.5" | ||||||
|  |     "@esbuild/linux-loong64" "0.21.5" | ||||||
|  |     "@esbuild/linux-mips64el" "0.21.5" | ||||||
|  |     "@esbuild/linux-ppc64" "0.21.5" | ||||||
|  |     "@esbuild/linux-riscv64" "0.21.5" | ||||||
|  |     "@esbuild/linux-s390x" "0.21.5" | ||||||
|  |     "@esbuild/linux-x64" "0.21.5" | ||||||
|  |     "@esbuild/netbsd-x64" "0.21.5" | ||||||
|  |     "@esbuild/openbsd-x64" "0.21.5" | ||||||
|  |     "@esbuild/sunos-x64" "0.21.5" | ||||||
|  |     "@esbuild/win32-arm64" "0.21.5" | ||||||
|  |     "@esbuild/win32-ia32" "0.21.5" | ||||||
|  |     "@esbuild/win32-x64" "0.21.5" | ||||||
|  |  | ||||||
| escalade@^3.1.1: | escalade@^3.1.1: | ||||||
|   version "3.1.1" |   version "3.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" |   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" | ||||||
| @@ -3234,6 +3378,13 @@ events@^3.2.0: | |||||||
|   resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" |   resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" | ||||||
|   integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== |   integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== | ||||||
|  |  | ||||||
|  | exec-sh@^0.2.0: | ||||||
|  |   version "0.2.2" | ||||||
|  |   resolved "https://registry.npmmirror.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" | ||||||
|  |   integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== | ||||||
|  |   dependencies: | ||||||
|  |     merge "^1.2.0" | ||||||
|  |  | ||||||
| execa@^7.0.0: | execa@^7.0.0: | ||||||
|   version "7.1.1" |   version "7.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" |   resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" | ||||||
| @@ -3376,6 +3527,11 @@ fsevents@~2.3.2: | |||||||
|   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" |   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" | ||||||
|   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== |   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== | ||||||
|  |  | ||||||
|  | fsevents@~2.3.3: | ||||||
|  |   version "2.3.3" | ||||||
|  |   resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" | ||||||
|  |   integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== | ||||||
|  |  | ||||||
| function-bind@^1.1.1: | function-bind@^1.1.1: | ||||||
|   version "1.1.1" |   version "1.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" |   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" | ||||||
| @@ -3433,6 +3589,13 @@ get-tsconfig@^4.5.0: | |||||||
|   resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f" |   resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f" | ||||||
|   integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ== |   integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ== | ||||||
|  |  | ||||||
|  | get-tsconfig@^4.7.5: | ||||||
|  |   version "4.7.5" | ||||||
|  |   resolved "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" | ||||||
|  |   integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== | ||||||
|  |   dependencies: | ||||||
|  |     resolve-pkg-maps "^1.0.0" | ||||||
|  |  | ||||||
| glob-parent@^5.1.2, glob-parent@~5.1.2: | glob-parent@^5.1.2, glob-parent@~5.1.2: | ||||||
|   version "5.1.2" |   version "5.1.2" | ||||||
|   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" | ||||||
| @@ -3525,7 +3688,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== | ||||||
| @@ -3669,6 +3832,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" | ||||||
| @@ -4382,6 +4550,11 @@ merge2@^1.3.0, merge2@^1.4.1: | |||||||
|   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" |   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" | ||||||
|   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== |   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== | ||||||
|  |  | ||||||
|  | merge@^1.2.0: | ||||||
|  |   version "1.2.1" | ||||||
|  |   resolved "https://registry.npmmirror.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" | ||||||
|  |   integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== | ||||||
|  |  | ||||||
| mermaid@^10.6.1: | mermaid@^10.6.1: | ||||||
|   version "10.6.1" |   version "10.6.1" | ||||||
|   resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc" |   resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc" | ||||||
| @@ -4753,10 +4926,10 @@ ms@^2.1.1: | |||||||
|   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" |   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | ||||||
|   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== |   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | ||||||
|  |  | ||||||
| nanoid@^3.3.4: | nanoid@^3.3.6: | ||||||
|   version "3.3.6" |   version "3.3.7" | ||||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" |   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" | ||||||
|   integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== |   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== | ||||||
|  |  | ||||||
| nanoid@^5.0.3: | nanoid@^5.0.3: | ||||||
|   version "5.0.3" |   version "5.0.3" | ||||||
| @@ -4773,29 +4946,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" | ||||||
| @@ -5036,12 +5208,12 @@ pidtree@^0.6.0: | |||||||
|   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" |   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" | ||||||
|   integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== |   integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== | ||||||
|  |  | ||||||
| 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" | ||||||
|  |  | ||||||
| @@ -5313,6 +5485,11 @@ resolve-from@^4.0.0: | |||||||
|   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" |   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" | ||||||
|   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== |   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== | ||||||
|  |  | ||||||
|  | resolve-pkg-maps@^1.0.0: | ||||||
|  |   version "1.0.0" | ||||||
|  |   resolved "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" | ||||||
|  |   integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== | ||||||
|  |  | ||||||
| resolve@^1.14.2, resolve@^1.22.1: | resolve@^1.14.2, resolve@^1.22.1: | ||||||
|   version "1.22.1" |   version "1.22.1" | ||||||
|   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" |   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" | ||||||
| @@ -5819,6 +5996,16 @@ tslib@^2.6.2: | |||||||
|   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" |   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" | ||||||
|   integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== |   integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== | ||||||
|  |  | ||||||
|  | tsx@^4.16.0: | ||||||
|  |   version "4.16.0" | ||||||
|  |   resolved "https://registry.npmmirror.com/tsx/-/tsx-4.16.0.tgz#913dd96f191b76f07a8744201d8c15d510aa1352" | ||||||
|  |   integrity sha512-MPgN+CuY+4iKxGoJNPv+1pyo5YWZAQ5XfsyobUG+zoKG7IkvCPLZDEyoIb8yLS2FcWci1nlxAqmvPlFWD5AFiQ== | ||||||
|  |   dependencies: | ||||||
|  |     esbuild "~0.21.5" | ||||||
|  |     get-tsconfig "^4.7.5" | ||||||
|  |   optionalDependencies: | ||||||
|  |     fsevents "~2.3.3" | ||||||
|  |  | ||||||
| type-check@^0.4.0, type-check@~0.4.0: | type-check@^0.4.0, type-check@~0.4.0: | ||||||
|   version "0.4.0" |   version "0.4.0" | ||||||
|   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" |   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" | ||||||
| @@ -6039,7 +6226,15 @@ 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: | watch@^1.0.2: | ||||||
|  |   version "1.0.2" | ||||||
|  |   resolved "https://registry.npmmirror.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" | ||||||
|  |   integrity sha512-1u+Z5n9Jc1E2c7qDO8SinPoZuHj7FgbgU1olSFoyaklduDvvtX7GMMtlE6OC9FTXq4KvNAOfj6Zu4vI1e9bAKA== | ||||||
|  |   dependencies: | ||||||
|  |     exec-sh "^0.2.0" | ||||||
|  |     minimist "^1.2.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== | ||||||
| @@ -6185,11 +6380,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