From 4b4fe29118cb98bfd9702dfe0ef18a86de06917c Mon Sep 17 00:00:00 2001 From: ocean-gao Date: Fri, 20 Dec 2024 13:21:52 +0800 Subject: [PATCH] =?UTF-8?q?style:=20eslint=20=E8=A7=84=E5=88=99=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/1_bug_report.yml | 156 +-- .github/ISSUE_TEMPLATE/1_bug_report_cn.yml | 154 +-- .github/ISSUE_TEMPLATE/2_feature_request.yml | 38 +- .../ISSUE_TEMPLATE/2_feature_request_cn.yml | 38 +- .github/PULL_REQUEST_TEMPLATE.md | 24 +- .github/dependabot.yml | 8 +- .github/workflows/docker.yml | 85 +- .github/workflows/issue-translator.yml | 24 +- .github/workflows/sync.yml | 58 +- README_CN.md | 10 +- app/api/[provider]/[...path]/route.ts | 66 +- app/api/alibaba.ts | 45 +- app/api/anthropic.ts | 81 +- app/api/artifacts/route.ts | 31 +- app/api/auth.ts | 52 +- app/api/azure.ts | 21 +- app/api/baidu.ts | 45 +- app/api/bytedance.ts | 45 +- app/api/common.ts | 113 +- app/api/config/route.ts | 6 +- app/api/glm.ts | 47 +- app/api/google.ts | 85 +- app/api/iflytek.ts | 45 +- app/api/moonshot.ts | 45 +- app/api/openai.ts | 35 +- app/api/proxy.ts | 55 +- app/api/stability.ts | 47 +- app/api/tencent/route.ts | 71 +- app/api/upstash/[action]/[...key]/route.ts | 35 +- app/api/webdav/[...path]/route.ts | 75 +- app/api/xai.ts | 45 +- app/client/api.ts | 132 +-- app/client/controller.ts | 2 +- app/client/platforms/alibaba.ts | 99 +- app/client/platforms/anthropic.ts | 163 +-- app/client/platforms/baidu.ts | 109 +- app/client/platforms/bytedance.ts | 93 +- app/client/platforms/glm.ts | 61 +- app/client/platforms/google.ts | 109 +- app/client/platforms/iflytek.ts | 90 +- app/client/platforms/moonshot.ts | 61 +- app/client/platforms/openai.ts | 191 ++-- app/client/platforms/tencent.ts | 94 +- app/client/platforms/xai.ts | 59 +- app/command.ts | 16 +- app/components/artifacts.tsx | 96 +- app/components/auth.tsx | 142 +-- app/components/button.tsx | 22 +- app/components/chat-list.tsx | 106 +- app/components/chat.module.scss | 16 +- app/components/chat.tsx | 945 +++++++++-------- app/components/emoji.tsx | 32 +- app/components/error.tsx | 22 +- app/components/exporter.module.scss | 15 +- app/components/exporter.tsx | 295 +++--- app/components/home.tsx | 147 +-- app/components/input-range.tsx | 11 +- app/components/markdown.tsx | 168 +-- app/components/mask.tsx | 366 +++---- app/components/message-selector.tsx | 111 +- app/components/model-config.tsx | 246 ++--- app/components/new-chat.module.scss | 7 +- app/components/new-chat.tsx | 84 +- app/components/plugin.module.scss | 4 +- app/components/plugin.tsx | 134 +-- app/components/realtime-chat/index.ts | 2 +- .../realtime-chat/realtime-chat.tsx | 109 +- .../realtime-chat/realtime-config.tsx | 47 +- app/components/sd/index.tsx | 4 +- app/components/sd/sd-panel.tsx | 196 ++-- app/components/sd/sd-sidebar.tsx | 142 +-- app/components/sd/sd.module.scss | 30 +- app/components/sd/sd.tsx | 246 +++-- app/components/search-chat.tsx | 57 +- app/components/settings.module.scss | 2 +- app/components/settings.tsx | 980 +++++++++--------- app/components/sidebar.tsx | 118 +-- app/components/tts-config.tsx | 34 +- app/components/ui-lib.module.scss | 1 - app/components/ui-lib.tsx | 230 ++-- app/components/voice-print/index.ts | 2 +- app/components/voice-print/voice-print.tsx | 28 +- app/config/build.ts | 26 +- app/config/client.ts | 13 +- app/config/server.ts | 60 +- app/constant.ts | 594 +++++------ app/global.d.ts | 14 +- app/layout.tsx | 35 +- app/lib/audio.ts | 35 +- app/locales/ar.ts | 593 +++++------ app/locales/bn.ts | 593 +++++------ app/locales/cn.ts | 890 ++++++++-------- app/locales/cs.ts | 593 +++++------ app/locales/de.ts | 595 +++++------ app/locales/en.ts | 894 ++++++++-------- app/locales/es.ts | 593 +++++------ app/locales/fr.ts | 593 +++++------ app/locales/id.ts | 595 +++++------ app/locales/index.ts | 135 +-- app/locales/it.ts | 595 +++++------ app/locales/jp.ts | 595 +++++------ app/locales/ko.ts | 593 +++++------ app/locales/no.ts | 593 +++++------ app/locales/pt.ts | 539 +++++----- app/locales/ru.ts | 593 +++++------ app/locales/sk.ts | 550 +++++----- app/locales/tr.ts | 593 +++++------ app/locales/tw.ts | 561 +++++----- app/locales/vi.ts | 591 +++++------ app/masks/build.ts | 18 +- app/masks/cn.ts | 318 +++--- app/masks/en.ts | 90 +- app/masks/index.ts | 18 +- app/masks/tw.ts | 318 +++--- app/masks/typing.ts | 8 +- app/page.tsx | 6 +- app/store/access.ts | 161 +-- app/store/chat.ts | 222 ++-- app/store/config.ts | 105 +- app/store/index.ts | 10 +- app/store/mask.ts | 38 +- app/store/plugin.ts | 115 +- app/store/prompt.ts | 42 +- app/store/sd.ts | 254 ++--- app/store/sync.ts | 56 +- app/store/update.ts | 68 +- app/styles/globals.scss | 12 +- app/styles/markdown.scss | 34 +- app/typing.ts | 8 +- app/utils.ts | 187 ++-- app/utils/audio.ts | 4 +- app/utils/auth-settings-events.ts | 10 +- app/utils/baidu.ts | 14 +- app/utils/chat.ts | 120 +-- app/utils/clone.ts | 2 +- app/utils/cloud/index.ts | 12 +- app/utils/cloud/upstash.ts | 53 +- app/utils/cloud/webdav.ts | 50 +- app/utils/cloudflare.ts | 16 +- app/utils/format.ts | 18 +- app/utils/hmac.ts | 83 +- app/utils/hooks.ts | 8 +- app/utils/indexedDB-storage.ts | 8 +- app/utils/merge.ts | 10 +- app/utils/model.ts | 73 +- app/utils/ms_edge_tts.ts | 133 +-- app/utils/object.ts | 4 +- app/utils/store.ts | 14 +- app/utils/stream.ts | 44 +- app/utils/sync.ts | 23 +- app/utils/tencent.ts | 114 +- docker-compose.yml | 72 +- docs/faq-cn.md | 5 +- docs/synchronise-chat-logs-cn.md | 17 +- eslint.config.mjs | 38 + next.config.mjs | 64 +- public/audio-processor.js | 10 +- public/prompts.json | 1 - public/serviceWorkerRegister.js | 18 +- scripts/fetch-prompts.mjs | 68 +- tsconfig.json | 24 +- 161 files changed, 12206 insertions(+), 11827 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml index b576629e3..1786df6d8 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml @@ -1,80 +1,80 @@ -name: '🐛 Bug Report' -description: 'Report an bug' +name: 🐛 Bug Report +description: Report an bug title: '[Bug] ' -labels: ['bug'] +labels: [bug] body: - - type: dropdown - attributes: - label: '📦 Deployment Method' - multiple: true - options: - - 'Official installation package' - - 'Vercel' - - 'Zeabur' - - 'Sealos' - - 'Netlify' - - 'Docker' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 Version' - validations: - required: true - - - type: dropdown - attributes: - label: '💻 Operating System' - multiple: true - options: - - 'Windows' - - 'macOS' - - 'Ubuntu' - - 'Other Linux' - - 'iOS' - - 'iPad OS' - - 'Android' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 System Version' - validations: - required: true - - type: dropdown - attributes: - label: '🌐 Browser' - multiple: true - options: - - 'Chrome' - - 'Edge' - - 'Safari' - - 'Firefox' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 Browser Version' - validations: - required: true - - type: textarea - attributes: - label: '🐛 Bug Description' - description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail. - validations: - required: true - - type: textarea - attributes: - label: '📷 Recurrence Steps' - description: A clear and concise description of how to recurrence. - - type: textarea - attributes: - label: '🚦 Expected Behavior' - description: A clear and concise description of what you expected to happen. - - type: textarea - attributes: - label: '📝 Additional Information' - description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. \ No newline at end of file + - type: dropdown + attributes: + label: 📦 Deployment Method + multiple: true + options: + - Official installation package + - Vercel + - Zeabur + - Sealos + - Netlify + - Docker + - Other + validations: + required: true + - type: input + attributes: + label: 📌 Version + validations: + required: true + + - type: dropdown + attributes: + label: 💻 Operating System + multiple: true + options: + - Windows + - macOS + - Ubuntu + - Other Linux + - iOS + - iPad OS + - Android + - Other + validations: + required: true + - type: input + attributes: + label: 📌 System Version + validations: + required: true + - type: dropdown + attributes: + label: 🌐 Browser + multiple: true + options: + - Chrome + - Edge + - Safari + - Firefox + - Other + validations: + required: true + - type: input + attributes: + label: 📌 Browser Version + validations: + required: true + - type: textarea + attributes: + label: 🐛 Bug Description + description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail. + validations: + required: true + - type: textarea + attributes: + label: 📷 Recurrence Steps + description: A clear and concise description of how to recurrence. + - type: textarea + attributes: + label: 🚦 Expected Behavior + description: A clear and concise description of what you expected to happen. + - type: textarea + attributes: + label: 📝 Additional Information + description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. diff --git a/.github/ISSUE_TEMPLATE/1_bug_report_cn.yml b/.github/ISSUE_TEMPLATE/1_bug_report_cn.yml index 1977237de..f8085b69c 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report_cn.yml +++ b/.github/ISSUE_TEMPLATE/1_bug_report_cn.yml @@ -1,80 +1,80 @@ -name: '🐛 反馈缺陷' -description: '反馈一个问题/缺陷' +name: 🐛 反馈缺陷 +description: 反馈一个问题/缺陷 title: '[Bug] ' -labels: ['bug'] +labels: [bug] body: - - type: dropdown - attributes: - label: '📦 部署方式' - multiple: true - options: - - '官方安装包' - - 'Vercel' - - 'Zeabur' - - 'Sealos' - - 'Netlify' - - 'Docker' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 软件版本' - validations: - required: true + - type: dropdown + attributes: + label: 📦 部署方式 + multiple: true + options: + - 官方安装包 + - Vercel + - Zeabur + - Sealos + - Netlify + - Docker + - Other + validations: + required: true + - type: input + attributes: + label: 📌 软件版本 + validations: + required: true - - type: dropdown - attributes: - label: '💻 系统环境' - multiple: true - options: - - 'Windows' - - 'macOS' - - 'Ubuntu' - - 'Other Linux' - - 'iOS' - - 'iPad OS' - - 'Android' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 系统版本' - validations: - required: true - - type: dropdown - attributes: - label: '🌐 浏览器' - multiple: true - options: - - 'Chrome' - - 'Edge' - - 'Safari' - - 'Firefox' - - 'Other' - validations: - required: true - - type: input - attributes: - label: '📌 浏览器版本' - validations: - required: true - - type: textarea - attributes: - label: '🐛 问题描述' - description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。 - validations: - required: true - - type: textarea - attributes: - label: '📷 复现步骤' - description: 请提供一个清晰且简洁的描述,说明如何复现问题。 - - type: textarea - attributes: - label: '🚦 期望结果' - description: 请提供一个清晰且简洁的描述,说明您期望发生什么。 - - type: textarea - attributes: - label: '📝 补充信息' - description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。 \ No newline at end of file + - type: dropdown + attributes: + label: 💻 系统环境 + multiple: true + options: + - Windows + - macOS + - Ubuntu + - Other Linux + - iOS + - iPad OS + - Android + - Other + validations: + required: true + - type: input + attributes: + label: 📌 系统版本 + validations: + required: true + - type: dropdown + attributes: + label: 🌐 浏览器 + multiple: true + options: + - Chrome + - Edge + - Safari + - Firefox + - Other + validations: + required: true + - type: input + attributes: + label: 📌 浏览器版本 + validations: + required: true + - type: textarea + attributes: + label: 🐛 问题描述 + description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。 + validations: + required: true + - type: textarea + attributes: + label: 📷 复现步骤 + description: 请提供一个清晰且简洁的描述,说明如何复现问题。 + - type: textarea + attributes: + label: 🚦 期望结果 + description: 请提供一个清晰且简洁的描述,说明您期望发生什么。 + - type: textarea + attributes: + label: 📝 补充信息 + description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。 diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.yml b/.github/ISSUE_TEMPLATE/2_feature_request.yml index 8576e8a83..1743ab30e 100644 --- a/.github/ISSUE_TEMPLATE/2_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/2_feature_request.yml @@ -1,21 +1,21 @@ -name: '🌠 Feature Request' -description: 'Suggest an idea' +name: 🌠 Feature Request +description: Suggest an idea title: '[Feature Request] ' -labels: ['enhancement'] +labels: [enhancement] body: - - type: textarea - attributes: - label: '🥰 Feature Description' - description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. - validations: - required: true - - type: textarea - attributes: - label: '🧐 Proposed Solution' - description: Describe the solution you'd like in a clear and concise manner. - validations: - required: true - - type: textarea - attributes: - label: '📝 Additional Information' - description: Add any other context about the problem here. \ No newline at end of file + - type: textarea + attributes: + label: 🥰 Feature Description + description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. + validations: + required: true + - type: textarea + attributes: + label: 🧐 Proposed Solution + description: Describe the solution you'd like in a clear and concise manner. + validations: + required: true + - type: textarea + attributes: + label: 📝 Additional Information + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml b/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml index c7a3cc370..29cb1582f 100644 --- a/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml +++ b/.github/ISSUE_TEMPLATE/2_feature_request_cn.yml @@ -1,21 +1,21 @@ -name: '🌠 功能需求' -description: '提出需求或建议' +name: 🌠 功能需求 +description: 提出需求或建议 title: '[Feature Request] ' -labels: ['enhancement'] +labels: [enhancement] body: - - type: textarea - attributes: - label: '🥰 需求描述' - description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。 - validations: - required: true - - type: textarea - attributes: - label: '🧐 解决方案' - description: 请清晰且简洁地描述您想要的解决方案。 - validations: - required: true - - type: textarea - attributes: - label: '📝 补充信息' - description: 在这里添加关于问题的任何其他背景信息。 \ No newline at end of file + - type: textarea + attributes: + label: 🥰 需求描述 + description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。 + validations: + required: true + - type: textarea + attributes: + label: 🧐 解决方案 + description: 请清晰且简洁地描述您想要的解决方案。 + validations: + required: true + - type: textarea + attributes: + label: 📝 补充信息 + description: 在这里添加关于问题的任何其他背景信息。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3c4c90803..24f4a4988 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,27 +2,27 @@ -- [ ] feat -- [ ] fix -- [ ] refactor -- [ ] perf -- [ ] style -- [ ] test -- [ ] docs -- [ ] ci -- [ ] chore -- [ ] build +- [ ] feat +- [ ] fix +- [ ] refactor +- [ ] perf +- [ ] style +- [ ] test +- [ ] docs +- [ ] ci +- [ ] chore +- [ ] build #### 🔀 变更说明 | Description of Change - #### 📝 补充信息 | Additional Information - diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a3cce576..26087ec1f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" + - package-ecosystem: npm # See documentation for possible values + directory: / # Location of package manifests + schedule: + interval: weekly diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8ac96f193..848628898 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,52 +1,45 @@ name: Publish Docker image on: - workflow_dispatch: - release: - types: [published] + workflow_dispatch: + release: + types: [published] jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - steps: - - - name: Check out the repo - uses: actions/checkout@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: yidadaa/chatgpt-next-web - tags: | - type=raw,value=latest - type=ref,event=tag - - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: yidadaa/chatgpt-next-web + tags: | + type=raw,value=latest + type=ref,event=tag + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml index 560f66d34..5b0ab510e 100644 --- a/.github/workflows/issue-translator.yml +++ b/.github/workflows/issue-translator.yml @@ -1,15 +1,15 @@ name: Issue Translator -on: - issue_comment: - types: [created] - issues: - types: [opened] +on: + issue_comment: + types: [created] + issues: + types: [opened] jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: usthe/issues-translate-action@v2.7 - with: - IS_MODIFY_TITLE: false - CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. + build: + runs-on: ubuntu-latest + steps: + - uses: usthe/issues-translate-action@v2.7 + with: + IS_MODIFY_TITLE: false + CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index c4f9cd1fb..59439adef 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -1,40 +1,40 @@ name: Upstream Sync permissions: - contents: write + contents: write on: - schedule: - - cron: "0 0 * * *" # every day - workflow_dispatch: + schedule: + - cron: '0 0 * * *' # every day + workflow_dispatch: jobs: - sync_latest_from_upstream: - name: Sync latest commits from upstream repo - runs-on: ubuntu-latest - if: ${{ github.event.repository.fork }} + sync_latest_from_upstream: + name: Sync latest commits from upstream repo + runs-on: ubuntu-latest + if: ${{ github.event.repository.fork }} - steps: - # Step 1: run a standard checkout action - - name: Checkout target repo - uses: actions/checkout@v3 + steps: + # Step 1: run a standard checkout action + - name: Checkout target repo + uses: actions/checkout@v3 - # Step 2: run the sync action - - name: Sync upstream changes - id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 - with: - upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web - upstream_sync_branch: main - target_sync_branch: sync-main - target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set + # Step 2: run the sync action + - name: Sync upstream changes + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + with: + upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web + upstream_sync_branch: main + target_sync_branch: sync-main + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set - # Set test_mode true to run tests instead of the true action!! - test_mode: false + # Set test_mode true to run tests instead of the true action!! + test_mode: false - - name: Sync check - if: failure() - run: | - echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" - echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" - exit 1 + - name: Sync check + if: failure() + run: | + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" + exit 1 diff --git a/README_CN.md b/README_CN.md index 96c5a9659..19caf99b2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,7 +11,7 @@ 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
- + ![主界面](./docs/images/cover.png)
@@ -158,7 +158,6 @@ ChatGLM Api Key. ChatGLM Api Url. - ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 @@ -178,8 +177,9 @@ ChatGLM Api Url. ### `WHITE_WEBDAV_ENDPOINTS` (可选) 如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求: + - 每一个地址必须是一个完整的 endpoint -> `https://xxxx/xxx` + > `https://xxxx/xxx` - 多个地址以`,`相连 ### `CUSTOM_MODELS` (可选) @@ -190,12 +190,13 @@ ChatGLM Api Url. 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) + > 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。 > 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)` 在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) -> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 +> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 ### `DEFAULT_MODEL` (可选) @@ -213,7 +214,6 @@ Stability API密钥 自定义的Stability API请求地址 - ## 开发 在开始写代码之前,需要在项目根目录新建一个 `.env.local` 文件,里面填入环境变量: diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 3017fd371..5ed0dab94 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -1,18 +1,18 @@ -import { ApiPath } from "@/app/constant"; -import { NextRequest } from "next/server"; -import { handle as openaiHandler } from "../../openai"; -import { handle as azureHandler } from "../../azure"; -import { handle as googleHandler } from "../../google"; -import { handle as anthropicHandler } from "../../anthropic"; -import { handle as baiduHandler } from "../../baidu"; -import { handle as bytedanceHandler } from "../../bytedance"; -import { handle as alibabaHandler } from "../../alibaba"; -import { handle as moonshotHandler } from "../../moonshot"; -import { handle as stabilityHandler } from "../../stability"; -import { handle as iflytekHandler } from "../../iflytek"; -import { handle as xaiHandler } from "../../xai"; -import { handle as chatglmHandler } from "../../glm"; -import { handle as proxyHandler } from "../../proxy"; +import type { NextRequest } from 'next/server'; +import { ApiPath } from '@/app/constant'; +import { handle as alibabaHandler } from '../../alibaba'; +import { handle as anthropicHandler } from '../../anthropic'; +import { handle as azureHandler } from '../../azure'; +import { handle as baiduHandler } from '../../baidu'; +import { handle as bytedanceHandler } from '../../bytedance'; +import { handle as chatglmHandler } from '../../glm'; +import { handle as googleHandler } from '../../google'; +import { handle as iflytekHandler } from '../../iflytek'; +import { handle as moonshotHandler } from '../../moonshot'; +import { handle as openaiHandler } from '../../openai'; +import { handle as proxyHandler } from '../../proxy'; +import { handle as stabilityHandler } from '../../stability'; +import { handle as xaiHandler } from '../../xai'; async function handle( req: NextRequest, @@ -54,23 +54,23 @@ async function handle( export const GET = handle; export const POST = handle; -export const runtime = "edge"; +export const runtime = 'edge'; export const preferredRegion = [ - "arn1", - "bom1", - "cdg1", - "cle1", - "cpt1", - "dub1", - "fra1", - "gru1", - "hnd1", - "iad1", - "icn1", - "kix1", - "lhr1", - "pdx1", - "sfo1", - "sin1", - "syd1", + 'arn1', + 'bom1', + 'cdg1', + 'cle1', + 'cpt1', + 'dub1', + 'fra1', + 'gru1', + 'hnd1', + 'iad1', + 'icn1', + 'kix1', + 'lhr1', + 'pdx1', + 'sfo1', + 'sin1', + 'syd1', ]; diff --git a/app/api/alibaba.ts b/app/api/alibaba.ts index 894b1ae4c..a21a8fe87 100644 --- a/app/api/alibaba.ts +++ b/app/api/alibaba.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { ALIBABA_BASE_URL, ApiPath, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -16,10 +17,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Alibaba Route] params ", params); + console.log('[Alibaba Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Qwen); @@ -33,7 +34,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Alibaba] ", e); + console.error('[Alibaba] ', e); return NextResponse.json(prettyObject(e)); } } @@ -42,20 +43,20 @@ async function request(req: NextRequest) { const controller = new AbortController(); // alibaba use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, ''); let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -67,15 +68,15 @@ async function request(req: NextRequest) { const fetchUrl = `${baseUrl}${path}`; const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", - "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', + 'X-DashScope-SSE': req.headers.get('X-DashScope-SSE') ?? 'disable', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -114,9 +115,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/anthropic.ts b/app/api/anthropic.ts index 7a4444371..868152ab0 100644 --- a/app/api/anthropic.ts +++ b/app/api/anthropic.ts @@ -1,16 +1,17 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; import { - ANTHROPIC_BASE_URL, Anthropic, + ANTHROPIC_BASE_URL, ApiPath, - ServiceProvider, ModelProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "./auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; -import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; + ServiceProvider, +} from '@/app/constant'; +import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; +import { auth } from './auth'; const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); @@ -18,20 +19,20 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Anthropic Route] params ", params); + console.log('[Anthropic Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } - const subpath = params.path.join("/"); + const subpath = params.path.join('/'); if (!ALLOWD_PATH.has(subpath)) { - console.log("[Anthropic Route] forbidden path ", subpath); + console.log('[Anthropic Route] forbidden path ', subpath); return NextResponse.json( { error: true, - msg: "you are not allowed to request " + subpath, + msg: `you are not allowed to request ${subpath}`, }, { status: 403, @@ -50,7 +51,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Anthropic] ", e); + console.error('[Anthropic] ', e); return NextResponse.json(prettyObject(e)); } } @@ -60,28 +61,28 @@ const serverConfig = getServerSideConfig(); async function request(req: NextRequest) { const controller = new AbortController(); - let authHeaderName = "x-api-key"; - let authValue = - req.headers.get(authHeaderName) || - req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() || - serverConfig.anthropicApiKey || - ""; + const authHeaderName = 'x-api-key'; + const authValue + = req.headers.get(authHeaderName) + || req.headers.get('Authorization')?.replaceAll('Bearer ', '').trim() + || serverConfig.anthropicApiKey + || ''; - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, ''); - let baseUrl = - serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL; + let baseUrl + = serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -95,20 +96,20 @@ async function request(req: NextRequest) { const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "anthropic-dangerous-direct-browser-access": "true", + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'anthropic-dangerous-direct-browser-access': 'true', [authHeaderName]: authValue, - "anthropic-version": - req.headers.get("anthropic-version") || - serverConfig.anthropicApiVersion || - Anthropic.Vision, + 'anthropic-version': + req.headers.get('anthropic-version') + || serverConfig.anthropicApiVersion + || Anthropic.Vision, }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -155,9 +156,9 @@ async function request(req: NextRequest) { // ); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/artifacts/route.ts b/app/api/artifacts/route.ts index 4707e795f..798ab802d 100644 --- a/app/api/artifacts/route.ts +++ b/app/api/artifacts/route.ts @@ -1,6 +1,7 @@ -import md5 from "spark-md5"; -import { NextRequest, NextResponse } from "next/server"; -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; +import { NextResponse } from 'next/server'; +import md5 from 'spark-md5'; async function handle(req: NextRequest, res: NextResponse) { const serverConfig = getServerSideConfig(); @@ -9,7 +10,7 @@ async function handle(req: NextRequest, res: NextResponse) { const storeHeaders = () => ({ Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, }); - if (req.method === "POST") { + if (req.method === 'POST') { const clonedBody = await req.text(); const hashedCode = md5.hash(clonedBody).trim(); const body: { @@ -21,9 +22,9 @@ async function handle(req: NextRequest, res: NextResponse) { value: clonedBody, }; try { - const ttl = parseInt(serverConfig.cloudflareKVTTL as string); + const ttl = Number.parseInt(serverConfig.cloudflareKVTTL as string); if (ttl > 60) { - body["expiration_ttl"] = ttl; + body.expiration_ttl = ttl; } } catch (e) { console.error(e); @@ -31,13 +32,13 @@ async function handle(req: NextRequest, res: NextResponse) { const res = await fetch(`${storeUrl()}/bulk`, { headers: { ...storeHeaders(), - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, - method: "PUT", + method: 'PUT', body: JSON.stringify([body]), }); const result = await res.json(); - console.log("save data", result); + console.log('save data', result); if (result?.success) { return NextResponse.json( { code: 0, id: hashedCode, result }, @@ -45,15 +46,15 @@ async function handle(req: NextRequest, res: NextResponse) { ); } return NextResponse.json( - { error: true, msg: "Save data error" }, + { error: true, msg: 'Save data error' }, { status: 400 }, ); } - if (req.method === "GET") { - const id = req?.nextUrl?.searchParams?.get("id"); + if (req.method === 'GET') { + const id = req?.nextUrl?.searchParams?.get('id'); const res = await fetch(`${storeUrl()}/values/${id}`, { headers: storeHeaders(), - method: "GET", + method: 'GET', }); return new Response(res.body, { status: res.status, @@ -62,7 +63,7 @@ async function handle(req: NextRequest, res: NextResponse) { }); } return NextResponse.json( - { error: true, msg: "Invalid request" }, + { error: true, msg: 'Invalid request' }, { status: 400 }, ); } @@ -70,4 +71,4 @@ async function handle(req: NextRequest, res: NextResponse) { export const POST = handle; export const GET = handle; -export const runtime = "edge"; +export const runtime = 'edge'; diff --git a/app/api/auth.ts b/app/api/auth.ts index 6703b64bd..0b3a61c82 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -1,55 +1,55 @@ -import { NextRequest } from "next/server"; -import { getServerSideConfig } from "../config/server"; -import md5 from "spark-md5"; -import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant"; +import type { NextRequest } from 'next/server'; +import md5 from 'spark-md5'; +import { getServerSideConfig } from '../config/server'; +import { ACCESS_CODE_PREFIX, ModelProvider } from '../constant'; function getIP(req: NextRequest) { - let ip = req.ip ?? req.headers.get("x-real-ip"); - const forwardedFor = req.headers.get("x-forwarded-for"); + let ip = req.ip ?? req.headers.get('x-real-ip'); + const forwardedFor = req.headers.get('x-forwarded-for'); if (!ip && forwardedFor) { - ip = forwardedFor.split(",").at(0) ?? ""; + ip = forwardedFor.split(',').at(0) ?? ''; } return ip; } function parseApiKey(bearToken: string) { - const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + const token = bearToken.trim().replaceAll('Bearer ', '').trim(); const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX); return { - accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length), - apiKey: isApiKey ? token : "", + accessCode: isApiKey ? '' : token.slice(ACCESS_CODE_PREFIX.length), + apiKey: isApiKey ? token : '', }; } export function auth(req: NextRequest, modelProvider: ModelProvider) { - const authToken = req.headers.get("Authorization") ?? ""; + const authToken = req.headers.get('Authorization') ?? ''; // check if it is openai api key or user token const { accessCode, apiKey } = parseApiKey(authToken); - const hashedCode = md5.hash(accessCode ?? "").trim(); + const hashedCode = md5.hash(accessCode ?? '').trim(); const serverConfig = getServerSideConfig(); - console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); - console.log("[Auth] got access code:", accessCode); - console.log("[Auth] hashed access code:", hashedCode); - console.log("[User IP] ", getIP(req)); - console.log("[Time] ", new Date().toLocaleString()); + console.log('[Auth] allowed hashed codes: ', [...serverConfig.codes]); + console.log('[Auth] got access code:', accessCode); + console.log('[Auth] hashed access code:', hashedCode); + console.log('[User IP] ', getIP(req)); + console.log('[Time] ', new Date().toLocaleString()); if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { return { error: true, - msg: !accessCode ? "empty access code" : "wrong access code", + msg: !accessCode ? 'empty access code' : 'wrong access code', }; } if (serverConfig.hideUserApiKey && !!apiKey) { return { error: true, - msg: "you are not allowed to access with your own api key", + msg: 'you are not allowed to access with your own api key', }; } @@ -89,8 +89,8 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { systemApiKey = serverConfig.moonshotApiKey; break; case ModelProvider.Iflytek: - systemApiKey = - serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret; + systemApiKey + = `${serverConfig.iflytekApiKey}:${serverConfig.iflytekApiSecret}`; break; case ModelProvider.XAI: systemApiKey = serverConfig.xaiApiKey; @@ -100,7 +100,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { break; case ModelProvider.GPT: default: - if (req.nextUrl.pathname.includes("azure/deployments")) { + if (req.nextUrl.pathname.includes('azure/deployments')) { systemApiKey = serverConfig.azureApiKey; } else { systemApiKey = serverConfig.apiKey; @@ -108,13 +108,13 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { } if (systemApiKey) { - console.log("[Auth] use system api key"); - req.headers.set("Authorization", `Bearer ${systemApiKey}`); + console.log('[Auth] use system api key'); + req.headers.set('Authorization', `Bearer ${systemApiKey}`); } else { - console.log("[Auth] admin did not provide an api key"); + console.log('[Auth] admin did not provide an api key'); } } else { - console.log("[Auth] use user api key"); + console.log('[Auth] use user api key'); } return { diff --git a/app/api/azure.ts b/app/api/azure.ts index 39d872e8c..0d8225410 100644 --- a/app/api/azure.ts +++ b/app/api/azure.ts @@ -1,20 +1,21 @@ -import { ModelProvider } from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "./auth"; -import { requestOpenai } from "./common"; +import type { NextRequest } from 'next/server'; +import { ModelProvider } from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { NextResponse } from 'next/server'; +import { auth } from './auth'; +import { requestOpenai } from './common'; export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Azure Route] params ", params); + console.log('[Azure Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } - const subpath = params.path.join("/"); + const subpath = params.path.join('/'); const authResult = auth(req, ModelProvider.GPT); if (authResult.error) { @@ -26,7 +27,7 @@ export async function handle( try { return await requestOpenai(req); } catch (e) { - console.error("[Azure] ", e); + console.error('[Azure] ', e); return NextResponse.json(prettyObject(e)); } } diff --git a/app/api/baidu.ts b/app/api/baidu.ts index 0408b43c5..84d2a018a 100644 --- a/app/api/baidu.ts +++ b/app/api/baidu.ts @@ -1,15 +1,16 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - BAIDU_BASE_URL, ApiPath, + BAIDU_BASE_URL, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; -import { getAccessToken } from "@/app/utils/baidu"; +} from '@/app/constant'; +import { getAccessToken } from '@/app/utils/baidu'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -17,10 +18,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Baidu Route] params ", params); + console.log('[Baidu Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Ernie); @@ -46,7 +47,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Baidu] ", e); + console.error('[Baidu] ', e); return NextResponse.json(prettyObject(e)); } } @@ -54,20 +55,20 @@ export async function handle( async function request(req: NextRequest) { const controller = new AbortController(); - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, ''); let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -84,13 +85,13 @@ async function request(req: NextRequest) { const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -129,9 +130,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/bytedance.ts b/app/api/bytedance.ts index cb65b1061..572dc4774 100644 --- a/app/api/bytedance.ts +++ b/app/api/bytedance.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - BYTEDANCE_BASE_URL, ApiPath, + BYTEDANCE_BASE_URL, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -16,10 +17,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[ByteDance Route] params ", params); + console.log('[ByteDance Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Doubao); @@ -33,7 +34,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[ByteDance] ", e); + console.error('[ByteDance] ', e); return NextResponse.json(prettyObject(e)); } } @@ -41,20 +42,20 @@ export async function handle( async function request(req: NextRequest) { const controller = new AbortController(); - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, ''); let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -67,14 +68,14 @@ async function request(req: NextRequest) { const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -114,9 +115,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/common.ts b/app/api/common.ts index 495a12ccd..683fb2f20 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,47 +1,48 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSideConfig } from "../config/server"; -import { OPENAI_BASE_URL, ServiceProvider } from "../constant"; -import { cloudflareAIGatewayUrl } from "../utils/cloudflare"; -import { getModelProvider, isModelAvailableInServer } from "../utils/model"; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { getServerSideConfig } from '../config/server'; +import { OPENAI_BASE_URL, ServiceProvider } from '../constant'; +import { cloudflareAIGatewayUrl } from '../utils/cloudflare'; +import { getModelProvider, isModelAvailableInServer } from '../utils/model'; const serverConfig = getServerSideConfig(); export async function requestOpenai(req: NextRequest) { const controller = new AbortController(); - const isAzure = req.nextUrl.pathname.includes("azure/deployments"); + const isAzure = req.nextUrl.pathname.includes('azure/deployments'); - var authValue, - authHeaderName = ""; + let authValue; + let authHeaderName = ''; if (isAzure) { - authValue = - req.headers - .get("Authorization") + authValue + = req.headers + .get('Authorization') ?.trim() - .replaceAll("Bearer ", "") - .trim() ?? ""; + .replaceAll('Bearer ', '') + .trim() ?? ''; - authHeaderName = "api-key"; + authHeaderName = 'api-key'; } else { - authValue = req.headers.get("Authorization") ?? ""; - authHeaderName = "Authorization"; + authValue = req.headers.get('Authorization') ?? ''; + authHeaderName = 'Authorization'; } - let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", ""); + let path = `${req.nextUrl.pathname}`.replaceAll('/api/openai/', ''); - let baseUrl = - (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; + let baseUrl + = (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -51,30 +52,30 @@ export async function requestOpenai(req: NextRequest) { ); if (isAzure) { - const azureApiVersion = - req?.nextUrl?.searchParams?.get("api-version") || - serverConfig.azureApiVersion; - baseUrl = baseUrl.split("/deployments").shift() as string; + const azureApiVersion + = req?.nextUrl?.searchParams?.get('api-version') + || serverConfig.azureApiVersion; + baseUrl = baseUrl.split('/deployments').shift() as string; path = `${req.nextUrl.pathname.replaceAll( - "/api/azure/", - "", + '/api/azure/', + '', )}?api-version=${azureApiVersion}`; // Forward compatibility: // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL // then using default '{deploy-id}' if (serverConfig.customModels && serverConfig.azureUrl) { - const modelName = path.split("/")[1]; - let realDeployName = ""; + const modelName = path.split('/')[1]; + let realDeployName = ''; serverConfig.customModels - .split(",") - .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName)) + .split(',') + .filter(v => !!v && !v.startsWith('-') && v.includes(modelName)) .forEach((m) => { - const [fullName, displayName] = m.split("="); + const [fullName, displayName] = m.split('='); const [_, providerName] = getModelProvider(fullName); - if (providerName === "azure" && !displayName) { - const [_, deployId] = (serverConfig?.azureUrl ?? "").split( - "deployments/", + if (providerName === 'azure' && !displayName) { + const [_, deployId] = (serverConfig?.azureUrl ?? '').split( + 'deployments/', ); if (deployId) { realDeployName = deployId; @@ -82,29 +83,29 @@ export async function requestOpenai(req: NextRequest) { } }); if (realDeployName) { - console.log("[Replace with DeployId", realDeployName); + console.log('[Replace with DeployId', realDeployName); path = path.replaceAll(modelName, realDeployName); } } } const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`); - console.log("fetchUrl", fetchUrl); + console.log('fetchUrl', fetchUrl); const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store", + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', [authHeaderName]: authValue, ...(serverConfig.openaiOrgId && { - "OpenAI-Organization": serverConfig.openaiOrgId, + 'OpenAI-Organization': serverConfig.openaiOrgId, }), }, method: req.method, body: req.body, // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -122,8 +123,8 @@ export async function requestOpenai(req: NextRequest) { serverConfig.customModels, jsonBody?.model as string, ServiceProvider.OpenAI as string, - ) || - isModelAvailableInServer( + ) + || isModelAvailableInServer( serverConfig.customModels, jsonBody?.model as string, ServiceProvider.Azure as string, @@ -140,7 +141,7 @@ export async function requestOpenai(req: NextRequest) { ); } } catch (e) { - console.error("[OpenAI] gpt4 filter", e); + console.error('[OpenAI] gpt4 filter', e); } } @@ -148,33 +149,33 @@ export async function requestOpenai(req: NextRequest) { const res = await fetch(fetchUrl, fetchOptions); // Extract the OpenAI-Organization header from the response - const openaiOrganizationHeader = res.headers.get("OpenAI-Organization"); + const openaiOrganizationHeader = res.headers.get('OpenAI-Organization'); // Check if serverConfig.openaiOrgId is defined and not an empty string - if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") { + if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== '') { // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present - console.log("[Org ID]", openaiOrganizationHeader); + console.log('[Org ID]', openaiOrganizationHeader); } else { - console.log("[Org ID] is not set up."); + console.log('[Org ID] is not set up.'); } // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV) // Also, this is to prevent the header from being sent to the client - if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") { - newHeaders.delete("OpenAI-Organization"); + if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === '') { + newHeaders.delete('OpenAI-Organization'); } // The latest version of the OpenAI API forced the content-encoding to be "br" in json response // So if the streaming is disabled, we need to remove the content-encoding header // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header // The browser will try to decode the response with brotli and fail - newHeaders.delete("content-encoding"); + newHeaders.delete('content-encoding'); return new Response(res.body, { status: res.status, diff --git a/app/api/config/route.ts b/app/api/config/route.ts index b0d9da031..a06e1f097 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -1,6 +1,6 @@ -import { NextResponse } from "next/server"; +import { NextResponse } from 'next/server'; -import { getServerSideConfig } from "../../config/server"; +import { getServerSideConfig } from '../../config/server'; const serverConfig = getServerSideConfig(); @@ -27,4 +27,4 @@ async function handle() { export const GET = handle; export const POST = handle; -export const runtime = "edge"; +export const runtime = 'edge'; diff --git a/app/api/glm.ts b/app/api/glm.ts index 3625b9f7b..51f4a178a 100644 --- a/app/api/glm.ts +++ b/app/api/glm.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - CHATGLM_BASE_URL, ApiPath, + CHATGLM_BASE_URL, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -16,10 +17,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[GLM Route] params ", params); + console.log('[GLM Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.ChatGLM); @@ -33,7 +34,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[GLM] ", e); + console.error('[GLM] ', e); return NextResponse.json(prettyObject(e)); } } @@ -42,20 +43,20 @@ async function request(req: NextRequest) { const controller = new AbortController(); // alibaba use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, ''); let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -65,17 +66,17 @@ async function request(req: NextRequest) { ); const fetchUrl = `${baseUrl}${path}`; - console.log("[Fetch Url] ", fetchUrl); + console.log('[Fetch Url] ', fetchUrl); const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -114,9 +115,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/google.ts b/app/api/google.ts index 707892c33..6d757e7c7 100644 --- a/app/api/google.ts +++ b/app/api/google.ts @@ -1,8 +1,9 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "./auth"; -import { getServerSideConfig } from "@/app/config/server"; -import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; +import { ApiPath, GEMINI_BASE_URL, ModelProvider } from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { NextResponse } from 'next/server'; +import { auth } from './auth'; const serverConfig = getServerSideConfig(); @@ -10,10 +11,10 @@ export async function handle( req: NextRequest, { params }: { params: { provider: string; path: string[] } }, ) { - console.log("[Google Route] params ", params); + console.log('[Google Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.GeminiPro); @@ -23,11 +24,11 @@ export async function handle( }); } - const bearToken = - req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || ""; - const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + const bearToken + = req.headers.get('x-goog-api-key') || req.headers.get('Authorization') || ''; + const token = bearToken.trim().replaceAll('Bearer ', '').trim(); - const apiKey = token ? token : serverConfig.googleApiKey; + const apiKey = token || serverConfig.googleApiKey; if (!apiKey) { return NextResponse.json( @@ -44,7 +45,7 @@ export async function handle( const response = await request(req, apiKey); return response; } catch (e) { - console.error("[Google] ", e); + console.error('[Google] ', e); return NextResponse.json(prettyObject(e)); } } @@ -52,20 +53,20 @@ export async function handle( export const GET = handle; export const POST = handle; -export const runtime = "edge"; +export const runtime = 'edge'; export const preferredRegion = [ - "bom1", - "cle1", - "cpt1", - "gru1", - "hnd1", - "iad1", - "icn1", - "kix1", - "pdx1", - "sfo1", - "sin1", - "syd1", + 'bom1', + 'cle1', + 'cpt1', + 'gru1', + 'hnd1', + 'iad1', + 'icn1', + 'kix1', + 'pdx1', + 'sfo1', + 'sin1', + 'syd1', ]; async function request(req: NextRequest, apiKey: string) { @@ -73,18 +74,18 @@ async function request(req: NextRequest, apiKey: string) { let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL; - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, ''); - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -93,24 +94,24 @@ async function request(req: NextRequest, apiKey: string) { 10 * 60 * 1000, ); const fetchUrl = `${baseUrl}${path}${ - req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : "" + req?.nextUrl?.searchParams?.get('alt') === 'sse' ? '?alt=sse' : '' }`; - console.log("[Fetch Url] ", fetchUrl); + console.log('[Fetch Url] ', fetchUrl); const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store", - "x-goog-api-key": - req.headers.get("x-goog-api-key") || - (req.headers.get("Authorization") ?? "").replace("Bearer ", ""), + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'x-goog-api-key': + req.headers.get('x-goog-api-key') + || (req.headers.get('Authorization') ?? '').replace('Bearer ', ''), }, method: req.method, body: req.body, // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -118,9 +119,9 @@ async function request(req: NextRequest, apiKey: string) { const res = await fetch(fetchUrl, fetchOptions); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/iflytek.ts b/app/api/iflytek.ts index 8b8227dce..16313b2f2 100644 --- a/app/api/iflytek.ts +++ b/app/api/iflytek.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - IFLYTEK_BASE_URL, ApiPath, + IFLYTEK_BASE_URL, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; // iflytek const serverConfig = getServerSideConfig(); @@ -17,10 +18,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Iflytek Route] params ", params); + console.log('[Iflytek Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Iflytek); @@ -34,7 +35,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Iflytek] ", e); + console.error('[Iflytek] ', e); return NextResponse.json(prettyObject(e)); } } @@ -43,20 +44,20 @@ async function request(req: NextRequest) { const controller = new AbortController(); // iflytek use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, ''); let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -68,14 +69,14 @@ async function request(req: NextRequest) { const fetchUrl = `${baseUrl}${path}`; const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -114,9 +115,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/moonshot.ts b/app/api/moonshot.ts index 5bf4807e3..60c1ddd10 100644 --- a/app/api/moonshot.ts +++ b/app/api/moonshot.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - MOONSHOT_BASE_URL, ApiPath, ModelProvider, + MOONSHOT_BASE_URL, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -16,10 +17,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Moonshot Route] params ", params); + console.log('[Moonshot Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Moonshot); @@ -33,7 +34,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Moonshot] ", e); + console.error('[Moonshot] ', e); return NextResponse.json(prettyObject(e)); } } @@ -42,20 +43,20 @@ async function request(req: NextRequest) { const controller = new AbortController(); // alibaba use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, ''); let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -67,14 +68,14 @@ async function request(req: NextRequest) { const fetchUrl = `${baseUrl}${path}`; const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -113,9 +114,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/openai.ts b/app/api/openai.ts index 2b5deca8b..98e5c967b 100644 --- a/app/api/openai.ts +++ b/app/api/openai.ts @@ -1,10 +1,11 @@ -import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; -import { getServerSideConfig } from "@/app/config/server"; -import { ModelProvider, OpenaiPath } from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "./auth"; -import { requestOpenai } from "./common"; +import type { OpenAIListModelResponse } from '@/app/client/platforms/openai'; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; +import { ModelProvider, OpenaiPath } from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { NextResponse } from 'next/server'; +import { auth } from './auth'; +import { requestOpenai } from './common'; const ALLOWED_PATH = new Set(Object.values(OpenaiPath)); @@ -13,9 +14,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) { if (config.disableGPT4) { remoteModelRes.data = remoteModelRes.data.filter( - (m) => - !(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o") || m.id.startsWith("o1")) || - m.id.startsWith("gpt-4o-mini"), + m => + !(m.id.startsWith('gpt-4') || m.id.startsWith('chatgpt-4o') || m.id.startsWith('o1')) + || m.id.startsWith('gpt-4o-mini'), ); } @@ -26,20 +27,20 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[OpenAI Route] params ", params); + console.log('[OpenAI Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } - const subpath = params.path.join("/"); + const subpath = params.path.join('/'); if (!ALLOWED_PATH.has(subpath)) { - console.log("[OpenAI Route] forbidden path ", subpath); + console.log('[OpenAI Route] forbidden path ', subpath); return NextResponse.json( { error: true, - msg: "you are not allowed to request " + subpath, + msg: `you are not allowed to request ${subpath}`, }, { status: 403, @@ -68,7 +69,7 @@ export async function handle( return response; } catch (e) { - console.error("[OpenAI] ", e); + console.error('[OpenAI] ', e); return NextResponse.json(prettyObject(e)); } } diff --git a/app/api/proxy.ts b/app/api/proxy.ts index b3e5e7b7b..6143605dd 100644 --- a/app/api/proxy.ts +++ b/app/api/proxy.ts @@ -1,32 +1,33 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; +import { NextResponse } from 'next/server'; export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Proxy Route] params ", params); + console.log('[Proxy Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const serverConfig = getServerSideConfig(); // remove path params from searchParams - req.nextUrl.searchParams.delete("path"); - req.nextUrl.searchParams.delete("provider"); + req.nextUrl.searchParams.delete('path'); + req.nextUrl.searchParams.delete('provider'); - const subpath = params.path.join("/"); + const subpath = params.path.join('/'); const fetchUrl = `${req.headers.get( - "x-base-url", + 'x-base-url', )}/${subpath}?${req.nextUrl.searchParams.toString()}`; - const skipHeaders = ["connection", "host", "origin", "referer", "cookie"]; + const skipHeaders = ['connection', 'host', 'origin', 'referer', 'cookie']; const headers = new Headers( Array.from(req.headers.entries()).filter((item) => { if ( - item[0].indexOf("x-") > -1 || - item[0].indexOf("sec-") > -1 || - skipHeaders.includes(item[0]) + item[0].includes('x-') + || item[0].includes('sec-') + || skipHeaders.includes(item[0]) ) { return false; } @@ -34,16 +35,16 @@ export async function handle( }), ); // if dalle3 use openai api key - const baseUrl = req.headers.get("x-base-url"); - if (baseUrl?.includes("api.openai.com")) { - if (!serverConfig.apiKey) { - return NextResponse.json( - { error: "OpenAI API key not configured" }, - { status: 500 }, - ); - } - headers.set("Authorization", `Bearer ${serverConfig.apiKey}`); + const baseUrl = req.headers.get('x-base-url'); + if (baseUrl?.includes('api.openai.com')) { + if (!serverConfig.apiKey) { + return NextResponse.json( + { error: 'OpenAI API key not configured' }, + { status: 500 }, + ); } + headers.set('Authorization', `Bearer ${serverConfig.apiKey}`); + } const controller = new AbortController(); const fetchOptions: RequestInit = { @@ -51,9 +52,9 @@ export async function handle( method: req.method, body: req.body, // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -68,15 +69,15 @@ export async function handle( const res = await fetch(fetchUrl, fetchOptions); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); // The latest version of the OpenAI API forced the content-encoding to be "br" in json response // So if the streaming is disabled, we need to remove the content-encoding header // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header // The browser will try to decode the response with brotli and fail - newHeaders.delete("content-encoding"); + newHeaders.delete('content-encoding'); return new Response(res.body, { status: res.status, diff --git a/app/api/stability.ts b/app/api/stability.ts index 2646ace85..e4a004e32 100644 --- a/app/api/stability.ts +++ b/app/api/stability.ts @@ -1,16 +1,17 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getServerSideConfig } from "@/app/config/server"; -import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant"; -import { auth } from "@/app/api/auth"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; +import { ModelProvider, STABILITY_BASE_URL } from '@/app/constant'; +import { NextResponse } from 'next/server'; export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Stability] params ", params); + console.log('[Stability] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const controller = new AbortController(); @@ -19,18 +20,18 @@ export async function handle( let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", ""); + const path = `${req.nextUrl.pathname}`.replaceAll('/api/stability/', ''); - console.log("[Stability Proxy] ", path); - console.log("[Stability Base Url]", baseUrl); + console.log('[Stability Proxy] ', path); + console.log('[Stability Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -47,10 +48,10 @@ export async function handle( }); } - const bearToken = req.headers.get("Authorization") ?? ""; - const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + const bearToken = req.headers.get('Authorization') ?? ''; + const token = bearToken.trim().replaceAll('Bearer ', '').trim(); - const key = token ? token : serverConfig.stabilityApiKey; + const key = token || serverConfig.stabilityApiKey; if (!key) { return NextResponse.json( @@ -65,19 +66,19 @@ export async function handle( } const fetchUrl = `${baseUrl}/${path}`; - console.log("[Stability Url] ", fetchUrl); + console.log('[Stability Url] ', fetchUrl); const fetchOptions: RequestInit = { headers: { - "Content-Type": req.headers.get("Content-Type") || "multipart/form-data", - Accept: req.headers.get("Accept") || "application/json", - Authorization: `Bearer ${key}`, + 'Content-Type': req.headers.get('Content-Type') || 'multipart/form-data', + 'Accept': req.headers.get('Accept') || 'application/json', + 'Authorization': `Bearer ${key}`, }, method: req.method, body: req.body, // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -85,9 +86,9 @@ export async function handle( const res = await fetch(fetchUrl, fetchOptions); // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, statusText: res.statusText, diff --git a/app/api/tencent/route.ts b/app/api/tencent/route.ts index fc4f8c79e..4c31e8007 100644 --- a/app/api/tencent/route.ts +++ b/app/api/tencent/route.ts @@ -1,9 +1,10 @@ -import { getServerSideConfig } from "@/app/config/server"; -import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { getHeader } from "@/app/utils/tencent"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; +import { ModelProvider, TENCENT_BASE_URL } from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { getHeader } from '@/app/utils/tencent'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -11,10 +12,10 @@ async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[Tencent Route] params ", params); + console.log('[Tencent Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.Hunyuan); @@ -28,7 +29,7 @@ async function handle( const response = await request(req); return response; } catch (e) { - console.error("[Tencent] ", e); + console.error('[Tencent] ', e); return NextResponse.json(prettyObject(e)); } } @@ -36,25 +37,25 @@ async function handle( export const GET = handle; export const POST = handle; -export const runtime = "edge"; +export const runtime = 'edge'; export const preferredRegion = [ - "arn1", - "bom1", - "cdg1", - "cle1", - "cpt1", - "dub1", - "fra1", - "gru1", - "hnd1", - "iad1", - "icn1", - "kix1", - "lhr1", - "pdx1", - "sfo1", - "sin1", - "syd1", + 'arn1', + 'bom1', + 'cdg1', + 'cle1', + 'cpt1', + 'dub1', + 'fra1', + 'gru1', + 'hnd1', + 'iad1', + 'icn1', + 'kix1', + 'lhr1', + 'pdx1', + 'sfo1', + 'sin1', + 'syd1', ]; async function request(req: NextRequest) { @@ -62,15 +63,15 @@ async function request(req: NextRequest) { let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Base Url]", baseUrl); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -91,9 +92,9 @@ async function request(req: NextRequest) { headers, method: req.method, body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -102,9 +103,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/api/upstash/[action]/[...key]/route.ts b/app/api/upstash/[action]/[...key]/route.ts index fcfef4718..46d10610e 100644 --- a/app/api/upstash/[action]/[...key]/route.ts +++ b/app/api/upstash/[action]/[...key]/route.ts @@ -1,22 +1,23 @@ -import { NextRequest, NextResponse } from "next/server"; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; async function handle( req: NextRequest, { params }: { params: { action: string; key: string[] } }, ) { const requestUrl = new URL(req.url); - const endpoint = requestUrl.searchParams.get("endpoint"); + const endpoint = requestUrl.searchParams.get('endpoint'); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const [...key] = params.key; // only allow to request to *.upstash.io - if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) { + if (!endpoint || !new URL(endpoint).hostname.endsWith('.upstash.io')) { return NextResponse.json( { error: true, - msg: "you are not allowed to request " + params.key.join("/"), + msg: `you are not allowed to request ${params.key.join('/')}`, }, { status: 403, @@ -25,12 +26,12 @@ async function handle( } // only allow upstash get and set method - if (params.action !== "get" && params.action !== "set") { - console.log("[Upstash Route] forbidden action ", params.action); + if (params.action !== 'get' && params.action !== 'set') { + console.log('[Upstash Route] forbidden action ', params.action); return NextResponse.json( { error: true, - msg: "you are not allowed to request " + params.action, + msg: `you are not allowed to request ${params.action}`, }, { status: 403, @@ -38,27 +39,27 @@ async function handle( ); } - const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`; + const targetUrl = `${endpoint}/${params.action}/${params.key.join('/')}`; const method = req.method; - const shouldNotHaveBody = ["get", "head"].includes( - method?.toLowerCase() ?? "", + const shouldNotHaveBody = ['get', 'head'].includes( + method?.toLowerCase() ?? '', ); const fetchOptions: RequestInit = { headers: { - authorization: req.headers.get("authorization") ?? "", + authorization: req.headers.get('authorization') ?? '', }, body: shouldNotHaveBody ? null : req.body, method, // @ts-ignore - duplex: "half", + duplex: 'half', }; - console.log("[Upstash Proxy]", targetUrl, fetchOptions); + console.log('[Upstash Proxy]', targetUrl, fetchOptions); const fetchResult = await fetch(targetUrl, fetchOptions); - console.log("[Any Proxy]", targetUrl, { + console.log('[Any Proxy]', targetUrl, { status: fetchResult.status, statusText: fetchResult.statusText, }); @@ -70,4 +71,4 @@ export const POST = handle; export const GET = handle; export const OPTIONS = handle; -export const runtime = "edge"; +export const runtime = 'edge'; diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index bb7743bda..ad965f443 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -1,47 +1,48 @@ -import { NextRequest, NextResponse } from "next/server"; -import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant"; -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { getServerSideConfig } from '@/app/config/server'; +import { NextResponse } from 'next/server'; +import { internalAllowedWebDavEndpoints, STORAGE_KEY } from '../../../constant'; const config = getServerSideConfig(); const mergedAllowedWebDavEndpoints = [ ...internalAllowedWebDavEndpoints, ...config.allowedWebDavEndpoints, -].filter((domain) => Boolean(domain.trim())); +].filter(domain => Boolean(domain.trim())); -const normalizeUrl = (url: string) => { +function normalizeUrl(url: string) { try { return new URL(url); } catch (err) { return null; } -}; +} async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const folder = STORAGE_KEY; const fileName = `${folder}/backup.json`; const requestUrl = new URL(req.url); - let endpoint = requestUrl.searchParams.get("endpoint"); - let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method; + let endpoint = requestUrl.searchParams.get('endpoint'); + const proxy_method = requestUrl.searchParams.get('proxy_method') || req.method; // Validate the endpoint to prevent potential SSRF attacks if ( - !endpoint || - !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => { + !endpoint + || !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => { const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); const normalizedEndpoint = normalizeUrl(endpoint as string); return ( - normalizedEndpoint && - normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && - normalizedEndpoint.pathname.startsWith( + normalizedEndpoint + && normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname + && normalizedEndpoint.pathname.startsWith( normalizedAllowedEndpoint.pathname, ) ); @@ -50,7 +51,7 @@ async function handle( return NextResponse.json( { error: true, - msg: "Invalid endpoint", + msg: 'Invalid endpoint', }, { status: 400, @@ -58,23 +59,23 @@ async function handle( ); } - if (!endpoint?.endsWith("/")) { - endpoint += "/"; + if (!endpoint?.endsWith('/')) { + endpoint += '/'; } - const endpointPath = params.path.join("/"); + const endpointPath = params.path.join('/'); const targetPath = `${endpoint}${endpointPath}`; // only allow MKCOL, GET, PUT if ( - proxy_method !== "MKCOL" && - proxy_method !== "GET" && - proxy_method !== "PUT" + proxy_method !== 'MKCOL' + && proxy_method !== 'GET' + && proxy_method !== 'PUT' ) { return NextResponse.json( { error: true, - msg: "you are not allowed to request " + targetPath, + msg: `you are not allowed to request ${targetPath}`, }, { status: 403, @@ -83,11 +84,11 @@ async function handle( } // for MKCOL request, only allow request ${folder} - if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) { + if (proxy_method === 'MKCOL' && !targetPath.endsWith(folder)) { return NextResponse.json( { error: true, - msg: "you are not allowed to request " + targetPath, + msg: `you are not allowed to request ${targetPath}`, }, { status: 403, @@ -96,11 +97,11 @@ async function handle( } // for GET request, only allow request ending with fileName - if (proxy_method === "GET" && !targetPath.endsWith(fileName)) { + if (proxy_method === 'GET' && !targetPath.endsWith(fileName)) { return NextResponse.json( { error: true, - msg: "you are not allowed to request " + targetPath, + msg: `you are not allowed to request ${targetPath}`, }, { status: 403, @@ -109,11 +110,11 @@ async function handle( } // for PUT request, only allow request ending with fileName - if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) { + if (proxy_method === 'PUT' && !targetPath.endsWith(fileName)) { return NextResponse.json( { error: true, - msg: "you are not allowed to request " + targetPath, + msg: `you are not allowed to request ${targetPath}`, }, { status: 403, @@ -124,19 +125,19 @@ async function handle( const targetUrl = targetPath; const method = proxy_method || req.method; - const shouldNotHaveBody = ["get", "head"].includes( - method?.toLowerCase() ?? "", + const shouldNotHaveBody = ['get', 'head'].includes( + method?.toLowerCase() ?? '', ); const fetchOptions: RequestInit = { headers: { - authorization: req.headers.get("authorization") ?? "", + authorization: req.headers.get('authorization') ?? '', }, body: shouldNotHaveBody ? null : req.body, - redirect: "manual", + redirect: 'manual', method, // @ts-ignore - duplex: "half", + duplex: 'half', }; let fetchResult; @@ -145,10 +146,10 @@ async function handle( fetchResult = await fetch(targetUrl, fetchOptions); } finally { console.log( - "[Any Proxy]", + '[Any Proxy]', targetUrl, { - method: method, + method, }, { status: fetchResult?.status, @@ -164,4 +165,4 @@ export const PUT = handle; export const GET = handle; export const OPTIONS = handle; -export const runtime = "edge"; +export const runtime = 'edge'; diff --git a/app/api/xai.ts b/app/api/xai.ts index a4ee8b397..f59f1329c 100644 --- a/app/api/xai.ts +++ b/app/api/xai.ts @@ -1,14 +1,15 @@ -import { getServerSideConfig } from "@/app/config/server"; +import type { NextRequest } from 'next/server'; +import { auth } from '@/app/api/auth'; +import { getServerSideConfig } from '@/app/config/server'; import { - XAI_BASE_URL, ApiPath, ModelProvider, ServiceProvider, -} from "@/app/constant"; -import { prettyObject } from "@/app/utils/format"; -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/app/api/auth"; -import { isModelAvailableInServer } from "@/app/utils/model"; + XAI_BASE_URL, +} from '@/app/constant'; +import { prettyObject } from '@/app/utils/format'; +import { isModelAvailableInServer } from '@/app/utils/model'; +import { NextResponse } from 'next/server'; const serverConfig = getServerSideConfig(); @@ -16,10 +17,10 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("[XAI Route] params ", params); + console.log('[XAI Route] params ', params); - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); + if (req.method === 'OPTIONS') { + return NextResponse.json({ body: 'OK' }, { status: 200 }); } const authResult = auth(req, ModelProvider.XAI); @@ -33,7 +34,7 @@ export async function handle( const response = await request(req); return response; } catch (e) { - console.error("[XAI] ", e); + console.error('[XAI] ', e); return NextResponse.json(prettyObject(e)); } } @@ -42,20 +43,20 @@ async function request(req: NextRequest) { const controller = new AbortController(); // alibaba use base url or just remove the path - let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, ""); + const path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, ''); let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL; - if (!baseUrl.startsWith("http")) { + if (!baseUrl.startsWith('http')) { baseUrl = `https://${baseUrl}`; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", path); - console.log("[Base Url]", baseUrl); + console.log('[Proxy] ', path); + console.log('[Base Url]', baseUrl); const timeoutId = setTimeout( () => { @@ -67,14 +68,14 @@ async function request(req: NextRequest) { const fetchUrl = `${baseUrl}${path}`; const fetchOptions: RequestInit = { headers: { - "Content-Type": "application/json", - Authorization: req.headers.get("Authorization") ?? "", + 'Content-Type': 'application/json', + 'Authorization': req.headers.get('Authorization') ?? '', }, method: req.method, body: req.body, - redirect: "manual", + redirect: 'manual', // @ts-ignore - duplex: "half", + duplex: 'half', signal: controller.signal, }; @@ -113,9 +114,9 @@ async function request(req: NextRequest) { // to prevent browser prompt for credentials const newHeaders = new Headers(res.headers); - newHeaders.delete("www-authenticate"); + newHeaders.delete('www-authenticate'); // to disable nginx buffering - newHeaders.set("X-Accel-Buffering", "no"); + newHeaders.set('X-Accel-Buffering', 'no'); return new Response(res.body, { status: res.status, diff --git a/app/client/api.ts b/app/client/api.ts index 1da81e964..c6cce515b 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,37 +1,40 @@ -import { getClientConfig } from "../config/client"; +import type { + ChatMessage, + ChatMessageTool, + ModelType, +} from '../store'; +import type { DalleRequestPayload } from './platforms/openai'; +import { getClientConfig } from '../config/client'; import { ACCESS_CODE_PREFIX, ModelProvider, ServiceProvider, -} from "../constant"; +} from '../constant'; import { - ChatMessageTool, - ChatMessage, - ModelType, useAccessStore, useChatStore, -} from "../store"; -import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; -import { GeminiProApi } from "./platforms/google"; -import { ClaudeApi } from "./platforms/anthropic"; -import { ErnieApi } from "./platforms/baidu"; -import { DoubaoApi } from "./platforms/bytedance"; -import { QwenApi } from "./platforms/alibaba"; -import { HunyuanApi } from "./platforms/tencent"; -import { MoonshotApi } from "./platforms/moonshot"; -import { SparkApi } from "./platforms/iflytek"; -import { XAIApi } from "./platforms/xai"; -import { ChatGLMApi } from "./platforms/glm"; +} from '../store'; +import { QwenApi } from './platforms/alibaba'; +import { ClaudeApi } from './platforms/anthropic'; +import { ErnieApi } from './platforms/baidu'; +import { DoubaoApi } from './platforms/bytedance'; +import { ChatGLMApi } from './platforms/glm'; +import { GeminiProApi } from './platforms/google'; +import { SparkApi } from './platforms/iflytek'; +import { MoonshotApi } from './platforms/moonshot'; +import { ChatGPTApi } from './platforms/openai'; +import { HunyuanApi } from './platforms/tencent'; +import { XAIApi } from './platforms/xai'; -export const ROLES = ["system", "user", "assistant"] as const; +export const ROLES = ['system', 'user', 'assistant'] as const; export type MessageRole = (typeof ROLES)[number]; -export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; -export const TTSModels = ["tts-1", "tts-1-hd"] as const; +export const Models = ['gpt-3.5-turbo', 'gpt-4'] as const; +export const TTSModels = ['tts-1', 'tts-1-hd'] as const; export type ChatModel = ModelType; export interface MultimodalContent { - type: "text" | "image_url"; + type: 'text' | 'image_url'; text?: string; image_url?: { url: string; @@ -51,9 +54,9 @@ export interface LLMConfig { stream?: boolean; presence_penalty?: number; frequency_penalty?: number; - size?: DalleRequestPayload["size"]; - quality?: DalleRequestPayload["quality"]; - style?: DalleRequestPayload["style"]; + size?: DalleRequestPayload['size']; + quality?: DalleRequestPayload['quality']; + style?: DalleRequestPayload['style']; } export interface SpeechOptions { @@ -104,7 +107,7 @@ export abstract class LLMApi { abstract models(): Promise; } -type ProviderName = "openai" | "azure" | "claude" | "palm"; +type ProviderName = 'openai' | 'azure' | 'claude' | 'palm'; interface Model { name: string; @@ -173,24 +176,24 @@ export class ClientApi { async share(messages: ChatMessage[], avatarUrl: string | null = null) { const msgs = messages - .map((m) => ({ - from: m.role === "user" ? "human" : "gpt", + .map(m => ({ + from: m.role === 'user' ? 'human' : 'gpt', value: m.content, })) .concat([ { - from: "human", + from: 'human', value: - "Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web", + 'Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web', }, ]); // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用 // Please do not modify this message - console.log("[Share]", messages, msgs); + console.log('[Share]', messages, msgs); const clientConfig = getClientConfig(); - const proxyUrl = "/sharegpt"; - const rawUrl = "https://sharegpt.com/api/conversations"; + const proxyUrl = '/sharegpt'; + const rawUrl = 'https://sharegpt.com/api/conversations'; const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl; const res = await fetch(shareUrl, { body: JSON.stringify({ @@ -198,13 +201,13 @@ export class ClientApi { items: msgs, }), headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, - method: "POST", + method: 'POST', }); const resJson = await res.json(); - console.log("[Share]", resJson); + console.log('[Share]', resJson); if (resJson.id) { return `https://shareg.pt/${resJson.id}`; } @@ -216,8 +219,8 @@ export function getBearerToken( noBearer: boolean = false, ): string { return validString(apiKey) - ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}` - : ""; + ? `${noBearer ? '' : 'Bearer '}${apiKey.trim()}` + : ''; } export function validString(x: string): boolean { @@ -230,8 +233,8 @@ export function getHeaders(ignoreHeaders: boolean = false) { let headers: Record = {}; if (!ignoreHeaders) { headers = { - "Content-Type": "application/json", - Accept: "application/json", + 'Content-Type': 'application/json', + 'Accept': 'application/json', }; } @@ -253,24 +256,24 @@ export function getHeaders(ignoreHeaders: boolean = false) { const apiKey = isGoogle ? accessStore.googleApiKey : isAzure - ? accessStore.azureApiKey - : isAnthropic - ? accessStore.anthropicApiKey - : isByteDance - ? accessStore.bytedanceApiKey - : isAlibaba - ? accessStore.alibabaApiKey - : isMoonshot - ? accessStore.moonshotApiKey - : isXAI - ? accessStore.xaiApiKey - : isChatGLM - ? accessStore.chatglmApiKey - : isIflytek - ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret - ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret - : "" - : accessStore.openaiApiKey; + ? accessStore.azureApiKey + : isAnthropic + ? accessStore.anthropicApiKey + : isByteDance + ? accessStore.bytedanceApiKey + : isAlibaba + ? accessStore.alibabaApiKey + : isMoonshot + ? accessStore.moonshotApiKey + : isXAI + ? accessStore.xaiApiKey + : isChatGLM + ? accessStore.chatglmApiKey + : isIflytek + ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret + ? `${accessStore.iflytekApiKey}:${accessStore.iflytekApiSecret}` + : '' + : accessStore.openaiApiKey; return { isGoogle, isAzure, @@ -289,12 +292,12 @@ export function getHeaders(ignoreHeaders: boolean = false) { function getAuthHeader(): string { return isAzure - ? "api-key" + ? 'api-key' : isAnthropic - ? "x-api-key" - : isGoogle - ? "x-goog-api-key" - : "Authorization"; + ? 'x-api-key' + : isGoogle + ? 'x-goog-api-key' + : 'Authorization'; } const { @@ -306,7 +309,8 @@ export function getHeaders(ignoreHeaders: boolean = false) { isEnabledAccessControl, } = getConfig(); // when using baidu api in app, not set auth header - if (isBaidu && clientConfig?.isApp) return headers; + if (isBaidu && clientConfig?.isApp) + { return headers; } const authHeader = getAuthHeader(); @@ -318,7 +322,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { if (bearerToken) { headers[authHeader] = bearerToken; } else if (isEnabledAccessControl && validString(accessStore.accessCode)) { - headers["Authorization"] = getBearerToken( + headers.Authorization = getBearerToken( ACCESS_CODE_PREFIX + accessStore.accessCode, ); } diff --git a/app/client/controller.ts b/app/client/controller.ts index a2e00173d..5990f476a 100644 --- a/app/client/controller.ts +++ b/app/client/controller.ts @@ -19,7 +19,7 @@ export const ChatControllerPool = { }, stopAll() { - Object.values(this.controllers).forEach((v) => v.abort()); + Object.values(this.controllers).forEach(v => v.abort()); }, hasPending() { diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 6fe69e87a..731176c4c 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -1,29 +1,31 @@ -"use client"; -import { - ApiPath, - Alibaba, - ALIBABA_BASE_URL, - REQUEST_TIMEOUT_MS, -} from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; - -import { +'use client'; +import type { ChatOptions, - getHeaders, LLMApi, LLMModel, - SpeechOptions, MultimodalContent, -} from "../api"; -import Locale from "../../locales"; + SpeechOptions, +} from '../api'; +import { getClientConfig } from '@/app/config/client'; + +import { + Alibaba, + ALIBABA_BASE_URL, + ApiPath, + REQUEST_TIMEOUT_MS, +} from '@/app/constant'; +import { useAccessStore, useAppConfig, useChatStore } from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { prettyObject } from '@/app/utils/format'; +import { fetch } from '@/app/utils/stream'; import { EventStreamContentType, fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { fetch } from "@/app/utils/stream"; +} from '@fortaine/fetch-event-source'; +import Locale from '../../locales'; +import { + getHeaders, +} from '../api'; export interface OpenAIListModelResponse { object: string; @@ -36,7 +38,7 @@ export interface OpenAIListModelResponse { interface RequestInput { messages: { - role: "system" | "user" | "assistant"; + role: 'system' | 'user' | 'assistant'; content: string | MultimodalContent[]; }[]; } @@ -58,7 +60,7 @@ export class QwenApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.alibabaUrl; @@ -69,28 +71,28 @@ export class QwenApi implements LLMApi { baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Alibaba)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res?.output?.choices?.at(0)?.message?.content ?? ""; + return res?.output?.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages = options.messages.map((v) => ({ + const messages = options.messages.map(v => ({ role: v.role, content: getMessageTextContent(v), })); @@ -110,7 +112,7 @@ export class QwenApi implements LLMApi { messages, }, parameters: { - result_format: "message", + result_format: 'message', incremental_output: shouldStream, temperature: modelConfig.temperature, // max_tokens: modelConfig.max_tokens, @@ -124,12 +126,12 @@ export class QwenApi implements LLMApi { try { const chatPath = this.path(Alibaba.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: { ...getHeaders(), - "X-DashScope-SSE": shouldStream ? "enable" : "disable", + 'X-DashScope-SSE': shouldStream ? 'enable' : 'disable', }, }; @@ -140,8 +142,8 @@ export class QwenApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; + let responseText = ''; + let remainText = ''; let finished = false; let responseRes: Response; @@ -149,9 +151,9 @@ export class QwenApi implements LLMApi { function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + console.log('[Response Animation] finished'); if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); + options.onError?.(new Error('empty response from server')); } return; } @@ -184,24 +186,24 @@ export class QwenApi implements LLMApi { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); + const contentType = res.headers.get('content-type'); console.log( - "[Alibaba] request response content type: ", + '[Alibaba] request response content type: ', contentType, ); responseRes = res; - if (contentType?.startsWith("text/plain")) { + if (contentType?.startsWith('text/plain')) { responseText = await res.clone().text(); return finish(); } if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + !res.ok + || !res.headers + .get('content-type') + ?.startsWith(EventStreamContentType) + || res.status !== 200 ) { const responseTexts = [responseText]; let extraInfo = await res.clone().text(); @@ -218,13 +220,13 @@ export class QwenApi implements LLMApi { responseTexts.push(extraInfo); } - responseText = responseTexts.join("\n\n"); + responseText = responseTexts.join('\n\n'); return finish(); } }, onmessage(msg) { - if (msg.data === "[DONE]" || finished) { + if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; @@ -238,7 +240,7 @@ export class QwenApi implements LLMApi { remainText += delta; } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error('[Request] parse error', text, msg); } }, onclose() { @@ -259,10 +261,11 @@ export class QwenApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 6747221a8..46db3ce83 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -1,34 +1,36 @@ -import { Anthropic, ApiPath } from "@/app/constant"; -import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api"; +import type { + ChatMessageTool, +} from '@/app/store'; +import type { ChatOptions, LLMApi, SpeechOptions } from '../api'; +import type { RequestPayload } from './openai'; +import { getClientConfig } from '@/app/config/client'; +import { Anthropic, ANTHROPIC_BASE_URL, ApiPath } from '@/app/constant'; import { useAccessStore, useAppConfig, useChatStore, usePluginStore, - ChatMessageTool, -} from "@/app/store"; -import { getClientConfig } from "@/app/config/client"; -import { ANTHROPIC_BASE_URL } from "@/app/constant"; -import { getMessageTextContent, isVisionModel } from "@/app/utils"; -import { preProcessImageContent, stream } from "@/app/utils/chat"; -import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; -import { RequestPayload } from "./openai"; -import { fetch } from "@/app/utils/stream"; +} from '@/app/store'; +import { getMessageTextContent, isVisionModel } from '@/app/utils'; +import { preProcessImageContent, stream } from '@/app/utils/chat'; +import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare'; +import { fetch } from '@/app/utils/stream'; +import { getHeaders } from '../api'; -export type MultiBlockContent = { - type: "image" | "text"; +export interface MultiBlockContent { + type: 'image' | 'text'; source?: { type: string; media_type: string; data: string; }; text?: string; -}; +} -export type AnthropicMessage = { +export interface AnthropicMessage { role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper]; content: string | MultiBlockContent[]; -}; +} export interface AnthropicChatRequest { model: string; // The model that will complete your prompt. @@ -56,7 +58,7 @@ export interface ChatRequest { export interface ChatResponse { completion: string; - stop_reason: "stop_sequence" | "max_tokens"; + stop_reason: 'stop_sequence' | 'max_tokens'; model: string; } @@ -66,23 +68,24 @@ export type ChatStreamResponse = ChatResponse & { }; const ClaudeMapper = { - assistant: "assistant", - user: "user", - system: "user", + assistant: 'assistant', + user: 'user', + system: 'user', } as const; -const keys = ["claude-2, claude-instant-1"]; +const keys = ['claude-2, claude-instant-1']; export class ClaudeApi implements LLMApi { speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } extractMessage(res: any) { - console.log("[Response] claude response: ", res); + console.log('[Response] claude response: ', res); return res?.content?.[0]?.text; } + async chat(options: ChatOptions): Promise { const visionModel = isVisionModel(options.config.model); @@ -99,13 +102,13 @@ export class ClaudeApi implements LLMApi { }; // try get base64image from local cache image_url - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = await preProcessImageContent(v.content); messages.push({ role: v.role, content }); } - const keys = ["system", "user"]; + const keys = ['system', 'user']; // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages for (let i = 0; i < messages.length - 1; i++) { @@ -116,8 +119,8 @@ export class ClaudeApi implements LLMApi { messages[i] = [ message, { - role: "assistant", - content: ";", + role: 'assistant', + content: ';', }, ] as any; } @@ -126,15 +129,17 @@ export class ClaudeApi implements LLMApi { const prompt = messages .flat() .filter((v) => { - if (!v.content) return false; - if (typeof v.content === "string" && !v.content.trim()) return false; + if (!v.content) + { return false; } + if (typeof v.content === 'string' && !v.content.trim()) + { return false; } return true; }) .map((v) => { const { role, content } = v; - const insideRole = ClaudeMapper[role] ?? "user"; + const insideRole = ClaudeMapper[role] ?? 'user'; - if (!visionModel || typeof content === "string") { + if (!visionModel || typeof content === 'string') { return { role: insideRole, content: getMessageTextContent(v), @@ -143,25 +148,25 @@ export class ClaudeApi implements LLMApi { return { role: insideRole, content: content - .filter((v) => v.image_url || v.text) + .filter(v => v.image_url || v.text) .map(({ type, text, image_url }) => { - if (type === "text") { + if (type === 'text') { return { type, text: text!, }; } - const { url = "" } = image_url || {}; - const colonIndex = url.indexOf(":"); - const semicolonIndex = url.indexOf(";"); - const comma = url.indexOf(","); + const { url = '' } = image_url || {}; + const colonIndex = url.indexOf(':'); + const semicolonIndex = url.indexOf(';'); + const comma = url.indexOf(','); const mimeType = url.slice(colonIndex + 1, semicolonIndex); const encodeType = url.slice(semicolonIndex + 1, comma); const data = url.slice(comma + 1); return { - type: "image" as const, + type: 'image' as const, source: { type: encodeType, media_type: mimeType, @@ -172,10 +177,10 @@ export class ClaudeApi implements LLMApi { }; }); - if (prompt[0]?.role === "assistant") { + if (prompt[0]?.role === 'assistant') { prompt.unshift({ - role: "user", - content: ";", + role: 'user', + content: ';', }); } @@ -208,10 +213,10 @@ export class ClaudeApi implements LLMApi { requestBody, { ...getHeaders(), - "anthropic-version": accessStore.anthropicApiVersion, + 'anthropic-version': accessStore.anthropicApiVersion, }, // @ts-ignore - tools.map((tool) => ({ + tools.map(tool => ({ name: tool?.function?.name, description: tool?.function?.description, input_schema: tool?.function?.parameters, @@ -224,41 +229,41 @@ export class ClaudeApi implements LLMApi { let chunkJson: | undefined | { - type: "content_block_delta" | "content_block_stop"; - content_block?: { - type: "tool_use"; - id: string; - name: string; - }; - delta?: { - type: "text_delta" | "input_json_delta"; - text?: string; - partial_json?: string; - }; - index: number; + type: 'content_block_delta' | 'content_block_stop'; + content_block?: { + type: 'tool_use'; + id: string; + name: string; }; + delta?: { + type: 'text_delta' | 'input_json_delta'; + text?: string; + partial_json?: string; + }; + index: number; + }; chunkJson = JSON.parse(text); - if (chunkJson?.content_block?.type == "tool_use") { + if (chunkJson?.content_block?.type == 'tool_use') { index += 1; const id = chunkJson?.content_block.id; const name = chunkJson?.content_block.name; runTools.push({ id, - type: "function", + type: 'function', function: { name, - arguments: "", + arguments: '', }, }); } if ( - chunkJson?.delta?.type == "input_json_delta" && - chunkJson?.delta?.partial_json + chunkJson?.delta?.type == 'input_json_delta' + && chunkJson?.delta?.partial_json ) { // @ts-ignore - runTools[index]["function"]["arguments"] += - chunkJson?.delta?.partial_json; + runTools[index].function.arguments + += chunkJson?.delta?.partial_json; } return chunkJson?.delta?.text; }, @@ -276,10 +281,10 @@ export class ClaudeApi implements LLMApi { requestPayload?.messages?.length, 0, { - role: "assistant", + role: 'assistant', content: toolCallMessage.tool_calls.map( (tool: ChatMessageTool) => ({ - type: "tool_use", + type: 'tool_use', id: tool.id, name: tool?.function?.name, input: tool?.function?.arguments @@ -289,11 +294,11 @@ export class ClaudeApi implements LLMApi { ), }, // @ts-ignore - ...toolCallResult.map((result) => ({ - role: "user", + ...toolCallResult.map(result => ({ + role: 'user', content: [ { - type: "tool_result", + type: 'tool_result', tool_use_id: result.tool_call_id, content: result.content, }, @@ -305,12 +310,12 @@ export class ClaudeApi implements LLMApi { ); } else { const payload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestBody), signal: controller.signal, headers: { ...getHeaders(), // get common headers - "anthropic-version": accessStore.anthropicApiVersion, + 'anthropic-version': accessStore.anthropicApiVersion, // do not send `anthropicApiKey` in browser!!! // Authorization: getAuthKey(accessStore.anthropicApiKey), }, @@ -318,7 +323,7 @@ export class ClaudeApi implements LLMApi { try { controller.signal.onabort = () => - options.onFinish("", new Response(null, { status: 400 })); + options.onFinish('', new Response(null, { status: 400 })); const res = await fetch(path, payload); const resJson = await res.json(); @@ -326,17 +331,19 @@ export class ClaudeApi implements LLMApi { const message = this.extractMessage(resJson); options.onFinish(message, res); } catch (e) { - console.error("failed to chat", e); + console.error('failed to chat', e); options.onError?.(e as Error); } } } + async usage() { return { used: 0, total: 0, }; } + async models() { // const provider = { // id: "anthropic", @@ -377,10 +384,11 @@ export class ClaudeApi implements LLMApi { // }, ]; } + path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl: string = ""; + let baseUrl: string = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.anthropicUrl; @@ -393,19 +401,20 @@ export class ClaudeApi implements LLMApi { baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic; } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith('/api')) { + baseUrl = `https://${baseUrl}`; } - baseUrl = trimEnd(baseUrl, "/"); + baseUrl = trimEnd(baseUrl, '/'); // try rebuild url, when using cloudflare ai gateway in client return cloudflareAIGatewayUrl(`${baseUrl}/${path}`); } } -function trimEnd(s: string, end = " ") { - if (end.length === 0) return s; +function trimEnd(s: string, end = ' ') { + if (end.length === 0) + { return s; } while (s.endsWith(end)) { s = s.slice(0, -end.length); diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 9e8c2f139..54f2976d6 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -1,30 +1,32 @@ -"use client"; +'use client'; +import type { + ChatOptions, + LLMApi, + LLMModel, + MultimodalContent, + SpeechOptions, +} from '../api'; +import { getClientConfig } from '@/app/config/client'; import { ApiPath, Baidu, BAIDU_BASE_URL, REQUEST_TIMEOUT_MS, -} from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; -import { getAccessToken } from "@/app/utils/baidu"; +} from '@/app/constant'; -import { - ChatOptions, - getHeaders, - LLMApi, - LLMModel, - MultimodalContent, - SpeechOptions, -} from "../api"; -import Locale from "../../locales"; +import { useAccessStore, useAppConfig, useChatStore } from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { getAccessToken } from '@/app/utils/baidu'; +import { prettyObject } from '@/app/utils/format'; +import { fetch } from '@/app/utils/stream'; import { EventStreamContentType, fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { fetch } from "@/app/utils/stream"; +} from '@fortaine/fetch-event-source'; +import Locale from '../../locales'; +import { + getHeaders, +} from '../api'; export interface OpenAIListModelResponse { object: string; @@ -37,7 +39,7 @@ export interface OpenAIListModelResponse { interface RequestPayload { messages: { - role: "system" | "user" | "assistant"; + role: 'system' | 'user' | 'assistant'; content: string | MultimodalContent[]; }[]; stream?: boolean; @@ -53,7 +55,7 @@ export class ErnieApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.baiduUrl; @@ -65,40 +67,40 @@ export class ErnieApi implements LLMApi { baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Baidu)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages = options.messages.map((v) => ({ + const messages = options.messages.map(v => ({ // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function", - role: v.role === "system" ? "user" : v.role, + role: v.role === 'system' ? 'user' : v.role, content: getMessageTextContent(v), })); // "error_code": 336006, "error_msg": "the length of messages must be an odd number", if (messages.length % 2 === 0) { - if (messages.at(0)?.role === "user") { + if (messages.at(0)?.role === 'user') { messages.splice(1, 0, { - role: "assistant", - content: " ", + role: 'assistant', + content: ' ', }); } else { messages.unshift({ - role: "user", - content: " ", + role: 'user', + content: ' ', }); } } @@ -122,7 +124,7 @@ export class ErnieApi implements LLMApi { top_p: modelConfig.top_p, }; - console.log("[Request] Baidu payload: ", requestPayload); + console.log('[Request] Baidu payload: ', requestPayload); const controller = new AbortController(); options.onController?.(controller); @@ -131,7 +133,7 @@ export class ErnieApi implements LLMApi { let chatPath = this.path(Baidu.ChatPath(modelConfig.model)); // getAccessToken can not run in browser, because cors error - if (!!getClientConfig()?.isApp) { + if (getClientConfig()?.isApp) { const accessStore = useAccessStore.getState(); if (accessStore.useCustomConfig) { if (accessStore.isValidBaidu()) { @@ -140,13 +142,13 @@ export class ErnieApi implements LLMApi { accessStore.baiduSecretKey, ); chatPath = `${chatPath}${ - chatPath.includes("?") ? "&" : "?" + chatPath.includes('?') ? '&' : '?' }access_token=${access_token}`; } } } const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -159,8 +161,8 @@ export class ErnieApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; + let responseText = ''; + let remainText = ''; let finished = false; let responseRes: Response; @@ -168,9 +170,9 @@ export class ErnieApi implements LLMApi { function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + console.log('[Response Animation] finished'); if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); + options.onError?.(new Error('empty response from server')); } return; } @@ -203,20 +205,20 @@ export class ErnieApi implements LLMApi { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log("[Baidu] request response content type: ", contentType); + const contentType = res.headers.get('content-type'); + console.log('[Baidu] request response content type: ', contentType); responseRes = res; - if (contentType?.startsWith("text/plain")) { + if (contentType?.startsWith('text/plain')) { responseText = await res.clone().text(); return finish(); } if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + !res.ok + || !res.headers + .get('content-type') + ?.startsWith(EventStreamContentType) + || res.status !== 200 ) { const responseTexts = [responseText]; let extraInfo = await res.clone().text(); @@ -233,13 +235,13 @@ export class ErnieApi implements LLMApi { responseTexts.push(extraInfo); } - responseText = responseTexts.join("\n\n"); + responseText = responseTexts.join('\n\n'); return finish(); } }, onmessage(msg) { - if (msg.data === "[DONE]" || finished) { + if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; @@ -250,7 +252,7 @@ export class ErnieApi implements LLMApi { remainText += delta; } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error('[Request] parse error', text, msg); } }, onclose() { @@ -271,10 +273,11 @@ export class ErnieApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index a2f0660d8..126ade377 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -1,29 +1,31 @@ -"use client"; +'use client'; +import type { + ChatOptions, + LLMApi, + LLMModel, + MultimodalContent, + SpeechOptions, +} from '../api'; +import { getClientConfig } from '@/app/config/client'; + import { ApiPath, ByteDance, BYTEDANCE_BASE_URL, REQUEST_TIMEOUT_MS, -} from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; - -import { - ChatOptions, - getHeaders, - LLMApi, - LLMModel, - MultimodalContent, - SpeechOptions, -} from "../api"; -import Locale from "../../locales"; +} from '@/app/constant'; +import { useAccessStore, useAppConfig, useChatStore } from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { prettyObject } from '@/app/utils/format'; +import { fetch } from '@/app/utils/stream'; import { EventStreamContentType, fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { fetch } from "@/app/utils/stream"; +} from '@fortaine/fetch-event-source'; +import Locale from '../../locales'; +import { + getHeaders, +} from '../api'; export interface OpenAIListModelResponse { object: string; @@ -36,7 +38,7 @@ export interface OpenAIListModelResponse { interface RequestPayload { messages: { - role: "system" | "user" | "assistant"; + role: 'system' | 'user' | 'assistant'; content: string | MultimodalContent[]; }[]; stream?: boolean; @@ -52,7 +54,7 @@ export class DoubaoApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.bytedanceUrl; @@ -63,28 +65,28 @@ export class DoubaoApi implements LLMApi { baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ByteDance)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages = options.messages.map((v) => ({ + const messages = options.messages.map(v => ({ role: v.role, content: getMessageTextContent(v), })); @@ -114,7 +116,7 @@ export class DoubaoApi implements LLMApi { try { const chatPath = this.path(ByteDance.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -127,8 +129,8 @@ export class DoubaoApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; + let responseText = ''; + let remainText = ''; let finished = false; let responseRes: Response; @@ -136,9 +138,9 @@ export class DoubaoApi implements LLMApi { function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + console.log('[Response Animation] finished'); if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); + options.onError?.(new Error('empty response from server')); } return; } @@ -171,23 +173,23 @@ export class DoubaoApi implements LLMApi { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); + const contentType = res.headers.get('content-type'); console.log( - "[ByteDance] request response content type: ", + '[ByteDance] request response content type: ', contentType, ); responseRes = res; - if (contentType?.startsWith("text/plain")) { + if (contentType?.startsWith('text/plain')) { responseText = await res.clone().text(); return finish(); } if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + !res.ok + || !res.headers + .get('content-type') + ?.startsWith(EventStreamContentType) + || res.status !== 200 ) { const responseTexts = [responseText]; let extraInfo = await res.clone().text(); @@ -204,13 +206,13 @@ export class DoubaoApi implements LLMApi { responseTexts.push(extraInfo); } - responseText = responseTexts.join("\n\n"); + responseText = responseTexts.join('\n\n'); return finish(); } }, onmessage(msg) { - if (msg.data === "[DONE]" || finished) { + if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; @@ -224,7 +226,7 @@ export class DoubaoApi implements LLMApi { remainText += delta; } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error('[Request] parse error', text, msg); } }, onclose() { @@ -245,10 +247,11 @@ export class DoubaoApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/glm.ts b/app/client/platforms/glm.ts index a7965947f..c7c201264 100644 --- a/app/client/platforms/glm.ts +++ b/app/client/platforms/glm.ts @@ -1,29 +1,33 @@ -"use client"; +'use client'; +import type { + ChatMessageTool, +} from '@/app/store'; +import type { + ChatOptions, + LLMApi, + LLMModel, + SpeechOptions, +} from '../api'; +import type { RequestPayload } from './openai'; +import { getClientConfig } from '@/app/config/client'; import { ApiPath, - CHATGLM_BASE_URL, ChatGLM, + CHATGLM_BASE_URL, REQUEST_TIMEOUT_MS, -} from "@/app/constant"; +} from '@/app/constant'; import { useAccessStore, useAppConfig, useChatStore, - ChatMessageTool, usePluginStore, -} from "@/app/store"; -import { stream } from "@/app/utils/chat"; +} from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { stream } from '@/app/utils/chat'; +import { fetch } from '@/app/utils/stream'; import { - ChatOptions, getHeaders, - LLMApi, - LLMModel, - SpeechOptions, -} from "../api"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { RequestPayload } from "./openai"; -import { fetch } from "@/app/utils/stream"; +} from '../api'; export class ChatGLMApi implements LLMApi { private disableListModels = true; @@ -31,7 +35,7 @@ export class ChatGLMApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.chatglmUrl; @@ -43,28 +47,28 @@ export class ChatGLMApi implements LLMApi { baseUrl = isApp ? CHATGLM_BASE_URL : apiPath; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.ChatGLM)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = getMessageTextContent(v); messages.push({ role: v.role, content }); @@ -89,7 +93,7 @@ export class ChatGLMApi implements LLMApi { top_p: modelConfig.top_p, }; - console.log("[Request] glm payload: ", requestPayload); + console.log('[Request] glm payload: ', requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); @@ -98,7 +102,7 @@ export class ChatGLMApi implements LLMApi { try { const chatPath = this.path(ChatGLM.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -149,7 +153,7 @@ export class ChatGLMApi implements LLMApi { }); } else { // @ts-ignore - runTools[index]["function"]["arguments"] += args; + runTools[index].function.arguments += args; } } return choices[0]?.delta?.content; @@ -180,10 +184,11 @@ export class ChatGLMApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index a7bce4fc2..91c09288d 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -1,38 +1,40 @@ -import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; -import { +import type { + ChatMessageTool, +} from '@/app/store'; +import type { ChatOptions, - getHeaders, LLMApi, LLMModel, LLMUsage, SpeechOptions, -} from "../api"; +} from '../api'; +import type { RequestPayload } from './openai'; +import { getClientConfig } from '@/app/config/client'; +import { ApiPath, GEMINI_BASE_URL, Google, REQUEST_TIMEOUT_MS } from '@/app/constant'; import { useAccessStore, useAppConfig, useChatStore, usePluginStore, - ChatMessageTool, -} from "@/app/store"; -import { stream } from "@/app/utils/chat"; -import { getClientConfig } from "@/app/config/client"; -import { GEMINI_BASE_URL } from "@/app/constant"; +} from '@/app/store'; import { - getMessageTextContent, getMessageImages, + getMessageTextContent, isVisionModel, -} from "@/app/utils"; -import { preProcessImageContent } from "@/app/utils/chat"; -import { nanoid } from "nanoid"; -import { RequestPayload } from "./openai"; -import { fetch } from "@/app/utils/stream"; +} from '@/app/utils'; +import { preProcessImageContent, stream } from '@/app/utils/chat'; +import { fetch } from '@/app/utils/stream'; +import { nanoid } from 'nanoid'; +import { + getHeaders, +} from '../api'; export class GeminiProApi implements LLMApi { path(path: string, shouldStream = false): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.googleUrl; } @@ -41,34 +43,36 @@ export class GeminiProApi implements LLMApi { if (baseUrl.length === 0) { baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Google)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - let chatPath = [baseUrl, path].join("/"); + let chatPath = [baseUrl, path].join('/'); if (shouldStream) { - chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse"; + chatPath += chatPath.includes('?') ? '&alt=sse' : '?alt=sse'; } return chatPath; } + extractMessage(res: any) { - console.log("[Response] gemini-pro response: ", res); + console.log('[Response] gemini-pro response: ', res); return ( - res?.candidates?.at(0)?.content?.parts.at(0)?.text || - res?.at(0)?.candidates?.at(0)?.content?.parts.at(0)?.text || - res?.error?.message || - "" + res?.candidates?.at(0)?.content?.parts.at(0)?.text + || res?.at(0)?.candidates?.at(0)?.content?.parts.at(0)?.text + || res?.error?.message + || '' ); } + speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions): Promise { @@ -76,7 +80,7 @@ export class GeminiProApi implements LLMApi { let multimodal = false; // try get base64image from local cache image_url - const _messages: ChatOptions["messages"] = []; + const _messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = await preProcessImageContent(v.content); _messages.push({ role: v.role, content }); @@ -89,8 +93,8 @@ export class GeminiProApi implements LLMApi { multimodal = true; parts = parts.concat( images.map((image) => { - const imageType = image.split(";")[0].split(":")[1]; - const imageData = image.split(",")[1]; + const imageType = image.split(';')[0].split(':')[1]; + const imageData = image.split(',')[1]; return { inline_data: { mime_type: imageType, @@ -102,13 +106,13 @@ export class GeminiProApi implements LLMApi { } } return { - role: v.role.replace("assistant", "model").replace("system", "user"), - parts: parts, + role: v.role.replace('assistant', 'model').replace('system', 'user'), + parts, }; }); // google requires that role in neighboring messages must not be the same - for (let i = 0; i < messages.length - 1; ) { + for (let i = 0; i < messages.length - 1;) { // Check if current and next item both have the role "model" if (messages[i].role === messages[i + 1].role) { // Concatenate the 'parts' of the current and next item @@ -146,25 +150,25 @@ export class GeminiProApi implements LLMApi { }, safetySettings: [ { - category: "HARM_CATEGORY_HARASSMENT", + category: 'HARM_CATEGORY_HARASSMENT', threshold: accessStore.googleSafetySettings, }, { - category: "HARM_CATEGORY_HATE_SPEECH", + category: 'HARM_CATEGORY_HATE_SPEECH', threshold: accessStore.googleSafetySettings, }, { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: accessStore.googleSafetySettings, }, { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: accessStore.googleSafetySettings, }, ], }; - let shouldStream = !!options.config.stream; + const shouldStream = !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); try { @@ -175,7 +179,7 @@ export class GeminiProApi implements LLMApi { ); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -200,7 +204,7 @@ export class GeminiProApi implements LLMApi { // @ts-ignore tools.length > 0 ? // @ts-ignore - [{ functionDeclarations: tools.map((tool) => tool.function) }] + [{ functionDeclarations: tools.map(tool => tool.function) }] : [], funcs, controller, @@ -211,12 +215,15 @@ export class GeminiProApi implements LLMApi { const functionCall = chunkJson?.candidates ?.at(0) - ?.content.parts.at(0)?.functionCall; + ?.content + .parts + .at(0) + ?.functionCall; if (functionCall) { const { name, args } = functionCall; runTools.push({ id: nanoid(), - type: "function", + type: 'function', function: { name, arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse @@ -237,7 +244,7 @@ export class GeminiProApi implements LLMApi { requestPayload?.contents?.length, 0, { - role: "model", + role: 'model', parts: toolCallMessage.tool_calls.map( (tool: ChatMessageTool) => ({ functionCall: { @@ -248,8 +255,8 @@ export class GeminiProApi implements LLMApi { ), }, // @ts-ignore - ...toolCallResult.map((result) => ({ - role: "function", + ...toolCallResult.map(result => ({ + role: 'function', parts: [ { functionResponse: { @@ -274,8 +281,8 @@ export class GeminiProApi implements LLMApi { // being blocked options.onError?.( new Error( - "Message is being blocked for reason: " + - resJson.promptFeedback.blockReason, + `Message is being blocked for reason: ${ + resJson.promptFeedback.blockReason}`, ), ); } @@ -283,13 +290,15 @@ export class GeminiProApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + usage(): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } + async models(): Promise { return []; } diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts index cfc37b3b2..1bb8444ba 100644 --- a/app/client/platforms/iflytek.ts +++ b/app/client/platforms/iflytek.ts @@ -1,30 +1,32 @@ -"use client"; -import { - ApiPath, - IFLYTEK_BASE_URL, - Iflytek, - REQUEST_TIMEOUT_MS, -} from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; - -import { +'use client'; +import type { ChatOptions, - getHeaders, LLMApi, LLMModel, SpeechOptions, -} from "../api"; -import Locale from "../../locales"; +} from '../api'; +import type { RequestPayload } from './openai'; + +import { getClientConfig } from '@/app/config/client'; +import { + ApiPath, + Iflytek, + IFLYTEK_BASE_URL, + REQUEST_TIMEOUT_MS, +} from '@/app/constant'; +import { useAccessStore, useAppConfig, useChatStore } from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { prettyObject } from '@/app/utils/format'; +import { fetch } from '@/app/utils/stream'; import { EventStreamContentType, fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { fetch } from "@/app/utils/stream"; +} from '@fortaine/fetch-event-source'; +import Locale from '../../locales'; -import { RequestPayload } from "./openai"; +import { + getHeaders, +} from '../api'; export class SparkApi implements LLMApi { private disableListModels = true; @@ -32,7 +34,7 @@ export class SparkApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.iflytekUrl; @@ -44,28 +46,28 @@ export class SparkApi implements LLMApi { baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Iflytek)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = getMessageTextContent(v); messages.push({ role: v.role, content }); @@ -92,7 +94,7 @@ export class SparkApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; - console.log("[Request] Spark payload: ", requestPayload); + console.log('[Request] Spark payload: ', requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); @@ -101,7 +103,7 @@ export class SparkApi implements LLMApi { try { const chatPath = this.path(Iflytek.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -114,8 +116,8 @@ export class SparkApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; + let responseText = ''; + let remainText = ''; let finished = false; let responseRes: Response; @@ -123,7 +125,7 @@ export class SparkApi implements LLMApi { function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + console.log('[Response Animation] finished'); return; } @@ -155,21 +157,21 @@ export class SparkApi implements LLMApi { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); - console.log("[Spark] request response content type: ", contentType); + const contentType = res.headers.get('content-type'); + console.log('[Spark] request response content type: ', contentType); responseRes = res; - if (contentType?.startsWith("text/plain")) { + if (contentType?.startsWith('text/plain')) { responseText = await res.clone().text(); return finish(); } // Handle different error scenarios if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + !res.ok + || !res.headers + .get('content-type') + ?.startsWith(EventStreamContentType) + || res.status !== 200 ) { let extraInfo = await res.clone().text(); try { @@ -190,7 +192,7 @@ export class SparkApi implements LLMApi { } }, onmessage(msg) { - if (msg.data === "[DONE]" || finished) { + if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; @@ -205,7 +207,7 @@ export class SparkApi implements LLMApi { remainText += delta; } } catch (e) { - console.error("[Request] parse error", text); + console.error('[Request] parse error', text); options.onError?.(new Error(`Failed to parse response: ${text}`)); } }, @@ -235,7 +237,7 @@ export class SparkApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index b6812c0d7..c3f99d7a2 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -1,30 +1,34 @@ -"use client"; +'use client'; +import type { + ChatMessageTool, +} from '@/app/store'; +import type { + ChatOptions, + LLMApi, + LLMModel, + SpeechOptions, +} from '../api'; +import type { RequestPayload } from './openai'; +import { getClientConfig } from '@/app/config/client'; // azure and openai, using same models. so using same LLMApi. import { ApiPath, - MOONSHOT_BASE_URL, Moonshot, + MOONSHOT_BASE_URL, REQUEST_TIMEOUT_MS, -} from "@/app/constant"; +} from '@/app/constant'; import { useAccessStore, useAppConfig, useChatStore, - ChatMessageTool, usePluginStore, -} from "@/app/store"; -import { stream } from "@/app/utils/chat"; +} from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { stream } from '@/app/utils/chat'; +import { fetch } from '@/app/utils/stream'; import { - ChatOptions, getHeaders, - LLMApi, - LLMModel, - SpeechOptions, -} from "../api"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { RequestPayload } from "./openai"; -import { fetch } from "@/app/utils/stream"; +} from '../api'; export class MoonshotApi implements LLMApi { private disableListModels = true; @@ -32,7 +36,7 @@ export class MoonshotApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.moonshotUrl; @@ -44,28 +48,28 @@ export class MoonshotApi implements LLMApi { baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Moonshot)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = getMessageTextContent(v); messages.push({ role: v.role, content }); @@ -92,7 +96,7 @@ export class MoonshotApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; - console.log("[Request] openai payload: ", requestPayload); + console.log('[Request] openai payload: ', requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); @@ -101,7 +105,7 @@ export class MoonshotApi implements LLMApi { try { const chatPath = this.path(Moonshot.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -152,7 +156,7 @@ export class MoonshotApi implements LLMApi { }); } else { // @ts-ignore - runTools[index]["function"]["arguments"] += args; + runTools[index].function.arguments += args; } } return choices[0]?.delta?.content; @@ -183,10 +187,11 @@ export class MoonshotApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 15cfb7ca6..44bb52c1d 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,48 +1,52 @@ -"use client"; -// azure and openai, using same models. so using same LLMApi. -import { - ApiPath, - OPENAI_BASE_URL, - DEFAULT_MODELS, - OpenaiPath, - Azure, - REQUEST_TIMEOUT_MS, - ServiceProvider, -} from "@/app/constant"; -import { +'use client'; +import type { ChatMessageTool, - useAccessStore, - useAppConfig, - useChatStore, - usePluginStore, -} from "@/app/store"; -import { collectModelsWithDefaultModel } from "@/app/utils/model"; -import { - preProcessImageContent, - uploadImage, - base64Image2Blob, - stream, -} from "@/app/utils/chat"; -import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; -import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing"; - -import { +} from '@/app/store'; +import type { DalleQuality, DalleSize, DalleStyle } from '@/app/typing'; +import type { ChatOptions, - getHeaders, LLMApi, LLMModel, LLMUsage, MultimodalContent, SpeechOptions, -} from "../api"; -import Locale from "../../locales"; -import { getClientConfig } from "@/app/config/client"; +} from '../api'; +import { getClientConfig } from '@/app/config/client'; +// azure and openai, using same models. so using same LLMApi. import { + ApiPath, + Azure, + DEFAULT_MODELS, + OPENAI_BASE_URL, + OpenaiPath, + REQUEST_TIMEOUT_MS, + ServiceProvider, +} from '@/app/constant'; +import { + useAccessStore, + useAppConfig, + useChatStore, + usePluginStore, +} from '@/app/store'; +import { + isDalle3 as _isDalle3, getMessageTextContent, isVisionModel, - isDalle3 as _isDalle3, -} from "@/app/utils"; -import { fetch } from "@/app/utils/stream"; +} from '@/app/utils'; + +import { + base64Image2Blob, + preProcessImageContent, + stream, + uploadImage, +} from '@/app/utils/chat'; +import { cloudflareAIGatewayUrl } from '@/app/utils/cloudflare'; +import { collectModelsWithDefaultModel } from '@/app/utils/model'; +import { fetch } from '@/app/utils/stream'; +import Locale from '../../locales'; +import { + getHeaders, +} from '../api'; export interface OpenAIListModelResponse { object: string; @@ -55,7 +59,7 @@ export interface OpenAIListModelResponse { export interface RequestPayload { messages: { - role: "system" | "user" | "assistant"; + role: 'system' | 'user' | 'assistant'; content: string | MultimodalContent[]; }[]; stream?: boolean; @@ -71,7 +75,7 @@ export interface RequestPayload { export interface DalleRequestPayload { model: string; prompt: string; - response_format: "url" | "b64_json"; + response_format: 'url' | 'b64_json'; n: number; size: DalleSize; quality: DalleQuality; @@ -84,13 +88,13 @@ export class ChatGPTApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; - const isAzure = path.includes("deployments"); + const isAzure = path.includes('deployments'); if (accessStore.useCustomConfig) { if (isAzure && !accessStore.isValidAzure()) { - throw Error( - "incomplete azure config, please check it in your settings page", + throw new Error( + 'incomplete azure config, please check it in your settings page', ); } @@ -103,38 +107,38 @@ export class ChatGPTApi implements LLMApi { baseUrl = isApp ? OPENAI_BASE_URL : apiPath; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } if ( - !baseUrl.startsWith("http") && - !isAzure && - !baseUrl.startsWith(ApiPath.OpenAI) + !baseUrl.startsWith('http') + && !isAzure + && !baseUrl.startsWith(ApiPath.OpenAI) ) { - baseUrl = "https://" + baseUrl; + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); // try rebuild url, when using cloudflare ai gateway in client - return cloudflareAIGatewayUrl([baseUrl, path].join("/")); + return cloudflareAIGatewayUrl([baseUrl, path].join('/')); } async extractMessage(res: any) { if (res.error) { - return "```\n" + JSON.stringify(res, null, 4) + "\n```"; + return `\`\`\`\n${JSON.stringify(res, null, 4)}\n\`\`\``; } // dalle3 model return url, using url create image message if (res.data) { - let url = res.data?.at(0)?.url ?? ""; - const b64_json = res.data?.at(0)?.b64_json ?? ""; + let url = res.data?.at(0)?.url ?? ''; + const b64_json = res.data?.at(0)?.b64_json ?? ''; if (!url && b64_json) { // uploadImage - url = await uploadImage(base64Image2Blob(b64_json, "image/png")); + url = await uploadImage(base64Image2Blob(b64_json, 'image/png')); } return [ { - type: "image_url", + type: 'image_url', image_url: { url, }, @@ -153,7 +157,7 @@ export class ChatGPTApi implements LLMApi { speed: options.speed, }; - console.log("[Request] openai speech payload: ", requestPayload); + console.log('[Request] openai speech payload: ', requestPayload); const controller = new AbortController(); options.onController?.(controller); @@ -161,7 +165,7 @@ export class ChatGPTApi implements LLMApi { try { const speechPath = this.path(OpenaiPath.SpeechPath); const speechPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -177,7 +181,7 @@ export class ChatGPTApi implements LLMApi { clearTimeout(requestTimeoutId); return await res.arrayBuffer(); } catch (e) { - console.log("[Request] failed to make a speech request", e); + console.log('[Request] failed to make a speech request', e); throw e; } } @@ -195,7 +199,7 @@ export class ChatGPTApi implements LLMApi { let requestPayload: RequestPayload | DalleRequestPayload; const isDalle3 = _isDalle3(options.config.model); - const isO1 = options.config.model.startsWith("o1"); + const isO1 = options.config.model.startsWith('o1'); if (isDalle3) { const prompt = getMessageTextContent( options.messages.slice(-1)?.pop() as any, @@ -204,21 +208,21 @@ export class ChatGPTApi implements LLMApi { model: options.config.model, prompt, // URLs are only valid for 60 minutes after the image has been generated. - response_format: "b64_json", // using b64_json, and save image in CacheStorage + response_format: 'b64_json', // using b64_json, and save image in CacheStorage n: 1, - size: options.config?.size ?? "1024x1024", - quality: options.config?.quality ?? "standard", - style: options.config?.style ?? "vivid", + size: options.config?.size ?? '1024x1024', + quality: options.config?.quality ?? 'standard', + style: options.config?.style ?? 'vivid', }; } else { const visionModel = isVisionModel(options.config.model); - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = visionModel ? await preProcessImageContent(v.content) : getMessageTextContent(v); - if (!(isO1 && v.role === "system")) - messages.push({ role: v.role, content }); + if (!(isO1 && v.role === 'system')) + { messages.push({ role: v.role, content }); } } // O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet. @@ -236,27 +240,27 @@ export class ChatGPTApi implements LLMApi { // O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs) if (isO1) { - requestPayload["max_completion_tokens"] = modelConfig.max_tokens; + requestPayload.max_completion_tokens = modelConfig.max_tokens; } // add max_tokens to vision model if (visionModel) { - requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); + requestPayload.max_tokens = Math.max(modelConfig.max_tokens, 4000); } } - console.log("[Request] openai payload: ", requestPayload); + console.log('[Request] openai payload: ', requestPayload); const shouldStream = !isDalle3 && !!options.config.stream; const controller = new AbortController(); options.onController?.(controller); try { - let chatPath = ""; + let chatPath = ''; if (modelConfig.providerName === ServiceProvider.Azure) { // find model, and get displayName as deployName - const { models: configModels, customModels: configCustomModels } = - useAppConfig.getState(); + const { models: configModels, customModels: configCustomModels } + = useAppConfig.getState(); const { defaultModel, customModels: accessCustomModels, @@ -264,18 +268,18 @@ export class ChatGPTApi implements LLMApi { } = useAccessStore.getState(); const models = collectModelsWithDefaultModel( configModels, - [configCustomModels, accessCustomModels].join(","), + [configCustomModels, accessCustomModels].join(','), defaultModel, ); const model = models.find( - (model) => - model.name === modelConfig.model && - model?.provider?.providerName === ServiceProvider.Azure, + model => + model.name === modelConfig.model + && model?.provider?.providerName === ServiceProvider.Azure, ); chatPath = this.path( (isDalle3 ? Azure.ImagePath : Azure.ChatPath)( (model?.displayName ?? model?.name) as string, - useCustomConfig ? useAccessStore.getState().azureApiVersion : "", + useCustomConfig ? useAccessStore.getState().azureApiVersion : '', ), ); } else { @@ -324,7 +328,7 @@ export class ChatGPTApi implements LLMApi { }); } else { // @ts-ignore - runTools[index]["function"]["arguments"] += args; + runTools[index].function.arguments += args; } } return choices[0]?.delta?.content; @@ -350,7 +354,7 @@ export class ChatGPTApi implements LLMApi { ); } else { const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -370,16 +374,17 @@ export class ChatGPTApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { const formatDate = (d: Date) => - `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d + `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d .getDate() .toString() - .padStart(2, "0")}`; + .padStart(2, '0')}`; const ONE_DAY = 1 * 24 * 60 * 60 * 1000; const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); @@ -392,12 +397,12 @@ export class ChatGPTApi implements LLMApi { `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`, ), { - method: "GET", + method: 'GET', headers: getHeaders(), }, ), fetch(this.path(OpenaiPath.SubsPath), { - method: "GET", + method: 'GET', headers: getHeaders(), }), ]); @@ -407,7 +412,7 @@ export class ChatGPTApi implements LLMApi { } if (!used.ok || !subs.ok) { - throw new Error("Failed to query usage from openai"); + throw new Error('Failed to query usage from openai'); } const response = (await used.json()) as { @@ -423,7 +428,7 @@ export class ChatGPTApi implements LLMApi { }; if (response.error && response.error.type) { - throw Error(response.error.message); + throw new Error(response.error.message); } if (response.total_usage) { @@ -446,7 +451,7 @@ export class ChatGPTApi implements LLMApi { } const res = await fetch(this.path(OpenaiPath.ListModelPath), { - method: "GET", + method: 'GET', headers: { ...getHeaders(), }, @@ -454,24 +459,24 @@ export class ChatGPTApi implements LLMApi { const resJson = (await res.json()) as OpenAIListModelResponse; const chatModels = resJson.data?.filter( - (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"), + m => m.id.startsWith('gpt-') || m.id.startsWith('chatgpt-'), ); - console.log("[Models]", chatModels); + console.log('[Models]', chatModels); if (!chatModels) { return []; } - //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场 - let seq = 1000; //同 Constant.ts 中的排序保持一致 - return chatModels.map((m) => ({ + // 由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场 + let seq = 1000; // 同 Constant.ts 中的排序保持一致 + return chatModels.map(m => ({ name: m.id, available: true, sorted: seq++, provider: { - id: "openai", - providerName: "OpenAI", - providerType: "openai", + id: 'openai', + providerName: 'OpenAI', + providerType: 'openai', sorted: 1, }, })); diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index 580844a5b..faa91ab3a 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -1,28 +1,30 @@ -"use client"; -import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant"; -import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; - -import { +'use client'; +import type { ChatOptions, - getHeaders, LLMApi, LLMModel, MultimodalContent, SpeechOptions, -} from "../api"; -import Locale from "../../locales"; +} from '../api'; +import { getClientConfig } from '@/app/config/client'; + +import { ApiPath, REQUEST_TIMEOUT_MS, TENCENT_BASE_URL } from '@/app/constant'; +import { useAccessStore, useAppConfig, useChatStore } from '@/app/store'; +import { getMessageTextContent, isVisionModel } from '@/app/utils'; +import { prettyObject } from '@/app/utils/format'; +import { fetch } from '@/app/utils/stream'; import { EventStreamContentType, fetchEventSource, -} from "@fortaine/fetch-event-source"; -import { prettyObject } from "@/app/utils/format"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent, isVisionModel } from "@/app/utils"; -import mapKeys from "lodash-es/mapKeys"; -import mapValues from "lodash-es/mapValues"; -import isArray from "lodash-es/isArray"; -import isObject from "lodash-es/isObject"; -import { fetch } from "@/app/utils/stream"; +} from '@fortaine/fetch-event-source'; +import isArray from 'lodash-es/isArray'; +import isObject from 'lodash-es/isObject'; +import mapKeys from 'lodash-es/mapKeys'; +import mapValues from 'lodash-es/mapValues'; +import Locale from '../../locales'; +import { + getHeaders, +} from '../api'; export interface OpenAIListModelResponse { object: string; @@ -35,7 +37,7 @@ export interface OpenAIListModelResponse { interface RequestPayload { Messages: { - Role: "system" | "user" | "assistant"; + Role: 'system' | 'user' | 'assistant'; Content: string | MultimodalContent[]; }[]; Stream?: boolean; @@ -50,8 +52,7 @@ function capitalizeKeys(obj: any): any { } else if (isObject(obj)) { return mapValues( mapKeys(obj, (value: any, key: string) => - key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()), - ), + key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase())), capitalizeKeys, ); } else { @@ -63,7 +64,7 @@ export class HunyuanApi implements LLMApi { path(): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.tencentUrl; @@ -74,30 +75,30 @@ export class HunyuanApi implements LLMApi { baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.Tencent)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl); + console.log('[Proxy Endpoint] ', baseUrl); return baseUrl; } extractMessage(res: any) { - return res.Choices?.at(0)?.Message?.Content ?? ""; + return res.Choices?.at(0)?.Message?.Content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { const visionModel = isVisionModel(options.config.model); const messages = options.messages.map((v, index) => ({ // "Messages 中 system 角色必须位于列表的最开始" - role: index !== 0 && v.role === "system" ? "user" : v.role, + role: index !== 0 && v.role === 'system' ? 'user' : v.role, content: visionModel ? v.content : getMessageTextContent(v), })); @@ -117,7 +118,7 @@ export class HunyuanApi implements LLMApi { stream: options.config.stream, }); - console.log("[Request] Tencent payload: ", requestPayload); + console.log('[Request] Tencent payload: ', requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); @@ -126,7 +127,7 @@ export class HunyuanApi implements LLMApi { try { const chatPath = this.path(); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -139,8 +140,8 @@ export class HunyuanApi implements LLMApi { ); if (shouldStream) { - let responseText = ""; - let remainText = ""; + let responseText = ''; + let remainText = ''; let finished = false; let responseRes: Response; @@ -148,9 +149,9 @@ export class HunyuanApi implements LLMApi { function animateResponseText() { if (finished || controller.signal.aborted) { responseText += remainText; - console.log("[Response Animation] finished"); + console.log('[Response Animation] finished'); if (responseText?.length === 0) { - options.onError?.(new Error("empty response from server")); + options.onError?.(new Error('empty response from server')); } return; } @@ -183,23 +184,23 @@ export class HunyuanApi implements LLMApi { ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); - const contentType = res.headers.get("content-type"); + const contentType = res.headers.get('content-type'); console.log( - "[Tencent] request response content type: ", + '[Tencent] request response content type: ', contentType, ); responseRes = res; - if (contentType?.startsWith("text/plain")) { + if (contentType?.startsWith('text/plain')) { responseText = await res.clone().text(); return finish(); } if ( - !res.ok || - !res.headers - .get("content-type") - ?.startsWith(EventStreamContentType) || - res.status !== 200 + !res.ok + || !res.headers + .get('content-type') + ?.startsWith(EventStreamContentType) + || res.status !== 200 ) { const responseTexts = [responseText]; let extraInfo = await res.clone().text(); @@ -216,13 +217,13 @@ export class HunyuanApi implements LLMApi { responseTexts.push(extraInfo); } - responseText = responseTexts.join("\n\n"); + responseText = responseTexts.join('\n\n'); return finish(); } }, onmessage(msg) { - if (msg.data === "[DONE]" || finished) { + if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; @@ -236,7 +237,7 @@ export class HunyuanApi implements LLMApi { remainText += delta; } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error('[Request] parse error', text, msg); } }, onclose() { @@ -257,10 +258,11 @@ export class HunyuanApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/client/platforms/xai.ts b/app/client/platforms/xai.ts index 06dbaaa29..20836a70a 100644 --- a/app/client/platforms/xai.ts +++ b/app/client/platforms/xai.ts @@ -1,25 +1,29 @@ -"use client"; +'use client'; +import type { + ChatMessageTool, +} from '@/app/store'; +import type { + ChatOptions, + LLMApi, + LLMModel, + SpeechOptions, +} from '../api'; +import type { RequestPayload } from './openai'; +import { getClientConfig } from '@/app/config/client'; // azure and openai, using same models. so using same LLMApi. -import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { ApiPath, REQUEST_TIMEOUT_MS, XAI, XAI_BASE_URL } from '@/app/constant'; import { useAccessStore, useAppConfig, useChatStore, - ChatMessageTool, usePluginStore, -} from "@/app/store"; -import { stream } from "@/app/utils/chat"; +} from '@/app/store'; +import { getMessageTextContent } from '@/app/utils'; +import { stream } from '@/app/utils/chat'; +import { fetch } from '@/app/utils/stream'; import { - ChatOptions, getHeaders, - LLMApi, - LLMModel, - SpeechOptions, -} from "../api"; -import { getClientConfig } from "@/app/config/client"; -import { getMessageTextContent } from "@/app/utils"; -import { RequestPayload } from "./openai"; -import { fetch } from "@/app/utils/stream"; +} from '../api'; export class XAIApi implements LLMApi { private disableListModels = true; @@ -27,7 +31,7 @@ export class XAIApi implements LLMApi { path(path: string): string { const accessStore = useAccessStore.getState(); - let baseUrl = ""; + let baseUrl = ''; if (accessStore.useCustomConfig) { baseUrl = accessStore.xaiUrl; @@ -39,28 +43,28 @@ export class XAIApi implements LLMApi { baseUrl = isApp ? XAI_BASE_URL : apiPath; } - if (baseUrl.endsWith("/")) { + if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } - if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) { - baseUrl = "https://" + baseUrl; + if (!baseUrl.startsWith('http') && !baseUrl.startsWith(ApiPath.XAI)) { + baseUrl = `https://${baseUrl}`; } - console.log("[Proxy Endpoint] ", baseUrl, path); + console.log('[Proxy Endpoint] ', baseUrl, path); - return [baseUrl, path].join("/"); + return [baseUrl, path].join('/'); } extractMessage(res: any) { - return res.choices?.at(0)?.message?.content ?? ""; + return res.choices?.at(0)?.message?.content ?? ''; } speech(options: SpeechOptions): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } async chat(options: ChatOptions) { - const messages: ChatOptions["messages"] = []; + const messages: ChatOptions['messages'] = []; for (const v of options.messages) { const content = getMessageTextContent(v); messages.push({ role: v.role, content }); @@ -85,7 +89,7 @@ export class XAIApi implements LLMApi { top_p: modelConfig.top_p, }; - console.log("[Request] xai payload: ", requestPayload); + console.log('[Request] xai payload: ', requestPayload); const shouldStream = !!options.config.stream; const controller = new AbortController(); @@ -94,7 +98,7 @@ export class XAIApi implements LLMApi { try { const chatPath = this.path(XAI.ChatPath); const chatPayload = { - method: "POST", + method: 'POST', body: JSON.stringify(requestPayload), signal: controller.signal, headers: getHeaders(), @@ -145,7 +149,7 @@ export class XAIApi implements LLMApi { }); } else { // @ts-ignore - runTools[index]["function"]["arguments"] += args; + runTools[index].function.arguments += args; } } return choices[0]?.delta?.content; @@ -176,10 +180,11 @@ export class XAIApi implements LLMApi { options.onFinish(message, res); } } catch (e) { - console.log("[Request] failed to make a chat request", e); + console.log('[Request] failed to make a chat request', e); options.onError?.(e as Error); } } + async usage() { return { used: 0, diff --git a/app/command.ts b/app/command.ts index aec73ef53..663278a7b 100644 --- a/app/command.ts +++ b/app/command.ts @@ -1,6 +1,6 @@ -import { useEffect } from "react"; -import { useSearchParams } from "react-router-dom"; -import Locale from "./locales"; +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import Locale from './locales'; type Command = (param: string) => void; interface Commands { @@ -18,7 +18,7 @@ export function useCommand(commands: Commands = {}) { let shouldUpdate = false; searchParams.forEach((param, name) => { const commandName = name as keyof Commands; - if (typeof commands[commandName] === "function") { + if (typeof commands[commandName] === 'function') { commands[commandName]!(param); searchParams.delete(name); shouldUpdate = true; @@ -58,16 +58,16 @@ export function useChatCommand(commands: ChatCommands = {}) { const input = extract(userInput); const desc = Locale.Chat.Commands; return Object.keys(commands) - .filter((c) => c.startsWith(input)) - .map((c) => ({ + .filter(c => c.startsWith(input)) + .map(c => ({ title: desc[c as keyof ChatCommands], - content: ":" + c, + content: `:${c}`, })); } function match(userInput: string) { const command = extract(userInput); - const matched = typeof commands[command] === "function"; + const matched = typeof commands[command] === 'function'; return { matched, diff --git a/app/components/artifacts.tsx b/app/components/artifacts.tsx index ce187fbcb..87cdacc50 100644 --- a/app/components/artifacts.tsx +++ b/app/components/artifacts.tsx @@ -1,44 +1,44 @@ +import { ApiPath, Path, REPO_URL } from '@/app/constant'; +import { nanoid } from 'nanoid'; import { - useEffect, - useState, - useRef, - useMemo, forwardRef, + useEffect, useImperativeHandle, -} from "react"; -import { useParams } from "react-router"; -import { IconButton } from "./button"; -import { nanoid } from "nanoid"; -import ExportIcon from "../icons/share.svg"; -import CopyIcon from "../icons/copy.svg"; -import DownloadIcon from "../icons/download.svg"; -import GithubIcon from "../icons/github.svg"; -import LoadingButtonIcon from "../icons/loading.svg"; -import ReloadButtonIcon from "../icons/reload.svg"; -import Locale from "../locales"; -import { Modal, showToast } from "./ui-lib"; -import { copyToClipboard, downloadAs } from "../utils"; -import { Path, ApiPath, REPO_URL } from "@/app/constant"; -import { Loading } from "./home"; -import styles from "./artifacts.module.scss"; + useMemo, + useRef, + useState, +} from 'react'; +import { useParams } from 'react-router'; +import CopyIcon from '../icons/copy.svg'; +import DownloadIcon from '../icons/download.svg'; +import GithubIcon from '../icons/github.svg'; +import LoadingButtonIcon from '../icons/loading.svg'; +import ReloadButtonIcon from '../icons/reload.svg'; +import ExportIcon from '../icons/share.svg'; +import Locale from '../locales'; +import { copyToClipboard, downloadAs } from '../utils'; +import styles from './artifacts.module.scss'; +import { IconButton } from './button'; +import { Loading } from './home'; +import { Modal, showToast } from './ui-lib'; -type HTMLPreviewProps = { +interface HTMLPreviewProps { code: string; autoHeight?: boolean; height?: number | string; onLoad?: (title?: string) => void; -}; +} -export type HTMLPreviewHander = { +export interface HTMLPreviewHander { reload: () => void; -}; +} export const HTMLPreview = forwardRef( - function HTMLPreview(props, ref) { + (props, ref) => { const iframeRef = useRef(null); const [frameId, setFrameId] = useState(nanoid()); const [iframeHeight, setIframeHeight] = useState(600); - const [title, setTitle] = useState(""); + const [title, setTitle] = useState(''); /* * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an * 1. using srcdoc @@ -55,9 +55,9 @@ export const HTMLPreview = forwardRef( setIframeHeight(height); } }; - window.addEventListener("message", handleMessage); + window.addEventListener('message', handleMessage); return () => { - window.removeEventListener("message", handleMessage); + window.removeEventListener('message', handleMessage); }; }, [frameId]); @@ -68,8 +68,9 @@ export const HTMLPreview = forwardRef( })); const height = useMemo(() => { - if (!props.autoHeight) return props.height || 600; - if (typeof props.height === "string") { + if (!props.autoHeight) + { return props.height || 600; } + if (typeof props.height === 'string') { return props.height; } const parentHeight = props.height || 600; @@ -80,8 +81,8 @@ export const HTMLPreview = forwardRef( const srcDoc = useMemo(() => { const script = ``; - if (props.code.includes("")) { - props.code.replace("", "" + script); + if (props.code.includes('')) { + props.code.replace('', `${script}`); } return script + props.code; }, [props.code, frameId]); @@ -94,7 +95,7 @@ export const HTMLPreview = forwardRef( return (