commit
dd8cc364fe
@ -1,10 +1,11 @@
|
|||||||
FROM node:16 as builder
|
FROM node:16 as builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
COPY web/package.json .
|
||||||
|
RUN npm install
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
COPY ./VERSION .
|
COPY ./VERSION .
|
||||||
RUN npm install
|
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
|
||||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
|
|
||||||
|
|
||||||
FROM golang AS builder2
|
FROM golang AS builder2
|
||||||
|
|
||||||
@ -13,9 +14,10 @@ ENV GO111MODULE=on \
|
|||||||
GOOS=linux
|
GOOS=linux
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
ADD go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /build/build ./web/build
|
COPY --from=builder /build/build ./web/build
|
||||||
RUN go mod download
|
|
||||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
31
README.en.md
31
README.en.md
@ -1,5 +1,5 @@
|
|||||||
<p align="right">
|
<p align="right">
|
||||||
<a href="./README.md">中文</a> | <strong>English</strong>
|
<a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.ja.md">日本語</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -57,15 +57,13 @@ _✨ Access all LLM through the standard OpenAI API format, easy to deploy & use
|
|||||||
> **Note**: The latest image pulled from Docker may be an `alpha` release. Specify the version manually if you require stability.
|
> **Note**: The latest image pulled from Docker may be an `alpha` release. Specify the version manually if you require stability.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
1. Supports multiple API access channels:
|
1. Support for multiple large models:
|
||||||
+ [x] Official OpenAI channel (support proxy configuration)
|
+ [x] [OpenAI ChatGPT Series Models](https://platform.openai.com/docs/guides/gpt/chat-completions-api) (Supports [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
|
||||||
+ [x] **Azure OpenAI API**
|
+ [x] [Anthropic Claude Series Models](https://anthropic.com)
|
||||||
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
+ [x] [Google PaLM2 Series Models](https://developers.generativeai.google)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [Baidu Wenxin Yiyuan Series Models](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [Alibaba Tongyi Qianwen Series Models](https://help.aliyun.com/document_detail/2400395.html)
|
||||||
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
|
+ [x] [Zhipu ChatGLM Series Models](https://bigmodel.cn)
|
||||||
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (invitation code: `OneAPI`)
|
|
||||||
+ [x] Custom channel: Various third-party proxy services not included in the list
|
|
||||||
2. Supports access to multiple channels through **load balancing**.
|
2. Supports access to multiple channels through **load balancing**.
|
||||||
3. Supports **stream mode** that enables typewriter-like effect through stream transmission.
|
3. Supports **stream mode** that enables typewriter-like effect through stream transmission.
|
||||||
4. Supports **multi-machine deployment**. [See here](#multi-machine-deployment) for more details.
|
4. Supports **multi-machine deployment**. [See here](#multi-machine-deployment) for more details.
|
||||||
@ -139,7 +137,7 @@ The initial account username is `root` and password is `123456`.
|
|||||||
cd one-api/web
|
cd one-api/web
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Build the backend
|
# Build the backend
|
||||||
cd ..
|
cd ..
|
||||||
go mod download
|
go mod download
|
||||||
@ -175,7 +173,12 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co
|
|||||||
<summary><strong>Deploy on Sealos</strong></summary>
|
<summary><strong>Deploy on Sealos</strong></summary>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md).
|
> Sealos supports high concurrency, dynamic scaling, and stable operations for millions of users.
|
||||||
|
|
||||||
|
> Click the button below to deploy with one click.👇
|
||||||
|
|
||||||
|
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@ -187,7 +190,7 @@ Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/d
|
|||||||
> Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage.
|
> Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage.
|
||||||
|
|
||||||
1. First, fork the code.
|
1. First, fork the code.
|
||||||
2. Go to [Zeabur](https://zeabur.com/), log in, and enter the console.
|
2. Go to [Zeabur](https://zeabur.com?referralCode=songquanpeng), log in, and enter the console.
|
||||||
3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port).
|
3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port).
|
||||||
4. Copy the connection parameters and run ```create database `one-api` ``` to create the database.
|
4. Copy the connection parameters and run ```create database `one-api` ``` to create the database.
|
||||||
5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository.
|
5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository.
|
||||||
@ -280,7 +283,7 @@ If the channel ID is not provided, load balancing will be used to distribute the
|
|||||||
+ Double-check that your interface address and API Key are correct.
|
+ Double-check that your interface address and API Key are correct.
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
[FastGPT](https://github.com/c121914yu/FastGPT): Build an AI knowledge base in three minutes
|
[FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
|
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
|
||||||
|
298
README.ja.md
Normal file
298
README.ja.md
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<p align="right">
|
||||||
|
<a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>日本語</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/songquanpeng/one-api"><img src="https://raw.githubusercontent.com/songquanpeng/one-api/main/web/public/logo.png" width="150" height="150" alt="one-api logo"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# One API
|
||||||
|
|
||||||
|
_✨ 標準的な OpenAI API フォーマットを通じてすべての LLM にアクセスでき、導入と利用が容易です ✨_
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://raw.githubusercontent.com/songquanpeng/one-api/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/songquanpeng/one-api?color=brightgreen" alt="license">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/songquanpeng/one-api/releases/latest">
|
||||||
|
<img src="https://img.shields.io/github/v/release/songquanpeng/one-api?color=brightgreen&include_prereleases" alt="release">
|
||||||
|
</a>
|
||||||
|
<a href="https://hub.docker.com/repository/docker/justsong/one-api">
|
||||||
|
<img src="https://img.shields.io/docker/pulls/justsong/one-api?color=brightgreen" alt="docker pull">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/songquanpeng/one-api/releases/latest">
|
||||||
|
<img src="https://img.shields.io/github/downloads/songquanpeng/one-api/total?color=brightgreen&include_prereleases" alt="release">
|
||||||
|
</a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/songquanpeng/one-api">
|
||||||
|
<img src="https://goreportcard.com/badge/github.com/songquanpeng/one-api" alt="GoReportCard">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="#deployment">デプロイチュートリアル</a>
|
||||||
|
·
|
||||||
|
<a href="#usage">使用方法</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/songquanpeng/one-api/issues">フィードバック</a>
|
||||||
|
·
|
||||||
|
<a href="#screenshots">スクリーンショット</a>
|
||||||
|
·
|
||||||
|
<a href="https://openai.justsong.cn/">ライブデモ</a>
|
||||||
|
·
|
||||||
|
<a href="#faq">FAQ</a>
|
||||||
|
·
|
||||||
|
<a href="#related-projects">関連プロジェクト</a>
|
||||||
|
·
|
||||||
|
<a href="https://iamazing.cn/page/reward">寄付</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> **警告**: この README は ChatGPT によって翻訳されています。翻訳ミスを発見した場合は遠慮なく PR を投稿してください。
|
||||||
|
|
||||||
|
> **警告**: 英語版の Docker イメージは `justsong/one-api-en` です。
|
||||||
|
|
||||||
|
> **注**: Docker からプルされた最新のイメージは、`alpha` リリースかもしれません。安定性が必要な場合は、手動でバージョンを指定してください。
|
||||||
|
|
||||||
|
## 特徴
|
||||||
|
1. 複数の大型モデルをサポート:
|
||||||
|
+ [x] [OpenAI ChatGPT シリーズモデル](https://platform.openai.com/docs/guides/gpt/chat-completions-api) ([Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) をサポート)
|
||||||
|
+ [x] [Anthropic Claude シリーズモデル](https://anthropic.com)
|
||||||
|
+ [x] [Google PaLM2 シリーズモデル](https://developers.generativeai.google)
|
||||||
|
+ [x] [Baidu Wenxin Yiyuan シリーズモデル](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||||
|
+ [x] [Alibaba Tongyi Qianwen シリーズモデル](https://help.aliyun.com/document_detail/2400395.html)
|
||||||
|
+ [x] [Zhipu ChatGLM シリーズモデル](https://bigmodel.cn)
|
||||||
|
2. **ロードバランシング**による複数チャンネルへのアクセスをサポート。
|
||||||
|
3. ストリーム伝送によるタイプライター的効果を可能にする**ストリームモード**に対応。
|
||||||
|
4. **マルチマシンデプロイ**に対応。[詳細はこちら](#multi-machine-deployment)を参照。
|
||||||
|
5. トークンの有効期限や使用回数を設定できる**トークン管理**に対応しています。
|
||||||
|
6. **バウチャー管理**に対応しており、バウチャーの一括生成やエクスポートが可能です。バウチャーは口座残高の補充に利用できます。
|
||||||
|
7. **チャンネル管理**に対応し、チャンネルの一括作成が可能。
|
||||||
|
8. グループごとに異なるレートを設定するための**ユーザーグループ**と**チャンネルグループ**をサポートしています。
|
||||||
|
9. チャンネル**モデルリスト設定**に対応。
|
||||||
|
10. **クォータ詳細チェック**をサポート。
|
||||||
|
11. **ユーザー招待報酬**をサポートします。
|
||||||
|
12. 米ドルでの残高表示が可能。
|
||||||
|
13. 新規ユーザー向けのお知らせ公開、リチャージリンク設定、初期残高設定に対応。
|
||||||
|
14. 豊富な**カスタマイズ**オプションを提供します:
|
||||||
|
1. システム名、ロゴ、フッターのカスタマイズが可能。
|
||||||
|
2. HTML と Markdown コードを使用したホームページとアバウトページのカスタマイズ、または iframe を介したスタンドアロンウェブページの埋め込みをサポートしています。
|
||||||
|
15. システム・アクセストークンによる管理 API アクセスをサポートする。
|
||||||
|
16. Cloudflare Turnstile によるユーザー認証に対応。
|
||||||
|
17. ユーザー管理と複数のユーザーログイン/登録方法をサポート:
|
||||||
|
+ 電子メールによるログイン/登録とパスワードリセット。
|
||||||
|
+ [GitHub OAuth](https://github.com/settings/applications/new)。
|
||||||
|
+ WeChat 公式アカウントの認証([WeChat Server](https://github.com/songquanpeng/wechat-server)の追加導入が必要)。
|
||||||
|
18. 他の主要なモデル API が利用可能になった場合、即座にサポートし、カプセル化する。
|
||||||
|
|
||||||
|
## デプロイメント
|
||||||
|
### Docker デプロイメント
|
||||||
|
デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-en`。
|
||||||
|
|
||||||
|
コマンドを更新する: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrr/watchtower -cR`。
|
||||||
|
|
||||||
|
`-p 3000:3000` の最初の `3000` はホストのポートで、必要に応じて変更できます。
|
||||||
|
|
||||||
|
データはホストの `/home/ubuntu/data/one-api` ディレクトリに保存される。このディレクトリが存在し、書き込み権限があることを確認する、もしくは適切なディレクトリに変更してください。
|
||||||
|
|
||||||
|
Nginxリファレンス設定:
|
||||||
|
```
|
||||||
|
server{
|
||||||
|
server_name openai.justsong.cn; # ドメイン名は適宜変更
|
||||||
|
|
||||||
|
location / {
|
||||||
|
client_max_body_size 64m;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_pass http://localhost:3000; # それに応じてポートを変更
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header Accept-Encoding gzip;
|
||||||
|
proxy_read_timeout 300s; # GPT-4 はより長いタイムアウトが必要
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
次に、Let's Encrypt certbot を使って HTTPS を設定します:
|
||||||
|
```bash
|
||||||
|
# Ubuntu に certbot をインストール:
|
||||||
|
sudo snap install --classic certbot
|
||||||
|
sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
||||||
|
# 証明書の生成と Nginx 設定の変更
|
||||||
|
sudo certbot --nginx
|
||||||
|
# プロンプトに従う
|
||||||
|
# Nginx を再起動
|
||||||
|
sudo service nginx restart
|
||||||
|
```
|
||||||
|
|
||||||
|
初期アカウントのユーザー名は `root` で、パスワードは `123456` です。
|
||||||
|
|
||||||
|
### マニュアルデプロイ
|
||||||
|
1. [GitHub Releases](https://github.com/songquanpeng/one-api/releases/latest) から実行ファイルをダウンロードする、もしくはソースからコンパイルする:
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/songquanpeng/one-api.git
|
||||||
|
|
||||||
|
# フロントエンドのビルド
|
||||||
|
cd one-api/web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# バックエンドのビルド
|
||||||
|
cd ..
|
||||||
|
go mod download
|
||||||
|
go build -ldflags "-s -w" -o one-api
|
||||||
|
```
|
||||||
|
2. 実行:
|
||||||
|
```shell
|
||||||
|
chmod u+x one-api
|
||||||
|
./one-api --port 3000 --log-dir ./logs
|
||||||
|
```
|
||||||
|
3. [http://localhost:3000/](http://localhost:3000/) にアクセスし、ログインする。初期アカウントのユーザー名は `root`、パスワードは `123456` である。
|
||||||
|
|
||||||
|
より詳細なデプロイのチュートリアルについては、[このページ](https://iamazing.cn/page/how-to-deploy-a-website) を参照してください。
|
||||||
|
|
||||||
|
### マルチマシンデプロイ
|
||||||
|
1. すべてのサーバに同じ `SESSION_SECRET` を設定する。
|
||||||
|
2. `SQL_DSN` を設定し、SQLite の代わりに MySQL を使用する。すべてのサーバは同じデータベースに接続する。
|
||||||
|
3. マスターノード以外のノードの `NODE_TYPE` を `slave` に設定する。
|
||||||
|
4. データベースから定期的に設定を同期するサーバーには `SYNC_FREQUENCY` を設定する。
|
||||||
|
5. マスター以外のノードでは、オプションで `FRONTEND_BASE_URL` を設定して、ページ要求をマスターサーバーにリダイレクトすることができます。
|
||||||
|
6. マスター以外のノードには Redis を個別にインストールし、`REDIS_CONN_STRING` を設定して、キャッシュの有効期限が切れていないときにデータベースにゼロレイテンシーでアクセスできるようにする。
|
||||||
|
7. メインサーバーでもデータベースへのアクセスが高レイテンシになる場合は、Redis を有効にし、`SYNC_FREQUENCY` を設定してデータベースから定期的に設定を同期する必要がある。
|
||||||
|
|
||||||
|
Please refer to the [environment variables](#environment-variables) section for details on using environment variables.
|
||||||
|
|
||||||
|
### コントロールパネル(例: Baota)への展開
|
||||||
|
詳しい手順は [#175](https://github.com/songquanpeng/one-api/issues/175) を参照してください。
|
||||||
|
|
||||||
|
配置後に空白のページが表示される場合は、[#97](https://github.com/songquanpeng/one-api/issues/97) を参照してください。
|
||||||
|
|
||||||
|
### サードパーティプラットフォームへのデプロイ
|
||||||
|
<details>
|
||||||
|
<summary><strong>Sealos へのデプロイ</strong></summary>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
> Sealos は、高い同時実行性、ダイナミックなスケーリング、数百万人のユーザーに対する安定した運用をサポートしています。
|
||||||
|
|
||||||
|
> 下のボタンをクリックすると、ワンクリックで展開できます。👇
|
||||||
|
|
||||||
|
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Zeabur へのデプロイ</strong></summary>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
> Zeabur のサーバーは海外にあるため、ネットワークの問題は自動的に解決されます。
|
||||||
|
|
||||||
|
1. まず、コードをフォークする。
|
||||||
|
2. [Zeabur](https://zeabur.com?referralCode=songquanpeng) にアクセスしてログインし、コンソールに入る。
|
||||||
|
3. 新しいプロジェクトを作成します。Service -> Add ServiceでMarketplace を選択し、MySQL を選択する。接続パラメータ(ユーザー名、パスワード、アドレス、ポート)をメモします。
|
||||||
|
4. 接続パラメータをコピーし、```create database `one-api` ``` を実行してデータベースを作成する。
|
||||||
|
5. その後、Service -> Add Service で Git を選択し(最初の使用には認証が必要です)、フォークしたリポジトリを選択します。
|
||||||
|
6. 自動デプロイが開始されますが、一旦キャンセルしてください。Variable タブで `PORT` に `3000` を追加し、`SQL_DSN` に `<username>:<password>@tcp(<addr>:<port>)/one-api` を追加します。変更を保存する。SQL_DSN` が設定されていないと、データが永続化されず、再デプロイ後にデータが失われるので注意すること。
|
||||||
|
7. 再デプロイを選択します。
|
||||||
|
8. Domains タブで、"my-one-api" のような適切なドメイン名の接頭辞を選択する。最終的なドメイン名は "my-one-api.zeabur.app" となります。独自のドメイン名を CNAME することもできます。
|
||||||
|
9. デプロイが完了するのを待ち、生成されたドメイン名をクリックして One API にアクセスします。
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## コンフィグ
|
||||||
|
システムは箱から出してすぐに使えます。
|
||||||
|
|
||||||
|
環境変数やコマンドラインパラメータを設定することで、システムを構成することができます。
|
||||||
|
|
||||||
|
システム起動後、`root` ユーザーとしてログインし、さらにシステムを設定します。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
`Channels` ページで API Key を追加し、`Tokens` ページでアクセストークンを追加する。
|
||||||
|
|
||||||
|
アクセストークンを使って One API にアクセスすることができる。使い方は [OpenAI API](https://platform.openai.com/docs/api-reference/introduction) と同じです。
|
||||||
|
|
||||||
|
OpenAI API が使用されている場所では、API Base に One API のデプロイアドレスを設定することを忘れないでください(例: `https://openai.justsong.cn`)。API Key は One API で生成されたトークンでなければなりません。
|
||||||
|
|
||||||
|
具体的な API Base のフォーマットは、使用しているクライアントに依存することに注意してください。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A(ユーザ)
|
||||||
|
A --->|リクエスト| B(One API)
|
||||||
|
B -->|中継リクエスト| C(OpenAI)
|
||||||
|
B -->|中継リクエスト| D(Azure)
|
||||||
|
B -->|中継リクエスト| E(その他のダウンストリームチャンネル)
|
||||||
|
```
|
||||||
|
|
||||||
|
現在のリクエストにどのチャネルを使うかを指定するには、トークンの後に チャネル ID を追加します: 例えば、`Authorization: Bearer ONE_API_KEY-CHANNEL_ID` のようにします。
|
||||||
|
チャンネル ID を指定するためには、トークンは管理者によって作成される必要があることに注意してください。
|
||||||
|
|
||||||
|
もしチャネル ID が指定されない場合、ロードバランシングによってリクエストが複数のチャネルに振り分けられます。
|
||||||
|
|
||||||
|
### 環境変数
|
||||||
|
1. `REDIS_CONN_STRING`: 設定すると、リクエストレート制限のためのストレージとして、メモリの代わりに Redis が使われる。
|
||||||
|
+ 例: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
||||||
|
2. `SESSION_SECRET`: 設定すると、固定セッションキーが使用され、システムの再起動後もログインユーザーのクッキーが有効であることが保証されます。
|
||||||
|
+ 例: `SESSION_SECRET=random_string`
|
||||||
|
3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。
|
||||||
|
+ 例: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
|
||||||
|
4. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。
|
||||||
|
+ 例: `FRONTEND_BASE_URL=https://openai.justsong.cn`
|
||||||
|
5. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。
|
||||||
|
+ 例: `SYNC_FREQUENCY=60`
|
||||||
|
6. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。
|
||||||
|
+ 例: `NODE_TYPE=slave`
|
||||||
|
7. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。
|
||||||
|
+ 例: `CHANNEL_UPDATE_FREQUENCY=1440`
|
||||||
|
8. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。
|
||||||
|
+ 例: `CHANNEL_TEST_FREQUENCY=1440`
|
||||||
|
9. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。
|
||||||
|
+ 例: `POLLING_INTERVAL=5`
|
||||||
|
|
||||||
|
### コマンドラインパラメータ
|
||||||
|
1. `--port <port_number>`: サーバがリッスンするポート番号を指定。デフォルトは `3000` です。
|
||||||
|
+ 例: `--port 3000`
|
||||||
|
2. `--log-dir <log_dir>`: ログディレクトリを指定。設定しない場合、ログは保存されません。
|
||||||
|
+ 例: `--log-dir ./logs`
|
||||||
|
3. `--version`: システムのバージョン番号を表示して終了する。
|
||||||
|
4. `--help`: コマンドの使用法ヘルプとパラメータの説明を表示。
|
||||||
|
|
||||||
|
## スクリーンショット
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
1. ノルマとは何か?どのように計算されますか?One API にはノルマ計算の問題はありますか?
|
||||||
|
+ ノルマ = グループ倍率 * モデル倍率 * (プロンプトトークンの数 + 完了トークンの数 * 完了倍率)
|
||||||
|
+ 完了倍率は、公式の定義と一致するように、GPT3.5 では 1.33、GPT4 では 2 に固定されています。
|
||||||
|
+ ストリームモードでない場合、公式 API は消費したトークンの総数を返す。ただし、プロンプトとコンプリートの消費倍率は異なるので注意してください。
|
||||||
|
2. アカウント残高は十分なのに、"insufficient quota" と表示されるのはなぜですか?
|
||||||
|
+ トークンのクォータが十分かどうかご確認ください。トークンクォータはアカウント残高とは別のものです。
|
||||||
|
+ トークンクォータは最大使用量を設定するためのもので、ユーザーが自由に設定できます。
|
||||||
|
3. チャンネルを使おうとすると "No available channels" と表示されます。どうすればいいですか?
|
||||||
|
+ ユーザーとチャンネルグループの設定を確認してください。
|
||||||
|
+ チャンネルモデルの設定も確認してください。
|
||||||
|
4. チャンネルテストがエラーを報告する: "invalid character '<' looking for beginning of value"
|
||||||
|
+ このエラーは、返された値が有効な JSON ではなく、HTML ページである場合に発生する。
|
||||||
|
+ ほとんどの場合、デプロイサイトのIPかプロキシのノードが CloudFlare によってブロックされています。
|
||||||
|
5. ChatGPT Next Web でエラーが発生しました: "Failed to fetch"
|
||||||
|
+ デプロイ時に `BASE_URL` を設定しないでください。
|
||||||
|
+ インターフェイスアドレスと API Key が正しいか再確認してください。
|
||||||
|
|
||||||
|
## 関連プロジェクト
|
||||||
|
[FastGPT](https://github.com/labring/FastGPT): LLM に基づく知識質問応答システム
|
||||||
|
|
||||||
|
## 注
|
||||||
|
本プロジェクトはオープンソースプロジェクトです。OpenAI の[利用規約](https://openai.com/policies/terms-of-use)および**適用される法令**を遵守してご利用ください。違法な目的での利用はご遠慮ください。
|
||||||
|
|
||||||
|
このプロジェクトは MIT ライセンスで公開されています。これに基づき、ページの最下部に帰属表示と本プロジェクトへのリンクを含める必要があります。
|
||||||
|
|
||||||
|
このプロジェクトを基にした派生プロジェクトについても同様です。
|
||||||
|
|
||||||
|
帰属表示を含めたくない場合は、事前に許可を得なければなりません。
|
||||||
|
|
||||||
|
MIT ライセンスによると、このプロジェクトを利用するリスクと責任は利用者が負うべきであり、このオープンソースプロジェクトの開発者は責任を負いません。
|
82
README.md
82
README.md
@ -1,5 +1,5 @@
|
|||||||
<p align="right">
|
<p align="right">
|
||||||
<strong>中文</strong> | <a href="./README.en.md">English</a>
|
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.ja.md">日本語</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@ -51,11 +51,13 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
<a href="https://iamazing.cn/page/reward">赞赏支持</a>
|
<a href="https://iamazing.cn/page/reward">赞赏支持</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> **Note**:本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
> **Note**
|
||||||
|
> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||||
|
>
|
||||||
|
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||||
|
|
||||||
> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
|
> **Warning**
|
||||||
|
> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
|
||||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种大模型:
|
1. 支持多种大模型:
|
||||||
@ -63,14 +65,16 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
||||||
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
||||||
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||||
|
+ [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html)
|
||||||
|
+ [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html)
|
||||||
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
|
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
|
||||||
|
+ [x] [360 智脑](https://ai.360.cn)
|
||||||
2. 支持配置镜像以及众多第三方代理服务:
|
2. 支持配置镜像以及众多第三方代理服务:
|
||||||
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
|
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
|
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
|
||||||
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
||||||
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
|
||||||
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
||||||
3. 支持通过**负载均衡**的方式访问多个渠道。
|
3. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
@ -93,7 +97,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
19. 支持通过系统访问令牌访问管理 API。
|
19. 支持通过系统访问令牌访问管理 API。
|
||||||
20. 支持 Cloudflare Turnstile 用户校验。
|
20. 支持 Cloudflare Turnstile 用户校验。
|
||||||
21. 支持用户管理,支持**多种用户登录注册方式**:
|
21. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
|
|
||||||
@ -101,16 +105,18 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
||||||
|
|
||||||
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
|
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
||||||
|
|
||||||
如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
|
|
||||||
|
|
||||||
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
|
|
||||||
|
|
||||||
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
|
|
||||||
|
|
||||||
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
|
||||||
|
|
||||||
|
如果启动失败,请添加 `--privileged=true`,具体参考 https://github.com/songquanpeng/one-api/issues/482 。
|
||||||
|
|
||||||
|
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
|
||||||
|
|
||||||
|
如果你的并发量较大,**务必**设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
|
||||||
|
|
||||||
|
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
|
||||||
|
|
||||||
Nginx 的参考配置:
|
Nginx 的参考配置:
|
||||||
```
|
```
|
||||||
server{
|
server{
|
||||||
@ -152,7 +158,7 @@ sudo service nginx restart
|
|||||||
cd one-api/web
|
cd one-api/web
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 构建后端
|
# 构建后端
|
||||||
cd ..
|
cd ..
|
||||||
go mod download
|
go mod download
|
||||||
@ -205,14 +211,23 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
|
|
||||||
注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。
|
注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。
|
||||||
|
|
||||||
|
#### QChatGPT - QQ机器人
|
||||||
|
项目主页:https://github.com/RockChinQ/QChatGPT
|
||||||
|
|
||||||
|
根据文档完成部署后,在`config.py`设置配置项`openai_config`的`reverse_proxy`为 One API 后端地址,设置`api_key`为 One API 生成的key,并在配置项`completion_api_params`的`model`参数设置为 One API 支持的模型名称。
|
||||||
|
|
||||||
|
可安装 [Switcher 插件](https://github.com/RockChinQ/Switcher)在运行时切换所使用的模型。
|
||||||
|
|
||||||
### 部署到第三方平台
|
### 部署到第三方平台
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>部署到 Sealos </strong></summary>
|
<summary><strong>部署到 Sealos </strong></summary>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
> Sealos 可视化部署,仅需 1 分钟。
|
> Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。
|
||||||
|
|
||||||
参考这个[教程](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md)中 1~5 步。
|
点击以下按钮一键部署(部署后访问出现 404 请等待 3~5 分钟):
|
||||||
|
|
||||||
|
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@ -224,7 +239,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
|||||||
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
|
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
|
||||||
|
|
||||||
1. 首先 fork 一份代码。
|
1. 首先 fork 一份代码。
|
||||||
2. 进入 [Zeabur](https://zeabur.com/),登录,进入控制台。
|
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
|
||||||
3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。
|
3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。
|
||||||
4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。
|
4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。
|
||||||
5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。
|
5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。
|
||||||
@ -269,15 +284,23 @@ graph LR
|
|||||||
不加的话将会使用负载均衡的方式使用多个渠道。
|
不加的话将会使用负载均衡的方式使用多个渠道。
|
||||||
|
|
||||||
### 环境变量
|
### 环境变量
|
||||||
1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为请求频率限制的存储,而非使用内存存储。
|
1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。
|
||||||
+ 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
+ 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
||||||
|
+ 如果数据库访问延迟很低,没有必要启用 Redis,启用后反而会出现数据滞后的问题。
|
||||||
2. `SESSION_SECRET`:设置之后将使用固定的会话密钥,这样系统重新启动后已登录用户的 cookie 将依旧有效。
|
2. `SESSION_SECRET`:设置之后将使用固定的会话密钥,这样系统重新启动后已登录用户的 cookie 将依旧有效。
|
||||||
+ 例子:`SESSION_SECRET=random_string`
|
+ 例子:`SESSION_SECRET=random_string`
|
||||||
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 8.0 版本。
|
3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 或 PostgreSQL。
|
||||||
+ 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
|
+ 例子:
|
||||||
|
+ MySQL:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
|
||||||
|
+ PostgreSQL:`SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi`(适配中,欢迎反馈)
|
||||||
+ 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
|
+ 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
|
||||||
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
|
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
|
||||||
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
|
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
|
||||||
|
+ 请根据你的数据库配置修改下列参数(或者保持默认值):
|
||||||
|
+ `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `100`。
|
||||||
|
+ `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `1000`。
|
||||||
|
+ 如果报错 `Error 1040: Too many connections`,请适当减小该值。
|
||||||
|
+ `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。
|
||||||
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
|
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
|
||||||
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
|
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
|
||||||
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
|
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
|
||||||
@ -290,6 +313,14 @@ graph LR
|
|||||||
+ 例子:`CHANNEL_TEST_FREQUENCY=1440`
|
+ 例子:`CHANNEL_TEST_FREQUENCY=1440`
|
||||||
9. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
|
9. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
|
||||||
+ 例子:`POLLING_INTERVAL=5`
|
+ 例子:`POLLING_INTERVAL=5`
|
||||||
|
10. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。
|
||||||
|
+ 例子:`BATCH_UPDATE_ENABLED=true`
|
||||||
|
+ 如果你遇到了数据库连接数过多的问题,可以尝试启用该选项。
|
||||||
|
11. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。
|
||||||
|
+ 例子:`BATCH_UPDATE_INTERVAL=5`
|
||||||
|
12. 请求频率限制:
|
||||||
|
+ `GLOBAL_API_RATE_LIMIT`:全局 API 速率限制(除中继请求外),单 ip 三分钟内的最大请求数,默认为 `180`。
|
||||||
|
+ `GLOBAL_WEB_RATE_LIMIT`:全局 Web 速率限制,单 ip 三分钟内的最大请求数,默认为 `60`。
|
||||||
|
|
||||||
### 命令行参数
|
### 命令行参数
|
||||||
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
||||||
@ -313,6 +344,7 @@ https://openai.justsong.cn
|
|||||||
+ 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率)
|
+ 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率)
|
||||||
+ 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。
|
+ 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。
|
||||||
+ 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。
|
+ 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。
|
||||||
|
+ 注意,One API 的默认倍率就是官方倍率,是已经调整过的。
|
||||||
2. 账户额度足够为什么提示额度不足?
|
2. 账户额度足够为什么提示额度不足?
|
||||||
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
|
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
|
||||||
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
|
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
|
||||||
@ -325,11 +357,13 @@ https://openai.justsong.cn
|
|||||||
5. ChatGPT Next Web 报错:`Failed to fetch`
|
5. ChatGPT Next Web 报错:`Failed to fetch`
|
||||||
+ 部署的时候不要设置 `BASE_URL`。
|
+ 部署的时候不要设置 `BASE_URL`。
|
||||||
+ 检查你的接口地址和 API Key 有没有填对。
|
+ 检查你的接口地址和 API Key 有没有填对。
|
||||||
|
+ 检查是否启用了 HTTPS,浏览器会拦截 HTTPS 域名下的 HTTP 请求。
|
||||||
6. 报错:`当前分组负载已饱和,请稍后再试`
|
6. 报错:`当前分组负载已饱和,请稍后再试`
|
||||||
+ 上游通道 429 了。
|
+ 上游通道 429 了。
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库
|
* [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统
|
||||||
|
* [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web): 一键拥有你自己的跨平台 ChatGPT 应用
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
@ -337,4 +371,4 @@ https://openai.justsong.cn
|
|||||||
|
|
||||||
同样适用于基于本项目的二开项目。
|
同样适用于基于本项目的二开项目。
|
||||||
|
|
||||||
依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。
|
依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。
|
||||||
|
@ -42,6 +42,21 @@ var WeChatAuthEnabled = false
|
|||||||
var TurnstileCheckEnabled = false
|
var TurnstileCheckEnabled = false
|
||||||
var RegisterEnabled = true
|
var RegisterEnabled = true
|
||||||
|
|
||||||
|
var EmailDomainRestrictionEnabled = false
|
||||||
|
var EmailDomainWhitelist = []string{
|
||||||
|
"gmail.com",
|
||||||
|
"163.com",
|
||||||
|
"126.com",
|
||||||
|
"qq.com",
|
||||||
|
"outlook.com",
|
||||||
|
"hotmail.com",
|
||||||
|
"icloud.com",
|
||||||
|
"yahoo.com",
|
||||||
|
"foxmail.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
var DebugEnabled = os.Getenv("DEBUG") == "true"
|
||||||
|
|
||||||
var LogConsumeEnabled = true
|
var LogConsumeEnabled = true
|
||||||
|
|
||||||
var SMTPServer = ""
|
var SMTPServer = ""
|
||||||
@ -77,6 +92,11 @@ var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
|||||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||||
var RequestInterval = time.Duration(requestInterval) * time.Second
|
var RequestInterval = time.Duration(requestInterval) * time.Second
|
||||||
|
|
||||||
|
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
|
||||||
|
|
||||||
|
var BatchUpdateEnabled = false
|
||||||
|
var BatchUpdateInterval = GetOrDefault("BATCH_UPDATE_INTERVAL", 5)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
@ -94,10 +114,10 @@ var (
|
|||||||
// All duration's unit is seconds
|
// All duration's unit is seconds
|
||||||
// Shouldn't larger then RateLimitKeyExpirationDuration
|
// Shouldn't larger then RateLimitKeyExpirationDuration
|
||||||
var (
|
var (
|
||||||
GlobalApiRateLimitNum = 180
|
GlobalApiRateLimitNum = GetOrDefault("GLOBAL_API_RATE_LIMIT", 180)
|
||||||
GlobalApiRateLimitDuration int64 = 3 * 60
|
GlobalApiRateLimitDuration int64 = 3 * 60
|
||||||
|
|
||||||
GlobalWebRateLimitNum = 60
|
GlobalWebRateLimitNum = GetOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
||||||
GlobalWebRateLimitDuration int64 = 3 * 60
|
GlobalWebRateLimitDuration int64 = 3 * 60
|
||||||
|
|
||||||
UploadRateLimitNum = 10
|
UploadRateLimitNum = 10
|
||||||
@ -137,41 +157,53 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelTypeUnknown = 0
|
ChannelTypeUnknown = 0
|
||||||
ChannelTypeOpenAI = 1
|
ChannelTypeOpenAI = 1
|
||||||
ChannelTypeAPI2D = 2
|
ChannelTypeAPI2D = 2
|
||||||
ChannelTypeAzure = 3
|
ChannelTypeAzure = 3
|
||||||
ChannelTypeCloseAI = 4
|
ChannelTypeCloseAI = 4
|
||||||
ChannelTypeOpenAISB = 5
|
ChannelTypeOpenAISB = 5
|
||||||
ChannelTypeOpenAIMax = 6
|
ChannelTypeOpenAIMax = 6
|
||||||
ChannelTypeOhMyGPT = 7
|
ChannelTypeOhMyGPT = 7
|
||||||
ChannelTypeCustom = 8
|
ChannelTypeCustom = 8
|
||||||
ChannelTypeAILS = 9
|
ChannelTypeAILS = 9
|
||||||
ChannelTypeAIProxy = 10
|
ChannelTypeAIProxy = 10
|
||||||
ChannelTypePaLM = 11
|
ChannelTypePaLM = 11
|
||||||
ChannelTypeAPI2GPT = 12
|
ChannelTypeAPI2GPT = 12
|
||||||
ChannelTypeAIGC2D = 13
|
ChannelTypeAIGC2D = 13
|
||||||
ChannelTypeAnthropic = 14
|
ChannelTypeAnthropic = 14
|
||||||
ChannelTypeBaidu = 15
|
ChannelTypeBaidu = 15
|
||||||
ChannelTypeZhipu = 16
|
ChannelTypeZhipu = 16
|
||||||
|
ChannelTypeAli = 17
|
||||||
|
ChannelTypeXunfei = 18
|
||||||
|
ChannelType360 = 19
|
||||||
|
ChannelTypeOpenRouter = 20
|
||||||
|
ChannelTypeAIProxyLibrary = 21
|
||||||
|
ChannelTypeFastGPT = 22
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
"", // 0
|
"", // 0
|
||||||
"https://api.openai.com", // 1
|
"https://api.openai.com", // 1
|
||||||
"https://oa.api2d.net", // 2
|
"https://oa.api2d.net", // 2
|
||||||
"", // 3
|
"", // 3
|
||||||
"https://api.closeai-proxy.xyz", // 4
|
"https://api.closeai-proxy.xyz", // 4
|
||||||
"https://api.openai-sb.com", // 5
|
"https://api.openai-sb.com", // 5
|
||||||
"https://api.openaimax.com", // 6
|
"https://api.openaimax.com", // 6
|
||||||
"https://api.ohmygpt.com", // 7
|
"https://api.ohmygpt.com", // 7
|
||||||
"", // 8
|
"", // 8
|
||||||
"https://api.caipacity.com", // 9
|
"https://api.caipacity.com", // 9
|
||||||
"https://api.aiproxy.io", // 10
|
"https://api.aiproxy.io", // 10
|
||||||
"", // 11
|
"", // 11
|
||||||
"https://api.api2gpt.com", // 12
|
"https://api.api2gpt.com", // 12
|
||||||
"https://api.aigc2d.com", // 13
|
"https://api.aigc2d.com", // 13
|
||||||
"https://api.anthropic.com", // 14
|
"https://api.anthropic.com", // 14
|
||||||
"https://aip.baidubce.com", // 15
|
"https://aip.baidubce.com", // 15
|
||||||
"https://open.bigmodel.cn", // 16
|
"https://open.bigmodel.cn", // 16
|
||||||
|
"https://dashscope.aliyuncs.com", // 17
|
||||||
|
"", // 18
|
||||||
|
"https://ai.360.cn", // 19
|
||||||
|
"https://openrouter.ai/api", // 20
|
||||||
|
"https://api.aiproxy.io", // 21
|
||||||
|
"https://fastgpt.run/api/openapi", // 22
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// ModelRatio
|
// ModelRatio
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
@ -10,42 +13,52 @@ import "encoding/json"
|
|||||||
// 1 === $0.002 / 1K tokens
|
// 1 === $0.002 / 1K tokens
|
||||||
// 1 === ¥0.014 / 1k tokens
|
// 1 === ¥0.014 / 1k tokens
|
||||||
var ModelRatio = map[string]float64{
|
var ModelRatio = map[string]float64{
|
||||||
"gpt-4": 15,
|
"gpt-4": 15,
|
||||||
"gpt-4-0314": 15,
|
"gpt-4-0314": 15,
|
||||||
"gpt-4-0613": 15,
|
"gpt-4-0613": 15,
|
||||||
"gpt-4-32k": 30,
|
"gpt-4-32k": 30,
|
||||||
"gpt-4-32k-0314": 30,
|
"gpt-4-32k-0314": 30,
|
||||||
"gpt-4-32k-0613": 30,
|
"gpt-4-32k-0613": 30,
|
||||||
"gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens
|
"gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens
|
||||||
"gpt-3.5-turbo-0301": 0.75,
|
"gpt-3.5-turbo-0301": 0.75,
|
||||||
"gpt-3.5-turbo-0613": 0.75,
|
"gpt-3.5-turbo-0613": 0.75,
|
||||||
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
|
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
|
||||||
"gpt-3.5-turbo-16k-0613": 1.5,
|
"gpt-3.5-turbo-16k-0613": 1.5,
|
||||||
"text-ada-001": 0.2,
|
"text-ada-001": 0.2,
|
||||||
"text-babbage-001": 0.25,
|
"text-babbage-001": 0.25,
|
||||||
"text-curie-001": 1,
|
"text-curie-001": 1,
|
||||||
"text-davinci-002": 10,
|
"text-davinci-002": 10,
|
||||||
"text-davinci-003": 10,
|
"text-davinci-003": 10,
|
||||||
"text-davinci-edit-001": 10,
|
"text-davinci-edit-001": 10,
|
||||||
"code-davinci-edit-001": 10,
|
"code-davinci-edit-001": 10,
|
||||||
"whisper-1": 10,
|
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
|
||||||
"davinci": 10,
|
"davinci": 10,
|
||||||
"curie": 10,
|
"curie": 10,
|
||||||
"babbage": 10,
|
"babbage": 10,
|
||||||
"ada": 10,
|
"ada": 10,
|
||||||
"text-embedding-ada-002": 0.05,
|
"text-embedding-ada-002": 0.05,
|
||||||
"text-search-ada-doc-001": 10,
|
"text-search-ada-doc-001": 10,
|
||||||
"text-moderation-stable": 0.1,
|
"text-moderation-stable": 0.1,
|
||||||
"text-moderation-latest": 0.1,
|
"text-moderation-latest": 0.1,
|
||||||
"dall-e": 8,
|
"dall-e": 8,
|
||||||
"claude-instant-1": 0.75,
|
"claude-instant-1": 0.815, // $1.63 / 1M tokens
|
||||||
"claude-2": 30,
|
"claude-2": 5.51, // $11.02 / 1M tokens
|
||||||
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
|
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
|
||||||
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
|
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
|
||||||
"PaLM-2": 1,
|
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
|
||||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
"PaLM-2": 1,
|
||||||
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||||
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
||||||
|
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
||||||
|
"qwen-v1": 0.8572, // ¥0.012 / 1k tokens
|
||||||
|
"qwen-plus-v1": 1, // ¥0.014 / 1k tokens
|
||||||
|
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
|
||||||
|
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens
|
||||||
|
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
|
||||||
|
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
|
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
|
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
|
"360GPT_S2_V9.4": 0.8572, // ¥0.012 / 1k tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModelRatio2JSONString() string {
|
func ModelRatio2JSONString() string {
|
||||||
@ -69,3 +82,19 @@ func GetModelRatio(name string) float64 {
|
|||||||
}
|
}
|
||||||
return ratio
|
return ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCompletionRatio(name string) float64 {
|
||||||
|
if strings.HasPrefix(name, "gpt-3.5") {
|
||||||
|
return 1.333333
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "gpt-4") {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "claude-instant-1") {
|
||||||
|
return 3.38
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "claude-2") {
|
||||||
|
return 2.965517
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
@ -61,3 +61,8 @@ func RedisDel(key string) error {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return RDB.Del(ctx, key).Err()
|
return RDB.Del(ctx, key).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RedisDecrease(key string, value int64) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
return RDB.DecrBy(ctx, key, value).Err()
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -177,3 +178,15 @@ func Max(a int, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrDefault(env string, defaultValue int) int {
|
||||||
|
if env == "" || os.Getenv(env) == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
num, err := strconv.Atoi(os.Getenv(env))
|
||||||
|
if err != nil {
|
||||||
|
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
@ -85,7 +85,6 @@ func GetAuthHeader(token string) http.Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -93,7 +92,7 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
|
|||||||
for k := range headers {
|
for k := range headers {
|
||||||
req.Header.Add(k, headers.Get(k))
|
req.Header.Add(k, headers.Get(k))
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,29 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIError) {
|
func testChannel(channel *model.Channel, request ChatRequest) (err error, openaiErr *OpenAIError) {
|
||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
|
case common.ChannelTypePaLM:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeAnthropic:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeBaidu:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeZhipu:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeAli:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelType360:
|
||||||
|
fallthrough
|
||||||
|
case common.ChannelTypeXunfei:
|
||||||
|
return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
|
||||||
case common.ChannelTypeAzure:
|
case common.ChannelTypeAzure:
|
||||||
request.Model = "gpt-35-turbo"
|
request.Model = "gpt-35-turbo"
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!")
|
||||||
|
}
|
||||||
|
}()
|
||||||
default:
|
default:
|
||||||
request.Model = "gpt-3.5-turbo"
|
request.Model = "gpt-3.5-turbo"
|
||||||
}
|
}
|
||||||
@ -45,8 +64,7 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
|
|||||||
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
@ -165,7 +183,7 @@ func testAllChannels(notify bool) error {
|
|||||||
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
||||||
disableChannel(channel.Id, channel.Name, err.Error())
|
disableChannel(channel.Id, channel.Name, err.Error())
|
||||||
}
|
}
|
||||||
if shouldDisableChannel(openaiErr) {
|
if shouldDisableChannel(openaiErr, -1) {
|
||||||
disableChannel(channel.Id, channel.Name, err.Error())
|
disableChannel(channel.Id, channel.Name, err.Error())
|
||||||
}
|
}
|
||||||
channel.UpdateResponseTime(milliseconds)
|
channel.UpdateResponseTime(milliseconds)
|
||||||
|
@ -85,7 +85,7 @@ func AddChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
channel.CreatedTime = common.GetTimestamp()
|
channel.CreatedTime = common.GetTimestamp()
|
||||||
keys := strings.Split(channel.Key, "\n")
|
keys := strings.Split(channel.Key, "\n")
|
||||||
channels := make([]model.Channel, 0)
|
channels := make([]model.Channel, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
|
@ -79,6 +79,14 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
|||||||
|
|
||||||
func GitHubOAuth(c *gin.Context) {
|
func GitHubOAuth(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
|
state := c.Query("state")
|
||||||
|
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "state is empty or not same",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
username := session.Get("username")
|
username := session.Get("username")
|
||||||
if username != nil {
|
if username != nil {
|
||||||
GitHubBind(c)
|
GitHubBind(c)
|
||||||
@ -205,3 +213,22 @@ func GitHubBind(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateOAuthCode(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
state := common.GetRandomString(12)
|
||||||
|
session.Set("oauth_state", state)
|
||||||
|
err := session.Save()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,10 +3,12 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetStatus(c *gin.Context) {
|
func GetStatus(c *gin.Context) {
|
||||||
@ -78,6 +80,22 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if common.EmailDomainRestrictionEnabled {
|
||||||
|
allowed := false
|
||||||
|
for _, domain := range common.EmailDomainWhitelist {
|
||||||
|
if strings.HasSuffix(email, "@"+domain) {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if model.IsEmailAlreadyTaken(email) {
|
if model.IsEmailAlreadyTaken(email) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
@ -63,6 +63,15 @@ func init() {
|
|||||||
Root: "dall-e",
|
Root: "dall-e",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "whisper-1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "whisper-1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Id: "gpt-3.5-turbo",
|
Id: "gpt-3.5-turbo",
|
||||||
Object: "model",
|
Object: "model",
|
||||||
@ -288,6 +297,15 @@ func init() {
|
|||||||
Root: "ERNIE-Bot-turbo",
|
Root: "ERNIE-Bot-turbo",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "Embedding-V1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "baidu",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "Embedding-V1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Id: "PaLM-2",
|
Id: "PaLM-2",
|
||||||
Object: "model",
|
Object: "model",
|
||||||
@ -324,6 +342,87 @@ func init() {
|
|||||||
Root: "chatglm_lite",
|
Root: "chatglm_lite",
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Id: "qwen-v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "ali",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "qwen-v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "qwen-plus-v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "ali",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "qwen-plus-v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "text-embedding-v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "ali",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "text-embedding-v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "SparkDesk",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "xunfei",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "SparkDesk",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "360GPT_S2_V9",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "360",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "360GPT_S2_V9",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "embedding-bert-512-v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "360",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "embedding-bert-512-v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "embedding_s1_v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "360",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "embedding_s1_v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "semantic_similarity_s1_v1",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "360",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "semantic_similarity_s1_v1",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "360GPT_S2_V9.4",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1677649963,
|
||||||
|
OwnedBy: "360",
|
||||||
|
Permission: permission,
|
||||||
|
Root: "360GPT_S2_V9.4",
|
||||||
|
Parent: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]OpenAIModels)
|
openAIModelsMap = make(map[string]OpenAIModels)
|
||||||
for _, model := range openAIModels {
|
for _, model := range openAIModels {
|
||||||
|
@ -2,11 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOptions(c *gin.Context) {
|
func GetOptions(c *gin.Context) {
|
||||||
@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "EmailDomainRestrictionEnabled":
|
||||||
|
if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
if option.Value == "true" && common.WeChatServerAddress == "" {
|
if option.Value == "true" && common.WeChatServerAddress == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
220
controller/relay-aiproxy.go
Normal file
220
controller/relay-aiproxy.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://docs.aiproxy.io/dev/library#使用已经定制好的知识库进行对话问答
|
||||||
|
|
||||||
|
type AIProxyLibraryRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
LibraryId string `json:"libraryId"`
|
||||||
|
Stream bool `json:"stream"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AIProxyLibraryError struct {
|
||||||
|
ErrCode int `json:"errCode"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AIProxyLibraryDocument struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AIProxyLibraryResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
Documents []AIProxyLibraryDocument `json:"documents"`
|
||||||
|
AIProxyLibraryError
|
||||||
|
}
|
||||||
|
|
||||||
|
type AIProxyLibraryStreamResponse struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Finish bool `json:"finish"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Documents []AIProxyLibraryDocument `json:"documents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestOpenAI2AIProxyLibrary(request GeneralOpenAIRequest) *AIProxyLibraryRequest {
|
||||||
|
query := ""
|
||||||
|
if len(request.Messages) != 0 {
|
||||||
|
query = request.Messages[len(request.Messages)-1].Content
|
||||||
|
}
|
||||||
|
return &AIProxyLibraryRequest{
|
||||||
|
Model: request.Model,
|
||||||
|
Stream: request.Stream,
|
||||||
|
Query: query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func aiProxyDocuments2Markdown(documents []AIProxyLibraryDocument) string {
|
||||||
|
if len(documents) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
content := "\n\n参考文档:\n"
|
||||||
|
for i, document := range documents {
|
||||||
|
content += fmt.Sprintf("%d. [%s](%s)\n", i+1, document.Title, document.URL)
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseAIProxyLibrary2OpenAI(response *AIProxyLibraryResponse) *OpenAITextResponse {
|
||||||
|
content := response.Answer + aiProxyDocuments2Markdown(response.Documents)
|
||||||
|
choice := OpenAITextResponseChoice{
|
||||||
|
Index: 0,
|
||||||
|
Message: Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: content,
|
||||||
|
},
|
||||||
|
FinishReason: "stop",
|
||||||
|
}
|
||||||
|
fullTextResponse := OpenAITextResponse{
|
||||||
|
Id: common.GetUUID(),
|
||||||
|
Object: "chat.completion",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Choices: []OpenAITextResponseChoice{choice},
|
||||||
|
}
|
||||||
|
return &fullTextResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentsAIProxyLibrary(documents []AIProxyLibraryDocument) *ChatCompletionsStreamResponse {
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = aiProxyDocuments2Markdown(documents)
|
||||||
|
choice.FinishReason = &stopFinishReason
|
||||||
|
return &ChatCompletionsStreamResponse{
|
||||||
|
Id: common.GetUUID(),
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: "",
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamResponseAIProxyLibrary2OpenAI(response *AIProxyLibraryStreamResponse) *ChatCompletionsStreamResponse {
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = response.Content
|
||||||
|
return &ChatCompletionsStreamResponse{
|
||||||
|
Id: common.GetUUID(),
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: response.Model,
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func aiProxyLibraryStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var usage Usage
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
dataChan := make(chan string)
|
||||||
|
stopChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Text()
|
||||||
|
if len(data) < 5 { // ignore blank line or wrong format
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data[:5] != "data:" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data = data[5:]
|
||||||
|
dataChan <- data
|
||||||
|
}
|
||||||
|
stopChan <- true
|
||||||
|
}()
|
||||||
|
setEventStreamHeaders(c)
|
||||||
|
var documents []AIProxyLibraryDocument
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case data := <-dataChan:
|
||||||
|
var AIProxyLibraryResponse AIProxyLibraryStreamResponse
|
||||||
|
err := json.Unmarshal([]byte(data), &AIProxyLibraryResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(AIProxyLibraryResponse.Documents) != 0 {
|
||||||
|
documents = AIProxyLibraryResponse.Documents
|
||||||
|
}
|
||||||
|
response := streamResponseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse)
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
return true
|
||||||
|
case <-stopChan:
|
||||||
|
response := documentsAIProxyLibrary(documents)
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
err := resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
return nil, &usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func aiProxyLibraryHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var AIProxyLibraryResponse AIProxyLibraryResponse
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &AIProxyLibraryResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
if AIProxyLibraryResponse.ErrCode != 0 {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: AIProxyLibraryResponse.Message,
|
||||||
|
Type: strconv.Itoa(AIProxyLibraryResponse.ErrCode),
|
||||||
|
Code: AIProxyLibraryResponse.ErrCode,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fullTextResponse := responseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
329
controller/relay-ali.go
Normal file
329
controller/relay-ali.go
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r
|
||||||
|
|
||||||
|
type AliMessage struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
Bot string `json:"bot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliInput struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
History []AliMessage `json:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliParameters struct {
|
||||||
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
|
TopK int `json:"top_k,omitempty"`
|
||||||
|
Seed uint64 `json:"seed,omitempty"`
|
||||||
|
EnableSearch bool `json:"enable_search,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliChatRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Input AliInput `json:"input"`
|
||||||
|
Parameters AliParameters `json:"parameters,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliEmbeddingRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Input struct {
|
||||||
|
Texts []string `json:"texts"`
|
||||||
|
} `json:"input"`
|
||||||
|
Parameters *struct {
|
||||||
|
TextType string `json:"text_type,omitempty"`
|
||||||
|
} `json:"parameters,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliEmbedding struct {
|
||||||
|
Embedding []float64 `json:"embedding"`
|
||||||
|
TextIndex int `json:"text_index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliEmbeddingResponse struct {
|
||||||
|
Output struct {
|
||||||
|
Embeddings []AliEmbedding `json:"embeddings"`
|
||||||
|
} `json:"output"`
|
||||||
|
Usage AliUsage `json:"usage"`
|
||||||
|
AliError
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliUsage struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliOutput struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliChatResponse struct {
|
||||||
|
Output AliOutput `json:"output"`
|
||||||
|
Usage AliUsage `json:"usage"`
|
||||||
|
AliError
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest {
|
||||||
|
messages := make([]AliMessage, 0, len(request.Messages))
|
||||||
|
prompt := ""
|
||||||
|
for i := 0; i < len(request.Messages); i++ {
|
||||||
|
message := request.Messages[i]
|
||||||
|
if message.Role == "system" {
|
||||||
|
messages = append(messages, AliMessage{
|
||||||
|
User: message.Content,
|
||||||
|
Bot: "Okay",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if i == len(request.Messages)-1 {
|
||||||
|
prompt = message.Content
|
||||||
|
break
|
||||||
|
}
|
||||||
|
messages = append(messages, AliMessage{
|
||||||
|
User: message.Content,
|
||||||
|
Bot: request.Messages[i+1].Content,
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &AliChatRequest{
|
||||||
|
Model: request.Model,
|
||||||
|
Input: AliInput{
|
||||||
|
Prompt: prompt,
|
||||||
|
History: messages,
|
||||||
|
},
|
||||||
|
//Parameters: AliParameters{ // ChatGPT's parameters are not compatible with Ali's
|
||||||
|
// TopP: request.TopP,
|
||||||
|
// TopK: 50,
|
||||||
|
// //Seed: 0,
|
||||||
|
// //EnableSearch: false,
|
||||||
|
//},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func embeddingRequestOpenAI2Ali(request GeneralOpenAIRequest) *AliEmbeddingRequest {
|
||||||
|
return &AliEmbeddingRequest{
|
||||||
|
Model: "text-embedding-v1",
|
||||||
|
Input: struct {
|
||||||
|
Texts []string `json:"texts"`
|
||||||
|
}{
|
||||||
|
Texts: request.ParseInput(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var aliResponse AliEmbeddingResponse
|
||||||
|
err := json.NewDecoder(resp.Body).Decode(&aliResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliResponse.Code != "" {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: aliResponse.Message,
|
||||||
|
Type: aliResponse.Code,
|
||||||
|
Param: aliResponse.RequestId,
|
||||||
|
Code: aliResponse.Code,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fullTextResponse := embeddingResponseAli2OpenAI(&aliResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse) *OpenAIEmbeddingResponse {
|
||||||
|
openAIEmbeddingResponse := OpenAIEmbeddingResponse{
|
||||||
|
Object: "list",
|
||||||
|
Data: make([]OpenAIEmbeddingResponseItem, 0, len(response.Output.Embeddings)),
|
||||||
|
Model: "text-embedding-v1",
|
||||||
|
Usage: Usage{TotalTokens: response.Usage.TotalTokens},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range response.Output.Embeddings {
|
||||||
|
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, OpenAIEmbeddingResponseItem{
|
||||||
|
Object: `embedding`,
|
||||||
|
Index: item.TextIndex,
|
||||||
|
Embedding: item.Embedding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &openAIEmbeddingResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse {
|
||||||
|
choice := OpenAITextResponseChoice{
|
||||||
|
Index: 0,
|
||||||
|
Message: Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: response.Output.Text,
|
||||||
|
},
|
||||||
|
FinishReason: response.Output.FinishReason,
|
||||||
|
}
|
||||||
|
fullTextResponse := OpenAITextResponse{
|
||||||
|
Id: response.RequestId,
|
||||||
|
Object: "chat.completion",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Choices: []OpenAITextResponseChoice{choice},
|
||||||
|
Usage: Usage{
|
||||||
|
PromptTokens: response.Usage.InputTokens,
|
||||||
|
CompletionTokens: response.Usage.OutputTokens,
|
||||||
|
TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &fullTextResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = aliResponse.Output.Text
|
||||||
|
if aliResponse.Output.FinishReason != "null" {
|
||||||
|
finishReason := aliResponse.Output.FinishReason
|
||||||
|
choice.FinishReason = &finishReason
|
||||||
|
}
|
||||||
|
response := ChatCompletionsStreamResponse{
|
||||||
|
Id: aliResponse.RequestId,
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: "ernie-bot",
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var usage Usage
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := strings.Index(string(data), "\n"); i >= 0 {
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
})
|
||||||
|
dataChan := make(chan string)
|
||||||
|
stopChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for scanner.Scan() {
|
||||||
|
data := scanner.Text()
|
||||||
|
if len(data) < 5 { // ignore blank line or wrong format
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data[:5] != "data:" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data = data[5:]
|
||||||
|
dataChan <- data
|
||||||
|
}
|
||||||
|
stopChan <- true
|
||||||
|
}()
|
||||||
|
setEventStreamHeaders(c)
|
||||||
|
lastResponseText := ""
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case data := <-dataChan:
|
||||||
|
var aliResponse AliChatResponse
|
||||||
|
err := json.Unmarshal([]byte(data), &aliResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if aliResponse.Usage.OutputTokens != 0 {
|
||||||
|
usage.PromptTokens = aliResponse.Usage.InputTokens
|
||||||
|
usage.CompletionTokens = aliResponse.Usage.OutputTokens
|
||||||
|
usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
|
||||||
|
}
|
||||||
|
response := streamResponseAli2OpenAI(&aliResponse)
|
||||||
|
response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText)
|
||||||
|
lastResponseText = aliResponse.Output.Text
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
return true
|
||||||
|
case <-stopChan:
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
err := resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
return nil, &usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func aliHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var aliResponse AliChatResponse
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &aliResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
if aliResponse.Code != "" {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: aliResponse.Message,
|
||||||
|
Type: aliResponse.Code,
|
||||||
|
Param: aliResponse.RequestId,
|
||||||
|
Code: aliResponse.Code,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fullTextResponse := responseAli2OpenAI(&aliResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
147
controller/relay-audio.go
Normal file
147
controller/relay-audio.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||||
|
audioModel := "whisper-1"
|
||||||
|
|
||||||
|
tokenId := c.GetInt("token_id")
|
||||||
|
channelType := c.GetInt("channel")
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
group := c.GetString("group")
|
||||||
|
|
||||||
|
preConsumedTokens := common.PreConsumedQuota
|
||||||
|
modelRatio := common.GetModelRatio(audioModel)
|
||||||
|
groupRatio := common.GetGroupRatio(group)
|
||||||
|
ratio := modelRatio * groupRatio
|
||||||
|
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
||||||
|
userQuota, err := model.CacheGetUserQuota(userId)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if userQuota > 100*preConsumedQuota {
|
||||||
|
// in this case, we do not pre-consume quota
|
||||||
|
// because the user has enough quota
|
||||||
|
preConsumedQuota = 0
|
||||||
|
}
|
||||||
|
if preConsumedQuota > 0 {
|
||||||
|
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map model name
|
||||||
|
modelMapping := c.GetString("model_mapping")
|
||||||
|
if modelMapping != "" {
|
||||||
|
modelMap := make(map[string]string)
|
||||||
|
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if modelMap[audioModel] != "" {
|
||||||
|
audioModel = modelMap[audioModel]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := common.ChannelBaseURLs[channelType]
|
||||||
|
requestURL := c.Request.URL.String()
|
||||||
|
|
||||||
|
if c.GetString("base_url") != "" {
|
||||||
|
baseURL = c.GetString("base_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||||
|
requestBody := c.Request.Body
|
||||||
|
|
||||||
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
||||||
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = req.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = c.Request.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
var audioResponse AudioResponse
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
go func() {
|
||||||
|
quota := countTokenText(audioResponse.Text, audioModel)
|
||||||
|
quotaDelta := quota - preConsumedQuota
|
||||||
|
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error consuming token remain quota: " + err.Error())
|
||||||
|
}
|
||||||
|
err = model.CacheUpdateUserQuota(userId)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error update user quota cache: " + err.Error())
|
||||||
|
}
|
||||||
|
if quota != 0 {
|
||||||
|
tokenName := c.GetString("token_name")
|
||||||
|
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||||
|
model.RecordConsumeLog(userId, 0, 0, audioModel, tokenName, quota, logContent)
|
||||||
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &audioResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
||||||
|
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
c.Writer.Header().Set(k, v[0])
|
||||||
|
}
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
|
||||||
|
_, err = io.Copy(c.Writer, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -3,22 +3,22 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
|
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
|
||||||
|
|
||||||
type BaiduTokenResponse struct {
|
type BaiduTokenResponse struct {
|
||||||
RefreshToken string `json:"refresh_token"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
AccessToken string `json:"access_token"`
|
||||||
SessionKey string `json:"session_key"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
SessionSecret string `json:"session_secret"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaiduMessage struct {
|
type BaiduMessage struct {
|
||||||
@ -54,13 +54,53 @@ type BaiduChatStreamResponse struct {
|
|||||||
IsEnd bool `json:"is_end"`
|
IsEnd bool `json:"is_end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaiduEmbeddingRequest struct {
|
||||||
|
Input []string `json:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaiduEmbeddingData struct {
|
||||||
|
Object string `json:"object"`
|
||||||
|
Embedding []float64 `json:"embedding"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaiduEmbeddingResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
Data []BaiduEmbeddingData `json:"data"`
|
||||||
|
Usage Usage `json:"usage"`
|
||||||
|
BaiduError
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaiduAccessToken struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
|
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||||
|
ExpiresAt time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var baiduTokenStore sync.Map
|
||||||
|
|
||||||
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
||||||
messages := make([]BaiduMessage, 0, len(request.Messages))
|
messages := make([]BaiduMessage, 0, len(request.Messages))
|
||||||
for _, message := range request.Messages {
|
for _, message := range request.Messages {
|
||||||
messages = append(messages, BaiduMessage{
|
if message.Role == "system" {
|
||||||
Role: message.Role,
|
messages = append(messages, BaiduMessage{
|
||||||
Content: message.Content,
|
Role: "user",
|
||||||
})
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
messages = append(messages, BaiduMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Okay",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messages = append(messages, BaiduMessage{
|
||||||
|
Role: message.Role,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &BaiduChatRequest{
|
return &BaiduChatRequest{
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
@ -90,7 +130,9 @@ func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse {
|
|||||||
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
|
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = baiduResponse.Result
|
choice.Delta.Content = baiduResponse.Result
|
||||||
choice.FinishReason = "stop"
|
if baiduResponse.IsEnd {
|
||||||
|
choice.FinishReason = &stopFinishReason
|
||||||
|
}
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Id: baiduResponse.Id,
|
Id: baiduResponse.Id,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
@ -101,6 +143,29 @@ func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCom
|
|||||||
return &response
|
return &response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingRequest {
|
||||||
|
return &BaiduEmbeddingRequest{
|
||||||
|
Input: request.ParseInput(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func embeddingResponseBaidu2OpenAI(response *BaiduEmbeddingResponse) *OpenAIEmbeddingResponse {
|
||||||
|
openAIEmbeddingResponse := OpenAIEmbeddingResponse{
|
||||||
|
Object: "list",
|
||||||
|
Data: make([]OpenAIEmbeddingResponseItem, 0, len(response.Data)),
|
||||||
|
Model: "baidu-embedding",
|
||||||
|
Usage: response.Usage,
|
||||||
|
}
|
||||||
|
for _, item := range response.Data {
|
||||||
|
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, OpenAIEmbeddingResponseItem{
|
||||||
|
Object: item.Object,
|
||||||
|
Index: item.Index,
|
||||||
|
Embedding: item.Embedding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &openAIEmbeddingResponse
|
||||||
|
}
|
||||||
|
|
||||||
func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
var usage Usage
|
var usage Usage
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
@ -129,11 +194,7 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
}
|
}
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
}()
|
}()
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
setEventStreamHeaders(c)
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
@ -143,9 +204,11 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
usage.PromptTokens += baiduResponse.Usage.PromptTokens
|
if baiduResponse.Usage.TotalTokens != 0 {
|
||||||
usage.CompletionTokens += baiduResponse.Usage.CompletionTokens
|
usage.TotalTokens = baiduResponse.Usage.TotalTokens
|
||||||
usage.TotalTokens += baiduResponse.Usage.TotalTokens
|
usage.PromptTokens = baiduResponse.Usage.PromptTokens
|
||||||
|
usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens
|
||||||
|
}
|
||||||
response := streamResponseBaidu2OpenAI(&baiduResponse)
|
response := streamResponseBaidu2OpenAI(&baiduResponse)
|
||||||
jsonResponse, err := json.Marshal(response)
|
jsonResponse, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -201,3 +264,96 @@ func baiduHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCo
|
|||||||
_, err = c.Writer.Write(jsonResponse)
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
return nil, &fullTextResponse.Usage
|
return nil, &fullTextResponse.Usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var baiduResponse BaiduEmbeddingResponse
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &baiduResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
if baiduResponse.ErrorMsg != "" {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: baiduResponse.ErrorMsg,
|
||||||
|
Type: "baidu_error",
|
||||||
|
Param: "",
|
||||||
|
Code: baiduResponse.ErrorCode,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBaiduAccessToken(apiKey string) (string, error) {
|
||||||
|
if val, ok := baiduTokenStore.Load(apiKey); ok {
|
||||||
|
var accessToken BaiduAccessToken
|
||||||
|
if accessToken, ok = val.(BaiduAccessToken); ok {
|
||||||
|
// soon this will expire
|
||||||
|
if time.Now().Add(time.Hour).After(accessToken.ExpiresAt) {
|
||||||
|
go func() {
|
||||||
|
_, _ = getBaiduAccessTokenHelper(apiKey)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return accessToken.AccessToken, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessToken, err := getBaiduAccessTokenHelper(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if accessToken == nil {
|
||||||
|
return "", errors.New("getBaiduAccessToken return a nil token")
|
||||||
|
}
|
||||||
|
return (*accessToken).AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBaiduAccessTokenHelper(apiKey string) (*BaiduAccessToken, error) {
|
||||||
|
parts := strings.Split(apiKey, "|")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errors.New("invalid baidu apikey")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
|
||||||
|
parts[0], parts[1]), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
res, err := impatientHTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
var accessToken BaiduAccessToken
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if accessToken.Error != "" {
|
||||||
|
return nil, errors.New(accessToken.Error + ": " + accessToken.ErrorDescription)
|
||||||
|
}
|
||||||
|
if accessToken.AccessToken == "" {
|
||||||
|
return nil, errors.New("getBaiduAccessTokenHelper get empty access token")
|
||||||
|
}
|
||||||
|
accessToken.ExpiresAt = time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second)
|
||||||
|
baiduTokenStore.Store(apiKey, accessToken)
|
||||||
|
return &accessToken, nil
|
||||||
|
}
|
||||||
|
@ -69,11 +69,11 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
|
|||||||
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
|
||||||
} else if message.Role == "assistant" {
|
} else if message.Role == "assistant" {
|
||||||
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
||||||
} else {
|
} else if message.Role == "system" {
|
||||||
// ignore other roles
|
prompt += fmt.Sprintf("\n\nSystem: %s", message.Content)
|
||||||
}
|
}
|
||||||
prompt += "\n\nAssistant:"
|
|
||||||
}
|
}
|
||||||
|
prompt += "\n\nAssistant:"
|
||||||
claudeRequest.Prompt = prompt
|
claudeRequest.Prompt = prompt
|
||||||
return &claudeRequest
|
return &claudeRequest
|
||||||
}
|
}
|
||||||
@ -81,7 +81,10 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
|
|||||||
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
|
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = claudeResponse.Completion
|
choice.Delta.Content = claudeResponse.Completion
|
||||||
choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
||||||
|
if finishReason != "null" {
|
||||||
|
choice.FinishReason = &finishReason
|
||||||
|
}
|
||||||
var response ChatCompletionsStreamResponse
|
var response ChatCompletionsStreamResponse
|
||||||
response.Object = "chat.completion.chunk"
|
response.Object = "chat.completion.chunk"
|
||||||
response.Model = claudeResponse.Model
|
response.Model = claudeResponse.Model
|
||||||
@ -138,11 +141,7 @@ func claudeStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithS
|
|||||||
}
|
}
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
}()
|
}()
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
setEventStreamHeaders(c)
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
|
@ -109,8 +109,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
|
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,9 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
if len(data) < 6 { // ignore blank line or wrong format
|
if len(data) < 6 { // ignore blank line or wrong format
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if data[:6] != "data: " && data[:6] != "[DONE]" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dataChan <- data
|
dataChan <- data
|
||||||
data = data[6:]
|
data = data[6:]
|
||||||
if !strings.HasPrefix(data, "[DONE]") {
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
@ -43,7 +46,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
err := json.Unmarshal([]byte(data), &streamResponse)
|
err := json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
return
|
continue // just ignore the error
|
||||||
}
|
}
|
||||||
for _, choice := range streamResponse.Choices {
|
for _, choice := range streamResponse.Choices {
|
||||||
responseText += choice.Delta.Content
|
responseText += choice.Delta.Content
|
||||||
@ -53,7 +56,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
err := json.Unmarshal([]byte(data), &streamResponse)
|
err := json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
for _, choice := range streamResponse.Choices {
|
for _, choice := range streamResponse.Choices {
|
||||||
responseText += choice.Text
|
responseText += choice.Text
|
||||||
@ -63,11 +66,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
}
|
}
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
}()
|
}()
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
setEventStreamHeaders(c)
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
@ -89,7 +88,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
return nil, responseText
|
return nil, responseText
|
||||||
}
|
}
|
||||||
|
|
||||||
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) {
|
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
var textResponse TextResponse
|
var textResponse TextResponse
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
@ -115,7 +114,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
|
|||||||
}
|
}
|
||||||
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
||||||
// And then we will have to send an error response, but in this case, the header has already been set.
|
// And then we will have to send an error response, but in this case, the header has already been set.
|
||||||
// So the client will be confused by the response.
|
// So the httpClient will be confused by the response.
|
||||||
// For example, Postman will report error, and we cannot check the response at all.
|
// For example, Postman will report error, and we cannot check the response at all.
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
c.Writer.Header().Set(k, v[0])
|
c.Writer.Header().Set(k, v[0])
|
||||||
@ -129,5 +128,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if textResponse.Usage.TotalTokens == 0 {
|
||||||
|
completionTokens := 0
|
||||||
|
for _, choice := range textResponse.Choices {
|
||||||
|
completionTokens += countTokenText(choice.Message.Content, model)
|
||||||
|
}
|
||||||
|
textResponse.Usage = Usage{
|
||||||
|
PromptTokens: promptTokens,
|
||||||
|
CompletionTokens: completionTokens,
|
||||||
|
TotalTokens: promptTokens + completionTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, &textResponse.Usage
|
return nil, &textResponse.Usage
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsS
|
|||||||
if len(palmResponse.Candidates) > 0 {
|
if len(palmResponse.Candidates) > 0 {
|
||||||
choice.Delta.Content = palmResponse.Candidates[0].Content
|
choice.Delta.Content = palmResponse.Candidates[0].Content
|
||||||
}
|
}
|
||||||
choice.FinishReason = "stop"
|
choice.FinishReason = &stopFinishReason
|
||||||
var response ChatCompletionsStreamResponse
|
var response ChatCompletionsStreamResponse
|
||||||
response.Object = "chat.completion.chunk"
|
response.Object = "chat.completion.chunk"
|
||||||
response.Model = "palm2"
|
response.Model = "palm2"
|
||||||
@ -143,11 +143,7 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSta
|
|||||||
dataChan <- string(jsonResponse)
|
dataChan <- string(jsonResponse)
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
}()
|
}()
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
setEventStreamHeaders(c)
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
|
@ -5,13 +5,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -20,8 +20,21 @@ const (
|
|||||||
APITypePaLM
|
APITypePaLM
|
||||||
APITypeBaidu
|
APITypeBaidu
|
||||||
APITypeZhipu
|
APITypeZhipu
|
||||||
|
APITypeAli
|
||||||
|
APITypeXunfei
|
||||||
|
APITypeAIProxyLibrary
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
var impatientHTTPClient *http.Client
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
httpClient = &http.Client{}
|
||||||
|
impatientHTTPClient = &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||||
channelType := c.GetInt("channel")
|
channelType := c.GetInt("channel")
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
@ -67,7 +80,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
// map model name
|
// map model name
|
||||||
modelMapping := c.GetString("model_mapping")
|
modelMapping := c.GetString("model_mapping")
|
||||||
isModelMapped := false
|
isModelMapped := false
|
||||||
if modelMapping != "" {
|
if modelMapping != "" && modelMapping != "{}" {
|
||||||
modelMap := make(map[string]string)
|
modelMap := make(map[string]string)
|
||||||
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -79,14 +92,21 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiType := APITypeOpenAI
|
apiType := APITypeOpenAI
|
||||||
if strings.HasPrefix(textRequest.Model, "claude") {
|
switch channelType {
|
||||||
|
case common.ChannelTypeAnthropic:
|
||||||
apiType = APITypeClaude
|
apiType = APITypeClaude
|
||||||
} else if strings.HasPrefix(textRequest.Model, "ERNIE") {
|
case common.ChannelTypeBaidu:
|
||||||
apiType = APITypeBaidu
|
apiType = APITypeBaidu
|
||||||
} else if strings.HasPrefix(textRequest.Model, "PaLM") {
|
case common.ChannelTypePaLM:
|
||||||
apiType = APITypePaLM
|
apiType = APITypePaLM
|
||||||
} else if strings.HasPrefix(textRequest.Model, "chatglm_") {
|
case common.ChannelTypeZhipu:
|
||||||
apiType = APITypeZhipu
|
apiType = APITypeZhipu
|
||||||
|
case common.ChannelTypeAli:
|
||||||
|
apiType = APITypeAli
|
||||||
|
case common.ChannelTypeXunfei:
|
||||||
|
apiType = APITypeXunfei
|
||||||
|
case common.ChannelTypeAIProxyLibrary:
|
||||||
|
apiType = APITypeAIProxyLibrary
|
||||||
}
|
}
|
||||||
baseURL := common.ChannelBaseURLs[channelType]
|
baseURL := common.ChannelBaseURLs[channelType]
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
@ -128,12 +148,21 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
|
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant"
|
||||||
case "BLOOMZ-7B":
|
case "BLOOMZ-7B":
|
||||||
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1"
|
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1"
|
||||||
|
case "Embedding-V1":
|
||||||
|
fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1"
|
||||||
}
|
}
|
||||||
apiKey := c.Request.Header.Get("Authorization")
|
apiKey := c.Request.Header.Get("Authorization")
|
||||||
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
||||||
fullRequestURL += "?access_token=" + apiKey // TODO: access token expire in 30 days
|
var err error
|
||||||
|
if apiKey, err = getBaiduAccessToken(apiKey); err != nil {
|
||||||
|
return errorWrapper(err, "invalid_baidu_config", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
fullRequestURL += "?access_token=" + apiKey
|
||||||
case APITypePaLM:
|
case APITypePaLM:
|
||||||
fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
|
fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage"
|
||||||
|
if baseURL != "" {
|
||||||
|
fullRequestURL = fmt.Sprintf("%s/v1beta2/models/chat-bison-001:generateMessage", baseURL)
|
||||||
|
}
|
||||||
apiKey := c.Request.Header.Get("Authorization")
|
apiKey := c.Request.Header.Get("Authorization")
|
||||||
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
||||||
fullRequestURL += "?key=" + apiKey
|
fullRequestURL += "?key=" + apiKey
|
||||||
@ -143,6 +172,13 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
method = "sse-invoke"
|
method = "sse-invoke"
|
||||||
}
|
}
|
||||||
fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method)
|
fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method)
|
||||||
|
case APITypeAli:
|
||||||
|
fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
|
||||||
|
if relayMode == RelayModeEmbeddings {
|
||||||
|
fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||||
|
}
|
||||||
|
case APITypeAIProxyLibrary:
|
||||||
|
fullRequestURL = fmt.Sprintf("%s/api/library/ask", baseURL)
|
||||||
}
|
}
|
||||||
var promptTokens int
|
var promptTokens int
|
||||||
var completionTokens int
|
var completionTokens int
|
||||||
@ -166,7 +202,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if userQuota > 10*preConsumedQuota {
|
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if userQuota > 100*preConsumedQuota {
|
||||||
// in this case, we do not pre-consume quota
|
// in this case, we do not pre-consume quota
|
||||||
// because the user has enough quota
|
// because the user has enough quota
|
||||||
preConsumedQuota = 0
|
preConsumedQuota = 0
|
||||||
@ -196,12 +236,20 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
requestBody = bytes.NewBuffer(jsonStr)
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
case APITypeBaidu:
|
case APITypeBaidu:
|
||||||
baiduRequest := requestOpenAI2Baidu(textRequest)
|
var jsonData []byte
|
||||||
jsonStr, err := json.Marshal(baiduRequest)
|
var err error
|
||||||
|
switch relayMode {
|
||||||
|
case RelayModeEmbeddings:
|
||||||
|
baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(textRequest)
|
||||||
|
jsonData, err = json.Marshal(baiduEmbeddingRequest)
|
||||||
|
default:
|
||||||
|
baiduRequest := requestOpenAI2Baidu(textRequest)
|
||||||
|
jsonData, err = json.Marshal(baiduRequest)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
requestBody = bytes.NewBuffer(jsonStr)
|
requestBody = bytes.NewBuffer(jsonData)
|
||||||
case APITypePaLM:
|
case APITypePaLM:
|
||||||
palmRequest := requestOpenAI2PaLM(textRequest)
|
palmRequest := requestOpenAI2PaLM(textRequest)
|
||||||
jsonStr, err := json.Marshal(palmRequest)
|
jsonStr, err := json.Marshal(palmRequest)
|
||||||
@ -216,100 +264,143 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
requestBody = bytes.NewBuffer(jsonStr)
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
}
|
case APITypeAli:
|
||||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
var jsonStr []byte
|
||||||
if err != nil {
|
var err error
|
||||||
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
switch relayMode {
|
||||||
}
|
case RelayModeEmbeddings:
|
||||||
apiKey := c.Request.Header.Get("Authorization")
|
aliEmbeddingRequest := embeddingRequestOpenAI2Ali(textRequest)
|
||||||
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
jsonStr, err = json.Marshal(aliEmbeddingRequest)
|
||||||
switch apiType {
|
default:
|
||||||
case APITypeOpenAI:
|
aliRequest := requestOpenAI2Ali(textRequest)
|
||||||
if channelType == common.ChannelTypeAzure {
|
jsonStr, err = json.Marshal(aliRequest)
|
||||||
req.Header.Set("api-key", apiKey)
|
|
||||||
} else {
|
|
||||||
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
|
||||||
}
|
}
|
||||||
case APITypeClaude:
|
if err != nil {
|
||||||
req.Header.Set("x-api-key", apiKey)
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
anthropicVersion := c.Request.Header.Get("anthropic-version")
|
|
||||||
if anthropicVersion == "" {
|
|
||||||
anthropicVersion = "2023-06-01"
|
|
||||||
}
|
}
|
||||||
req.Header.Set("anthropic-version", anthropicVersion)
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
case APITypeZhipu:
|
case APITypeAIProxyLibrary:
|
||||||
token := getZhipuToken(apiKey)
|
aiProxyLibraryRequest := requestOpenAI2AIProxyLibrary(textRequest)
|
||||||
req.Header.Set("Authorization", token)
|
aiProxyLibraryRequest.LibraryId = c.GetString("library_id")
|
||||||
|
jsonStr, err := json.Marshal(aiProxyLibraryRequest)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
requestBody = bytes.NewBuffer(jsonStr)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
|
||||||
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
err = req.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
err = c.Request.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
var textResponse TextResponse
|
|
||||||
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
|
||||||
var streamResponseText string
|
|
||||||
|
|
||||||
defer func() {
|
var req *http.Request
|
||||||
if consumeQuota {
|
var resp *http.Response
|
||||||
quota := 0
|
isStream := textRequest.Stream
|
||||||
completionRatio := 1.0
|
|
||||||
if strings.HasPrefix(textRequest.Model, "gpt-3.5") {
|
if apiType != APITypeXunfei { // cause xunfei use websocket
|
||||||
completionRatio = 1.333333
|
req, err = http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||||
}
|
if err != nil {
|
||||||
if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
||||||
completionRatio = 2
|
}
|
||||||
}
|
apiKey := c.Request.Header.Get("Authorization")
|
||||||
if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu {
|
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
||||||
completionTokens = countTokenText(streamResponseText, textRequest.Model)
|
switch apiType {
|
||||||
|
case APITypeOpenAI:
|
||||||
|
if channelType == common.ChannelTypeAzure {
|
||||||
|
req.Header.Set("api-key", apiKey)
|
||||||
} else {
|
} else {
|
||||||
promptTokens = textResponse.Usage.PromptTokens
|
req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
|
||||||
completionTokens = textResponse.Usage.CompletionTokens
|
if channelType == common.ChannelTypeOpenRouter {
|
||||||
if apiType == APITypeZhipu {
|
req.Header.Set("HTTP-Referer", "https://github.com/songquanpeng/one-api")
|
||||||
// zhipu's API does not return prompt tokens & completion tokens
|
req.Header.Set("X-Title", "One API")
|
||||||
promptTokens = textResponse.Usage.TotalTokens
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
case APITypeClaude:
|
||||||
quota = int(float64(quota) * ratio)
|
req.Header.Set("x-api-key", apiKey)
|
||||||
if ratio != 0 && quota <= 0 {
|
anthropicVersion := c.Request.Header.Get("anthropic-version")
|
||||||
quota = 1
|
if anthropicVersion == "" {
|
||||||
|
anthropicVersion = "2023-06-01"
|
||||||
}
|
}
|
||||||
totalTokens := promptTokens + completionTokens
|
req.Header.Set("anthropic-version", anthropicVersion)
|
||||||
if totalTokens == 0 {
|
case APITypeZhipu:
|
||||||
// in this case, must be some error happened
|
token := getZhipuToken(apiKey)
|
||||||
// we cannot just return, because we may have to return the pre-consumed quota
|
req.Header.Set("Authorization", token)
|
||||||
quota = 0
|
case APITypeAli:
|
||||||
}
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
quotaDelta := quota - preConsumedQuota
|
if textRequest.Stream {
|
||||||
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
req.Header.Set("X-DashScope-SSE", "enable")
|
||||||
if err != nil {
|
|
||||||
common.SysError("error consuming token remain quota: " + err.Error())
|
|
||||||
}
|
|
||||||
err = model.CacheUpdateUserQuota(userId)
|
|
||||||
if err != nil {
|
|
||||||
common.SysError("error update user quota cache: " + err.Error())
|
|
||||||
}
|
|
||||||
if quota != 0 {
|
|
||||||
tokenName := c.GetString("token_name")
|
|
||||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
|
||||||
model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent)
|
|
||||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
|
||||||
channelId := c.GetInt("channel_id")
|
|
||||||
model.UpdateChannelUsedQuota(channelId, quota)
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
|
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
||||||
|
resp, err = httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = req.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = c.Request.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if preConsumedQuota != 0 {
|
||||||
|
go func() {
|
||||||
|
// return pre-consumed quota
|
||||||
|
err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error return pre-consumed quota: " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return relayErrorHandler(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var textResponse TextResponse
|
||||||
|
tokenName := c.GetString("token_name")
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// c.Writer.Flush()
|
||||||
|
go func() {
|
||||||
|
if consumeQuota {
|
||||||
|
quota := 0
|
||||||
|
completionRatio := common.GetCompletionRatio(textRequest.Model)
|
||||||
|
promptTokens = textResponse.Usage.PromptTokens
|
||||||
|
completionTokens = textResponse.Usage.CompletionTokens
|
||||||
|
|
||||||
|
quota = promptTokens + int(float64(completionTokens)*completionRatio)
|
||||||
|
quota = int(float64(quota) * ratio)
|
||||||
|
if ratio != 0 && quota <= 0 {
|
||||||
|
quota = 1
|
||||||
|
}
|
||||||
|
totalTokens := promptTokens + completionTokens
|
||||||
|
if totalTokens == 0 {
|
||||||
|
// in this case, must be some error happened
|
||||||
|
// we cannot just return, because we may have to return the pre-consumed quota
|
||||||
|
quota = 0
|
||||||
|
}
|
||||||
|
quotaDelta := quota - preConsumedQuota
|
||||||
|
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error consuming token remain quota: " + err.Error())
|
||||||
|
}
|
||||||
|
err = model.CacheUpdateUserQuota(userId)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error update user quota cache: " + err.Error())
|
||||||
|
}
|
||||||
|
if quota != 0 {
|
||||||
|
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||||
|
model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent)
|
||||||
|
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||||
|
model.UpdateChannelUsedQuota(channelId, quota)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}()
|
}()
|
||||||
switch apiType {
|
switch apiType {
|
||||||
case APITypeOpenAI:
|
case APITypeOpenAI:
|
||||||
@ -318,10 +409,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
streamResponseText = responseText
|
textResponse.Usage.PromptTokens = promptTokens
|
||||||
|
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := openaiHandler(c, resp, consumeQuota)
|
err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -336,7 +428,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
streamResponseText = responseText
|
textResponse.Usage.PromptTokens = promptTokens
|
||||||
|
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model)
|
err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model)
|
||||||
@ -359,7 +452,14 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := baiduHandler(c, resp)
|
var err *OpenAIErrorWithStatusCode
|
||||||
|
var usage *Usage
|
||||||
|
switch relayMode {
|
||||||
|
case RelayModeEmbeddings:
|
||||||
|
err, usage = baiduEmbeddingHandler(c, resp)
|
||||||
|
default:
|
||||||
|
err, usage = baiduHandler(c, resp)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -374,7 +474,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
streamResponseText = responseText
|
textResponse.Usage.PromptTokens = promptTokens
|
||||||
|
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := palmHandler(c, resp, promptTokens, textRequest.Model)
|
err, usage := palmHandler(c, resp, promptTokens, textRequest.Model)
|
||||||
@ -395,6 +496,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if usage != nil {
|
if usage != nil {
|
||||||
textResponse.Usage = *usage
|
textResponse.Usage = *usage
|
||||||
}
|
}
|
||||||
|
// zhipu's API does not return prompt tokens & completion tokens
|
||||||
|
textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := zhipuHandler(c, resp)
|
err, usage := zhipuHandler(c, resp)
|
||||||
@ -404,6 +507,74 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if usage != nil {
|
if usage != nil {
|
||||||
textResponse.Usage = *usage
|
textResponse.Usage = *usage
|
||||||
}
|
}
|
||||||
|
// zhipu's API does not return prompt tokens & completion tokens
|
||||||
|
textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case APITypeAli:
|
||||||
|
if isStream {
|
||||||
|
err, usage := aliStreamHandler(c, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
var err *OpenAIErrorWithStatusCode
|
||||||
|
var usage *Usage
|
||||||
|
switch relayMode {
|
||||||
|
case RelayModeEmbeddings:
|
||||||
|
err, usage = aliEmbeddingHandler(c, resp)
|
||||||
|
default:
|
||||||
|
err, usage = aliHandler(c, resp)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case APITypeXunfei:
|
||||||
|
if isStream {
|
||||||
|
auth := c.Request.Header.Get("Authorization")
|
||||||
|
auth = strings.TrimPrefix(auth, "Bearer ")
|
||||||
|
splits := strings.Split(auth, "|")
|
||||||
|
if len(splits) != 3 {
|
||||||
|
return errorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
err, usage := xunfeiStreamHandler(c, textRequest, splits[0], splits[1], splits[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errorWrapper(errors.New("xunfei api does not support non-stream mode"), "invalid_api_type", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
case APITypeAIProxyLibrary:
|
||||||
|
if isStream {
|
||||||
|
err, usage := aiProxyLibraryStreamHandler(c, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
err, usage := aiProxyLibraryHandler(c, resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usage != nil {
|
||||||
|
textResponse.Usage = *usage
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -1,13 +1,38 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkoukk/tiktoken-go"
|
"github.com/pkoukk/tiktoken-go"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var stopFinishReason = "stop"
|
||||||
|
|
||||||
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
||||||
|
|
||||||
|
func InitTokenEncoders() {
|
||||||
|
common.SysLog("initializing token encoders")
|
||||||
|
fallbackTokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(fmt.Sprintf("failed to get fallback token encoder: %s", err.Error()))
|
||||||
|
}
|
||||||
|
for model, _ := range common.ModelRatio {
|
||||||
|
tokenEncoder, err := tiktoken.EncodingForModel(model)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("using fallback encoder for model %s", model))
|
||||||
|
tokenEncoderMap[model] = fallbackTokenEncoder
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokenEncoderMap[model] = tokenEncoder
|
||||||
|
}
|
||||||
|
common.SysLog("token encoders initialized")
|
||||||
|
}
|
||||||
|
|
||||||
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
func getTokenEncoder(model string) *tiktoken.Tiktoken {
|
||||||
if tokenEncoder, ok := tokenEncoderMap[model]; ok {
|
if tokenEncoder, ok := tokenEncoderMap[model]; ok {
|
||||||
return tokenEncoder
|
return tokenEncoder
|
||||||
@ -92,15 +117,53 @@ func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldDisableChannel(err *OpenAIError) bool {
|
func shouldDisableChannel(err *OpenAIError, statusCode int) bool {
|
||||||
if !common.AutomaticDisableChannelEnabled {
|
if !common.AutomaticDisableChannelEnabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if statusCode == http.StatusUnauthorized {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
|
if err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setEventStreamHeaders(c *gin.Context) {
|
||||||
|
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||||
|
c.Writer.Header().Set("Connection", "keep-alive")
|
||||||
|
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
||||||
|
}
|
||||||
|
|
||||||
|
func relayErrorHandler(resp *http.Response) (openAIErrorWithStatusCode *OpenAIErrorWithStatusCode) {
|
||||||
|
openAIErrorWithStatusCode = &OpenAIErrorWithStatusCode{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: fmt.Sprintf("bad response status code %d", resp.StatusCode),
|
||||||
|
Type: "one_api_error",
|
||||||
|
Code: "bad_response_status_code",
|
||||||
|
Param: strconv.Itoa(resp.StatusCode),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var textResponse TextResponse
|
||||||
|
err = json.Unmarshal(responseBody, &textResponse)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openAIErrorWithStatusCode.OpenAIError = textResponse.Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
290
controller/relay-xunfei.go
Normal file
290
controller/relay-xunfei.go
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://console.xfyun.cn/services/cbm
|
||||||
|
// https://www.xfyun.cn/doc/spark/Web.html
|
||||||
|
|
||||||
|
type XunfeiMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XunfeiChatRequest struct {
|
||||||
|
Header struct {
|
||||||
|
AppId string `json:"app_id"`
|
||||||
|
} `json:"header"`
|
||||||
|
Parameter struct {
|
||||||
|
Chat struct {
|
||||||
|
Domain string `json:"domain,omitempty"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
TopK int `json:"top_k,omitempty"`
|
||||||
|
MaxTokens int `json:"max_tokens,omitempty"`
|
||||||
|
Auditing bool `json:"auditing,omitempty"`
|
||||||
|
} `json:"chat"`
|
||||||
|
} `json:"parameter"`
|
||||||
|
Payload struct {
|
||||||
|
Message struct {
|
||||||
|
Text []XunfeiMessage `json:"text"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XunfeiChatResponseTextItem struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XunfeiChatResponse struct {
|
||||||
|
Header struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Sid string `json:"sid"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"header"`
|
||||||
|
Payload struct {
|
||||||
|
Choices struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Seq int `json:"seq"`
|
||||||
|
Text []XunfeiChatResponseTextItem `json:"text"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Usage struct {
|
||||||
|
//Text struct {
|
||||||
|
// QuestionTokens string `json:"question_tokens"`
|
||||||
|
// PromptTokens string `json:"prompt_tokens"`
|
||||||
|
// CompletionTokens string `json:"completion_tokens"`
|
||||||
|
// TotalTokens string `json:"total_tokens"`
|
||||||
|
//} `json:"text"`
|
||||||
|
Text Usage `json:"text"`
|
||||||
|
} `json:"usage"`
|
||||||
|
} `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, domain string) *XunfeiChatRequest {
|
||||||
|
messages := make([]XunfeiMessage, 0, len(request.Messages))
|
||||||
|
for _, message := range request.Messages {
|
||||||
|
if message.Role == "system" {
|
||||||
|
messages = append(messages, XunfeiMessage{
|
||||||
|
Role: "user",
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
messages = append(messages, XunfeiMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Okay",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messages = append(messages, XunfeiMessage{
|
||||||
|
Role: message.Role,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xunfeiRequest := XunfeiChatRequest{}
|
||||||
|
xunfeiRequest.Header.AppId = xunfeiAppId
|
||||||
|
xunfeiRequest.Parameter.Chat.Domain = domain
|
||||||
|
xunfeiRequest.Parameter.Chat.Temperature = request.Temperature
|
||||||
|
xunfeiRequest.Parameter.Chat.TopK = request.N
|
||||||
|
xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens
|
||||||
|
xunfeiRequest.Payload.Message.Text = messages
|
||||||
|
return &xunfeiRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
|
||||||
|
if len(response.Payload.Choices.Text) == 0 {
|
||||||
|
response.Payload.Choices.Text = []XunfeiChatResponseTextItem{
|
||||||
|
{
|
||||||
|
Content: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
choice := OpenAITextResponseChoice{
|
||||||
|
Index: 0,
|
||||||
|
Message: Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: response.Payload.Choices.Text[0].Content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fullTextResponse := OpenAITextResponse{
|
||||||
|
Object: "chat.completion",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Choices: []OpenAITextResponseChoice{choice},
|
||||||
|
Usage: response.Payload.Usage.Text,
|
||||||
|
}
|
||||||
|
return &fullTextResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatCompletionsStreamResponse {
|
||||||
|
if len(xunfeiResponse.Payload.Choices.Text) == 0 {
|
||||||
|
xunfeiResponse.Payload.Choices.Text = []XunfeiChatResponseTextItem{
|
||||||
|
{
|
||||||
|
Content: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
|
choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
|
||||||
|
if xunfeiResponse.Payload.Choices.Status == 2 {
|
||||||
|
choice.FinishReason = &stopFinishReason
|
||||||
|
}
|
||||||
|
response := ChatCompletionsStreamResponse{
|
||||||
|
Object: "chat.completion.chunk",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: "SparkDesk",
|
||||||
|
Choices: []ChatCompletionsStreamResponseChoice{choice},
|
||||||
|
}
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string {
|
||||||
|
HmacWithShaToBase64 := func(algorithm, data, key string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(key))
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
encodeData := mac.Sum(nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(encodeData)
|
||||||
|
}
|
||||||
|
ul, err := url.Parse(hostUrl)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
date := time.Now().UTC().Format(time.RFC1123)
|
||||||
|
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
||||||
|
sign := strings.Join(signString, "\n")
|
||||||
|
sha := HmacWithShaToBase64("hmac-sha256", sign, apiSecret)
|
||||||
|
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
||||||
|
"hmac-sha256", "host date request-line", sha)
|
||||||
|
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("host", ul.Host)
|
||||||
|
v.Add("date", date)
|
||||||
|
v.Add("authorization", authorization)
|
||||||
|
callUrl := hostUrl + "?" + v.Encode()
|
||||||
|
return callUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var usage Usage
|
||||||
|
query := c.Request.URL.Query()
|
||||||
|
apiVersion := query.Get("api-version")
|
||||||
|
if apiVersion == "" {
|
||||||
|
apiVersion = c.GetString("api_version")
|
||||||
|
}
|
||||||
|
if apiVersion == "" {
|
||||||
|
apiVersion = "v1.1"
|
||||||
|
common.SysLog("api_version not found, use default: " + apiVersion)
|
||||||
|
}
|
||||||
|
domain := "general"
|
||||||
|
if apiVersion == "v2.1" {
|
||||||
|
domain = "generalv2"
|
||||||
|
}
|
||||||
|
hostUrl := fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion)
|
||||||
|
d := websocket.Dialer{
|
||||||
|
HandshakeTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil)
|
||||||
|
if err != nil || resp.StatusCode != 101 {
|
||||||
|
return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
data := requestOpenAI2Xunfei(textRequest, appId, domain)
|
||||||
|
err = conn.WriteJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
dataChan := make(chan XunfeiChatResponse)
|
||||||
|
stopChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error reading stream response: " + err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var response XunfeiChatResponse
|
||||||
|
err = json.Unmarshal(msg, &response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dataChan <- response
|
||||||
|
if response.Payload.Choices.Status == 2 {
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error closing websocket connection: " + err.Error())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopChan <- true
|
||||||
|
}()
|
||||||
|
setEventStreamHeaders(c)
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case xunfeiResponse := <-dataChan:
|
||||||
|
usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens
|
||||||
|
usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens
|
||||||
|
usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens
|
||||||
|
response := streamResponseXunfei2OpenAI(&xunfeiResponse)
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error marshalling stream response: " + err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||||
|
return true
|
||||||
|
case <-stopChan:
|
||||||
|
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil, &usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func xunfeiHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
|
||||||
|
var xunfeiResponse XunfeiChatResponse
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(responseBody, &xunfeiResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
if xunfeiResponse.Header.Code != 0 {
|
||||||
|
return &OpenAIErrorWithStatusCode{
|
||||||
|
OpenAIError: OpenAIError{
|
||||||
|
Message: xunfeiResponse.Header.Message,
|
||||||
|
Type: "xunfei_error",
|
||||||
|
Param: "",
|
||||||
|
Code: xunfeiResponse.Header.Code,
|
||||||
|
},
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fullTextResponse := responseXunfei2OpenAI(&xunfeiResponse)
|
||||||
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, err = c.Writer.Write(jsonResponse)
|
||||||
|
return nil, &fullTextResponse.Usage
|
||||||
|
}
|
@ -111,10 +111,21 @@ func getZhipuToken(apikey string) string {
|
|||||||
func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
||||||
messages := make([]ZhipuMessage, 0, len(request.Messages))
|
messages := make([]ZhipuMessage, 0, len(request.Messages))
|
||||||
for _, message := range request.Messages {
|
for _, message := range request.Messages {
|
||||||
messages = append(messages, ZhipuMessage{
|
if message.Role == "system" {
|
||||||
Role: message.Role,
|
messages = append(messages, ZhipuMessage{
|
||||||
Content: message.Content,
|
Role: "system",
|
||||||
})
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
messages = append(messages, ZhipuMessage{
|
||||||
|
Role: "user",
|
||||||
|
Content: "Okay",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messages = append(messages, ZhipuMessage{
|
||||||
|
Role: message.Role,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &ZhipuRequest{
|
return &ZhipuRequest{
|
||||||
Prompt: messages,
|
Prompt: messages,
|
||||||
@ -152,7 +163,6 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
|
|||||||
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = zhipuResponse
|
choice.Delta.Content = zhipuResponse
|
||||||
choice.FinishReason = ""
|
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: common.GetTimestamp(),
|
Created: common.GetTimestamp(),
|
||||||
@ -165,7 +175,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResp
|
|||||||
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
|
||||||
var choice ChatCompletionsStreamResponseChoice
|
var choice ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = ""
|
choice.Delta.Content = ""
|
||||||
choice.FinishReason = "stop"
|
choice.FinishReason = &stopFinishReason
|
||||||
response := ChatCompletionsStreamResponse{
|
response := ChatCompletionsStreamResponse{
|
||||||
Id: zhipuResponse.RequestId,
|
Id: zhipuResponse.RequestId,
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
@ -183,8 +193,8 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
if atEOF && len(data) == 0 {
|
if atEOF && len(data) == 0 {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
if i := strings.Index(string(data), "\n"); i >= 0 {
|
if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 {
|
||||||
return i + 1, data[0:i], nil
|
return i + 2, data[0:i], nil
|
||||||
}
|
}
|
||||||
if atEOF {
|
if atEOF {
|
||||||
return len(data), data, nil
|
return len(data), data, nil
|
||||||
@ -197,23 +207,24 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
go func() {
|
go func() {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
data := scanner.Text()
|
data := scanner.Text()
|
||||||
data = strings.Trim(data, "\"")
|
lines := strings.Split(data, "\n")
|
||||||
if len(data) < 5 { // ignore blank line or wrong format
|
for i, line := range lines {
|
||||||
continue
|
if len(line) < 5 {
|
||||||
}
|
continue
|
||||||
if data[:5] == "data:" {
|
}
|
||||||
dataChan <- data[5:]
|
if line[:5] == "data:" {
|
||||||
} else if data[:5] == "meta:" {
|
dataChan <- line[5:]
|
||||||
metaChan <- data[5:]
|
if i != len(lines)-1 {
|
||||||
|
dataChan <- "\n"
|
||||||
|
}
|
||||||
|
} else if line[:5] == "meta:" {
|
||||||
|
metaChan <- line[5:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
}()
|
}()
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
setEventStreamHeaders(c)
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
|
@ -24,6 +24,7 @@ const (
|
|||||||
RelayModeModerations
|
RelayModeModerations
|
||||||
RelayModeImagesGenerations
|
RelayModeImagesGenerations
|
||||||
RelayModeEdits
|
RelayModeEdits
|
||||||
|
RelayModeAudio
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://platform.openai.com/docs/api-reference/chat
|
// https://platform.openai.com/docs/api-reference/chat
|
||||||
@ -40,6 +41,26 @@ type GeneralOpenAIRequest struct {
|
|||||||
Input any `json:"input,omitempty"`
|
Input any `json:"input,omitempty"`
|
||||||
Instruction string `json:"instruction,omitempty"`
|
Instruction string `json:"instruction,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
|
Functions any `json:"functions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r GeneralOpenAIRequest) ParseInput() []string {
|
||||||
|
if r.Input == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var input []string
|
||||||
|
switch r.Input.(type) {
|
||||||
|
case string:
|
||||||
|
input = []string{r.Input.(string)}
|
||||||
|
case []any:
|
||||||
|
input = make([]string, 0, len(r.Input.([]any)))
|
||||||
|
for _, item := range r.Input.([]any) {
|
||||||
|
if str, ok := item.(string); ok {
|
||||||
|
input = append(input, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatRequest struct {
|
type ChatRequest struct {
|
||||||
@ -62,6 +83,10 @@ type ImageRequest struct {
|
|||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AudioResponse struct {
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
@ -81,8 +106,9 @@ type OpenAIErrorWithStatusCode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TextResponse struct {
|
type TextResponse struct {
|
||||||
Usage `json:"usage"`
|
Choices []OpenAITextResponseChoice `json:"choices"`
|
||||||
Error OpenAIError `json:"error"`
|
Usage `json:"usage"`
|
||||||
|
Error OpenAIError `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAITextResponseChoice struct {
|
type OpenAITextResponseChoice struct {
|
||||||
@ -99,6 +125,19 @@ type OpenAITextResponse struct {
|
|||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIEmbeddingResponseItem struct {
|
||||||
|
Object string `json:"object"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Embedding []float64 `json:"embedding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIEmbeddingResponse struct {
|
||||||
|
Object string `json:"object"`
|
||||||
|
Data []OpenAIEmbeddingResponseItem `json:"data"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
type ImageResponse struct {
|
type ImageResponse struct {
|
||||||
Created int `json:"created"`
|
Created int `json:"created"`
|
||||||
Data []struct {
|
Data []struct {
|
||||||
@ -110,7 +149,7 @@ type ChatCompletionsStreamResponseChoice struct {
|
|||||||
Delta struct {
|
Delta struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
} `json:"delta"`
|
} `json:"delta"`
|
||||||
FinishReason string `json:"finish_reason,omitempty"`
|
FinishReason *string `json:"finish_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatCompletionsStreamResponse struct {
|
type ChatCompletionsStreamResponse struct {
|
||||||
@ -144,11 +183,15 @@ func Relay(c *gin.Context) {
|
|||||||
relayMode = RelayModeImagesGenerations
|
relayMode = RelayModeImagesGenerations
|
||||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
|
||||||
relayMode = RelayModeEdits
|
relayMode = RelayModeEdits
|
||||||
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||||
|
relayMode = RelayModeAudio
|
||||||
}
|
}
|
||||||
var err *OpenAIErrorWithStatusCode
|
var err *OpenAIErrorWithStatusCode
|
||||||
switch relayMode {
|
switch relayMode {
|
||||||
case RelayModeImagesGenerations:
|
case RelayModeImagesGenerations:
|
||||||
err = relayImageHelper(c, relayMode)
|
err = relayImageHelper(c, relayMode)
|
||||||
|
case RelayModeAudio:
|
||||||
|
err = relayAudioHelper(c, relayMode)
|
||||||
default:
|
default:
|
||||||
err = relayTextHelper(c, relayMode)
|
err = relayTextHelper(c, relayMode)
|
||||||
}
|
}
|
||||||
@ -162,7 +205,7 @@ func Relay(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
||||||
} else {
|
} else {
|
||||||
if err.StatusCode == http.StatusTooManyRequests {
|
if err.StatusCode == http.StatusTooManyRequests {
|
||||||
err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试"
|
||||||
}
|
}
|
||||||
c.JSON(err.StatusCode, gin.H{
|
c.JSON(err.StatusCode, gin.H{
|
||||||
"error": err.OpenAIError,
|
"error": err.OpenAIError,
|
||||||
@ -171,7 +214,7 @@ func Relay(c *gin.Context) {
|
|||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
||||||
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
||||||
if shouldDisableChannel(&err.OpenAIError) {
|
if shouldDisableChannel(&err.OpenAIError, err.StatusCode) {
|
||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
channelName := c.GetString("channel_name")
|
channelName := c.GetString("channel_name")
|
||||||
disableChannel(channelId, channelName, err.Message)
|
disableChannel(channelId, channelName, err.Message)
|
||||||
@ -193,10 +236,10 @@ func RelayNotImplemented(c *gin.Context) {
|
|||||||
|
|
||||||
func RelayNotFound(c *gin.Context) {
|
func RelayNotFound(c *gin.Context) {
|
||||||
err := OpenAIError{
|
err := OpenAIError{
|
||||||
Message: fmt.Sprintf("API not found: %s:%s", c.Request.Method, c.Request.URL.Path),
|
Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path),
|
||||||
Type: "one_api_error",
|
Type: "invalid_request_error",
|
||||||
Param: "",
|
Param: "",
|
||||||
Code: "api_not_found",
|
Code: "",
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
|
@ -109,10 +109,10 @@ func AddToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(token.Name) == 0 || len(token.Name) > 20 {
|
if len(token.Name) > 30 {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "令牌名称长度必须在1-20之间",
|
"message": "令牌名称过长",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -171,6 +171,13 @@ func UpdateToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(token.Name) > 30 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "令牌名称过长",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
cleanToken, err := model.GetTokenByIds(token.Id, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
11
go.mod
11
go.mod
@ -13,11 +13,12 @@ require (
|
|||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1
|
github.com/gorilla/websocket v1.5.0
|
||||||
|
github.com/pkoukk/tiktoken-go v0.1.5
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.9.0
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.4.3
|
||||||
gorm.io/driver/sqlite v1.4.3
|
gorm.io/driver/sqlite v1.4.3
|
||||||
gorm.io/gorm v1.24.0
|
gorm.io/gorm v1.25.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -25,7 +26,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
@ -35,6 +36,9 @@ require (
|
|||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.3.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@ -53,4 +57,5 @@ require (
|
|||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/postgres v1.5.2 // indirect
|
||||||
)
|
)
|
||||||
|
20
go.sum
20
go.sum
@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
@ -67,6 +67,14 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
|||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
||||||
|
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
@ -110,8 +118,8 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
|
|||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
|
github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
|
github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
@ -185,9 +193,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
||||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||||
|
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
||||||
|
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
||||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
||||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
|
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
||||||
|
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
31
i18n/en.json
31
i18n/en.json
@ -3,6 +3,11 @@
|
|||||||
"%d 点额度": "%d point quota",
|
"%d 点额度": "%d point quota",
|
||||||
"尚未实现": "Not yet implemented",
|
"尚未实现": "Not yet implemented",
|
||||||
"余额不足": "Insufficient balance",
|
"余额不足": "Insufficient balance",
|
||||||
|
"危险操作": "Hazardous operations",
|
||||||
|
"输入你的账户名": "Enter your account name",
|
||||||
|
"确认删除": "Confirm Delete",
|
||||||
|
"确认绑定": "Confirm Binding",
|
||||||
|
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.",
|
||||||
"\"通道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
|
"\"通道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
|
||||||
"通道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s",
|
"通道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s",
|
||||||
"测试已在运行中": "Test is already running",
|
"测试已在运行中": "Test is already running",
|
||||||
@ -34,8 +39,8 @@
|
|||||||
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
||||||
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
||||||
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
||||||
"当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.",
|
"当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
|
||||||
"令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20",
|
"令牌名称过长": "Token name is too long",
|
||||||
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
||||||
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
||||||
"管理员关闭了密码登录": "The administrator has turned off password login",
|
"管理员关闭了密码登录": "The administrator has turned off password login",
|
||||||
@ -224,7 +229,7 @@
|
|||||||
"已是最新版本": "Is the latest version",
|
"已是最新版本": "Is the latest version",
|
||||||
"检查更新": "Check for updates",
|
"检查更新": "Check for updates",
|
||||||
"公告": "Announcement",
|
"公告": "Announcement",
|
||||||
"在此输入新的公告内容": "Enter new announcement content here",
|
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
|
||||||
"保存公告": "Save Announcement",
|
"保存公告": "Save Announcement",
|
||||||
"个性化设置": "Personalization Settings",
|
"个性化设置": "Personalization Settings",
|
||||||
"系统名称": "System Name",
|
"系统名称": "System Name",
|
||||||
@ -427,7 +432,7 @@
|
|||||||
"一分钟后过期": "Expires after one minute",
|
"一分钟后过期": "Expires after one minute",
|
||||||
"创建新的令牌": "Create New Token",
|
"创建新的令牌": "Create New Token",
|
||||||
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
|
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
|
||||||
"设置为无限额度": "Set to unlimited quota",
|
"设为无限额度": "Set to unlimited quota",
|
||||||
"更新令牌信息": "Update Token Information",
|
"更新令牌信息": "Update Token Information",
|
||||||
"请输入充值码!": "Please enter the recharge code!",
|
"请输入充值码!": "Please enter the recharge code!",
|
||||||
"请输入名称": "Please enter a name",
|
"请输入名称": "Please enter a name",
|
||||||
@ -493,6 +498,7 @@
|
|||||||
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
|
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
|
||||||
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
||||||
"取消无限额度": "Cancel unlimited quota",
|
"取消无限额度": "Cancel unlimited quota",
|
||||||
|
"取消": "Cancel",
|
||||||
"请输入新的剩余额度": "Please enter the new remaining quota",
|
"请输入新的剩余额度": "Please enter the new remaining quota",
|
||||||
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
|
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
|
||||||
"请输入用户名": "Please enter username",
|
"请输入用户名": "Please enter username",
|
||||||
@ -503,5 +509,20 @@
|
|||||||
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
||||||
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
||||||
"Homepage URL 填": "Fill in the Homepage URL",
|
"Homepage URL 填": "Fill in the Homepage URL",
|
||||||
"Authorization callback URL 填": "Fill in the Authorization callback URL"
|
"Authorization callback URL 填": "Fill in the Authorization callback URL",
|
||||||
|
"请为通道命名": "Please name the channel",
|
||||||
|
"此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
|
||||||
|
"模型重定向": "Model redirection",
|
||||||
|
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
||||||
|
"注意,": "Note that, ",
|
||||||
|
",图片演示。": "related image demo.",
|
||||||
|
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
|
||||||
|
"代理": "Proxy",
|
||||||
|
"此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
|
||||||
|
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
|
||||||
|
"按照如下格式输入:": "Enter in the following format:",
|
||||||
|
"模型版本": "Model version",
|
||||||
|
"请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
|
||||||
|
"点击查看": "click to view",
|
||||||
|
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!"
|
||||||
}
|
}
|
||||||
|
10
main.go
10
main.go
@ -26,6 +26,9 @@ func main() {
|
|||||||
if os.Getenv("GIN_MODE") != "debug" {
|
if os.Getenv("GIN_MODE") != "debug" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
if common.DebugEnabled {
|
||||||
|
common.SysLog("running in debug mode")
|
||||||
|
}
|
||||||
// Initialize SQL Database
|
// Initialize SQL Database
|
||||||
err := model.InitDB()
|
err := model.InitDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -54,6 +57,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
||||||
}
|
}
|
||||||
|
common.SyncFrequency = frequency
|
||||||
go model.SyncOptions(frequency)
|
go model.SyncOptions(frequency)
|
||||||
if common.RedisEnabled {
|
if common.RedisEnabled {
|
||||||
go model.SyncChannelCache(frequency)
|
go model.SyncChannelCache(frequency)
|
||||||
@ -73,6 +77,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
go controller.AutomaticallyTestChannels(frequency)
|
go controller.AutomaticallyTestChannels(frequency)
|
||||||
}
|
}
|
||||||
|
if os.Getenv("BATCH_UPDATE_ENABLED") == "true" {
|
||||||
|
common.BatchUpdateEnabled = true
|
||||||
|
common.SysLog("batch update enabled with interval " + strconv.Itoa(common.BatchUpdateInterval) + "s")
|
||||||
|
model.InitBatchUpdater()
|
||||||
|
}
|
||||||
|
controller.InitTokenEncoders()
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.Default()
|
server := gin.Default()
|
||||||
|
@ -100,7 +100,18 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !model.CacheIsUserEnabled(token.UserId) {
|
userEnabled, err := model.IsUserEnabled(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
"type": "one_api_error",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !userEnabled {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "用户已被封禁",
|
"message": "用户已被封禁",
|
||||||
|
@ -58,7 +58,10 @@ func Distribute() func(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
// Select a channel for the user
|
// Select a channel for the user
|
||||||
var modelRequest ModelRequest
|
var modelRequest ModelRequest
|
||||||
err := common.UnmarshalBodyReusable(c, &modelRequest)
|
var err error
|
||||||
|
if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||||
|
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
@ -84,6 +87,11 @@ func Distribute() func(c *gin.Context) {
|
|||||||
modelRequest.Model = "dall-e"
|
modelRequest.Model = "dall-e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||||
|
if modelRequest.Model == "" {
|
||||||
|
modelRequest.Model = "whisper-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
||||||
@ -107,8 +115,13 @@ func Distribute() func(c *gin.Context) {
|
|||||||
c.Set("model_mapping", channel.ModelMapping)
|
c.Set("model_mapping", channel.ModelMapping)
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
c.Set("base_url", channel.BaseURL)
|
c.Set("base_url", channel.BaseURL)
|
||||||
if channel.Type == common.ChannelTypeAzure {
|
switch channel.Type {
|
||||||
|
case common.ChannelTypeAzure:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
|
case common.ChannelTypeXunfei:
|
||||||
|
c.Set("api_version", channel.Other)
|
||||||
|
case common.ChannelTypeAIProxyLibrary:
|
||||||
|
c.Set("library_id", channel.Other)
|
||||||
}
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
TokenCacheSeconds = 60 * 60
|
TokenCacheSeconds = common.SyncFrequency
|
||||||
UserId2GroupCacheSeconds = 60 * 60
|
UserId2GroupCacheSeconds = common.SyncFrequency
|
||||||
UserId2QuotaCacheSeconds = 10 * 60
|
UserId2QuotaCacheSeconds = common.SyncFrequency
|
||||||
UserId2StatusCacheSeconds = 60 * 60
|
UserId2StatusCacheSeconds = common.SyncFrequency
|
||||||
)
|
)
|
||||||
|
|
||||||
func CacheGetTokenByKey(key string) (*Token, error) {
|
func CacheGetTokenByKey(key string) (*Token, error) {
|
||||||
@ -35,7 +35,7 @@ func CacheGetTokenByKey(key string) (*Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), TokenCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set token error: " + err.Error())
|
common.SysError("Redis set token error: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func CacheGetUserGroup(id int) (group string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, UserId2GroupCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, time.Duration(UserId2GroupCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user group error: " + err.Error())
|
common.SysError("Redis set user group error: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ func CacheGetUserQuota(id int) (quota int, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user quota error: " + err.Error())
|
common.SysError("Redis set user quota error: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -91,27 +91,40 @@ func CacheUpdateUserQuota(id int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func CacheIsUserEnabled(userId int) bool {
|
func CacheDecreaseUserQuota(id int, quota int) error {
|
||||||
|
if !common.RedisEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := common.RedisDecrease(fmt.Sprintf("user_quota:%d", id), int64(quota))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CacheIsUserEnabled(userId int) (bool, error) {
|
||||||
if !common.RedisEnabled {
|
if !common.RedisEnabled {
|
||||||
return IsUserEnabled(userId)
|
return IsUserEnabled(userId)
|
||||||
}
|
}
|
||||||
enabled, err := common.RedisGet(fmt.Sprintf("user_enabled:%d", userId))
|
enabled, err := common.RedisGet(fmt.Sprintf("user_enabled:%d", userId))
|
||||||
if err != nil {
|
if err == nil {
|
||||||
status := common.UserStatusDisabled
|
return enabled == "1", nil
|
||||||
if IsUserEnabled(userId) {
|
|
||||||
status = common.UserStatusEnabled
|
|
||||||
}
|
|
||||||
enabled = fmt.Sprintf("%d", status)
|
|
||||||
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, UserId2StatusCacheSeconds*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
common.SysError("Redis set user enabled error: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return enabled == "1"
|
|
||||||
|
userEnabled, err := IsUserEnabled(userId)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
enabled = "0"
|
||||||
|
if userEnabled {
|
||||||
|
enabled = "1"
|
||||||
|
}
|
||||||
|
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, time.Duration(UserId2StatusCacheSeconds)*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("Redis set user enabled error: " + err.Error())
|
||||||
|
}
|
||||||
|
return userEnabled, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var group2model2channels map[string]map[string][]*Channel
|
var group2model2channels map[string]map[string][]*Channel
|
||||||
|
@ -141,6 +141,14 @@ func UpdateChannelStatusById(id int, status int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateChannelUsedQuota(id int, quota int) {
|
func UpdateChannelUsedQuota(id int, quota int) {
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateChannelUsedQuota(id, quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateChannelUsedQuota(id int, quota int) {
|
||||||
err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error
|
err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("failed to update channel used quota: " + err.Error())
|
common.SysError("failed to update channel used quota: " + err.Error())
|
||||||
|
@ -2,10 +2,13 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
@ -33,34 +36,52 @@ func createRootAccountIfNeed() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountTable(tableName string) (num int64) {
|
func chooseDB() (*gorm.DB, error) {
|
||||||
DB.Table(tableName).Count(&num)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitDB() (err error) {
|
|
||||||
var db *gorm.DB
|
|
||||||
if os.Getenv("SQL_DSN") != "" {
|
if os.Getenv("SQL_DSN") != "" {
|
||||||
|
dsn := os.Getenv("SQL_DSN")
|
||||||
|
if strings.HasPrefix(dsn, "postgres://") {
|
||||||
|
// Use PostgreSQL
|
||||||
|
common.SysLog("using PostgreSQL as database")
|
||||||
|
return gorm.Open(postgres.New(postgres.Config{
|
||||||
|
DSN: dsn,
|
||||||
|
PreferSimpleProtocol: true, // disables implicit prepared statement usage
|
||||||
|
}), &gorm.Config{
|
||||||
|
PrepareStmt: true, // precompile SQL
|
||||||
|
})
|
||||||
|
}
|
||||||
// Use MySQL
|
// Use MySQL
|
||||||
common.SysLog("using MySQL as database")
|
common.SysLog("using MySQL as database")
|
||||||
db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{
|
return gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
PrepareStmt: true, // precompile SQL
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Use SQLite
|
|
||||||
common.SysLog("SQL_DSN not set, using SQLite as database")
|
|
||||||
common.UsingSQLite = true
|
|
||||||
db, err = gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
|
|
||||||
PrepareStmt: true, // precompile SQL
|
PrepareStmt: true, // precompile SQL
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
common.SysLog("database connected")
|
// Use SQLite
|
||||||
|
common.SysLog("SQL_DSN not set, using SQLite as database")
|
||||||
|
common.UsingSQLite = true
|
||||||
|
return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
|
||||||
|
PrepareStmt: true, // precompile SQL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDB() (err error) {
|
||||||
|
db, err := chooseDB()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if common.DebugEnabled {
|
||||||
|
db = db.Debug()
|
||||||
|
}
|
||||||
DB = db
|
DB = db
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 100))
|
||||||
|
sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 1000))
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetOrDefault("SQL_MAX_LIFETIME", 60)))
|
||||||
|
|
||||||
if !common.IsMasterNode {
|
if !common.IsMasterNode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := db.AutoMigrate(&Channel{})
|
err = db.AutoMigrate(&Channel{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
||||||
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
|
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
|
||||||
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
||||||
|
common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled)
|
||||||
|
common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",")
|
||||||
common.OptionMap["SMTPServer"] = ""
|
common.OptionMap["SMTPServer"] = ""
|
||||||
common.OptionMap["SMTPFrom"] = ""
|
common.OptionMap["SMTPFrom"] = ""
|
||||||
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
||||||
@ -141,6 +143,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.TurnstileCheckEnabled = boolValue
|
common.TurnstileCheckEnabled = boolValue
|
||||||
case "RegisterEnabled":
|
case "RegisterEnabled":
|
||||||
common.RegisterEnabled = boolValue
|
common.RegisterEnabled = boolValue
|
||||||
|
case "EmailDomainRestrictionEnabled":
|
||||||
|
common.EmailDomainRestrictionEnabled = boolValue
|
||||||
case "AutomaticDisableChannelEnabled":
|
case "AutomaticDisableChannelEnabled":
|
||||||
common.AutomaticDisableChannelEnabled = boolValue
|
common.AutomaticDisableChannelEnabled = boolValue
|
||||||
case "ApproximateTokenEnabled":
|
case "ApproximateTokenEnabled":
|
||||||
@ -154,6 +158,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
|
case "EmailDomainWhitelist":
|
||||||
|
common.EmailDomainWhitelist = strings.Split(value, ",")
|
||||||
case "SMTPServer":
|
case "SMTPServer":
|
||||||
common.SMTPServer = value
|
common.SMTPServer = value
|
||||||
case "SMTPPort":
|
case "SMTPPort":
|
||||||
|
@ -51,20 +51,21 @@ func Redeem(key string, userId int) (quota int, err error) {
|
|||||||
redemption := &Redemption{}
|
redemption := &Redemption{}
|
||||||
|
|
||||||
err = DB.Transaction(func(tx *gorm.DB) error {
|
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||||
err := DB.Where("`key` = ?", key).First(redemption).Error
|
err := tx.Set("gorm:query_option", "FOR UPDATE").Where("`key` = ?", key).First(redemption).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("无效的兑换码")
|
return errors.New("无效的兑换码")
|
||||||
}
|
}
|
||||||
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
||||||
return errors.New("该兑换码已被使用")
|
return errors.New("该兑换码已被使用")
|
||||||
}
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
|
err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
redemption.RedeemedTime = common.GetTimestamp()
|
redemption.RedeemedTime = common.GetTimestamp()
|
||||||
redemption.Status = common.RedemptionCodeStatusUsed
|
redemption.Status = common.RedemptionCodeStatusUsed
|
||||||
return redemption.SelectUpdate()
|
err = tx.Save(redemption).Error
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.New("兑换失败," + err.Error())
|
return 0, errors.New("兑换失败," + err.Error())
|
||||||
|
@ -39,32 +39,35 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
|||||||
}
|
}
|
||||||
token, err = CacheGetTokenByKey(key)
|
token, err = CacheGetTokenByKey(key)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if token.Status == common.TokenStatusExhausted {
|
||||||
|
return nil, errors.New("该令牌额度已用尽")
|
||||||
|
} else if token.Status == common.TokenStatusExpired {
|
||||||
|
return nil, errors.New("该令牌已过期")
|
||||||
|
}
|
||||||
if token.Status != common.TokenStatusEnabled {
|
if token.Status != common.TokenStatusEnabled {
|
||||||
return nil, errors.New("该令牌状态不可用")
|
return nil, errors.New("该令牌状态不可用")
|
||||||
}
|
}
|
||||||
if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
|
if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
|
||||||
token.Status = common.TokenStatusExpired
|
if !common.RedisEnabled {
|
||||||
err := token.SelectUpdate()
|
token.Status = common.TokenStatusExpired
|
||||||
if err != nil {
|
err := token.SelectUpdate()
|
||||||
common.SysError("failed to update token status" + err.Error())
|
if err != nil {
|
||||||
|
common.SysError("failed to update token status" + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, errors.New("该令牌已过期")
|
return nil, errors.New("该令牌已过期")
|
||||||
}
|
}
|
||||||
if !token.UnlimitedQuota && token.RemainQuota <= 0 {
|
if !token.UnlimitedQuota && token.RemainQuota <= 0 {
|
||||||
token.Status = common.TokenStatusExhausted
|
if !common.RedisEnabled {
|
||||||
err := token.SelectUpdate()
|
// in this case, we can make sure the token is exhausted
|
||||||
if err != nil {
|
token.Status = common.TokenStatusExhausted
|
||||||
common.SysError("failed to update token status" + err.Error())
|
err := token.SelectUpdate()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update token status" + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, errors.New("该令牌额度已用尽")
|
return nil, errors.New("该令牌额度已用尽")
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
token.AccessedTime = common.GetTimestamp()
|
|
||||||
err := token.SelectUpdate()
|
|
||||||
if err != nil {
|
|
||||||
common.SysError("failed to update token" + err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("无效的令牌")
|
return nil, errors.New("无效的令牌")
|
||||||
@ -131,10 +134,19 @@ func IncreaseTokenQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeTokenQuota, id, quota)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return increaseTokenQuota(id, quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseTokenQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"remain_quota": gorm.Expr("remain_quota + ?", quota),
|
"remain_quota": gorm.Expr("remain_quota + ?", quota),
|
||||||
"used_quota": gorm.Expr("used_quota - ?", quota),
|
"used_quota": gorm.Expr("used_quota - ?", quota),
|
||||||
|
"accessed_time": common.GetTimestamp(),
|
||||||
},
|
},
|
||||||
).Error
|
).Error
|
||||||
return err
|
return err
|
||||||
@ -144,10 +156,19 @@ func DecreaseTokenQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeTokenQuota, id, -quota)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return decreaseTokenQuota(id, quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decreaseTokenQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"remain_quota": gorm.Expr("remain_quota - ?", quota),
|
"remain_quota": gorm.Expr("remain_quota - ?", quota),
|
||||||
"used_quota": gorm.Expr("used_quota + ?", quota),
|
"used_quota": gorm.Expr("used_quota + ?", quota),
|
||||||
|
"accessed_time": common.GetTimestamp(),
|
||||||
},
|
},
|
||||||
).Error
|
).Error
|
||||||
return err
|
return err
|
||||||
|
@ -226,17 +226,16 @@ func IsAdmin(userId int) bool {
|
|||||||
return user.Role >= common.RoleAdminUser
|
return user.Role >= common.RoleAdminUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsUserEnabled(userId int) bool {
|
func IsUserEnabled(userId int) (bool, error) {
|
||||||
if userId == 0 {
|
if userId == 0 {
|
||||||
return false
|
return false, errors.New("user id is empty")
|
||||||
}
|
}
|
||||||
var user User
|
var user User
|
||||||
err := DB.Where("id = ?", userId).Select("status").Find(&user).Error
|
err := DB.Where("id = ?", userId).Select("status").Find(&user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("no such user " + err.Error())
|
return false, err
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return user.Status == common.UserStatusEnabled
|
return user.Status == common.UserStatusEnabled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAccessToken(token string) (user *User) {
|
func ValidateAccessToken(token string) (user *User) {
|
||||||
@ -275,6 +274,14 @@ func IncreaseUserQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeUserQuota, id, quota)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return increaseUserQuota(id, quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func increaseUserQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -283,6 +290,14 @@ func DecreaseUserQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeUserQuota, id, -quota)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return decreaseUserQuota(id, quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decreaseUserQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -293,10 +308,18 @@ func GetRootUserEmail() (email string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
|
func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
|
||||||
|
if common.BatchUpdateEnabled {
|
||||||
|
addNewRecord(BatchUpdateTypeUsedQuotaAndRequestCount, id, quota)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateUserUsedQuotaAndRequestCount(id, quota, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUserUsedQuotaAndRequestCount(id int, quota int, count int) {
|
||||||
err := DB.Model(&User{}).Where("id = ?", id).Updates(
|
err := DB.Model(&User{}).Where("id = ?", id).Updates(
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"used_quota": gorm.Expr("used_quota + ?", quota),
|
"used_quota": gorm.Expr("used_quota + ?", quota),
|
||||||
"request_count": gorm.Expr("request_count + ?", 1),
|
"request_count": gorm.Expr("request_count + ?", count),
|
||||||
},
|
},
|
||||||
).Error
|
).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
75
model/utils.go
Normal file
75
model/utils.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BatchUpdateTypeCount = 4 // if you add a new type, you need to add a new map and a new lock
|
||||||
|
|
||||||
|
const (
|
||||||
|
BatchUpdateTypeUserQuota = iota
|
||||||
|
BatchUpdateTypeTokenQuota
|
||||||
|
BatchUpdateTypeUsedQuotaAndRequestCount
|
||||||
|
BatchUpdateTypeChannelUsedQuota
|
||||||
|
)
|
||||||
|
|
||||||
|
var batchUpdateStores []map[int]int
|
||||||
|
var batchUpdateLocks []sync.Mutex
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := 0; i < BatchUpdateTypeCount; i++ {
|
||||||
|
batchUpdateStores = append(batchUpdateStores, make(map[int]int))
|
||||||
|
batchUpdateLocks = append(batchUpdateLocks, sync.Mutex{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitBatchUpdater() {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(common.BatchUpdateInterval) * time.Second)
|
||||||
|
batchUpdate()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewRecord(type_ int, id int, value int) {
|
||||||
|
batchUpdateLocks[type_].Lock()
|
||||||
|
defer batchUpdateLocks[type_].Unlock()
|
||||||
|
if _, ok := batchUpdateStores[type_][id]; !ok {
|
||||||
|
batchUpdateStores[type_][id] = value
|
||||||
|
} else {
|
||||||
|
batchUpdateStores[type_][id] += value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func batchUpdate() {
|
||||||
|
common.SysLog("batch update started")
|
||||||
|
for i := 0; i < BatchUpdateTypeCount; i++ {
|
||||||
|
batchUpdateLocks[i].Lock()
|
||||||
|
store := batchUpdateStores[i]
|
||||||
|
batchUpdateStores[i] = make(map[int]int)
|
||||||
|
batchUpdateLocks[i].Unlock()
|
||||||
|
|
||||||
|
for key, value := range store {
|
||||||
|
switch i {
|
||||||
|
case BatchUpdateTypeUserQuota:
|
||||||
|
err := increaseUserQuota(key, value)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to batch update user quota: " + err.Error())
|
||||||
|
}
|
||||||
|
case BatchUpdateTypeTokenQuota:
|
||||||
|
err := increaseTokenQuota(key, value)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to batch update token quota: " + err.Error())
|
||||||
|
}
|
||||||
|
case BatchUpdateTypeUsedQuotaAndRequestCount:
|
||||||
|
updateUserUsedQuotaAndRequestCount(key, value, 1) // TODO: count is incorrect
|
||||||
|
case BatchUpdateTypeChannelUsedQuota:
|
||||||
|
updateChannelUsedQuota(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.SysLog("batch update finished")
|
||||||
|
}
|
@ -21,6 +21,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
||||||
|
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
@ -36,7 +37,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
selfRoute.GET("/self", controller.GetSelf)
|
selfRoute.GET("/self", controller.GetSelf)
|
||||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf)
|
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
selfRoute.POST("/topup", controller.TopUp)
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
|
@ -12,7 +12,7 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
modelsRouter := router.Group("/v1/models")
|
modelsRouter := router.Group("/v1/models")
|
||||||
modelsRouter.Use(middleware.TokenAuth())
|
modelsRouter.Use(middleware.TokenAuth())
|
||||||
{
|
{
|
||||||
modelsRouter.GET("/", controller.ListModels)
|
modelsRouter.GET("", controller.ListModels)
|
||||||
modelsRouter.GET("/:model", controller.RetrieveModel)
|
modelsRouter.GET("/:model", controller.RetrieveModel)
|
||||||
}
|
}
|
||||||
relayV1Router := router.Group("/v1")
|
relayV1Router := router.Group("/v1")
|
||||||
@ -26,8 +26,8 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/embeddings", controller.Relay)
|
relayV1Router.POST("/embeddings", controller.Relay)
|
||||||
relayV1Router.POST("/engines/:model/embeddings", controller.Relay)
|
relayV1Router.POST("/engines/:model/embeddings", controller.Relay)
|
||||||
relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/transcriptions", controller.Relay)
|
||||||
relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/translations", controller.Relay)
|
||||||
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/files", controller.RelayNotImplemented)
|
relayV1Router.POST("/files", controller.RelayNotImplemented)
|
||||||
relayV1Router.DELETE("/files/:id", controller.RelayNotImplemented)
|
relayV1Router.DELETE("/files/:id", controller.RelayNotImplemented)
|
||||||
|
@ -18,7 +18,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
|||||||
router.Use(middleware.Cache())
|
router.Use(middleware.Cache())
|
||||||
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
|
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
|
||||||
router.NoRoute(func(c *gin.Context) {
|
router.NoRoute(func(c *gin.Context) {
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/v1") {
|
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") {
|
||||||
controller.RelayNotFound(c)
|
controller.RelayNotFound(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -363,9 +363,12 @@ const ChannelsTable = () => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Popup
|
<Popup
|
||||||
content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'}
|
trigger={<span onClick={() => {
|
||||||
key={channel.id}
|
updateChannelBalance(channel.id, channel.name, idx);
|
||||||
trigger={renderBalance(channel.type, channel.balance)}
|
}} style={{ cursor: 'pointer' }}>
|
||||||
|
{renderBalance(channel.type, channel.balance)}
|
||||||
|
</span>}
|
||||||
|
content='点击更新'
|
||||||
basic
|
basic
|
||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
@ -380,16 +383,16 @@ const ChannelsTable = () => {
|
|||||||
>
|
>
|
||||||
测试
|
测试
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/*<Button*/}
|
||||||
size={'small'}
|
{/* size={'small'}*/}
|
||||||
positive
|
{/* positive*/}
|
||||||
loading={updatingBalance}
|
{/* loading={updatingBalance}*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
updateChannelBalance(channel.id, channel.name, idx);
|
{/* updateChannelBalance(channel.id, channel.name, idx);*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
>
|
{/*>*/}
|
||||||
更新余额
|
{/* 更新余额*/}
|
||||||
</Button>
|
{/*</Button>*/}
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<Button size='small' negative>
|
<Button size='small' negative>
|
||||||
|
@ -13,8 +13,8 @@ const GitHubOAuth = () => {
|
|||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const sendCode = async (code, count) => {
|
const sendCode = async (code, state, count) => {
|
||||||
const res = await API.get(`/api/oauth/github?code=${code}`);
|
const res = await API.get(`/api/oauth/github?code=${code}&state=${state}`);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
if (message === 'bind') {
|
if (message === 'bind') {
|
||||||
@ -36,13 +36,14 @@ const GitHubOAuth = () => {
|
|||||||
count++;
|
count++;
|
||||||
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
await sendCode(code, count);
|
await sendCode(code, state, count);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let code = searchParams.get('code');
|
let code = searchParams.get('code');
|
||||||
sendCode(code, 0).then();
|
let state = searchParams.get('state');
|
||||||
|
sendCode(code, state, 0).then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,6 +3,7 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f
|
|||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, getLogo, showError, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showSuccess } from '../helpers';
|
||||||
|
import { getOAuthState, onGitHubOAuthClicked } from './utils';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@ -31,12 +32,6 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(true);
|
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(true);
|
||||||
|
|
||||||
const onGitHubOAuthClicked = () => {
|
|
||||||
window.open(
|
|
||||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onWeChatLoginClicked = () => {
|
const onWeChatLoginClicked = () => {
|
||||||
setShowWeChatLoginModal(true);
|
setShowWeChatLoginModal(true);
|
||||||
};
|
};
|
||||||
@ -131,7 +126,7 @@ const LoginForm = () => {
|
|||||||
circular
|
circular
|
||||||
color='black'
|
color='black'
|
||||||
icon='github'
|
icon='github'
|
||||||
onClick={onGitHubOAuthClicked}
|
onClick={()=>onGitHubOAuthClicked(status.github_client_id)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
@ -43,6 +43,7 @@ function renderType(type) {
|
|||||||
|
|
||||||
const LogsTable = () => {
|
const LogsTable = () => {
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [showStat, setShowStat] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activePage, setActivePage] = useState(1);
|
const [activePage, setActivePage] = useState(1);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
@ -92,6 +93,17 @@ const LogsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEyeClick = async () => {
|
||||||
|
if (!showStat) {
|
||||||
|
if (isAdminUser) {
|
||||||
|
await getLogStat();
|
||||||
|
} else {
|
||||||
|
await getLogSelfStat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowStat(!showStat);
|
||||||
|
};
|
||||||
|
|
||||||
const loadLogs = async (startIdx) => {
|
const loadLogs = async (startIdx) => {
|
||||||
let url = '';
|
let url = '';
|
||||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||||
@ -129,13 +141,8 @@ const LogsTable = () => {
|
|||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setActivePage(1)
|
setActivePage(1);
|
||||||
await loadLogs(0);
|
await loadLogs(0);
|
||||||
if (isAdminUser) {
|
|
||||||
getLogStat().then();
|
|
||||||
} else {
|
|
||||||
getLogSelfStat().then();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -169,7 +176,7 @@ const LogsTable = () => {
|
|||||||
if (logs.length === 0) return;
|
if (logs.length === 0) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let sortedLogs = [...logs];
|
let sortedLogs = [...logs];
|
||||||
if (typeof sortedLogs[0][key] === 'string'){
|
if (typeof sortedLogs[0][key] === 'string') {
|
||||||
sortedLogs.sort((a, b) => {
|
sortedLogs.sort((a, b) => {
|
||||||
return ('' + a[key]).localeCompare(b[key]);
|
return ('' + a[key]).localeCompare(b[key]);
|
||||||
});
|
});
|
||||||
@ -190,7 +197,12 @@ const LogsTable = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
|
<Header as='h3'>
|
||||||
|
使用明细(总消耗额度:
|
||||||
|
{showStat && renderQuota(stat.quota)}
|
||||||
|
{!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>}
|
||||||
|
)
|
||||||
|
</Header>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
{
|
{
|
||||||
@ -312,7 +324,7 @@ const LogsTable = () => {
|
|||||||
.map((log, idx) => {
|
.map((log, idx) => {
|
||||||
if (log.deleted) return <></>;
|
if (log.deleted) return <></>;
|
||||||
return (
|
return (
|
||||||
<Table.Row key={log.created_at}>
|
<Table.Row key={log.id}>
|
||||||
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
||||||
{
|
{
|
||||||
isAdminUser && (
|
isAdminUser && (
|
||||||
|
@ -112,7 +112,7 @@ const OtherSetting = () => {
|
|||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='公告'
|
label='公告'
|
||||||
placeholder='在此输入新的公告内容'
|
placeholder='在此输入新的公告内容,支持 Markdown & HTML 代码'
|
||||||
value={inputs.Notice}
|
value={inputs.Notice}
|
||||||
name='Notice'
|
name='Notice'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
@ -4,6 +4,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
import { onGitHubOAuthClicked } from './utils';
|
||||||
|
|
||||||
const PersonalSetting = () => {
|
const PersonalSetting = () => {
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
@ -25,6 +26,8 @@ const PersonalSetting = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [disableButton, setDisableButton] = useState(false);
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(30);
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
const [affLink, setAffLink] = useState("");
|
||||||
|
const [systemToken, setSystemToken] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@ -59,8 +62,10 @@ const PersonalSetting = () => {
|
|||||||
const res = await API.get('/api/user/token');
|
const res = await API.get('/api/user/token');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
|
setSystemToken(data);
|
||||||
|
setAffLink("");
|
||||||
await copy(data);
|
await copy(data);
|
||||||
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
|
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@ -71,13 +76,27 @@ const PersonalSetting = () => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let link = `${window.location.origin}/register?aff=${data}`;
|
let link = `${window.location.origin}/register?aff=${data}`;
|
||||||
|
setAffLink(link);
|
||||||
|
setSystemToken("");
|
||||||
await copy(link);
|
await copy(link);
|
||||||
showNotice(`邀请链接已复制到剪切板:${link}`);
|
showSuccess(`邀请链接已复制到剪切板`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAffLinkClick = async (e) => {
|
||||||
|
e.target.select();
|
||||||
|
await copy(e.target.value);
|
||||||
|
showSuccess(`邀请链接已复制到剪切板`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSystemTokenClick = async (e) => {
|
||||||
|
e.target.select();
|
||||||
|
await copy(e.target.value);
|
||||||
|
showSuccess(`系统令牌已复制到剪切板`);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteAccount = async () => {
|
const deleteAccount = async () => {
|
||||||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||||
showError('请输入你的账户名以确认删除!');
|
showError('请输入你的账户名以确认删除!');
|
||||||
@ -112,12 +131,6 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openGitHubOAuth = () => {
|
|
||||||
window.open(
|
|
||||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
const sendVerificationCode = async () => {
|
||||||
setDisableButton(true);
|
setDisableButton(true);
|
||||||
if (inputs.email === '') return;
|
if (inputs.email === '') return;
|
||||||
@ -168,6 +181,25 @@ const PersonalSetting = () => {
|
|||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
setShowAccountDeleteModal(true);
|
setShowAccountDeleteModal(true);
|
||||||
}}>删除个人账户</Button>
|
}}>删除个人账户</Button>
|
||||||
|
|
||||||
|
{systemToken && (
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
readOnly
|
||||||
|
value={systemToken}
|
||||||
|
onClick={handleSystemTokenClick}
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{affLink && (
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
readOnly
|
||||||
|
value={affLink}
|
||||||
|
onClick={handleAffLinkClick}
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>账号绑定</Header>
|
<Header as='h3'>账号绑定</Header>
|
||||||
{
|
{
|
||||||
@ -212,7 +244,7 @@ const PersonalSetting = () => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
{
|
{
|
||||||
status.github_oauth && (
|
status.github_oauth && (
|
||||||
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Button
|
<Button
|
||||||
@ -262,6 +294,7 @@ const PersonalSetting = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color=''
|
||||||
fluid
|
fluid
|
||||||
@ -269,8 +302,17 @@ const PersonalSetting = () => {
|
|||||||
onClick={bindEmail}
|
onClick={bindEmail}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
绑定
|
确认绑定
|
||||||
</Button>
|
</Button>
|
||||||
|
<div style={{ width: '1rem' }}></div>
|
||||||
|
<Button
|
||||||
|
fluid
|
||||||
|
size='large'
|
||||||
|
onClick={() => setShowEmailBindModal(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Description>
|
</Modal.Description>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
@ -282,13 +324,14 @@ const PersonalSetting = () => {
|
|||||||
size={'tiny'}
|
size={'tiny'}
|
||||||
style={{ maxWidth: '450px' }}
|
style={{ maxWidth: '450px' }}
|
||||||
>
|
>
|
||||||
<Modal.Header>确认删除自己的帐户</Modal.Header>
|
<Modal.Header>危险操作</Modal.Header>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
|
<Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
|
||||||
<Modal.Description>
|
<Modal.Description>
|
||||||
<Form size='large'>
|
<Form size='large'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
placeholder={`输入你的账户名 ${userState.user.username} 以确认删除`}
|
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||||
name='self_account_deletion_confirmation'
|
name='self_account_deletion_confirmation'
|
||||||
value={inputs.self_account_deletion_confirmation}
|
value={inputs.self_account_deletion_confirmation}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
@ -303,15 +346,25 @@ const PersonalSetting = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||||
color='red'
|
<Button
|
||||||
fluid
|
color='red'
|
||||||
size='large'
|
fluid
|
||||||
onClick={deleteAccount}
|
size='large'
|
||||||
loading={loading}
|
onClick={deleteAccount}
|
||||||
>
|
loading={loading}
|
||||||
删除
|
>
|
||||||
</Button>
|
确认删除
|
||||||
|
</Button>
|
||||||
|
<div style={{ width: '1rem' }}></div>
|
||||||
|
<Button
|
||||||
|
fluid
|
||||||
|
size='large'
|
||||||
|
onClick={() => setShowAccountDeleteModal(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Description>
|
</Modal.Description>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react';
|
import { Button, Form, Label, Popup, Pagination, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
|
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
@ -240,15 +240,25 @@ const RedemptionsTable = () => {
|
|||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Popup
|
||||||
size={'small'}
|
trigger={
|
||||||
negative
|
<Button size='small' negative>
|
||||||
onClick={() => {
|
删除
|
||||||
manageRedemption(redemption.id, 'delete', idx);
|
</Button>
|
||||||
}}
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageRedemption(redemption.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
disabled={redemption.status === 3} // used
|
disabled={redemption.status === 3} // used
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
|
||||||
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
import { API, removeTrailingSlash, showError } from '../helpers';
|
||||||
|
|
||||||
const SystemSetting = () => {
|
const SystemSetting = () => {
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
@ -26,9 +26,14 @@ const SystemSetting = () => {
|
|||||||
TurnstileSiteKey: '',
|
TurnstileSiteKey: '',
|
||||||
TurnstileSecretKey: '',
|
TurnstileSecretKey: '',
|
||||||
RegisterEnabled: '',
|
RegisterEnabled: '',
|
||||||
|
EmailDomainRestrictionEnabled: '',
|
||||||
|
EmailDomainWhitelist: ''
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||||
|
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||||
|
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/option/');
|
||||||
@ -38,8 +43,15 @@ const SystemSetting = () => {
|
|||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs({
|
||||||
|
...newInputs,
|
||||||
|
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
|
||||||
|
});
|
||||||
setOriginInputs(newInputs);
|
setOriginInputs(newInputs);
|
||||||
|
|
||||||
|
setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||||
|
return { key: item, text: item, value: item };
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@ -58,6 +70,7 @@ const SystemSetting = () => {
|
|||||||
case 'GitHubOAuthEnabled':
|
case 'GitHubOAuthEnabled':
|
||||||
case 'WeChatAuthEnabled':
|
case 'WeChatAuthEnabled':
|
||||||
case 'TurnstileCheckEnabled':
|
case 'TurnstileCheckEnabled':
|
||||||
|
case 'EmailDomainRestrictionEnabled':
|
||||||
case 'RegisterEnabled':
|
case 'RegisterEnabled':
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||||
break;
|
break;
|
||||||
@ -70,7 +83,12 @@ const SystemSetting = () => {
|
|||||||
});
|
});
|
||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
if (key === 'EmailDomainWhitelist') {
|
||||||
|
value = value.split(',');
|
||||||
|
}
|
||||||
|
setInputs((inputs) => ({
|
||||||
|
...inputs, [key]: value
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@ -78,6 +96,11 @@ const SystemSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = async (e, { name, value }) => {
|
const handleInputChange = async (e, { name, value }) => {
|
||||||
|
if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
|
||||||
|
// block disabling password login
|
||||||
|
setShowPasswordWarningModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
name === 'Notice' ||
|
name === 'Notice' ||
|
||||||
name.startsWith('SMTP') ||
|
name.startsWith('SMTP') ||
|
||||||
@ -88,7 +111,8 @@ const SystemSetting = () => {
|
|||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey'
|
name === 'TurnstileSecretKey' ||
|
||||||
|
name === 'EmailDomainWhitelist'
|
||||||
) {
|
) {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
} else {
|
} else {
|
||||||
@ -125,6 +149,16 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const submitEmailDomainWhitelist = async () => {
|
||||||
|
if (
|
||||||
|
originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
|
||||||
|
inputs.SMTPToken !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitWeChat = async () => {
|
const submitWeChat = async () => {
|
||||||
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
||||||
await updateOption(
|
await updateOption(
|
||||||
@ -173,6 +207,22 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitNewRestrictedDomain = () => {
|
||||||
|
const localDomainList = inputs.EmailDomainWhitelist;
|
||||||
|
if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
|
||||||
|
setRestrictedDomainInput('');
|
||||||
|
setInputs({
|
||||||
|
...inputs,
|
||||||
|
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
||||||
|
});
|
||||||
|
setEmailDomainWhitelist([...EmailDomainWhitelist, {
|
||||||
|
key: restrictedDomainInput,
|
||||||
|
text: restrictedDomainInput,
|
||||||
|
value: restrictedDomainInput,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid columns={1}>
|
<Grid columns={1}>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
@ -199,6 +249,32 @@ const SystemSetting = () => {
|
|||||||
name='PasswordLoginEnabled'
|
name='PasswordLoginEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
showPasswordWarningModal &&
|
||||||
|
<Modal
|
||||||
|
open={showPasswordWarningModal}
|
||||||
|
onClose={() => setShowPasswordWarningModal(false)}
|
||||||
|
size={'tiny'}
|
||||||
|
style={{ maxWidth: '450px' }}
|
||||||
|
>
|
||||||
|
<Modal.Header>警告</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
color='yellow'
|
||||||
|
onClick={async () => {
|
||||||
|
setShowPasswordWarningModal(false);
|
||||||
|
await updateOption('PasswordLoginEnabled', 'false');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.PasswordRegisterEnabled === 'true'}
|
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||||
label='允许通过密码进行注册'
|
label='允许通过密码进行注册'
|
||||||
@ -239,6 +315,54 @@ const SystemSetting = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
配置邮箱域名白名单
|
||||||
|
<Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Checkbox
|
||||||
|
label='启用邮箱域名白名单'
|
||||||
|
name='EmailDomainRestrictionEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group widths={2}>
|
||||||
|
<Form.Dropdown
|
||||||
|
label='允许的邮箱域名'
|
||||||
|
placeholder='允许的邮箱域名'
|
||||||
|
name='EmailDomainWhitelist'
|
||||||
|
required
|
||||||
|
fluid
|
||||||
|
multiple
|
||||||
|
selection
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.EmailDomainWhitelist}
|
||||||
|
autoComplete='new-password'
|
||||||
|
options={EmailDomainWhitelist}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='添加新的允许的邮箱域名'
|
||||||
|
action={
|
||||||
|
<Button type='button' onClick={() => {
|
||||||
|
submitNewRestrictedDomain();
|
||||||
|
}}>填入</Button>
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
submitNewRestrictedDomain();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoComplete='new-password'
|
||||||
|
placeholder='输入新的允许的邮箱域名'
|
||||||
|
value={restrictedDomainInput}
|
||||||
|
onChange={(e, { value }) => {
|
||||||
|
setRestrictedDomainInput(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
|
||||||
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
配置 SMTP
|
配置 SMTP
|
||||||
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||||
@ -284,7 +408,7 @@ const SystemSetting = () => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
value={inputs.SMTPToken}
|
checked={inputs.RegisterEnabled === 'true'}
|
||||||
placeholder='敏感信息不会发送到前端显示'
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react';
|
import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
import { renderQuota } from '../helpers/render';
|
||||||
|
|
||||||
|
const COPY_OPTIONS = [
|
||||||
|
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||||
|
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
|
||||||
|
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const OPEN_LINK_OPTIONS = [
|
||||||
|
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
|
||||||
|
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||||
|
];
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -68,6 +79,84 @@ const TokensTable = () => {
|
|||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await loadTokens(activePage - 1);
|
await loadTokens(activePage - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopy = async (type, key) => {
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
let serverAddress = '';
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
serverAddress = status.server_address;
|
||||||
|
}
|
||||||
|
if (serverAddress === '') {
|
||||||
|
serverAddress = window.location.origin;
|
||||||
|
}
|
||||||
|
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||||
|
const nextLink = localStorage.getItem('chat_link');
|
||||||
|
let nextUrl;
|
||||||
|
|
||||||
|
if (nextLink) {
|
||||||
|
nextUrl = nextLink + `/#/?settings={"key":"sk-${key}"}`;
|
||||||
|
} else {
|
||||||
|
nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
switch (type) {
|
||||||
|
case 'ama':
|
||||||
|
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||||
|
break;
|
||||||
|
case 'opencat':
|
||||||
|
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
url = nextUrl;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
url = `sk-${key}`;
|
||||||
|
}
|
||||||
|
if (await copy(url)) {
|
||||||
|
showSuccess('已复制到剪贴板!');
|
||||||
|
} else {
|
||||||
|
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
||||||
|
setSearchKeyword(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenLink = async (type, key) => {
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
let serverAddress = '';
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
serverAddress = status.server_address;
|
||||||
|
}
|
||||||
|
if (serverAddress === '') {
|
||||||
|
serverAddress = window.location.origin;
|
||||||
|
}
|
||||||
|
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||||
|
const chatLink = localStorage.getItem('chat_link');
|
||||||
|
let defaultUrl;
|
||||||
|
|
||||||
|
if (chatLink) {
|
||||||
|
defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}"}`;
|
||||||
|
} else {
|
||||||
|
defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||||
|
}
|
||||||
|
let url;
|
||||||
|
switch (type) {
|
||||||
|
case 'ama':
|
||||||
|
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'opencat':
|
||||||
|
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
url = defaultUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -235,21 +324,51 @@ const TokensTable = () => {
|
|||||||
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button.Group color='green' size={'small'}>
|
||||||
size={'small'}
|
<Button
|
||||||
positive
|
size={'small'}
|
||||||
onClick={async () => {
|
positive
|
||||||
let key = "sk-" + token.key;
|
onClick={async () => {
|
||||||
if (await copy(key)) {
|
await onCopy('', token.key);
|
||||||
showSuccess('已复制到剪贴板!');
|
}}
|
||||||
} else {
|
>
|
||||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
复制
|
||||||
setSearchKeyword(key);
|
</Button>
|
||||||
}
|
<Dropdown
|
||||||
}}
|
className='button icon'
|
||||||
>
|
floating
|
||||||
复制
|
options={COPY_OPTIONS.map(option => ({
|
||||||
</Button>
|
...option,
|
||||||
|
onClick: async () => {
|
||||||
|
await onCopy(option.value, token.key);
|
||||||
|
}
|
||||||
|
}))}
|
||||||
|
trigger={<></>}
|
||||||
|
/>
|
||||||
|
</Button.Group>
|
||||||
|
{' '}
|
||||||
|
<Button.Group color='blue' size={'small'}>
|
||||||
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
positive
|
||||||
|
onClick={() => {
|
||||||
|
onOpenLink('', token.key);
|
||||||
|
}}>
|
||||||
|
聊天
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
className="button icon"
|
||||||
|
floating
|
||||||
|
options={OPEN_LINK_OPTIONS.map(option => ({
|
||||||
|
...option,
|
||||||
|
onClick: async () => {
|
||||||
|
await onOpenLink(option.value, token.key);
|
||||||
|
}
|
||||||
|
}))}
|
||||||
|
trigger={<></>}
|
||||||
|
/>
|
||||||
|
</Button.Group>
|
||||||
|
{' '}
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<Button size='small' negative>
|
<Button size='small' negative>
|
||||||
|
@ -227,7 +227,7 @@ const UsersTable = () => {
|
|||||||
content={user.email ? user.email : '未绑定邮箱地址'}
|
content={user.email ? user.email : '未绑定邮箱地址'}
|
||||||
key={user.username}
|
key={user.username}
|
||||||
header={user.display_name ? user.display_name : user.username}
|
header={user.display_name ? user.display_name : user.username}
|
||||||
trigger={<span>{renderText(user.username, 10)}</span>}
|
trigger={<span>{renderText(user.username, 15)}</span>}
|
||||||
hoverable
|
hoverable
|
||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
20
web/src/components/utils.js
Normal file
20
web/src/components/utils.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { API, showError } from '../helpers';
|
||||||
|
|
||||||
|
export async function getOAuthState() {
|
||||||
|
const res = await API.get('/api/oauth/state');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onGitHubOAuthClicked(github_client_id) {
|
||||||
|
const state = await getOAuthState();
|
||||||
|
if (!state) return;
|
||||||
|
window.open(
|
||||||
|
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
|
||||||
|
);
|
||||||
|
}
|
@ -4,8 +4,14 @@ export const CHANNEL_OPTIONS = [
|
|||||||
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
|
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
|
||||||
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
|
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
|
||||||
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
||||||
|
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
|
||||||
|
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
|
||||||
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
||||||
|
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
|
||||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||||
|
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||||
|
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||||
|
{ key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
|
||||||
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||||
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
||||||
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export const toastConstants = {
|
export const toastConstants = {
|
||||||
SUCCESS_TIMEOUT: 500,
|
SUCCESS_TIMEOUT: 1500,
|
||||||
INFO_TIMEOUT: 3000,
|
INFO_TIMEOUT: 3000,
|
||||||
ERROR_TIMEOUT: 5000,
|
ERROR_TIMEOUT: 5000,
|
||||||
WARNING_TIMEOUT: 10000,
|
WARNING_TIMEOUT: 10000,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { toastConstants } from '../constants';
|
import { toastConstants } from '../constants';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HTMLToastContent = ({ htmlContent }) => {
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||||
|
};
|
||||||
|
export default HTMLToastContent;
|
||||||
export function isAdmin() {
|
export function isAdmin() {
|
||||||
let user = localStorage.getItem('user');
|
let user = localStorage.getItem('user');
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
@ -107,8 +112,12 @@ export function showInfo(message) {
|
|||||||
toast.info(message, showInfoOptions);
|
toast.info(message, showInfoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showNotice(message) {
|
export function showNotice(message, isHTML = false) {
|
||||||
toast.info(message, showNoticeOptions);
|
if (isHTML) {
|
||||||
|
toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
|
||||||
|
} else {
|
||||||
|
toast.info(message, showNoticeOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openPage(url) {
|
export function openPage(url) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
|
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../constants';
|
||||||
|
|
||||||
@ -10,11 +10,30 @@ const MODEL_MAPPING_EXAMPLE = {
|
|||||||
'gpt-4-32k-0314': 'gpt-4-32k'
|
'gpt-4-32k-0314': 'gpt-4-32k'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function type2secretPrompt(type) {
|
||||||
|
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
||||||
|
switch (type) {
|
||||||
|
case 15:
|
||||||
|
return '按照如下格式输入:APIKey|SecretKey';
|
||||||
|
case 18:
|
||||||
|
return '按照如下格式输入:APPID|APISecret|APIKey';
|
||||||
|
case 22:
|
||||||
|
return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
|
||||||
|
default:
|
||||||
|
return '请输入渠道对应的鉴权密钥';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const EditChannel = () => {
|
const EditChannel = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const channelId = params.id;
|
const channelId = params.id;
|
||||||
const isEdit = channelId !== undefined;
|
const isEdit = channelId !== undefined;
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const [loading, setLoading] = useState(isEdit);
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate('/channel');
|
||||||
|
};
|
||||||
|
|
||||||
const originInputs = {
|
const originInputs = {
|
||||||
name: '',
|
name: '',
|
||||||
type: 1,
|
type: 1,
|
||||||
@ -35,6 +54,33 @@ const EditChannel = () => {
|
|||||||
const [customModel, setCustomModel] = useState('');
|
const [customModel, setCustomModel] = useState('');
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
if (name === 'type' && inputs.models.length === 0) {
|
||||||
|
let localModels = [];
|
||||||
|
switch (value) {
|
||||||
|
case 14:
|
||||||
|
localModels = ['claude-instant-1', 'claude-2'];
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
localModels = ['PaLM-2'];
|
||||||
|
break;
|
||||||
|
case 15:
|
||||||
|
localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'Embedding-V1'];
|
||||||
|
break;
|
||||||
|
case 17:
|
||||||
|
localModels = ['qwen-v1', 'qwen-plus-v1', 'text-embedding-v1'];
|
||||||
|
break;
|
||||||
|
case 16:
|
||||||
|
localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
|
||||||
|
break;
|
||||||
|
case 18:
|
||||||
|
localModels = ['SparkDesk'];
|
||||||
|
break;
|
||||||
|
case 19:
|
||||||
|
localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1', '360GPT_S2_V9.4'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadChannel = async () => {
|
const loadChannel = async () => {
|
||||||
@ -132,7 +178,13 @@ const EditChannel = () => {
|
|||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||||
}
|
}
|
||||||
if (localInputs.type === 3 && localInputs.other === '') {
|
if (localInputs.type === 3 && localInputs.other === '') {
|
||||||
localInputs.other = '2023-03-15-preview';
|
localInputs.other = '2023-06-01-preview';
|
||||||
|
}
|
||||||
|
if (localInputs.type === 18 && localInputs.other === '') {
|
||||||
|
localInputs.other = 'v2.1';
|
||||||
|
}
|
||||||
|
if (localInputs.model_mapping === '') {
|
||||||
|
localInputs.model_mapping = '{}';
|
||||||
}
|
}
|
||||||
let res;
|
let res;
|
||||||
localInputs.models = localInputs.models.join(',');
|
localInputs.models = localInputs.models.join(',');
|
||||||
@ -155,6 +207,24 @@ const EditChannel = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addCustomModel = () => {
|
||||||
|
if (customModel.trim() === '') return;
|
||||||
|
if (inputs.models.includes(customModel)) return;
|
||||||
|
let localModels = [...inputs.models];
|
||||||
|
localModels.push(customModel);
|
||||||
|
let localModelOptions = [];
|
||||||
|
localModelOptions.push({
|
||||||
|
key: customModel,
|
||||||
|
text: customModel,
|
||||||
|
value: customModel
|
||||||
|
});
|
||||||
|
setModelOptions(modelOptions => {
|
||||||
|
return [...modelOptions, ...localModelOptions];
|
||||||
|
});
|
||||||
|
setCustomModel('');
|
||||||
|
handleInputChange(null, { name: 'models', value: localModels });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment loading={loading}>
|
<Segment loading={loading}>
|
||||||
@ -192,7 +262,7 @@ const EditChannel = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='默认 API 版本'
|
label='默认 API 版本'
|
||||||
name='other'
|
name='other'
|
||||||
placeholder={'请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖'}
|
placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@ -243,6 +313,34 @@ const EditChannel = () => {
|
|||||||
options={groupOptions}
|
options={groupOptions}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
{
|
||||||
|
inputs.type === 18 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='模型版本'
|
||||||
|
name='other'
|
||||||
|
placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.other}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
inputs.type === 21 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='知识库 ID'
|
||||||
|
name='other'
|
||||||
|
placeholder={'请输入知识库 ID,例如:123456'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.other}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Dropdown
|
<Form.Dropdown
|
||||||
label='模型'
|
label='模型'
|
||||||
@ -270,34 +368,24 @@ const EditChannel = () => {
|
|||||||
}}>清除所有模型</Button>
|
}}>清除所有模型</Button>
|
||||||
<Input
|
<Input
|
||||||
action={
|
action={
|
||||||
<Button type={'button'} onClick={()=>{
|
<Button type={'button'} onClick={addCustomModel}>填入</Button>
|
||||||
if (customModel.trim() === "") return;
|
|
||||||
if (inputs.models.includes(customModel)) return;
|
|
||||||
let localModels = [...inputs.models];
|
|
||||||
localModels.push(customModel);
|
|
||||||
let localModelOptions = [];
|
|
||||||
localModelOptions.push({
|
|
||||||
key: customModel,
|
|
||||||
text: customModel,
|
|
||||||
value: customModel,
|
|
||||||
});
|
|
||||||
setModelOptions(modelOptions=>{
|
|
||||||
return [...modelOptions, ...localModelOptions];
|
|
||||||
});
|
|
||||||
setCustomModel('');
|
|
||||||
handleInputChange(null, { name: 'models', value: localModels });
|
|
||||||
}}>填入</Button>
|
|
||||||
}
|
}
|
||||||
placeholder='输入自定义模型名称'
|
placeholder='输入自定义模型名称'
|
||||||
value={customModel}
|
value={customModel}
|
||||||
onChange={(e, { value }) => {
|
onChange={(e, { value }) => {
|
||||||
setCustomModel(value);
|
setCustomModel(value);
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
addCustomModel();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='模型映射'
|
label='模型重定向'
|
||||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||||
name='model_mapping'
|
name='model_mapping'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
@ -323,7 +411,7 @@ const EditChannel = () => {
|
|||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
required
|
required
|
||||||
placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入渠道对应的鉴权密钥'}
|
placeholder={type2secretPrompt(inputs.type)}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@ -341,12 +429,12 @@ const EditChannel = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='镜像'
|
label='代理'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'}
|
placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@ -354,7 +442,22 @@ const EditChannel = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
|
{
|
||||||
|
inputs.type === 22 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='私有部署地址'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
|
<Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
|
@ -14,10 +14,11 @@ const Home = () => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let oldNotice = localStorage.getItem('notice');
|
let oldNotice = localStorage.getItem('notice');
|
||||||
if (data !== oldNotice && data !== '') {
|
if (data !== oldNotice && data !== '') {
|
||||||
showNotice(data);
|
const htmlNotice = marked(data);
|
||||||
localStorage.setItem('notice', data);
|
showNotice(htmlNotice, true);
|
||||||
}
|
localStorage.setItem('notice', data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ const Home = () => {
|
|||||||
<Card.Meta>系统信息总览</Card.Meta>
|
<Card.Meta>系统信息总览</Card.Meta>
|
||||||
<Card.Description>
|
<Card.Description>
|
||||||
<p>名称:{statusState?.status?.system_name}</p>
|
<p>名称:{statusState?.status?.system_name}</p>
|
||||||
<p>版本:{statusState?.status?.version}</p>
|
<p>版本:{statusState?.status?.version ? statusState?.status?.version : "unknown"}</p>
|
||||||
<p>
|
<p>
|
||||||
源码:
|
源码:
|
||||||
<a
|
<a
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
|
import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
const EditRedemption = () => {
|
const EditRedemption = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const redemptionId = params.id;
|
const redemptionId = params.id;
|
||||||
const isEdit = redemptionId !== undefined;
|
const isEdit = redemptionId !== undefined;
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const [loading, setLoading] = useState(isEdit);
|
||||||
@ -17,6 +18,10 @@ const EditRedemption = () => {
|
|||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, quota, count } = inputs;
|
const { name, quota, count } = inputs;
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate('/redemption');
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
@ -113,6 +118,7 @@ const EditRedemption = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button positive onClick={submit}>提交</Button>
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
@ -17,11 +17,13 @@ const EditToken = () => {
|
|||||||
};
|
};
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
||||||
|
const navigate = useNavigate();
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate("/token");
|
||||||
|
}
|
||||||
const setExpiredTime = (month, day, hour, minute) => {
|
const setExpiredTime = (month, day, hour, minute) => {
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
let timestamp = now.getTime() / 1000;
|
let timestamp = now.getTime() / 1000;
|
||||||
@ -83,7 +85,7 @@ const EditToken = () => {
|
|||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
showSuccess('令牌更新成功!');
|
showSuccess('令牌更新成功!');
|
||||||
} else {
|
} else {
|
||||||
showSuccess('令牌创建成功!');
|
showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!');
|
||||||
setInputs(originInputs);
|
setInputs(originInputs);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -150,8 +152,9 @@ const EditToken = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button type={'button'} onClick={() => {
|
||||||
setUnlimitedQuota();
|
setUnlimitedQuota();
|
||||||
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
}}>{unlimited_quota ? '取消无限额度' : '设为无限额度'}</Button>
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button floated='right' positive onClick={submit}>提交</Button>
|
||||||
|
<Button floated='right' onClick={handleCancel}>取消</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||||
|
|
||||||
@ -36,7 +36,10 @@ const EditUser = () => {
|
|||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate("/setting");
|
||||||
|
}
|
||||||
const loadUser = async () => {
|
const loadUser = async () => {
|
||||||
let res = undefined;
|
let res = undefined;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@ -176,6 +179,7 @@ const EditUser = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
<Button positive onClick={submit}>提交</Button>
|
<Button positive onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
Loading…
Reference in New Issue
Block a user