├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── docker-image.yml │ ├── linux-release.yml │ ├── macos-release.yml │ └── windows-release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.ja.md ├── README.md ├── VERSION ├── bin ├── migration_v0.2-v0.3.sql ├── migration_v0.3-v0.4.sql └── time_test.sh ├── common ├── blacklist │ └── main.go ├── client │ └── init.go ├── config │ └── config.go ├── constants.go ├── conv │ └── any.go ├── crypto.go ├── ctxkey │ └── key.go ├── custom-event.go ├── database.go ├── embed-file-system.go ├── env │ └── helper.go ├── gin.go ├── helper │ ├── helper.go │ ├── key.go │ └── time.go ├── i18n │ ├── i18n.go │ └── locales │ │ ├── en.json │ │ └── zh-CN.json ├── image │ ├── image.go │ └── image_test.go ├── init.go ├── logger │ ├── constants.go │ └── logger.go ├── message │ ├── email.go │ ├── main.go │ ├── message-pusher.go │ └── template.go ├── network │ ├── ip.go │ └── ip_test.go ├── random │ └── main.go ├── rate-limit.go ├── redis.go ├── render │ └── render.go ├── utils.go ├── utils │ └── array.go ├── validate.go └── verification.go ├── controller ├── auth │ ├── github.go │ ├── lark.go │ ├── oidc.go │ └── wechat.go ├── billing.go ├── channel-billing.go ├── channel-test.go ├── channel.go ├── group.go ├── log.go ├── misc.go ├── model.go ├── option.go ├── redemption.go ├── relay.go ├── token.go └── user.go ├── docker-compose.yml ├── docs └── API.md ├── go.mod ├── go.sum ├── main.go ├── middleware ├── auth.go ├── cache.go ├── cors.go ├── distributor.go ├── gzip.go ├── language.go ├── logger.go ├── rate-limit.go ├── recover.go ├── request-id.go ├── turnstile-check.go └── utils.go ├── model ├── ability.go ├── cache.go ├── channel.go ├── log.go ├── main.go ├── option.go ├── redemption.go ├── token.go ├── user.go └── utils.go ├── monitor ├── channel.go ├── manage.go └── metric.go ├── one-api.service ├── pull_request_template.md ├── relay ├── adaptor.go ├── adaptor │ ├── ai360 │ │ └── constants.go │ ├── aiproxy │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── ali │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── image.go │ │ ├── main.go │ │ └── model.go │ ├── alibailian │ │ ├── constants.go │ │ └── main.go │ ├── anthropic │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── aws │ │ ├── adaptor.go │ │ ├── claude │ │ │ ├── adapter.go │ │ │ ├── main.go │ │ │ └── model.go │ │ ├── llama3 │ │ │ ├── adapter.go │ │ │ ├── main.go │ │ │ ├── main_test.go │ │ │ └── model.go │ │ ├── registry.go │ │ └── utils │ │ │ ├── adaptor.go │ │ │ └── utils.go │ ├── baichuan │ │ └── constants.go │ ├── baidu │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── baiduv2 │ │ ├── constants.go │ │ └── main.go │ ├── cloudflare │ │ ├── adaptor.go │ │ ├── constant.go │ │ ├── main.go │ │ └── model.go │ ├── cohere │ │ ├── adaptor.go │ │ ├── constant.go │ │ ├── main.go │ │ └── model.go │ ├── common.go │ ├── coze │ │ ├── adaptor.go │ │ ├── constant │ │ │ ├── contenttype │ │ │ │ └── define.go │ │ │ ├── event │ │ │ │ └── define.go │ │ │ └── messagetype │ │ │ │ └── define.go │ │ ├── constants.go │ │ ├── helper.go │ │ ├── main.go │ │ └── model.go │ ├── deepl │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── helper.go │ │ ├── main.go │ │ └── model.go │ ├── deepseek │ │ └── constants.go │ ├── doubao │ │ ├── constants.go │ │ └── main.go │ ├── gemini │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── geminiv2 │ │ ├── constants.go │ │ └── main.go │ ├── groq │ │ └── constants.go │ ├── interface.go │ ├── lingyiwanwu │ │ └── constants.go │ ├── minimax │ │ ├── constants.go │ │ └── main.go │ ├── mistral │ │ └── constants.go │ ├── moonshot │ │ └── constants.go │ ├── novita │ │ ├── constants.go │ │ └── main.go │ ├── ollama │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── openai │ │ ├── adaptor.go │ │ ├── compatible.go │ │ ├── constants.go │ │ ├── helper.go │ │ ├── image.go │ │ ├── main.go │ │ ├── model.go │ │ ├── token.go │ │ └── util.go │ ├── openrouter │ │ └── constants.go │ ├── palm │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── model.go │ │ └── palm.go │ ├── proxy │ │ └── adaptor.go │ ├── replicate │ │ ├── adaptor.go │ │ ├── chat.go │ │ ├── constant.go │ │ ├── image.go │ │ └── model.go │ ├── siliconflow │ │ └── constants.go │ ├── stepfun │ │ └── constants.go │ ├── tencent │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go │ ├── togetherai │ │ └── constants.go │ ├── vertexai │ │ ├── adaptor.go │ │ ├── claude │ │ │ ├── adapter.go │ │ │ └── model.go │ │ ├── gemini │ │ │ └── adapter.go │ │ ├── registry.go │ │ └── token.go │ ├── xai │ │ └── constants.go │ ├── xunfei │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── domain.go │ │ ├── main.go │ │ └── model.go │ ├── xunfeiv2 │ │ └── constants.go │ └── zhipu │ │ ├── adaptor.go │ │ ├── constants.go │ │ ├── main.go │ │ └── model.go ├── adaptor_test.go ├── apitype │ └── define.go ├── billing │ ├── billing.go │ └── ratio │ │ ├── group.go │ │ ├── image.go │ │ └── model.go ├── channeltype │ ├── define.go │ ├── helper.go │ ├── url.go │ └── url_test.go ├── constant │ ├── common.go │ ├── finishreason │ │ └── define.go │ └── role │ │ └── define.go ├── controller │ ├── audio.go │ ├── error.go │ ├── helper.go │ ├── image.go │ ├── proxy.go │ ├── text.go │ └── validator │ │ └── validation.go ├── meta │ └── relay_meta.go ├── model │ ├── constant.go │ ├── general.go │ ├── image.go │ ├── message.go │ ├── misc.go │ └── tool.go └── relaymode │ ├── define.go │ └── helper.go ├── router ├── api.go ├── dashboard.go ├── main.go ├── relay.go └── web.go └── web ├── README.md ├── THEMES ├── air ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo.png │ └── robots.txt ├── src │ ├── App.js │ ├── components │ │ ├── ChannelsTable.js │ │ ├── Footer.js │ │ ├── GitHubOAuth.js │ │ ├── HeaderBar.js │ │ ├── Loading.js │ │ ├── LoginForm.js │ │ ├── LogsTable.js │ │ ├── MjLogsTable.js │ │ ├── OperationSetting.js │ │ ├── OtherSetting.js │ │ ├── PasswordResetConfirm.js │ │ ├── PasswordResetForm.js │ │ ├── PersonalSetting.js │ │ ├── PrivateRoute.js │ │ ├── RedemptionsTable.js │ │ ├── RegisterForm.js │ │ ├── SiderBar.js │ │ ├── SystemSetting.js │ │ ├── TokensTable.js │ │ ├── UsersTable.js │ │ ├── WeChatIcon.js │ │ └── utils.js │ ├── constants │ │ ├── channel.constants.js │ │ ├── common.constant.js │ │ ├── index.js │ │ ├── toast.constants.js │ │ └── user.constants.js │ ├── context │ │ ├── Status │ │ │ ├── index.js │ │ │ └── reducer.js │ │ └── User │ │ │ ├── index.js │ │ │ └── reducer.js │ ├── helpers │ │ ├── api.js │ │ ├── auth-header.js │ │ ├── history.js │ │ ├── index.js │ │ ├── render.js │ │ └── utils.js │ ├── index.css │ ├── index.js │ └── pages │ │ ├── About │ │ └── index.js │ │ ├── Channel │ │ ├── EditChannel.js │ │ └── index.js │ │ ├── Chat │ │ └── index.js │ │ ├── Detail │ │ └── index.js │ │ ├── Home │ │ └── index.js │ │ ├── Log │ │ └── index.js │ │ ├── Midjourney │ │ └── index.js │ │ ├── NotFound │ │ └── index.js │ │ ├── Redemption │ │ ├── EditRedemption.js │ │ └── index.js │ │ ├── Setting │ │ └── index.js │ │ ├── Token │ │ ├── EditToken.js │ │ └── index.js │ │ ├── TopUp │ │ └── index.js │ │ └── User │ │ ├── AddUser.js │ │ ├── EditUser.js │ │ └── index.js └── vercel.json ├── berry ├── .gitignore ├── .prettierrc ├── README.md ├── jsconfig.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.js │ ├── assets │ ├── fonts │ │ ├── roboto-500.woff2 │ │ ├── roboto-700.woff2 │ │ └── roboto-regular.woff2 │ ├── images │ │ ├── 404.svg │ │ ├── auth │ │ │ ├── auth-blue-card.svg │ │ │ ├── auth-pattern-dark.svg │ │ │ ├── auth-pattern.svg │ │ │ ├── auth-purple-card.svg │ │ │ ├── auth-signup-blue-card.svg │ │ │ └── auth-signup-white-card.svg │ │ ├── icons │ │ │ ├── earning.svg │ │ │ ├── github.svg │ │ │ ├── lark.svg │ │ │ ├── oidc.svg │ │ │ ├── shape-avatar.svg │ │ │ ├── social-google.svg │ │ │ └── wechat.svg │ │ ├── invite │ │ │ ├── cover.jpg │ │ │ └── cwok_casual_19.webp │ │ ├── logo-2.svg │ │ ├── logo-white.svg │ │ ├── logo.svg │ │ └── users │ │ │ └── user-round.svg │ └── scss │ │ ├── _themes-vars.module.scss │ │ ├── fonts.scss │ │ └── style.scss │ ├── config.js │ ├── constants │ ├── ChannelConstants.js │ ├── CommonConstants.js │ ├── SnackbarConstants.js │ └── index.js │ ├── contexts │ ├── StatusContext.js │ └── UserContext.js │ ├── hooks │ ├── useAuth.js │ ├── useLogin.js │ ├── useRegister.js │ └── useScriptRef.js │ ├── index.js │ ├── layout │ ├── MainLayout │ │ ├── Header │ │ │ ├── ProfileSection │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── LogoSection │ │ │ └── index.js │ │ ├── Sidebar │ │ │ ├── MenuCard │ │ │ │ └── index.js │ │ │ ├── MenuList │ │ │ │ ├── NavCollapse │ │ │ │ │ └── index.js │ │ │ │ ├── NavGroup │ │ │ │ │ └── index.js │ │ │ │ ├── NavItem │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── index.js │ ├── MinimalLayout │ │ ├── Header │ │ │ └── index.js │ │ └── index.js │ ├── NavMotion.js │ └── NavigationScroll.js │ ├── menu-items │ ├── index.js │ └── panel.js │ ├── routes │ ├── MainRoutes.js │ ├── OtherRoutes.js │ └── index.js │ ├── serviceWorker.js │ ├── store │ ├── accountReducer.js │ ├── actions.js │ ├── constant.js │ ├── customizationReducer.js │ ├── index.js │ ├── reducer.js │ └── siteInfoReducer.js │ ├── themes │ ├── compStyleOverride.js │ ├── index.js │ ├── palette.js │ └── typography.js │ ├── ui-component │ ├── AdminContainer.js │ ├── Footer.js │ ├── Label.js │ ├── Loadable.js │ ├── Loader.js │ ├── Logo.js │ ├── SvgColor.js │ ├── Switch.js │ ├── TableToolBar.js │ ├── ThemeButton.js │ ├── cards │ │ ├── CardSecondaryAction.js │ │ ├── MainCard.js │ │ ├── Skeleton │ │ │ ├── EarningCard.js │ │ │ ├── ImagePlaceholder.js │ │ │ ├── PopularCard.js │ │ │ ├── ProductPlaceholder.js │ │ │ ├── TotalGrowthBarChart.js │ │ │ └── TotalIncomeCard.js │ │ ├── SubCard.js │ │ └── UserCard.js │ └── extended │ │ ├── AnimateButton.js │ │ ├── Avatar.js │ │ ├── Breadcrumbs.js │ │ └── Transitions.js │ ├── utils │ ├── api.js │ ├── chart.js │ ├── common.js │ ├── password-strength.js │ └── route-guard │ │ └── AuthGuard.js │ └── views │ ├── About │ └── index.js │ ├── Authentication │ ├── Auth │ │ ├── ForgetPassword.js │ │ ├── GitHubOAuth.js │ │ ├── LarkOAuth.js │ │ ├── Login.js │ │ ├── OidcOAuth.js │ │ ├── Register.js │ │ └── ResetPassword.js │ ├── AuthCardWrapper.js │ ├── AuthForms │ │ ├── AuthLogin.js │ │ ├── AuthRegister.js │ │ ├── ForgetPasswordForm.js │ │ ├── ResetPasswordForm.js │ │ └── WechatModal.js │ └── AuthWrapper.js │ ├── Channel │ ├── component │ │ ├── EditModal.js │ │ ├── GroupLabel.js │ │ ├── NameLabel.js │ │ ├── ResponseTimeLabel.js │ │ ├── TableHead.js │ │ └── TableRow.js │ ├── index.js │ └── type │ │ └── Config.js │ ├── Dashboard │ ├── component │ │ ├── StatisticalBarChart.js │ │ ├── StatisticalCard.js │ │ └── StatisticalLineChartCard.js │ └── index.js │ ├── Error │ └── index.js │ ├── Home │ ├── baseIndex.js │ └── index.js │ ├── Log │ ├── component │ │ ├── TableHead.js │ │ ├── TableRow.js │ │ └── TableToolBar.js │ ├── index.js │ └── type │ │ └── LogType.js │ ├── Profile │ ├── component │ │ └── EmailModal.js │ └── index.js │ ├── Redemption │ ├── component │ │ ├── EditModal.js │ │ ├── TableHead.js │ │ └── TableRow.js │ └── index.js │ ├── Setting │ ├── component │ │ ├── OperationSetting.js │ │ ├── OtherSetting.js │ │ └── SystemSetting.js │ └── index.js │ ├── Token │ ├── component │ │ ├── EditModal.js │ │ ├── TableHead.js │ │ └── TableRow.js │ └── index.js │ ├── Topup │ ├── component │ │ ├── InviteCard.js │ │ └── TopupCard.js │ └── index.js │ └── User │ ├── component │ ├── EditModal.js │ ├── TableHead.js │ └── TableRow.js │ └── index.js ├── build.sh ├── build └── .gitkeep └── default ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.png └── robots.txt ├── src ├── App.js ├── components │ ├── ChannelsTable.js │ ├── Footer.js │ ├── GitHubOAuth.js │ ├── Header.js │ ├── LarkOAuth.js │ ├── Loading.js │ ├── LoginForm.js │ ├── LogsTable.js │ ├── OperationSetting.js │ ├── OtherSetting.js │ ├── PasswordResetConfirm.js │ ├── PasswordResetForm.js │ ├── PersonalSetting.js │ ├── PrivateRoute.js │ ├── RedemptionsTable.js │ ├── RegisterForm.js │ ├── SystemSetting.js │ ├── TokensTable.js │ ├── UsersTable.js │ └── utils.js ├── constants │ ├── channel.constants.js │ ├── common.constant.js │ ├── index.js │ ├── toast.constants.js │ └── user.constants.js ├── context │ ├── Status │ │ ├── index.js │ │ └── reducer.js │ └── User │ │ ├── index.js │ │ └── reducer.js ├── helpers │ ├── api.js │ ├── auth-header.js │ ├── helper.js │ ├── history.js │ ├── index.js │ ├── render.js │ └── utils.js ├── i18n.js ├── images │ └── lark.svg ├── index.css ├── index.js ├── locales │ ├── en │ │ └── translation.json │ └── zh │ │ └── translation.json └── pages │ ├── About │ └── index.js │ ├── Channel │ ├── EditChannel.js │ └── index.js │ ├── Chat │ └── index.js │ ├── Dashboard │ ├── Dashboard.css │ └── index.js │ ├── Home │ └── index.js │ ├── Log │ └── index.js │ ├── NotFound │ └── index.js │ ├── Redemption │ ├── EditRedemption.js │ └── index.js │ ├── Setting │ └── index.js │ ├── Token │ ├── EditToken.js │ └── index.js │ ├── TopUp │ └── index.js │ └── User │ ├── AddUser.js │ ├── EditUser.js │ └── index.js └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DEBUG=false 3 | HTTPS_PROXY=http://localhost:7890 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://iamazing.cn/page/reward'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告问题 3 | about: 使用简练详细的语言描述你遇到的问题 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | 12 | [//]: # (方框内删除已有的空格,填 x 号) 13 | + [ ] 我已确认目前没有类似 issue 14 | + [ ] 我已确认我已升级到最新版本 15 | + [ ] 我已完整查看过项目 README,尤其是常见问题部分 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 18 | 19 | **问题描述** 20 | 21 | **复现步骤** 22 | 23 | **预期结果** 24 | 25 | **相关截图** 26 | 如果没有的话,请删除此节。 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 项目群聊 4 | url: https://openai.justsong.cn/ 5 | about: QQ 群:828520184,自动审核,备注 One API 6 | - name: 赞赏支持 7 | url: https://iamazing.cn/page/reward 8 | about: 请作者喝杯咖啡,以激励作者持续开发 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 使用简练详细的语言描述希望加入的新功能 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **例行检查** 11 | 12 | [//]: # (方框内删除已有的空格,填 x 号) 13 | + [ ] 我已确认目前没有类似 issue 14 | + [ ] 我已确认我已升级到最新版本 15 | + [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 18 | 19 | **功能描述** 20 | 21 | **应用场景** 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # This setup assumes that you run the unit tests with code coverage in the same 4 | # workflow that will also print the coverage report as comment to the pull request. 5 | # Therefore, you need to trigger this workflow when a pull request is (re)opened or 6 | # when new code is pushed to the branch of the pull request. In addition, you also 7 | # need to trigger this workflow when new code is pushed to the main branch because 8 | # we need to upload the code coverage results as artifact for the main branch as 9 | # well since it will be the baseline code coverage. 10 | # 11 | # We do not want to trigger the workflow for pushes to *any* branch because this 12 | # would trigger our jobs twice on pull requests (once from "push" event and once 13 | # from "pull_request->synchronize") 14 | on: 15 | push: 16 | branches: 17 | - 'main' 18 | 19 | jobs: 20 | unit_tests: 21 | name: "Unit tests" 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Go 28 | uses: actions/setup-go@v4 29 | with: 30 | go-version: ^1.22 31 | 32 | # When you execute your unit tests, make sure to use the "-coverprofile" flag to write a 33 | # coverage profile to a file. You will need the name of the file (e.g. "coverage.txt") 34 | # in the next step as well as the next job. 35 | - name: Test 36 | run: go test -cover -coverprofile=coverage.txt ./... 37 | - uses: codecov/codecov-action@v4 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | 41 | commit_lint: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: wagoid/commitlint-github-action@v6 46 | -------------------------------------------------------------------------------- /.github/workflows/macos-release.yml: -------------------------------------------------------------------------------- 1 | name: macOS Release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - 'v*.*.*' 9 | - '!*-alpha*' 10 | - '!*-preview*' 11 | workflow_dispatch: 12 | inputs: 13 | name: 14 | description: 'reason' 15 | required: false 16 | jobs: 17 | release: 18 | runs-on: macos-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Check repository URL 25 | run: | 26 | REPO_URL=$(git config --get remote.origin.url) 27 | if [[ $REPO_URL == *"pro" ]]; then 28 | exit 1 29 | fi 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 16 33 | - name: Build Frontend 34 | env: 35 | CI: "" 36 | run: | 37 | cd web 38 | git describe --tags > VERSION 39 | REACT_APP_VERSION=$(git describe --tags) chmod u+x ./build.sh && ./build.sh 40 | cd .. 41 | - name: Set up Go 42 | uses: actions/setup-go@v3 43 | with: 44 | go-version: '>=1.18.0' 45 | - name: Build Backend 46 | run: | 47 | go mod download 48 | go build -ldflags "-X 'github.com/songquanpeng/one-api/common.Version=$(git describe --tags)'" -o one-api-macos 49 | - name: Release 50 | uses: softprops/action-gh-release@v1 51 | if: startsWith(github.ref, 'refs/tags/') 52 | with: 53 | files: one-api-macos 54 | draft: true 55 | generate_release_notes: true 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /.github/workflows/windows-release.yml: -------------------------------------------------------------------------------- 1 | name: Windows Release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - 'v*.*.*' 9 | - '!*-alpha*' 10 | - '!*-preview*' 11 | workflow_dispatch: 12 | inputs: 13 | name: 14 | description: 'reason' 15 | required: false 16 | jobs: 17 | release: 18 | runs-on: windows-latest 19 | defaults: 20 | run: 21 | shell: bash 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - name: Check repository URL 28 | run: | 29 | REPO_URL=$(git config --get remote.origin.url) 30 | if [[ $REPO_URL == *"pro" ]]; then 31 | exit 1 32 | fi 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: 16 36 | - name: Build Frontend 37 | env: 38 | CI: "" 39 | run: | 40 | cd web/default 41 | npm install 42 | REACT_APP_VERSION=$(git describe --tags) npm run build 43 | cd ../.. 44 | - name: Set up Go 45 | uses: actions/setup-go@v3 46 | with: 47 | go-version: '>=1.18.0' 48 | - name: Build Backend 49 | run: | 50 | go mod download 51 | go build -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(git describe --tags)'" -o one-api.exe 52 | - name: Release 53 | uses: softprops/action-gh-release@v1 54 | if: startsWith(github.ref, 'refs/tags/') 55 | with: 56 | files: one-api.exe 57 | draft: true 58 | generate_release_notes: true 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | upload 4 | *.exe 5 | *.db 6 | build 7 | *.db-journal 8 | logs 9 | data 10 | /web/node_modules 11 | cmd.md 12 | .env 13 | /one-api 14 | temp 15 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM node:16 AS builder 2 | 3 | WORKDIR /web 4 | COPY ./VERSION . 5 | COPY ./web . 6 | 7 | RUN npm install --prefix /web/default & \ 8 | npm install --prefix /web/berry & \ 9 | npm install --prefix /web/air & \ 10 | wait 11 | 12 | RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/default & \ 13 | DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/berry & \ 14 | DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/air & \ 15 | wait 16 | 17 | FROM golang:alpine AS builder2 18 | 19 | RUN apk add --no-cache \ 20 | gcc \ 21 | musl-dev \ 22 | sqlite-dev \ 23 | build-base 24 | 25 | ENV GO111MODULE=on \ 26 | CGO_ENABLED=1 \ 27 | GOOS=linux 28 | 29 | WORKDIR /build 30 | 31 | ADD go.mod go.sum ./ 32 | RUN go mod download 33 | 34 | COPY . . 35 | COPY --from=builder /web/build ./web/build 36 | 37 | RUN go build -trimpath -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)' -linkmode external -extldflags '-static'" -o one-api 38 | 39 | FROM alpine:latest 40 | 41 | RUN apk add --no-cache ca-certificates tzdata 42 | 43 | COPY --from=builder2 /build/one-api / 44 | 45 | EXPOSE 3000 46 | WORKDIR /data 47 | ENTRYPOINT ["/one-api"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 JustSong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songquanpeng/one-api/8df4a2670b98266bd287c698243fff327d9748cf/VERSION -------------------------------------------------------------------------------- /bin/migration_v0.2-v0.3.sql: -------------------------------------------------------------------------------- 1 | UPDATE users 2 | SET quota = quota + ( 3 | SELECT SUM(remain_quota) 4 | FROM tokens 5 | WHERE tokens.user_id = users.id 6 | ) 7 | -------------------------------------------------------------------------------- /bin/migration_v0.3-v0.4.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO abilities (`group`, model, channel_id, enabled) 2 | SELECT c.`group`, m.model, c.id, 1 3 | FROM channels c 4 | CROSS JOIN ( 5 | SELECT 'gpt-3.5-turbo' AS model UNION ALL 6 | SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL 7 | SELECT 'gpt-4' AS model UNION ALL 8 | SELECT 'gpt-4-0314' AS model 9 | ) AS m 10 | WHERE c.status = 1 11 | AND NOT EXISTS ( 12 | SELECT 1 13 | FROM abilities a 14 | WHERE a.`group` = c.`group` 15 | AND a.model = m.model 16 | AND a.channel_id = c.id 17 | ); 18 | -------------------------------------------------------------------------------- /bin/time_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "Usage: time_test.sh []" 5 | exit 1 6 | fi 7 | 8 | domain=$1 9 | key=$2 10 | count=$3 11 | model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo 12 | 13 | total_time=0 14 | times=() 15 | 16 | for ((i=1; i<=count; i++)); do 17 | result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \ 18 | https://"$domain"/v1/chat/completions \ 19 | -H "Content-Type: application/json" \ 20 | -H "Authorization: Bearer $key" \ 21 | -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}') 22 | http_code=$(echo "$result" | awk '{print $1}') 23 | time=$(echo "$result" | awk '{print $2}') 24 | echo "HTTP status code: $http_code, Time taken: $time" 25 | total_time=$(bc <<< "$total_time + $time") 26 | times+=("$time") 27 | done 28 | 29 | average_time=$(echo "scale=4; $total_time / $count" | bc) 30 | 31 | sum_of_squares=0 32 | for time in "${times[@]}"; do 33 | difference=$(echo "scale=4; $time - $average_time" | bc) 34 | square=$(echo "scale=4; $difference * $difference" | bc) 35 | sum_of_squares=$(echo "scale=4; $sum_of_squares + $square" | bc) 36 | done 37 | 38 | standard_deviation=$(echo "scale=4; sqrt($sum_of_squares / $count)" | bc) 39 | 40 | echo "Average time: $average_time±$standard_deviation" 41 | -------------------------------------------------------------------------------- /common/blacklist/main.go: -------------------------------------------------------------------------------- 1 | package blacklist 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var blackList sync.Map 9 | 10 | func init() { 11 | blackList = sync.Map{} 12 | } 13 | 14 | func userId2Key(id int) string { 15 | return fmt.Sprintf("userid_%d", id) 16 | } 17 | 18 | func BanUser(id int) { 19 | blackList.Store(userId2Key(id), true) 20 | } 21 | 22 | func UnbanUser(id int) { 23 | blackList.Delete(userId2Key(id)) 24 | } 25 | 26 | func IsUserBanned(id int) bool { 27 | _, ok := blackList.Load(userId2Key(id)) 28 | return ok 29 | } 30 | -------------------------------------------------------------------------------- /common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "time" 4 | 5 | var StartTime = time.Now().Unix() // unit: second 6 | var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change 7 | -------------------------------------------------------------------------------- /common/conv/any.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | func AsString(v any) string { 4 | str, _ := v.(string) 5 | return str 6 | } 7 | -------------------------------------------------------------------------------- /common/crypto.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func Password2Hash(password string) (string, error) { 6 | passwordBytes := []byte(password) 7 | hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) 8 | return string(hashedPassword), err 9 | } 10 | 11 | func ValidatePasswordAndHash(password string, hash string) bool { 12 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 13 | return err == nil 14 | } 15 | -------------------------------------------------------------------------------- /common/ctxkey/key.go: -------------------------------------------------------------------------------- 1 | package ctxkey 2 | 3 | const ( 4 | Config = "config" 5 | Id = "id" 6 | Username = "username" 7 | Role = "role" 8 | Status = "status" 9 | Channel = "channel" 10 | ChannelId = "channel_id" 11 | SpecificChannelId = "specific_channel_id" 12 | RequestModel = "request_model" 13 | ConvertedRequest = "converted_request" 14 | OriginalModel = "original_model" 15 | Group = "group" 16 | ModelMapping = "model_mapping" 17 | ChannelName = "channel_name" 18 | TokenId = "token_id" 19 | TokenName = "token_name" 20 | BaseURL = "base_url" 21 | AvailableModels = "available_models" 22 | KeyRequestBody = "key_request_body" 23 | SystemPrompt = "system_prompt" 24 | ) 25 | -------------------------------------------------------------------------------- /common/database.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/songquanpeng/one-api/common/env" 5 | ) 6 | 7 | var UsingSQLite = false 8 | var UsingPostgreSQL = false 9 | var UsingMySQL = false 10 | 11 | var SQLitePath = "one-api.db" 12 | var SQLiteBusyTimeout = env.Int("SQLITE_BUSY_TIMEOUT", 3000) 13 | -------------------------------------------------------------------------------- /common/embed-file-system.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "embed" 5 | "github.com/gin-contrib/static" 6 | "io/fs" 7 | "net/http" 8 | ) 9 | 10 | // Credit: https://github.com/gin-contrib/static/issues/19 11 | 12 | type embedFileSystem struct { 13 | http.FileSystem 14 | } 15 | 16 | func (e embedFileSystem) Exists(prefix string, path string) bool { 17 | _, err := e.Open(path) 18 | return err == nil 19 | } 20 | 21 | func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { 22 | efs, err := fs.Sub(fsEmbed, targetPath) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return embedFileSystem{ 27 | FileSystem: http.FS(efs), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/env/helper.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | func Bool(env string, defaultValue bool) bool { 9 | if env == "" || os.Getenv(env) == "" { 10 | return defaultValue 11 | } 12 | return os.Getenv(env) == "true" 13 | } 14 | 15 | func Int(env string, defaultValue int) int { 16 | if env == "" || os.Getenv(env) == "" { 17 | return defaultValue 18 | } 19 | num, err := strconv.Atoi(os.Getenv(env)) 20 | if err != nil { 21 | return defaultValue 22 | } 23 | return num 24 | } 25 | 26 | func Float64(env string, defaultValue float64) float64 { 27 | if env == "" || os.Getenv(env) == "" { 28 | return defaultValue 29 | } 30 | num, err := strconv.ParseFloat(os.Getenv(env), 64) 31 | if err != nil { 32 | return defaultValue 33 | } 34 | return num 35 | } 36 | 37 | func String(env string, defaultValue string) string { 38 | if env == "" || os.Getenv(env) == "" { 39 | return defaultValue 40 | } 41 | return os.Getenv(env) 42 | } 43 | -------------------------------------------------------------------------------- /common/gin.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/songquanpeng/one-api/common/ctxkey" 11 | ) 12 | 13 | func GetRequestBody(c *gin.Context) ([]byte, error) { 14 | requestBody, _ := c.Get(ctxkey.KeyRequestBody) 15 | if requestBody != nil { 16 | return requestBody.([]byte), nil 17 | } 18 | requestBody, err := io.ReadAll(c.Request.Body) 19 | if err != nil { 20 | return nil, err 21 | } 22 | _ = c.Request.Body.Close() 23 | c.Set(ctxkey.KeyRequestBody, requestBody) 24 | return requestBody.([]byte), nil 25 | } 26 | 27 | func UnmarshalBodyReusable(c *gin.Context, v any) error { 28 | requestBody, err := GetRequestBody(c) 29 | if err != nil { 30 | return err 31 | } 32 | contentType := c.Request.Header.Get("Content-Type") 33 | if strings.HasPrefix(contentType, "application/json") { 34 | err = json.Unmarshal(requestBody, &v) 35 | } else { 36 | c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) 37 | err = c.ShouldBind(&v) 38 | } 39 | if err != nil { 40 | return err 41 | } 42 | // Reset request body 43 | c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) 44 | return nil 45 | } 46 | 47 | func SetEventStreamHeaders(c *gin.Context) { 48 | c.Writer.Header().Set("Content-Type", "text/event-stream") 49 | c.Writer.Header().Set("Cache-Control", "no-cache") 50 | c.Writer.Header().Set("Connection", "keep-alive") 51 | c.Writer.Header().Set("Transfer-Encoding", "chunked") 52 | c.Writer.Header().Set("X-Accel-Buffering", "no") 53 | } 54 | -------------------------------------------------------------------------------- /common/helper/key.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | const ( 4 | RequestIdKey = "X-Oneapi-Request-Id" 5 | ) 6 | -------------------------------------------------------------------------------- /common/helper/time.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func GetTimestamp() int64 { 9 | return time.Now().Unix() 10 | } 11 | 12 | func GetTimeString() string { 13 | now := time.Now() 14 | return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) 15 | } 16 | 17 | // CalcElapsedTime return the elapsed time in milliseconds (ms) 18 | func CalcElapsedTime(start time.Time) int64 { 19 | return time.Now().Sub(start).Milliseconds() 20 | } 21 | -------------------------------------------------------------------------------- /common/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | //go:embed locales/*.json 12 | var localesFS embed.FS 13 | 14 | var ( 15 | translations = make(map[string]map[string]string) 16 | defaultLang = "en" 17 | ContextKey = "i18n" 18 | ) 19 | 20 | // Init loads all translation files from embedded filesystem 21 | func Init() error { 22 | entries, err := localesFS.ReadDir("locales") 23 | if err != nil { 24 | return err 25 | } 26 | 27 | for _, entry := range entries { 28 | if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { 29 | continue 30 | } 31 | 32 | langCode := strings.TrimSuffix(entry.Name(), ".json") 33 | content, err := localesFS.ReadFile("locales/" + entry.Name()) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | var translation map[string]string 39 | if err := json.Unmarshal(content, &translation); err != nil { 40 | return err 41 | } 42 | translations[langCode] = translation 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func GetLang(c *gin.Context) string { 49 | rawLang, ok := c.Get(ContextKey) 50 | if !ok { 51 | return defaultLang 52 | } 53 | lang, _ := rawLang.(string) 54 | if lang != "" { 55 | return lang 56 | } 57 | return defaultLang 58 | } 59 | 60 | func Translate(c *gin.Context, message string) string { 61 | lang := GetLang(c) 62 | return translateHelper(lang, message) 63 | } 64 | 65 | func translateHelper(lang, message string) string { 66 | if trans, ok := translations[lang]; ok { 67 | if translated, exists := trans[message]; exists { 68 | return translated 69 | } 70 | } 71 | return message 72 | } 73 | -------------------------------------------------------------------------------- /common/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalid_input": "Invalid input, please check your input", 3 | "send_email_failed": "failed to send email: ", 4 | "invalid_parameter": "invalid parameter" 5 | } 6 | -------------------------------------------------------------------------------- /common/i18n/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalid_input": "无效的输入,请检查您的输入", 3 | "send_email_failed": "发送邮件失败:", 4 | "invalid_parameter": "无效的参数" 5 | } 6 | -------------------------------------------------------------------------------- /common/init.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/songquanpeng/one-api/common/config" 7 | "github.com/songquanpeng/one-api/common/logger" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | var ( 14 | Port = flag.Int("port", 3000, "the listening port") 15 | PrintVersion = flag.Bool("version", false, "print version and exit") 16 | PrintHelp = flag.Bool("help", false, "print help and exit") 17 | LogDir = flag.String("log-dir", "./logs", "specify the log directory") 18 | ) 19 | 20 | func printHelp() { 21 | fmt.Println("One API " + Version + " - All in one API service for OpenAI API.") 22 | fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.") 23 | fmt.Println("GitHub: https://github.com/songquanpeng/one-api") 24 | fmt.Println("Usage: one-api [--port ] [--log-dir ] [--version] [--help]") 25 | } 26 | 27 | func Init() { 28 | flag.Parse() 29 | 30 | if *PrintVersion { 31 | fmt.Println(Version) 32 | os.Exit(0) 33 | } 34 | 35 | if *PrintHelp { 36 | printHelp() 37 | os.Exit(0) 38 | } 39 | 40 | if os.Getenv("SESSION_SECRET") != "" { 41 | if os.Getenv("SESSION_SECRET") == "random_string" { 42 | logger.SysError("SESSION_SECRET is set to an example value, please change it to a random string.") 43 | } else { 44 | config.SessionSecret = os.Getenv("SESSION_SECRET") 45 | } 46 | } 47 | if os.Getenv("SQLITE_PATH") != "" { 48 | SQLitePath = os.Getenv("SQLITE_PATH") 49 | } 50 | if *LogDir != "" { 51 | var err error 52 | *LogDir, err = filepath.Abs(*LogDir) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | if _, err := os.Stat(*LogDir); os.IsNotExist(err) { 57 | err = os.Mkdir(*LogDir, 0777) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | } 62 | logger.LogDir = *LogDir 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /common/logger/constants.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | var LogDir string 4 | -------------------------------------------------------------------------------- /common/message/main.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "github.com/songquanpeng/one-api/common/config" 6 | ) 7 | 8 | const ( 9 | ByAll = "all" 10 | ByEmail = "email" 11 | ByMessagePusher = "message_pusher" 12 | ) 13 | 14 | func Notify(by string, title string, description string, content string) error { 15 | if by == ByEmail { 16 | return SendEmail(title, config.RootUserEmail, content) 17 | } 18 | if by == ByMessagePusher { 19 | return SendMessage(title, description, content) 20 | } 21 | return fmt.Errorf("unknown notify method: %s", by) 22 | } 23 | -------------------------------------------------------------------------------- /common/message/message-pusher.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "github.com/songquanpeng/one-api/common/config" 8 | "net/http" 9 | ) 10 | 11 | type request struct { 12 | Title string `json:"title"` 13 | Description string `json:"description"` 14 | Content string `json:"content"` 15 | URL string `json:"url"` 16 | Channel string `json:"channel"` 17 | Token string `json:"token"` 18 | } 19 | 20 | type response struct { 21 | Success bool `json:"success"` 22 | Message string `json:"message"` 23 | } 24 | 25 | func SendMessage(title string, description string, content string) error { 26 | if config.MessagePusherAddress == "" { 27 | return errors.New("message pusher address is not set") 28 | } 29 | req := request{ 30 | Title: title, 31 | Description: description, 32 | Content: content, 33 | Token: config.MessagePusherToken, 34 | } 35 | data, err := json.Marshal(req) 36 | if err != nil { 37 | return err 38 | } 39 | resp, err := http.Post(config.MessagePusherAddress, 40 | "application/json", bytes.NewBuffer(data)) 41 | if err != nil { 42 | return err 43 | } 44 | var res response 45 | err = json.NewDecoder(resp.Body).Decode(&res) 46 | if err != nil { 47 | return err 48 | } 49 | if !res.Success { 50 | return errors.New(res.Message) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /common/message/template.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/songquanpeng/one-api/common/config" 7 | ) 8 | 9 | // EmailTemplate 生成美观的 HTML 邮件内容 10 | func EmailTemplate(title, content string) string { 11 | return fmt.Sprintf(` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

%s

22 |
23 |
24 | %s 25 |
26 |
27 |

此邮件由系统自动发送,请勿直接回复

28 |

%s

29 |
30 |
31 | 32 | 33 | `, title, content, config.SystemName) 34 | } 35 | -------------------------------------------------------------------------------- /common/network/ip.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/songquanpeng/one-api/common/logger" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | func splitSubnets(subnets string) []string { 12 | res := strings.Split(subnets, ",") 13 | for i := 0; i < len(res); i++ { 14 | res[i] = strings.TrimSpace(res[i]) 15 | } 16 | return res 17 | } 18 | 19 | func isValidSubnet(subnet string) error { 20 | _, _, err := net.ParseCIDR(subnet) 21 | if err != nil { 22 | return fmt.Errorf("failed to parse subnet: %w", err) 23 | } 24 | return nil 25 | } 26 | 27 | func isIpInSubnet(ctx context.Context, ip string, subnet string) bool { 28 | _, ipNet, err := net.ParseCIDR(subnet) 29 | if err != nil { 30 | logger.Errorf(ctx, "failed to parse subnet: %s", err.Error()) 31 | return false 32 | } 33 | return ipNet.Contains(net.ParseIP(ip)) 34 | } 35 | 36 | func IsValidSubnets(subnets string) error { 37 | for _, subnet := range splitSubnets(subnets) { 38 | if err := isValidSubnet(subnet); err != nil { 39 | return err 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func IsIpInSubnets(ctx context.Context, ip string, subnets string) bool { 46 | for _, subnet := range splitSubnets(subnets) { 47 | if isIpInSubnet(ctx, ip, subnet) { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /common/network/ip_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestIsIpInSubnet(t *testing.T) { 11 | ctx := context.Background() 12 | ip1 := "192.168.0.5" 13 | ip2 := "125.216.250.89" 14 | subnet := "192.168.0.0/24" 15 | Convey("TestIsIpInSubnet", t, func() { 16 | So(isIpInSubnet(ctx, ip1, subnet), ShouldBeTrue) 17 | So(isIpInSubnet(ctx, ip2, subnet), ShouldBeFalse) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /common/random/main.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func GetUUID() string { 11 | code := uuid.New().String() 12 | code = strings.Replace(code, "-", "", -1) 13 | return code 14 | } 15 | 16 | const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 17 | const keyNumbers = "0123456789" 18 | 19 | func init() { 20 | rand.Seed(time.Now().UnixNano()) 21 | } 22 | 23 | func GenerateKey() string { 24 | rand.Seed(time.Now().UnixNano()) 25 | key := make([]byte, 48) 26 | for i := 0; i < 16; i++ { 27 | key[i] = keyChars[rand.Intn(len(keyChars))] 28 | } 29 | uuid_ := GetUUID() 30 | for i := 0; i < 32; i++ { 31 | c := uuid_[i] 32 | if i%2 == 0 && c >= 'a' && c <= 'z' { 33 | c = c - 'a' + 'A' 34 | } 35 | key[i+16] = c 36 | } 37 | return string(key) 38 | } 39 | 40 | func GetRandomString(length int) string { 41 | rand.Seed(time.Now().UnixNano()) 42 | key := make([]byte, length) 43 | for i := 0; i < length; i++ { 44 | key[i] = keyChars[rand.Intn(len(keyChars))] 45 | } 46 | return string(key) 47 | } 48 | 49 | func GetRandomNumberString(length int) string { 50 | rand.Seed(time.Now().UnixNano()) 51 | key := make([]byte, length) 52 | for i := 0; i < length; i++ { 53 | key[i] = keyNumbers[rand.Intn(len(keyNumbers))] 54 | } 55 | return string(key) 56 | } 57 | 58 | // RandRange returns a random number between min and max (max is not included) 59 | func RandRange(min, max int) int { 60 | return min + rand.Intn(max-min) 61 | } 62 | -------------------------------------------------------------------------------- /common/rate-limit.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type InMemoryRateLimiter struct { 9 | store map[string]*[]int64 10 | mutex sync.Mutex 11 | expirationDuration time.Duration 12 | } 13 | 14 | func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) { 15 | if l.store == nil { 16 | l.mutex.Lock() 17 | if l.store == nil { 18 | l.store = make(map[string]*[]int64) 19 | l.expirationDuration = expirationDuration 20 | if expirationDuration > 0 { 21 | go l.clearExpiredItems() 22 | } 23 | } 24 | l.mutex.Unlock() 25 | } 26 | } 27 | 28 | func (l *InMemoryRateLimiter) clearExpiredItems() { 29 | for { 30 | time.Sleep(l.expirationDuration) 31 | l.mutex.Lock() 32 | now := time.Now().Unix() 33 | for key := range l.store { 34 | queue := l.store[key] 35 | size := len(*queue) 36 | if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) { 37 | delete(l.store, key) 38 | } 39 | } 40 | l.mutex.Unlock() 41 | } 42 | } 43 | 44 | // Request parameter duration's unit is seconds 45 | func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool { 46 | l.mutex.Lock() 47 | defer l.mutex.Unlock() 48 | // [old <-- new] 49 | queue, ok := l.store[key] 50 | now := time.Now().Unix() 51 | if ok { 52 | if len(*queue) < maxRequestNum { 53 | *queue = append(*queue, now) 54 | return true 55 | } else { 56 | if now-(*queue)[0] >= duration { 57 | *queue = (*queue)[1:] 58 | *queue = append(*queue, now) 59 | return true 60 | } else { 61 | return false 62 | } 63 | } 64 | } else { 65 | s := make([]int64, 0, maxRequestNum) 66 | l.store[key] = &s 67 | *(l.store[key]) = append(*(l.store[key]), now) 68 | } 69 | return true 70 | } 71 | -------------------------------------------------------------------------------- /common/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/songquanpeng/one-api/common" 10 | ) 11 | 12 | func StringData(c *gin.Context, str string) { 13 | str = strings.TrimPrefix(str, "data: ") 14 | str = strings.TrimSuffix(str, "\r") 15 | c.Render(-1, common.CustomEvent{Data: "data: " + str}) 16 | c.Writer.Flush() 17 | } 18 | 19 | func ObjectData(c *gin.Context, object interface{}) error { 20 | jsonData, err := json.Marshal(object) 21 | if err != nil { 22 | return fmt.Errorf("error marshalling object: %w", err) 23 | } 24 | StringData(c, string(jsonData)) 25 | return nil 26 | } 27 | 28 | func Done(c *gin.Context) { 29 | StringData(c, "[DONE]") 30 | } 31 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "github.com/songquanpeng/one-api/common/config" 6 | ) 7 | 8 | func LogQuota(quota int64) string { 9 | if config.DisplayInCurrencyEnabled { 10 | return fmt.Sprintf("$%.6f 额度", float64(quota)/config.QuotaPerUnit) 11 | } else { 12 | return fmt.Sprintf("%d 点额度", quota) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/utils/array.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func DeDuplication(slice []string) []string { 4 | m := make(map[string]bool) 5 | for _, v := range slice { 6 | m[v] = true 7 | } 8 | result := make([]string, 0, len(m)) 9 | for v := range m { 10 | result = append(result, v) 11 | } 12 | return result 13 | } 14 | -------------------------------------------------------------------------------- /common/validate.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/go-playground/validator/v10" 4 | 5 | var Validate *validator.Validate 6 | 7 | func init() { 8 | Validate = validator.New() 9 | } 10 | -------------------------------------------------------------------------------- /controller/group.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | billingratio "github.com/songquanpeng/one-api/relay/billing/ratio" 6 | "net/http" 7 | ) 8 | 9 | func GetGroups(c *gin.Context) { 10 | groupNames := make([]string, 0) 11 | for groupName := range billingratio.GroupRatio { 12 | groupNames = append(groupNames, groupName) 13 | } 14 | c.JSON(http.StatusOK, gin.H{ 15 | "success": true, 16 | "message": "", 17 | "data": groupNames, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | one-api: 5 | image: "${REGISTRY:-docker.io}/justsong/one-api:latest" 6 | container_name: one-api 7 | restart: always 8 | command: --log-dir /app/logs 9 | ports: 10 | - "3000:3000" 11 | volumes: 12 | - ./data/oneapi:/data 13 | - ./logs:/app/logs 14 | environment: 15 | - SQL_DSN=oneapi:123456@tcp(db:3306)/one-api # 修改此行,或注释掉以使用 SQLite 作为数据库 16 | - REDIS_CONN_STRING=redis://redis 17 | - SESSION_SECRET=random_string # 修改为随机字符串 18 | - TZ=Asia/Shanghai 19 | # - NODE_TYPE=slave # 多机部署时从节点取消注释该行 20 | # - SYNC_FREQUENCY=60 # 需要定期从数据库加载数据时取消注释该行 21 | # - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行 22 | depends_on: 23 | - redis 24 | - db 25 | healthcheck: 26 | test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ] 27 | interval: 30s 28 | timeout: 10s 29 | retries: 3 30 | 31 | redis: 32 | image: "${REGISTRY:-docker.io}/redis:latest" 33 | container_name: redis 34 | restart: always 35 | 36 | db: 37 | image: "${REGISTRY:-docker.io}/mysql:8.2.0" 38 | restart: always 39 | container_name: mysql 40 | volumes: 41 | - ./data/mysql:/var/lib/mysql # 挂载目录,持久化存储 42 | ports: 43 | - '3306:3306' 44 | environment: 45 | TZ: Asia/Shanghai # 设置时区 46 | MYSQL_ROOT_PASSWORD: 'OneAPI@justsong' # 设置 root 用户的密码 47 | MYSQL_USER: oneapi # 创建专用用户 48 | MYSQL_PASSWORD: '123456' # 设置专用用户密码 49 | MYSQL_DATABASE: one-api # 自动创建数据库 -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # 使用 API 操控 & 扩展 One API 2 | > 欢迎提交 PR 在此放上你的拓展项目。 3 | 4 | 例如,虽然 One API 本身没有直接支持支付,但是你可以通过系统扩展的 API 来实现支付功能。 5 | 6 | 又或者你想自定义渠道管理策略,也可以通过 API 来实现渠道的禁用与启用。 7 | 8 | ## 鉴权 9 | One API 支持两种鉴权方式:Cookie 和 Token,对于 Token,参照下图获取: 10 | 11 | ![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/c15281a7-83ed-47cb-a1f6-913cb6bf4a7c) 12 | 13 | 之后,将 Token 作为请求头的 Authorization 字段的值即可,例如下面使用 Token 调用测试渠道的 API: 14 | ![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/1273b7ae-cb60-4c0d-93a6-b1cbc039c4f8) 15 | 16 | ## 请求格式与响应格式 17 | One API 使用 JSON 格式进行请求和响应。 18 | 19 | 对于响应体,一般格式如下: 20 | ```json 21 | { 22 | "message": "请求信息", 23 | "success": true, 24 | "data": {} 25 | } 26 | ``` 27 | 28 | ## API 列表 29 | > 当前 API 列表不全,请自行通过浏览器抓取前端请求 30 | 31 | 如果现有的 API 没有办法满足你的需求,欢迎提交 issue 讨论。 32 | 33 | ### 获取当前登录用户信息 34 | **GET** `/api/user/self` 35 | 36 | ### 为给定用户充值额度 37 | **POST** `/api/topup` 38 | ```json 39 | { 40 | "user_id": 1, 41 | "quota": 100000, 42 | "remark": "充值 100000 额度" 43 | } 44 | ``` 45 | 46 | ## 其他 47 | ### 充值链接上的附加参数 48 | One API 会在用户点击充值按钮的时候,将用户的信息和充值信息附加在链接上,例如: 49 | `https://example.com?username=root&user_id=1&transaction_id=4b3eed80-55d5-443f-bd44-fb18c648c837` 50 | 51 | 你可以通过解析链接上的参数来获取用户信息和充值信息,然后调用 API 来为用户充值。 52 | 53 | 注意,不是所有主题都支持该功能,欢迎 PR 补齐。 -------------------------------------------------------------------------------- /middleware/cache.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func Cache() func(c *gin.Context) { 8 | return func(c *gin.Context) { 9 | if c.Request.RequestURI == "/" { 10 | c.Header("Cache-Control", "no-cache") 11 | } else { 12 | c.Header("Cache-Control", "max-age=604800") // one week 13 | } 14 | c.Next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func CORS() gin.HandlerFunc { 9 | config := cors.DefaultConfig() 10 | config.AllowAllOrigins = true 11 | config.AllowCredentials = true 12 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} 13 | config.AllowHeaders = []string{"*"} 14 | return cors.New(config) 15 | } 16 | -------------------------------------------------------------------------------- /middleware/gzip.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "compress/gzip" 5 | "github.com/gin-gonic/gin" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | func GzipDecodeMiddleware() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | if c.GetHeader("Content-Encoding") == "gzip" { 13 | gzipReader, err := gzip.NewReader(c.Request.Body) 14 | if err != nil { 15 | c.AbortWithStatus(http.StatusBadRequest) 16 | return 17 | } 18 | defer gzipReader.Close() 19 | 20 | // Replace the request body with the decompressed data 21 | c.Request.Body = io.NopCloser(gzipReader) 22 | } 23 | 24 | // Continue processing the request 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /middleware/language.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/songquanpeng/one-api/common/i18n" 9 | ) 10 | 11 | func Language() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | lang := c.GetHeader("Accept-Language") 14 | if lang == "" { 15 | lang = "en" 16 | } 17 | if strings.HasPrefix(strings.ToLower(lang), "zh") { 18 | lang = "zh-CN" 19 | } else { 20 | lang = "en" 21 | } 22 | c.Set(i18n.ContextKey, lang) 23 | c.Next() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/songquanpeng/one-api/common/helper" 7 | ) 8 | 9 | func SetUpLogger(server *gin.Engine) { 10 | server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 11 | var requestID string 12 | if param.Keys != nil { 13 | requestID = param.Keys[helper.RequestIdKey].(string) 14 | } 15 | return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", 16 | param.TimeStamp.Format("2006/01/02 - 15:04:05"), 17 | requestID, 18 | param.StatusCode, 19 | param.Latency, 20 | param.ClientIP, 21 | param.Method, 22 | param.Path, 23 | ) 24 | })) 25 | } 26 | -------------------------------------------------------------------------------- /middleware/recover.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/songquanpeng/one-api/common" 7 | "github.com/songquanpeng/one-api/common/logger" 8 | "net/http" 9 | "runtime/debug" 10 | ) 11 | 12 | func RelayPanicRecover() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | defer func() { 15 | if err := recover(); err != nil { 16 | ctx := c.Request.Context() 17 | logger.Errorf(ctx, fmt.Sprintf("panic detected: %v", err)) 18 | logger.Errorf(ctx, fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack()))) 19 | logger.Errorf(ctx, fmt.Sprintf("request: %s %s", c.Request.Method, c.Request.URL.Path)) 20 | body, _ := common.GetRequestBody(c) 21 | logger.Errorf(ctx, fmt.Sprintf("request body: %s", string(body))) 22 | c.JSON(http.StatusInternalServerError, gin.H{ 23 | "error": gin.H{ 24 | "message": fmt.Sprintf("Panic detected, error: %v. Please submit an issue with the related log here: https://github.com/songquanpeng/one-api", err), 25 | "type": "one_api_panic", 26 | }, 27 | }) 28 | c.Abort() 29 | } 30 | }() 31 | c.Next() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /middleware/request-id.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/songquanpeng/one-api/common/helper" 7 | ) 8 | 9 | func RequestId() func(c *gin.Context) { 10 | return func(c *gin.Context) { 11 | id := helper.GenRequestID() 12 | c.Set(helper.RequestIdKey, id) 13 | ctx := helper.SetRequestID(c.Request.Context(), id) 14 | c.Request = c.Request.WithContext(ctx) 15 | c.Header(helper.RequestIdKey, id) 16 | c.Next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /monitor/manage.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/songquanpeng/one-api/common/config" 8 | "github.com/songquanpeng/one-api/relay/model" 9 | ) 10 | 11 | func ShouldDisableChannel(err *model.Error, statusCode int) bool { 12 | if !config.AutomaticDisableChannelEnabled { 13 | return false 14 | } 15 | if err == nil { 16 | return false 17 | } 18 | if statusCode == http.StatusUnauthorized { 19 | return true 20 | } 21 | switch err.Type { 22 | case "insufficient_quota", "authentication_error", "permission_error", "forbidden": 23 | return true 24 | } 25 | if err.Code == "invalid_api_key" || err.Code == "account_deactivated" { 26 | return true 27 | } 28 | 29 | lowerMessage := strings.ToLower(err.Message) 30 | if strings.Contains(lowerMessage, "your access was terminated") || 31 | strings.Contains(lowerMessage, "violation of our policies") || 32 | strings.Contains(lowerMessage, "your credit balance is too low") || 33 | strings.Contains(lowerMessage, "organization has been disabled") || 34 | strings.Contains(lowerMessage, "credit") || 35 | strings.Contains(lowerMessage, "balance") || 36 | strings.Contains(lowerMessage, "permission denied") || 37 | strings.Contains(lowerMessage, "organization has been restricted") || // groq 38 | strings.Contains(lowerMessage, "api key not valid") || // gemini 39 | strings.Contains(lowerMessage, "api key expired") || // gemini 40 | strings.Contains(lowerMessage, "已欠费") { 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | func ShouldEnableChannel(err error, openAIErr *model.Error) bool { 47 | if !config.AutomaticEnableChannelEnabled { 48 | return false 49 | } 50 | if err != nil { 51 | return false 52 | } 53 | if openAIErr != nil { 54 | return false 55 | } 56 | return true 57 | } 58 | -------------------------------------------------------------------------------- /one-api.service: -------------------------------------------------------------------------------- 1 | # File path: /etc/systemd/system/one-api.service 2 | # sudo systemctl daemon-reload 3 | # sudo systemctl start one-api 4 | # sudo systemctl enable one-api 5 | # sudo systemctl status one-api 6 | [Unit] 7 | Description=One API Service 8 | After=network.target 9 | 10 | [Service] 11 | User=ubuntu # 注意修改用户名 12 | WorkingDirectory=/path/to/one-api # 注意修改路径 13 | ExecStart=/path/to/one-api/one-api --port 3000 --log-dir /path/to/one-api/logs # 注意修改路径和端口号 14 | Restart=always 15 | RestartSec=5 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | [//]: # (请按照以下格式关联 issue) 2 | [//]: # (请在提交 PR 前确认所提交的功能可用,需要附上截图,谢谢) 3 | [//]: # (项目维护者一般仅在周末处理 PR,因此如若未能及时回复希望能理解) 4 | [//]: # (开发者交流群:910657413) 5 | [//]: # (请在提交 PR 之前删除上面的注释) 6 | 7 | close #issue_number 8 | 9 | 我已确认该 PR 已自测通过,相关截图如下: 10 | (此处放上测试通过的截图,如果不涉及前端改动或从 UI 上无法看出,请放终端启动成功的截图) 11 | -------------------------------------------------------------------------------- /relay/adaptor/ai360/constants.go: -------------------------------------------------------------------------------- 1 | package ai360 2 | 3 | var ModelList = []string{ 4 | "360GPT_S2_V9", 5 | "embedding-bert-512-v1", 6 | "embedding_s1_v1", 7 | "semantic_similarity_s1_v1", 8 | } 9 | -------------------------------------------------------------------------------- /relay/adaptor/aiproxy/constants.go: -------------------------------------------------------------------------------- 1 | package aiproxy 2 | 3 | import "github.com/songquanpeng/one-api/relay/adaptor/openai" 4 | 5 | var ModelList = []string{""} 6 | 7 | func init() { 8 | ModelList = openai.ModelList 9 | } 10 | -------------------------------------------------------------------------------- /relay/adaptor/aiproxy/model.go: -------------------------------------------------------------------------------- 1 | package aiproxy 2 | 3 | type LibraryRequest struct { 4 | Model string `json:"model"` 5 | Query string `json:"query"` 6 | LibraryId string `json:"libraryId"` 7 | Stream bool `json:"stream"` 8 | } 9 | 10 | type LibraryError struct { 11 | ErrCode int `json:"errCode"` 12 | Message string `json:"message"` 13 | } 14 | 15 | type LibraryDocument struct { 16 | Title string `json:"title"` 17 | URL string `json:"url"` 18 | } 19 | 20 | type LibraryResponse struct { 21 | Success bool `json:"success"` 22 | Answer string `json:"answer"` 23 | Documents []LibraryDocument `json:"documents"` 24 | LibraryError 25 | } 26 | 27 | type LibraryStreamResponse struct { 28 | Content string `json:"content"` 29 | Finish bool `json:"finish"` 30 | Model string `json:"model"` 31 | Documents []LibraryDocument `json:"documents"` 32 | } 33 | -------------------------------------------------------------------------------- /relay/adaptor/alibailian/constants.go: -------------------------------------------------------------------------------- 1 | package alibailian 2 | 3 | // https://help.aliyun.com/zh/model-studio/getting-started/models 4 | 5 | var ModelList = []string{ 6 | "qwen-turbo", 7 | "qwen-plus", 8 | "qwen-long", 9 | "qwen-max", 10 | "qwen-coder-plus", 11 | "qwen-coder-plus-latest", 12 | "qwen-coder-turbo", 13 | "qwen-coder-turbo-latest", 14 | "qwen-mt-plus", 15 | "qwen-mt-turbo", 16 | "qwq-32b-preview", 17 | 18 | "deepseek-r1", 19 | "deepseek-v3", 20 | } 21 | -------------------------------------------------------------------------------- /relay/adaptor/alibailian/main.go: -------------------------------------------------------------------------------- 1 | package alibailian 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/songquanpeng/one-api/relay/meta" 7 | "github.com/songquanpeng/one-api/relay/relaymode" 8 | ) 9 | 10 | func GetRequestURL(meta *meta.Meta) (string, error) { 11 | switch meta.Mode { 12 | case relaymode.ChatCompletions: 13 | return fmt.Sprintf("%s/compatible-mode/v1/chat/completions", meta.BaseURL), nil 14 | case relaymode.Embeddings: 15 | return fmt.Sprintf("%s/compatible-mode/v1/embeddings", meta.BaseURL), nil 16 | default: 17 | } 18 | return "", fmt.Errorf("unsupported relay mode %d for ali bailian", meta.Mode) 19 | } 20 | -------------------------------------------------------------------------------- /relay/adaptor/anthropic/constants.go: -------------------------------------------------------------------------------- 1 | package anthropic 2 | 3 | var ModelList = []string{ 4 | "claude-instant-1.2", "claude-2.0", "claude-2.1", 5 | "claude-3-haiku-20240307", 6 | "claude-3-5-haiku-20241022", 7 | "claude-3-5-haiku-latest", 8 | "claude-3-sonnet-20240229", 9 | "claude-3-opus-20240229", 10 | "claude-3-5-sonnet-20240620", 11 | "claude-3-5-sonnet-20241022", 12 | "claude-3-5-sonnet-latest", 13 | } 14 | -------------------------------------------------------------------------------- /relay/adaptor/aws/claude/adapter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" 5 | "github.com/gin-gonic/gin" 6 | "github.com/pkg/errors" 7 | "github.com/songquanpeng/one-api/common/ctxkey" 8 | "github.com/songquanpeng/one-api/relay/adaptor/anthropic" 9 | "github.com/songquanpeng/one-api/relay/adaptor/aws/utils" 10 | "github.com/songquanpeng/one-api/relay/meta" 11 | "github.com/songquanpeng/one-api/relay/model" 12 | ) 13 | 14 | var _ utils.AwsAdapter = new(Adaptor) 15 | 16 | type Adaptor struct { 17 | } 18 | 19 | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { 20 | if request == nil { 21 | return nil, errors.New("request is nil") 22 | } 23 | 24 | claudeReq := anthropic.ConvertRequest(*request) 25 | c.Set(ctxkey.RequestModel, request.Model) 26 | c.Set(ctxkey.ConvertedRequest, claudeReq) 27 | return claudeReq, nil 28 | } 29 | 30 | func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { 31 | if meta.IsStream { 32 | err, usage = StreamHandler(c, awsCli) 33 | } else { 34 | err, usage = Handler(c, awsCli, meta.ActualModelName) 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /relay/adaptor/aws/claude/model.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import "github.com/songquanpeng/one-api/relay/adaptor/anthropic" 4 | 5 | // Request is the request to AWS Claude 6 | // 7 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html 8 | type Request struct { 9 | // AnthropicVersion should be "bedrock-2023-05-31" 10 | AnthropicVersion string `json:"anthropic_version"` 11 | Messages []anthropic.Message `json:"messages"` 12 | System string `json:"system,omitempty"` 13 | MaxTokens int `json:"max_tokens,omitempty"` 14 | Temperature *float64 `json:"temperature,omitempty"` 15 | TopP *float64 `json:"top_p,omitempty"` 16 | TopK int `json:"top_k,omitempty"` 17 | StopSequences []string `json:"stop_sequences,omitempty"` 18 | Tools []anthropic.Tool `json:"tools,omitempty"` 19 | ToolChoice any `json:"tool_choice,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /relay/adaptor/aws/llama3/adapter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" 5 | "github.com/songquanpeng/one-api/common/ctxkey" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/pkg/errors" 9 | "github.com/songquanpeng/one-api/relay/adaptor/aws/utils" 10 | "github.com/songquanpeng/one-api/relay/meta" 11 | "github.com/songquanpeng/one-api/relay/model" 12 | ) 13 | 14 | var _ utils.AwsAdapter = new(Adaptor) 15 | 16 | type Adaptor struct { 17 | } 18 | 19 | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { 20 | if request == nil { 21 | return nil, errors.New("request is nil") 22 | } 23 | 24 | llamaReq := ConvertRequest(*request) 25 | c.Set(ctxkey.RequestModel, request.Model) 26 | c.Set(ctxkey.ConvertedRequest, llamaReq) 27 | return llamaReq, nil 28 | } 29 | 30 | func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { 31 | if meta.IsStream { 32 | err, usage = StreamHandler(c, awsCli) 33 | } else { 34 | err, usage = Handler(c, awsCli, meta.ActualModelName) 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /relay/adaptor/aws/llama3/main_test.go: -------------------------------------------------------------------------------- 1 | package aws_test 2 | 3 | import ( 4 | "testing" 5 | 6 | aws "github.com/songquanpeng/one-api/relay/adaptor/aws/llama3" 7 | relaymodel "github.com/songquanpeng/one-api/relay/model" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRenderPrompt(t *testing.T) { 12 | messages := []relaymodel.Message{ 13 | { 14 | Role: "user", 15 | Content: "What's your name?", 16 | }, 17 | } 18 | prompt := aws.RenderPrompt(messages) 19 | expected := `<|begin_of_text|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|> 20 | ` 21 | assert.Equal(t, expected, prompt) 22 | 23 | messages = []relaymodel.Message{ 24 | { 25 | Role: "system", 26 | Content: "Your name is Kat. You are a detective.", 27 | }, 28 | { 29 | Role: "user", 30 | Content: "What's your name?", 31 | }, 32 | { 33 | Role: "assistant", 34 | Content: "Kat", 35 | }, 36 | { 37 | Role: "user", 38 | Content: "What's your job?", 39 | }, 40 | } 41 | prompt = aws.RenderPrompt(messages) 42 | expected = `<|begin_of_text|><|start_header_id|>system<|end_header_id|>Your name is Kat. You are a detective.<|eot_id|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|>Kat<|eot_id|><|start_header_id|>user<|end_header_id|>What's your job?<|eot_id|><|start_header_id|>assistant<|end_header_id|> 43 | ` 44 | assert.Equal(t, expected, prompt) 45 | } 46 | -------------------------------------------------------------------------------- /relay/adaptor/aws/llama3/model.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // Request is the request to AWS Llama3 4 | // 5 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html 6 | type Request struct { 7 | Prompt string `json:"prompt"` 8 | MaxGenLen int `json:"max_gen_len,omitempty"` 9 | Temperature *float64 `json:"temperature,omitempty"` 10 | TopP *float64 `json:"top_p,omitempty"` 11 | } 12 | 13 | // Response is the response from AWS Llama3 14 | // 15 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html 16 | type Response struct { 17 | Generation string `json:"generation"` 18 | PromptTokenCount int `json:"prompt_token_count"` 19 | GenerationTokenCount int `json:"generation_token_count"` 20 | StopReason string `json:"stop_reason"` 21 | } 22 | 23 | // {'generation': 'Hi', 'prompt_token_count': 15, 'generation_token_count': 1, 'stop_reason': None} 24 | type StreamResponse struct { 25 | Generation string `json:"generation"` 26 | PromptTokenCount int `json:"prompt_token_count"` 27 | GenerationTokenCount int `json:"generation_token_count"` 28 | StopReason string `json:"stop_reason"` 29 | } 30 | -------------------------------------------------------------------------------- /relay/adaptor/aws/registry.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | claude "github.com/songquanpeng/one-api/relay/adaptor/aws/claude" 5 | llama3 "github.com/songquanpeng/one-api/relay/adaptor/aws/llama3" 6 | "github.com/songquanpeng/one-api/relay/adaptor/aws/utils" 7 | ) 8 | 9 | type AwsModelType int 10 | 11 | const ( 12 | AwsClaude AwsModelType = iota + 1 13 | AwsLlama3 14 | ) 15 | 16 | var ( 17 | adaptors = map[string]AwsModelType{} 18 | ) 19 | 20 | func init() { 21 | for model := range claude.AwsModelIDMap { 22 | adaptors[model] = AwsClaude 23 | } 24 | for model := range llama3.AwsModelIDMap { 25 | adaptors[model] = AwsLlama3 26 | } 27 | } 28 | 29 | func GetAdaptor(model string) utils.AwsAdapter { 30 | adaptorType := adaptors[model] 31 | switch adaptorType { 32 | case AwsClaude: 33 | return &claude.Adaptor{} 34 | case AwsLlama3: 35 | return &llama3.Adaptor{} 36 | default: 37 | return nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /relay/adaptor/aws/utils/adaptor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/credentials" 10 | "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" 11 | "github.com/gin-gonic/gin" 12 | "github.com/songquanpeng/one-api/relay/meta" 13 | "github.com/songquanpeng/one-api/relay/model" 14 | ) 15 | 16 | type AwsAdapter interface { 17 | ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) 18 | DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) 19 | } 20 | 21 | type Adaptor struct { 22 | Meta *meta.Meta 23 | AwsClient *bedrockruntime.Client 24 | } 25 | 26 | func (a *Adaptor) Init(meta *meta.Meta) { 27 | a.Meta = meta 28 | a.AwsClient = bedrockruntime.New(bedrockruntime.Options{ 29 | Region: meta.Config.Region, 30 | Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), 31 | }) 32 | } 33 | 34 | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { 35 | return "", nil 36 | } 37 | 38 | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { 39 | return nil 40 | } 41 | 42 | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { 43 | if request == nil { 44 | return nil, errors.New("request is nil") 45 | } 46 | return request, nil 47 | } 48 | 49 | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { 50 | return nil, nil 51 | } 52 | -------------------------------------------------------------------------------- /relay/adaptor/aws/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | 6 | relaymodel "github.com/songquanpeng/one-api/relay/model" 7 | ) 8 | 9 | func WrapErr(err error) *relaymodel.ErrorWithStatusCode { 10 | return &relaymodel.ErrorWithStatusCode{ 11 | StatusCode: http.StatusInternalServerError, 12 | Error: relaymodel.Error{ 13 | Message: err.Error(), 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /relay/adaptor/baichuan/constants.go: -------------------------------------------------------------------------------- 1 | package baichuan 2 | 3 | var ModelList = []string{ 4 | "Baichuan2-Turbo", 5 | "Baichuan2-Turbo-192k", 6 | "Baichuan-Text-Embedding", 7 | } 8 | -------------------------------------------------------------------------------- /relay/adaptor/baidu/constants.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | var ModelList = []string{ 4 | "ERNIE-4.0-8K", 5 | "ERNIE-3.5-8K", 6 | "ERNIE-3.5-8K-0205", 7 | "ERNIE-3.5-8K-1222", 8 | "ERNIE-Bot-8K", 9 | "ERNIE-3.5-4K-0205", 10 | "ERNIE-Speed-8K", 11 | "ERNIE-Speed-128K", 12 | "ERNIE-Lite-8K-0922", 13 | "ERNIE-Lite-8K-0308", 14 | "ERNIE-Tiny-8K", 15 | "BLOOMZ-7B", 16 | "Embedding-V1", 17 | "bge-large-zh", 18 | "bge-large-en", 19 | "tao-8k", 20 | } 21 | -------------------------------------------------------------------------------- /relay/adaptor/baidu/model.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "github.com/songquanpeng/one-api/relay/model" 5 | "time" 6 | ) 7 | 8 | type ChatResponse struct { 9 | Id string `json:"id"` 10 | Object string `json:"object"` 11 | Created int64 `json:"created"` 12 | Result string `json:"result"` 13 | IsTruncated bool `json:"is_truncated"` 14 | NeedClearHistory bool `json:"need_clear_history"` 15 | Usage model.Usage `json:"usage"` 16 | Error 17 | } 18 | 19 | type ChatStreamResponse struct { 20 | ChatResponse 21 | SentenceId int `json:"sentence_id"` 22 | IsEnd bool `json:"is_end"` 23 | } 24 | 25 | type EmbeddingRequest struct { 26 | Input []string `json:"input"` 27 | } 28 | 29 | type EmbeddingData struct { 30 | Object string `json:"object"` 31 | Embedding []float64 `json:"embedding"` 32 | Index int `json:"index"` 33 | } 34 | 35 | type EmbeddingResponse struct { 36 | Id string `json:"id"` 37 | Object string `json:"object"` 38 | Created int64 `json:"created"` 39 | Data []EmbeddingData `json:"data"` 40 | Usage model.Usage `json:"usage"` 41 | Error 42 | } 43 | 44 | type AccessToken struct { 45 | AccessToken string `json:"access_token"` 46 | Error string `json:"error,omitempty"` 47 | ErrorDescription string `json:"error_description,omitempty"` 48 | ExpiresIn int64 `json:"expires_in,omitempty"` 49 | ExpiresAt time.Time `json:"-"` 50 | } 51 | -------------------------------------------------------------------------------- /relay/adaptor/baiduv2/constants.go: -------------------------------------------------------------------------------- 1 | package baiduv2 2 | 3 | // https://console.bce.baidu.com/support/?_=1692863460488×tamp=1739074632076#/api?product=QIANFAN&project=%E5%8D%83%E5%B8%86ModelBuilder&parent=%E5%AF%B9%E8%AF%9DChat%20V2&api=v2%2Fchat%2Fcompletions&method=post 4 | // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu#%E6%94%AF%E6%8C%81%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8 5 | 6 | var ModelList = []string{ 7 | "ernie-4.0-8k-latest", 8 | "ernie-4.0-8k-preview", 9 | "ernie-4.0-8k", 10 | "ernie-4.0-turbo-8k-latest", 11 | "ernie-4.0-turbo-8k-preview", 12 | "ernie-4.0-turbo-8k", 13 | "ernie-4.0-turbo-128k", 14 | "ernie-3.5-8k-preview", 15 | "ernie-3.5-8k", 16 | "ernie-3.5-128k", 17 | "ernie-speed-8k", 18 | "ernie-speed-128k", 19 | "ernie-speed-pro-128k", 20 | "ernie-lite-8k", 21 | "ernie-lite-pro-128k", 22 | "ernie-tiny-8k", 23 | "ernie-char-8k", 24 | "ernie-char-fiction-8k", 25 | "ernie-novel-8k", 26 | "deepseek-v3", 27 | "deepseek-r1", 28 | "deepseek-r1-distill-qwen-32b", 29 | "deepseek-r1-distill-qwen-14b", 30 | } 31 | -------------------------------------------------------------------------------- /relay/adaptor/baiduv2/main.go: -------------------------------------------------------------------------------- 1 | package baiduv2 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/songquanpeng/one-api/relay/meta" 7 | "github.com/songquanpeng/one-api/relay/relaymode" 8 | ) 9 | 10 | func GetRequestURL(meta *meta.Meta) (string, error) { 11 | switch meta.Mode { 12 | case relaymode.ChatCompletions: 13 | return fmt.Sprintf("%s/v2/chat/completions", meta.BaseURL), nil 14 | default: 15 | } 16 | return "", fmt.Errorf("unsupported relay mode %d for baidu v2", meta.Mode) 17 | } 18 | -------------------------------------------------------------------------------- /relay/adaptor/cloudflare/constant.go: -------------------------------------------------------------------------------- 1 | package cloudflare 2 | 3 | var ModelList = []string{ 4 | "@cf/meta/llama-3.1-8b-instruct", 5 | "@cf/meta/llama-2-7b-chat-fp16", 6 | "@cf/meta/llama-2-7b-chat-int8", 7 | "@cf/mistral/mistral-7b-instruct-v0.1", 8 | "@hf/thebloke/deepseek-coder-6.7b-base-awq", 9 | "@hf/thebloke/deepseek-coder-6.7b-instruct-awq", 10 | "@cf/deepseek-ai/deepseek-math-7b-base", 11 | "@cf/deepseek-ai/deepseek-math-7b-instruct", 12 | "@cf/thebloke/discolm-german-7b-v1-awq", 13 | "@cf/tiiuae/falcon-7b-instruct", 14 | "@cf/google/gemma-2b-it-lora", 15 | "@hf/google/gemma-7b-it", 16 | "@cf/google/gemma-7b-it-lora", 17 | "@hf/nousresearch/hermes-2-pro-mistral-7b", 18 | "@hf/thebloke/llama-2-13b-chat-awq", 19 | "@cf/meta-llama/llama-2-7b-chat-hf-lora", 20 | "@cf/meta/llama-3-8b-instruct", 21 | "@hf/thebloke/llamaguard-7b-awq", 22 | "@hf/thebloke/mistral-7b-instruct-v0.1-awq", 23 | "@hf/mistralai/mistral-7b-instruct-v0.2", 24 | "@cf/mistral/mistral-7b-instruct-v0.2-lora", 25 | "@hf/thebloke/neural-chat-7b-v3-1-awq", 26 | "@cf/openchat/openchat-3.5-0106", 27 | "@hf/thebloke/openhermes-2.5-mistral-7b-awq", 28 | "@cf/microsoft/phi-2", 29 | "@cf/qwen/qwen1.5-0.5b-chat", 30 | "@cf/qwen/qwen1.5-1.8b-chat", 31 | "@cf/qwen/qwen1.5-14b-chat-awq", 32 | "@cf/qwen/qwen1.5-7b-chat-awq", 33 | "@cf/defog/sqlcoder-7b-2", 34 | "@hf/nexusflow/starling-lm-7b-beta", 35 | "@cf/tinyllama/tinyllama-1.1b-chat-v1.0", 36 | "@hf/thebloke/zephyr-7b-beta-awq", 37 | } 38 | -------------------------------------------------------------------------------- /relay/adaptor/cloudflare/model.go: -------------------------------------------------------------------------------- 1 | package cloudflare 2 | 3 | import "github.com/songquanpeng/one-api/relay/model" 4 | 5 | type Request struct { 6 | Messages []model.Message `json:"messages,omitempty"` 7 | Lora string `json:"lora,omitempty"` 8 | MaxTokens int `json:"max_tokens,omitempty"` 9 | Prompt string `json:"prompt,omitempty"` 10 | Raw bool `json:"raw,omitempty"` 11 | Stream bool `json:"stream,omitempty"` 12 | Temperature *float64 `json:"temperature,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /relay/adaptor/cohere/constant.go: -------------------------------------------------------------------------------- 1 | package cohere 2 | 3 | var ModelList = []string{ 4 | "command", "command-nightly", 5 | "command-light", "command-light-nightly", 6 | "command-r", "command-r-plus", 7 | } 8 | 9 | func init() { 10 | num := len(ModelList) 11 | for i := 0; i < num; i++ { 12 | ModelList = append(ModelList, ModelList[i]+"-internet") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /relay/adaptor/common.go: -------------------------------------------------------------------------------- 1 | package adaptor 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/songquanpeng/one-api/common/client" 8 | "github.com/songquanpeng/one-api/relay/meta" 9 | "io" 10 | "net/http" 11 | ) 12 | 13 | func SetupCommonRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) { 14 | req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) 15 | req.Header.Set("Accept", c.Request.Header.Get("Accept")) 16 | if meta.IsStream && c.Request.Header.Get("Accept") == "" { 17 | req.Header.Set("Accept", "text/event-stream") 18 | } 19 | } 20 | 21 | func DoRequestHelper(a Adaptor, c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { 22 | fullRequestURL, err := a.GetRequestURL(meta) 23 | if err != nil { 24 | return nil, fmt.Errorf("get request url failed: %w", err) 25 | } 26 | req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) 27 | if err != nil { 28 | return nil, fmt.Errorf("new request failed: %w", err) 29 | } 30 | err = a.SetupRequestHeader(c, req, meta) 31 | if err != nil { 32 | return nil, fmt.Errorf("setup request header failed: %w", err) 33 | } 34 | resp, err := DoRequest(c, req) 35 | if err != nil { 36 | return nil, fmt.Errorf("do request failed: %w", err) 37 | } 38 | return resp, nil 39 | } 40 | 41 | func DoRequest(c *gin.Context, req *http.Request) (*http.Response, error) { 42 | resp, err := client.HTTPClient.Do(req) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if resp == nil { 47 | return nil, errors.New("resp is nil") 48 | } 49 | _ = req.Body.Close() 50 | _ = c.Request.Body.Close() 51 | return resp, nil 52 | } 53 | -------------------------------------------------------------------------------- /relay/adaptor/coze/constant/contenttype/define.go: -------------------------------------------------------------------------------- 1 | package contenttype 2 | 3 | const ( 4 | Text = "text" 5 | ) 6 | -------------------------------------------------------------------------------- /relay/adaptor/coze/constant/event/define.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | const ( 4 | Message = "message" 5 | Done = "done" 6 | Error = "error" 7 | ) 8 | -------------------------------------------------------------------------------- /relay/adaptor/coze/constant/messagetype/define.go: -------------------------------------------------------------------------------- 1 | package messagetype 2 | 3 | const ( 4 | Answer = "answer" 5 | FollowUp = "follow_up" 6 | ) 7 | -------------------------------------------------------------------------------- /relay/adaptor/coze/constants.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | var ModelList = []string{} 4 | -------------------------------------------------------------------------------- /relay/adaptor/coze/helper.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | import "github.com/songquanpeng/one-api/relay/adaptor/coze/constant/event" 4 | 5 | func event2StopReason(e *string) string { 6 | if e == nil || *e == event.Message { 7 | return "" 8 | } 9 | return "stop" 10 | } 11 | -------------------------------------------------------------------------------- /relay/adaptor/coze/model.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | type Message struct { 4 | Role string `json:"role"` 5 | Type string `json:"type"` 6 | Content string `json:"content"` 7 | ContentType string `json:"content_type"` 8 | } 9 | 10 | type ErrorInformation struct { 11 | Code int `json:"code"` 12 | Msg string `json:"msg"` 13 | } 14 | 15 | type Request struct { 16 | ConversationId string `json:"conversation_id,omitempty"` 17 | BotId string `json:"bot_id"` 18 | User string `json:"user"` 19 | Query string `json:"query"` 20 | ChatHistory []Message `json:"chat_history,omitempty"` 21 | Stream bool `json:"stream"` 22 | } 23 | 24 | type Response struct { 25 | ConversationId string `json:"conversation_id,omitempty"` 26 | Messages []Message `json:"messages,omitempty"` 27 | Code int `json:"code,omitempty"` 28 | Msg string `json:"msg,omitempty"` 29 | } 30 | 31 | type StreamResponse struct { 32 | Event string `json:"event,omitempty"` 33 | Message *Message `json:"message,omitempty"` 34 | IsFinish bool `json:"is_finish,omitempty"` 35 | Index int `json:"index,omitempty"` 36 | ConversationId string `json:"conversation_id,omitempty"` 37 | ErrorInformation *ErrorInformation `json:"error_information,omitempty"` 38 | } 39 | -------------------------------------------------------------------------------- /relay/adaptor/deepl/constants.go: -------------------------------------------------------------------------------- 1 | package deepl 2 | 3 | // https://developers.deepl.com/docs/api-reference/glossaries 4 | 5 | var ModelList = []string{ 6 | "deepl-zh", 7 | "deepl-en", 8 | "deepl-ja", 9 | } 10 | -------------------------------------------------------------------------------- /relay/adaptor/deepl/helper.go: -------------------------------------------------------------------------------- 1 | package deepl 2 | 3 | import "strings" 4 | 5 | func parseLangFromModelName(modelName string) string { 6 | parts := strings.Split(modelName, "-") 7 | if len(parts) == 1 { 8 | return "ZH" 9 | } 10 | return parts[1] 11 | } 12 | -------------------------------------------------------------------------------- /relay/adaptor/deepl/model.go: -------------------------------------------------------------------------------- 1 | package deepl 2 | 3 | type Request struct { 4 | Text []string `json:"text"` 5 | TargetLang string `json:"target_lang"` 6 | } 7 | 8 | type Translation struct { 9 | DetectedSourceLanguage string `json:"detected_source_language,omitempty"` 10 | Text string `json:"text,omitempty"` 11 | } 12 | 13 | type Response struct { 14 | Translations []Translation `json:"translations,omitempty"` 15 | Message string `json:"message,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /relay/adaptor/deepseek/constants.go: -------------------------------------------------------------------------------- 1 | package deepseek 2 | 3 | var ModelList = []string{ 4 | "deepseek-chat", 5 | "deepseek-reasoner", 6 | } 7 | -------------------------------------------------------------------------------- /relay/adaptor/doubao/constants.go: -------------------------------------------------------------------------------- 1 | package doubao 2 | 3 | // https://console.volcengine.com/ark/region:ark+cn-beijing/model 4 | 5 | var ModelList = []string{ 6 | "Doubao-pro-128k", 7 | "Doubao-pro-32k", 8 | "Doubao-pro-4k", 9 | "Doubao-lite-128k", 10 | "Doubao-lite-32k", 11 | "Doubao-lite-4k", 12 | "Doubao-embedding", 13 | } 14 | -------------------------------------------------------------------------------- /relay/adaptor/doubao/main.go: -------------------------------------------------------------------------------- 1 | package doubao 2 | 3 | import ( 4 | "fmt" 5 | "github.com/songquanpeng/one-api/relay/meta" 6 | "github.com/songquanpeng/one-api/relay/relaymode" 7 | ) 8 | 9 | func GetRequestURL(meta *meta.Meta) (string, error) { 10 | switch meta.Mode { 11 | case relaymode.ChatCompletions: 12 | return fmt.Sprintf("%s/api/v3/chat/completions", meta.BaseURL), nil 13 | case relaymode.Embeddings: 14 | return fmt.Sprintf("%s/api/v3/embeddings", meta.BaseURL), nil 15 | default: 16 | } 17 | return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) 18 | } 19 | -------------------------------------------------------------------------------- /relay/adaptor/gemini/constants.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "github.com/songquanpeng/one-api/relay/adaptor/geminiv2" 5 | ) 6 | 7 | // https://ai.google.dev/models/gemini 8 | 9 | var ModelList = geminiv2.ModelList 10 | 11 | // ModelsSupportSystemInstruction is the list of models that support system instruction. 12 | // 13 | // https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/system-instructions 14 | var ModelsSupportSystemInstruction = []string{ 15 | // "gemini-1.0-pro-002", 16 | // "gemini-1.5-flash", "gemini-1.5-flash-001", "gemini-1.5-flash-002", 17 | // "gemini-1.5-flash-8b", 18 | // "gemini-1.5-pro", "gemini-1.5-pro-001", "gemini-1.5-pro-002", 19 | // "gemini-1.5-pro-experimental", 20 | "gemini-2.0-flash", "gemini-2.0-flash-exp", 21 | "gemini-2.0-flash-thinking-exp-01-21", 22 | } 23 | 24 | // IsModelSupportSystemInstruction check if the model support system instruction. 25 | // 26 | // Because the main version of Go is 1.20, slice.Contains cannot be used 27 | func IsModelSupportSystemInstruction(model string) bool { 28 | for _, m := range ModelsSupportSystemInstruction { 29 | if m == model { 30 | return true 31 | } 32 | } 33 | 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /relay/adaptor/geminiv2/constants.go: -------------------------------------------------------------------------------- 1 | package geminiv2 2 | 3 | // https://ai.google.dev/models/gemini 4 | 5 | var ModelList = []string{ 6 | "gemini-pro", "gemini-1.0-pro", 7 | // "gemma-2-2b-it", "gemma-2-9b-it", "gemma-2-27b-it", 8 | "gemini-1.5-flash", "gemini-1.5-flash-8b", 9 | "gemini-1.5-pro", "gemini-1.5-pro-experimental", 10 | "text-embedding-004", "aqa", 11 | "gemini-2.0-flash", "gemini-2.0-flash-exp", 12 | "gemini-2.0-flash-lite-preview-02-05", 13 | "gemini-2.0-flash-thinking-exp-01-21", 14 | "gemini-2.0-pro-exp-02-05", 15 | } 16 | -------------------------------------------------------------------------------- /relay/adaptor/geminiv2/main.go: -------------------------------------------------------------------------------- 1 | package geminiv2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/songquanpeng/one-api/relay/meta" 8 | ) 9 | 10 | func GetRequestURL(meta *meta.Meta) (string, error) { 11 | baseURL := strings.TrimSuffix(meta.BaseURL, "/") 12 | requestPath := strings.TrimPrefix(meta.RequestURLPath, "/v1") 13 | return fmt.Sprintf("%s%s", baseURL, requestPath), nil 14 | } 15 | -------------------------------------------------------------------------------- /relay/adaptor/groq/constants.go: -------------------------------------------------------------------------------- 1 | package groq 2 | 3 | // https://console.groq.com/docs/models 4 | 5 | var ModelList = []string{ 6 | "gemma2-9b-it", 7 | "llama-3.1-70b-versatile", 8 | "llama-3.1-8b-instant", 9 | "llama-3.2-11b-text-preview", 10 | "llama-3.2-11b-vision-preview", 11 | "llama-3.2-1b-preview", 12 | "llama-3.2-3b-preview", 13 | "llama-3.2-90b-text-preview", 14 | "llama-3.2-90b-vision-preview", 15 | "llama-guard-3-8b", 16 | "llama3-70b-8192", 17 | "llama3-8b-8192", 18 | "llama3-groq-70b-8192-tool-use-preview", 19 | "llama3-groq-8b-8192-tool-use-preview", 20 | "llava-v1.5-7b-4096-preview", 21 | "mixtral-8x7b-32768", 22 | "distil-whisper-large-v3-en", 23 | "whisper-large-v3", 24 | "whisper-large-v3-turbo", 25 | "deepseek-r1-distill-llama-70b-specdec", 26 | "deepseek-r1-distill-llama-70b", 27 | } 28 | -------------------------------------------------------------------------------- /relay/adaptor/interface.go: -------------------------------------------------------------------------------- 1 | package adaptor 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/songquanpeng/one-api/relay/meta" 6 | "github.com/songquanpeng/one-api/relay/model" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type Adaptor interface { 12 | Init(meta *meta.Meta) 13 | GetRequestURL(meta *meta.Meta) (string, error) 14 | SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error 15 | ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) 16 | ConvertImageRequest(request *model.ImageRequest) (any, error) 17 | DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) 18 | DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) 19 | GetModelList() []string 20 | GetChannelName() string 21 | } 22 | -------------------------------------------------------------------------------- /relay/adaptor/lingyiwanwu/constants.go: -------------------------------------------------------------------------------- 1 | package lingyiwanwu 2 | 3 | // https://platform.lingyiwanwu.com/docs 4 | 5 | var ModelList = []string{ 6 | "yi-34b-chat-0205", 7 | "yi-34b-chat-200k", 8 | "yi-vl-plus", 9 | } 10 | -------------------------------------------------------------------------------- /relay/adaptor/minimax/constants.go: -------------------------------------------------------------------------------- 1 | package minimax 2 | 3 | // https://www.minimaxi.com/document/guides/chat-model/V2?id=65e0736ab2845de20908e2dd 4 | 5 | var ModelList = []string{ 6 | "abab6.5-chat", 7 | "abab6.5s-chat", 8 | "abab6-chat", 9 | "abab5.5-chat", 10 | "abab5.5s-chat", 11 | "MiniMax-VL-01", 12 | "MiniMax-Text-01", 13 | } 14 | -------------------------------------------------------------------------------- /relay/adaptor/minimax/main.go: -------------------------------------------------------------------------------- 1 | package minimax 2 | 3 | import ( 4 | "fmt" 5 | "github.com/songquanpeng/one-api/relay/meta" 6 | "github.com/songquanpeng/one-api/relay/relaymode" 7 | ) 8 | 9 | func GetRequestURL(meta *meta.Meta) (string, error) { 10 | if meta.Mode == relaymode.ChatCompletions { 11 | return fmt.Sprintf("%s/v1/text/chatcompletion_v2", meta.BaseURL), nil 12 | } 13 | return "", fmt.Errorf("unsupported relay mode %d for minimax", meta.Mode) 14 | } 15 | -------------------------------------------------------------------------------- /relay/adaptor/mistral/constants.go: -------------------------------------------------------------------------------- 1 | package mistral 2 | 3 | var ModelList = []string{ 4 | "open-mistral-7b", 5 | "open-mixtral-8x7b", 6 | "mistral-small-latest", 7 | "mistral-medium-latest", 8 | "mistral-large-latest", 9 | "mistral-embed", 10 | } 11 | -------------------------------------------------------------------------------- /relay/adaptor/moonshot/constants.go: -------------------------------------------------------------------------------- 1 | package moonshot 2 | 3 | var ModelList = []string{ 4 | "moonshot-v1-8k", 5 | "moonshot-v1-32k", 6 | "moonshot-v1-128k", 7 | } 8 | -------------------------------------------------------------------------------- /relay/adaptor/novita/constants.go: -------------------------------------------------------------------------------- 1 | package novita 2 | 3 | // https://novita.ai/llm-api 4 | 5 | var ModelList = []string{ 6 | "meta-llama/llama-3-8b-instruct", 7 | "meta-llama/llama-3-70b-instruct", 8 | "nousresearch/hermes-2-pro-llama-3-8b", 9 | "nousresearch/nous-hermes-llama2-13b", 10 | "mistralai/mistral-7b-instruct", 11 | "cognitivecomputations/dolphin-mixtral-8x22b", 12 | "sao10k/l3-70b-euryale-v2.1", 13 | "sophosympatheia/midnight-rose-70b", 14 | "gryphe/mythomax-l2-13b", 15 | "Nous-Hermes-2-Mixtral-8x7B-DPO", 16 | "lzlv_70b", 17 | "teknium/openhermes-2.5-mistral-7b", 18 | "microsoft/wizardlm-2-8x22b", 19 | } 20 | -------------------------------------------------------------------------------- /relay/adaptor/novita/main.go: -------------------------------------------------------------------------------- 1 | package novita 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/songquanpeng/one-api/relay/meta" 7 | "github.com/songquanpeng/one-api/relay/relaymode" 8 | ) 9 | 10 | func GetRequestURL(meta *meta.Meta) (string, error) { 11 | if meta.Mode == relaymode.ChatCompletions { 12 | return fmt.Sprintf("%s/chat/completions", meta.BaseURL), nil 13 | } 14 | return "", fmt.Errorf("unsupported relay mode %d for novita", meta.Mode) 15 | } 16 | -------------------------------------------------------------------------------- /relay/adaptor/ollama/constants.go: -------------------------------------------------------------------------------- 1 | package ollama 2 | 3 | var ModelList = []string{ 4 | "codellama:7b-instruct", 5 | "llama2:7b", 6 | "llama2:latest", 7 | "llama3:latest", 8 | "phi3:latest", 9 | "qwen:0.5b-chat", 10 | "qwen:7b", 11 | } 12 | -------------------------------------------------------------------------------- /relay/adaptor/openai/constants.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | var ModelList = []string{ 4 | "gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", 5 | "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", 6 | "gpt-3.5-turbo-instruct", 7 | "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", 8 | "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", 9 | "gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", 10 | "gpt-4o", "gpt-4o-2024-05-13", 11 | "gpt-4o-2024-08-06", 12 | "gpt-4o-2024-11-20", 13 | "chatgpt-4o-latest", 14 | "gpt-4o-mini", "gpt-4o-mini-2024-07-18", 15 | "gpt-4-vision-preview", 16 | "text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large", 17 | "text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003", 18 | "text-moderation-latest", "text-moderation-stable", 19 | "text-davinci-edit-001", 20 | "davinci-002", "babbage-002", 21 | "dall-e-2", "dall-e-3", 22 | "whisper-1", 23 | "tts-1", "tts-1-1106", "tts-1-hd", "tts-1-hd-1106", 24 | "o1", "o1-2024-12-17", 25 | "o1-preview", "o1-preview-2024-09-12", 26 | "o1-mini", "o1-mini-2024-09-12", 27 | } 28 | -------------------------------------------------------------------------------- /relay/adaptor/openai/helper.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/songquanpeng/one-api/relay/channeltype" 8 | "github.com/songquanpeng/one-api/relay/model" 9 | ) 10 | 11 | func ResponseText2Usage(responseText string, modelName string, promptTokens int) *model.Usage { 12 | usage := &model.Usage{} 13 | usage.PromptTokens = promptTokens 14 | usage.CompletionTokens = CountTokenText(responseText, modelName) 15 | usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens 16 | return usage 17 | } 18 | 19 | func GetFullRequestURL(baseURL string, requestURL string, channelType int) string { 20 | if channelType == channeltype.OpenAICompatible { 21 | return fmt.Sprintf("%s%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(requestURL, "/v1")) 22 | } 23 | fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) 24 | 25 | if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") { 26 | switch channelType { 27 | case channeltype.OpenAI: 28 | fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1")) 29 | case channeltype.Azure: 30 | fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments")) 31 | } 32 | } 33 | return fullRequestURL 34 | } 35 | -------------------------------------------------------------------------------- /relay/adaptor/openai/image.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/gin-gonic/gin" 7 | "github.com/songquanpeng/one-api/relay/model" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func ImageHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { 13 | var imageResponse ImageResponse 14 | responseBody, err := io.ReadAll(resp.Body) 15 | 16 | if err != nil { 17 | return ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil 18 | } 19 | err = resp.Body.Close() 20 | if err != nil { 21 | return ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil 22 | } 23 | err = json.Unmarshal(responseBody, &imageResponse) 24 | if err != nil { 25 | return ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil 26 | } 27 | 28 | resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) 29 | 30 | for k, v := range resp.Header { 31 | c.Writer.Header().Set(k, v[0]) 32 | } 33 | c.Writer.WriteHeader(resp.StatusCode) 34 | 35 | _, err = io.Copy(c.Writer, resp.Body) 36 | if err != nil { 37 | return ErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil 38 | } 39 | err = resp.Body.Close() 40 | if err != nil { 41 | return ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil 42 | } 43 | return nil, nil 44 | } 45 | -------------------------------------------------------------------------------- /relay/adaptor/openai/util.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/songquanpeng/one-api/common/logger" 8 | "github.com/songquanpeng/one-api/relay/model" 9 | ) 10 | 11 | func ErrorWrapper(err error, code string, statusCode int) *model.ErrorWithStatusCode { 12 | logger.Error(context.TODO(), fmt.Sprintf("[%s]%+v", code, err)) 13 | 14 | Error := model.Error{ 15 | Message: err.Error(), 16 | Type: "one_api_error", 17 | Code: code, 18 | } 19 | return &model.ErrorWithStatusCode{ 20 | Error: Error, 21 | StatusCode: statusCode, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /relay/adaptor/palm/constants.go: -------------------------------------------------------------------------------- 1 | package palm 2 | 3 | var ModelList = []string{ 4 | "PaLM-2", 5 | } 6 | -------------------------------------------------------------------------------- /relay/adaptor/palm/model.go: -------------------------------------------------------------------------------- 1 | package palm 2 | 3 | import ( 4 | "github.com/songquanpeng/one-api/relay/model" 5 | ) 6 | 7 | type ChatMessage struct { 8 | Author string `json:"author"` 9 | Content string `json:"content"` 10 | } 11 | 12 | type Filter struct { 13 | Reason string `json:"reason"` 14 | Message string `json:"message"` 15 | } 16 | 17 | type Prompt struct { 18 | Messages []ChatMessage `json:"messages"` 19 | } 20 | 21 | type ChatRequest struct { 22 | Prompt Prompt `json:"prompt"` 23 | Temperature *float64 `json:"temperature,omitempty"` 24 | CandidateCount int `json:"candidateCount,omitempty"` 25 | TopP *float64 `json:"topP,omitempty"` 26 | TopK int `json:"topK,omitempty"` 27 | } 28 | 29 | type Error struct { 30 | Code int `json:"code"` 31 | Message string `json:"message"` 32 | Status string `json:"status"` 33 | } 34 | 35 | type ChatResponse struct { 36 | Candidates []ChatMessage `json:"candidates"` 37 | Messages []model.Message `json:"messages"` 38 | Filters []Filter `json:"filters"` 39 | Error Error `json:"error"` 40 | } 41 | -------------------------------------------------------------------------------- /relay/adaptor/siliconflow/constants.go: -------------------------------------------------------------------------------- 1 | package siliconflow 2 | 3 | // https://docs.siliconflow.cn/docs/getting-started 4 | 5 | var ModelList = []string{ 6 | "deepseek-ai/deepseek-llm-67b-chat", 7 | "Qwen/Qwen1.5-14B-Chat", 8 | "Qwen/Qwen1.5-7B-Chat", 9 | "Qwen/Qwen1.5-110B-Chat", 10 | "Qwen/Qwen1.5-32B-Chat", 11 | "01-ai/Yi-1.5-6B-Chat", 12 | "01-ai/Yi-1.5-9B-Chat-16K", 13 | "01-ai/Yi-1.5-34B-Chat-16K", 14 | "THUDM/chatglm3-6b", 15 | "deepseek-ai/DeepSeek-V2-Chat", 16 | "THUDM/glm-4-9b-chat", 17 | "Qwen/Qwen2-72B-Instruct", 18 | "Qwen/Qwen2-7B-Instruct", 19 | "Qwen/Qwen2-57B-A14B-Instruct", 20 | "deepseek-ai/DeepSeek-Coder-V2-Instruct", 21 | "Qwen/Qwen2-1.5B-Instruct", 22 | "internlm/internlm2_5-7b-chat", 23 | "BAAI/bge-large-en-v1.5", 24 | "BAAI/bge-large-zh-v1.5", 25 | "Pro/Qwen/Qwen2-7B-Instruct", 26 | "Pro/Qwen/Qwen2-1.5B-Instruct", 27 | "Pro/Qwen/Qwen1.5-7B-Chat", 28 | "Pro/THUDM/glm-4-9b-chat", 29 | "Pro/THUDM/chatglm3-6b", 30 | "Pro/01-ai/Yi-1.5-9B-Chat-16K", 31 | "Pro/01-ai/Yi-1.5-6B-Chat", 32 | "Pro/google/gemma-2-9b-it", 33 | "Pro/internlm/internlm2_5-7b-chat", 34 | "Pro/meta-llama/Meta-Llama-3-8B-Instruct", 35 | "Pro/mistralai/Mistral-7B-Instruct-v0.2", 36 | } 37 | -------------------------------------------------------------------------------- /relay/adaptor/stepfun/constants.go: -------------------------------------------------------------------------------- 1 | package stepfun 2 | 3 | var ModelList = []string{ 4 | "step-1-8k", 5 | "step-1-32k", 6 | "step-1-128k", 7 | "step-1-256k", 8 | "step-1-flash", 9 | "step-2-16k", 10 | "step-1v-8k", 11 | "step-1v-32k", 12 | "step-1x-medium", 13 | } 14 | -------------------------------------------------------------------------------- /relay/adaptor/tencent/constants.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | var ModelList = []string{ 4 | "hunyuan-lite", 5 | "hunyuan-standard", 6 | "hunyuan-standard-256K", 7 | "hunyuan-pro", 8 | "hunyuan-vision", 9 | "hunyuan-embedding", 10 | } 11 | -------------------------------------------------------------------------------- /relay/adaptor/togetherai/constants.go: -------------------------------------------------------------------------------- 1 | package togetherai 2 | 3 | // https://docs.together.ai/docs/inference-models 4 | 5 | var ModelList = []string{ 6 | "meta-llama/Llama-3-70b-chat-hf", 7 | "deepseek-ai/deepseek-coder-33b-instruct", 8 | "mistralai/Mixtral-8x22B-Instruct-v0.1", 9 | "Qwen/Qwen1.5-72B-Chat", 10 | } 11 | -------------------------------------------------------------------------------- /relay/adaptor/vertexai/claude/model.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import "github.com/songquanpeng/one-api/relay/adaptor/anthropic" 4 | 5 | type Request struct { 6 | // AnthropicVersion must be "vertex-2023-10-16" 7 | AnthropicVersion string `json:"anthropic_version"` 8 | // Model string `json:"model"` 9 | Messages []anthropic.Message `json:"messages"` 10 | System string `json:"system,omitempty"` 11 | MaxTokens int `json:"max_tokens,omitempty"` 12 | StopSequences []string `json:"stop_sequences,omitempty"` 13 | Stream bool `json:"stream,omitempty"` 14 | Temperature *float64 `json:"temperature,omitempty"` 15 | TopP *float64 `json:"top_p,omitempty"` 16 | TopK int `json:"top_k,omitempty"` 17 | Tools []anthropic.Tool `json:"tools,omitempty"` 18 | ToolChoice any `json:"tool_choice,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /relay/adaptor/vertexai/registry.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | claude "github.com/songquanpeng/one-api/relay/adaptor/vertexai/claude" 8 | gemini "github.com/songquanpeng/one-api/relay/adaptor/vertexai/gemini" 9 | "github.com/songquanpeng/one-api/relay/meta" 10 | "github.com/songquanpeng/one-api/relay/model" 11 | ) 12 | 13 | type VertexAIModelType int 14 | 15 | const ( 16 | VerterAIClaude VertexAIModelType = iota + 1 17 | VerterAIGemini 18 | ) 19 | 20 | var modelMapping = map[string]VertexAIModelType{} 21 | var modelList = []string{} 22 | 23 | func init() { 24 | modelList = append(modelList, claude.ModelList...) 25 | for _, model := range claude.ModelList { 26 | modelMapping[model] = VerterAIClaude 27 | } 28 | 29 | modelList = append(modelList, gemini.ModelList...) 30 | for _, model := range gemini.ModelList { 31 | modelMapping[model] = VerterAIGemini 32 | } 33 | } 34 | 35 | type innerAIAdapter interface { 36 | ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) 37 | DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) 38 | } 39 | 40 | func GetAdaptor(model string) innerAIAdapter { 41 | adaptorType := modelMapping[model] 42 | switch adaptorType { 43 | case VerterAIClaude: 44 | return &claude.Adaptor{} 45 | case VerterAIGemini: 46 | return &gemini.Adaptor{} 47 | default: 48 | return nil 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /relay/adaptor/xai/constants.go: -------------------------------------------------------------------------------- 1 | package xai 2 | 3 | //https://console.x.ai/ 4 | 5 | var ModelList = []string{ 6 | "grok-2", 7 | "grok-vision-beta", 8 | "grok-2-vision-1212", 9 | "grok-2-vision", 10 | "grok-2-vision-latest", 11 | "grok-2-1212", 12 | "grok-2-latest", 13 | "grok-beta", 14 | } 15 | -------------------------------------------------------------------------------- /relay/adaptor/xunfei/constants.go: -------------------------------------------------------------------------------- 1 | package xunfei 2 | 3 | var ModelList = []string{ 4 | "Spark-Lite", 5 | "Spark-Pro", 6 | "Spark-Pro-128K", 7 | "Spark-Max", 8 | "Spark-Max-32K", 9 | "Spark-4.0-Ultra", 10 | } 11 | -------------------------------------------------------------------------------- /relay/adaptor/xunfeiv2/constants.go: -------------------------------------------------------------------------------- 1 | package xunfeiv2 2 | 3 | // https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_3-%E8%AF%B7%E6%B1%82%E8%AF%B4%E6%98%8E 4 | 5 | var ModelList = []string{ 6 | "lite", 7 | "generalv3", 8 | "pro-128k", 9 | "generalv3.5", 10 | "max-32k", 11 | "4.0Ultra", 12 | } 13 | -------------------------------------------------------------------------------- /relay/adaptor/zhipu/constants.go: -------------------------------------------------------------------------------- 1 | package zhipu 2 | 3 | // https://open.bigmodel.cn/pricing 4 | 5 | var ModelList = []string{ 6 | "glm-zero-preview", "glm-4-plus", "glm-4-0520", "glm-4-airx", 7 | "glm-4-air", "glm-4-long", "glm-4-flashx", "glm-4-flash", 8 | "glm-4", "glm-3-turbo", 9 | "glm-4v-plus", "glm-4v", "glm-4v-flash", 10 | "cogview-3-plus", "cogview-3", "cogview-3-flash", 11 | "cogviewx", "cogviewx-flash", 12 | "charglm-4", "emohaa", "codegeex-4", 13 | "embedding-2", "embedding-3", 14 | } 15 | -------------------------------------------------------------------------------- /relay/adaptor_test.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "github.com/songquanpeng/one-api/relay/apitype" 6 | "testing" 7 | ) 8 | 9 | func TestGetAdaptor(t *testing.T) { 10 | Convey("get adaptor", t, func() { 11 | for i := 0; i < apitype.Dummy; i++ { 12 | a := GetAdaptor(i) 13 | So(a, ShouldNotBeNil) 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /relay/apitype/define.go: -------------------------------------------------------------------------------- 1 | package apitype 2 | 3 | const ( 4 | OpenAI = iota 5 | Anthropic 6 | PaLM 7 | Baidu 8 | Zhipu 9 | Ali 10 | Xunfei 11 | AIProxyLibrary 12 | Tencent 13 | Gemini 14 | Ollama 15 | AwsClaude 16 | Coze 17 | Cohere 18 | Cloudflare 19 | DeepL 20 | VertexAI 21 | Proxy 22 | Replicate 23 | 24 | Dummy // this one is only for count, do not add any channel after this 25 | ) 26 | -------------------------------------------------------------------------------- /relay/billing/ratio/group.go: -------------------------------------------------------------------------------- 1 | package ratio 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/songquanpeng/one-api/common/logger" 6 | "sync" 7 | ) 8 | 9 | var groupRatioLock sync.RWMutex 10 | var GroupRatio = map[string]float64{ 11 | "default": 1, 12 | "vip": 1, 13 | "svip": 1, 14 | } 15 | 16 | func GroupRatio2JSONString() string { 17 | jsonBytes, err := json.Marshal(GroupRatio) 18 | if err != nil { 19 | logger.SysError("error marshalling model ratio: " + err.Error()) 20 | } 21 | return string(jsonBytes) 22 | } 23 | 24 | func UpdateGroupRatioByJSONString(jsonStr string) error { 25 | groupRatioLock.Lock() 26 | defer groupRatioLock.Unlock() 27 | GroupRatio = make(map[string]float64) 28 | return json.Unmarshal([]byte(jsonStr), &GroupRatio) 29 | } 30 | 31 | func GetGroupRatio(name string) float64 { 32 | groupRatioLock.RLock() 33 | defer groupRatioLock.RUnlock() 34 | ratio, ok := GroupRatio[name] 35 | if !ok { 36 | logger.SysError("group ratio not found: " + name) 37 | return 1 38 | } 39 | return ratio 40 | } 41 | -------------------------------------------------------------------------------- /relay/billing/ratio/image.go: -------------------------------------------------------------------------------- 1 | package ratio 2 | 3 | var ImageSizeRatios = map[string]map[string]float64{ 4 | "dall-e-2": { 5 | "256x256": 1, 6 | "512x512": 1.125, 7 | "1024x1024": 1.25, 8 | }, 9 | "dall-e-3": { 10 | "1024x1024": 1, 11 | "1024x1792": 2, 12 | "1792x1024": 2, 13 | }, 14 | "ali-stable-diffusion-xl": { 15 | "512x1024": 1, 16 | "1024x768": 1, 17 | "1024x1024": 1, 18 | "576x1024": 1, 19 | "1024x576": 1, 20 | }, 21 | "ali-stable-diffusion-v1.5": { 22 | "512x1024": 1, 23 | "1024x768": 1, 24 | "1024x1024": 1, 25 | "576x1024": 1, 26 | "1024x576": 1, 27 | }, 28 | "wanx-v1": { 29 | "1024x1024": 1, 30 | "720x1280": 1, 31 | "1280x720": 1, 32 | }, 33 | "step-1x-medium": { 34 | "256x256": 1, 35 | "512x512": 1, 36 | "768x768": 1, 37 | "1024x1024": 1, 38 | "1280x800": 1, 39 | "800x1280": 1, 40 | }, 41 | } 42 | 43 | var ImageGenerationAmounts = map[string][2]int{ 44 | "dall-e-2": {1, 10}, 45 | "dall-e-3": {1, 1}, // OpenAI allows n=1 currently. 46 | "ali-stable-diffusion-xl": {1, 4}, // Ali 47 | "ali-stable-diffusion-v1.5": {1, 4}, // Ali 48 | "wanx-v1": {1, 4}, // Ali 49 | "cogview-3": {1, 1}, 50 | "step-1x-medium": {1, 1}, 51 | } 52 | 53 | var ImagePromptLengthLimitations = map[string]int{ 54 | "dall-e-2": 1000, 55 | "dall-e-3": 4000, 56 | "ali-stable-diffusion-xl": 4000, 57 | "ali-stable-diffusion-v1.5": 4000, 58 | "wanx-v1": 4000, 59 | "cogview-3": 833, 60 | "step-1x-medium": 4000, 61 | } 62 | 63 | var ImageOriginModelName = map[string]string{ 64 | "ali-stable-diffusion-xl": "stable-diffusion-xl", 65 | "ali-stable-diffusion-v1.5": "stable-diffusion-v1.5", 66 | } 67 | -------------------------------------------------------------------------------- /relay/channeltype/define.go: -------------------------------------------------------------------------------- 1 | package channeltype 2 | 3 | const ( 4 | Unknown = iota 5 | OpenAI 6 | API2D 7 | Azure 8 | CloseAI 9 | OpenAISB 10 | OpenAIMax 11 | OhMyGPT 12 | Custom 13 | Ails 14 | AIProxy 15 | PaLM 16 | API2GPT 17 | AIGC2D 18 | Anthropic 19 | Baidu 20 | Zhipu 21 | Ali 22 | Xunfei 23 | AI360 24 | OpenRouter 25 | AIProxyLibrary 26 | FastGPT 27 | Tencent 28 | Gemini 29 | Moonshot 30 | Baichuan 31 | Minimax 32 | Mistral 33 | Groq 34 | Ollama 35 | LingYiWanWu 36 | StepFun 37 | AwsClaude 38 | Coze 39 | Cohere 40 | DeepSeek 41 | Cloudflare 42 | DeepL 43 | TogetherAI 44 | Doubao 45 | Novita 46 | VertextAI 47 | Proxy 48 | SiliconFlow 49 | XAI 50 | Replicate 51 | BaiduV2 52 | XunfeiV2 53 | AliBailian 54 | OpenAICompatible 55 | GeminiOpenAICompatible 56 | Dummy 57 | ) 58 | -------------------------------------------------------------------------------- /relay/channeltype/helper.go: -------------------------------------------------------------------------------- 1 | package channeltype 2 | 3 | import "github.com/songquanpeng/one-api/relay/apitype" 4 | 5 | func ToAPIType(channelType int) int { 6 | apiType := apitype.OpenAI 7 | switch channelType { 8 | case Anthropic: 9 | apiType = apitype.Anthropic 10 | case Baidu: 11 | apiType = apitype.Baidu 12 | case PaLM: 13 | apiType = apitype.PaLM 14 | case Zhipu: 15 | apiType = apitype.Zhipu 16 | case Ali: 17 | apiType = apitype.Ali 18 | case Xunfei: 19 | apiType = apitype.Xunfei 20 | case AIProxyLibrary: 21 | apiType = apitype.AIProxyLibrary 22 | case Tencent: 23 | apiType = apitype.Tencent 24 | case Gemini: 25 | apiType = apitype.Gemini 26 | case Ollama: 27 | apiType = apitype.Ollama 28 | case AwsClaude: 29 | apiType = apitype.AwsClaude 30 | case Coze: 31 | apiType = apitype.Coze 32 | case Cohere: 33 | apiType = apitype.Cohere 34 | case Cloudflare: 35 | apiType = apitype.Cloudflare 36 | case DeepL: 37 | apiType = apitype.DeepL 38 | case VertextAI: 39 | apiType = apitype.VertexAI 40 | case Replicate: 41 | apiType = apitype.Replicate 42 | case Proxy: 43 | apiType = apitype.Proxy 44 | } 45 | 46 | return apiType 47 | } 48 | -------------------------------------------------------------------------------- /relay/channeltype/url_test.go: -------------------------------------------------------------------------------- 1 | package channeltype 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestChannelBaseURLs(t *testing.T) { 9 | Convey("channel base urls", t, func() { 10 | So(len(ChannelBaseURLs), ShouldEqual, Dummy) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /relay/constant/common.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var StopFinishReason = "stop" 4 | var StreamObject = "chat.completion.chunk" 5 | var NonStreamObject = "chat.completion" 6 | -------------------------------------------------------------------------------- /relay/constant/finishreason/define.go: -------------------------------------------------------------------------------- 1 | package finishreason 2 | 3 | const ( 4 | Stop = "stop" 5 | ) 6 | -------------------------------------------------------------------------------- /relay/constant/role/define.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | const ( 4 | System = "system" 5 | Assistant = "assistant" 6 | ) 7 | -------------------------------------------------------------------------------- /relay/controller/proxy.go: -------------------------------------------------------------------------------- 1 | // Package controller is a package for handling the relay controller 2 | package controller 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/songquanpeng/one-api/common/logger" 10 | "github.com/songquanpeng/one-api/relay" 11 | "github.com/songquanpeng/one-api/relay/adaptor/openai" 12 | "github.com/songquanpeng/one-api/relay/meta" 13 | relaymodel "github.com/songquanpeng/one-api/relay/model" 14 | ) 15 | 16 | // RelayProxyHelper is a helper function to proxy the request to the upstream service 17 | func RelayProxyHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode { 18 | ctx := c.Request.Context() 19 | meta := meta.GetByContext(c) 20 | 21 | adaptor := relay.GetAdaptor(meta.APIType) 22 | if adaptor == nil { 23 | return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) 24 | } 25 | adaptor.Init(meta) 26 | 27 | resp, err := adaptor.DoRequest(c, meta, c.Request.Body) 28 | if err != nil { 29 | logger.Errorf(ctx, "DoRequest failed: %s", err.Error()) 30 | return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) 31 | } 32 | 33 | // do response 34 | _, respErr := adaptor.DoResponse(c, resp, meta) 35 | if respErr != nil { 36 | logger.Errorf(ctx, "respErr is not nil: %+v", respErr) 37 | return respErr 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /relay/controller/validator/validation.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "errors" 5 | "github.com/songquanpeng/one-api/relay/model" 6 | "github.com/songquanpeng/one-api/relay/relaymode" 7 | "math" 8 | ) 9 | 10 | func ValidateTextRequest(textRequest *model.GeneralOpenAIRequest, relayMode int) error { 11 | if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 { 12 | return errors.New("max_tokens is invalid") 13 | } 14 | if textRequest.Model == "" { 15 | return errors.New("model is required") 16 | } 17 | switch relayMode { 18 | case relaymode.Completions: 19 | if textRequest.Prompt == "" { 20 | return errors.New("field prompt is required") 21 | } 22 | case relaymode.ChatCompletions: 23 | if textRequest.Messages == nil || len(textRequest.Messages) == 0 { 24 | return errors.New("field messages is required") 25 | } 26 | case relaymode.Embeddings: 27 | case relaymode.Moderations: 28 | if textRequest.Input == "" { 29 | return errors.New("field input is required") 30 | } 31 | case relaymode.Edits: 32 | if textRequest.Instruction == "" { 33 | return errors.New("field instruction is required") 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /relay/model/constant.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | ContentTypeText = "text" 5 | ContentTypeImageURL = "image_url" 6 | ContentTypeInputAudio = "input_audio" 7 | ) 8 | -------------------------------------------------------------------------------- /relay/model/image.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ImageRequest struct { 4 | Model string `json:"model"` 5 | Prompt string `json:"prompt" binding:"required"` 6 | N int `json:"n,omitempty"` 7 | Size string `json:"size,omitempty"` 8 | Quality string `json:"quality,omitempty"` 9 | ResponseFormat string `json:"response_format,omitempty"` 10 | Style string `json:"style,omitempty"` 11 | User string `json:"user,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /relay/model/misc.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Usage struct { 4 | PromptTokens int `json:"prompt_tokens"` 5 | CompletionTokens int `json:"completion_tokens"` 6 | TotalTokens int `json:"total_tokens"` 7 | 8 | CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details,omitempty"` 9 | } 10 | 11 | type CompletionTokensDetails struct { 12 | ReasoningTokens int `json:"reasoning_tokens"` 13 | AcceptedPredictionTokens int `json:"accepted_prediction_tokens"` 14 | RejectedPredictionTokens int `json:"rejected_prediction_tokens"` 15 | } 16 | 17 | type Error struct { 18 | Message string `json:"message"` 19 | Type string `json:"type"` 20 | Param string `json:"param"` 21 | Code any `json:"code"` 22 | } 23 | 24 | type ErrorWithStatusCode struct { 25 | Error 26 | StatusCode int `json:"status_code"` 27 | } 28 | -------------------------------------------------------------------------------- /relay/model/tool.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Tool struct { 4 | Id string `json:"id,omitempty"` 5 | Type string `json:"type,omitempty"` // when splicing claude tools stream messages, it is empty 6 | Function Function `json:"function"` 7 | } 8 | 9 | type Function struct { 10 | Description string `json:"description,omitempty"` 11 | Name string `json:"name,omitempty"` // when splicing claude tools stream messages, it is empty 12 | Parameters any `json:"parameters,omitempty"` // request 13 | Arguments any `json:"arguments,omitempty"` // response 14 | } 15 | -------------------------------------------------------------------------------- /relay/relaymode/define.go: -------------------------------------------------------------------------------- 1 | package relaymode 2 | 3 | const ( 4 | Unknown = iota 5 | ChatCompletions 6 | Completions 7 | Embeddings 8 | Moderations 9 | ImagesGenerations 10 | Edits 11 | AudioSpeech 12 | AudioTranscription 13 | AudioTranslation 14 | // Proxy is a special relay mode for proxying requests to custom upstream 15 | Proxy 16 | ) 17 | -------------------------------------------------------------------------------- /relay/relaymode/helper.go: -------------------------------------------------------------------------------- 1 | package relaymode 2 | 3 | import "strings" 4 | 5 | func GetByPath(path string) int { 6 | relayMode := Unknown 7 | if strings.HasPrefix(path, "/v1/chat/completions") { 8 | relayMode = ChatCompletions 9 | } else if strings.HasPrefix(path, "/v1/completions") { 10 | relayMode = Completions 11 | } else if strings.HasPrefix(path, "/v1/embeddings") { 12 | relayMode = Embeddings 13 | } else if strings.HasSuffix(path, "embeddings") { 14 | relayMode = Embeddings 15 | } else if strings.HasPrefix(path, "/v1/moderations") { 16 | relayMode = Moderations 17 | } else if strings.HasPrefix(path, "/v1/images/generations") { 18 | relayMode = ImagesGenerations 19 | } else if strings.HasPrefix(path, "/v1/edits") { 20 | relayMode = Edits 21 | } else if strings.HasPrefix(path, "/v1/audio/speech") { 22 | relayMode = AudioSpeech 23 | } else if strings.HasPrefix(path, "/v1/audio/transcriptions") { 24 | relayMode = AudioTranscription 25 | } else if strings.HasPrefix(path, "/v1/audio/translations") { 26 | relayMode = AudioTranslation 27 | } else if strings.HasPrefix(path, "/v1/oneapi/proxy") { 28 | relayMode = Proxy 29 | } 30 | return relayMode 31 | } 32 | -------------------------------------------------------------------------------- /router/dashboard.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-contrib/gzip" 5 | "github.com/gin-gonic/gin" 6 | "github.com/songquanpeng/one-api/controller" 7 | "github.com/songquanpeng/one-api/middleware" 8 | ) 9 | 10 | func SetDashboardRouter(router *gin.Engine) { 11 | apiRouter := router.Group("/") 12 | apiRouter.Use(middleware.CORS()) 13 | apiRouter.Use(gzip.Gzip(gzip.DefaultCompression)) 14 | apiRouter.Use(middleware.GlobalAPIRateLimit()) 15 | apiRouter.Use(middleware.TokenAuth()) 16 | { 17 | apiRouter.GET("/dashboard/billing/subscription", controller.GetSubscription) 18 | apiRouter.GET("/v1/dashboard/billing/subscription", controller.GetSubscription) 19 | apiRouter.GET("/dashboard/billing/usage", controller.GetUsage) 20 | apiRouter.GET("/v1/dashboard/billing/usage", controller.GetUsage) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /router/main.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/songquanpeng/one-api/common/config" 8 | "github.com/songquanpeng/one-api/common/logger" 9 | "net/http" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | func SetRouter(router *gin.Engine, buildFS embed.FS) { 15 | SetApiRouter(router) 16 | SetDashboardRouter(router) 17 | SetRelayRouter(router) 18 | frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL") 19 | if config.IsMasterNode && frontendBaseUrl != "" { 20 | frontendBaseUrl = "" 21 | logger.SysLog("FRONTEND_BASE_URL is ignored on master node") 22 | } 23 | if frontendBaseUrl == "" { 24 | SetWebRouter(router, buildFS) 25 | } else { 26 | frontendBaseUrl = strings.TrimSuffix(frontendBaseUrl, "/") 27 | router.NoRoute(func(c *gin.Context) { 28 | c.Redirect(http.StatusMovedPermanently, fmt.Sprintf("%s%s", frontendBaseUrl, c.Request.RequestURI)) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /router/web.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "github.com/gin-contrib/gzip" 7 | "github.com/gin-contrib/static" 8 | "github.com/gin-gonic/gin" 9 | "github.com/songquanpeng/one-api/common" 10 | "github.com/songquanpeng/one-api/common/config" 11 | "github.com/songquanpeng/one-api/controller" 12 | "github.com/songquanpeng/one-api/middleware" 13 | "net/http" 14 | "strings" 15 | ) 16 | 17 | func SetWebRouter(router *gin.Engine, buildFS embed.FS) { 18 | indexPageData, _ := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", config.Theme)) 19 | router.Use(gzip.Gzip(gzip.DefaultCompression)) 20 | router.Use(middleware.GlobalWebRateLimit()) 21 | router.Use(middleware.Cache()) 22 | router.Use(static.Serve("/", common.EmbedFolder(buildFS, fmt.Sprintf("web/build/%s", config.Theme)))) 23 | router.NoRoute(func(c *gin.Context) { 24 | if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") { 25 | controller.RelayNotFound(c) 26 | return 27 | } 28 | c.Header("Cache-Control", "no-cache") 29 | c.Data(http.StatusOK, "text/html; charset=utf-8", indexPageData) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /web/THEMES: -------------------------------------------------------------------------------- 1 | default 2 | berry 3 | air 4 | -------------------------------------------------------------------------------- /web/air/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea 25 | package-lock.json 26 | yarn.lock -------------------------------------------------------------------------------- /web/air/README.md: -------------------------------------------------------------------------------- 1 | # React Template 2 | 3 | ## Basic Usages 4 | 5 | ```shell 6 | # Runs the app in the development mode 7 | npm start 8 | 9 | # Builds the app for production to the `build` folder 10 | npm run build 11 | ``` 12 | 13 | If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build, 14 | for example: `REACT_APP_SERVER=http://your.domain.com`. 15 | 16 | Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. 17 | 18 | ## Reference 19 | 20 | 1. https://github.com/OIerDb-ng/OIerDb 21 | 2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example -------------------------------------------------------------------------------- /web/air/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@douyinfe/semi-icons": "^2.46.1", 7 | "@douyinfe/semi-ui": "^2.46.1", 8 | "@visactor/react-vchart": "~1.8.8", 9 | "@visactor/vchart": "~1.8.8", 10 | "@visactor/vchart-semi-theme": "~1.8.8", 11 | "axios": "^0.27.2", 12 | "history": "^5.3.0", 13 | "marked": "^4.1.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-dropzone": "^14.2.3", 17 | "react-fireworks": "^1.0.4", 18 | "react-router-dom": "^6.3.0", 19 | "react-scripts": "5.0.1", 20 | "react-telegram-login": "^1.1.2", 21 | "react-toastify": "^9.0.8", 22 | "react-turnstile": "^1.0.5", 23 | "semantic-ui-css": "^2.5.0", 24 | "semantic-ui-react": "^2.1.3", 25 | "usehooks-ts": "^2.9.1" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build && mv -f build ../build/air", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "prettier": "2.8.8", 53 | "typescript": "4.4.2" 54 | }, 55 | "prettier": { 56 | "singleQuote": true, 57 | "jsxSingleQuote": true 58 | }, 59 | "proxy": "http://localhost:3000" 60 | } 61 | -------------------------------------------------------------------------------- /web/air/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songquanpeng/one-api/8df4a2670b98266bd287c698243fff327d9748cf/web/air/public/favicon.ico -------------------------------------------------------------------------------- /web/air/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | One API 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /web/air/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songquanpeng/one-api/8df4a2670b98266bd287c698243fff327d9748cf/web/air/public/logo.png -------------------------------------------------------------------------------- /web/air/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/air/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; 3 | 4 | const Loading = ({ prompt: name = 'page' }) => { 5 | return ( 6 | 7 | 8 | 加载{name}中... 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /web/air/src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | 3 | import { history } from '../helpers'; 4 | 5 | 6 | function PrivateRoute({ children }) { 7 | if (!localStorage.getItem('user')) { 8 | return ; 9 | } 10 | return children; 11 | } 12 | 13 | export { PrivateRoute }; -------------------------------------------------------------------------------- /web/air/src/components/utils.js: -------------------------------------------------------------------------------- 1 | import { API, showError } from '../helpers'; 2 | 3 | export async function getOAuthState() { 4 | const res = await API.get('/api/oauth/state'); 5 | const { success, message, data } = res.data; 6 | if (success) { 7 | return data; 8 | } else { 9 | showError(message); 10 | return ''; 11 | } 12 | } 13 | 14 | export async function onGitHubOAuthClicked(github_client_id) { 15 | const state = await getOAuthState(); 16 | if (!state) return; 17 | window.open( 18 | `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email` 19 | ); 20 | } -------------------------------------------------------------------------------- /web/air/src/constants/common.constant.js: -------------------------------------------------------------------------------- 1 | export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! 2 | -------------------------------------------------------------------------------- /web/air/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './toast.constants'; 2 | export * from './user.constants'; 3 | export * from './common.constant'; 4 | export * from './channel.constants'; -------------------------------------------------------------------------------- /web/air/src/constants/toast.constants.js: -------------------------------------------------------------------------------- 1 | export const toastConstants = { 2 | SUCCESS_TIMEOUT: 1500, 3 | INFO_TIMEOUT: 3000, 4 | ERROR_TIMEOUT: 5000, 5 | WARNING_TIMEOUT: 10000, 6 | NOTICE_TIMEOUT: 20000 7 | }; 8 | -------------------------------------------------------------------------------- /web/air/src/constants/user.constants.js: -------------------------------------------------------------------------------- 1 | export const userConstants = { 2 | REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', 3 | REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', 4 | REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', 5 | 6 | LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', 7 | LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', 8 | LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', 9 | 10 | LOGOUT: 'USERS_LOGOUT', 11 | 12 | GETALL_REQUEST: 'USERS_GETALL_REQUEST', 13 | GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', 14 | GETALL_FAILURE: 'USERS_GETALL_FAILURE', 15 | 16 | DELETE_REQUEST: 'USERS_DELETE_REQUEST', 17 | DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', 18 | DELETE_FAILURE: 'USERS_DELETE_FAILURE' 19 | }; 20 | -------------------------------------------------------------------------------- /web/air/src/context/Status/index.js: -------------------------------------------------------------------------------- 1 | // contexts/User/index.jsx 2 | 3 | import React from 'react'; 4 | import { initialState, reducer } from './reducer'; 5 | 6 | export const StatusContext = React.createContext({ 7 | state: initialState, 8 | dispatch: () => null, 9 | }); 10 | 11 | export const StatusProvider = ({ children }) => { 12 | const [state, dispatch] = React.useReducer(reducer, initialState); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; -------------------------------------------------------------------------------- /web/air/src/context/Status/reducer.js: -------------------------------------------------------------------------------- 1 | export const reducer = (state, action) => { 2 | switch (action.type) { 3 | case 'set': 4 | return { 5 | ...state, 6 | status: action.payload, 7 | }; 8 | case 'unset': 9 | return { 10 | ...state, 11 | status: undefined, 12 | }; 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export const initialState = { 19 | status: undefined, 20 | }; 21 | -------------------------------------------------------------------------------- /web/air/src/context/User/index.js: -------------------------------------------------------------------------------- 1 | // contexts/User/index.jsx 2 | 3 | import React from "react" 4 | import { reducer, initialState } from "./reducer" 5 | 6 | export const UserContext = React.createContext({ 7 | state: initialState, 8 | dispatch: () => null 9 | }) 10 | 11 | export const UserProvider = ({ children }) => { 12 | const [state, dispatch] = React.useReducer(reducer, initialState) 13 | 14 | return ( 15 | 16 | { children } 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /web/air/src/context/User/reducer.js: -------------------------------------------------------------------------------- 1 | export const reducer = (state, action) => { 2 | switch (action.type) { 3 | case 'login': 4 | return { 5 | ...state, 6 | user: action.payload 7 | }; 8 | case 'logout': 9 | return { 10 | ...state, 11 | user: undefined 12 | }; 13 | 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export const initialState = { 20 | user: undefined 21 | }; -------------------------------------------------------------------------------- /web/air/src/helpers/api.js: -------------------------------------------------------------------------------- 1 | import { showError } from './utils'; 2 | import axios from 'axios'; 3 | 4 | export const API = axios.create({ 5 | baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '', 6 | }); 7 | 8 | API.interceptors.response.use( 9 | (response) => response, 10 | (error) => { 11 | showError(error); 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /web/air/src/helpers/auth-header.js: -------------------------------------------------------------------------------- 1 | export function authHeader() { 2 | // return authorization header with jwt token 3 | let user = JSON.parse(localStorage.getItem('user')); 4 | 5 | if (user && user.token) { 6 | return { 'Authorization': 'Bearer ' + user.token }; 7 | } else { 8 | return {}; 9 | } 10 | } -------------------------------------------------------------------------------- /web/air/src/helpers/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export const history = createBrowserHistory(); -------------------------------------------------------------------------------- /web/air/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './history'; 2 | export * from './auth-header'; 3 | export * from './utils'; 4 | export * from './api'; -------------------------------------------------------------------------------- /web/air/src/pages/Channel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChannelsTable from '../../components/ChannelsTable'; 3 | import {Layout} from "@douyinfe/semi-ui"; 4 | 5 | const File = () => ( 6 | <> 7 | 8 | 9 |

管理渠道

10 |
11 | 12 | 13 | 14 |
15 | 16 | ); 17 | 18 | export default File; 19 | -------------------------------------------------------------------------------- /web/air/src/pages/Chat/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Chat = () => { 4 | const chatLink = localStorage.getItem('chat_link'); 5 | 6 | return ( 7 |