mirror of
https://github.com/songquanpeng/one-api.git
synced 2026-01-12 18:05:57 +08:00
Compare commits
16 Commits
patch/prox
...
de5faa145f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de5faa145f | ||
|
|
f9774698e9 | ||
|
|
2af6f6a166 | ||
|
|
04bb3ef392 | ||
|
|
b4bfa418a8 | ||
|
|
e7e99e558a | ||
|
|
402fcf7f79 | ||
|
|
36039e329e | ||
|
|
c936198ac8 | ||
|
|
296ab013b8 | ||
|
|
5f03c856b4 | ||
|
|
39383e5532 | ||
|
|
ade00eed1c | ||
|
|
66d31102d3 | ||
|
|
1aba1e4ac3 | ||
|
|
b86a3acd37 |
61
.github/workflows/docker-image-amd64.yml
vendored
61
.github/workflows/docker-image-amd64.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Publish Docker image (amd64)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
description: 'reason'
|
||||
required: false
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check repository URL
|
||||
run: |
|
||||
REPO_URL=$(git config --get remote.origin.url)
|
||||
if [[ $REPO_URL == *"pro" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Save version info
|
||||
run: |
|
||||
git describe --tags > VERSION
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
justsong/one-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Publish Docker image (amd64, English)
|
||||
name: Publish Docker image (English)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -34,6 +34,13 @@ jobs:
|
||||
- name: Translate
|
||||
run: |
|
||||
python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -51,6 +58,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,10 +1,9 @@
|
||||
name: Publish Docker image (arm64)
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- '!*-alpha*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16 as builder
|
||||
FROM --platform=$BUILDPLATFORM node:16 AS builder
|
||||
|
||||
WORKDIR /web
|
||||
COPY ./VERSION .
|
||||
|
||||
@@ -89,6 +89,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
||||
+ [x] [DeepL](https://www.deepl.com/)
|
||||
+ [x] [together.ai](https://www.together.ai/)
|
||||
+ [x] [novita.ai](https://www.novita.ai/)
|
||||
+ [x] [硅基流动 SiliconCloud](https://siliconflow.cn/siliconcloud)
|
||||
2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。
|
||||
3. 支持通过**负载均衡**的方式访问多个渠道。
|
||||
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||
@@ -251,9 +252,9 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
|
||||
#### 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 支持的模型名称。
|
||||
根据[文档](https://qchatgpt.rockchin.top)完成部署后,在 `data/provider.json`设置`requester.openai-chat-completions.base-url`为 One API 实例地址,并填写 API Key 到 `keys.openai` 组中,设置 `model` 为要使用的模型名称。
|
||||
|
||||
可安装 [Switcher 插件](https://github.com/RockChinQ/Switcher)在运行时切换所使用的模型。
|
||||
运行期间可以通过`!model`命令查看、切换可用模型。
|
||||
|
||||
### 部署到第三方平台
|
||||
<details>
|
||||
|
||||
@@ -99,6 +99,7 @@ var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||
var RequestInterval = time.Duration(requestInterval) * time.Second
|
||||
|
||||
var SyncFrequency = env.Int("SYNC_FREQUENCY", 10*60) // unit is second
|
||||
var TimerFrequency = env.Int("TIMER_FREQUENCY", 24) // unit is hour
|
||||
|
||||
var BatchUpdateEnabled = false
|
||||
var BatchUpdateInterval = env.Int("BATCH_UPDATE_INTERVAL", 5)
|
||||
|
||||
3
go.mod
3
go.mod
@@ -55,6 +55,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-co-op/gocron v1.37.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -87,6 +88,7 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
@@ -96,6 +98,7 @@ require (
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
|
||||
22
go.sum
22
go.sum
@@ -31,6 +31,7 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
@@ -67,6 +68,8 @@ github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0Nglqm
|
||||
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -116,6 +119,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
@@ -156,7 +160,12 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
@@ -177,6 +186,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||
@@ -184,7 +194,12 @@ github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYde
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
@@ -198,6 +213,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -217,6 +233,8 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
@@ -266,6 +284,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
|
||||
google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
@@ -296,7 +315,10 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
3
main.go
3
main.go
@@ -73,6 +73,8 @@ func main() {
|
||||
go model.SyncOptions(config.SyncFrequency)
|
||||
go model.SyncChannelCache(config.SyncFrequency)
|
||||
}
|
||||
go model.ScheduleCheckAndDowngrade()
|
||||
|
||||
if os.Getenv("CHANNEL_TEST_FREQUENCY") != "" {
|
||||
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY"))
|
||||
if err != nil {
|
||||
@@ -112,4 +114,5 @@ func main() {
|
||||
if err != nil {
|
||||
logger.FatalLog("failed to start HTTP server: " + err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -78,6 +80,21 @@ func InitOptionMap() {
|
||||
loadOptionsFromDatabase()
|
||||
}
|
||||
|
||||
func ScheduleCheckAndDowngrade() {
|
||||
s := gocron.NewScheduler(time.UTC)
|
||||
|
||||
// 设置每天0点执行
|
||||
_, err := s.Every(1).Day().At("00:00").Do(checkAndDowngradeUsers)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("创建调度任务失败: %v", err)
|
||||
}
|
||||
|
||||
// 开始调度
|
||||
s.StartBlocking()
|
||||
log.Printf("开始调度")
|
||||
}
|
||||
|
||||
func loadOptionsFromDatabase() {
|
||||
options, _ := AllOption()
|
||||
for _, option := range options {
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -47,6 +49,7 @@ type User struct {
|
||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
|
||||
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
|
||||
ExpirationDate int64 `json:"expiration_date" gorm:"bigint;default:0;column:expiration_date"` // Expiration date of the user's subscription or account.
|
||||
}
|
||||
|
||||
func GetMaxUserId() int {
|
||||
@@ -210,6 +213,25 @@ func (user *User) ValidateAndFill() (err error) {
|
||||
if !okay || user.Status != UserStatusEnabled {
|
||||
return errors.New("用户名或密码错误,或用户已被封禁")
|
||||
}
|
||||
// 校验用户是不是非default,如果是非default,判断到期时间如果过期了降级为default
|
||||
if !(user.ExpirationDate > 0 && user.Username == "root") {
|
||||
// 将时间戳转换为 time.Time 类型
|
||||
expirationTime := time.Unix(user.ExpirationDate, 0)
|
||||
// 获取当前时间
|
||||
currentTime := time.Now()
|
||||
|
||||
// 比较当前时间和到期时间
|
||||
if expirationTime.Before(currentTime) {
|
||||
// 降级为 default
|
||||
user.Group = "default"
|
||||
err := DB.Model(user).Updates(user).Error
|
||||
if err != nil {
|
||||
fmt.Printf("用户: %s, 降级为 default 时发生错误: %v\n", user.Username, err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("用户: %s, 特权组过期降为 default\n", user.Username)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -435,3 +457,48 @@ func GetUsernameById(id int) (username string) {
|
||||
DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username)
|
||||
return username
|
||||
}
|
||||
|
||||
func checkAndDowngradeUsers() {
|
||||
// 获取昨天的时间戳
|
||||
yesterdayTimestamp := time.Now().AddDate(0, 0, -1).Unix()
|
||||
|
||||
// 获取需要降级的用户ID列表
|
||||
var userList []int
|
||||
query := DB.Model(&User{}).
|
||||
Where("`Group` != ?", "default").
|
||||
Where("`username` != ?", "root").
|
||||
Where("`expiration_date` > 0").
|
||||
Where("`expiration_date` <= ?", yesterdayTimestamp).
|
||||
Select("id").
|
||||
Find(&userList)
|
||||
|
||||
// 处理查询错误
|
||||
if query.Error != nil {
|
||||
log.Printf("查询用户列表失败: %v", query.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有用户需要降级,直接返回
|
||||
if len(userList) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 批量降级用户
|
||||
updateQuery := DB.Model(&User{}).Where("id IN ?", userList).Update("Group", "default")
|
||||
|
||||
// 处理更新错误
|
||||
if updateQuery.Error != nil {
|
||||
log.Printf("批量更新用户分组失败: %v", updateQuery.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 删除已过期用户的Redis缓存
|
||||
if common.RedisEnabled {
|
||||
for _, userId := range userList {
|
||||
err := common.RedisSet(fmt.Sprintf("user_group:%d", userId), "default", time.Duration(UserId2GroupCacheSeconds)*time.Second)
|
||||
if err != nil {
|
||||
log.Printf("更新用户: %d, 权益缓存失败, Error: %v", userId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cloudflare
|
||||
|
||||
var ModelList = []string{
|
||||
"@cf/meta/llama-3.1-8b-instruct",
|
||||
"@cf/meta/llama-2-7b-chat-fp16",
|
||||
"@cf/meta/llama-2-7b-chat-int8",
|
||||
"@cf/mistral/mistral-7b-instruct-v0.1",
|
||||
|
||||
@@ -7,8 +7,12 @@ import (
|
||||
)
|
||||
|
||||
func GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
if meta.Mode == relaymode.ChatCompletions {
|
||||
switch meta.Mode {
|
||||
case relaymode.ChatCompletions:
|
||||
return fmt.Sprintf("%s/api/v3/chat/completions", meta.BaseURL), nil
|
||||
case relaymode.Embeddings:
|
||||
return fmt.Sprintf("%s/api/v3/embeddings", meta.BaseURL), nil
|
||||
default:
|
||||
}
|
||||
return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,5 @@ package gemini
|
||||
// https://ai.google.dev/models/gemini
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-pro", "gemini-1.0-pro-001", "gemini-1.5-pro",
|
||||
"gemini-pro-vision", "gemini-1.0-pro-vision-001", "embedding-001", "text-embedding-004",
|
||||
"gemini-pro", "gemini-1.0-pro", "gemini-1.5-flash", "gemini-1.5-pro", "text-embedding-004", "aqa",
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@ package groq
|
||||
|
||||
var ModelList = []string{
|
||||
"gemma-7b-it",
|
||||
"llama2-7b-2048",
|
||||
"llama2-70b-4096",
|
||||
"mixtral-8x7b-32768",
|
||||
"llama3-8b-8192",
|
||||
"llama3-70b-8192",
|
||||
"gemma2-9b-it",
|
||||
"llama-3.1-405b-reasoning",
|
||||
"llama-3.1-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"llama3-groq-70b-8192-tool-use-preview",
|
||||
"llama3-groq-8b-8192-tool-use-preview",
|
||||
"whisper-large-v3",
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
// https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||
fullRequestURL := fmt.Sprintf("%s/api/chat", meta.BaseURL)
|
||||
if meta.Mode == relaymode.Embeddings {
|
||||
fullRequestURL = fmt.Sprintf("%s/api/embeddings", meta.BaseURL)
|
||||
fullRequestURL = fmt.Sprintf("%s/api/embed", meta.BaseURL)
|
||||
}
|
||||
return fullRequestURL, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest {
|
||||
TopP: request.TopP,
|
||||
FrequencyPenalty: request.FrequencyPenalty,
|
||||
PresencePenalty: request.PresencePenalty,
|
||||
NumPredict: request.MaxTokens,
|
||||
NumCtx: request.NumCtx,
|
||||
},
|
||||
Stream: request.Stream,
|
||||
}
|
||||
@@ -118,8 +120,10 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC
|
||||
common.SetEventStreamHeaders(c)
|
||||
|
||||
for scanner.Scan() {
|
||||
data := strings.TrimPrefix(scanner.Text(), "}")
|
||||
data = data + "}"
|
||||
data := scanner.Text()
|
||||
if strings.HasPrefix(data, "}") {
|
||||
data = strings.TrimPrefix(data, "}") + "}"
|
||||
}
|
||||
|
||||
var ollamaResponse ChatResponse
|
||||
err := json.Unmarshal([]byte(data), &ollamaResponse)
|
||||
@@ -157,8 +161,15 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC
|
||||
|
||||
func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest {
|
||||
return &EmbeddingRequest{
|
||||
Model: request.Model,
|
||||
Prompt: strings.Join(request.ParseInput(), " "),
|
||||
Model: request.Model,
|
||||
Input: request.ParseInput(),
|
||||
Options: &Options{
|
||||
Seed: int(request.Seed),
|
||||
Temperature: request.Temperature,
|
||||
TopP: request.TopP,
|
||||
FrequencyPenalty: request.FrequencyPenalty,
|
||||
PresencePenalty: request.PresencePenalty,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,15 +212,17 @@ func embeddingResponseOllama2OpenAI(response *EmbeddingResponse) *openai.Embeddi
|
||||
openAIEmbeddingResponse := openai.EmbeddingResponse{
|
||||
Object: "list",
|
||||
Data: make([]openai.EmbeddingResponseItem, 0, 1),
|
||||
Model: "text-embedding-v1",
|
||||
Model: response.Model,
|
||||
Usage: model.Usage{TotalTokens: 0},
|
||||
}
|
||||
|
||||
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{
|
||||
Object: `embedding`,
|
||||
Index: 0,
|
||||
Embedding: response.Embedding,
|
||||
})
|
||||
for i, embedding := range response.Embeddings {
|
||||
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{
|
||||
Object: `embedding`,
|
||||
Index: i,
|
||||
Embedding: embedding,
|
||||
})
|
||||
}
|
||||
return &openAIEmbeddingResponse
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ type Options struct {
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
NumPredict int `json:"num_predict,omitempty"`
|
||||
NumCtx int `json:"num_ctx,omitempty"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
@@ -37,11 +39,15 @@ type ChatResponse struct {
|
||||
}
|
||||
|
||||
type EmbeddingRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Model string `json:"model"`
|
||||
Input []string `json:"input"`
|
||||
// Truncate bool `json:"truncate,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
// KeepAlive string `json:"keep_alive,omitempty"`
|
||||
}
|
||||
|
||||
type EmbeddingResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Embedding []float64 `json:"embedding,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Embeddings [][]float64 `json:"embeddings"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/novita"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/stepfun"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/togetherai"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/siliconflow"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,7 @@ var CompatibleChannels = []int{
|
||||
channeltype.DeepSeek,
|
||||
channeltype.TogetherAI,
|
||||
channeltype.Novita,
|
||||
channeltype.SiliconFlow,
|
||||
}
|
||||
|
||||
func GetCompatibleChannelMeta(channelType int) (string, []string) {
|
||||
@@ -60,6 +62,8 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) {
|
||||
return "doubao", doubao.ModelList
|
||||
case channeltype.Novita:
|
||||
return "novita", novita.ModelList
|
||||
case channeltype.SiliconFlow:
|
||||
return "siliconflow", siliconflow.ModelList
|
||||
default:
|
||||
return "openai", ModelList
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ var ModelList = []string{
|
||||
"gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613",
|
||||
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||
"gpt-4o", "gpt-4o-2024-05-13",
|
||||
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
|
||||
"gpt-4-vision-preview",
|
||||
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
|
||||
"text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003",
|
||||
|
||||
@@ -110,7 +110,7 @@ func CountTokenMessages(messages []model.Message, model string) int {
|
||||
if imageUrl["detail"] != nil {
|
||||
detail = imageUrl["detail"].(string)
|
||||
}
|
||||
imageTokens, err := countImageTokens(url, detail)
|
||||
imageTokens, err := countImageTokens(url, detail, model)
|
||||
if err != nil {
|
||||
logger.SysError("error counting image tokens: " + err.Error())
|
||||
} else {
|
||||
@@ -134,11 +134,15 @@ const (
|
||||
lowDetailCost = 85
|
||||
highDetailCostPerTile = 170
|
||||
additionalCost = 85
|
||||
// gpt-4o-mini cost higher than other model
|
||||
gpt4oMiniLowDetailCost = 2833
|
||||
gpt4oMiniHighDetailCost = 5667
|
||||
gpt4oMiniAdditionalCost = 2833
|
||||
)
|
||||
|
||||
// https://platform.openai.com/docs/guides/vision/calculating-costs
|
||||
// https://github.com/openai/openai-cookbook/blob/05e3f9be4c7a2ae7ecf029a7c32065b024730ebe/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
func countImageTokens(url string, detail string) (_ int, err error) {
|
||||
func countImageTokens(url string, detail string, model string) (_ int, err error) {
|
||||
var fetchSize = true
|
||||
var width, height int
|
||||
// Reference: https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding
|
||||
@@ -172,6 +176,9 @@ func countImageTokens(url string, detail string) (_ int, err error) {
|
||||
}
|
||||
switch detail {
|
||||
case "low":
|
||||
if strings.HasPrefix(model, "gpt-4o-mini") {
|
||||
return gpt4oMiniLowDetailCost, nil
|
||||
}
|
||||
return lowDetailCost, nil
|
||||
case "high":
|
||||
if fetchSize {
|
||||
@@ -191,6 +198,9 @@ func countImageTokens(url string, detail string) (_ int, err error) {
|
||||
height = int(float64(height) * ratio)
|
||||
}
|
||||
numSquares := int(math.Ceil(float64(width)/512) * math.Ceil(float64(height)/512))
|
||||
if strings.HasPrefix(model, "gpt-4o-mini") {
|
||||
return numSquares*gpt4oMiniHighDetailCost + gpt4oMiniAdditionalCost, nil
|
||||
}
|
||||
result := numSquares*highDetailCostPerTile + additionalCost
|
||||
return result, nil
|
||||
default:
|
||||
|
||||
36
relay/adaptor/siliconflow/constants.go
Normal file
36
relay/adaptor/siliconflow/constants.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package siliconflow
|
||||
|
||||
// https://docs.siliconflow.cn/docs/getting-started
|
||||
|
||||
var ModelList = []string{
|
||||
"deepseek-ai/deepseek-llm-67b-chat",
|
||||
"Qwen/Qwen1.5-14B-Chat",
|
||||
"Qwen/Qwen1.5-7B-Chat",
|
||||
"Qwen/Qwen1.5-110B-Chat",
|
||||
"Qwen/Qwen1.5-32B-Chat",
|
||||
"01-ai/Yi-1.5-6B-Chat",
|
||||
"01-ai/Yi-1.5-9B-Chat-16K",
|
||||
"01-ai/Yi-1.5-34B-Chat-16K",
|
||||
"THUDM/chatglm3-6b",
|
||||
"deepseek-ai/DeepSeek-V2-Chat",
|
||||
"THUDM/glm-4-9b-chat",
|
||||
"Qwen/Qwen2-72B-Instruct",
|
||||
"Qwen/Qwen2-7B-Instruct",
|
||||
"Qwen/Qwen2-57B-A14B-Instruct",
|
||||
"deepseek-ai/DeepSeek-Coder-V2-Instruct",
|
||||
"Qwen/Qwen2-1.5B-Instruct",
|
||||
"internlm/internlm2_5-7b-chat",
|
||||
"BAAI/bge-large-en-v1.5",
|
||||
"BAAI/bge-large-zh-v1.5",
|
||||
"Pro/Qwen/Qwen2-7B-Instruct",
|
||||
"Pro/Qwen/Qwen2-1.5B-Instruct",
|
||||
"Pro/Qwen/Qwen1.5-7B-Chat",
|
||||
"Pro/THUDM/glm-4-9b-chat",
|
||||
"Pro/THUDM/chatglm3-6b",
|
||||
"Pro/01-ai/Yi-1.5-9B-Chat-16K",
|
||||
"Pro/01-ai/Yi-1.5-6B-Chat",
|
||||
"Pro/google/gemma-2-9b-it",
|
||||
"Pro/internlm/internlm2_5-7b-chat",
|
||||
"Pro/meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
"Pro/mistralai/Mistral-7B-Instruct-v0.2",
|
||||
}
|
||||
@@ -28,15 +28,17 @@ var ModelRatio = map[string]float64{
|
||||
"gpt-4-32k": 30,
|
||||
"gpt-4-32k-0314": 30,
|
||||
"gpt-4-32k-0613": 30,
|
||||
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
|
||||
"gpt-4o": 2.5, // $0.005 / 1K tokens
|
||||
"gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens
|
||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-3.5-turbo": 0.25, // $0.0005 / 1K tokens
|
||||
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
|
||||
"gpt-4o": 2.5, // $0.005 / 1K tokens
|
||||
"gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens
|
||||
"gpt-4o-mini": 0.075, // $0.00015 / 1K tokens
|
||||
"gpt-4o-mini-2024-07-18": 0.075, // $0.00015 / 1K tokens
|
||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-3.5-turbo": 0.25, // $0.0005 / 1K tokens
|
||||
"gpt-3.5-turbo-0301": 0.75,
|
||||
"gpt-3.5-turbo-0613": 0.75,
|
||||
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
|
||||
@@ -96,12 +98,11 @@ var ModelRatio = map[string]float64{
|
||||
"bge-large-en": 0.002 * RMB,
|
||||
"tao-8k": 0.002 * RMB,
|
||||
// https://ai.google.dev/pricing
|
||||
"PaLM-2": 1,
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro-vision-001": 1,
|
||||
"gemini-1.0-pro-001": 1,
|
||||
"gemini-1.5-pro": 1,
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro": 1,
|
||||
"gemini-1.5-flash": 1,
|
||||
"gemini-1.5-pro": 1,
|
||||
"aqa": 1,
|
||||
// https://open.bigmodel.cn/pricing
|
||||
"glm-4": 0.1 * RMB,
|
||||
"glm-4v": 0.1 * RMB,
|
||||
@@ -156,12 +157,16 @@ var ModelRatio = map[string]float64{
|
||||
"mistral-large-latest": 8.0 / 1000 * USD,
|
||||
"mistral-embed": 0.1 / 1000 * USD,
|
||||
// https://wow.groq.com/#:~:text=inquiries%C2%A0here.-,Model,-Current%20Speed
|
||||
"llama3-70b-8192": 0.59 / 1000 * USD,
|
||||
"mixtral-8x7b-32768": 0.27 / 1000 * USD,
|
||||
"llama3-8b-8192": 0.05 / 1000 * USD,
|
||||
"gemma-7b-it": 0.1 / 1000 * USD,
|
||||
"llama2-70b-4096": 0.64 / 1000 * USD,
|
||||
"llama2-7b-2048": 0.1 / 1000 * USD,
|
||||
"gemma-7b-it": 0.07 / 1000000 * USD,
|
||||
"mixtral-8x7b-32768": 0.24 / 1000000 * USD,
|
||||
"llama3-8b-8192": 0.05 / 1000000 * USD,
|
||||
"llama3-70b-8192": 0.59 / 1000000 * USD,
|
||||
"gemma2-9b-it": 0.20 / 1000000 * USD,
|
||||
"llama-3.1-405b-reasoning": 0.89 / 1000000 * USD,
|
||||
"llama-3.1-70b-versatile": 0.59 / 1000000 * USD,
|
||||
"llama-3.1-8b-instant": 0.05 / 1000000 * USD,
|
||||
"llama3-groq-70b-8192-tool-use-preview": 0.89 / 1000000 * USD,
|
||||
"llama3-groq-8b-8192-tool-use-preview": 0.19 / 1000000 * USD,
|
||||
// https://platform.lingyiwanwu.com/docs#-计费单元
|
||||
"yi-34b-chat-0205": 2.5 / 1000 * RMB,
|
||||
"yi-34b-chat-200k": 12.0 / 1000 * RMB,
|
||||
@@ -308,6 +313,9 @@ func GetCompletionRatio(name string, channelType int) float64 {
|
||||
return 4.0 / 3.0
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4") {
|
||||
if strings.HasPrefix(name, "gpt-4o-mini") {
|
||||
return 4
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4-turbo") ||
|
||||
strings.HasPrefix(name, "gpt-4o") ||
|
||||
strings.HasSuffix(name, "preview") {
|
||||
|
||||
@@ -45,5 +45,6 @@ const (
|
||||
Novita
|
||||
VertextAI
|
||||
Proxy
|
||||
SiliconFlow
|
||||
Dummy
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ var ChannelBaseURLs = []string{
|
||||
"https://api.novita.ai/v3/openai", // 41
|
||||
"", // 42
|
||||
"", // 43
|
||||
"https://api.siliconflow.cn", // 44
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/relay"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
"github.com/songquanpeng/one-api/relay/apitype"
|
||||
"github.com/songquanpeng/one-api/relay/billing"
|
||||
@@ -31,9 +32,8 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
||||
meta.IsStream = textRequest.Stream
|
||||
|
||||
// map model name
|
||||
var isModelMapped bool
|
||||
meta.OriginModelName = textRequest.Model
|
||||
textRequest.Model, isModelMapped = getMappedModelName(textRequest.Model, meta.ModelMapping)
|
||||
textRequest.Model, _ = getMappedModelName(textRequest.Model, meta.ModelMapping)
|
||||
meta.ActualModelName = textRequest.Model
|
||||
// get model ratio & group ratio
|
||||
modelRatio := billingratio.GetModelRatio(textRequest.Model, meta.ChannelType)
|
||||
@@ -55,30 +55,9 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
||||
adaptor.Init(meta)
|
||||
|
||||
// get request body
|
||||
var requestBody io.Reader
|
||||
if meta.APIType == apitype.OpenAI {
|
||||
// no need to convert request for openai
|
||||
shouldResetRequestBody := isModelMapped || meta.ChannelType == channeltype.Baichuan // frequency_penalty 0 is not acceptable for baichuan
|
||||
if shouldResetRequestBody {
|
||||
jsonStr, err := json.Marshal(textRequest)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "json_marshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
requestBody = bytes.NewBuffer(jsonStr)
|
||||
} else {
|
||||
requestBody = c.Request.Body
|
||||
}
|
||||
} else {
|
||||
convertedRequest, err := adaptor.ConvertRequest(c, meta.Mode, textRequest)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "convert_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
jsonData, err := json.Marshal(convertedRequest)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "json_marshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
logger.Debugf(ctx, "converted request: \n%s", string(jsonData))
|
||||
requestBody = bytes.NewBuffer(jsonData)
|
||||
requestBody, err := getRequestBody(c, meta, textRequest, adaptor)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "convert_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// do request
|
||||
@@ -103,3 +82,26 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
||||
go postConsumeQuota(ctx, usage, meta, textRequest, ratio, preConsumedQuota, modelRatio, groupRatio)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRequestBody(c *gin.Context, meta *meta.Meta, textRequest *model.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) {
|
||||
if meta.APIType == apitype.OpenAI && meta.OriginModelName == meta.ActualModelName && meta.ChannelType != channeltype.Baichuan {
|
||||
// no need to convert request for openai
|
||||
return c.Request.Body, nil
|
||||
}
|
||||
|
||||
// get request body
|
||||
var requestBody io.Reader
|
||||
convertedRequest, err := adaptor.ConvertRequest(c, meta.Mode, textRequest)
|
||||
if err != nil {
|
||||
logger.Debugf(c.Request.Context(), "converted request failed: %s\n", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
jsonData, err := json.Marshal(convertedRequest)
|
||||
if err != nil {
|
||||
logger.Debugf(c.Request.Context(), "converted request json_marshal_failed: %s\n", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf(c.Request.Context(), "converted request: \n%s", string(jsonData))
|
||||
requestBody = bytes.NewBuffer(jsonData)
|
||||
return requestBody, nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type GeneralOpenAIRequest struct {
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
NumCtx int `json:"num_ctx,omitempty"`
|
||||
}
|
||||
|
||||
func (r GeneralOpenAIRequest) ParseInput() []string {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const CHANNEL_OPTIONS = [
|
||||
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
|
||||
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
|
||||
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
|
||||
{ key: 44, text: 'SiliconFlow', value: 44, color: 'blue' },
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"apexcharts": "3.35.3",
|
||||
"axios": "^0.27.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^6.3.16",
|
||||
@@ -27,6 +28,7 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-apexcharts": "1.4.0",
|
||||
"react-datepicker": "^7.3.0",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
|
||||
@@ -173,6 +173,12 @@ export const CHANNEL_OPTIONS = {
|
||||
value: 43,
|
||||
color: 'primary'
|
||||
},
|
||||
44: {
|
||||
key: 44,
|
||||
text: 'SiliconFlow',
|
||||
value: 44,
|
||||
color: 'primary'
|
||||
},
|
||||
41: {
|
||||
key: 41,
|
||||
text: 'Novita',
|
||||
|
||||
@@ -2,7 +2,9 @@ import PropTypes from 'prop-types';
|
||||
import * as Yup from 'yup';
|
||||
import { Formik } from 'formik';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -17,7 +19,11 @@ import {
|
||||
Select,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
FormHelperText
|
||||
FormHelperText,
|
||||
TextField,
|
||||
Typography,
|
||||
Switch,
|
||||
FormControlLabel
|
||||
} from '@mui/material';
|
||||
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
@@ -44,6 +50,17 @@ const validationSchema = Yup.object().shape({
|
||||
is: false,
|
||||
then: Yup.number().min(0, '额度 不能小于 0'),
|
||||
otherwise: Yup.number()
|
||||
}),
|
||||
expiration_date: Yup.mixed().when('group', {
|
||||
is: (group) => group !== 'default',
|
||||
then: Yup.mixed().test(
|
||||
'expiration_date-required',
|
||||
'到期时间 不能为空',
|
||||
function (value) {
|
||||
const { expiration_date } = this.parent;
|
||||
return expiration_date === -1 || !!expiration_date;
|
||||
}
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -53,7 +70,8 @@ const originInputs = {
|
||||
display_name: '',
|
||||
password: '',
|
||||
group: 'default',
|
||||
quota: 0
|
||||
quota: 0,
|
||||
expiration_date: null
|
||||
};
|
||||
|
||||
const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
@@ -65,6 +83,12 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
// 将到期时间转换为 Unix 时间戳
|
||||
if (values.expiration_date && values.expiration_date !== -1) {
|
||||
const date = new Date(values.expiration_date);
|
||||
values.expiration_date = Math.floor(date.getTime() / 1000); // 转换为秒级的 Unix 时间戳
|
||||
}
|
||||
|
||||
let res;
|
||||
if (values.is_edit) {
|
||||
res = await API.put(`/api/user/`, { ...values, id: parseInt(userId) });
|
||||
@@ -95,16 +119,23 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const loadUser = async () => {
|
||||
const loadUser = useCallback(async () => {
|
||||
let res = await API.get(`/api/user/${userId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.is_edit = true;
|
||||
|
||||
// 将 Unix 时间戳转换为日期字符串
|
||||
if (data.expiration_date && data.expiration_date !== -1) {
|
||||
const date = new Date(data.expiration_date * 1000); // 转换为毫秒级的时间戳
|
||||
data.expiration_date = format(date, 'yyyy-MM-dd'); // 格式化为 date 格式
|
||||
}
|
||||
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
@@ -122,159 +153,203 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
}
|
||||
}, [userId]);
|
||||
}, [userId, loadUser]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{userId ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-username-label">用户名</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-username-label"
|
||||
label="用户名"
|
||||
type="text"
|
||||
value={values.username}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'username' }}
|
||||
aria-describedby="helper-text-channel-username-label"
|
||||
/>
|
||||
{touched.username && errors.username && (
|
||||
<FormHelperText error id="helper-tex-channel-username-label">
|
||||
{errors.username}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.display_name && errors.display_name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-display_name-label">显示名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-display_name-label"
|
||||
label="显示名称"
|
||||
type="text"
|
||||
value={values.display_name}
|
||||
name="display_name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'display_name' }}
|
||||
aria-describedby="helper-text-channel-display_name-label"
|
||||
/>
|
||||
{touched.display_name && errors.display_name && (
|
||||
<FormHelperText error id="helper-tex-channel-display_name-label">
|
||||
{errors.display_name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-password-label">密码</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-password-label"
|
||||
label="密码"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'password' }}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
size="large"
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby="helper-text-channel-password-label"
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<FormHelperText error id="helper-tex-channel-password-label">
|
||||
{errors.password}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{values.is_edit && (
|
||||
<>
|
||||
<FormControl fullWidth error={Boolean(touched.quota && errors.quota)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-quota-label">额度</InputLabel>
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{userId ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, setFieldValue, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-username-label">用户名</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.quota}
|
||||
name="quota"
|
||||
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.quota)}</InputAdornment>}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
id="channel-username-label"
|
||||
label="用户名"
|
||||
type="text"
|
||||
value={values.username}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'username' }}
|
||||
aria-describedby="helper-text-channel-username-label"
|
||||
/>
|
||||
|
||||
{touched.quota && errors.quota && (
|
||||
<FormHelperText error id="helper-tex-channel-quota-label">
|
||||
{errors.quota}
|
||||
</FormHelperText>
|
||||
{touched.username && errors.username && (
|
||||
<FormHelperText error id="helper-tex-channel-username-label">
|
||||
{errors.username}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.group && errors.group)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-group-label">分组</InputLabel>
|
||||
<Select
|
||||
id="channel-group-label"
|
||||
label="分组"
|
||||
value={values.group}
|
||||
name="group"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
<FormControl fullWidth error={Boolean(touched.display_name && errors.display_name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-display_name-label">显示名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-display_name-label"
|
||||
label="显示名称"
|
||||
type="text"
|
||||
value={values.display_name}
|
||||
name="display_name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'display_name' }}
|
||||
aria-describedby="helper-text-channel-display_name-label"
|
||||
/>
|
||||
{touched.display_name && errors.display_name && (
|
||||
<FormHelperText error id="helper-tex-channel-display_name-label">
|
||||
{errors.display_name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-password-label">密码</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-password-label"
|
||||
label="密码"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'password' }}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
size="large"
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupOptions.map((option) => {
|
||||
return (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{touched.group && errors.group && (
|
||||
<FormHelperText error id="helper-tex-channel-group-label">
|
||||
{errors.group}
|
||||
</FormHelperText>
|
||||
aria-describedby="helper-text-channel-password-label"
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<FormHelperText error id="helper-tex-channel-password-label">
|
||||
{errors.password}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{values.is_edit && (
|
||||
<>
|
||||
<FormControl fullWidth error={Boolean(touched.quota && errors.quota)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-quota-label">额度</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.quota}
|
||||
name="quota"
|
||||
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.quota)}</InputAdornment>}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
/>
|
||||
|
||||
{touched.quota && errors.quota && (
|
||||
<FormHelperText error id="helper-tex-channel-quota-label">
|
||||
{errors.quota}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.group && errors.group)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-group-label">分组</InputLabel>
|
||||
<Select
|
||||
id="channel-group-label"
|
||||
label="分组"
|
||||
value={values.group}
|
||||
name="group"
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => {
|
||||
handleChange(e);
|
||||
if (e.target.value === 'default') {
|
||||
setFieldValue('expiration_date', null);
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupOptions.map((option) => {
|
||||
return (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{touched.group && errors.group && (
|
||||
<FormHelperText error id="helper-tex-channel-group-label">
|
||||
{errors.group}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
{values.group !== 'default' && (
|
||||
<FormControl fullWidth error={Boolean(touched.expiration_date && errors.expiration_date)} sx={{ ...theme.typography.otherInput }}>
|
||||
<TextField
|
||||
id="channel-expiration_date-label"
|
||||
label="到期时间"
|
||||
type="date" // 修改为 date
|
||||
value={values.expiration_date}
|
||||
name="expiration_date"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
inputProps={{
|
||||
max: '9999-12-31' // 设置最大日期
|
||||
}}
|
||||
aria-describedby="helper-text-channel-expiration_date-label"
|
||||
disabled={values.expiration_date === -1}
|
||||
/>
|
||||
{touched.expiration_date && errors.expiration_date && (
|
||||
<FormHelperText error id="helper-tex-channel-expiration_date-label">
|
||||
{errors.expiration_date}
|
||||
</FormHelperText>
|
||||
)}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={values.expiration_date === -1}
|
||||
onChange={(e) => setFieldValue('expiration_date', e.target.checked ? -1 : '')}
|
||||
name="permanent"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="永不过期"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -285,4 +360,4 @@ EditModal.propTypes = {
|
||||
userId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const CHANNEL_OPTIONS = [
|
||||
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
|
||||
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
|
||||
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
|
||||
{ key: 44, text: 'SiliconFlow', value: 44, color: 'blue' },
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
|
||||
Reference in New Issue
Block a user