diff --git a/.github/workflows/docker-image-arm64.yml b/.github/workflows/docker-image-arm64.yml index 2015983649e6ec1ca756e940d1d04eb9cf630f1c..44aec8072f6e601f9173e4021467dfc6c9c6b494 100644 --- a/.github/workflows/docker-image-arm64.yml +++ b/.github/workflows/docker-image-arm64.yml @@ -52,9 +52,6 @@ jobs: calciumion/new-api ghcr.io/${{ github.repository }} - - name: Update Go dependencies - run: go get -u ./... - - name: Build and push Docker images uses: docker/build-push-action@v3 with: diff --git a/BT.md b/BT.md index b4ea5b2fc04e5ba3746be6eaff12fd83321a92e7..e57cdab792db4d347622bbe9d915e8caf1fc7a42 100644 --- a/BT.md +++ b/BT.md @@ -1,3 +1,3 @@ -密钥为环境变量SESSION_SECRET - -![8285bba413e770fe9620f1bf9b40d44e](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0) +密钥为环境变量SESSION_SECRET + +![8285bba413e770fe9620f1bf9b40d44e](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0) diff --git a/Dockerfile b/Dockerfile index 81d351742da6a01f6f559b17f829e85303e0f196..214ceaa3d5f2e897fd65e212af4aad4c08c75aac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:latest as builder +FROM oven/bun:latest AS builder WORKDIR /build COPY web/package.json . @@ -7,25 +7,29 @@ COPY ./web . COPY ./VERSION . RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build -FROM golang AS builder2 +FROM golang:alpine AS builder2 -ENV GO111MODULE=on -ENV CGO_ENABLED=1 -ENV GOOS=linux +ENV GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux WORKDIR /build + ADD go.mod go.sum ./ RUN go mod download + COPY . . COPY --from=builder /build/dist ./web/dist -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)'" -o one-api FROM alpine -RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata ffmpeg && update-ca-certificates +RUN apk update \ + && apk upgrade \ + && apk add --no-cache ca-certificates tzdata ffmpeg \ + && update-ca-certificates COPY --from=builder2 /build/one-api / -RUN mkdir -p /data/logs && chmod 777 /data/logs # 创建 logs 目录并设置 777 权限 EXPOSE 3000 WORKDIR /data -ENTRYPOINT ["/one-api"] \ No newline at end of file +ENTRYPOINT ["/one-api"] diff --git a/README.en.md b/README.en.md index dc6696a048b1c40c33190b979e5bf106448c5ba3..feb4b0bb46f9167aa633168d37ccade2e7f29380 100644 --- a/README.en.md +++ b/README.en.md @@ -91,13 +91,10 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model - `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, if not specified in channel settings, use this version, default `2024-12-01-preview` ## Deployment + > [!TIP] > Latest Docker image: `calciumion/new-api:latest` -> Default account: root, password: 123456 -> Update command: -> ``` -> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR -> ``` +> Default account: root, password: 123456 ### Multi-Server Deployment - Must set `SESSION_SECRET` environment variable, otherwise login state will not be consistent across multiple servers. @@ -107,26 +104,58 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model - Local database (default): SQLite (Docker deployment must mount `/data` directory) - Remote database: MySQL >= 5.7.8, PgSQL >= 9.6 +### Deployment with BT Panel +Install BT Panel (**version 9.2.0** or above) from [BT Panel Official Website](https://www.bt.cn/new/download.html), choose the stable version script to download and install. +After installation, log in to BT Panel and click Docker in the menu bar. First-time access will prompt to install Docker service. Click Install Now and follow the prompts to complete installation. +After installation, find **New-API** in the app store, click install, configure basic options to complete installation. +[Pictorial Guide](BT.md) + ### Docker Deployment + ### Using Docker Compose (Recommended) ```shell # Clone project git clone https://github.com/Calcium-Ion/new-api.git cd new-api # Edit docker-compose.yml as needed +# nano docker-compose.yml +# vim docker-compose.yml # Start docker-compose up -d ``` +#### Update Version +```shell +docker-compose pull +docker-compose up -d +``` + ### Direct Docker Image Usage ```shell # SQLite deployment: docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest + # MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed # Example: docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest ``` +#### Update Version +```shell +# Pull the latest image +docker pull calciumion/new-api:latest +# Stop and remove the old container +docker stop new-api +docker rm new-api +# Run the new container with the same parameters as before +docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest +``` + +Alternatively, you can use Watchtower for automatic updates (not recommended, may cause database incompatibility): +```shell +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR +``` + ## Channel Retry Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**. First retry uses same priority, second retry uses next priority, and so on. diff --git a/README.md b/README.md index 2298258ee9098b3dd561deb7757c47f3e64eb1e4..cecefca6789dfa6b7fe0a9561487da7ae5e4a733 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,217 @@ ---- -title: Epa -emoji: 🐢 -colorFrom: pink -colorTo: purple -sdk: docker -pinned: false -app_port: 3000 ---- -THIS IS EPA. \ No newline at end of file +

+ 中文 | English +

+
+ +![new-api](/web/public/logo.png) + +# New API + + +🍥新一代大模型网关与AI资产管理系统 + +Calcium-Ion%2Fnew-api | Trendshift + +

+ + license + + + release + + + docker + + + docker + + + GoReportCard + +

+
+ +## 📝 项目说明 + +> [!NOTE] +> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发 + +> [!IMPORTANT] +> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 +> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。 +> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 + +## ✨ 主要特性 + +1. 🎨 全新的UI界面(部分界面还待更新) +2. 🌍 多语言支持(待完善) +3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md) +4. 💰 支持在线充值功能,可在系统设置中设置: + - [x] 易支付 +5. 🔍 支持用key查询使用额度: + - 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用 +6. 📑 分页支持选择每页显示数量 +7. 🔄 兼容原版One API的数据库,可直接使用原版数据库(one-api.db) +8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置 +9. ⚖️ 支持渠道**加权随机** +10. 📈 数据看板(控制台) +11. 🔒 可设置令牌能调用的模型 +12. 🤖 支持Telegram授权登录: + 1. 系统设置-配置登录注册-允许通过Telegram登录 + 2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain + 3. 选择你的bot,然后输入http(s)://你的网站地址/login + 4. Telegram Bot 名称是bot username 去掉@后的字符串 +13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md) +14. 🔄 支持Rerank模型,目前兼容Cohere和Jina,可接入Dify,[对接文档](Rerank.md) +15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API,支持Azure渠道 +16. 支持使用路由/chat2link 进入聊天界面 +17. 🧠 支持通过模型名称后缀设置 reasoning effort: + - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`) + - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`) + - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`) + +## 模型支持 +此版本额外支持以下模型: +1. 第三方模型 **gps** (gpt-4-gizmo-*) +2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md) +3. 自定义渠道,支持填入完整调用地址 +4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md) +5. Rerank模型,目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/),[对接文档](Rerank.md) +6. Dify + +您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。 + +## 比原版One API多出的配置 +- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。 +- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。 +- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。 +- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。 +- `GET_MEDIA_TOKEN`:是否统计图片token,默认为 `true`,关闭后将不再在本地计算图片token,可能会导致和上游计费不同,此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。 +- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`)情况下统计图片token,默认为 `true`。 +- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。 +- `GEMINI_MODEL_MAP`:Gemini模型指定版本(v1/v1beta),使用"模型:版本"指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta) +- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认为 `NONE`。 +- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认为 `16`,设置为 `-1` 则不限制。 +- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位 MB,默认为 `20`。 +- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容。 +- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,如果渠道设置中未指定API版本,则使用此版本,默认为 `2024-12-01-preview` +## 部署 + +> [!TIP] +> 最新版Docker镜像:`calciumion/new-api:latest` +> 默认账号root 密码123456 + +### 多机部署 +- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致。 +- 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取。 + +### 部署要求 +- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机) +- 远程数据库:MySQL 版本 >= 5.7.8,PgSQL 版本 >= 9.6 + +### 使用宝塔面板Docker功能部署 +安装宝塔面板 (**9.2.0版本**及以上),前往 [宝塔面板](https://www.bt.cn/new/download.html) 官网,选择正式版的脚本下载安装 +安装后登录宝塔面板,在菜单栏中点击 Docker ,首次进入会提示安装 Docker 服务,点击立即安装,按提示完成安装 +安装完成后在应用商店中找到 **New-API** ,点击安装,配置基本选项 即可完成安装 +[图文教程](BT.md) + +### 基于 Docker 进行部署 + +> [!TIP] +> 默认管理员账号root 密码123456 + +### 使用 Docker Compose 部署(推荐) +```shell +# 下载项目 +git clone https://github.com/Calcium-Ion/new-api.git +cd new-api +# 按需编辑 docker-compose.yml +# nano docker-compose.yml +# vim docker-compose.yml +# 启动 +docker-compose up -d +``` + +#### 更新版本 +```shell +docker-compose pull +docker-compose up -d +``` + +### 直接使用 Docker 镜像 +```shell +# 使用 SQLite 的部署命令: +docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest + +# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。 +# 例如: +docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest +``` + +#### 更新版本 +```shell +# 拉取最新镜像 +docker pull calciumion/new-api:latest +# 停止并删除旧容器 +docker stop new-api +docker rm new-api +# 使用相同参数运行新容器 +docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest +``` + +或者使用 Watchtower 自动更新(不推荐,可能会导致数据库不兼容): +```shell +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR +``` + +## 渠道重试 +渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。 +如果开启了重试功能,第一次重试使用同优先级,第二次重试使用下一个优先级,以此类推。 +### 缓存设置方法 +1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。 + + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` +2. `MEMORY_CACHE_ENABLED`:启用内存缓存(如果设置了`REDIS_CONN_STRING`,则无需手动设置),会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。 + + 例子:`MEMORY_CACHE_ENABLED=true` +### 为什么有的时候没有重试 +这些错误码不会重试:400,504,524 +### 我想让400也重试 +在`渠道->编辑`中,将`状态码复写`改为 +```json +{ + "400": "500" +} +``` +可以实现400错误转为500错误,从而重试 + +## Midjourney接口设置文档 +[对接文档](Midjourney.md) + +## Suno接口设置文档 +[对接文档](Suno.md) + +## 界面截图 +![image](https://github.com/user-attachments/assets/a0dcd349-5df8-4dc8-9acf-ca272b239919) + + +![image](https://github.com/user-attachments/assets/c7d0f7e1-729c-43e2-ac7c-2cb73b0afc8e) + +![image](https://github.com/user-attachments/assets/29f81de5-33fc-4fc5-a5ff-f9b54b653c7c) + +![image](https://github.com/user-attachments/assets/4fa53e18-d2c5-477a-9b26-b86e44c71e35) + +## 交流群 + + +## 相关项目 +- [One API](https://github.com/songquanpeng/one-api):原版项目 +- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持 +- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案 +- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度 + +其他基于New API的项目: +- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版,并支持Claude格式 +- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的闭源项目 + +## 🌟 Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date) diff --git a/VERSION b/VERSION index 41657fe06d31b099be11846a92b05c7660cb7afc..c302e31b8afc35c818fbba2f2d1a927f255a5861 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.4.6.11 \ No newline at end of file +v0.4.7.2.1 \ No newline at end of file diff --git a/common/constants.go b/common/constants.go index e2acf83bbc3fe09fe47e2ee7ed283ecc2d2ec086..f967d066e29cd04796272fc57cf53d2ea5d69c40 100644 --- a/common/constants.go +++ b/common/constants.go @@ -231,8 +231,10 @@ const ( ChannelTypeVertexAi = 41 ChannelTypeMistral = 42 ChannelTypeDeepSeek = 43 - - ChannelTypeDummy // this one is only for count, do not add any channel after this + ChannelTypeMokaAI = 44 + ChannelTypeVolcEngine = 45 + ChannelTypeBaiduV2 = 46 + ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -281,4 +283,7 @@ var ChannelBaseURLs = []string{ "", //41 "https://api.mistral.ai", //42 "https://api.deepseek.com", //43 + "https://api.moka.ai", //44 + "https://ark.cn-beijing.volces.com", //45 + "https://qianfan.baidubce.com", //46 } diff --git a/common/database.go b/common/database.go index ce7a9bc1f772f86dc4f045d4461914ff43f027b4..3c0a944b24093e12b5b6c0f79ce7603e51e83451 100644 --- a/common/database.go +++ b/common/database.go @@ -3,5 +3,6 @@ package common var UsingSQLite = false var UsingPostgreSQL = false var UsingMySQL = false +var UsingClickHouse = false var SQLitePath = "one-api.db?_busy_timeout=5000" diff --git a/controller/channel-test.go b/controller/channel-test.go index 8d791ea09f166e61ef5ac685fb5556a88e7f8ff5..7e74bec23dbbf2a050e363ace4020cd313f00909 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -41,9 +41,21 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + + requestPath := "/v1/chat/completions" + + // 先判断是否为 Embedding 模型 + if strings.Contains(strings.ToLower(testModel), "embedding") || + strings.HasPrefix(testModel, "m3e") || // m3e 系列模型 + strings.Contains(testModel, "bge-") || // bge 系列模型 + testModel == "text-embedding-v1" || + channel.Type == common.ChannelTypeMokaAI { // 其他 embedding 模型 + requestPath = "/v1/embeddings" // 修改请求路径 + } + c.Request = &http.Request{ Method: "POST", - URL: &url.URL{Path: "/v1/chat/completions"}, + URL: &url.URL{Path: requestPath}, // 使用动态路径 Body: nil, Header: make(http.Header), } @@ -55,7 +67,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr if len(channel.GetModels()) > 0 { testModel = channel.GetModels()[0] } else { - testModel = "gpt-3.5-turbo" + testModel = "gpt-4o-mini" } } } @@ -88,7 +100,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr request := buildTestRequest(testModel) meta.UpstreamModelName = testModel - common.SysLog(fmt.Sprintf("testing channel %d with model %s", channel.Id, testModel)) + common.SysLog(fmt.Sprintf("testing channel %d with model %s , meta %v ", channel.Id, testModel, meta)) adaptor.Init(meta) @@ -156,12 +168,21 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest { Model: "", // this will be set later Stream: false, } + + // 先判断是否为 Embedding 模型 + if strings.Contains(strings.ToLower(model), "embedding") || + strings.HasPrefix(model, "m3e") || // m3e 系列模型 + strings.Contains(model, "bge-") || // bge 系列模型 + model == "text-embedding-v1" { // 其他 embedding 模型 + // Embedding 请求 + testRequest.Input = []string{"hello world"} + return testRequest + } + // 并非Embedding 模型 if strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") { testRequest.MaxCompletionTokens = 10 - } else if strings.HasPrefix(model, "gemini-2.0-flash-thinking") { - testRequest.MaxTokens = 10 } else { - testRequest.MaxTokens = 1 + testRequest.MaxTokens = 10 } content, _ := json.Marshal("hi") testMessage := dto.Message{ diff --git a/controller/relay.go b/controller/relay.go index 72d421e3dcc33cb48a0c14093a7dbdb21c7f4a63..d7e0f00ad4916a9eb0cb2d72976fef82de39514a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -33,6 +33,8 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode err = relay.AudioHelper(c) case relayconstant.RelayModeRerank: err = relay.RerankHelper(c, relayMode) + case relayconstant.RelayModeEmbeddings: + err = relay.EmbeddingHelper(c) default: err = relay.TextHelper(c) } diff --git a/dto/embedding.go b/dto/embedding.go new file mode 100644 index 0000000000000000000000000000000000000000..9d7222920acf16946559e5f26715582c9ea21497 --- /dev/null +++ b/dto/embedding.go @@ -0,0 +1,57 @@ +package dto + +type EmbeddingOptions struct { + Seed int `json:"seed,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopK int `json:"top_k,omitempty"` + 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 EmbeddingRequest struct { + Model string `json:"model"` + Input any `json:"input"` + EncodingFormat string `json:"encoding_format,omitempty"` + Dimensions int `json:"dimensions,omitempty"` + User string `json:"user,omitempty"` + Seed float64 `json:"seed,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` +} + +func (r EmbeddingRequest) 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 EmbeddingResponseItem struct { + Object string `json:"object"` + Index int `json:"index"` + Embedding []float64 `json:"embedding"` +} + +type EmbeddingResponse struct { + Object string `json:"object"` + Data []EmbeddingResponseItem `json:"data"` + Model string `json:"model"` + Usage `json:"usage"` +} diff --git a/dto/openai_request.go b/dto/openai_request.go index 628b3dd2b588f8de2ba991623b7fe93c03e8706f..0f6411bb7a0ed792d722eb0072ccaeb96c225a97 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -86,11 +86,13 @@ func (r GeneralOpenAIRequest) ParseInput() []string { } type Message struct { - Role string `json:"role"` - Content json.RawMessage `json:"content"` - Name *string `json:"name,omitempty"` - ToolCalls json.RawMessage `json:"tool_calls,omitempty"` - ToolCallId string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + Content json.RawMessage `json:"content"` + Name *string `json:"name,omitempty"` + Prefix *bool `json:"prefix,omitempty"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls json.RawMessage `json:"tool_calls,omitempty"` + ToolCallId string `json:"tool_call_id,omitempty"` } type MediaContent struct { @@ -116,6 +118,17 @@ const ( ContentTypeInputAudio = "input_audio" ) +func (m *Message) GetPrefix() bool { + if m.Prefix == nil { + return false + } + return *m.Prefix +} + +func (m *Message) SetPrefix(prefix bool) { + m.Prefix = &prefix +} + func (m *Message) ParseToolCalls() []ToolCall { if m.ToolCalls == nil { return nil diff --git a/dto/openai_response.go b/dto/openai_response.go index 4b4a614e4526defd8c3940c2c39eb19aeb78fbcf..2e0e2221e1ce5e27d9aa9ae4afb3abf7021a3380 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -81,7 +81,7 @@ func (c *ChatCompletionsStreamResponseChoiceDelta) GetContentString() string { type ToolCall struct { // Index is not nil only in chat completion chunk object Index *int `json:"index,omitempty"` - ID string `json:"id"` + ID string `json:"id,omitempty"` Type any `json:"type"` Function FunctionCall `json:"function"` } diff --git a/go.mod b/go.mod index 7400f00a5d98658bf0b808a8e4cf50e24c414ff4..c9da57c64f3b5398916de713a98e0e9626505c66 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.9.1 + github.com/glebarez/sqlite v1.9.0 github.com/go-playground/validator/v10 v10.20.0 github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt v3.2.2+incompatible @@ -32,8 +33,7 @@ require ( golang.org/x/net v0.28.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 - gorm.io/driver/sqlite v1.4.3 - gorm.io/gorm v1.25.0 + gorm.io/gorm v1.25.2 ) require ( @@ -49,12 +49,14 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/context v1.1.1 // indirect @@ -70,11 +72,11 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -87,4 +89,8 @@ require ( golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index fcbf534adec29fd8193178923475ce4cffc38f5a..0194ca30290915f948cfdc9f25dd83ef2febf37d 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -58,6 +60,10 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= +github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -77,8 +83,9 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -90,6 +97,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 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/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -140,9 +149,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -167,6 +173,9 @@ github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQ github.com/pkoukk/tiktoken-go v0.1.7/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -263,11 +272,16 @@ gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= 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/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -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= +gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index cf1b8be33e67344976cca00e3fa7e3481a6560c9..68dae8f496c4cc455a4327a12b64094f67cfa4f4 100644 --- a/main.go +++ b/main.go @@ -145,6 +145,13 @@ func main() { middleware.SetUpLogger(server) // Initialize session store store := cookie.NewStore([]byte(common.SessionSecret)) + store.Options(sessions.Options{ + Path: "/", + MaxAge: 2592000, // 30 days + HttpOnly: true, + Secure: false, + SameSite: http.SameSiteStrictMode, + }) server.Use(sessions.Sessions("session", store)) router.SetRouter(server, buildFS, indexPage) diff --git a/middleware/distributor.go b/middleware/distributor.go index 49cca260679e6cbe93a1e2f3fef758fdd0d20ff9..c90f3e5eeb286ae484668f07a27a989c2bd61ccf 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -239,5 +239,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode c.Set("plugin", channel.Other) case common.ChannelCloudflare: c.Set("api_version", channel.Other) + case common.ChannelTypeMokaAI: + c.Set("api_version", channel.Other) } } diff --git a/model/cache.go b/model/cache.go index b6102200fa762515a7c9d965f6f056ae3d4db1c3..bda1ed572f9fe54d389bb4aa97c4676ead787e21 100644 --- a/model/cache.go +++ b/model/cache.go @@ -11,106 +11,6 @@ import ( "time" ) -//func CacheGetUserGroup(id int) (group string, err error) { -// if !common.RedisEnabled { -// return GetUserGroup(id) -// } -// group, err = common.RedisGet(fmt.Sprintf("user_group:%d", id)) -// if err != nil { -// group, err = GetUserGroup(id) -// if err != nil { -// return "", err -// } -// err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, time.Duration(constant.UserId2GroupCacheSeconds)*time.Second) -// if err != nil { -// common.SysError("Redis set user group error: " + err.Error()) -// } -// } -// return group, err -//} -// -//func CacheGetUsername(id int) (username string, err error) { -// if !common.RedisEnabled { -// return GetUsernameById(id) -// } -// username, err = common.RedisGet(fmt.Sprintf("user_name:%d", id)) -// if err != nil { -// username, err = GetUsernameById(id) -// if err != nil { -// return "", err -// } -// err = common.RedisSet(fmt.Sprintf("user_name:%d", id), username, time.Duration(constant.UserId2GroupCacheSeconds)*time.Second) -// if err != nil { -// common.SysError("Redis set user group error: " + err.Error()) -// } -// } -// return username, err -//} -// -//func CacheGetUserQuota(id int) (quota int, err error) { -// if !common.RedisEnabled { -// return GetUserQuota(id) -// } -// quotaString, err := common.RedisGet(fmt.Sprintf("user_quota:%d", id)) -// if err != nil { -// quota, err = GetUserQuota(id) -// if err != nil { -// return 0, err -// } -// return quota, nil -// } -// quota, err = strconv.Atoi(quotaString) -// return quota, nil -//} -// -//func CacheUpdateUserQuota(id int) error { -// if !common.RedisEnabled { -// return nil -// } -// quota, err := GetUserQuota(id) -// if err != nil { -// return err -// } -// return cacheSetUserQuota(id, quota) -//} -// -//func cacheSetUserQuota(id int, quota int) error { -// err := common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(constant.UserId2QuotaCacheSeconds)*time.Second) -// return err -//} -// -//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 { -// return IsUserEnabled(userId) -// } -// enabled, err := common.RedisGet(fmt.Sprintf("user_enabled:%d", userId)) -// if err == nil { -// return enabled == "1", nil -// } -// -// 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(constant.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 channelsIDM map[int]*Channel var channelSyncLock sync.RWMutex diff --git a/model/log.go b/model/log.go index d050fb6a782797a175cbf20d718410b24853d5aa..82278c60b69a692b8748a3c32cc2a4ad2f0d3051 100644 --- a/model/log.go +++ b/model/log.go @@ -133,9 +133,6 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName tx = LOG_DB.Where("logs.type = ?", logType) } - tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id") - tx = tx.Select("logs.*, channels.name as channel_name") - if modelName != "" { tx = tx.Where("logs.model_name like ?", modelName) } @@ -165,6 +162,30 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName if err != nil { return nil, 0, err } + + channelIds := make([]int, 0) + channelMap := make(map[int]string) + for _, log := range logs { + if log.ChannelId != 0 { + channelIds = append(channelIds, log.ChannelId) + } + } + if len(channelIds) > 0 { + var channels []struct { + Id int `gorm:"column:id"` + Name string `gorm:"column:name"` + } + if err = DB.Table("channels").Select("id, name").Where("id IN ?", channelIds).Find(&channels).Error; err != nil { + return logs, total, err + } + for _, channel := range channels { + channelMap[channel.Id] = channel.Name + } + for i := range logs { + logs[i].ChannelName = channelMap[logs[i].ChannelId] + } + } + return logs, total, err } @@ -176,9 +197,6 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int tx = LOG_DB.Where("logs.user_id = ? and logs.type = ?", userId, logType) } - tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id") - tx = tx.Select("logs.*, channels.name as channel_name") - if modelName != "" { tx = tx.Where("logs.model_name like ?", modelName) } @@ -199,6 +217,10 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int return nil, 0, err } err = tx.Order("logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error + if err != nil { + return nil, 0, err + } + formatUserLogs(logs) return logs, total, err } diff --git a/model/main.go b/model/main.go index beaa318873fe9301d26ccf54edc37bdb65ee248c..c0bf927c4dbc2d5fb2b583306bdff0eb0377d7ef 100644 --- a/model/main.go +++ b/model/main.go @@ -1,9 +1,9 @@ package model import ( + "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" "gorm.io/gorm" "log" "one-api/common" diff --git a/model/option.go b/model/option.go index f1f2809df92f445244235f25c18c24f3c99b86d7..0c4114a42256be3bbb4b0d2fdbc11b2bc3fd41ae 100644 --- a/model/option.go +++ b/model/option.go @@ -110,6 +110,7 @@ func InitOptionMap() { common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled) common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString() common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength) + common.OptionMap["AutomaticDisableKeywords"] = setting.AutomaticDisableKeywordsToString() common.OptionMapRWMutex.Unlock() loadOptionsFromDatabase() @@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) { common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64) case "SensitiveWords": setting.SensitiveWordsFromString(value) + case "AutomaticDisableKeywords": + setting.AutomaticDisableKeywordsFromString(value) case "StreamCacheQueueLength": setting.StreamCacheQueueLength, _ = strconv.Atoi(value) } diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index d72db6e42825e35b2d43286d709e3667e117bbf4..c970fd4854ae34cc22a0903d52b134c5682354c1 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -15,6 +15,7 @@ type Adaptor interface { SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) + ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index aa01ca66c0e959b729bf8f7b22f40b16032c1f90..32be399b9a698371d4ff18564355a2881d5b0613 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -49,9 +49,6 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re return nil, errors.New("request is nil") } switch info.RelayMode { - case constant.RelayModeEmbeddings: - baiduEmbeddingRequest := embeddingRequestOpenAI2Ali(*request) - return baiduEmbeddingRequest, nil default: aliReq := requestOpenAI2Ali(*request) return aliReq, nil @@ -67,6 +64,10 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return embeddingRequestOpenAI2Ali(request), nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") diff --git a/relay/channel/ali/text.go b/relay/channel/ali/text.go index aec857fad97e3a9034a8d03ceac390e8dfd63bf5..db4df0a9a1b59b3210be3d5d7cfa74ac60273ec6 100644 --- a/relay/channel/ali/text.go +++ b/relay/channel/ali/text.go @@ -25,9 +25,12 @@ func requestOpenAI2Ali(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIReque return &request } -func embeddingRequestOpenAI2Ali(request dto.GeneralOpenAIRequest) *AliEmbeddingRequest { +func embeddingRequestOpenAI2Ali(request dto.EmbeddingRequest) *AliEmbeddingRequest { + if request.Model == "" { + request.Model = "text-embedding-v1" + } return &AliEmbeddingRequest{ - Model: "text-embedding-v1", + Model: request.Model, Input: struct { Texts []string `json:"texts"` }{ diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index be72c04c8efba9544079b40f799d2ef4c7a83023..5a3d09b976623a7d2526c0395402a337e314491c 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -59,6 +59,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return nil, nil } diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go index 3991a5e9b06b5384c0f1787f67535ad849cdcfc6..46a1f964af4bde2cde253812521e63cf7176f655 100644 --- a/relay/channel/baidu/adaptor.go +++ b/relay/channel/baidu/adaptor.go @@ -109,9 +109,6 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re return nil, errors.New("request is nil") } switch info.RelayMode { - case constant.RelayModeEmbeddings: - baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(*request) - return baiduEmbeddingRequest, nil default: baiduRequest := requestOpenAI2Baidu(*request) return baiduRequest, nil @@ -122,6 +119,11 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(request) + return baiduEmbeddingRequest, nil +} + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/baidu/relay-baidu.go b/relay/channel/baidu/relay-baidu.go index 09a99e4d8669e04f845ed55531273c9fa72fa52d..d88f521205878e45d376ec329c87aa84a9a6d2a9 100644 --- a/relay/channel/baidu/relay-baidu.go +++ b/relay/channel/baidu/relay-baidu.go @@ -87,7 +87,7 @@ func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *dto.Cha return &response } -func embeddingRequestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduEmbeddingRequest { +func embeddingRequestOpenAI2Baidu(request dto.EmbeddingRequest) *BaiduEmbeddingRequest { return &BaiduEmbeddingRequest{ Input: request.ParseInput(), } diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go new file mode 100644 index 0000000000000000000000000000000000000000..fd25ecc10431b7c8d219d7cf2b02247c79beeb70 --- /dev/null +++ b/relay/channel/baidu_v2/adaptor.go @@ -0,0 +1,76 @@ +package baidu_v2 + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + "one-api/relay/channel/openai" + relaycommon "one-api/relay/common" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/v2/chat/completions", info.BaseUrl), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, nil +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { + if info.IsStream { + err, usage = openai.OaiStreamHandler(c, resp, info) + } else { + err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/baidu_v2/constants.go b/relay/channel/baidu_v2/constants.go new file mode 100644 index 0000000000000000000000000000000000000000..a7cee24890065c8e69d0b17aece08d8e9a019960 --- /dev/null +++ b/relay/channel/baidu_v2/constants.go @@ -0,0 +1,29 @@ +package baidu_v2 + +var ModelList = []string{ + "ernie-4.0-8k-latest", + "ernie-4.0-8k-preview", + "ernie-4.0-8k", + "ernie-4.0-turbo-8k-latest", + "ernie-4.0-turbo-8k-preview", + "ernie-4.0-turbo-8k", + "ernie-4.0-turbo-128k", + "ernie-3.5-8k-preview", + "ernie-3.5-8k", + "ernie-3.5-128k", + "ernie-speed-8k", + "ernie-speed-128k", + "ernie-speed-pro-128k", + "ernie-lite-8k", + "ernie-lite-pro-128k", + "ernie-tiny-8k", + "ernie-char-8k", + "ernie-char-fiction-8k", + "ernie-novel-8k", + "deepseek-v3", + "deepseek-r1", + "deepseek-r1-distill-qwen-32b", + "deepseek-r1-distill-qwen-14b", +} + +var ChannelName = "volcengine" diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index 488d87dc20130c05ee5faf056bc23bc7b5b80c60..83168382d45dd7c21d66d2b63c6980fb4e415a17 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -73,6 +73,11 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/cloudflare/adaptor.go b/relay/channel/cloudflare/adaptor.go index fc0ec271102974a062cd1f25827a35fcd83e2420..754000980249221a0aac7c0a4f06950650421894 100644 --- a/relay/channel/cloudflare/adaptor.go +++ b/relay/channel/cloudflare/adaptor.go @@ -56,6 +56,10 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return request, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { // 添加文件字段 file, _, err := c.Request.FormFile("file") diff --git a/relay/channel/cohere/adaptor.go b/relay/channel/cohere/adaptor.go index f8b190ecc4c374db1a0a9a7b34b62a65d53bedc4..d552a53b03d717b03b87a948e79bcd74c421eab7 100644 --- a/relay/channel/cohere/adaptor.go +++ b/relay/channel/cohere/adaptor.go @@ -54,6 +54,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return requestConvertRerank2Cohere(request), nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { if info.RelayMode == constant.RelayModeRerank { err, usage = cohereRerankHandler(c, resp, info) diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index cc94a58f4f5dde80f97db0da36c9de21f361a536..14dd74f03d762c8c65a9487d92322764839bee59 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -29,7 +29,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - return fmt.Sprintf("%s/chat/completions", info.BaseUrl), nil + return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -49,6 +49,11 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go index 53ba26e66a0afdffbce38fd9c64ef2dfc83e2cc7..ce73c78c194563b8f1e6ce8b65004cc3f7e425eb 100644 --- a/relay/channel/dify/adaptor.go +++ b/relay/channel/dify/adaptor.go @@ -48,6 +48,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 9a5bc2517c4cc99316053346174c9b5425987c93..681e9988a7a42d1fe02738e7e6fd277adec62d5f 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -68,6 +68,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/jina/adaptor.go b/relay/channel/jina/adaptor.go index ad488f285d263830a7804c87c672e6f474edaaed..3706e3b8e938a317eac2d9ee15db108844d5eafc 100644 --- a/relay/channel/jina/adaptor.go +++ b/relay/channel/jina/adaptor.go @@ -55,6 +55,10 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return request, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { if info.RelayMode == constant.RelayModeRerank { err, usage = jinaRerankHandler(c, resp) diff --git a/relay/channel/mistral/adaptor.go b/relay/channel/mistral/adaptor.go index 4ab1a35a6dbfd921bd7fb91b8d506b510512e29c..c99e539617371781f4e51590928611c2253f7506 100644 --- a/relay/channel/mistral/adaptor.go +++ b/relay/channel/mistral/adaptor.go @@ -50,6 +50,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/mokaai/adaptor.go b/relay/channel/mokaai/adaptor.go new file mode 100644 index 0000000000000000000000000000000000000000..9670ec9471ed08ac2de64be065ca983036d4499c --- /dev/null +++ b/relay/channel/mokaai/adaptor.go @@ -0,0 +1,93 @@ +package mokaai + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + relaycommon "one-api/relay/common" + "one-api/relay/constant" + "strings" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return request, nil +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { + +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t + suffix := "chat/" + if strings.HasPrefix(info.UpstreamModelName, "m3e") { + suffix = "embeddings" + } + fullRequestURL := fmt.Sprintf("%s/%s", info.BaseUrl, suffix) + return fullRequestURL, nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey)) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + switch info.RelayMode { + case constant.RelayModeEmbeddings: + baiduEmbeddingRequest := embeddingRequestOpenAI2Moka(*request) + return baiduEmbeddingRequest, nil + default: + return nil, errors.New("not implemented") + } +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { + + switch info.RelayMode { + case constant.RelayModeEmbeddings: + err, usage = mokaEmbeddingHandler(c, resp) + default: + // err, usage = mokaHandler(c, resp) + + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/mokaai/constants.go b/relay/channel/mokaai/constants.go new file mode 100644 index 0000000000000000000000000000000000000000..415d83b7f9fc8eb47910541dad46eaa7f32a9d13 --- /dev/null +++ b/relay/channel/mokaai/constants.go @@ -0,0 +1,9 @@ +package mokaai + +var ModelList = []string{ + "m3e-large", + "m3e-base", + "m3e-small", +} + +var ChannelName = "mokaai" \ No newline at end of file diff --git a/relay/channel/mokaai/relay-mokaai.go b/relay/channel/mokaai/relay-mokaai.go new file mode 100644 index 0000000000000000000000000000000000000000..d7580d7a9178989c8527c3a8c6568f6646bbd816 --- /dev/null +++ b/relay/channel/mokaai/relay-mokaai.go @@ -0,0 +1,83 @@ +package mokaai + +import ( + "encoding/json" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/service" +) + +func embeddingRequestOpenAI2Moka(request dto.GeneralOpenAIRequest) *dto.EmbeddingRequest { + var input []string // Change input to []string + + switch v := request.Input.(type) { + case string: + input = []string{v} // Convert string to []string + case []string: + input = v // Already a []string, no conversion needed + case []interface{}: + for _, part := range v { + if str, ok := part.(string); ok { + input = append(input, str) // Append each string to the slice + } + } + } + return &dto.EmbeddingRequest{ + Input: input, + Model: request.Model, + } +} + +func embeddingResponseMoka2OpenAI(response *dto.EmbeddingResponse) *dto.OpenAIEmbeddingResponse { + openAIEmbeddingResponse := dto.OpenAIEmbeddingResponse{ + Object: "list", + Data: make([]dto.OpenAIEmbeddingResponseItem, 0, len(response.Data)), + Model: "baidu-embedding", + Usage: response.Usage, + } + for _, item := range response.Data { + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, dto.OpenAIEmbeddingResponseItem{ + Object: item.Object, + Index: item.Index, + Embedding: item.Embedding, + }) + } + return &openAIEmbeddingResponse +} + +func mokaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + var baiduResponse dto.EmbeddingResponse + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &baiduResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + // if baiduResponse.ErrorMsg != "" { + // return &dto.OpenAIErrorWithStatusCode{ + // Error: dto.OpenAIError{ + // Type: "baidu_error", + // Param: "", + // }, + // StatusCode: resp.StatusCode, + // }, nil + // } + fullTextResponse := embeddingResponseMoka2OpenAI(&baiduResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return service.OpenAIErrorWrapper(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 +} + diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index 30798402f264ef01150c441f608155ae8bad1400..36889cb8d1728674f9fee57d92e52c73dc14644b 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -46,18 +46,17 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re if request == nil { return nil, errors.New("request is nil") } - switch info.RelayMode { - case relayconstant.RelayModeEmbeddings: - return requestOpenAI2Embeddings(*request), nil - default: - return requestOpenAI2Ollama(*request), nil - } + return requestOpenAI2Ollama(*request), nil } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return requestOpenAI2Embeddings(request), nil +} + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go index 2ef716b39ee375f9f6cb95cc7d49cda3f55d3bf6..4ecdd19bef927d9941716199a9db31dab9e75808 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -42,7 +42,7 @@ func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) *OllamaRequest { } } -func requestOpenAI2Embeddings(request dto.GeneralOpenAIRequest) *OllamaEmbeddingRequest { +func requestOpenAI2Embeddings(request dto.EmbeddingRequest) *OllamaEmbeddingRequest { return &OllamaEmbeddingRequest{ Model: request.Model, Input: request.ParseInput(), @@ -123,9 +123,9 @@ func ollamaEmbeddingHandler(c *gin.Context, resp *http.Response, promptTokens in } func flattenEmbeddings(embeddings [][]float64) []float64 { -flattened := []float64{} -for _, row := range embeddings { - flattened = append(flattened, row...) + flattened := []float64{} + for _, row := range embeddings { + flattened = append(flattened, row...) + } + return flattened } -return flattened -} \ No newline at end of file diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 68c365286e3fa354f46ef611e16b78868470a926..e94399eaa04417e5dc3a7ea67379ee57286f76b4 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -149,6 +149,10 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { a.ResponseFormat = request.ResponseFormat if info.RelayMode == constant.RelayModeAudioSpeech { diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 537ccb32e62c7e12e6e40e94d592401bf8a4a006..6c9359f334bd7af5bff02272a123a1d55d33670a 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -5,6 +5,9 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/bytedance/gopkg/util/gopool" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" "github.com/pkg/errors" "io" "math" @@ -20,10 +23,6 @@ import ( "strings" "sync" "time" - - "github.com/bytedance/gopkg/util/gopool" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" ) func sendStreamData(c *gin.Context, data string, forceFormat bool) error { @@ -91,11 +90,12 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel if len(data) < 6 { // ignore blank line or wrong format continue } - if data[:6] != "data: " && data[:6] != "[DONE]" { + if data[:5] != "data:" && data[:6] != "[DONE]" { continue } mu.Lock() - data = data[6:] + data = data[5:] + data = strings.TrimSpace(data) if !strings.HasPrefix(data, "[DONE]") { if lastStreamData != "" { err := sendStreamData(c, lastStreamData, forceFormat) diff --git a/relay/channel/palm/adaptor.go b/relay/channel/palm/adaptor.go index 91272337e4a52f451d149306bdd66d557fccadfe..f38fa95b8044e3c0e7c1d7700f33b4bda97d8bdf 100644 --- a/relay/channel/palm/adaptor.go +++ b/relay/channel/palm/adaptor.go @@ -49,6 +49,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 18b66a9a224bdc6dc1d8c4b44a95f01c3d6f6317..2b27bdb1ef863288649f42c1deb8fa9bfea57c57 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -52,6 +52,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index ac722b22c56a6090aeb1e56b4fba9bd6e2bfb3f9..c02d18a3e84fc016e92a95108e4d4b27dfa3c5c9 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -58,6 +58,10 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return request, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { switch info.RelayMode { case constant.RelayModeRerank: diff --git a/relay/channel/tencent/adaptor.go b/relay/channel/tencent/adaptor.go index d831cc83dbcac30cf05c1ab69774a6d38b18fa45..768ef64683db22d770c0ff486aba71bed2a0ae00 100644 --- a/relay/channel/tencent/adaptor.go +++ b/relay/channel/tencent/adaptor.go @@ -73,6 +73,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 764e5c4b7822285d77ad9829f56eed911f0c8c75..07659c20ab62e0e3bf00b3231a1702f2d932fe77 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -151,6 +151,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go new file mode 100644 index 0000000000000000000000000000000000000000..3b57c67ca0422b9e97edd835074fd90c1341e8d9 --- /dev/null +++ b/relay/channel/volcengine/adaptor.go @@ -0,0 +1,92 @@ +package volcengine + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + "one-api/relay/channel/openai" + relaycommon "one-api/relay/common" + "one-api/relay/constant" + "strings" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + switch info.RelayMode { + case constant.RelayModeChatCompletions: + if strings.HasPrefix(info.UpstreamModelName, "bot") { + return fmt.Sprintf("%s/api/v3/bots/chat/completions", info.BaseUrl), nil + } + return fmt.Sprintf("%s/api/v3/chat/completions", info.BaseUrl), nil + case constant.RelayModeEmbeddings: + return fmt.Sprintf("%s/api/v3/embeddings", info.BaseUrl), nil + default: + } + return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode) +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, nil +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { + switch info.RelayMode { + case constant.RelayModeChatCompletions: + if info.IsStream { + err, usage = openai.OaiStreamHandler(c, resp, info) + } else { + err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + } + case constant.RelayModeEmbeddings: + err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/volcengine/constants.go b/relay/channel/volcengine/constants.go new file mode 100644 index 0000000000000000000000000000000000000000..30cc902e776e97168989cae8c845843e067cc4c7 --- /dev/null +++ b/relay/channel/volcengine/constants.go @@ -0,0 +1,13 @@ +package volcengine + +var ModelList = []string{ + "Doubao-pro-128k", + "Doubao-pro-32k", + "Doubao-pro-4k", + "Doubao-lite-128k", + "Doubao-lite-32k", + "Doubao-lite-4k", + "Doubao-embedding", +} + +var ChannelName = "volcengine" diff --git a/relay/channel/xunfei/adaptor.go b/relay/channel/xunfei/adaptor.go index 31d426a676f83a17e62166b5bfd6d8331efef3a5..71fd1367c058ea9a32317edbb011e64fe4e29fe7 100644 --- a/relay/channel/xunfei/adaptor.go +++ b/relay/channel/xunfei/adaptor.go @@ -50,6 +50,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { // xunfei's request is not http request, so we don't need to do anything here dummyResp := &http.Response{} diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index f0538edca9d37fd2163245420926b4b81cbe7abd..87ff20d50a633c137c208a59f39ed6320c3b852c 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -56,6 +56,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go index 3d46b7996386b403ab07ae758bd5952ba923ab82..5983c1d98ef1d758599b5bc19ee674d8fa89c780 100644 --- a/relay/channel/zhipu_4v/adaptor.go +++ b/relay/channel/zhipu_4v/adaptor.go @@ -53,6 +53,12 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt return nil, nil } +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //TODO implement me + return nil, errors.New("not implemented") +} + + func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index c5c1d08926687d18498804678dab321cb502e1f6..f7a875369a4aea07820b9d4b7bc858b78ace5f50 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -27,7 +27,9 @@ const ( APITypeVertexAi APITypeMistral APITypeDeepSeek - + APITypeMokaAI + APITypeVolcEngine + APITypeBaiduV2 APITypeDummy // this one is only for count, do not add any channel after this ) @@ -78,6 +80,12 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = APITypeMistral case common.ChannelTypeDeepSeek: apiType = APITypeDeepSeek + case common.ChannelTypeMokaAI: + apiType = APITypeMokaAI + case common.ChannelTypeVolcEngine: + apiType = APITypeVolcEngine + case common.ChannelTypeBaiduV2: + apiType = APITypeBaiduV2 } if apiType == -1 { return APITypeOpenAI, false diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 1c7d11e959795825ff14e31e6b85685226c58c5e..c9111106c923c3c3138d403d811c204f07a7f892 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -6,6 +6,7 @@ import ( "one-api/relay/channel/ali" "one-api/relay/channel/aws" "one-api/relay/channel/baidu" + "one-api/relay/channel/baidu_v2" "one-api/relay/channel/claude" "one-api/relay/channel/cloudflare" "one-api/relay/channel/cohere" @@ -14,6 +15,7 @@ import ( "one-api/relay/channel/gemini" "one-api/relay/channel/jina" "one-api/relay/channel/mistral" + "one-api/relay/channel/mokaai" "one-api/relay/channel/ollama" "one-api/relay/channel/openai" "one-api/relay/channel/palm" @@ -22,6 +24,7 @@ import ( "one-api/relay/channel/task/suno" "one-api/relay/channel/tencent" "one-api/relay/channel/vertex" + "one-api/relay/channel/volcengine" "one-api/relay/channel/xunfei" "one-api/relay/channel/zhipu" "one-api/relay/channel/zhipu_4v" @@ -74,6 +77,12 @@ func GetAdaptor(apiType int) channel.Adaptor { return &mistral.Adaptor{} case constant.APITypeDeepSeek: return &deepseek.Adaptor{} + case constant.APITypeMokaAI: + return &mokaai.Adaptor{} + case constant.APITypeVolcEngine: + return &volcengine.Adaptor{} + case constant.APITypeBaiduV2: + return &baidu_v2.Adaptor{} } return nil } diff --git a/relay/relay_embedding.go b/relay/relay_embedding.go new file mode 100644 index 0000000000000000000000000000000000000000..0a41c11d59738c30104fd1abfbb1c730efa509a5 --- /dev/null +++ b/relay/relay_embedding.go @@ -0,0 +1,137 @@ +package relay + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "one-api/common" + "one-api/dto" + relaycommon "one-api/relay/common" + relayconstant "one-api/relay/constant" + "one-api/service" + "one-api/setting" +) + +func getEmbeddingPromptToken(embeddingRequest dto.EmbeddingRequest) int { + token, _ := service.CountTokenInput(embeddingRequest.Input, embeddingRequest.Model) + return token +} + +func validateEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, embeddingRequest dto.EmbeddingRequest) error { + if embeddingRequest.Input == nil { + return fmt.Errorf("input is empty") + } + if info.RelayMode == relayconstant.RelayModeModerations && embeddingRequest.Model == "" { + embeddingRequest.Model = "omni-moderation-latest" + } + if info.RelayMode == relayconstant.RelayModeEmbeddings && embeddingRequest.Model == "" { + embeddingRequest.Model = c.Param("model") + } + return nil +} + +func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { + relayInfo := relaycommon.GenRelayInfo(c) + + var embeddingRequest *dto.EmbeddingRequest + err := common.UnmarshalBodyReusable(c, &embeddingRequest) + if err != nil { + common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error())) + return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest) + } + + err = validateEmbeddingRequest(c, relayInfo, *embeddingRequest) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "invalid_embedding_request", http.StatusBadRequest) + } + + // map model name + modelMapping := c.GetString("model_mapping") + //isModelMapped := false + if modelMapping != "" && modelMapping != "{}" { + modelMap := make(map[string]string) + err := json.Unmarshal([]byte(modelMapping), &modelMap) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError) + } + if modelMap[embeddingRequest.Model] != "" { + embeddingRequest.Model = modelMap[embeddingRequest.Model] + // set upstream model name + //isModelMapped = true + } + } + + relayInfo.UpstreamModelName = embeddingRequest.Model + modelPrice, success := common.GetModelPrice(embeddingRequest.Model, false) + groupRatio := setting.GetGroupRatio(relayInfo.Group) + + var preConsumedQuota int + var ratio float64 + var modelRatio float64 + + promptToken := getEmbeddingPromptToken(*embeddingRequest) + if !success { + preConsumedTokens := promptToken + modelRatio = common.GetModelRatio(embeddingRequest.Model) + ratio = modelRatio * groupRatio + preConsumedQuota = int(float64(preConsumedTokens) * ratio) + } else { + preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio) + } + relayInfo.PromptTokens = promptToken + + // pre-consume quota 预消耗配额 + preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, preConsumedQuota, relayInfo) + if openaiErr != nil { + return openaiErr + } + defer func() { + if openaiErr != nil { + returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota) + } + }() + + adaptor := GetAdaptor(relayInfo.ApiType) + if adaptor == nil { + return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest) + } + adaptor.Init(relayInfo) + + convertedRequest, err := adaptor.ConvertEmbeddingRequest(c, relayInfo, *embeddingRequest) + + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError) + } + jsonData, err := json.Marshal(convertedRequest) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError) + } + requestBody := bytes.NewBuffer(jsonData) + statusCodeMappingStr := c.GetString("status_code_mapping") + resp, err := adaptor.DoRequest(c, relayInfo, requestBody) + if err != nil { + return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) + } + + var httpResp *http.Response + if resp != nil { + httpResp = resp.(*http.Response) + if httpResp.StatusCode != http.StatusOK { + openaiErr = service.RelayErrorHandler(httpResp) + // reset status code 重置状态码 + service.ResetStatusCode(openaiErr, statusCodeMappingStr) + return openaiErr + } + } + + usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo) + if openaiErr != nil { + // reset status code 重置状态码 + service.ResetStatusCode(openaiErr, statusCodeMappingStr) + return openaiErr + } + postConsumeQuota(c, relayInfo, embeddingRequest.Model, usage.(*dto.Usage), ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, success, "") + return nil +} diff --git a/service/channel.go b/service/channel.go index 047550d8ee48298391df1a0c4a3e6167168ba4a8..73545b1e65b26e6d7156badf0cdc8aa68dcef0f1 100644 --- a/service/channel.go +++ b/service/channel.go @@ -6,6 +6,7 @@ import ( "one-api/common" relaymodel "one-api/dto" "one-api/model" + "one-api/setting" "strings" ) @@ -64,21 +65,10 @@ func ShouldDisableChannel(channelType int, err *relaymodel.OpenAIErrorWithStatus case "forbidden": return true } - if strings.HasPrefix(err.Error.Message, "Your credit balance is too low") { // anthropic - return true - } else if strings.HasPrefix(err.Error.Message, "This organization has been disabled.") { - return true - } else if strings.HasPrefix(err.Error.Message, "You exceeded your current quota") { - return true - } else if strings.HasPrefix(err.Error.Message, "Permission denied") { - return true - } - if strings.Contains(err.Error.Message, "The security token included in the request is invalid") { // anthropic - return true - } else if strings.Contains(err.Error.Message, "Operation not allowed") { - return true - } else if strings.Contains(err.Error.Message, "Your account is not authorized") { + lowerMessage := strings.ToLower(err.Error.Message) + search, _ := AcSearch(lowerMessage, setting.AutomaticDisableKeywords, true) + if search { return true } diff --git a/service/sensitive.go b/service/sensitive.go index 321f55af5c5ed952311c578571be56c9aa4d1ddd..14ac94819e772e6dbc3fc568080e453413345be1 100644 --- a/service/sensitive.go +++ b/service/sensitive.go @@ -60,17 +60,7 @@ func SensitiveWordContains(text string) (bool, []string) { return false, nil } checkText := strings.ToLower(text) - // 构建一个AC自动机 - m := InitAc() - hits := m.MultiPatternSearch([]rune(checkText), false) - if len(hits) > 0 { - words := make([]string, 0) - for _, hit := range hits { - words = append(words, string(hit.Word)) - } - return true, words - } - return false, nil + return AcSearch(checkText, setting.SensitiveWords, false) } // SensitiveWordReplace 敏感词替换,返回是否包含敏感词和替换后的文本 @@ -79,7 +69,7 @@ func SensitiveWordReplace(text string, returnImmediately bool) (bool, []string, return false, nil, text } checkText := strings.ToLower(text) - m := InitAc() + m := InitAc(setting.SensitiveWords) hits := m.MultiPatternSearch([]rune(checkText), returnImmediately) if len(hits) > 0 { words := make([]string, 0) diff --git a/service/str.go b/service/str.go index 8137bf559d7e54083646a064d027249b46997834..4390e99be74d8698edfe864296abfc7401664e6b 100644 --- a/service/str.go +++ b/service/str.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" goahocorasick "github.com/anknown/ahocorasick" - "one-api/setting" "strings" ) @@ -57,9 +56,9 @@ func RemoveDuplicate(s []string) []string { return result } -func InitAc() *goahocorasick.Machine { +func InitAc(words []string) *goahocorasick.Machine { m := new(goahocorasick.Machine) - dict := readRunes() + dict := readRunes(words) if err := m.Build(dict); err != nil { fmt.Println(err) return nil @@ -67,10 +66,10 @@ func InitAc() *goahocorasick.Machine { return m } -func readRunes() [][]rune { +func readRunes(words []string) [][]rune { var dict [][]rune - for _, word := range setting.SensitiveWords { + for _, word := range words { word = strings.ToLower(word) l := bytes.TrimSpace([]byte(word)) dict = append(dict, bytes.Runes(l)) @@ -78,3 +77,25 @@ func readRunes() [][]rune { return dict } + +func AcSearch(findText string, dict []string, stopImmediately bool) (bool, []string) { + if len(dict) == 0 { + return false, nil + } + if len(findText) == 0 { + return false, nil + } + m := InitAc(dict) + if m == nil { + return false, nil + } + hits := m.MultiPatternSearch([]rune(findText), stopImmediately) + if len(hits) > 0 { + words := make([]string, 0) + for _, hit := range hits { + words = append(words, string(hit.Word)) + } + return true, words + } + return false, nil +} diff --git a/service/token_counter.go b/service/token_counter.go index 93feab2d1939dedf8989336cb9e9bcc26ca99906..319c9b112f51052e9ba32fbf3b6e6b9833b306f8 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/pkoukk/tiktoken-go" "image" "log" "math" @@ -14,6 +13,8 @@ import ( relaycommon "one-api/relay/common" "strings" "unicode/utf8" + + "github.com/pkoukk/tiktoken-go" ) // tokenEncoderMap won't grow after initialization @@ -323,6 +324,12 @@ func CountTokenInput(input any, model string) (int, error) { text += s } return CountTextToken(text, model) + case []interface{}: + text := "" + for _, item := range v { + text += fmt.Sprintf("%v", item) + } + return CountTextToken(text, model) } return CountTokenInput(fmt.Sprintf("%v", input), model) } diff --git a/setting/chat.go b/setting/chat.go index 0b96ef1870a4399c75fba323584b3d54ae48b44d..ef308000729673a0c291bc0c3310c0164ec064d8 100644 --- a/setting/chat.go +++ b/setting/chat.go @@ -12,6 +12,9 @@ var Chats = []map[string]string{ { "Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}", }, + { + "AI as Workspace": "https://aiaw.app/set-provider?provider={\"type\":\"openai\",\"settings\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\",\"compatibility\":\"strict\"}}", + }, { "AMA 问天": "ama://set-api-key?server={address}&key={key}", }, diff --git a/setting/operation_setting.go b/setting/operation_setting.go index 0f2b4ffdd39b3d38a4d291e522f9d7338a841e5d..9a28e987103d1c7ba9d422cb015f5dfc839e0890 100644 --- a/setting/operation_setting.go +++ b/setting/operation_setting.go @@ -1,3 +1,30 @@ package setting +import "strings" + var DemoSiteEnabled = false + +var AutomaticDisableKeywords = []string{ + "Your credit balance is too low", + "This organization has been disabled.", + "You exceeded your current quota", + "Permission denied", + "The security token included in the request is invalid", + "Operation not allowed", + "Your account is not authorized", +} + +func AutomaticDisableKeywordsToString() string { + return strings.Join(AutomaticDisableKeywords, "\n") +} + +func AutomaticDisableKeywordsFromString(s string) { + AutomaticDisableKeywords = []string{} + ak := strings.Split(s, "\n") + for _, k := range ak { + k = strings.TrimSpace(k) + if k != "" { + AutomaticDisableKeywords = append(AutomaticDisableKeywords, k) + } + } +} diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 890e32bab59870dc64ca8d2bbf9c71735bb6b910..605103ae258bd78c85115fdcf0fad5861706eaf6 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -44,7 +44,7 @@ function renderTimestamp(timestamp) { const ChannelsTable = () => { const { t } = useTranslation(); - + let type2label = undefined; const renderType = (type) => { @@ -53,11 +53,11 @@ const ChannelsTable = () => { for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; } - type2label[0] = { value: 0, text: t('未知类型'), color: 'grey' }; + type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' }; } return ( - {type2label[type]?.text} + {type2label[type]?.label} ); }; @@ -559,7 +559,7 @@ const ChannelsTable = () => { if (!enableTagMode) { channelDates.push(channels[i]); } else { - let tag = channels[i].tag?channels[i].tag:""; + let tag = channels[i].tag ? channels[i].tag : ""; // find from channelTags let tagIndex = channelTags[tag]; let tagChannelDates = undefined; @@ -805,6 +805,9 @@ const ChannelsTable = () => { record.response_time = time * 1000; record.test_time = Date.now() / 1000; showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2))); + + // 刷新列表 + await refresh(); } else { showError(message); } @@ -838,6 +841,8 @@ const ChannelsTable = () => { record.balance = balance; record.balance_updated_time = Date.now() / 1000; showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name)); + // 刷新列表 + await refresh(); } else { showError(message); } @@ -1186,7 +1191,7 @@ const ChannelsTable = () => {
- + {t('标签聚合模式')} { }} /> + disabled={!enableBatchDelete} + theme="light" + type="primary" + style={{ marginRight: 8 }} + onClick={() => setShowBatchSetTag(true)} + > + {t('批量设置标签')} +
diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 98b67c67eec400660d2e407d71f5e062ee53d305..caa9cc2ee25429c206ba8e39bfad51761afc51de 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -59,6 +59,7 @@ const OperationSetting = () => { RetryTimes: 0, Chats: "[]", DemoSiteEnabled: false, + AutomaticDisableKeywords: '', }); let [loading, setLoading] = useState(false); diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 32bf8bcec57306cce0f00f8a71472ca858662d09..dec74b06237a768e3126453fab2b720853e04492 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -1,129 +1,112 @@ export const CHANNEL_OPTIONS = [ - { key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' }, + { value: 1, color: 'green', label: 'OpenAI' }, { - key: 2, - text: 'Midjourney Proxy', value: 2, color: 'light-blue', label: 'Midjourney Proxy' }, { - key: 5, - text: 'Midjourney Proxy Plus', value: 5, color: 'blue', label: 'Midjourney Proxy Plus' }, { - key: 36, - text: 'Suno API', value: 36, color: 'purple', label: 'Suno API' }, - { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' }, + { value: 4, color: 'grey', label: 'Ollama' }, { - key: 14, - text: 'Anthropic Claude', value: 14, color: 'indigo', label: 'Anthropic Claude' }, { - key: 33, - text: 'AWS Claude', value: 33, color: 'indigo', label: 'AWS Claude' }, - { key: 41, text: 'Vertex AI', value: 41, color: 'blue', label: 'Vertex AI' }, + { value: 41, color: 'blue', label: 'Vertex AI' }, { - key: 3, - text: 'Azure OpenAI', value: 3, color: 'teal', label: 'Azure OpenAI' }, { - key: 34, - text: 'Cohere', value: 34, color: 'purple', label: 'Cohere' }, - { key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' }, - { key: 43, text: 'DeepSeek', value: 43, color: 'blue', label: 'DeepSeek' }, + { value: 39, color: 'grey', label: 'Cloudflare' }, + { value: 43, color: 'blue', label: 'DeepSeek' }, { - key: 15, - text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆' }, { - key: 17, - text: '阿里通义千问', + value: 46, + color: 'blue', + label: '百度文心千帆V2' + }, + { value: 17, color: 'orange', label: '阿里通义千问' }, { - key: 18, - text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知' }, { - key: 16, - text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM' }, { - key: 26, - text: '智谱 GLM-4V', value: 26, color: 'purple', label: '智谱 GLM-4V' }, { - key: 24, - text: 'Google Gemini', value: 24, color: 'orange', label: 'Google Gemini' }, { - key: 11, - text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2' }, - { key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' }, - { key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' }, - { key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' }, - { key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' }, - { key: 35, text: 'MiniMax', value: 35, color: 'green', label: 'MiniMax' }, - { key: 37, text: 'Dify', value: 37, color: 'teal', label: 'Dify' }, - { key: 38, text: 'Jina', value: 38, color: 'blue', label: 'Jina' }, - { key: 40, text: 'SiliconCloud', value: 40, color: 'purple', label: 'SiliconCloud' }, - { key: 42, text: 'Mistral AI', value: 42, color: 'blue', label: 'Mistral AI' }, - { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' }, - { - key: 22, - text: '知识库:FastGPT', + { + value: 45, + color: 'blue', + label: '火山方舟(豆包)' + }, + { value: 25, color: 'green', label: 'Moonshot' }, + { value: 19, color: 'blue', label: '360 智脑' }, + { value: 23, color: 'teal', label: '腾讯混元' }, + { value: 31, color: 'green', label: '零一万物' }, + { value: 35, color: 'green', label: 'MiniMax' }, + { value: 37, color: 'teal', label: 'Dify' }, + { value: 38, color: 'blue', label: 'Jina' }, + { value: 40, color: 'purple', label: 'SiliconCloud' }, + { value: 42, color: 'blue', label: 'Mistral AI' }, + { value: 8, color: 'pink', label: '自定义渠道' }, + { value: 22, color: 'blue', label: '知识库:FastGPT' }, { - key: 21, - text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy' + }, + { + value: 44, + color: 'purple', + label: '嵌入模型:MokaAI M3E' } ]; diff --git a/web/src/helpers/other.js b/web/src/helpers/other.js index a61dd6aebc70d477c039a68d1efa914a6f1ff8dd..3e1721809f91538bd01391047116084ede8ec705 100644 --- a/web/src/helpers/other.js +++ b/web/src/helpers/other.js @@ -1,7 +1,7 @@ -export function getLogOther(otherStr) { - if (otherStr === undefined || otherStr === '') { - otherStr = '{}' - } - let other = JSON.parse(otherStr) - return other +export function getLogOther(otherStr) { + if (otherStr === undefined || otherStr === '') { + otherStr = '{}' + } + let other = JSON.parse(otherStr) + return other } \ No newline at end of file diff --git a/web/src/i18n/locales/en copy.json b/web/src/i18n/locales/en copy.json deleted file mode 100644 index efaa1a6a4671a72769dee353c5d94c3925acfc08..0000000000000000000000000000000000000000 --- a/web/src/i18n/locales/en copy.json +++ /dev/null @@ -1,1238 +0,0 @@ -{ - "主页": "Home", - "控制台": "Console", - "$%.6f 额度": "$%.6f quota", - "%d 点额度": "%d point quota", - "尚未实现": "Not yet implemented", - "余额不足": "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)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s", - "测试已在运行中": "Test is already running", - "响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs", - "通道测试完成": "Channel test completed", - "通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal", - "无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server, please try again later!", - "返回值非法,用户字段为空,请稍后重试!": "The return value is illegal, the user field is empty, please try again later!", - "管理员未开启通过 GitHub 登录以及注册": "The administrator did not turn on login and registration via GitHub", - "管理员关闭了新用户注册": "The administrator has turned off new user registration", - "用户已被封禁": "User has been banned", - "该 GitHub 账户已被绑定": "The GitHub account has been bound", - "邮箱地址已被占用": "Email address is occupied", - "%s邮箱验证邮件": "%s Email verification email", - "

您好,你正在进行%s邮箱验证。

": "

Hello, you are verifying %s email.

", - "

您的验证码为: %s

": "

Your verification code is: %s

", - "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

": "

The verification code is valid within %d minutes. If it is not your operation, please ignore it.

", - "无效的参数": "Invalid parameter", - "该邮箱地址未注册": "The email address is not registered", - "%s密码重置": "%s Password reset", - "

您好,你正在进行%s密码重置。

": "

Hello, you are resetting %s password.

", - "

点击此处进行密码重置。

": "

Click here to reset your password.

", - "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

": "

The reset link is valid within %d minutes. If it is not your operation, please ignore it.

", - "重置链接非法或已过期": "Reset link is illegal or expired", - "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!": "Unable to enable GitHub OAuth, please fill in GitHub Client ID and GitHub Client Secret first!", - "无法启用微信登录,请先填入微信登录相关配置信息!": "Unable to enable WeChat login, please fill in the relevant configuration information for WeChat login first!", - "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Unable to enable Turnstile verification, please fill in the relevant configuration information for Turnstile verification first!", - "兑换码名称长度必须在1-20之间": "The length of the redemption code name must be between 1-20", - "兑换码个数必须大于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", - "当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later", - "令牌名称过长": "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 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", - "无法保存会话信息,请重试": "Unable to save session information, please try again", - "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "The administrator has turned off registration via password. Please use the form of third-party account verification to register", - "输入不合法 ": "Input is illegal ", - "管理员开启了邮箱验证,请输入邮箱地址和验证码": "The administrator has turned on email verification, please enter the email address and verification code", - "验证码错误或已过期": "Verification code error or expired", - "无权获取同级或更高等级用户的信息": "No permission to get information of users at the same level or higher", - "请重试,系统生成的 UUID 竟然重复了!": "Please try again, the system-generated UUID is actually duplicated!", - "输入不合法": "Input is illegal", - "无权更新同权限等级或更高权限等级的用户信息": "No permission to update user information with the same permission level or higher permission level", - "管理员将用户额度从 %s修改为 %s": "The administrator changed the user quota from %s to %s", - "无权删除同权限等级或更高权限等级的用户": "No permission to delete users with the same permission level or higher permission level", - "无法创建权限大于等于自己的用户": "Unable to create users with permissions greater than or equal to your own", - "用户不存在": "User does not exist", - "无法禁用超级管理员用户": "Unable to disable super administrator user", - "无法删除超级管理员用户": "Unable to delete super administrator user", - "普通管理员用户无法提升其他用户为管理员": "Ordinary administrator users cannot promote other users to administrators", - "该用户已经是管理员": "The user is already an administrator", - "无法降级超级管理员用户": "Unable to downgrade super administrator user", - "该用户已经是普通用户": "The user is already an ordinary user", - "管理员未开启通过微信登录以及注册": "The administrator has not enabled login and registration via WeChat", - "该微信账号已被绑定": "The WeChat account has been bound", - "无权进行此操作,未登录且未提供 access token": "No permission to perform this operation, not logged in and no access token provided", - "无权进行此操作,access token 无效": "No permission to perform this operation, access token is invalid", - "���权进行此操作,权���不足": "No permission to perform this operation, insufficient permissions", - "普通用户不支持指定渠道": "Ordinary users do not support specifying channels", - "无效的渠道 ID": "Invalid channel ID", - "该渠道已被禁用": "The channel has been disabled", - "无效的请求": "Invalid request", - "无可用渠道": "No available channels", - "Turnstile token 为空": "Turnstile token is empty", - "Turnstile 校验失败,请刷新重试!": "Turnstile verification failed, please refresh and try again!", - "id 为空!": "id is empty!", - "未提供兑换码": "No redemption code provided", - "无效的 user id": "Invalid user id", - "无效的兑换码": "Invalid redemption code", - "该兑换码已被使用": "The redemption code has been used", - "通过兑换码充值 %s": "Recharge %s through redemption code", - "未提供令牌": "No token provided", - "该令牌状态不可用": "The token status is not available", - "该令牌已过期": "The token has expired", - "该令牌额度已用尽": "The token quota has been used up", - "无效的令牌": "Invalid token", - "id 或 userId 为空!": "id or userId is empty!", - "quota 不能为负数!": "quota cannot be negative!", - "令牌额度不足": "Insufficient token quota", - "用户额度不足": "Insufficient user quota", - "您的额度即将用尽": "Your quota is about to run out", - "您的额度已用尽": "Your quota has been used up", - "%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s": "%s, the current remaining quota is %d, in order not to affect your use, please recharge in time.
Recharge link: %s", - "affCode 为空!": "affCode is empty!", - "新用户注册赠送 %s": "New user registration gives %s", - "使用邀请码赠送 %s": "Use invitation code to give %s", - "邀请用户赠送 %s": "Invite users to give %s", - "用户名或密码为空": "Username or password is empty", - "用户名或密码错误,或用户已被封禁": "Username or password is wrong, or user has been banned", - "email 为空!": "email is empty!", - "GitHub id 为空!": "GitHub id is empty!", - "WeChat id 为空!": "WeChat id is empty!", - "username 为空!": "username is empty!", - "邮箱地址或密码为空!": "Email address or password is empty!", - "OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用": "OpenAI interface aggregation management, supports multiple channels including Azure, can be used for secondary distribution management key, only single executable file, Docker image has been packaged, one-click deployment, out of the box", - "未知类型": "Unknown type", - "不支持": "Not supported", - "操作成功完成!": "Operation completed successfully!", - "已启用": "Enabled", - "已禁用": "Disabled", - "未知状态": "Unknown status", - " 秒": "s", - " 分钟 ": " m ", - " 小时 ": " h ", - " 天 ": " d ", - " 个月 ": " M ", - " 年 ": " y ", - "未测试": "Not tested", - "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.", - "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.", - "通道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!", - "已更新完毕所有已启用通道余额!": "The balance of all enabled channels has been updated!", - "搜索渠道的 ID,名称和密钥 ...": "Search for channel ID, name and key ...", - "名称": "Name", - "分组": "Group", - "类型": "Type", - "状态": "Status", - "响应时间": "Response time", - "余额": "Balance", - "操作": "Operation", - "未更新": "Not updated", - "测试": "Test", - "更新余额": "Update balance", - "删除": "Delete", - "删除渠道 {channel.name}": "Delete channel {channel.name}", - "禁用": "Disable", - "启用": "Enable", - "编辑": "Edit", - "添加新的渠道": "Add a new channel", - "测试所有已启用通道": "Test all enabled channels", - "更新所有已启用通道余额": "Update the balance of all enabled channels", - "刷新": "Refresh", - "处理中...": "Processing...", - "绑定成功!": "Binding successful!", - "登录成功!": "Login succeeded!", - "操作失败,重定向至登录界面中...": "Operation failed, redirecting to the login page...", - "出现错误,第 ${count} 次重试中...": "An error occurred, retrying for the ${count} time...", - "首页": "Home", - "渠道": "Channels", - "令牌": "Token", - "兑换": "Redeem", - "充值": "Recharge", - "用户": "User", - "日志": "Logs", - "设置": "Settings", - "关于": "About", - "价格": "Price", - "聊天": "Chat", - "注销成功!": "Logout succeeded!", - "注销": "Logout", - "登录": "Login", - "注册": "Register", - "加载{name}中...": "Loading {name}...", - "未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!", - "用户登录": "User login", - "密码": "password", - "忘记密码?": "Forget password?", - "点击重置": "Click to reset", - "; 没有账户?": "; No account?", - "点击注册": "Click to register", - "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code of WeChat to follow the official account, enter \"verification code\" to get the verification code (valid within three minutes)", - "全部用户": "All users", - "当前用户": "Current user", - "全部'": "All", - "充值'": "Recharge'", - "消费'": "Consume'", - "管理'": "Manage'", - "系统'": "System'", - " 充值 ": " Recharge ", - " 消费 ": " Consume ", - " 管理 ": " Manage ", - " 系统 ": " System ", - " 未知 ": " Unknown ", - "时间": "Time", - "详情": "Details", - "选择模式": "Select mode", - "选择明细分类": "Select details category", - "模型倍率不是合法的 JSON 字符串": "Model ratio is not a valid JSON string", - "通用设置": "General Settings", - "充值链接": "Recharge Link", - "例如发卡网站的购买链接": "For example, the purchase link of the card issuing website", - "聊天页面链接": "Chat Page Link", - "例如 ChatGPT Next Web 的部署地址": "For example, the deployment address of ChatGPT Next Web", - "单位美元额度": "Unit Dollar Quota", - "一单位货币能兑换的额度": "Quota that can be exchanged for one unit of currency", - "启用额度消费日志记录": "Enable quota consumption log recording", - "以货币形式显示额度": "Display quota in the form of currency", - "相关 API 显示令牌额度而非用户额度": "Related API displays token quota instead of user quota", - "保存通用设置": "Save General Settings", - "监控设置": "Monitoring Settings", - "最长响应时间": "Longest Response Time", - "单位秒": "Unit in seconds", - "当运行通道全部测试时": "When all operating channels are tested", - "超过此时间将自动禁用通道": "Channels will be automatically disabled if this time is exceeded", - "额度提醒阈值": "Quota reminder threshold", - "低于此额度时���发送邮件提醒用户": "Email will be sent to remind users when the quota is below this", - "失败时自动禁用通道": "Automatically disable the channel when it fails", - "保存监控设置": "Save Monitoring Settings", - "额度设置": "Quota Settings", - "新用户初始额度": "Initial quota for new users", - "例如": "For example", - "���求预扣费额度": "Request for pre-deducted quota", - "请求结束后多退少补": "Refund more or less after the request ends", - "邀请新用户奖励额度": "Invite new users to reward quota", - "新用户使用邀请码奖励额度": "New user rewards quota using invitation code", - "保存额度设置": "Save Quota Settings", - "倍率设置": "Ratio Settings", - "模型倍率": "Model ratio", - "为一个 JSON 文本": "Is a JSON text", - "键为模型名称": "Key is model name", - "值为倍率": "Value is the ratio", - "分组倍率": "Group ratio", - "键为分组名称": "Key is group name", - "保存倍率设置": "Save ratio settings", - "已是最新版本": "Is the latest version", - "检查更新": "Check for updates", - "公告": "Announcement", - "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", - "保存公告": "Save Announcement", - "个性化设置": "Personalization Settings", - "系统名称": "System Name", - "在此输入系统名称": "Enter the system name here", - "设置系统名称": "Set system name", - "图片地址": "Image URL", - "在此输入 Logo 图片地址": "Enter the Logo image URL here", - "首页内容": "Home Page Content", - "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the homepage content here, supports Markdown & HTML code. Once set, the status information of the homepage will not be displayed. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the homepage.", - "保存首页内容": "Save Home Page Content", - "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.", - "保存关于": "Save About", - "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.", - "页脚": "Footer", - "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.", - "设置页脚": "Set Footer", - "新版本": "New Version", - "关闭": "Close", - "密码已重置并已复制到剪贴板": "Password has been reset and copied to clipboard", - "密码重置确认": "Password Reset Confirmation", - "邮箱地址": "Email address", - "提交": "Submit", - "请稍后几秒重试": "Please retry in a few seconds", - "正在检查用户环境": "Checking user environment", - "重置邮件发送成功": "Reset mail sent successfully", - "请检查邮箱": "Please check your email", - "密码重置": "Password Reset", - "令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard", - "邀请链接已复制到剪切板": "Invitation link has been copied to clipboard", - "微信账户绑定成功": "WeChat account binding succeeded", - "验证码发送成功": "Verification code sent successfully", - "邮箱账户绑定成功": "Email account binding succeeded", - "注意": "Note", - "此处生成的令牌用于系统管理": "The token generated here is used for system management", - "而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services", - "请知悉": "Please be aware", - "更新个人信息": "Update Personal Information", - "生成系统访问令牌": "Generate System Access Token", - "复制邀请链接": "Copy Invitation Link", - "账号绑定": "Account Binding", - "绑定微信账号": "Bind WeChat Account", - "微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account", - "输入": "Enter", - "验证码": "Verification Code", - "获取验证码": "Get Verification Code", - "三分钟内有效": "Valid for three minutes", - "绑定": "Bind", - "绑定 GitHub 账号": "Bind GitHub Account", - "绑定邮箱地址": "Bind Email Address", - "输入邮箱地址": "Enter Email Address", - "未使用": "Unused", - "已使用": "Used", - "操作成功完成": "Operation successfully completed", - "搜索兑换码的 ID 和名称": "Search for ID and name", - "额度": "Quota", - "创建时间": "Creation Time", - "兑换时间": "Redemption Time", - "尚未兑换": "Not yet redeemed", - "已复制到剪贴板": "Copied to clipboard", - "无法复制到剪贴板": "Unable to copy to clipboard", - "请手动复制": "Please copy manually", - "已将兑换码填入搜索框": "The voucher code has been filled into the search box", - "复制": "Copy", - "添加新的兑换码": "Add a new voucher", - "密码长度不得小于 8 位": "Password length must not be less than 8 characters", - "两次输入的密码不一致": "The two passwords entered do not match", - "注册成功": "Registration succeeded", - "请稍后几秒重试,Turnstile 正在检查用户环境": "Please retry in a few seconds, Turnstile is checking user environment", - "验证码发送成功,请检查你的邮箱": "Verification code sent successfully, please check your email", - "新用户注册": "New User Registration", - "输入用户名,最长 12 位": "Enter username, up to 12 characters", - "输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters", - "输入验证码": "Enter Verification Code", - "已有账户": "Already have an account", - "点击登录": "Click to log in", - "服务器地址": "Server Address", - "更新服务器地址": "Update Server Address", - "配置登录注册": "Configure Login/Registration", - "允许通过密码进行登录": "Allow login via password", - "允许通过密码进行注册": "Allow registration via password", - "通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password", - "允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account", - "允许通过微信登录 & 注册": "Allow login & registration via WeChat", - "允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way", - "启用 Turnstile 用户校验": "Enable Turnstile user verification", - "配置 SMTP": "Configure SMTP", - "用以支持系统的邮件发送": "To support the system email sending", - "SMTP 服务器地址": "SMTP Server Address", - "例如:smtp.qq.com": "For example: smtp.qq.com", - "SMTP 端口": "SMTP Port", - "默认: 587": "Default: 587", - "SMTP 账户": "SMTP Account", - "通常是邮箱地址": "Usually an email address", - "发送者邮箱": "Sender email", - "通常和邮箱地址保持一致": "Usually consistent with the email address", - "SMTP 访问凭证": "SMTP Access Credential", - "敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend", - "保存 SMTP 设置": "Save SMTP Settings", - "配置 GitHub OAuth App": "Configure GitHub OAuth App", - "用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub", - "点击此处": "click here", - "管理你的 GitHub OAuth App": "Manage your GitHub OAuth App", - "输入你注册的 GitHub OAuth APP 的 ID": "Enter your registered GitHub OAuth APP ID", - "保存 GitHub OAuth 设置": "Save GitHub OAuth Settings", - "配置 WeChat Server": "Configure WeChat Server", - "用以支持通过微信进行登录注册": "To support login & registration via WeChat", - "了解 WeChat Server": "Learn about WeChat Server", - "WeChat Server 访问凭证": "WeChat Server Access Credential", - "微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link", - "输入一个图片链接": "Enter an image link", - "保存 WeChat Server 设置": "Save WeChat Server Settings", - "配置 Turnstile": "Configure Turnstile", - "用以支持用户校验": "To support user verification", - "管理你的 Turnstile Sites,推荐选择 Invisible Widget Type": "Manage your Turnstile Sites, recommend selecting Invisible Widget Type", - "输入你注册的 Turnstile Site Key": "Enter your registered Turnstile Site Key", - "保存 Turnstile 设置": "Save Turnstile Settings", - "已过期": "Expired", - "已耗尽": "Exhausted", - "搜索令牌的名称 ...": "Search for the name of the token...", - "已用额度": "Quota used", - "剩余额度": "Remaining quota", - "过期时间": "Expiration time", - "无": "None", - "无限制": "Unlimited", - "永不过期": "Never expires", - "无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Unable to copy to clipboard, please copy manually, the token has been entered into the search box", - "删除令牌": "Delete Token", - "添加新的令牌": "Add New Token", - "普通用户": "Normal User", - "管理员": "Admin", - "超级管理员": "Super Admin", - "未知身份": "Unknown Identity", - "已激活": "Activated", - "已封禁": "Banned", - "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Search user ID, username, display name, and email address...", - "用户名": "Username", - "统计信息": "Statistics", - "用户角色": "User Role", - "未绑定邮箱地址": "Email not bound", - "请求次数": "Number of Requests", - "提升": "Promote", - "降级": "Demote", - "删除用户": "Delete User", - "添加新的用户": "Add New User", - "自定义": "Custom", - "等价金额": "Equivalent Amount", - "未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again", - "请求次数过多,请稍后再试": "Too many requests, please try again later", - "服务器内部错误,请联系管理员": "Server internal error, please contact the administrator", - "本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side", - "超级管理员未设置充值链接!": "Super administrator has not set the recharge link!", - "错误:": "Error: ", - "新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using shortcut Shift + F5", - "无法正常连接至服务器": "Unable to connect to the server normally", - "管理渠道": "Manage Channels", - "系统状况": "System Status", - "系统信息": "System Information", - "系统信息总览": "System Information Overview", - "版本": "Version", - "源码": "Source Code", - "启动时间": "Startup Time", - "系统配置": "System Configuration", - "系统配置总览": "System Configuration Overview", - "邮箱验证": "Email Verification", - "未启用": "Not Enabled", - "GitHub 身份验证": "GitHub Authentication", - "微信身份验证": "WeChat Authentication", - "Turnstile 用户校验": "Turnstile User Verification", - "创建新的渠道": "Create New Channel", - "镜像": "Mirror", - "请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used", - "模型": "Model", - "请选择该通道所支持的模型": "Please select the model supported by the channel", - "填入基础模型": "Fill in the basic model", - "填入所有模型": "Fill in all models", - "清除所有模型": "Clear all models", - "密钥": "Key", - "请输入密钥": "Please enter the key", - "批量创建": "Batch Create", - "更新渠道信息": "Update Channel Information", - "我的令牌": "My Tokens", - "管理兑换码": "Manage Redeem Codes", - "兑换码": "Redeem Code", - "管理用户": "Manage Users", - "额度明细": "Quota Details", - "个人设置": "Personal Settings", - "运营设置": "Operation Settings", - "系统设置": "System Settings", - "其他设置": "Other Settings", - "项目仓库地址": "Project Repository Address", - "可在设置页面设置关于内容,支持 HTML & Markdown": "You can set the content about in the settings page, support HTML & Markdown", - "由": "developed by", - "开发,基于": "based on", - "MIT 协议": "MIT License", - "充值额度": "Recharge Quota", - "获取兑换码": "Get Redeem Code", - "一个月后过期": "Expires after one month", - "一天后过期": "Expires after one day", - "一小时后过期": "Expires after one hour", - "一分钟后过期": "Expires after one minute", - "创建新的令牌": "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.", - "设为无限额度": "Set to unlimited quota", - "更新令牌信息": "Update Token Information", - "请输入充值码!": "Please enter the recharge code!", - "请输入名称": "Please enter a name", - "请输入密钥,一行一个": "Please enter the key, one per line", - "请输入额度": "Please enter the quota", - "令牌创建成功": "Token created successfully", - "令牌更新成功": "Token updated successfully", - "充值成功!": "Recharge successful!", - "更新用户信息": "Update User Information", - "请输入新的用户名": "Please enter a new username", - "请输入新的密码": "Please enter a new password", - "显示名称": "Display Name", - "请输入新的显示名称": "Please enter a new display name", - "已绑定的 GitHub 账户": "GitHub Account Bound", - "此项只读,要用户通过个人设置页面的相关绑��按钮进��绑���,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly", - "已绑定的微信账户": "WeChat Account Bound", - "已绑定的邮箱账户": "Email Account Bound", - "用户信息更新成功!": "User information updated successfully!", - "使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})", - "用户名称": "User Name", - "令牌名称": "Token Name", - "留空则查询全部用户": "Leave blank to query all users", - "留空则查询全部令牌": "Leave blank to query all tokens", - "模型名称": "Model Name", - "留空则查询全部模型": "Leave blank to query all models", - "起始时间": "Start Time", - "结束时间": "End Time", - "查询": "Query", - "提示": "Prompt", - "补全": "Completion", - "消耗额度": "Used Quota", - "渠道不存在:%d": "Channel does not exist: %d", - "数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator", - "使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load", - "请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!", - "请至少选择一个Model!": "Please select at least one Model!", - "加载关于内容失败": "Failed to load content about", - "用户账户创建成功!": "User account created successfully!", - "生成数量": "Generate quantity", - "请输入生成数量": "Please enter the quantity to generate", - "创建新用户账户": "Create new user account", - "渠道更新成功!": "Channel updated successfully!", - "渠道创建成功!": "Channel created successfully!", - "请选择分组": "Please select a group", - "更新兑换码信息": "Update redemption code information", - "创建新的兑换码": "Create a new redemption code", - "未找到所请求的页面": "The requested page was not found", - "过期时间格式错误!": "Expiration time format error!", - "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit", - "此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:", - "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:", - "模型映射": "Model mapping", - "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", - "默认": "default", - "图片演示": "Image demo", - "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)", - "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", - "取消无限额度": "Cancel unlimited quota", - "取消": "Cancel", - "请输入新的剩余额度": "Please enter the new remaining quota", - "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", - "请输入用户名": "Please enter username", - "请输入显示名称": "Please enter display name", - "请输入密码": "Please enter password", - "模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name", - ",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body", - "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", - "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", - "Homepage URL 填": "Fill in the Homepage 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!", - "建议收藏所有地址,以防失联。": "It is recommended to bookmark all addresses to prevent losing contact.", - "无法正常请求API的用户,请联系管理员。": "For users who cannot request the API normally, please contact the administrator.", - "温馨提示": "Kind tips", - "获取API URL列表时发生错误,请稍后重试。": "An error occurred while retrieving the API URL list, please try again later.", - ",时间:": ",time:", - "已用/剩余": "Used/Remaining", - ",点击更新": ", click Update", - "确定是否要清空此渠道记录额度?": "Are you sure you want to clear the record quota of this channel?", - "此修改将不可逆": "This modification will be irreversible", - "优先级": "Priority", - "权重": "Weight", - "测试操作项目组": "Test operation project team", - "确定是否要删除此渠道?": "Are you sure you want to delete this channel?", - "确定是否要复制此渠道?": "Are you sure you want to copy this channel?", - "复制渠道的所有信息": "Copy all information for a channel", - "展开操作": "Expand operation", - "_复制": "_copy", - "渠道未找到,请刷新页面后重试。": "Channel not found, please refresh the page and try again.", - "渠道复制成功": "Channel copy successful", - "渠道复制失败: ": "Channel copy failed:", - "已成功开始测试所有通道,请刷新页面查看结果。": "Testing of all channels has been started successfully, please refresh the page to view the results.", - "请先选择要删除的通道!": "Please select the channel you want to delete first!", - "搜索渠道关键词": "Search channel keywords", - "模型关键字": "model keyword", - "选择分组": "Select group", - "使用ID排序": "Sort by ID", - "是否用ID排序": "Whether to sort by ID", - "确定?": "Sure?", - "确定是否要删除禁用通道?": "Are you sure you want to delete the disabled channel?", - "开启批量操作": "Enable batch selection", - "是否开启批量操作": "Whether to enable batch selection", - "确定是否要删除所选通道?": "Are you sure you want to delete the selected channels?", - "确定是否要修复数据库一致性?": "Are you sure you want to repair database consistency?", - "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.", - "获取签到历史失败:": "Failed to obtain check-in history:", - "签到成功": "Sign in successfully", - "签到失败": "Sign in failed", - "签到失败:": "Sign-in failed:", - "已签到": "Check in", - "去签到": "Go Check", - "未签到": "Not Check", - "等待签到": "Waiting", - "当前没有可用的启用令牌,请确认是否有令牌处于启用状态!": "There are currently no enablement tokens available, please confirm if one is enabled!", - "API令牌": "API Token", - "使用日志": "Usage log", - "Midjourney日志": "Midjourney", - "数据看板": "Dashboard", - "模型列表": "Model list", - "常见问题": "FAQ", - "免费体验": "Free trial", - "新用户注册赠送$": "Free $ for new user registration", - "测试金额": "Test amount", - "请稍后几秒重试,Turnstile 正在检查用户环境!": "Please try again in a few seconds, Turnstile is checking the user environment!", - "您正在使用默认密码!": "You are using the default password!", - "请立刻修改默认密码!": "Please change the default password immediately!", - "请输入用户名和密码!": "Please enter username and password!", - "用户名/邮箱": "Username/email", - "微信扫码登录": "WeChat scan code to log in", - "刷新成功": "Refresh successful", - "刷新失败": "Refresh failed", - "用时/首字": "Time/first word", - "重试": "Retry", - "用户信息": "User information", - "无法复制到剪贴板,请手动复制": "Unable to copy to clipboard, please copy manually", - "消费": "Consume", - "管理": "Manage", - "系统": "System", - "用时": "time", - "首字时间": "First word time", - "是否流式": "Whether to stream", - "非流": "not stream", - "渠道 ID": "Channel ID", - "用户ID": "User ID", - "花费": "Spend", - "列设置": "Column settings", - "补偿": "compensate", - "错误": "mistake", - "未知": "unknown", - "全选": "Select all", - "组名必须唯一": "Group name must be unique", - "解析 JSON 出错:": "Error parsing JSON:", - "解析 GroupModel 时发生错误: ": "An error occurred while parsing GroupModel:", - "GroupModel 未定义,无法更新分组": "GroupModel is not defined, cannot update grouping", - "重置成功": "Reset successful", - "加载数据出错:": "Error loading data:", - "加载数据时发生错误: ": "An error occurred while loading data:", - "保存成功": "Saved successfully", - "部分保存失败,请重试": "Partial saving failed, please try again", - "请检查输入": "Please check your input", - "如何区分不同分组不同模型的价格:供参考的配置方式": "How to distinguish the prices of different models in different groups: configuration method for reference", - "获取价格顺序": "Get price order", - "确定同步远程数据吗?": "Are you sure you want to synchronize remote data?", - "此修改将不可逆!建议同步前先备份自己的设置!": "This modification will be irreversible! It is recommended to back up your settings before synchronizing!", - "模型固定价格(按次计费模型用)": "Model fixed price (for pay-per-view models)", - "模型倍率(按量计费模型用)": "Model magnification (for pay-as-you-go model)", - "为一个 JSON 文本,键为模型名称,值为倍率": "is a JSON text, the key is the model name, and the value is the magnification", - "隐藏": "Hide", - "分组名称": "Group name", - "提交结果": "Results", - "模式": "Mode", - "任务状态": "Status", - "耗时": "Time consuming", - "结果图片": "Result", - "失败原因": "Failure reason", - "全部": "All", - "成功": "Success", - "未启动": "No start", - "队列中": "In queue", - "窗口等待": "window wait", - "失败": "Failed", - "绘图": "Drawing", - "放大": "Upscalers", - "微妙放大": "Upscale (Subtle)", - "创造放大": "Upscale (Creative)", - "强变换": "Low Variation", - "弱变换": "High Variation", - "图生文": "Describe", - "图混合": "Blend", - "重绘": "Vary", - "局部重绘-提交": "Vary Region", - "自定义变焦-提交": "Custom Zoom-Submit", - "窗口处理": "window handling", - "缩词后生图": "epigenetic diagram of abbreviation", - "图生文按钮生图": "Picture and text button", - "任务 ID": "Task ID", - "速度模式": "speed mode", - "错误:未登录或登录已过期,请重新登录!": "Error: Not logged in or your login has expired, please log in again!", - "错误:请求次数过多,请稍后再试!": "Error: Too many requests, please try again later!", - "错误:服务器内部错误,请联系管理员!": "Error: Internal server error, please contact the administrator!", - "本站仅作演示之用,无服务端!": "This site is for demonstration purposes only, no server!", - "已用额度:": "Used amount:", - "请求次数:": "Number of requests:", - "平移": "Pan", - "上传文件": "Upload", - "图生文后生图": "Pictures give rise to text and later pictures", - "已提交": "Submitted", - "重复提交": "Duplicate submission", - "未提交": "Not submitted", - "缩词": "Shorten", - "变焦": "zoom", - "按次计费": "Pay per view", - "按量计费": "Pay as you go", - "标签": "Label", - "人民币": "RMB", - "说明": "illustrate", - "可用性": "Availability", - "数据加载失败": "Data loading failed", - "发生错误,请重试": "An error occurred, please try again", - "本站汇率1美金=": "The exchange rate of this site is 1 USD =", - "模糊搜索": "fuzzy search", - "选择标签": "Select label", - "令牌分组": "Token grouping", - "隐": "hidden", - "本站当前已启用模型": "The model is currently enabled on this site", - "个": "indivual", - "倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "The magnification is the calculation method of this website. Different models have different magnifications, which are not multiples of the official price. Please be sure to know.", - "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库
Claude()Claude官方格式请求": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library
Claude()Claude official format request", - "复制选中模型": "Copy selected model", - "分组说明": "Group description", - "倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.", - "点击查看倍率说明": "Click to view the magnification description", - "显": "show", - "当前分组可用": "Available in current group", - "当前分组不可用": "The current group is unavailable", - "提示:": "input:", - "补全:": "output:", - "模型价格:": "Model price:", - "模型:": "Model:", - "分组:": "Grouping:", - "最终价格": "final price", - "计费类型": "Billing type", - "美元": "Dollar", - "倍率": "Ratio", - "常见问题不是合法的 JSON 字符串": "FAQ is not a valid JSON string", - "常见问题更新失败": "FAQ update failed", - "活动内容已更新": "Event content has been updated", - "活动内容更新失败": "Event content update failed", - "页脚内容已更新": "Footer content updated", - "页脚内容更新失败": "Footer content update failed", - "Logo 图片地址": "Logo image address", - "在此输入图片地址": "Enter image address here", - "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。": "Enter the home page content here, support Markdown", - "令牌分组说明": "Token grouping description", - "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。": "Enter new about content here, support Markdown", - "API地址列表": "API address list", - "在此输入新的常见问题,json格式;键为问题,值为答案。": "Enter a new FAQ here in json format; the key is the question and the value is the answer.", - "活动内容": "Activity content", - "在此输入新的活动内容。": "Enter new event content here.", - "总计": "Total", - "无数据": "No data", - "小时": "Hour", - "新密码": "New Password", - "重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!", - "请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!", - "账户已删除!": "Account has been deleted!", - "微信账户绑定成功!": "WeChat account bound successfully!", - "两次输入的密码不一致!": "The passwords entered twice are inconsistent!", - "密码修改成功!": "Password changed successfully!", - "划转金额最低为": "The minimum transfer amount is", - "请输入邮箱!": "Please enter your email!", - "验证码发送成功,请检查邮箱!": "The verification code was sent successfully, please check your email!", - "请输入邮箱验证码!": "Please enter the email verification code!", - "请输入要划转的数量": "Please enter the amount to be transferred", - "当前余额": "Current balance", - "单独并发限制": "Individual concurrency limits", - "未设置单独并发限制": "No individual concurrency limit is set", - "无效的用户单独并发限制数据": "Invalid user individual concurrency limit data", - "未绑定": "Not bound", - "修改绑定": "Modify binding", - "绑定邮箱": "Bind email", - "确认新密码": "Confirm new password", - "历史消耗": "Historical consumption", - "查看": "Check", - "修改密码": "Change password", - "删除个人账户": "Delete personal account", - "已绑定": "Bound", - "获取二维码失败": "Failed to obtain QR code", - "获取当前设置失败": "Failed to get current settings", - "设置已更新": "Settings updated", - "更新设置失败": "Update settings failed", - "确认解绑": "Confirm unbinding", - "您确定要解绑WxPusher吗?": "Are you sure you want to unbind WxPusher?", - "解绑失败": "Unbinding failed", - "订阅事件": "Subscribe to events", - "通知方式": "Notification method", - "留空将通知到账号邮箱": "Leave this blank to be notified to the account email", - "查看接入文档": "View access documentation", - "企业微信机器人Key": "Enterprise WeChat Robot Key", - "您已绑定WxPusher,可以点击下方解绑": "You have bound WxPusher, you can click below to unbind", - "请扫描二维码绑定WxPusher": "Please scan the QR code to bind WxPusher", - "预警额度(需订阅事件)": "Alert quota (need to subscribe to events)", - " 时,将收到预警邮件(2小时最多1次)": "When, you will receive an early warning email (maximum once every 2 hours)", - "兑换人ID": "Redeemer ID", - "确定是否要删除此兑换码?": "Are you sure you want to delete this redemption code?", - "已复制到剪贴板!": "Copied to clipboard!", - "搜索关键字": "Search keywords", - "关键字(id或者名称)": "Keyword (id or name)", - "复制所选兑换码": "Copy selected redemption code", - "请至少选择一个兑换码!": "Please select at least one redemption code!", - "密码长度不得小于 8 位!": "Password must be at least 8 characters long!", - "注册成功!": "Registration successful!", - "验证码发送成功,请检查你的邮箱!": "The verification code was sent successfully, please check your email!", - "确认密码": "Confirm Password", - "邀请码": "Invitation code", - "输入邀请码": "Enter invitation code", - "账户": "Account", - "邮箱": "Mail", - "已有账户?": "Already have an account?", - "创意任务": "Tasks", - "用户管理": "User Management", - "任务ID(点击查看详情)": "Task ID (click to view details)", - "进度": "schedule", - "花费时间": "spend time", - "生成音乐": "generate music", - "生成歌词": "Generate lyrics", - "歌曲拼接": "song splicing", - "上传歌曲": "Upload songs", - "生成视频": "Generate video", - "扩展视频": "Extended video", - "获取无水印": "Get no watermark", - "生成图片": "Generate pictures", - "可灵": "Kling", - "正在提交": "Submitting", - "执行中": "processing", - "平台": "platform", - "排队中": "Queuing", - "已启用:限制模型": "Enabled: restricted model", - "AMA 问天": "AMA Wentian", - "项目操作按钮组": "Project action button group", - "AMA 问天(BotGem)": "AMA Wentian (BotGem)", - "确定是否要删除此令牌?": "Are you sure you want to delete this token?", - "管理员未设置聊天链接": "The administrator has not set up a chat link", - "复制所选令牌": "Copy selected token", - "请至少选择一个令牌!": "Please select at least one token!", - "管理员未设置查询页链接": "The administrator has not set the query page link", - "复制所选令牌到剪贴板": "Copy selected token to clipboard", - "查看API地址": "View API address", - "打开查询页": "Open query page", - "时间(仅显示近3天)": "Time (only displays the last 3 days)", - "请输入兑换码!": "Please enter the redemption code!", - "兑换成功!": "Redemption successful!", - "成功兑换额度:": "Successful redemption amount:", - "请求失败": "Request failed", - "管理员未开启在线充值!": "The administrator has not enabled online recharge!", - "充值数量不能小于": "The recharge amount cannot be less than", - "管理员未开启Stripe在线充值!": "The administrator has not enabled Stripe online recharge!", - "当前充值1美金=": "Current recharge = 1 USD =", - "请选择充值方式!": "Please choose a recharge method!", - "元": "RMB/CNY", - "充值记录": "Recharge record", - "返利记录": "Rebate record", - "确定要充值 $": "Confirm to top up $", - "兑换中...": "Redemming", - "微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:", - "Stripe 实付金额:": "Stripe actual payment amount:", - "支付中...": "Paying", - "支付宝": "Alipay", - "待使用收益": "Proceeds to be used", - "邀请人数": "Number of people invited", - "兑换余额": "Exchange balance", - "在线充值": "Online recharge", - "充值数量,最低 ": "Recharge quantity, minimum", - "请选择充值金额": "Please select the recharge amount", - "微信": "WeChat", - "邀请返利": "Invite rebate", - "总收益": "total revenue", - "邀请信息": "Invitation information", - "代理加盟": "Agent to join", - "代理商信息": "Agent information", - "分红记录": "Dividend record", - "提现记录": "Withdrawal records", - "代理商管理": "Agent management", - "自定义输入": "custom input", - "加载token失败": "Failed to load token", - "配置聊天": "Configure chat", - "模型消耗分布": "Model consumption distribution", - "模型调用次数占比": "Proportion of model calls", - "用户消耗分布": "User consumption distribution", - "时间粒度": "time granularity", - "天": "sky", - "模型概览": "Model overview", - "用户概览": "User overview", - "正在策划中": "Under planning", - "请求首页内容失败": "Requesting homepage content failed", - "返回首页": "Return to home page", - "获取用户数据时发生错误,请稍后重试。": "An error occurred while retrieving user data, please try again later.", - "无额度": "No limit", - "累计消费": "Accumulated consumption", - "累计请求": "Cumulative requests", - "你好,": "Hello,", - "线路监控": "line monitoring", - "查看全部": "View all", - "高延迟": "high latency", - "异常": "abnormal", - "API地址": "API address", - "的未命名令牌": "unnamed token", - "令牌更新成功!": "Token updated successfully!", - "(origin) Discord原链接": "(origin) Discord original link", - "请选择过期时间": "Please select expiration time", - "数量": "quantity", - "请选择或输入创建令牌的数量": "Please select or enter the number of tokens to create", - "请选择渠道": "Please select a channel", - "允许的IP,一行一个": "Allowed IPs, one per line", - "IP黑名单": "IP blacklist", - "不允许的IP,一行一个": "IPs not allowed, one per line", - "请选择该渠道所支持的模型": "Please select the model supported by this channel", - "次": "Second-rate", - "达到限速报错内容": "Error content when the speed limit is reached", - "不填则使用默认报错": "If not filled in, the default error will be reported.", - "Midjouney 设置 (可选)": "Midjouney settings (optional)", - "令牌纬度控制 Midjouney 配置,设置优先级:令牌 {": "Token latitude controls Midjouney configuration, setting priority: token {", - "图片代理地址最好用自己的,本站绘图量大,公用代理地址可能有时网速不佳": "It is best to use your own image proxy address. This site has a large amount of drawings, and public proxy addresses may sometimes have poor network speeds.", - "【突发备用号池】用于应对高强度风控情况,当普通号池全部重试失败,任务进入备用号池执行并额外计费。": "[Sudden backup number pool] is used to deal with high-intensity risk control situations. When all retries in the ordinary number pool fail, the task will be executed in the backup number pool and additional charges will be incurred.", - "绘图模式": "Drawing mode", - "请选择模式": "Please select mode", - "图片代理方式": "Picture agency method", - "用于替换 https://cdn.discordapp.com 的域名": "The domain name used to replace https://cdn.discordapp.com", - "一个月": "a month", - "一天": "one day", - "令牌渠道分组选择": "Token channel grouping selection", - "只可使用对应分组包含的模型。": "Only models contained in the corresponding group can be used.", - "渠道分组": "Channel grouping", - "安全设置(可选)": "Security settings (optional)", - "IP 限制": "IP restrictions", - "启用模型限制(非必要,不建议启用)": "Enable model restrictions (not necessary, not recommended)", - "秒": "Second", - "更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.", - "一小时": "one hour", - "新建数量": "New quantity", - "加载失败,请稍后重试": "Loading failed, please try again later", - "未设置": "Not set", - "API文档": "API documentation", - "不是合法的 JSON 字符串": "Not a valid JSON string", - "个人中心": "Personal center", - "代理商": "Agent", - "钱包": "Wallet", - "备注": "Remark", - "工作台": "Workbench", - "已复制:": "Copied:", - "提交时间": "Submission time", - "无法正常连接至服务器!": "Unable to connect to the server properly!", - "无记录": "No record", - "日间模式": "day mode", - "活动福利": "Activity benefits", - "聊天/绘画": "Chat/Draw", - "跟随系统": "Follow the system", - "黑夜模式": "Dark mode", - "管理员设置": "Admin", - "待更新": "To be updated", - "定价": "Pricing", - "支付中..": "Paying", - "查看图片": "View pictures", - "并发限制": "Concurrency limit", - "正常": "normal", - "周期": "cycle", - "同步频率10-20分钟": "Synchronization frequency 10-20 minutes", - "模型调用占比": "Model call proportion", - "签到日历": "Check-in calendar", - "次,平均每天": "times, average per day", - ",平均每天": ", on average every day", - "(proxy) 自填代理地址": "(proxy) self-filled proxy address", - "启用突发备用号池(建议勾选,极大降低故障率)": "Enable burst backup number pool (it is recommended to check this box to greatly reduce the failure rate)", - "查看说明": "View instructions", - "添加令牌": "Create token", - "令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "Token latitude controls Midjouney configuration, setting priority: token > path parameter > system default", - "启用速率限制": "Enable rate limiting", - "复制BaseURL": "Copy BaseURL", - "总消耗额度": "Total consumption amount", - "近一分钟内消耗Token数": "Number of tokens consumed in the past minute", - "近一分钟内消耗额度": "Quota consumed in the past minute", - "近一分钟内请求次数": "Number of requests in the past minute", - "预估一天消耗量": "Estimated daily consumption", - "模型固定价格:": "Model fixed price:", - "仅供参考,以实际扣费为准": "For reference only, actual deduction shall prevail", - "导出CSV": "Export CSV", - "流": "stream", - "任务ID": "Task ID", - "周": "week", - "总计:": "Total:", - "划转": "transfer", - "可用额度": "Available credit", - "邀请码:": "Invitation code:", - "最低": "lowest", - "划转额度": "Transfer amount", - "邀请链接": "Invitation link", - "更多优惠": "More offers", - "企业微信": "Enterprise WeChat", - "点击解绑WxPusher": "Click to unbind WxPusher", - "点击显示二维码": "Click to display the QR code", - "二维码已过期,点击重新获取": "The QR code has expired, click to get it again", - "邮件": "mail", - "个人信息": "personal information", - "余额不足预警": "Insufficient balance warning", - "促销活动通知": "Promotion notification", - "修改密码、邮箱、微信等": "Change password, email, WeChat, etc.", - "更多选项": "More options", - "模型调价通知": "Model price adjustment notice", - "系统公告通知": "System announcement notification", - "订阅管理": "Subscription management", - "防失联-定期通知": "Prevent loss of contact - regular notifications", - "订阅事件后,当事件触发时,您将会收到相应的通知": "After subscribing to the event, you will receive the corresponding notification when the event is triggered.", - "当余额低于 ": "When the balance is lower than", - "保存": "save", - "计费说明": "Billing instructions", - "高稳定性": "High stability", - "没有账号请先": "If you don't have an account, please", - "注册账号": "Register an account", - "第三方登录": "Third party login", - "欢迎回来": "welcome back", - "忘记密码": "forget the password", - "想起来了?": "Remember?", - "退出": "quit", - "确定": "Sure", - "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2[1]": "Please enter the Spark model version, note that it is the version number in the interface address, for example: v2.1", - "等待中": "Waiting", - "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library.", - "实付金额:": "Actual payment amount:", - "金额": "Amount", - "充值金额": "Recharge amount", - "易支付 实付金额:": "Easy Pay Actual payment amount:", - "微信扫码关注公众号,输入 ": "Scan the QR code on WeChat to follow the official account and enter", - " 获取验证码(三分钟内有效)": "Get verification code (valid within three minutes)", - "不可用模型": "Unavailable model", - "关": "close", - "加载首页内容失败": "", - "打开聊天": "Open chat", - "新窗口打开": "New window opens", - "禁用(仍可为用户单独开启)": "Disabled (can still be turned on individually for users)", - "重新配置": "Reconfigure", - "隐藏不可用模型": "Hide unavailable models", - " 时,将收到预警通知(2小时最多1次)": "When, you will receive an early warning notification (maximum once every 2 hours)", - "在iframe中加载": "Load in iframe", - "补全倍率": "Completion ratio", - "保存分组数据失败": "Failed to save group data", - "保存失败,请重试": "Save failed, please try again", - "没有可用的使用信息": "No usage information available", - "使用详情": "Usage details", - "收起": "close", - "计费详情": "Billing details", - "提示Token": "Tip Token", - "补全Token": "Complete Token", - "提示Token详情": "Prompt Token details", - "补全Token详情": "Complete Token details", - "输出Token详情": "Output Token details", - "缓存Token": "CacheToken", - "内部缓存Token": "Internal cache token", - "图像Token": "ImageToken", - "音频Token": "AudioToken", - "开": "open", - "推理Token": "ReasoningToken", - "文本Token": "TextToken", - "显示禁用渠道": "Show disabled channels", - "输入Token详情": "Enter Token details", - "输出Token": "OutputToken", - "隐藏禁用渠道": "Hide disabled channels", - "今日不再提醒": "No more reminders today", - "平台/类型": "Platform/Type", - "平台和类型": "Platforms and types", - "当前选择分组": "Currently selected group", - "表情迁移": "Expression migration", - "音频输入:": "Audio input:", - "音频输出:": "Audio output:", - "风格重绘": "style repaint", - "测速": "Speed ​​test", - "13800138000,多个手机号用英文逗号隔开,all表示@所有人": "13800138000, multiple mobile phone numbers separated by commas, all means @everyone", - "Bearer Token,不需要可以不填": "Bearer Token, don't need to fill it in", - "Css更新失败": "CSS update failed", - "Javascript已更新": "Javascript has been updated", - "Javascript更新失败": "Javascript update failed", - "免费": "free", - "全局Css": "Global CSS", - "冷却中": "Cooling down", - "分辨率": "resolution", - "发送测试通知失败": "Failed to send test notification", - "在此输入Css代码,支持HTML代码": "Enter Css code here, HTML code is supported", - "在此输入Javascript代码,支持HTML代码": "Enter Javascript code here, HTML code is supported", - "开始时间": "start time", - "当前所选分组不可用": "The currently selected group is unavailable", - "微信扫码关注公众号,输入 rixcode 获取验证码(三分钟内有效)": "Scan the WeChat code to follow the official account, enter rixcode to get the verification code (valid within three minutes)", - "接口凭证": "Interface credentials", - "文字输入": "Text input", - "文字输出": "text output", - "日志详情": "Log details", - "未完成": "Not completed", - "测试单个渠道操作项目组": "Test a single channel operation project group", - "测试通知": "Test notification", - "测试通知发送成功": "Test notification sent successfully", - "点击此处查看接入文档": "Click here to view access documentation", - "类型1": "Type 1", - "类型1 (Imagine)": "Type 1 (Imagine)", - "类型1价格": "Type 1 price", - "类型2": "Type 2", - "类型2 (Upscale)": "Type 2 (Upscale)", - "类型2价格": "Type 2 price", - "类型3价格": "Type 3 price", - "计费过程": "Binning process", - "语音输入": "Voice input", - "语音输出": "Voice output", - "请在右侧切换到可用分组": "Please switch to available groups on the right", - "请联系管理员~": "Please contact the administrator~", - "调用消费": "Call consumption", - "质量": "quality", - "速度": "speed", - "钉钉机器人Key": "DingTalk Robot Key", - "需要@的用户手机号": "Need @ user mobile phone number", - "(提示": "(hint", - "下载文件": "Download file", - "https...xxx.com.webhook": "", - "搜索渠道的 ID,名称和密钥 ": "", - "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ": "", - "操作失败,重定向至登录界面中": "", - "支付中": "", - "等级": "grade", - "钉钉": "DingTalk", - "模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}": "Model price: ${{price}} * Group ratio: {{ratio}} = ${{total}}", - "提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Prompt: ${{price}} * {{ratio}} = ${{total}} / 1M tokens", - "补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Completion: ${{price}} * {{ratio}} = ${{total}} / 1M tokens", - "提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{input}} tokens / 1M tokens * ${{price}} + Completion {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}", - "价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}", - "模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}", - "统计额度": "Statistical quota", - "统计Tokens": "Statistical Tokens", - "统计次数": "Statistical count", - "平均RPM": "Average RPM", - "平均TPM": "Average TPM", - "消耗分布": "Consumption distribution", - "调用次数分布": "Models call distribution", - "添加渠道": "Add channel", - "测试所有通道": "Test all channels", - "删除禁用通道": "Delete disabled channels", - "修复数据库一致性": "Fix database consistency", - "删除所选通道": "Delete selected channels", - "标签聚合模式": "Enable tag mode", - "没有账户?": "No account? ", - "注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)": "Note: The model deployment name must match the model name because One API will replace the model parameter in the request body with your deployment name (dots in the model name will be removed)", - "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com", - "默认 API 版本": "Default API Version", - "请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter default API version, e.g.: 2023-06-01-preview. This configuration can be overridden by actual request query parameters", - "请为渠道命名": "Please name the channel", - "请选择可以使用该渠道的分组": "Please select groups that can use this channel", - "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:", - "部署地区": "Deployment Region", - "请输入部署地区,例如:us-central1\n支持使用模型映射格式": "Please enter deployment region, e.g.: us-central1\nSupports model mapping format", - "填入模板": "Fill Template", - "鉴权json": "Authentication JSON", - "请输入鉴权json": "Please enter authentication JSON", - "组织": "Organization", - "组织,可选,不填则为默认组织": "Organization (optional), default if empty", - "请输入组织org-xxx": "Please enter organization org-xxx", - "默认测试模型": "Default Test Model", - "不填则为模型列表第一个": "First model in list if empty", - "是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:": "Auto-disable (only effective when auto-disable is enabled). When turned off, this channel will not be automatically disabled:", - "状态码复写(仅影响本地判断,不修改返回到上游的状态码)": "Status Code Override (only affects local judgment, does not modify status code returned upstream)", - "此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Optional, used to override returned status codes, e.g. rewriting Claude channel's 400 error to 500 (for retry). Do not abuse this feature. Example:", - "渠道标签": "Channel Tag", - "渠道优先级": "Channel Priority", - "渠道权重": "Channel Weight", - "仅支持 OpenAI 接口格式": "Only OpenAI interface format is supported", - "请填写密钥": "Please enter the key", - "获取模型列表成功": "Successfully retrieved model list", - "获取模型列表失败": "Failed to retrieve model list", - "请填写渠道名称和渠道密钥!": "Please enter channel name and key!", - "请至少选择一个模型!": "Please select at least one model!", - "提交失败,请勿重复提交!": "Submission failed, please do not submit repeatedly!", - "某些模型已存在!": "Some models already exist!", - "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.", - "完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions", - "此项可选,用于通过代理站来进行 API 调用": "Optional, used for API calls through proxy sites", - "私有部署地址": "Private Deployment Address", - "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi", - "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work", - "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Please enter the path before /suno, usually the domain, e.g.: https://api.example.com", - "填入相关模型": "Fill Related Models", - "新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出": "When creating a new channel, requests are sent through the current browser; when editing an existing channel, requests are sent through the backend server", - "获取模型列表": "Get Model List", - "填入": "Fill", - "输入自定义模型名称": "Enter Custom Model Name", - "知识库 ID": "Knowledge Base ID", - "请输入知识库 ID,例如:123456": "Please enter knowledge base ID, e.g.: 123456", - "可选值": "Optional value", - "异步任务": "Async task", - "你好": "Hello", - "你好,请问有什么可以帮助您的吗?": "Hello, how may I help you?", - "用户分组": "Your default group", - "每页条数": "Items per page", - "令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。": "Tokens cannot accurately control usage, only for self-use, please do not distribute tokens directly to others.", - "添加兑换码": "Add redemption code", - "复制所选兑换码到剪贴板": "Copy selected redemption codes to clipboard", - "第 {{start}} - {{end}} 条,共 {{total}} 条": "Items {{start}} - {{end}} of {{total}}", - "新建兑换码": "Code", - "兑换码更新成功!": "Redemption code updated successfully!", - "兑换码创建成功!": "Redemption code created successfully!", - "兑换码创建成功": "Redemption Code Created", - "兑换码创建成功,是否下载兑换码?": "Redemption code created successfully. Do you want to download it?", - "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "The redemption code will be downloaded as a text file, with the filename being the redemption code name.", - "模型价格": "Model price", - "可用分组": "Available groups", - "您的默认分组为:{{group}},分组倍率为:{{ratio}}": "Your default group is: {{group}}, group ratio: {{ratio}}", - "按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)": "The cost of pay-as-you-go = Group ratio × Model ratio × (Prompt token number + Completion token number × Completion ratio) / 500000 (Unit: USD)", - "模糊搜索模型名称": "Fuzzy search model name", - "您还未登陆,显示的价格为默认分组倍率: {{ratio}}": "You are not logged in, the displayed price is the default group ratio: {{ratio}}", - "你的分组无权使用该模型": "Your group is not authorized to use this model", - "您的分组可以使用该模型": "Your group can use this model", - "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Current group: {{group}}, ratio: {{ratio}}", - "添加用户": "Add user", - "角色": "Role", - "已绑定的GitHub账户": "已绑定的GitHub账户", - "已绑定的Telegram账户": "已绑定的Telegram账户", - "新额度": "New quota", - "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)", - "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly", - "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss", - "添加额度": "Add quota", - "以下信息不可修改": "The following information cannot be modified", - "确定要充值吗": "Check to confirm recharge", - "充值数量": "Recharge quantity", - "实付金额": "Actual payment amount", - "是否确认充值?": "Confirm recharge?", - "我的钱包": "My wallet", - "默认聊天页面链接": "Default chat page link", - "聊天页面 2 链接": "Chat page 2 link", - "失败重试次数": "Failed retry times", - "额度查询接口返回令牌额度而非用户额度": "Displays token quota instead of user quota", - "默认折叠侧边栏": "Default collapse sidebar", - "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below", - "你似乎并没有修改什么": "You seem to have not modified anything", - "令牌聊天设置": "Token chat settings", - "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below", - "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1", - "聊天配置": "Chat configuration", - "保存聊天设置": "Save chat settings", - "请求预扣费额度": "Request pre-deduction quota", - "绘图设置": "Drawing settings", - "启用绘图功能": "Enable drawing function", - "允许回调(会泄露服务器 IP 地址)": "Allow callback (will leak server IP address)", - "允许 AccountFilter 参数": "Allow AccountFilter parameter", - "开启之后将上游地址替换为服务器地址": "After enabling, the upstream address will be replaced with the server address", - "开启之后会清除用户提示词中的": "After enabling, the user prompt will be cleared", - "检测必须等待绘图成功才能进行放大等操作": "Detection must wait for drawing to succeed before performing zooming and other operations", - "保存绘图设置": "Save drawing settings", - "以及": "and", - "参数": "parameter", - "屏蔽词过滤设置": "Sensitive word filtering settings", - "启用屏蔽词过滤功能": "Enable sensitive word filtering function", - "启用 Prompt 检查": "Enable Prompt check", - "屏蔽词列表": "Sensitive word list", - "一行一个屏蔽词,不需要符号分割": "One line per sensitive word, no symbols are required", - "保存屏蔽词过滤设置": "Save sensitive word filtering settings", - "日志设置": "Log settings", - "日志记录时间": "Log record time", - "请选择日志记录时间": "Please select log record time", - "清除历史日志": "Clear historical logs", - "条日志已清理!": "logs have been cleared!", - "保存日志设置": "Save log settings", - "数据看板设置": "Data dashboard settings", - "启用数据看板(实验性)": "Enable data dashboard (experimental)", - "数据看板更新间隔": "Data dashboard update interval", - "数据看板默认时间粒度": "Data dashboard default time granularity", - "保存数据看板设置": "Save data dashboard settings", - "请选择最长响应时间": "Please select longest response time", - "成功时自动启用通道": "Enable channel when successful", - "分钟": "minutes", - "设置过短会影响数据库性能": "Setting too short will affect database performance", - "低于此额度时将发送邮件提醒用户": "Send email reminder when below this quota", - "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour", - "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded", - "设置公告": "Set notice", - "设置 Logo": "Set Logo", - "设置首页内容": "Set home page content", - "设置关于": "Set about", - "公告已更新": "Notice updated", - "系统名称已更新": "System name updated", - "Logo 已更新": "Logo updated", - "首页内容已更新": "Home page content updated", - "关于已更新": "About updated", - "模型测试": "model test", - "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings." -} \ No newline at end of file diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index ec2769c2abe25e814986ed3d31b8c5aaa9e0ee15..3d2c7a55a32efa9fd605a61a6fcde3a2f6d6ef3b 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -201,7 +201,7 @@ "相关 API 显示令牌额度而非用户额度": "Related APIs show token quota instead of user quota", "保存通用设置": "Save General Settings", "监控设置": "Monitoring Settings", - "最长响应时间": "Maximum Response Time", + "测试所有渠道的最长响应时间": "Maximum response time for testing all channels", "单位秒": "Unit: seconds", "当运行通道全部测试时": "When running all channel tests", "超过此时间将自动禁用通道": "Channels exceeding this time will be automatically disabled", @@ -498,8 +498,7 @@ "请输入用户名": "Please enter username", "请输入显示名称": "Please enter display name", "请输入密码": "Please enter password", - "模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name", - ",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body", + "注意,模型部署名称必须和模型名称保持一致": "Note that the model deployment name must be consistent with the model name", "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", "Homepage URL 填": "Fill in the Homepage URL", @@ -1109,7 +1108,7 @@ "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.", "完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}", "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions", - "此项可选,用于通过代理站来进行 API 调用": "Optional, used for API calls through proxy sites", + "此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through proxy sites, do not end with /v1 and /", "私有部署地址": "Private Deployment Address", "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work", @@ -1247,5 +1246,8 @@ "请输入要设置的标签名称": "Please enter the tag name to be set", "请输入标签名称": "Please enter the tag name", "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address", - "已注销": "Logged out" + "已注销": "Logged out", + "自动禁用关键词": "Automatic disable keywords", + "一行一个,不区分大小写": "One line per keyword, not case-sensitive", + "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "When the upstream channel returns an error containing these keywords (not case-sensitive), automatically disable the channel" } \ No newline at end of file diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 3e99b7da13fbea616f5120261addbf492f4069af..e98610594f7d604ea4c5a7a027846d5e57033a91 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -429,6 +429,7 @@ const EditChannel = (props) => { >
+ {t('类型')}:
{ + handleInputChange('name', value); + }} + value={inputs.name} + autoComplete="new-password" + /> {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && ( <>
- {t('代理')}: + {t('BaseURL')}:
{ handleInputChange('base_url', value); }} @@ -518,6 +557,77 @@ const EditChannel = (props) => { /> )} +
+ {t('密钥')}: +
+ {batch ? ( +