Compare commits

...

15 Commits

Author SHA1 Message Date
Aihararaです
e6984cdda7
Merge e83f2bad84 into 0031544e14 2025-07-12 20:33:38 -03:00
RiverRay
0031544e14
Merge pull request #6552 from hyiip/main
Some checks failed
Run Tests / test (push) Has been cancelled
Migrate to claude 4
2025-07-08 23:35:22 +08:00
RiverRay
1f33ceee8f
Merge pull request #6557 from JI4JUN/feat/support-302ai-provider
Feat/support 302ai provider
2025-07-08 23:34:38 +08:00
JI4JUN
666ca734ec docs: update README 2025-07-07 18:20:04 +08:00
JI4JUN
fda2eb1fb5 docs: update README 2025-07-07 18:18:46 +08:00
JI4JUN
93f8340744 Merge branch 'main' into feat/support-302ai-provider 2025-07-07 18:17:57 +08:00
hyiip
21d39b8dd6 Migrate to claude 4 2025-07-02 22:14:32 +08:00
RiverRay
814fd2786e
Merge pull request #6542 from JI4JUN/feat/support-302ai-provider
Some checks failed
Run Tests / test (push) Has been cancelled
Feat/support 302ai provider
2025-06-30 20:29:47 +08:00
river
29dbffac3e fix: update section title for Sponsor AI API in README 2025-06-30 14:58:36 +08:00
river
92532b2c74 fix: update 302.AI banners in README files and standardize formatting 2025-06-30 14:57:37 +08:00
river
4f16ca1320 fix: update 302.AI banners in README files and remove old images 2025-06-30 14:54:47 +08:00
JI4JUN
4d43fac12a chore: add banners of 302.AI 2025-06-26 19:02:08 +08:00
JI4JUN
d3e164f23e feat: add 302.AI provider 2025-06-25 18:10:02 +08:00
RiverRay
673f907ea4
Update README.md
Some checks failed
Run Tests / test (push) Has been cancelled
2025-06-19 20:18:28 +08:00
Aiharara
e83f2bad84 Fix provider comparison issue in model comparison
Fixed an issue where comparing models using the provider object directly resulted in errors. Changed to compare provider.id to ensure the comparison is based on a unique identifier, accurately determining if the models are the same.

If the provider comparison fails, more models will be stored in persistStore. Although this issue is not immediately visible in the frontend due to subsequent processing, it leads to increased memory usage and longer startup times with each page reload.
2025-04-29 04:51:14 +08:00
35 changed files with 855 additions and 51 deletions

View File

@ -81,3 +81,9 @@ SILICONFLOW_API_KEY=
### siliconflow Api url (optional)
SILICONFLOW_URL=
### 302.AI Api key (optional)
AI302_API_KEY=
### 302.AI Api url (optional)
AI302_URL=

View File

@ -4,16 +4,12 @@
<img src="https://github.com/user-attachments/assets/83bdcc07-ae5e-4954-a53a-ac151ba6ccf3" width="1000" alt="icon"/>
</a>
<h1 align="center">NextChat</h1>
English / [简体中文](./README_CN.md)
<a href="https://trendshift.io/repositories/5973" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5973" alt="ChatGPTNextWeb%2FChatGPT-Next-Web | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
✨ Light and Fast AI Assistant,with Claude, DeepSeek, GPT4 & Gemini Pro support.
[![Saas][Saas-image]][saas-url]
@ -22,12 +18,11 @@ English / [简体中文](./README_CN.md)
[![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url]
[NextChatAI](https://nextchat.club?utm_source=readme) / [iOS APP](https://apps.apple.com/us/app/nextchat-ai/id6743085599) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Enterprise Edition](#enterprise-edition)
[NextChatAI](https://nextchat.club?utm_source=readme) / [iOS APP](https://apps.apple.com/us/app/nextchat-ai/id6743085599) / [Web App Demo](https://app.nextchat.club) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Enterprise Edition](#enterprise-edition)
[saas-url]: https://nextchat.club?utm_source=readme
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
[web-url]: https://app.nextchat.dev/
[web-url]: https://app.nextchat.club/
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
@ -40,23 +35,32 @@ English / [简体中文](./README_CN.md)
</div>
## ❤️ Sponsor AI API
<a href='https://302.ai/'>
<img src="https://github.com/user-attachments/assets/a03edf82-2031-4f23-bdb8-bfc0bfd168a4" width="100%" alt="icon"/>
</a>
[302.AI](https://302.ai/) is a pay-as-you-go AI application platform that offers the most comprehensive AI APIs and online applications available.
## 🥳 Cheer for NextChat iOS Version Online!
> [👉 Click Here to Install Now](https://apps.apple.com/us/app/nextchat-ai/id6743085599)
> [❤️ Source Code Coming Soon](https://github.com/ChatGPTNextWeb/NextChat-iOS)
![Github iOS Image](https://github.com/user-attachments/assets/e0aa334f-4c13-4dc9-8310-e3b09fa4b9f3)
## 🫣 NextChat Support MCP !
> Before build, please set env ENABLE_MCP=true
<img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
## Enterprise Edition
Meeting Your Company's Privatization and Customization Deployment Requirements:
- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
@ -73,7 +77,6 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
![More](./docs/images/more.png)
## Features
- **Deploy for free with one-click** on Vercel in under 1 minute
@ -109,6 +112,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [ ] local knowledge base
## What's New
- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
@ -312,10 +316,12 @@ 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.
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.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option 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
@ -332,8 +338,9 @@ Add additional models to have vision capabilities, beyond the default pattern ma
### `WHITE_WEBDAV_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
- Each address must be a complete endpoint
> `https://xxxx/yyy`
> `https://xxxx/yyy`
- Multiple addresses are connected by ', '
### `DEFAULT_INPUT_TEMPLATE` (optional)
@ -348,7 +355,6 @@ Stability API key.
Customize Stability API url.
### `ENABLE_MCP` (optional)
Enable MCPModel Context ProtocolFeature
@ -361,13 +367,20 @@ SiliconFlow API Key.
SiliconFlow API URL.
### `AI302_API_KEY` (optional)
302.AI API Key.
### `AI302_URL` (optional)
302.AI API URL.
## Requirements
NodeJS >= 18, Docker >= 20
## Development
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
Before starting development, you must create a new `.env.local` file at project root, and place your api key into it:
@ -391,7 +404,6 @@ yarn dev
## Deployment
### Docker (Recommended)
```shell
@ -449,8 +461,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
- [How to use Vercel (No English)](./docs/vercel-cn.md)
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
## Translation
If you want to add a new translation, read this [document](./docs/translation.md).
@ -461,8 +471,6 @@ If you want to add a new translation, read this [document](./docs/translation.md
## Special Thanks
### Contributors
<a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">

View File

@ -14,9 +14,18 @@
</div>
## Sponsor AI API
<a href='https://302.ai/'>
<img src="https://github.com/user-attachments/assets/d8c0c513-1e18-4d3b-a2a9-ff3696aec0d4" width="100%" alt="icon"/>
</a>
[302.AI](https://302.ai/) 是一个按需付费的AI应用平台提供市面上最全的AI API和AI在线应用。
## 企业版
满足您公司私有化部署和定制需求
- **品牌定制**:企业量身定制 VI/UI与企业品牌形象无缝契合
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
@ -29,7 +38,6 @@
<img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
## 开始使用
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
@ -201,7 +209,6 @@ DeepSeek Api Key.
DeepSeek Api Url.
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@ -221,8 +228,9 @@ DeepSeek Api Url.
### `WHITE_WEBDAV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint
> `https://xxxx/xxx`
> `https://xxxx/xxx`
- 多个地址以`,`相连
### `CUSTOM_MODELS` (可选)
@ -233,12 +241,13 @@ DeepSeek Api Url.
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
在Azure的模式下支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+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)`的选项
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
### `DEFAULT_MODEL` (可选)
@ -275,6 +284,14 @@ SiliconFlow API Key.
SiliconFlow API URL.
### `AI302_API_KEY` (optional)
302.AI API Key.
### `AI302_URL` (optional)
302.AI API URL.
## 开发
点击下方按钮,开始二次开发:
@ -299,6 +316,7 @@ BASE_URL=https://b.nextweb.fun/api/proxy
## 部署
### 宝塔面板部署
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
### 容器部署 (推荐)

View File

@ -9,12 +9,20 @@
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
</div>
## Sponsor AI API
<a href='https://302.ai/'>
<img src="https://github.com/user-attachments/assets/6cf24233-1010-43e0-9a83-a11159866175" width="100%" alt="icon"/>
</a>
[302.AI](https://302.ai/) は、オンデマンドで支払うAIアプリケーションプラットフォームで、最も安全なAI APIとAIオンラインアプリケーションを提供します。
## 企業版
あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
- **リソース統合**企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
- **権限管理**メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
@ -25,7 +33,6 @@
企業版のお問い合わせ: **business@nextchat.dev**
## 始めに
1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
@ -40,7 +47,6 @@
</div>
## 更新を維持する
もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
@ -51,7 +57,6 @@
- ページ右上の fork ボタンを使って、本プロジェクトを fork する
- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
### 自動更新を開く
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
@ -62,15 +67,12 @@
![自動更新を有効にする](./docs/images/enable-actions-sync.jpg)
### 手動でコードを更新する
手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
## ページアクセスパスワードを設定する
> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
@ -85,7 +87,6 @@ code1,code2,code3
この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
## 環境変数
> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
@ -196,8 +197,9 @@ ByteDance API の URL。
### `WHITE_WEBDAV_ENDPOINTS` (オプション)
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
- 各アドレスは完全なエンドポイントでなければなりません。
> `https://xxxx/xxx`
> `https://xxxx/xxx`
- 複数のアドレスは `,` で接続します。
### `CUSTOM_MODELS` (オプション)
@ -208,9 +210,11 @@ ByteDance 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` (オプション)
@ -228,6 +232,13 @@ ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデ
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
### `AI302_API_KEY` (オプション)
302.AI API キー.
### `AI302_URL` (オプション)
302.AI API の URL.
## 開発
@ -241,14 +252,12 @@ ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデ
OPENAI_API_KEY=<your api key here>
```
### ローカル開発
1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
## デプロイ
### コンテナデプロイ(推奨)
@ -285,7 +294,6 @@ docker run -d -p 3000:3000 \
他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
### ローカルデプロイ
コンソールで以下のコマンドを実行します:
@ -296,7 +304,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
⚠️ 注意インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
## 謝辞
### 寄付者
@ -311,7 +318,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
## オープンソースライセンス
[MIT](https://opensource.org/license/mit/)

128
app/api/302ai.ts Normal file
View File

@ -0,0 +1,128 @@
import { getServerSideConfig } from "@/app/config/server";
import {
AI302_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 { isModelNotavailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[302.AI Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider["302.AI"]);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[302.AI] ", e);
return NextResponse.json(prettyObject(e));
}
}
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["302.AI"], "");
let baseUrl = serverConfig.ai302Url || AI302_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 (
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider["302.AI"] as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[302.AI] 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);
}
}

View File

@ -15,6 +15,7 @@ import { handle as siliconflowHandler } from "../../siliconflow";
import { handle as xaiHandler } from "../../xai";
import { handle as chatglmHandler } from "../../glm";
import { handle as proxyHandler } from "../../proxy";
import { handle as ai302Handler } from "../../302ai";
async function handle(
req: NextRequest,
@ -52,6 +53,8 @@ async function handle(
return siliconflowHandler(req, { params });
case ApiPath.OpenAI:
return openaiHandler(req, { params });
case ApiPath["302.AI"]:
return ai302Handler(req, { params });
default:
return proxyHandler(req, { params });
}

View File

@ -24,6 +24,7 @@ import { DeepSeekApi } from "./platforms/deepseek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
import { SiliconflowApi } from "./platforms/siliconflow";
import { Ai302Api } from "./platforms/ai302";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
@ -173,6 +174,9 @@ export class ClientApi {
case ModelProvider.SiliconFlow:
this.llm = new SiliconflowApi();
break;
case ModelProvider["302.AI"]:
this.llm = new Ai302Api();
break;
default:
this.llm = new ChatGPTApi();
}
@ -265,6 +269,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
const isSiliconFlow =
modelConfig.providerName === ServiceProvider.SiliconFlow;
const isAI302 = modelConfig.providerName === ServiceProvider["302.AI"];
const isEnabledAccessControl = accessStore.enabledAccessControl();
const apiKey = isGoogle
? accessStore.googleApiKey
@ -290,6 +295,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
: ""
: isAI302
? accessStore.ai302ApiKey
: accessStore.openaiApiKey;
return {
isGoogle,
@ -304,6 +311,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
isXAI,
isChatGLM,
isSiliconFlow,
isAI302,
apiKey,
isEnabledAccessControl,
};
@ -332,6 +340,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
isXAI,
isChatGLM,
isSiliconFlow,
isAI302,
apiKey,
isEnabledAccessControl,
} = getConfig();
@ -382,6 +391,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
return new ClientApi(ModelProvider.ChatGLM);
case ServiceProvider.SiliconFlow:
return new ClientApi(ModelProvider.SiliconFlow);
case ServiceProvider["302.AI"]:
return new ClientApi(ModelProvider["302.AI"]);
default:
return new ClientApi(ModelProvider.GPT);
}

View File

@ -0,0 +1,287 @@
"use client";
import {
ApiPath,
AI302_BASE_URL,
DEFAULT_MODELS,
AI302,
} from "@/app/constant";
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import {
getMessageTextContent,
getMessageTextContentWithoutThinking,
isVisionModel,
getTimeoutMSByModel,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export interface Ai302ListModelResponse {
object: string;
data: Array<{
id: string;
object: string;
root: string;
}>;
}
export class Ai302Api implements LLMApi {
private disableListModels = false;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.ai302Url;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath["302.AI"];
baseUrl = isApp ? AI302_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (
!baseUrl.startsWith("http") &&
!baseUrl.startsWith(ApiPath["302.AI"])
) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
if (v.role === "assistant") {
const content = getMessageTextContentWithoutThinking(v);
messages.push({ role: v.role, content });
} else {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
messages.push({ role: v.role, content });
}
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
providerName: options.config.providerName,
},
};
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(AI302.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// console.log(chatPayload);
// Use extended timeout for thinking models as they typically require more processing time
const requestTimeoutId = setTimeout(
() => controller.abort(),
getTimeoutMSByModel(options.config.model),
);
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return streamWithThink(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string | null;
tool_calls: ChatMessageTool[];
reasoning_content: string | null;
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
const reasoning = choices[0]?.delta?.reasoning_content;
const content = choices[0]?.delta?.content;
// Skip if both content and reasoning_content are empty or null
if (
(!reasoning || reasoning.length === 0) &&
(!content || content.length === 0)
) {
return {
isThinking: false,
content: "",
};
}
if (reasoning && reasoning.length > 0) {
return {
isThinking: true,
content: reasoning,
};
} else if (content && content.length > 0) {
return {
isThinking: false,
content: content,
};
}
return {
isThinking: false,
content: "",
};
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message, res);
}
} 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[]> {
if (this.disableListModels) {
return DEFAULT_MODELS.slice();
}
const res = await fetch(this.path(AI302.ListModelPath), {
method: "GET",
headers: {
...getHeaders(),
},
});
const resJson = (await res.json()) as Ai302ListModelResponse;
const chatModels = resJson.data;
console.log("[Models]", chatModels);
if (!chatModels) {
return [];
}
let seq = 1000; //同 Constant.ts 中的排序保持一致
return chatModels.map((m) => ({
name: m.id,
available: true,
sorted: seq++,
provider: {
id: "ai302",
providerName: "302.AI",
providerType: "ai302",
sorted: 15,
},
}));
}
}

View File

@ -224,7 +224,7 @@ export class ClaudeApi implements LLMApi {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
type: "content_block_delta" | "content_block_stop" | "message_delta" | "message_stop";
content_block?: {
type: "tool_use";
id: string;
@ -234,11 +234,20 @@ export class ClaudeApi implements LLMApi {
type: "text_delta" | "input_json_delta";
text?: string;
partial_json?: string;
stop_reason?: string;
};
index: number;
};
chunkJson = JSON.parse(text);
// Handle refusal stop reason in message_delta
if (chunkJson?.delta?.stop_reason === "refusal") {
// Return a message to display to the user
const refusalMessage = "\n\n[Assistant refused to respond. Please modify your request and try again.]";
options.onError?.(new Error("Content policy violation: " + refusalMessage));
return refusalMessage;
}
if (chunkJson?.content_block?.type == "tool_use") {
index += 1;
const id = chunkJson?.content_block.id;

View File

@ -75,6 +75,7 @@ import {
ChatGLM,
DeepSeek,
SiliconFlow,
AI302,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
@ -1458,6 +1459,46 @@ export function Settings() {
</>
);
const ai302ConfigComponent = accessStore.provider === ServiceProvider["302.AI"] && (
<>
<ListItem
title={Locale.Settings.Access.AI302.Endpoint.Title}
subTitle={
Locale.Settings.Access.AI302.Endpoint.SubTitle +
AI302.ExampleEndpoint
}
>
<input
aria-label={Locale.Settings.Access.AI302.Endpoint.Title}
type="text"
value={accessStore.ai302Url}
placeholder={AI302.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.ai302Url = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.AI302.ApiKey.Title}
subTitle={Locale.Settings.Access.AI302.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.AI302.ApiKey.Title}
value={accessStore.ai302ApiKey}
type="text"
placeholder={Locale.Settings.Access.AI302.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.ai302ApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);
return (
<ErrorBoundary>
<div className="window-header" data-tauri-drag-region>
@ -1822,6 +1863,7 @@ export function Settings() {
{XAIConfigComponent}
{chatglmConfigComponent}
{siliconflowConfigComponent}
{ai302ConfigComponent}
</>
)}
</>

View File

@ -88,6 +88,10 @@ declare global {
SILICONFLOW_URL?: string;
SILICONFLOW_API_KEY?: string;
// 302.AI only
AI302_URL?: string;
AI302_API_KEY?: string;
// custom template for preprocessing user input
DEFAULT_INPUT_TEMPLATE?: string;
@ -163,6 +167,7 @@ export const getServerSideConfig = () => {
const isXAI = !!process.env.XAI_API_KEY;
const isChatGLM = !!process.env.CHATGLM_API_KEY;
const isSiliconFlow = !!process.env.SILICONFLOW_API_KEY;
const isAI302 = !!process.env.AI302_API_KEY;
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
@ -246,6 +251,10 @@ export const getServerSideConfig = () => {
siliconFlowUrl: process.env.SILICONFLOW_URL,
siliconFlowApiKey: getApiKey(process.env.SILICONFLOW_API_KEY),
isAI302,
ai302Url: process.env.AI302_URL,
ai302ApiKey: getApiKey(process.env.AI302_API_KEY),
gtmId: process.env.GTM_ID,
gaId: process.env.GA_ID || DEFAULT_GA_ID,

View File

@ -36,6 +36,8 @@ export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
export const SILICONFLOW_BASE_URL = "https://api.siliconflow.cn";
export const AI302_BASE_URL = "https://api.302.ai";
export const CACHE_URL_PREFIX = "/api/cache";
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
@ -72,6 +74,7 @@ export enum ApiPath {
ChatGLM = "/api/chatglm",
DeepSeek = "/api/deepseek",
SiliconFlow = "/api/siliconflow",
"302.AI" = "/api/302ai",
}
export enum SlotID {
@ -130,6 +133,7 @@ export enum ServiceProvider {
ChatGLM = "ChatGLM",
DeepSeek = "DeepSeek",
SiliconFlow = "SiliconFlow",
"302.AI" = "302.AI",
}
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
@ -156,6 +160,7 @@ export enum ModelProvider {
ChatGLM = "ChatGLM",
DeepSeek = "DeepSeek",
SiliconFlow = "SiliconFlow",
"302.AI" = "302.AI",
}
export const Stability = {
@ -266,6 +271,13 @@ export const SiliconFlow = {
ListModelPath: "v1/models?&sub_type=chat",
};
export const AI302 = {
ExampleEndpoint: AI302_BASE_URL,
ChatPath: "v1/chat/completions",
EmbeddingsPath: "jina/v1/embeddings",
ListModelPath: "v1/models?llm=1",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
// export const DEFAULT_SYSTEM_TEMPLATE = `
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
@ -559,6 +571,8 @@ const anthropicModels = [
"claude-3-5-sonnet-latest",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
];
const baiduModels = [
@ -679,6 +693,31 @@ const siliconflowModels = [
"Pro/deepseek-ai/DeepSeek-V3",
];
const ai302Models = [
"deepseek-chat",
"gpt-4o",
"chatgpt-4o-latest",
"llama3.3-70b",
"deepseek-reasoner",
"gemini-2.0-flash",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"grok-3-beta",
"grok-3-mini-beta",
"gpt-4.1",
"gpt-4.1-mini",
"o3",
"o4-mini",
"qwen3-235b-a22b",
"qwen3-32b",
"gemini-2.5-pro-preview-05-06",
"llama-4-maverick",
"gemini-2.5-flash",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"gemini-2.5-pro",
];
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
@ -835,6 +874,17 @@ export const DEFAULT_MODELS = [
sorted: 14,
},
})),
...ai302Models.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "ai302",
providerName: "302.AI",
providerType: "ai302",
sorted: 15,
},
})),
] as const;
export const CHAT_PAGE_SIZE = 15;

View File

@ -416,6 +416,17 @@ const ar: PartialLocaleType = {
SubTitle: "مثال:",
},
},
AI302: {
ApiKey: {
Title: "مفتاح 302.AI API",
SubTitle: "استخدم مفتاح 302.AI API مخصص",
Placeholder: "مفتاح 302.AI API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
},
CustomModel: {
Title: "اسم النموذج المخصص",
SubTitle: "أضف خيارات نموذج مخصص، مفصولة بفواصل إنجليزية",

View File

@ -423,6 +423,17 @@ const bn: PartialLocaleType = {
SubTitle: "উদাহরণ:",
},
},
AI302: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle: "স্বনির্ধারিত 302.AI API কী ব্যবহার করুন",
Placeholder: "302.AI API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
},
CustomModel: {
Title: "স্বনির্ধারিত মডেল নাম",
SubTitle:

View File

@ -538,6 +538,17 @@ const cn = {
Title: "自定义模型名",
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
},
AI302: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义302.AI API Key",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
},
Model: "模型 (model)",

View File

@ -423,6 +423,17 @@ const cs: PartialLocaleType = {
SubTitle: "Příklad:",
},
},
AI302: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní 302.AI API Key",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
},
CustomModel: {
Title: "Vlastní názvy modelů",
SubTitle: "Přidejte možnosti vlastních modelů, oddělené čárkami",

View File

@ -517,6 +517,17 @@ const da: PartialLocaleType = {
SubTitle: "Vælg et niveau for indholdskontrol",
},
},
AI302: {
ApiKey: {
Title: "302.AI API Key",
SubTitle: "Brug en custom 302.AI API Key",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Endpoint-adresse",
SubTitle: "Eksempel: ",
},
},
},
Model: "Model",
CompressModel: {

View File

@ -434,6 +434,17 @@ const de: PartialLocaleType = {
SubTitle: "Beispiel:",
},
},
AI302: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle: "Verwenden Sie einen benutzerdefinierten 302.AI API-Schlüssel",
Placeholder: "302.AI API-Schlüssel",
},
Endpoint: {
Title: "Endpunktadresse",
SubTitle: "Beispiel:",
},
},
CustomModel: {
Title: "Benutzerdefinierter Modellname",
SubTitle:

View File

@ -543,6 +543,17 @@ const en: LocaleType = {
SubTitle: "Select a safety filtering level",
},
},
AI302: {
ApiKey: {
Title: "302.AI API Key",
SubTitle: "Use a custom 302.AI API Key",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
},
Model: "Model",

View File

@ -436,6 +436,17 @@ const es: PartialLocaleType = {
SubTitle: "Ejemplo:",
},
},
AI302: {
ApiKey: {
Title: "Clave de interfaz",
SubTitle: "Usa una clave API de 302.AI personalizada",
Placeholder: "Clave API de 302.AI",
},
Endpoint: {
Title: "Dirección del endpoint",
SubTitle: "Ejemplo:",
},
},
CustomModel: {
Title: "Nombre del modelo personalizado",
SubTitle:

View File

@ -435,6 +435,17 @@ const fr: PartialLocaleType = {
SubTitle: "Exemple :",
},
},
AI302: {
ApiKey: {
Title: "Clé d'interface",
SubTitle: "Utiliser une clé API 302.AI personnalisée",
Placeholder: "Clé API 302.AI",
},
Endpoint: {
Title: "Adresse de l'endpoint",
SubTitle: "Exemple :",
},
},
CustomModel: {
Title: "Nom du modèle personnalisé",
SubTitle:

View File

@ -424,6 +424,17 @@ const id: PartialLocaleType = {
SubTitle: "Contoh:",
},
},
AI302: {
ApiKey: {
Title: "Kunci Antarmuka",
SubTitle: "Gunakan 302.AI API Key kustom",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Alamat Antarmuka",
SubTitle: "Contoh:",
},
},
CustomModel: {
Title: "Nama Model Kustom",
SubTitle: "Tambahkan opsi model kustom, pisahkan dengan koma",

View File

@ -436,6 +436,17 @@ const it: PartialLocaleType = {
SubTitle: "Esempio:",
},
},
AI302: {
ApiKey: {
Title: "Chiave dell'interfaccia",
SubTitle: "Utilizza una chiave API 302.AI personalizzata",
Placeholder: "Chiave API 302.AI",
},
Endpoint: {
Title: "Indirizzo dell'interfaccia",
SubTitle: "Esempio:",
},
},
CustomModel: {
Title: "Nome del modello personalizzato",
SubTitle:

View File

@ -420,6 +420,17 @@ const jp: PartialLocaleType = {
SubTitle: "例:",
},
},
AI302: {
ApiKey: {
Title: "APIキー",
SubTitle: "カスタム302.AI APIキーを使用",
Placeholder: "302.AI APIキー",
},
Endpoint: {
Title: "エンドポイント",
SubTitle: "例:",
},
},
CustomModel: {
Title: "カスタムモデル名",
SubTitle: "カスタムモデルの選択肢を追加、英語のカンマで区切る",

View File

@ -421,6 +421,17 @@ const ko: PartialLocaleType = {
Title: "커스텀 모델 이름",
SubTitle: "커스텀 모델 옵션 추가, 영어 쉼표로 구분",
},
AI302: {
ApiKey: {
Title: "엔드포인트 키",
SubTitle: "커스텀 302.AI API 키 사용",
Placeholder: "302.AI API 키",
},
Endpoint: {
Title: "엔드포인트 주소",
SubTitle: "예: ",
},
},
},
Model: "모델 (model)",

View File

@ -433,6 +433,17 @@ const no: PartialLocaleType = {
Title: "Egendefinert modellnavn",
SubTitle: "Legg til egendefinerte modellalternativer, skill med komma",
},
AI302: {
ApiKey: {
Title: "API-nøkkel",
SubTitle: "Bruk egendefinert 302.AI API-nøkkel",
Placeholder: "302.AI API-nøkkel",
},
Endpoint: {
Title: "API-adresse",
SubTitle: "Eksempel:",
},
},
},
Model: "Modell",

View File

@ -359,6 +359,17 @@ const pt: PartialLocaleType = {
SubTitle: "Verifique sua versão API do console Anthropic",
},
},
AI302: {
ApiKey: {
Title: "Chave API 302.AI",
SubTitle: "Use uma chave API 302.AI personalizada",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Exemplo: ",
},
},
CustomModel: {
Title: "Modelos Personalizados",
SubTitle: "Opções de modelo personalizado, separados por vírgula",

View File

@ -426,6 +426,17 @@ const ru: PartialLocaleType = {
SubTitle: "Пример:",
},
},
AI302: {
ApiKey: {
Title: "Ключ интерфейса",
SubTitle: "Использовать пользовательский 302.AI API-ключ",
Placeholder: "302.AI API-ключ",
},
Endpoint: {
Title: "Адрес интерфейса",
SubTitle: "Пример:",
},
},
CustomModel: {
Title: "Название пользовательской модели",
SubTitle:

View File

@ -381,6 +381,17 @@ const sk: PartialLocaleType = {
SubTitle: "Vyberte špecifickú verziu časti",
},
},
AI302: {
ApiKey: {
Title: "API kľúč",
SubTitle: "Použiť vlastný API kľúč 302.AI",
Placeholder: "302.AI API kľúč",
},
Endpoint: {
Title: "Adresa koncového bodu",
SubTitle: "Príklad:",
},
},
},
Model: "Model",

View File

@ -426,6 +426,17 @@ const tr: PartialLocaleType = {
SubTitle: "Örnek:",
},
},
AI302: {
ApiKey: {
Title: "API Anahtarı",
SubTitle: "Özelleştirilmiş 302.AI API Anahtarı kullanın",
Placeholder: "302.AI API Anahtarı",
},
Endpoint: {
Title: "API Adresi",
SubTitle: "Örnek:",
},
},
CustomModel: {
Title: "Özelleştirilmiş Model Adı",
SubTitle:

View File

@ -382,6 +382,17 @@ const tw = {
SubTitle: "選擇一個特定的 API 版本",
},
},
AI302: {
ApiKey: {
Title: "API 金鑰",
SubTitle: "使用自訂 302.AI API 金鑰",
Placeholder: "302.AI API 金鑰",
},
Endpoint: {
Title: "端點位址",
SubTitle: "範例:",
},
},
CustomModel: {
Title: "自訂模型名稱",
SubTitle: "增加自訂模型可選擇項目,使用英文逗號隔開",

View File

@ -422,6 +422,17 @@ const vi: PartialLocaleType = {
SubTitle: "Ví dụ:",
},
},
AI302: {
ApiKey: {
Title: "Khóa API 302.AI",
SubTitle: "Sử dụng khóa API 302.AI tùy chỉnh",
Placeholder: "302.AI API Key",
},
Endpoint: {
Title: "Địa chỉ giao diện",
SubTitle: "Ví dụ:",
},
},
CustomModel: {
Title: "Tên mô hình tùy chỉnh",
SubTitle:

View File

@ -17,6 +17,7 @@ import {
XAI_BASE_URL,
CHATGLM_BASE_URL,
SILICONFLOW_BASE_URL,
AI302_BASE_URL,
} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
@ -59,6 +60,8 @@ const DEFAULT_SILICONFLOW_URL = isApp
? SILICONFLOW_BASE_URL
: ApiPath.SiliconFlow;
const DEFAULT_AI302_URL = isApp ? AI302_BASE_URL : ApiPath["302.AI"];
const DEFAULT_ACCESS_STATE = {
accessCode: "",
useCustomConfig: false,
@ -132,6 +135,10 @@ const DEFAULT_ACCESS_STATE = {
siliconflowUrl: DEFAULT_SILICONFLOW_URL,
siliconflowApiKey: "",
// 302.AI
ai302Url: DEFAULT_AI302_URL,
ai302ApiKey: "",
// server config
needCode: true,
hideUserApiKey: false,

View File

@ -203,7 +203,7 @@ export const useAppConfig = createPersistStore(
const models = currentState.models.slice();
state.models.forEach((pModel) => {
const idx = models.findIndex(
(v) => v.name === pModel.name && v.provider === pModel.provider,
(v) => v.name === pModel.name && v.provider.id === pModel.provider.id,
);
if (idx !== -1) models[idx] = pModel;
else models.push(pModel);

View File

@ -3077,9 +3077,9 @@ camelcase@^6.2.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646:
version "1.0.30001692"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz"
integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
version "1.0.30001724"
resolved "https://mirrors.huaweicloud.com/repository/npm/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz"
integrity sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==
ccount@^2.0.0:
version "2.0.1"
@ -4334,14 +4334,14 @@ eslint-plugin-react@^7.31.7:
eslint-plugin-unused-imports@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz#63a98c9ad5f622cd9f830f70bc77739f25ccfe0d"
resolved "https://mirrors.huaweicloud.com/repository/npm/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz#63a98c9ad5f622cd9f830f70bc77739f25ccfe0d"
integrity sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==
dependencies:
eslint-rule-composer "^0.3.0"
eslint-rule-composer@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
resolved "https://mirrors.huaweicloud.com/repository/npm/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==
eslint-scope@5.1.1:
@ -8156,7 +8156,7 @@ typed-array-length@^1.0.4:
typescript@5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
resolved "https://mirrors.huaweicloud.com/repository/npm/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
unbox-primitive@^1.0.2: