maltose1 commited on
Commit
853f6aa
·
verified ·
1 Parent(s): 67e2cad

Upload 365 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.babelrc ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "presets": [
3
+ [
4
+ "next/babel",
5
+ {
6
+ "preset-env": {
7
+ "targets": {
8
+ "browsers": ["> 0.25%, not dead"]
9
+ }
10
+ }
11
+ }
12
+ ]
13
+ ]
14
+ }
.dockerignore ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+
8
+ # Runtime data
9
+ pids
10
+ *.pid
11
+ *.seed
12
+ *.pid.lock
13
+
14
+ # Directory for instrumented libs generated by jscoverage/JSCover
15
+ lib-cov
16
+
17
+ # Coverage directory used by tools like istanbul
18
+ coverage
19
+ *.lcov
20
+
21
+ # nyc test coverage
22
+ .nyc_output
23
+
24
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25
+ .grunt
26
+
27
+ # Node.js dependencies
28
+ /node_modules
29
+ /jspm_packages
30
+
31
+ # TypeScript v1 declaration files
32
+ typings
33
+
34
+ # Optional npm cache directory
35
+ .npm
36
+
37
+ # Optional eslint cache
38
+ .eslintcache
39
+
40
+ # Optional REPL history
41
+ .node_repl_history
42
+
43
+ # Output of 'npm pack'
44
+ *.tgz
45
+
46
+ # Yarn Integrity file
47
+ .yarn-integrity
48
+
49
+ # dotenv environment variable files
50
+ .env
51
+ .env.test
52
+
53
+ # local env files
54
+ .env*.local
55
+
56
+ # Next.js build output
57
+ .next
58
+ out
59
+
60
+ # Nuxt.js build output
61
+ .nuxt
62
+ dist
63
+
64
+ # Gatsby files
65
+ .cache/
66
+
67
+
68
+ # Vuepress build output
69
+ .vuepress/dist
70
+
71
+ # Serverless directories
72
+ .serverless/
73
+
74
+ # FuseBox cache
75
+ .fusebox/
76
+
77
+ # DynamoDB Local files
78
+ .dynamodb/
79
+
80
+ # Temporary folders
81
+ tmp
82
+ temp
83
+
84
+ # IDE and editor directories
85
+ .idea
86
+ .vscode
87
+ *.swp
88
+ *.swo
89
+ *~
90
+
91
+ # OS generated files
92
+ .DS_Store
93
+ Thumbs.db
94
+
95
+ # secret key
96
+ *.key
97
+ *.key.pub
.env.template ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Your openai api key. (required)
2
+ OPENAI_API_KEY=sk-xxxx
3
+
4
+ # DeepSeek Api Key. (Optional)
5
+ DEEPSEEK_API_KEY=
6
+
7
+ # Access password, separated by comma. (optional)
8
+ CODE=your-password
9
+
10
+ # You can start service behind a proxy. (optional)
11
+ PROXY_URL=http://localhost:7890
12
+
13
+ # Enable MCP functionality (optional)
14
+ # Default: Empty (disabled)
15
+ # Set to "true" to enable MCP functionality
16
+ ENABLE_MCP=
17
+
18
+ # (optional)
19
+ # Default: Empty
20
+ # Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
21
+ GOOGLE_API_KEY=
22
+
23
+ # (optional)
24
+ # Default: https://generativelanguage.googleapis.com/
25
+ # Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
26
+ GOOGLE_URL=
27
+
28
+ # Override openai api request base url. (optional)
29
+ # Default: https://api.openai.com
30
+ # Examples: http://your-openai-proxy.com
31
+ BASE_URL=
32
+
33
+ # Specify OpenAI organization ID.(optional)
34
+ # Default: Empty
35
+ OPENAI_ORG_ID=
36
+
37
+ # (optional)
38
+ # Default: Empty
39
+ # If you do not want users to use GPT-4, set this value to 1.
40
+ DISABLE_GPT4=
41
+
42
+ # (optional)
43
+ # Default: Empty
44
+ # If you do not want users to input their own API key, set this value to 1.
45
+ HIDE_USER_API_KEY=
46
+
47
+ # (optional)
48
+ # Default: Empty
49
+ # If you do want users to query balance, set this value to 1.
50
+ ENABLE_BALANCE_QUERY=
51
+
52
+ # (optional)
53
+ # Default: Empty
54
+ # If you want to disable parse settings from url, set this value to 1.
55
+ DISABLE_FAST_LINK=
56
+
57
+ # (optional)
58
+ # Default: Empty
59
+ # To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma.
60
+ CUSTOM_MODELS=
61
+
62
+ # (optional)
63
+ # Default: Empty
64
+ # Change default model
65
+ DEFAULT_MODEL=
66
+
67
+ # anthropic claude Api Key.(optional)
68
+ ANTHROPIC_API_KEY=
69
+
70
+ ### anthropic claude Api version. (optional)
71
+ ANTHROPIC_API_VERSION=
72
+
73
+ ### anthropic claude Api url (optional)
74
+ ANTHROPIC_URL=
75
+
76
+ ### (optional)
77
+ WHITE_WEBDAV_ENDPOINTS=
78
+
79
+ ### siliconflow Api key (optional)
80
+ SILICONFLOW_API_KEY=
81
+
82
+ ### siliconflow Api url (optional)
83
+ SILICONFLOW_URL=
.eslintignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ public/serviceWorker.js
2
+ app/mcp/mcp_config.json
3
+ app/mcp/mcp_config.default.json
.eslintrc.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals",
3
+ "plugins": ["prettier", "unused-imports"],
4
+ "rules": {
5
+ "unused-imports/no-unused-imports": "warn"
6
+ }
7
+ }
.gitattributes CHANGED
@@ -1,35 +1,49 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ docs/images/bt/bt-install-1.jpeg filter=lfs diff=lfs merge=lfs -text
37
+ docs/images/bt/bt-install-2.jpeg filter=lfs diff=lfs merge=lfs -text
38
+ docs/images/bt/bt-install-3.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ docs/images/bt/bt-install-4.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ docs/images/bt/bt-install-6.jpeg filter=lfs diff=lfs merge=lfs -text
41
+ docs/images/cover.png filter=lfs diff=lfs merge=lfs -text
42
+ docs/images/enable-actions-sync.jpg filter=lfs diff=lfs merge=lfs -text
43
+ docs/images/enable-actions.jpg filter=lfs diff=lfs merge=lfs -text
44
+ docs/images/head-cover.png filter=lfs diff=lfs merge=lfs -text
45
+ docs/images/vercel/vercel-create-2.jpg filter=lfs diff=lfs merge=lfs -text
46
+ docs/images/vercel/vercel-create-3.jpg filter=lfs diff=lfs merge=lfs -text
47
+ docs/images/vercel/vercel-env-edit.jpg filter=lfs diff=lfs merge=lfs -text
48
+ docs/images/vercel/vercel-redeploy.jpg filter=lfs diff=lfs merge=lfs -text
49
+ src-tauri/icons/icon.icns filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+ .pnpm-debug.log*
27
+
28
+ # local env files
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
37
+ dev
38
+
39
+ .vscode
40
+ .idea
41
+
42
+ # docker-compose env files
43
+ .env
44
+
45
+ *.key
46
+ *.key.pub
47
+
48
+ masks.json
49
+
50
+ # mcp config
51
+ app/mcp/mcp_config.json
.gitpod.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This configuration file was automatically generated by Gitpod.
2
+ # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
3
+ # and commit this file to your remote git repository to share the goodness with others.
4
+
5
+ # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
6
+
7
+ tasks:
8
+ - init: yarn install && yarn run dev
9
+ command: yarn run dev
10
+
11
+
.husky/pre-commit ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
.lintstagedrc.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
3
+ "eslint --fix",
4
+ "prettier --write"
5
+ ]
6
+ }
.prettierrc.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ printWidth: 80,
3
+ tabWidth: 2,
4
+ useTabs: false,
5
+ semi: true,
6
+ singleQuote: false,
7
+ trailingComma: 'all',
8
+ bracketSpacing: true,
9
+ arrowParens: 'always',
10
+ };
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
Dockerfile ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine AS base
2
+
3
+ FROM base AS deps
4
+
5
+ RUN apk add --no-cache libc6-compat
6
+
7
+ WORKDIR /app
8
+
9
+ COPY package.json yarn.lock ./
10
+
11
+ RUN yarn config set registry 'https://registry.npmmirror.com/'
12
+ RUN yarn install
13
+
14
+ FROM base AS builder
15
+
16
+ RUN apk update && apk add --no-cache git
17
+
18
+ ENV OPENAI_API_KEY=""
19
+ ENV GOOGLE_API_KEY=""
20
+ ENV CODE=""
21
+
22
+ WORKDIR /app
23
+ COPY --from=deps /app/node_modules ./node_modules
24
+ COPY . .
25
+
26
+ RUN yarn build
27
+
28
+ FROM base AS runner
29
+ WORKDIR /app
30
+
31
+ RUN apk add proxychains-ng
32
+
33
+ ENV PROXY_URL=""
34
+ ENV OPENAI_API_KEY=""
35
+ ENV GOOGLE_API_KEY=""
36
+ ENV CODE=""
37
+ ENV ENABLE_MCP=""
38
+
39
+ COPY --from=builder /app/public ./public
40
+ COPY --from=builder /app/.next/standalone ./
41
+ COPY --from=builder /app/.next/static ./.next/static
42
+ COPY --from=builder /app/.next/server ./.next/server
43
+
44
+ RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
45
+ COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json
46
+
47
+ EXPOSE 3000
48
+
49
+ CMD if [ -n "$PROXY_URL" ]; then \
50
+ export HOSTNAME="0.0.0.0"; \
51
+ protocol=$(echo $PROXY_URL | cut -d: -f1); \
52
+ host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
53
+ port=$(echo $PROXY_URL | cut -d: -f3); \
54
+ conf=/etc/proxychains.conf; \
55
+ echo "strict_chain" > $conf; \
56
+ echo "proxy_dns" >> $conf; \
57
+ echo "remote_dns_subnet 224" >> $conf; \
58
+ echo "tcp_read_time_out 15000" >> $conf; \
59
+ echo "tcp_connect_time_out 8000" >> $conf; \
60
+ echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
61
+ echo "localnet ::1/128" >> $conf; \
62
+ echo "[ProxyList]" >> $conf; \
63
+ echo "$protocol $host $port" >> $conf; \
64
+ cat /etc/proxychains.conf; \
65
+ proxychains -f $conf node server.js; \
66
+ else \
67
+ node server.js; \
68
+ fi
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2025 NextChat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,11 +1,520 @@
1
- ---
2
- title: Work House
3
- emoji: 👁
4
- colorFrom: yellow
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: NextChat
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ sdk_version: "3.0.0"
8
+ app_file: app.py
9
+ app_port: 3000
10
+ pinned: false
11
+ ---
12
+
13
+ # NextChat
14
+
15
+ NextChat 是一个强大的聊天应用,支持多种 AI 模型和功能。
16
+
17
+ ## 功能特点
18
+
19
+ - 支持多种 AI 模型(OpenAI、Azure、Anthropic、Google 等)
20
+ - 实时对话功能
21
+ - 文本转语音(TTS)
22
+ - 插件系统
23
+ - 提示词模板
24
+ - 多语言支持
25
+ - 数据同步
26
+ - 自定义主题
27
+
28
+ ## 部署说明
29
+
30
+ 1. 克隆仓库
31
+ 2. 配置环境变量
32
+ 3. 运行应用
33
+
34
+ ## 环境变量配置
35
+
36
+ 主要的环境变量包括:
37
+
38
+ - `OPENAI_API_KEY`: OpenAI API 密钥
39
+ - `AZURE_API_KEY`: Azure API 密钥
40
+ - `ANTHROPIC_API_KEY`: Anthropic API 密钥
41
+ - `GOOGLE_API_KEY`: Google API 密钥
42
+
43
+ ## 许可证
44
+
45
+ MIT License
46
+
47
+ <div align="center">
48
+
49
+ <a href='https://nextchat.club'>
50
+ <img src="https://github.com/user-attachments/assets/83bdcc07-ae5e-4954-a53a-ac151ba6ccf3" width="1000" alt="icon"/>
51
+ </a>
52
+
53
+
54
+
55
+
56
+ <h1 align="center">NextChat (ChatGPT Next Web)</h1>
57
+
58
+ English / [简体中文](./README_CN.md)
59
+
60
+ <a href="https://trendshift.io/repositories/5973" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5973" alt="ChatGPTNextWeb%2FChatGPT-Next-Web | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
61
+
62
+
63
+ ✨ Light and Fast AI Assistant,with Claude, DeepSeek, GPT4 & Gemini Pro support.
64
+
65
+ [![Saas][Saas-image]][saas-url]
66
+ [![Web][Web-image]][web-url]
67
+ [![Windows][Windows-image]][download-url]
68
+ [![MacOS][MacOS-image]][download-url]
69
+ [![Linux][Linux-image]][download-url]
70
+
71
+ [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
72
+ [NextChatAI](https://nextchat.club?utm_source=readme) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
73
+
74
+
75
+ [saas-url]: https://nextchat.club?utm_source=readme
76
+ [saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
77
+ [web-url]: https://app.nextchat.dev/
78
+ [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
79
+ [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
80
+ [Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
81
+ [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
82
+ [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
83
+
84
+ [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://vercel.com/button" alt="Deploy on Vercel" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/ChatGPTNextWeb/NextChat)
85
+
86
+ [<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="50" width="" >](https://monica.im/?utm=nxcrp)
87
+
88
+ </div>
89
+
90
+ ## 🥳 Cheer for DeepSeek, China's AI star!
91
+ > Purpose-Built UI for DeepSeek Reasoner Model
92
+
93
+ <img src="https://github.com/user-attachments/assets/f3952210-3af1-4dc0-9b81-40eaa4847d9a"/>
94
+
95
+
96
+
97
+ ## 🫣 NextChat Support MCP !
98
+ > Before build, please set env ENABLE_MCP=true
99
+
100
+ <img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
101
+
102
+
103
+ ## Enterprise Edition
104
+
105
+ Meeting Your Company's Privatization and Customization Deployment Requirements:
106
+ - **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
107
+ - **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
108
+ - **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
109
+ - **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
110
+ - **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
111
+ - **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
112
+ - **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
113
+
114
+ For enterprise inquiries, please contact: **[email protected]**
115
+
116
+ ## Screenshots
117
+
118
+ ![Settings](./docs/images/settings.png)
119
+
120
+ ![More](./docs/images/more.png)
121
+
122
+
123
+ ## Features
124
+
125
+ - **Deploy for free with one-click** on Vercel in under 1 minute
126
+ - Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
127
+ - Fully compatible with self-deployed LLMs, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
128
+ - Privacy first, all data is stored locally in the browser
129
+ - Markdown support: LaTex, mermaid, code highlight, etc.
130
+ - Responsive design, dark mode and PWA
131
+ - Fast first screen loading speed (~100kb), support streaming response
132
+ - New in v2: create, share and debug your chat tools with prompt templates (mask)
133
+ - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
134
+ - Automatically compresses chat history to support long conversations while also saving your tokens
135
+ - I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
136
+
137
+ <div align="center">
138
+
139
+ ![主界面](./docs/images/cover.png)
140
+
141
+ </div>
142
+
143
+ ## Roadmap
144
+
145
+ - [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
146
+ - [x] User Prompt: user can edit and save custom prompts to prompt list
147
+ - [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
148
+ - [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
149
+ - [x] Desktop App with tauri
150
+ - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
151
+ - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
152
+ - [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
153
+ - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
154
+ - [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
155
+ - [ ] local knowledge base
156
+
157
+ ## What's New
158
+ - 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
159
+ - 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
160
+ - 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
161
+ - 🚀 v2.14.0 Now supports Artifacts & SD
162
+ - 🚀 v2.10.1 support Google Gemini Pro model.
163
+ - 🚀 v2.9.11 you can use azure endpoint now.
164
+ - 🚀 v2.8 now we have a client that runs across all platforms!
165
+ - 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
166
+ - 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
167
+
168
+ ## Get Started
169
+
170
+ 1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
171
+ 2. Click
172
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), remember that `CODE` is your page password;
173
+ 3. Enjoy :)
174
+
175
+ ## FAQ
176
+
177
+ [English > FAQ](./docs/faq-en.md)
178
+
179
+ ## Keep Updated
180
+
181
+ If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
182
+
183
+ We recommend that you follow the steps below to re-deploy:
184
+
185
+ - Delete the original repository;
186
+ - Use the fork button in the upper right corner of the page to fork this project;
187
+ - Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md).
188
+
189
+ ### Enable Automatic Updates
190
+
191
+ > If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
192
+
193
+ After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
194
+
195
+ ![Automatic Updates](./docs/images/enable-actions.jpg)
196
+
197
+ ![Enable Automatic Updates](./docs/images/enable-actions-sync.jpg)
198
+
199
+ ### Manually Updating Code
200
+
201
+ If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
202
+
203
+ You can star or watch this project or follow author to get release notifications in time.
204
+
205
+ ## Access Password
206
+
207
+ This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
208
+
209
+ ```
210
+ code1,code2,code3
211
+ ```
212
+
213
+ After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
214
+
215
+ ## Environment Variables
216
+
217
+ ### `CODE` (optional)
218
+
219
+ Access password, separated by comma.
220
+
221
+ ### `OPENAI_API_KEY` (required)
222
+
223
+ Your openai api key, join multiple api keys with comma.
224
+
225
+ ### `BASE_URL` (optional)
226
+
227
+ > Default: `https://api.openai.com`
228
+
229
+ > Examples: `http://your-openai-proxy.com`
230
+
231
+ Override openai api request base url.
232
+
233
+ ### `OPENAI_ORG_ID` (optional)
234
+
235
+ Specify OpenAI organization ID.
236
+
237
+ ### `AZURE_URL` (optional)
238
+
239
+ > Example: https://{azure-resource-url}/openai
240
+
241
+ Azure deploy url.
242
+
243
+ ### `AZURE_API_KEY` (optional)
244
+
245
+ Azure Api Key.
246
+
247
+ ### `AZURE_API_VERSION` (optional)
248
+
249
+ Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
250
+
251
+ ### `GOOGLE_API_KEY` (optional)
252
+
253
+ Google Gemini Pro Api Key.
254
+
255
+ ### `GOOGLE_URL` (optional)
256
+
257
+ Google Gemini Pro Api Url.
258
+
259
+ ### `ANTHROPIC_API_KEY` (optional)
260
+
261
+ anthropic claude Api Key.
262
+
263
+ ### `ANTHROPIC_API_VERSION` (optional)
264
+
265
+ anthropic claude Api version.
266
+
267
+ ### `ANTHROPIC_URL` (optional)
268
+
269
+ anthropic claude Api Url.
270
+
271
+ ### `BAIDU_API_KEY` (optional)
272
+
273
+ Baidu Api Key.
274
+
275
+ ### `BAIDU_SECRET_KEY` (optional)
276
+
277
+ Baidu Secret Key.
278
+
279
+ ### `BAIDU_URL` (optional)
280
+
281
+ Baidu Api Url.
282
+
283
+ ### `BYTEDANCE_API_KEY` (optional)
284
+
285
+ ByteDance Api Key.
286
+
287
+ ### `BYTEDANCE_URL` (optional)
288
+
289
+ ByteDance Api Url.
290
+
291
+ ### `ALIBABA_API_KEY` (optional)
292
+
293
+ Alibaba Cloud Api Key.
294
+
295
+ ### `ALIBABA_URL` (optional)
296
+
297
+ Alibaba Cloud Api Url.
298
+
299
+ ### `IFLYTEK_URL` (Optional)
300
+
301
+ iflytek Api Url.
302
+
303
+ ### `IFLYTEK_API_KEY` (Optional)
304
+
305
+ iflytek Api Key.
306
+
307
+ ### `IFLYTEK_API_SECRET` (Optional)
308
+
309
+ iflytek Api Secret.
310
+
311
+ ### `CHATGLM_API_KEY` (optional)
312
+
313
+ ChatGLM Api Key.
314
+
315
+ ### `CHATGLM_URL` (optional)
316
+
317
+ ChatGLM Api Url.
318
+
319
+ ### `DEEPSEEK_API_KEY` (optional)
320
+
321
+ DeepSeek Api Key.
322
+
323
+ ### `DEEPSEEK_URL` (optional)
324
+
325
+ DeepSeek Api Url.
326
+
327
+ ### `HIDE_USER_API_KEY` (optional)
328
+
329
+ > Default: Empty
330
+
331
+ If you do not want users to input their own API key, set this value to 1.
332
+
333
+ ### `DISABLE_GPT4` (optional)
334
+
335
+ > Default: Empty
336
+
337
+ If you do not want users to use GPT-4, set this value to 1.
338
+
339
+ ### `ENABLE_BALANCE_QUERY` (optional)
340
+
341
+ > Default: Empty
342
+
343
+ If you do want users to query balance, set this value to 1.
344
+
345
+ ### `DISABLE_FAST_LINK` (optional)
346
+
347
+ > Default: Empty
348
+
349
+ If you want to disable parse settings from url, set this to 1.
350
+
351
+ ### `CUSTOM_MODELS` (optional)
352
+
353
+ > Default: Empty
354
+ > Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
355
+
356
+ To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name=displayName` to customize model name, separated by comma.
357
+
358
+ User `-all` to disable all default models, `+all` to enable all default models.
359
+
360
+ For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
361
+ > Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
362
+ > If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
363
+
364
+ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
365
+ > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
366
+
367
+ ### `DEFAULT_MODEL` (optional)
368
+
369
+ Change default model
370
+
371
+ ### `VISION_MODELS` (optional)
372
+
373
+ > Default: Empty
374
+ > Example: `gpt-4-vision,claude-3-opus,my-custom-model` means add vision capabilities to these models in addition to the default pattern matches (which detect models containing keywords like "vision", "claude-3", "gemini-1.5", etc).
375
+
376
+ Add additional models to have vision capabilities, beyond the default pattern matching. Multiple models should be separated by commas.
377
+
378
+ ### `WHITE_WEBDAV_ENDPOINTS` (optional)
379
+
380
+ You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
381
+ - Each address must be a complete endpoint
382
+ > `https://xxxx/yyy`
383
+ - Multiple addresses are connected by ', '
384
+
385
+ ### `DEFAULT_INPUT_TEMPLATE` (optional)
386
+
387
+ Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
388
+
389
+ ### `STABILITY_API_KEY` (optional)
390
+
391
+ Stability API key.
392
+
393
+ ### `STABILITY_URL` (optional)
394
+
395
+ Customize Stability API url.
396
+
397
+
398
+ ### `ENABLE_MCP` (optional)
399
+
400
+ Enable MCP(Model Context Protocol)Feature
401
+
402
+ ### `SILICONFLOW_API_KEY` (optional)
403
+
404
+ SiliconFlow API Key.
405
+
406
+ ### `SILICONFLOW_URL` (optional)
407
+
408
+ SiliconFlow API URL.
409
+
410
+ ## Requirements
411
+
412
+ NodeJS >= 18, Docker >= 20
413
+
414
+ ## Development
415
+
416
+
417
+ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
418
+
419
+ Before starting development, you must create a new `.env.local` file at project root, and place your api key into it:
420
+
421
+ ```
422
+ OPENAI_API_KEY=<your api key here>
423
+
424
+ # if you are not able to access openai service, use this BASE_URL
425
+ BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
426
+ ```
427
+
428
+ ### Local Development
429
+
430
+ ```shell
431
+ # 1. install nodejs and yarn first
432
+ # 2. config local env vars in `.env.local`
433
+ # 3. run
434
+ yarn install
435
+ yarn dev
436
+ ```
437
+
438
+ ## Deployment
439
+
440
+
441
+ ### Docker (Recommended)
442
+
443
+ ```shell
444
+ docker pull yidadaa/chatgpt-next-web
445
+
446
+ docker run -d -p 3000:3000 \
447
+ -e OPENAI_API_KEY=sk-xxxx \
448
+ -e CODE=your-password \
449
+ yidadaa/chatgpt-next-web
450
+ ```
451
+
452
+ You can start service behind a proxy:
453
+
454
+ ```shell
455
+ docker run -d -p 3000:3000 \
456
+ -e OPENAI_API_KEY=sk-xxxx \
457
+ -e CODE=your-password \
458
+ -e PROXY_URL=http://localhost:7890 \
459
+ yidadaa/chatgpt-next-web
460
+ ```
461
+
462
+ If your proxy needs password, use:
463
+
464
+ ```shell
465
+ -e PROXY_URL="http://127.0.0.1:7890 user pass"
466
+ ```
467
+
468
+ If enable MCP, use:
469
+
470
+ ```
471
+ docker run -d -p 3000:3000 \
472
+ -e OPENAI_API_KEY=sk-xxxx \
473
+ -e CODE=your-password \
474
+ -e ENABLE_MCP=true \
475
+ yidadaa/chatgpt-next-web
476
+ ```
477
+
478
+ ### Shell
479
+
480
+ ```shell
481
+ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
482
+ ```
483
+
484
+ ## Synchronizing Chat Records (UpStash)
485
+
486
+ | [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
487
+
488
+ ## Documentation
489
+
490
+ > Please go to the [docs][./docs] directory for more documentation instructions.
491
+
492
+ - [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md)
493
+ - [Frequent Ask Questions](./docs/faq-en.md)
494
+ - [How to add a new translation](./docs/translation.md)
495
+ - [How to use Vercel (No English)](./docs/vercel-cn.md)
496
+ - [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
497
+
498
+
499
+
500
+ ## Translation
501
+
502
+ If you want to add a new translation, read this [document](./docs/translation.md).
503
+
504
+ ## Donation
505
+
506
+ [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
507
+
508
+ ## Special Thanks
509
+
510
+
511
+
512
+ ### Contributors
513
+
514
+ <a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">
515
+ <img src="https://contrib.rocks/image?repo=ChatGPTNextWeb/ChatGPT-Next-Web" />
516
+ </a>
517
+
518
+ ## LICENSE
519
+
520
+ [MIT](https://opensource.org/license/mit/)
README_CN.md ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ <a href='#企业版'>
4
+ <img src="./docs/images/ent.svg" alt="icon"/>
5
+ </a>
6
+
7
+ <h1 align="center">NextChat</h1>
8
+
9
+ 一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。
10
+
11
+ [NextChatAI](https://nextchat.club?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
12
+
13
+ [<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
14
+
15
+ </div>
16
+
17
+ ## 企业版
18
+
19
+ 满足您公司私有化部署和定制需求
20
+ - **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
21
+ - **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
22
+ - **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
23
+ - **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
24
+ - **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
25
+ - **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
26
+ - **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
27
+
28
+ 企业版咨询: **[email protected]**
29
+
30
+ <img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
31
+
32
+
33
+ ## 开始使用
34
+
35
+ 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
36
+ 2. 点击右侧按钮开始部署:
37
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
38
+ 3. 部署完毕后,即可开始使用;
39
+ 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
40
+
41
+ <div align="center">
42
+
43
+ ![主界面](./docs/images/cover.png)
44
+
45
+ </div>
46
+
47
+ ## 保持更新
48
+
49
+ 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
50
+ 推荐你按照下列步骤重新部署:
51
+
52
+ - 删除掉原先的仓库;
53
+ - 使用页面右上角的 fork 按钮,fork 本项目;
54
+ - 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
55
+
56
+ ### 打开自动更新
57
+
58
+ > 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)!
59
+
60
+ 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
61
+
62
+ ![自动更新](./docs/images/enable-actions.jpg)
63
+
64
+ ![启用自动更新](./docs/images/enable-actions-sync.jpg)
65
+
66
+ ### 手动更新代码
67
+
68
+ 如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
69
+
70
+ 你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
71
+
72
+ ## 配置页面访问密码
73
+
74
+ > 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。
75
+
76
+ > **警告**:请务必将密码的位数设置得足够长,最好 7 位以上,否则[会被爆破](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
77
+
78
+ 本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码:
79
+
80
+ ```
81
+ code1,code2,code3
82
+ ```
83
+
84
+ 增加或修改该环境变量后,请**重新部署**项目使改动生效。
85
+
86
+ ## 环境变量
87
+
88
+ > 本项目大多数配置项都通过环境变量来设置,教程:[如何���改 Vercel 环境变量](./docs/vercel-cn.md)。
89
+
90
+ ### `OPENAI_API_KEY` (必填项)
91
+
92
+ OpenAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
93
+
94
+ ### `CODE` (可选)
95
+
96
+ 访问密码,可选,可以使用逗号隔开多个密码。
97
+
98
+ **警告**:如果不填写此项,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。
99
+
100
+ ### `BASE_URL` (可选)
101
+
102
+ > Default: `https://api.openai.com`
103
+
104
+ > Examples: `http://your-openai-proxy.com`
105
+
106
+ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。
107
+
108
+ > 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
109
+
110
+ ### `OPENAI_ORG_ID` (可选)
111
+
112
+ 指定 OpenAI 中的组织 ID。
113
+
114
+ ### `AZURE_URL` (可选)
115
+
116
+ > 形如:https://{azure-resource-url}/openai
117
+
118
+ Azure 部署地址。
119
+
120
+ ### `AZURE_API_KEY` (可选)
121
+
122
+ Azure 密钥。
123
+
124
+ ### `AZURE_API_VERSION` (可选)
125
+
126
+ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
127
+
128
+ ### `GOOGLE_API_KEY` (可选)
129
+
130
+ Google Gemini Pro 密钥.
131
+
132
+ ### `GOOGLE_URL` (可选)
133
+
134
+ Google Gemini Pro Api Url.
135
+
136
+ ### `ANTHROPIC_API_KEY` (可选)
137
+
138
+ anthropic claude Api Key.
139
+
140
+ ### `ANTHROPIC_API_VERSION` (可选)
141
+
142
+ anthropic claude Api version.
143
+
144
+ ### `ANTHROPIC_URL` (可选)
145
+
146
+ anthropic claude Api Url.
147
+
148
+ ### `BAIDU_API_KEY` (可选)
149
+
150
+ Baidu Api Key.
151
+
152
+ ### `BAIDU_SECRET_KEY` (可选)
153
+
154
+ Baidu Secret Key.
155
+
156
+ ### `BAIDU_URL` (可选)
157
+
158
+ Baidu Api Url.
159
+
160
+ ### `BYTEDANCE_API_KEY` (可选)
161
+
162
+ ByteDance Api Key.
163
+
164
+ ### `BYTEDANCE_URL` (可选)
165
+
166
+ ByteDance Api Url.
167
+
168
+ ### `ALIBABA_API_KEY` (可选)
169
+
170
+ 阿里云(千问)Api Key.
171
+
172
+ ### `ALIBABA_URL` (可选)
173
+
174
+ 阿里云(千问)Api Url.
175
+
176
+ ### `IFLYTEK_URL` (可选)
177
+
178
+ 讯飞星火Api Url.
179
+
180
+ ### `IFLYTEK_API_KEY` (可选)
181
+
182
+ 讯飞星火Api Key.
183
+
184
+ ### `IFLYTEK_API_SECRET` (可选)
185
+
186
+ 讯飞星火Api Secret.
187
+
188
+ ### `CHATGLM_API_KEY` (可选)
189
+
190
+ ChatGLM Api Key.
191
+
192
+ ### `CHATGLM_URL` (可选)
193
+
194
+ ChatGLM Api Url.
195
+
196
+ ### `DEEPSEEK_API_KEY` (可选)
197
+
198
+ DeepSeek Api Key.
199
+
200
+ ### `DEEPSEEK_URL` (可选)
201
+
202
+ DeepSeek Api Url.
203
+
204
+
205
+ ### `HIDE_USER_API_KEY` (可选)
206
+
207
+ 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
208
+
209
+ ### `DISABLE_GPT4` (可选)
210
+
211
+ 如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
212
+
213
+ ### `ENABLE_BALANCE_QUERY` (可选)
214
+
215
+ 如果你想启用余额查询功能,将此环境变量设置为 1 即可。
216
+
217
+ ### `DISABLE_FAST_LINK` (可选)
218
+
219
+ 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
220
+
221
+ ### `WHITE_WEBDAV_ENDPOINTS` (可选)
222
+
223
+ 如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
224
+ - 每一个地址必须是一个完整的 endpoint
225
+ > `https://xxxx/xxx`
226
+ - 多个地址以`,`相连
227
+
228
+ ### `CUSTOM_MODELS` (可选)
229
+
230
+ > 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
231
+ > 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
232
+
233
+ 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
234
+
235
+ 在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
236
+ > 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
237
+ > 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
238
+
239
+ 在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
240
+ > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
241
+
242
+
243
+ ### `DEFAULT_MODEL` (可选)
244
+
245
+ 更改默认模型
246
+
247
+ ### `VISION_MODELS` (可选)
248
+
249
+ > 默认值:空
250
+ > 示例:`gpt-4-vision,claude-3-opus,my-custom-model` 表示为这些模型添加视觉能力,作为对默认模式匹配的补充(默认会检测包含"vision"、"claude-3"、"gemini-1.5"等关键词的模型)。
251
+
252
+ 在默认模式匹配之外,添加更多具有视觉能力的模型。多个模型用逗号分隔。
253
+
254
+ ### `DEFAULT_INPUT_TEMPLATE` (可选)
255
+
256
+ 自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
257
+
258
+ ### `STABILITY_API_KEY` (optional)
259
+
260
+ Stability API密钥
261
+
262
+ ### `STABILITY_URL` (optional)
263
+
264
+ 自定义的Stability API请求地址
265
+
266
+ ### `ENABLE_MCP` (optional)
267
+
268
+ 启用MCP(Model Context Protocol)功能
269
+
270
+ ### `SILICONFLOW_API_KEY` (optional)
271
+
272
+ SiliconFlow API Key.
273
+
274
+ ### `SILICONFLOW_URL` (optional)
275
+
276
+ SiliconFlow API URL.
277
+
278
+ ## 开发
279
+
280
+ 点击下方按钮,开始二次开发:
281
+
282
+ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
283
+
284
+ 在开始写代码之前,需要在项目根目录新建一个 `.env.local` 文件,里面填入环境变量:
285
+
286
+ ```
287
+ OPENAI_API_KEY=<your api key here>
288
+
289
+ # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
290
+ BASE_URL=https://b.nextweb.fun/api/proxy
291
+ ```
292
+
293
+ ### 本地开发
294
+
295
+ 1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
296
+ 2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
297
+ 3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
298
+
299
+ ## 部署
300
+
301
+ ### 宝塔面板部署
302
+ > [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
303
+
304
+ ### 容器部署 (推荐)
305
+
306
+ > Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
307
+
308
+ > ⚠️ 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
309
+
310
+ ```shell
311
+ docker pull yidadaa/chatgpt-next-web
312
+
313
+ docker run -d -p 3000:3000 \
314
+ -e OPENAI_API_KEY=sk-xxxx \
315
+ -e CODE=页面访问密码 \
316
+ yidadaa/chatgpt-next-web
317
+ ```
318
+
319
+ 你也可以指定 proxy:
320
+
321
+ ```shell
322
+ docker run -d -p 3000:3000 \
323
+ -e OPENAI_API_KEY=sk-xxxx \
324
+ -e CODE=页面访问密码 \
325
+ --net=host \
326
+ -e PROXY_URL=http://127.0.0.1:7890 \
327
+ yidadaa/chatgpt-next-web
328
+ ```
329
+
330
+ 如需启用 MCP 功能,可以使用:
331
+
332
+ ```shell
333
+ docker run -d -p 3000:3000 \
334
+ -e OPENAI_API_KEY=sk-xxxx \
335
+ -e CODE=页面访问密码 \
336
+ -e ENABLE_MCP=true \
337
+ yidadaa/chatgpt-next-web
338
+ ```
339
+
340
+ 如果你的本地代理需要账号密码,可以使用:
341
+
342
+ ```shell
343
+ -e PROXY_URL="http://127.0.0.1:7890 user password"
344
+ ```
345
+
346
+ 如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
347
+
348
+ ### 本地部署
349
+
350
+ 在控制台运行下方命令:
351
+
352
+ ```shell
353
+ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
354
+ ```
355
+
356
+ ⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
357
+
358
+ ## 鸣谢
359
+
360
+ ### 捐赠者
361
+
362
+ > 见英文版。
363
+
364
+ ### 贡献者
365
+
366
+ [见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
367
+
368
+ ### 相关项目
369
+
370
+ - [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
371
+
372
+ ## 开源协议
373
+
374
+ [MIT](https://opensource.org/license/mit/)
README_JA.md ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img src="./docs/images/ent.svg" alt="プレビュー"/>
3
+
4
+ <h1 align="center">NextChat</h1>
5
+
6
+ ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
7
+
8
+ [NextChatAI](https://nextchat.club?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
9
+
10
+ [<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
11
+
12
+
13
+ </div>
14
+
15
+ ## 企業版
16
+
17
+ あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
18
+ - **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
19
+ - **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
20
+ - **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
21
+ - **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
22
+ - **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
23
+ - **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
24
+ - **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
25
+
26
+ 企業版のお問い合わせ: **[email protected]**
27
+
28
+
29
+ ## 始めに
30
+
31
+ 1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
32
+ 2. 右側のボタンをクリックしてデプロイを開始:
33
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 、GitHubアカウントで直接ログインし、環境変数ページにAPI Keyと[ページアクセスパスワード](#設定ページアクセスパスワード) CODEを入力してください;
34
+ 3. デプロイが完了したら、すぐに使用を開始できます;
35
+ 4. (オプション)[カスタムドメインをバインド](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercelが割り当てたドメインDNSは一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
36
+
37
+ <div align="center">
38
+
39
+ ![メインインターフェース](./docs/images/cover.png)
40
+
41
+ </div>
42
+
43
+
44
+ ## 更新を維持する
45
+
46
+ もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
47
+
48
+ 以下の手順に従って再デプロイすることをお勧めします:
49
+
50
+ - 元のリポジトリを削除する
51
+ - ページ右上の fork ボタンを使って、本プロジェクトを fork する
52
+ - Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
53
+
54
+
55
+ ### 自動更新を開く
56
+
57
+ > Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
58
+
59
+ プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
60
+
61
+ ![自動更新](./docs/images/enable-actions.jpg)
62
+
63
+ ![自動更新を有効にする](./docs/images/enable-actions-sync.jpg)
64
+
65
+
66
+ ### 手動でコードを更新する
67
+
68
+ 手動で即座に更新したい場��は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
69
+
70
+ このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
71
+
72
+
73
+
74
+ ## ページアクセスパスワードを設定する
75
+
76
+ > パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
77
+
78
+ > **警告**:パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
79
+
80
+ このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
81
+
82
+ ```
83
+ code1,code2,code3
84
+ ```
85
+
86
+ この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
87
+
88
+
89
+ ## 環境変数
90
+
91
+ > 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
92
+
93
+ ### `OPENAI_API_KEY` (必須)
94
+
95
+ OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
96
+
97
+ ### `CODE` (オプション)
98
+
99
+ アクセスパスワード。カンマで区切って複数設定可能。
100
+
101
+ **警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
102
+
103
+ ### `BASE_URL` (オプション)
104
+
105
+ > デフォルト: `https://api.openai.com`
106
+
107
+ > 例: `http://your-openai-proxy.com`
108
+
109
+ OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
110
+
111
+ > SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
112
+
113
+ ### `OPENAI_ORG_ID` (オプション)
114
+
115
+ OpenAI の組織 ID を指定します。
116
+
117
+ ### `AZURE_URL` (オプション)
118
+
119
+ > 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
120
+ > `CUSTOM_MODELS` で `displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
121
+
122
+ Azure のデプロイ URL。
123
+
124
+ ### `AZURE_API_KEY` (オプション)
125
+
126
+ Azure の API キー。
127
+
128
+ ### `AZURE_API_VERSION` (オプション)
129
+
130
+ Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
131
+
132
+ ### `GOOGLE_API_KEY` (オプション)
133
+
134
+ Google Gemini Pro API キー。
135
+
136
+ ### `GOOGLE_URL` (オプション)
137
+
138
+ Google Gemini Pro API の URL。
139
+
140
+ ### `ANTHROPIC_API_KEY` (オプション)
141
+
142
+ Anthropic Claude API キー。
143
+
144
+ ### `ANTHROPIC_API_VERSION` (オプション)
145
+
146
+ Anthropic Claude API バージョン。
147
+
148
+ ### `ANTHROPIC_URL` (オプション)
149
+
150
+ Anthropic Claude API の URL。
151
+
152
+ ### `BAIDU_API_KEY` (オプション)
153
+
154
+ Baidu API キー。
155
+
156
+ ### `BAIDU_SECRET_KEY` (オプション)
157
+
158
+ Baidu シークレットキー。
159
+
160
+ ### `BAIDU_URL` (オプション)
161
+
162
+ Baidu API の URL。
163
+
164
+ ### `BYTEDANCE_API_KEY` (オプション)
165
+
166
+ ByteDance API キー。
167
+
168
+ ### `BYTEDANCE_URL` (オプション)
169
+
170
+ ByteDance API の URL。
171
+
172
+ ### `ALIBABA_API_KEY` (オプション)
173
+
174
+ アリババ(千问)API キー。
175
+
176
+ ### `ALIBABA_URL` (オプション)
177
+
178
+ アリババ(千问)API の URL。
179
+
180
+ ### `HIDE_USER_API_KEY` (オプション)
181
+
182
+ ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
183
+
184
+ ### `DISABLE_GPT4` (オプション)
185
+
186
+ ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
187
+
188
+ ### `ENABLE_BALANCE_QUERY` (オプション)
189
+
190
+ バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
191
+
192
+ ### `DISABLE_FAST_LINK` (オプション)
193
+
194
+ リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
195
+
196
+ ### `WHITE_WEBDAV_ENDPOINTS` (オプション)
197
+
198
+ アク��ス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
199
+ - 各アドレスは完全なエンドポイントでなければなりません。
200
+ > `https://xxxx/xxx`
201
+ - 複数のアドレスは `,` で接続します。
202
+
203
+ ### `CUSTOM_MODELS` (オプション)
204
+
205
+ > 例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` は `qwen-7b-chat` と `glm-6b` をモデルリストに追加し、`gpt-3.5-turbo` を削除し、`gpt-4-1106-preview` のモデル名を `gpt-4-turbo` として表示します。
206
+ > すべてのモデルを無効にし、特定のモデルを有効にしたい場合は、`-all,+gpt-3.5-turbo` を使用します。これは `gpt-3.5-turbo` のみを有効にすることを意味します。
207
+
208
+ モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
209
+
210
+ Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
211
+ > 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
212
+
213
+ ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
214
+ > 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
215
+
216
+ ### `DEFAULT_MODEL` (オプション)
217
+
218
+ デフォルトのモデルを変更します。
219
+
220
+ ### `VISION_MODELS` (オプション)
221
+
222
+ > デフォルト:空
223
+ > 例:`gpt-4-vision,claude-3-opus,my-custom-model` は、これらのモデルにビジョン機能を追加します。これはデフォルトのパターンマッチング("vision"、"claude-3"、"gemini-1.5"などのキーワードを含むモデルを検出)に加えて適用されます。
224
+
225
+ デフォルトのパターンマッチングに加えて、追加のモデルにビジョン機能を付与します。複数のモデルはカンマで区切ります。
226
+
227
+ ### `DEFAULT_INPUT_TEMPLATE` (オプション)
228
+
229
+ 『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
230
+
231
+
232
+ ## 開発
233
+
234
+ 下のボタンをクリックして二次開発を開始してください:
235
+
236
+ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
237
+
238
+ コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
239
+
240
+ ```
241
+ OPENAI_API_KEY=<your api key here>
242
+ ```
243
+
244
+
245
+ ### ローカル開発
246
+
247
+ 1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
248
+ 2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
249
+ 3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
250
+
251
+
252
+ ## デプロイ
253
+
254
+ ### コンテナデプロイ(推奨)
255
+
256
+ > Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
257
+
258
+ > ⚠️ 注意:Docker バージョンは最新バージョンより 1~2 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
259
+
260
+ ```shell
261
+ docker pull yidadaa/chatgpt-next-web
262
+
263
+ docker run -d -p 3000:3000 \
264
+ -e OPENAI_API_KEY=sk-xxxx \
265
+ -e CODE=ページアクセスパスワード \
266
+ yidadaa/chatgpt-next-web
267
+ ```
268
+
269
+ プロキシを指定することもできます:
270
+
271
+ ```shell
272
+ docker run -d -p 3000:3000 \
273
+ -e OPENAI_API_KEY=sk-xxxx \
274
+ -e CODE=ページアクセスパスワード \
275
+ --net=host \
276
+ -e PROXY_URL=http://127.0.0.1:7890 \
277
+ yidadaa/chatgpt-next-web
278
+ ```
279
+
280
+ ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
281
+
282
+ ```shell
283
+ -e PROXY_URL="http://127.0.0.1:7890 user password"
284
+ ```
285
+
286
+ 他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
287
+
288
+
289
+ ### ローカルデプロイ
290
+
291
+ コンソールで以下のコマンドを実行します:
292
+
293
+ ```shell
294
+ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
295
+ ```
296
+
297
+ ⚠️ 注意:インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
298
+
299
+
300
+ ## 謝辞
301
+
302
+ ### 寄付者
303
+
304
+ > 英語版をご覧ください。
305
+
306
+ ### 貢献者
307
+
308
+ [プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
309
+
310
+ ### 関連プロジェクト
311
+
312
+ - [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
313
+
314
+
315
+ ## オープンソースライセンス
316
+
317
+ [MIT](https://opensource.org/license/mit/)
app/api/[provider]/[...path]/route.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiPath } from "@/app/constant";
2
+ import { NextRequest } from "next/server";
3
+ import { handle as openaiHandler } from "../../openai";
4
+ import { handle as azureHandler } from "../../azure";
5
+ import { handle as googleHandler } from "../../google";
6
+ import { handle as anthropicHandler } from "../../anthropic";
7
+ import { handle as baiduHandler } from "../../baidu";
8
+ import { handle as bytedanceHandler } from "../../bytedance";
9
+ import { handle as alibabaHandler } from "../../alibaba";
10
+ import { handle as moonshotHandler } from "../../moonshot";
11
+ import { handle as stabilityHandler } from "../../stability";
12
+ import { handle as iflytekHandler } from "../../iflytek";
13
+ import { handle as deepseekHandler } from "../../deepseek";
14
+ import { handle as siliconflowHandler } from "../../siliconflow";
15
+ import { handle as xaiHandler } from "../../xai";
16
+ import { handle as chatglmHandler } from "../../glm";
17
+ import { handle as proxyHandler } from "../../proxy";
18
+
19
+ async function handle(
20
+ req: NextRequest,
21
+ { params }: { params: { provider: string; path: string[] } },
22
+ ) {
23
+ const apiPath = `/api/${params.provider}`;
24
+ console.log(`[${params.provider} Route] params `, params);
25
+ switch (apiPath) {
26
+ case ApiPath.Azure:
27
+ return azureHandler(req, { params });
28
+ case ApiPath.Google:
29
+ return googleHandler(req, { params });
30
+ case ApiPath.Anthropic:
31
+ return anthropicHandler(req, { params });
32
+ case ApiPath.Baidu:
33
+ return baiduHandler(req, { params });
34
+ case ApiPath.ByteDance:
35
+ return bytedanceHandler(req, { params });
36
+ case ApiPath.Alibaba:
37
+ return alibabaHandler(req, { params });
38
+ // case ApiPath.Tencent: using "/api/tencent"
39
+ case ApiPath.Moonshot:
40
+ return moonshotHandler(req, { params });
41
+ case ApiPath.Stability:
42
+ return stabilityHandler(req, { params });
43
+ case ApiPath.Iflytek:
44
+ return iflytekHandler(req, { params });
45
+ case ApiPath.DeepSeek:
46
+ return deepseekHandler(req, { params });
47
+ case ApiPath.XAI:
48
+ return xaiHandler(req, { params });
49
+ case ApiPath.ChatGLM:
50
+ return chatglmHandler(req, { params });
51
+ case ApiPath.SiliconFlow:
52
+ return siliconflowHandler(req, { params });
53
+ case ApiPath.OpenAI:
54
+ return openaiHandler(req, { params });
55
+ default:
56
+ return proxyHandler(req, { params });
57
+ }
58
+ }
59
+
60
+ export const GET = handle;
61
+ export const POST = handle;
62
+
63
+ export const runtime = "edge";
64
+ export const preferredRegion = [
65
+ "arn1",
66
+ "bom1",
67
+ "cdg1",
68
+ "cle1",
69
+ "cpt1",
70
+ "dub1",
71
+ "fra1",
72
+ "gru1",
73
+ "hnd1",
74
+ "iad1",
75
+ "icn1",
76
+ "kix1",
77
+ "lhr1",
78
+ "pdx1",
79
+ "sfo1",
80
+ "sin1",
81
+ "syd1",
82
+ ];
app/api/alibaba.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ ALIBABA_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[Alibaba Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.Qwen);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[Alibaba] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, "");
46
+
47
+ let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable",
73
+ },
74
+ method: req.method,
75
+ body: req.body,
76
+ redirect: "manual",
77
+ // @ts-ignore
78
+ duplex: "half",
79
+ signal: controller.signal,
80
+ };
81
+
82
+ // #1815 try to refuse some request to some models
83
+ if (serverConfig.customModels && req.body) {
84
+ try {
85
+ const clonedBody = await req.text();
86
+ fetchOptions.body = clonedBody;
87
+
88
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
89
+
90
+ // not undefined and is false
91
+ if (
92
+ isModelNotavailableInServer(
93
+ serverConfig.customModels,
94
+ jsonBody?.model as string,
95
+ ServiceProvider.Alibaba as string,
96
+ )
97
+ ) {
98
+ return NextResponse.json(
99
+ {
100
+ error: true,
101
+ message: `you are not allowed to use ${jsonBody?.model} model`,
102
+ },
103
+ {
104
+ status: 403,
105
+ },
106
+ );
107
+ }
108
+ } catch (e) {
109
+ console.error(`[Alibaba] filter`, e);
110
+ }
111
+ }
112
+ try {
113
+ const res = await fetch(fetchUrl, fetchOptions);
114
+
115
+ // to prevent browser prompt for credentials
116
+ const newHeaders = new Headers(res.headers);
117
+ newHeaders.delete("www-authenticate");
118
+ // to disable nginx buffering
119
+ newHeaders.set("X-Accel-Buffering", "no");
120
+
121
+ return new Response(res.body, {
122
+ status: res.status,
123
+ statusText: res.statusText,
124
+ headers: newHeaders,
125
+ });
126
+ } finally {
127
+ clearTimeout(timeoutId);
128
+ }
129
+ }
app/api/anthropic.ts ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ ANTHROPIC_BASE_URL,
4
+ Anthropic,
5
+ ApiPath,
6
+ ServiceProvider,
7
+ ModelProvider,
8
+ } from "@/app/constant";
9
+ import { prettyObject } from "@/app/utils/format";
10
+ import { NextRequest, NextResponse } from "next/server";
11
+ import { auth } from "./auth";
12
+ import { isModelNotavailableInServer } from "@/app/utils/model";
13
+ import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
14
+
15
+ const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
16
+
17
+ export async function handle(
18
+ req: NextRequest,
19
+ { params }: { params: { path: string[] } },
20
+ ) {
21
+ console.log("[Anthropic Route] params ", params);
22
+
23
+ if (req.method === "OPTIONS") {
24
+ return NextResponse.json({ body: "OK" }, { status: 200 });
25
+ }
26
+
27
+ const subpath = params.path.join("/");
28
+
29
+ if (!ALLOWD_PATH.has(subpath)) {
30
+ console.log("[Anthropic Route] forbidden path ", subpath);
31
+ return NextResponse.json(
32
+ {
33
+ error: true,
34
+ msg: "you are not allowed to request " + subpath,
35
+ },
36
+ {
37
+ status: 403,
38
+ },
39
+ );
40
+ }
41
+
42
+ const authResult = auth(req, ModelProvider.Claude);
43
+ if (authResult.error) {
44
+ return NextResponse.json(authResult, {
45
+ status: 401,
46
+ });
47
+ }
48
+
49
+ try {
50
+ const response = await request(req);
51
+ return response;
52
+ } catch (e) {
53
+ console.error("[Anthropic] ", e);
54
+ return NextResponse.json(prettyObject(e));
55
+ }
56
+ }
57
+
58
+ const serverConfig = getServerSideConfig();
59
+
60
+ async function request(req: NextRequest) {
61
+ const controller = new AbortController();
62
+
63
+ let authHeaderName = "x-api-key";
64
+ let authValue =
65
+ req.headers.get(authHeaderName) ||
66
+ req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
67
+ serverConfig.anthropicApiKey ||
68
+ "";
69
+
70
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
71
+
72
+ let baseUrl =
73
+ serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
74
+
75
+ if (!baseUrl.startsWith("http")) {
76
+ baseUrl = `https://${baseUrl}`;
77
+ }
78
+
79
+ if (baseUrl.endsWith("/")) {
80
+ baseUrl = baseUrl.slice(0, -1);
81
+ }
82
+
83
+ console.log("[Proxy] ", path);
84
+ console.log("[Base Url]", baseUrl);
85
+
86
+ const timeoutId = setTimeout(
87
+ () => {
88
+ controller.abort();
89
+ },
90
+ 10 * 60 * 1000,
91
+ );
92
+
93
+ // try rebuild url, when using cloudflare ai gateway in server
94
+ const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
95
+
96
+ const fetchOptions: RequestInit = {
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ "Cache-Control": "no-store",
100
+ "anthropic-dangerous-direct-browser-access": "true",
101
+ [authHeaderName]: authValue,
102
+ "anthropic-version":
103
+ req.headers.get("anthropic-version") ||
104
+ serverConfig.anthropicApiVersion ||
105
+ Anthropic.Vision,
106
+ },
107
+ method: req.method,
108
+ body: req.body,
109
+ redirect: "manual",
110
+ // @ts-ignore
111
+ duplex: "half",
112
+ signal: controller.signal,
113
+ };
114
+
115
+ // #1815 try to refuse some request to some models
116
+ if (serverConfig.customModels && req.body) {
117
+ try {
118
+ const clonedBody = await req.text();
119
+ fetchOptions.body = clonedBody;
120
+
121
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
122
+
123
+ // not undefined and is false
124
+ if (
125
+ isModelNotavailableInServer(
126
+ serverConfig.customModels,
127
+ jsonBody?.model as string,
128
+ ServiceProvider.Anthropic as string,
129
+ )
130
+ ) {
131
+ return NextResponse.json(
132
+ {
133
+ error: true,
134
+ message: `you are not allowed to use ${jsonBody?.model} model`,
135
+ },
136
+ {
137
+ status: 403,
138
+ },
139
+ );
140
+ }
141
+ } catch (e) {
142
+ console.error(`[Anthropic] filter`, e);
143
+ }
144
+ }
145
+ // console.log("[Anthropic request]", fetchOptions.headers, req.method);
146
+ try {
147
+ const res = await fetch(fetchUrl, fetchOptions);
148
+
149
+ // console.log(
150
+ // "[Anthropic response]",
151
+ // res.status,
152
+ // " ",
153
+ // res.headers,
154
+ // res.url,
155
+ // );
156
+ // to prevent browser prompt for credentials
157
+ const newHeaders = new Headers(res.headers);
158
+ newHeaders.delete("www-authenticate");
159
+ // to disable nginx buffering
160
+ newHeaders.set("X-Accel-Buffering", "no");
161
+
162
+ return new Response(res.body, {
163
+ status: res.status,
164
+ statusText: res.statusText,
165
+ headers: newHeaders,
166
+ });
167
+ } finally {
168
+ clearTimeout(timeoutId);
169
+ }
170
+ }
app/api/artifacts/route.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import md5 from "spark-md5";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+ import { getServerSideConfig } from "@/app/config/server";
4
+
5
+ async function handle(req: NextRequest, res: NextResponse) {
6
+ const serverConfig = getServerSideConfig();
7
+ const storeUrl = () =>
8
+ `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
9
+ const storeHeaders = () => ({
10
+ Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
11
+ });
12
+ if (req.method === "POST") {
13
+ const clonedBody = await req.text();
14
+ const hashedCode = md5.hash(clonedBody).trim();
15
+ const body: {
16
+ key: string;
17
+ value: string;
18
+ expiration_ttl?: number;
19
+ } = {
20
+ key: hashedCode,
21
+ value: clonedBody,
22
+ };
23
+ try {
24
+ const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
25
+ if (ttl > 60) {
26
+ body["expiration_ttl"] = ttl;
27
+ }
28
+ } catch (e) {
29
+ console.error(e);
30
+ }
31
+ const res = await fetch(`${storeUrl()}/bulk`, {
32
+ headers: {
33
+ ...storeHeaders(),
34
+ "Content-Type": "application/json",
35
+ },
36
+ method: "PUT",
37
+ body: JSON.stringify([body]),
38
+ });
39
+ const result = await res.json();
40
+ console.log("save data", result);
41
+ if (result?.success) {
42
+ return NextResponse.json(
43
+ { code: 0, id: hashedCode, result },
44
+ { status: res.status },
45
+ );
46
+ }
47
+ return NextResponse.json(
48
+ { error: true, msg: "Save data error" },
49
+ { status: 400 },
50
+ );
51
+ }
52
+ if (req.method === "GET") {
53
+ const id = req?.nextUrl?.searchParams?.get("id");
54
+ const res = await fetch(`${storeUrl()}/values/${id}`, {
55
+ headers: storeHeaders(),
56
+ method: "GET",
57
+ });
58
+ return new Response(res.body, {
59
+ status: res.status,
60
+ statusText: res.statusText,
61
+ headers: res.headers,
62
+ });
63
+ }
64
+ return NextResponse.json(
65
+ { error: true, msg: "Invalid request" },
66
+ { status: 400 },
67
+ );
68
+ }
69
+
70
+ export const POST = handle;
71
+ export const GET = handle;
72
+
73
+ export const runtime = "edge";
app/api/auth.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest } from "next/server";
2
+ import { getServerSideConfig } from "../config/server";
3
+ import md5 from "spark-md5";
4
+ import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
5
+
6
+ function getIP(req: NextRequest) {
7
+ let ip = req.ip ?? req.headers.get("x-real-ip");
8
+ const forwardedFor = req.headers.get("x-forwarded-for");
9
+
10
+ if (!ip && forwardedFor) {
11
+ ip = forwardedFor.split(",").at(0) ?? "";
12
+ }
13
+
14
+ return ip;
15
+ }
16
+
17
+ function parseApiKey(bearToken: string) {
18
+ const token = bearToken.trim().replaceAll("Bearer ", "").trim();
19
+ const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
20
+
21
+ return {
22
+ accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
23
+ apiKey: isApiKey ? token : "",
24
+ };
25
+ }
26
+
27
+ export function auth(req: NextRequest, modelProvider: ModelProvider) {
28
+ const authToken = req.headers.get("Authorization") ?? "";
29
+
30
+ // check if it is openai api key or user token
31
+ const { accessCode, apiKey } = parseApiKey(authToken);
32
+
33
+ const hashedCode = md5.hash(accessCode ?? "").trim();
34
+
35
+ const serverConfig = getServerSideConfig();
36
+ console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
37
+ console.log("[Auth] got access code:", accessCode);
38
+ console.log("[Auth] hashed access code:", hashedCode);
39
+ console.log("[User IP] ", getIP(req));
40
+ console.log("[Time] ", new Date().toLocaleString());
41
+
42
+ if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
43
+ return {
44
+ error: true,
45
+ msg: !accessCode ? "empty access code" : "wrong access code",
46
+ };
47
+ }
48
+
49
+ if (serverConfig.hideUserApiKey && !!apiKey) {
50
+ return {
51
+ error: true,
52
+ msg: "you are not allowed to access with your own api key",
53
+ };
54
+ }
55
+
56
+ // if user does not provide an api key, inject system api key
57
+ if (!apiKey) {
58
+ const serverConfig = getServerSideConfig();
59
+
60
+ // const systemApiKey =
61
+ // modelProvider === ModelProvider.GeminiPro
62
+ // ? serverConfig.googleApiKey
63
+ // : serverConfig.isAzure
64
+ // ? serverConfig.azureApiKey
65
+ // : serverConfig.apiKey;
66
+
67
+ let systemApiKey: string | undefined;
68
+
69
+ switch (modelProvider) {
70
+ case ModelProvider.Stability:
71
+ systemApiKey = serverConfig.stabilityApiKey;
72
+ break;
73
+ case ModelProvider.GeminiPro:
74
+ systemApiKey = serverConfig.googleApiKey;
75
+ break;
76
+ case ModelProvider.Claude:
77
+ systemApiKey = serverConfig.anthropicApiKey;
78
+ break;
79
+ case ModelProvider.Doubao:
80
+ systemApiKey = serverConfig.bytedanceApiKey;
81
+ break;
82
+ case ModelProvider.Ernie:
83
+ systemApiKey = serverConfig.baiduApiKey;
84
+ break;
85
+ case ModelProvider.Qwen:
86
+ systemApiKey = serverConfig.alibabaApiKey;
87
+ break;
88
+ case ModelProvider.Moonshot:
89
+ systemApiKey = serverConfig.moonshotApiKey;
90
+ break;
91
+ case ModelProvider.Iflytek:
92
+ systemApiKey =
93
+ serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
94
+ break;
95
+ case ModelProvider.DeepSeek:
96
+ systemApiKey = serverConfig.deepseekApiKey;
97
+ break;
98
+ case ModelProvider.XAI:
99
+ systemApiKey = serverConfig.xaiApiKey;
100
+ break;
101
+ case ModelProvider.ChatGLM:
102
+ systemApiKey = serverConfig.chatglmApiKey;
103
+ break;
104
+ case ModelProvider.SiliconFlow:
105
+ systemApiKey = serverConfig.siliconFlowApiKey;
106
+ break;
107
+ case ModelProvider.GPT:
108
+ default:
109
+ if (req.nextUrl.pathname.includes("azure/deployments")) {
110
+ systemApiKey = serverConfig.azureApiKey;
111
+ } else {
112
+ systemApiKey = serverConfig.apiKey;
113
+ }
114
+ }
115
+
116
+ if (systemApiKey) {
117
+ console.log("[Auth] use system api key");
118
+ req.headers.set("Authorization", `Bearer ${systemApiKey}`);
119
+ } else {
120
+ console.log("[Auth] admin did not provide an api key");
121
+ }
122
+ } else {
123
+ console.log("[Auth] use user api key");
124
+ }
125
+
126
+ return {
127
+ error: false,
128
+ };
129
+ }
app/api/azure.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ModelProvider } from "@/app/constant";
2
+ import { prettyObject } from "@/app/utils/format";
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { auth } from "./auth";
5
+ import { requestOpenai } from "./common";
6
+
7
+ export async function handle(
8
+ req: NextRequest,
9
+ { params }: { params: { path: string[] } },
10
+ ) {
11
+ console.log("[Azure Route] params ", params);
12
+
13
+ if (req.method === "OPTIONS") {
14
+ return NextResponse.json({ body: "OK" }, { status: 200 });
15
+ }
16
+
17
+ const subpath = params.path.join("/");
18
+
19
+ const authResult = auth(req, ModelProvider.GPT);
20
+ if (authResult.error) {
21
+ return NextResponse.json(authResult, {
22
+ status: 401,
23
+ });
24
+ }
25
+
26
+ try {
27
+ return await requestOpenai(req);
28
+ } catch (e) {
29
+ console.error("[Azure] ", e);
30
+ return NextResponse.json(prettyObject(e));
31
+ }
32
+ }
app/api/baidu.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ BAIDU_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+ import { getAccessToken } from "@/app/utils/baidu";
13
+
14
+ const serverConfig = getServerSideConfig();
15
+
16
+ export async function handle(
17
+ req: NextRequest,
18
+ { params }: { params: { path: string[] } },
19
+ ) {
20
+ console.log("[Baidu Route] params ", params);
21
+
22
+ if (req.method === "OPTIONS") {
23
+ return NextResponse.json({ body: "OK" }, { status: 200 });
24
+ }
25
+
26
+ const authResult = auth(req, ModelProvider.Ernie);
27
+ if (authResult.error) {
28
+ return NextResponse.json(authResult, {
29
+ status: 401,
30
+ });
31
+ }
32
+
33
+ if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) {
34
+ return NextResponse.json(
35
+ {
36
+ error: true,
37
+ message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`,
38
+ },
39
+ {
40
+ status: 401,
41
+ },
42
+ );
43
+ }
44
+
45
+ try {
46
+ const response = await request(req);
47
+ return response;
48
+ } catch (e) {
49
+ console.error("[Baidu] ", e);
50
+ return NextResponse.json(prettyObject(e));
51
+ }
52
+ }
53
+
54
+ async function request(req: NextRequest) {
55
+ const controller = new AbortController();
56
+
57
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, "");
58
+
59
+ let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL;
60
+
61
+ if (!baseUrl.startsWith("http")) {
62
+ baseUrl = `https://${baseUrl}`;
63
+ }
64
+
65
+ if (baseUrl.endsWith("/")) {
66
+ baseUrl = baseUrl.slice(0, -1);
67
+ }
68
+
69
+ console.log("[Proxy] ", path);
70
+ console.log("[Base Url]", baseUrl);
71
+
72
+ const timeoutId = setTimeout(
73
+ () => {
74
+ controller.abort();
75
+ },
76
+ 10 * 60 * 1000,
77
+ );
78
+
79
+ const { access_token } = await getAccessToken(
80
+ serverConfig.baiduApiKey as string,
81
+ serverConfig.baiduSecretKey as string,
82
+ );
83
+ const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`;
84
+
85
+ const fetchOptions: RequestInit = {
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ },
89
+ method: req.method,
90
+ body: req.body,
91
+ redirect: "manual",
92
+ // @ts-ignore
93
+ duplex: "half",
94
+ signal: controller.signal,
95
+ };
96
+
97
+ // #1815 try to refuse some request to some models
98
+ if (serverConfig.customModels && req.body) {
99
+ try {
100
+ const clonedBody = await req.text();
101
+ fetchOptions.body = clonedBody;
102
+
103
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
104
+
105
+ // not undefined and is false
106
+ if (
107
+ isModelNotavailableInServer(
108
+ serverConfig.customModels,
109
+ jsonBody?.model as string,
110
+ ServiceProvider.Baidu as string,
111
+ )
112
+ ) {
113
+ return NextResponse.json(
114
+ {
115
+ error: true,
116
+ message: `you are not allowed to use ${jsonBody?.model} model`,
117
+ },
118
+ {
119
+ status: 403,
120
+ },
121
+ );
122
+ }
123
+ } catch (e) {
124
+ console.error(`[Baidu] filter`, e);
125
+ }
126
+ }
127
+ try {
128
+ const res = await fetch(fetchUrl, fetchOptions);
129
+
130
+ // to prevent browser prompt for credentials
131
+ const newHeaders = new Headers(res.headers);
132
+ newHeaders.delete("www-authenticate");
133
+ // to disable nginx buffering
134
+ newHeaders.set("X-Accel-Buffering", "no");
135
+
136
+ return new Response(res.body, {
137
+ status: res.status,
138
+ statusText: res.statusText,
139
+ headers: newHeaders,
140
+ });
141
+ } finally {
142
+ clearTimeout(timeoutId);
143
+ }
144
+ }
app/api/bytedance.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ BYTEDANCE_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[ByteDance Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.Doubao);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[ByteDance] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, "");
45
+
46
+ let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL;
47
+
48
+ if (!baseUrl.startsWith("http")) {
49
+ baseUrl = `https://${baseUrl}`;
50
+ }
51
+
52
+ if (baseUrl.endsWith("/")) {
53
+ baseUrl = baseUrl.slice(0, -1);
54
+ }
55
+
56
+ console.log("[Proxy] ", path);
57
+ console.log("[Base Url]", baseUrl);
58
+
59
+ const timeoutId = setTimeout(
60
+ () => {
61
+ controller.abort();
62
+ },
63
+ 10 * 60 * 1000,
64
+ );
65
+
66
+ const fetchUrl = `${baseUrl}${path}`;
67
+
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ },
73
+ method: req.method,
74
+ body: req.body,
75
+ redirect: "manual",
76
+ // @ts-ignore
77
+ duplex: "half",
78
+ signal: controller.signal,
79
+ };
80
+
81
+ // #1815 try to refuse some request to some models
82
+ if (serverConfig.customModels && req.body) {
83
+ try {
84
+ const clonedBody = await req.text();
85
+ fetchOptions.body = clonedBody;
86
+
87
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
88
+
89
+ // not undefined and is false
90
+ if (
91
+ isModelNotavailableInServer(
92
+ serverConfig.customModels,
93
+ jsonBody?.model as string,
94
+ ServiceProvider.ByteDance as string,
95
+ )
96
+ ) {
97
+ return NextResponse.json(
98
+ {
99
+ error: true,
100
+ message: `you are not allowed to use ${jsonBody?.model} model`,
101
+ },
102
+ {
103
+ status: 403,
104
+ },
105
+ );
106
+ }
107
+ } catch (e) {
108
+ console.error(`[ByteDance] filter`, e);
109
+ }
110
+ }
111
+
112
+ try {
113
+ const res = await fetch(fetchUrl, fetchOptions);
114
+
115
+ // to prevent browser prompt for credentials
116
+ const newHeaders = new Headers(res.headers);
117
+ newHeaders.delete("www-authenticate");
118
+ // to disable nginx buffering
119
+ newHeaders.set("X-Accel-Buffering", "no");
120
+
121
+ return new Response(res.body, {
122
+ status: res.status,
123
+ statusText: res.statusText,
124
+ headers: newHeaders,
125
+ });
126
+ } finally {
127
+ clearTimeout(timeoutId);
128
+ }
129
+ }
app/api/common.ts ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getServerSideConfig } from "../config/server";
3
+ import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
4
+ import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
5
+ import { getModelProvider, isModelNotavailableInServer } from "../utils/model";
6
+
7
+ const serverConfig = getServerSideConfig();
8
+
9
+ export async function requestOpenai(req: NextRequest) {
10
+ const controller = new AbortController();
11
+
12
+ const isAzure = req.nextUrl.pathname.includes("azure/deployments");
13
+
14
+ var authValue,
15
+ authHeaderName = "";
16
+ if (isAzure) {
17
+ authValue =
18
+ req.headers
19
+ .get("Authorization")
20
+ ?.trim()
21
+ .replaceAll("Bearer ", "")
22
+ .trim() ?? "";
23
+
24
+ authHeaderName = "api-key";
25
+ } else {
26
+ authValue = req.headers.get("Authorization") ?? "";
27
+ authHeaderName = "Authorization";
28
+ }
29
+
30
+ let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
31
+
32
+ let baseUrl =
33
+ (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
34
+
35
+ if (!baseUrl.startsWith("http")) {
36
+ baseUrl = `https://${baseUrl}`;
37
+ }
38
+
39
+ if (baseUrl.endsWith("/")) {
40
+ baseUrl = baseUrl.slice(0, -1);
41
+ }
42
+
43
+ console.log("[Proxy] ", path);
44
+ console.log("[Base Url]", baseUrl);
45
+
46
+ const timeoutId = setTimeout(
47
+ () => {
48
+ controller.abort();
49
+ },
50
+ 10 * 60 * 1000,
51
+ );
52
+
53
+ if (isAzure) {
54
+ const azureApiVersion =
55
+ req?.nextUrl?.searchParams?.get("api-version") ||
56
+ serverConfig.azureApiVersion;
57
+ baseUrl = baseUrl.split("/deployments").shift() as string;
58
+ path = `${req.nextUrl.pathname.replaceAll(
59
+ "/api/azure/",
60
+ "",
61
+ )}?api-version=${azureApiVersion}`;
62
+
63
+ // Forward compatibility:
64
+ // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
65
+ // then using default '{deploy-id}'
66
+ if (serverConfig.customModels && serverConfig.azureUrl) {
67
+ const modelName = path.split("/")[1];
68
+ let realDeployName = "";
69
+ serverConfig.customModels
70
+ .split(",")
71
+ .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
72
+ .forEach((m) => {
73
+ const [fullName, displayName] = m.split("=");
74
+ const [_, providerName] = getModelProvider(fullName);
75
+ if (providerName === "azure" && !displayName) {
76
+ const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
77
+ "deployments/",
78
+ );
79
+ if (deployId) {
80
+ realDeployName = deployId;
81
+ }
82
+ }
83
+ });
84
+ if (realDeployName) {
85
+ console.log("[Replace with DeployId", realDeployName);
86
+ path = path.replaceAll(modelName, realDeployName);
87
+ }
88
+ }
89
+ }
90
+
91
+ const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
92
+ console.log("fetchUrl", fetchUrl);
93
+ const fetchOptions: RequestInit = {
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ "Cache-Control": "no-store",
97
+ [authHeaderName]: authValue,
98
+ ...(serverConfig.openaiOrgId && {
99
+ "OpenAI-Organization": serverConfig.openaiOrgId,
100
+ }),
101
+ },
102
+ method: req.method,
103
+ body: req.body,
104
+ // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
105
+ redirect: "manual",
106
+ // @ts-ignore
107
+ duplex: "half",
108
+ signal: controller.signal,
109
+ };
110
+
111
+ // #1815 try to refuse gpt4 request
112
+ if (serverConfig.customModels && req.body) {
113
+ try {
114
+ const clonedBody = await req.text();
115
+ fetchOptions.body = clonedBody;
116
+
117
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
118
+
119
+ // not undefined and is false
120
+ if (
121
+ isModelNotavailableInServer(
122
+ serverConfig.customModels,
123
+ jsonBody?.model as string,
124
+ [
125
+ ServiceProvider.OpenAI,
126
+ ServiceProvider.Azure,
127
+ jsonBody?.model as string, // support provider-unspecified model
128
+ ],
129
+ )
130
+ ) {
131
+ return NextResponse.json(
132
+ {
133
+ error: true,
134
+ message: `you are not allowed to use ${jsonBody?.model} model`,
135
+ },
136
+ {
137
+ status: 403,
138
+ },
139
+ );
140
+ }
141
+ } catch (e) {
142
+ console.error("[OpenAI] gpt4 filter", e);
143
+ }
144
+ }
145
+
146
+ try {
147
+ const res = await fetch(fetchUrl, fetchOptions);
148
+
149
+ // Extract the OpenAI-Organization header from the response
150
+ const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
151
+
152
+ // Check if serverConfig.openaiOrgId is defined and not an empty string
153
+ if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
154
+ // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
155
+ console.log("[Org ID]", openaiOrganizationHeader);
156
+ } else {
157
+ console.log("[Org ID] is not set up.");
158
+ }
159
+
160
+ // to prevent browser prompt for credentials
161
+ const newHeaders = new Headers(res.headers);
162
+ newHeaders.delete("www-authenticate");
163
+ // to disable nginx buffering
164
+ newHeaders.set("X-Accel-Buffering", "no");
165
+
166
+ // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
167
+ // Also, this is to prevent the header from being sent to the client
168
+ if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
169
+ newHeaders.delete("OpenAI-Organization");
170
+ }
171
+
172
+ // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
173
+ // So if the streaming is disabled, we need to remove the content-encoding header
174
+ // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
175
+ // The browser will try to decode the response with brotli and fail
176
+ newHeaders.delete("content-encoding");
177
+
178
+ return new Response(res.body, {
179
+ status: res.status,
180
+ statusText: res.statusText,
181
+ headers: newHeaders,
182
+ });
183
+ } finally {
184
+ clearTimeout(timeoutId);
185
+ }
186
+ }
app/api/config/route.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { getServerSideConfig } from "../../config/server";
4
+
5
+ const serverConfig = getServerSideConfig();
6
+
7
+ // Danger! Do not hard code any secret value here!
8
+ // 警告!不要在这里写入任何敏感信息!
9
+ const DANGER_CONFIG = {
10
+ needCode: serverConfig.needCode,
11
+ hideUserApiKey: serverConfig.hideUserApiKey,
12
+ disableGPT4: serverConfig.disableGPT4,
13
+ hideBalanceQuery: serverConfig.hideBalanceQuery,
14
+ disableFastLink: serverConfig.disableFastLink,
15
+ customModels: serverConfig.customModels,
16
+ defaultModel: serverConfig.defaultModel,
17
+ visionModels: serverConfig.visionModels,
18
+ };
19
+
20
+ declare global {
21
+ type DangerConfig = typeof DANGER_CONFIG;
22
+ }
23
+
24
+ async function handle() {
25
+ return NextResponse.json(DANGER_CONFIG);
26
+ }
27
+
28
+ export const GET = handle;
29
+ export const POST = handle;
30
+
31
+ export const runtime = "edge";
app/api/deepseek.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ DEEPSEEK_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[DeepSeek Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.DeepSeek);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[DeepSeek] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.DeepSeek, "");
46
+
47
+ let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ },
73
+ method: req.method,
74
+ body: req.body,
75
+ redirect: "manual",
76
+ // @ts-ignore
77
+ duplex: "half",
78
+ signal: controller.signal,
79
+ };
80
+
81
+ // #1815 try to refuse some request to some models
82
+ if (serverConfig.customModels && req.body) {
83
+ try {
84
+ const clonedBody = await req.text();
85
+ fetchOptions.body = clonedBody;
86
+
87
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
88
+
89
+ // not undefined and is false
90
+ if (
91
+ isModelNotavailableInServer(
92
+ serverConfig.customModels,
93
+ jsonBody?.model as string,
94
+ ServiceProvider.DeepSeek as string,
95
+ )
96
+ ) {
97
+ return NextResponse.json(
98
+ {
99
+ error: true,
100
+ message: `you are not allowed to use ${jsonBody?.model} model`,
101
+ },
102
+ {
103
+ status: 403,
104
+ },
105
+ );
106
+ }
107
+ } catch (e) {
108
+ console.error(`[DeepSeek] filter`, e);
109
+ }
110
+ }
111
+ try {
112
+ const res = await fetch(fetchUrl, fetchOptions);
113
+
114
+ // to prevent browser prompt for credentials
115
+ const newHeaders = new Headers(res.headers);
116
+ newHeaders.delete("www-authenticate");
117
+ // to disable nginx buffering
118
+ newHeaders.set("X-Accel-Buffering", "no");
119
+
120
+ return new Response(res.body, {
121
+ status: res.status,
122
+ statusText: res.statusText,
123
+ headers: newHeaders,
124
+ });
125
+ } finally {
126
+ clearTimeout(timeoutId);
127
+ }
128
+ }
app/api/glm.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ CHATGLM_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[GLM Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.ChatGLM);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[GLM] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
46
+
47
+ let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ console.log("[Fetch Url] ", fetchUrl);
69
+ const fetchOptions: RequestInit = {
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ Authorization: req.headers.get("Authorization") ?? "",
73
+ },
74
+ method: req.method,
75
+ body: req.body,
76
+ redirect: "manual",
77
+ // @ts-ignore
78
+ duplex: "half",
79
+ signal: controller.signal,
80
+ };
81
+
82
+ // #1815 try to refuse some request to some models
83
+ if (serverConfig.customModels && req.body) {
84
+ try {
85
+ const clonedBody = await req.text();
86
+ fetchOptions.body = clonedBody;
87
+
88
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
89
+
90
+ // not undefined and is false
91
+ if (
92
+ isModelNotavailableInServer(
93
+ serverConfig.customModels,
94
+ jsonBody?.model as string,
95
+ ServiceProvider.ChatGLM as string,
96
+ )
97
+ ) {
98
+ return NextResponse.json(
99
+ {
100
+ error: true,
101
+ message: `you are not allowed to use ${jsonBody?.model} model`,
102
+ },
103
+ {
104
+ status: 403,
105
+ },
106
+ );
107
+ }
108
+ } catch (e) {
109
+ console.error(`[GLM] filter`, e);
110
+ }
111
+ }
112
+ try {
113
+ const res = await fetch(fetchUrl, fetchOptions);
114
+
115
+ // to prevent browser prompt for credentials
116
+ const newHeaders = new Headers(res.headers);
117
+ newHeaders.delete("www-authenticate");
118
+ // to disable nginx buffering
119
+ newHeaders.set("X-Accel-Buffering", "no");
120
+
121
+ return new Response(res.body, {
122
+ status: res.status,
123
+ statusText: res.statusText,
124
+ headers: newHeaders,
125
+ });
126
+ } finally {
127
+ clearTimeout(timeoutId);
128
+ }
129
+ }
app/api/google.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { auth } from "./auth";
3
+ import { getServerSideConfig } from "@/app/config/server";
4
+ import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
5
+ import { prettyObject } from "@/app/utils/format";
6
+
7
+ const serverConfig = getServerSideConfig();
8
+
9
+ export async function handle(
10
+ req: NextRequest,
11
+ { params }: { params: { provider: string; path: string[] } },
12
+ ) {
13
+ console.log("[Google Route] params ", params);
14
+
15
+ if (req.method === "OPTIONS") {
16
+ return NextResponse.json({ body: "OK" }, { status: 200 });
17
+ }
18
+
19
+ const authResult = auth(req, ModelProvider.GeminiPro);
20
+ if (authResult.error) {
21
+ return NextResponse.json(authResult, {
22
+ status: 401,
23
+ });
24
+ }
25
+
26
+ const bearToken =
27
+ req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
28
+ const token = bearToken.trim().replaceAll("Bearer ", "").trim();
29
+
30
+ const apiKey = token ? token : serverConfig.googleApiKey;
31
+
32
+ if (!apiKey) {
33
+ return NextResponse.json(
34
+ {
35
+ error: true,
36
+ message: `missing GOOGLE_API_KEY in server env vars`,
37
+ },
38
+ {
39
+ status: 401,
40
+ },
41
+ );
42
+ }
43
+ try {
44
+ const response = await request(req, apiKey);
45
+ return response;
46
+ } catch (e) {
47
+ console.error("[Google] ", e);
48
+ return NextResponse.json(prettyObject(e));
49
+ }
50
+ }
51
+
52
+ export const GET = handle;
53
+ export const POST = handle;
54
+
55
+ export const runtime = "edge";
56
+ export const preferredRegion = [
57
+ "bom1",
58
+ "cle1",
59
+ "cpt1",
60
+ "gru1",
61
+ "hnd1",
62
+ "iad1",
63
+ "icn1",
64
+ "kix1",
65
+ "pdx1",
66
+ "sfo1",
67
+ "sin1",
68
+ "syd1",
69
+ ];
70
+
71
+ async function request(req: NextRequest, apiKey: string) {
72
+ const controller = new AbortController();
73
+
74
+ let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
75
+
76
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
77
+
78
+ if (!baseUrl.startsWith("http")) {
79
+ baseUrl = `https://${baseUrl}`;
80
+ }
81
+
82
+ if (baseUrl.endsWith("/")) {
83
+ baseUrl = baseUrl.slice(0, -1);
84
+ }
85
+
86
+ console.log("[Proxy] ", path);
87
+ console.log("[Base Url]", baseUrl);
88
+
89
+ const timeoutId = setTimeout(
90
+ () => {
91
+ controller.abort();
92
+ },
93
+ 10 * 60 * 1000,
94
+ );
95
+ const fetchUrl = `${baseUrl}${path}${
96
+ req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
97
+ }`;
98
+
99
+ console.log("[Fetch Url] ", fetchUrl);
100
+ const fetchOptions: RequestInit = {
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ "Cache-Control": "no-store",
104
+ "x-goog-api-key":
105
+ req.headers.get("x-goog-api-key") ||
106
+ (req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
107
+ },
108
+ method: req.method,
109
+ body: req.body,
110
+ // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
111
+ redirect: "manual",
112
+ // @ts-ignore
113
+ duplex: "half",
114
+ signal: controller.signal,
115
+ };
116
+
117
+ try {
118
+ const res = await fetch(fetchUrl, fetchOptions);
119
+ // to prevent browser prompt for credentials
120
+ const newHeaders = new Headers(res.headers);
121
+ newHeaders.delete("www-authenticate");
122
+ // to disable nginx buffering
123
+ newHeaders.set("X-Accel-Buffering", "no");
124
+
125
+ return new Response(res.body, {
126
+ status: res.status,
127
+ statusText: res.statusText,
128
+ headers: newHeaders,
129
+ });
130
+ } finally {
131
+ clearTimeout(timeoutId);
132
+ }
133
+ }
app/api/iflytek.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ IFLYTEK_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+ // iflytek
13
+
14
+ const serverConfig = getServerSideConfig();
15
+
16
+ export async function handle(
17
+ req: NextRequest,
18
+ { params }: { params: { path: string[] } },
19
+ ) {
20
+ console.log("[Iflytek Route] params ", params);
21
+
22
+ if (req.method === "OPTIONS") {
23
+ return NextResponse.json({ body: "OK" }, { status: 200 });
24
+ }
25
+
26
+ const authResult = auth(req, ModelProvider.Iflytek);
27
+ if (authResult.error) {
28
+ return NextResponse.json(authResult, {
29
+ status: 401,
30
+ });
31
+ }
32
+
33
+ try {
34
+ const response = await request(req);
35
+ return response;
36
+ } catch (e) {
37
+ console.error("[Iflytek] ", e);
38
+ return NextResponse.json(prettyObject(e));
39
+ }
40
+ }
41
+
42
+ async function request(req: NextRequest) {
43
+ const controller = new AbortController();
44
+
45
+ // iflytek use base url or just remove the path
46
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
47
+
48
+ let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
49
+
50
+ if (!baseUrl.startsWith("http")) {
51
+ baseUrl = `https://${baseUrl}`;
52
+ }
53
+
54
+ if (baseUrl.endsWith("/")) {
55
+ baseUrl = baseUrl.slice(0, -1);
56
+ }
57
+
58
+ console.log("[Proxy] ", path);
59
+ console.log("[Base Url]", baseUrl);
60
+
61
+ const timeoutId = setTimeout(
62
+ () => {
63
+ controller.abort();
64
+ },
65
+ 10 * 60 * 1000,
66
+ );
67
+
68
+ const fetchUrl = `${baseUrl}${path}`;
69
+ const fetchOptions: RequestInit = {
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ Authorization: req.headers.get("Authorization") ?? "",
73
+ },
74
+ method: req.method,
75
+ body: req.body,
76
+ redirect: "manual",
77
+ // @ts-ignore
78
+ duplex: "half",
79
+ signal: controller.signal,
80
+ };
81
+
82
+ // try to refuse some request to some models
83
+ if (serverConfig.customModels && req.body) {
84
+ try {
85
+ const clonedBody = await req.text();
86
+ fetchOptions.body = clonedBody;
87
+
88
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
89
+
90
+ // not undefined and is false
91
+ if (
92
+ isModelNotavailableInServer(
93
+ serverConfig.customModels,
94
+ jsonBody?.model as string,
95
+ ServiceProvider.Iflytek as string,
96
+ )
97
+ ) {
98
+ return NextResponse.json(
99
+ {
100
+ error: true,
101
+ message: `you are not allowed to use ${jsonBody?.model} model`,
102
+ },
103
+ {
104
+ status: 403,
105
+ },
106
+ );
107
+ }
108
+ } catch (e) {
109
+ console.error(`[Iflytek] filter`, e);
110
+ }
111
+ }
112
+ try {
113
+ const res = await fetch(fetchUrl, fetchOptions);
114
+
115
+ // to prevent browser prompt for credentials
116
+ const newHeaders = new Headers(res.headers);
117
+ newHeaders.delete("www-authenticate");
118
+ // to disable nginx buffering
119
+ newHeaders.set("X-Accel-Buffering", "no");
120
+
121
+ return new Response(res.body, {
122
+ status: res.status,
123
+ statusText: res.statusText,
124
+ headers: newHeaders,
125
+ });
126
+ } finally {
127
+ clearTimeout(timeoutId);
128
+ }
129
+ }
app/api/moonshot.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ MOONSHOT_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[Moonshot Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.Moonshot);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[Moonshot] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
46
+
47
+ let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ },
73
+ method: req.method,
74
+ body: req.body,
75
+ redirect: "manual",
76
+ // @ts-ignore
77
+ duplex: "half",
78
+ signal: controller.signal,
79
+ };
80
+
81
+ // #1815 try to refuse some request to some models
82
+ if (serverConfig.customModels && req.body) {
83
+ try {
84
+ const clonedBody = await req.text();
85
+ fetchOptions.body = clonedBody;
86
+
87
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
88
+
89
+ // not undefined and is false
90
+ if (
91
+ isModelNotavailableInServer(
92
+ serverConfig.customModels,
93
+ jsonBody?.model as string,
94
+ ServiceProvider.Moonshot as string,
95
+ )
96
+ ) {
97
+ return NextResponse.json(
98
+ {
99
+ error: true,
100
+ message: `you are not allowed to use ${jsonBody?.model} model`,
101
+ },
102
+ {
103
+ status: 403,
104
+ },
105
+ );
106
+ }
107
+ } catch (e) {
108
+ console.error(`[Moonshot] filter`, e);
109
+ }
110
+ }
111
+ try {
112
+ const res = await fetch(fetchUrl, fetchOptions);
113
+
114
+ // to prevent browser prompt for credentials
115
+ const newHeaders = new Headers(res.headers);
116
+ newHeaders.delete("www-authenticate");
117
+ // to disable nginx buffering
118
+ newHeaders.set("X-Accel-Buffering", "no");
119
+
120
+ return new Response(res.body, {
121
+ status: res.status,
122
+ statusText: res.statusText,
123
+ headers: newHeaders,
124
+ });
125
+ } finally {
126
+ clearTimeout(timeoutId);
127
+ }
128
+ }
app/api/openai.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
2
+ import { getServerSideConfig } from "@/app/config/server";
3
+ import { ModelProvider, OpenaiPath } from "@/app/constant";
4
+ import { prettyObject } from "@/app/utils/format";
5
+ import { NextRequest, NextResponse } from "next/server";
6
+ import { auth } from "./auth";
7
+ import { requestOpenai } from "./common";
8
+
9
+ const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
10
+
11
+ function getModels(remoteModelRes: OpenAIListModelResponse) {
12
+ const config = getServerSideConfig();
13
+
14
+ if (config.disableGPT4) {
15
+ remoteModelRes.data = remoteModelRes.data.filter(
16
+ (m) =>
17
+ !(
18
+ m.id.startsWith("gpt-4") ||
19
+ m.id.startsWith("chatgpt-4o") ||
20
+ m.id.startsWith("o1") ||
21
+ m.id.startsWith("o3")
22
+ ) || m.id.startsWith("gpt-4o-mini"),
23
+ );
24
+ }
25
+
26
+ return remoteModelRes;
27
+ }
28
+
29
+ export async function handle(
30
+ req: NextRequest,
31
+ { params }: { params: { path: string[] } },
32
+ ) {
33
+ console.log("[OpenAI Route] params ", params);
34
+
35
+ if (req.method === "OPTIONS") {
36
+ return NextResponse.json({ body: "OK" }, { status: 200 });
37
+ }
38
+
39
+ const subpath = params.path.join("/");
40
+
41
+ if (!ALLOWED_PATH.has(subpath)) {
42
+ console.log("[OpenAI Route] forbidden path ", subpath);
43
+ return NextResponse.json(
44
+ {
45
+ error: true,
46
+ msg: "you are not allowed to request " + subpath,
47
+ },
48
+ {
49
+ status: 403,
50
+ },
51
+ );
52
+ }
53
+
54
+ const authResult = auth(req, ModelProvider.GPT);
55
+ if (authResult.error) {
56
+ return NextResponse.json(authResult, {
57
+ status: 401,
58
+ });
59
+ }
60
+
61
+ try {
62
+ const response = await requestOpenai(req);
63
+
64
+ // list models
65
+ if (subpath === OpenaiPath.ListModelPath && response.status === 200) {
66
+ const resJson = (await response.json()) as OpenAIListModelResponse;
67
+ const availableModels = getModels(resJson);
68
+ return NextResponse.json(availableModels, {
69
+ status: response.status,
70
+ });
71
+ }
72
+
73
+ return response;
74
+ } catch (e) {
75
+ console.error("[OpenAI] ", e);
76
+ return NextResponse.json(prettyObject(e));
77
+ }
78
+ }
app/api/proxy.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getServerSideConfig } from "@/app/config/server";
3
+
4
+ export async function handle(
5
+ req: NextRequest,
6
+ { params }: { params: { path: string[] } },
7
+ ) {
8
+ console.log("[Proxy Route] params ", params);
9
+
10
+ if (req.method === "OPTIONS") {
11
+ return NextResponse.json({ body: "OK" }, { status: 200 });
12
+ }
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ // remove path params from searchParams
16
+ req.nextUrl.searchParams.delete("path");
17
+ req.nextUrl.searchParams.delete("provider");
18
+
19
+ const subpath = params.path.join("/");
20
+ const fetchUrl = `${req.headers.get(
21
+ "x-base-url",
22
+ )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
23
+ const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
24
+ const headers = new Headers(
25
+ Array.from(req.headers.entries()).filter((item) => {
26
+ if (
27
+ item[0].indexOf("x-") > -1 ||
28
+ item[0].indexOf("sec-") > -1 ||
29
+ skipHeaders.includes(item[0])
30
+ ) {
31
+ return false;
32
+ }
33
+ return true;
34
+ }),
35
+ );
36
+ // if dalle3 use openai api key
37
+ const baseUrl = req.headers.get("x-base-url");
38
+ if (baseUrl?.includes("api.openai.com")) {
39
+ if (!serverConfig.apiKey) {
40
+ return NextResponse.json(
41
+ { error: "OpenAI API key not configured" },
42
+ { status: 500 },
43
+ );
44
+ }
45
+ headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
46
+ }
47
+
48
+ const controller = new AbortController();
49
+ const fetchOptions: RequestInit = {
50
+ headers,
51
+ method: req.method,
52
+ body: req.body,
53
+ // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
54
+ redirect: "manual",
55
+ // @ts-ignore
56
+ duplex: "half",
57
+ signal: controller.signal,
58
+ };
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ try {
68
+ const res = await fetch(fetchUrl, fetchOptions);
69
+ // to prevent browser prompt for credentials
70
+ const newHeaders = new Headers(res.headers);
71
+ newHeaders.delete("www-authenticate");
72
+ // to disable nginx buffering
73
+ newHeaders.set("X-Accel-Buffering", "no");
74
+
75
+ // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
76
+ // So if the streaming is disabled, we need to remove the content-encoding header
77
+ // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
78
+ // The browser will try to decode the response with brotli and fail
79
+ newHeaders.delete("content-encoding");
80
+
81
+ return new Response(res.body, {
82
+ status: res.status,
83
+ statusText: res.statusText,
84
+ headers: newHeaders,
85
+ });
86
+ } finally {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ }
app/api/siliconflow.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ SILICONFLOW_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[SiliconFlow Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.SiliconFlow);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[SiliconFlow] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.SiliconFlow, "");
46
+
47
+ let baseUrl = serverConfig.siliconFlowUrl || SILICONFLOW_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ },
73
+ method: req.method,
74
+ body: req.body,
75
+ redirect: "manual",
76
+ // @ts-ignore
77
+ duplex: "half",
78
+ signal: controller.signal,
79
+ };
80
+
81
+ // #1815 try to refuse some request to some models
82
+ if (serverConfig.customModels && req.body) {
83
+ try {
84
+ const clonedBody = await req.text();
85
+ fetchOptions.body = clonedBody;
86
+
87
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
88
+
89
+ // not undefined and is false
90
+ if (
91
+ isModelNotavailableInServer(
92
+ serverConfig.customModels,
93
+ jsonBody?.model as string,
94
+ ServiceProvider.SiliconFlow as string,
95
+ )
96
+ ) {
97
+ return NextResponse.json(
98
+ {
99
+ error: true,
100
+ message: `you are not allowed to use ${jsonBody?.model} model`,
101
+ },
102
+ {
103
+ status: 403,
104
+ },
105
+ );
106
+ }
107
+ } catch (e) {
108
+ console.error(`[SiliconFlow] filter`, e);
109
+ }
110
+ }
111
+ try {
112
+ const res = await fetch(fetchUrl, fetchOptions);
113
+
114
+ // to prevent browser prompt for credentials
115
+ const newHeaders = new Headers(res.headers);
116
+ newHeaders.delete("www-authenticate");
117
+ // to disable nginx buffering
118
+ newHeaders.set("X-Accel-Buffering", "no");
119
+
120
+ return new Response(res.body, {
121
+ status: res.status,
122
+ statusText: res.statusText,
123
+ headers: newHeaders,
124
+ });
125
+ } finally {
126
+ clearTimeout(timeoutId);
127
+ }
128
+ }
app/api/stability.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getServerSideConfig } from "@/app/config/server";
3
+ import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
4
+ import { auth } from "@/app/api/auth";
5
+
6
+ export async function handle(
7
+ req: NextRequest,
8
+ { params }: { params: { path: string[] } },
9
+ ) {
10
+ console.log("[Stability] params ", params);
11
+
12
+ if (req.method === "OPTIONS") {
13
+ return NextResponse.json({ body: "OK" }, { status: 200 });
14
+ }
15
+
16
+ const controller = new AbortController();
17
+
18
+ const serverConfig = getServerSideConfig();
19
+
20
+ let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
21
+
22
+ if (!baseUrl.startsWith("http")) {
23
+ baseUrl = `https://${baseUrl}`;
24
+ }
25
+
26
+ if (baseUrl.endsWith("/")) {
27
+ baseUrl = baseUrl.slice(0, -1);
28
+ }
29
+
30
+ let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
31
+
32
+ console.log("[Stability Proxy] ", path);
33
+ console.log("[Stability Base Url]", baseUrl);
34
+
35
+ const timeoutId = setTimeout(
36
+ () => {
37
+ controller.abort();
38
+ },
39
+ 10 * 60 * 1000,
40
+ );
41
+
42
+ const authResult = auth(req, ModelProvider.Stability);
43
+
44
+ if (authResult.error) {
45
+ return NextResponse.json(authResult, {
46
+ status: 401,
47
+ });
48
+ }
49
+
50
+ const bearToken = req.headers.get("Authorization") ?? "";
51
+ const token = bearToken.trim().replaceAll("Bearer ", "").trim();
52
+
53
+ const key = token ? token : serverConfig.stabilityApiKey;
54
+
55
+ if (!key) {
56
+ return NextResponse.json(
57
+ {
58
+ error: true,
59
+ message: `missing STABILITY_API_KEY in server env vars`,
60
+ },
61
+ {
62
+ status: 401,
63
+ },
64
+ );
65
+ }
66
+
67
+ const fetchUrl = `${baseUrl}/${path}`;
68
+ console.log("[Stability Url] ", fetchUrl);
69
+ const fetchOptions: RequestInit = {
70
+ headers: {
71
+ "Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
72
+ Accept: req.headers.get("Accept") || "application/json",
73
+ Authorization: `Bearer ${key}`,
74
+ },
75
+ method: req.method,
76
+ body: req.body,
77
+ // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
78
+ redirect: "manual",
79
+ // @ts-ignore
80
+ duplex: "half",
81
+ signal: controller.signal,
82
+ };
83
+
84
+ try {
85
+ const res = await fetch(fetchUrl, fetchOptions);
86
+ // to prevent browser prompt for credentials
87
+ const newHeaders = new Headers(res.headers);
88
+ newHeaders.delete("www-authenticate");
89
+ // to disable nginx buffering
90
+ newHeaders.set("X-Accel-Buffering", "no");
91
+ return new Response(res.body, {
92
+ status: res.status,
93
+ statusText: res.statusText,
94
+ headers: newHeaders,
95
+ });
96
+ } finally {
97
+ clearTimeout(timeoutId);
98
+ }
99
+ }
app/api/tencent/route.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
3
+ import { prettyObject } from "@/app/utils/format";
4
+ import { NextRequest, NextResponse } from "next/server";
5
+ import { auth } from "@/app/api/auth";
6
+ import { getHeader } from "@/app/utils/tencent";
7
+
8
+ const serverConfig = getServerSideConfig();
9
+
10
+ async function handle(
11
+ req: NextRequest,
12
+ { params }: { params: { path: string[] } },
13
+ ) {
14
+ console.log("[Tencent Route] params ", params);
15
+
16
+ if (req.method === "OPTIONS") {
17
+ return NextResponse.json({ body: "OK" }, { status: 200 });
18
+ }
19
+
20
+ const authResult = auth(req, ModelProvider.Hunyuan);
21
+ if (authResult.error) {
22
+ return NextResponse.json(authResult, {
23
+ status: 401,
24
+ });
25
+ }
26
+
27
+ try {
28
+ const response = await request(req);
29
+ return response;
30
+ } catch (e) {
31
+ console.error("[Tencent] ", e);
32
+ return NextResponse.json(prettyObject(e));
33
+ }
34
+ }
35
+
36
+ export const GET = handle;
37
+ export const POST = handle;
38
+
39
+ export const runtime = "edge";
40
+ export const preferredRegion = [
41
+ "arn1",
42
+ "bom1",
43
+ "cdg1",
44
+ "cle1",
45
+ "cpt1",
46
+ "dub1",
47
+ "fra1",
48
+ "gru1",
49
+ "hnd1",
50
+ "iad1",
51
+ "icn1",
52
+ "kix1",
53
+ "lhr1",
54
+ "pdx1",
55
+ "sfo1",
56
+ "sin1",
57
+ "syd1",
58
+ ];
59
+
60
+ async function request(req: NextRequest) {
61
+ const controller = new AbortController();
62
+
63
+ let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
64
+
65
+ if (!baseUrl.startsWith("http")) {
66
+ baseUrl = `https://${baseUrl}`;
67
+ }
68
+
69
+ if (baseUrl.endsWith("/")) {
70
+ baseUrl = baseUrl.slice(0, -1);
71
+ }
72
+
73
+ console.log("[Base Url]", baseUrl);
74
+
75
+ const timeoutId = setTimeout(
76
+ () => {
77
+ controller.abort();
78
+ },
79
+ 10 * 60 * 1000,
80
+ );
81
+
82
+ const fetchUrl = baseUrl;
83
+
84
+ const body = await req.text();
85
+ const headers = await getHeader(
86
+ body,
87
+ serverConfig.tencentSecretId as string,
88
+ serverConfig.tencentSecretKey as string,
89
+ );
90
+ const fetchOptions: RequestInit = {
91
+ headers,
92
+ method: req.method,
93
+ body,
94
+ redirect: "manual",
95
+ // @ts-ignore
96
+ duplex: "half",
97
+ signal: controller.signal,
98
+ };
99
+
100
+ try {
101
+ const res = await fetch(fetchUrl, fetchOptions);
102
+
103
+ // to prevent browser prompt for credentials
104
+ const newHeaders = new Headers(res.headers);
105
+ newHeaders.delete("www-authenticate");
106
+ // to disable nginx buffering
107
+ newHeaders.set("X-Accel-Buffering", "no");
108
+
109
+ return new Response(res.body, {
110
+ status: res.status,
111
+ statusText: res.statusText,
112
+ headers: newHeaders,
113
+ });
114
+ } finally {
115
+ clearTimeout(timeoutId);
116
+ }
117
+ }
app/api/upstash/[action]/[...key]/route.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ async function handle(
4
+ req: NextRequest,
5
+ { params }: { params: { action: string; key: string[] } },
6
+ ) {
7
+ const requestUrl = new URL(req.url);
8
+ const endpoint = requestUrl.searchParams.get("endpoint");
9
+
10
+ if (req.method === "OPTIONS") {
11
+ return NextResponse.json({ body: "OK" }, { status: 200 });
12
+ }
13
+ const [...key] = params.key;
14
+ // only allow to request to *.upstash.io
15
+ if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
16
+ return NextResponse.json(
17
+ {
18
+ error: true,
19
+ msg: "you are not allowed to request " + params.key.join("/"),
20
+ },
21
+ {
22
+ status: 403,
23
+ },
24
+ );
25
+ }
26
+
27
+ // only allow upstash get and set method
28
+ if (params.action !== "get" && params.action !== "set") {
29
+ console.log("[Upstash Route] forbidden action ", params.action);
30
+ return NextResponse.json(
31
+ {
32
+ error: true,
33
+ msg: "you are not allowed to request " + params.action,
34
+ },
35
+ {
36
+ status: 403,
37
+ },
38
+ );
39
+ }
40
+
41
+ const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
42
+
43
+ const method = req.method;
44
+ const shouldNotHaveBody = ["get", "head"].includes(
45
+ method?.toLowerCase() ?? "",
46
+ );
47
+
48
+ const fetchOptions: RequestInit = {
49
+ headers: {
50
+ authorization: req.headers.get("authorization") ?? "",
51
+ },
52
+ body: shouldNotHaveBody ? null : req.body,
53
+ method,
54
+ // @ts-ignore
55
+ duplex: "half",
56
+ };
57
+
58
+ console.log("[Upstash Proxy]", targetUrl, fetchOptions);
59
+ const fetchResult = await fetch(targetUrl, fetchOptions);
60
+
61
+ console.log("[Any Proxy]", targetUrl, {
62
+ status: fetchResult.status,
63
+ statusText: fetchResult.statusText,
64
+ });
65
+
66
+ return fetchResult;
67
+ }
68
+
69
+ export const POST = handle;
70
+ export const GET = handle;
71
+ export const OPTIONS = handle;
72
+
73
+ export const runtime = "edge";
app/api/webdav/[...path]/route.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
3
+ import { getServerSideConfig } from "@/app/config/server";
4
+
5
+ const config = getServerSideConfig();
6
+
7
+ const mergedAllowedWebDavEndpoints = [
8
+ ...internalAllowedWebDavEndpoints,
9
+ ...config.allowedWebDavEndpoints,
10
+ ].filter((domain) => Boolean(domain.trim()));
11
+
12
+ const normalizeUrl = (url: string) => {
13
+ try {
14
+ return new URL(url);
15
+ } catch (err) {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ async function handle(
21
+ req: NextRequest,
22
+ { params }: { params: { path: string[] } },
23
+ ) {
24
+ if (req.method === "OPTIONS") {
25
+ return NextResponse.json({ body: "OK" }, { status: 200 });
26
+ }
27
+ const folder = STORAGE_KEY;
28
+ const fileName = `${folder}/backup.json`;
29
+
30
+ const requestUrl = new URL(req.url);
31
+ let endpoint = requestUrl.searchParams.get("endpoint");
32
+ let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
33
+
34
+ // Validate the endpoint to prevent potential SSRF attacks
35
+ if (
36
+ !endpoint ||
37
+ !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
38
+ const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
39
+ const normalizedEndpoint = normalizeUrl(endpoint as string);
40
+
41
+ return (
42
+ normalizedEndpoint &&
43
+ normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
44
+ normalizedEndpoint.pathname.startsWith(
45
+ normalizedAllowedEndpoint.pathname,
46
+ )
47
+ );
48
+ })
49
+ ) {
50
+ return NextResponse.json(
51
+ {
52
+ error: true,
53
+ msg: "Invalid endpoint",
54
+ },
55
+ {
56
+ status: 400,
57
+ },
58
+ );
59
+ }
60
+
61
+ if (!endpoint?.endsWith("/")) {
62
+ endpoint += "/";
63
+ }
64
+
65
+ const endpointPath = params.path.join("/");
66
+ const targetPath = `${endpoint}${endpointPath}`;
67
+
68
+ // only allow MKCOL, GET, PUT
69
+ if (
70
+ proxy_method !== "MKCOL" &&
71
+ proxy_method !== "GET" &&
72
+ proxy_method !== "PUT"
73
+ ) {
74
+ return NextResponse.json(
75
+ {
76
+ error: true,
77
+ msg: "you are not allowed to request " + targetPath,
78
+ },
79
+ {
80
+ status: 403,
81
+ },
82
+ );
83
+ }
84
+
85
+ // for MKCOL request, only allow request ${folder}
86
+ if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
87
+ return NextResponse.json(
88
+ {
89
+ error: true,
90
+ msg: "you are not allowed to request " + targetPath,
91
+ },
92
+ {
93
+ status: 403,
94
+ },
95
+ );
96
+ }
97
+
98
+ // for GET request, only allow request ending with fileName
99
+ if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
100
+ return NextResponse.json(
101
+ {
102
+ error: true,
103
+ msg: "you are not allowed to request " + targetPath,
104
+ },
105
+ {
106
+ status: 403,
107
+ },
108
+ );
109
+ }
110
+
111
+ // for PUT request, only allow request ending with fileName
112
+ if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
113
+ return NextResponse.json(
114
+ {
115
+ error: true,
116
+ msg: "you are not allowed to request " + targetPath,
117
+ },
118
+ {
119
+ status: 403,
120
+ },
121
+ );
122
+ }
123
+
124
+ const targetUrl = targetPath;
125
+
126
+ const method = proxy_method || req.method;
127
+ const shouldNotHaveBody = ["get", "head"].includes(
128
+ method?.toLowerCase() ?? "",
129
+ );
130
+
131
+ const fetchOptions: RequestInit = {
132
+ headers: {
133
+ authorization: req.headers.get("authorization") ?? "",
134
+ },
135
+ body: shouldNotHaveBody ? null : req.body,
136
+ redirect: "manual",
137
+ method,
138
+ // @ts-ignore
139
+ duplex: "half",
140
+ };
141
+
142
+ let fetchResult;
143
+
144
+ try {
145
+ fetchResult = await fetch(targetUrl, fetchOptions);
146
+ } finally {
147
+ console.log(
148
+ "[Any Proxy]",
149
+ targetUrl,
150
+ {
151
+ method: method,
152
+ },
153
+ {
154
+ status: fetchResult?.status,
155
+ statusText: fetchResult?.statusText,
156
+ },
157
+ );
158
+ }
159
+
160
+ return fetchResult;
161
+ }
162
+
163
+ export const PUT = handle;
164
+ export const GET = handle;
165
+ export const OPTIONS = handle;
166
+
167
+ export const runtime = "edge";
app/api/xai.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getServerSideConfig } from "@/app/config/server";
2
+ import {
3
+ XAI_BASE_URL,
4
+ ApiPath,
5
+ ModelProvider,
6
+ ServiceProvider,
7
+ } from "@/app/constant";
8
+ import { prettyObject } from "@/app/utils/format";
9
+ import { NextRequest, NextResponse } from "next/server";
10
+ import { auth } from "@/app/api/auth";
11
+ import { isModelNotavailableInServer } from "@/app/utils/model";
12
+
13
+ const serverConfig = getServerSideConfig();
14
+
15
+ export async function handle(
16
+ req: NextRequest,
17
+ { params }: { params: { path: string[] } },
18
+ ) {
19
+ console.log("[XAI Route] params ", params);
20
+
21
+ if (req.method === "OPTIONS") {
22
+ return NextResponse.json({ body: "OK" }, { status: 200 });
23
+ }
24
+
25
+ const authResult = auth(req, ModelProvider.XAI);
26
+ if (authResult.error) {
27
+ return NextResponse.json(authResult, {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ try {
33
+ const response = await request(req);
34
+ return response;
35
+ } catch (e) {
36
+ console.error("[XAI] ", e);
37
+ return NextResponse.json(prettyObject(e));
38
+ }
39
+ }
40
+
41
+ async function request(req: NextRequest) {
42
+ const controller = new AbortController();
43
+
44
+ // alibaba use base url or just remove the path
45
+ let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
46
+
47
+ let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
48
+
49
+ if (!baseUrl.startsWith("http")) {
50
+ baseUrl = `https://${baseUrl}`;
51
+ }
52
+
53
+ if (baseUrl.endsWith("/")) {
54
+ baseUrl = baseUrl.slice(0, -1);
55
+ }
56
+
57
+ console.log("[Proxy] ", path);
58
+ console.log("[Base Url]", baseUrl);
59
+
60
+ const timeoutId = setTimeout(
61
+ () => {
62
+ controller.abort();
63
+ },
64
+ 10 * 60 * 1000,
65
+ );
66
+
67
+ const fetchUrl = `${baseUrl}${path}`;
68
+ const fetchOptions: RequestInit = {
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: req.headers.get("Authorization") ?? "",
72
+ },
73
+ method: req.method,
74
+ body: req.body,
75
+ redirect: "manual",
76
+ // @ts-ignore
77
+ duplex: "half",
78
+ signal: controller.signal,
79
+ };
80
+
81
+ // #1815 try to refuse some request to some models
82
+ if (serverConfig.customModels && req.body) {
83
+ try {
84
+ const clonedBody = await req.text();
85
+ fetchOptions.body = clonedBody;
86
+
87
+ const jsonBody = JSON.parse(clonedBody) as { model?: string };
88
+
89
+ // not undefined and is false
90
+ if (
91
+ isModelNotavailableInServer(
92
+ serverConfig.customModels,
93
+ jsonBody?.model as string,
94
+ ServiceProvider.XAI as string,
95
+ )
96
+ ) {
97
+ return NextResponse.json(
98
+ {
99
+ error: true,
100
+ message: `you are not allowed to use ${jsonBody?.model} model`,
101
+ },
102
+ {
103
+ status: 403,
104
+ },
105
+ );
106
+ }
107
+ } catch (e) {
108
+ console.error(`[XAI] filter`, e);
109
+ }
110
+ }
111
+ try {
112
+ const res = await fetch(fetchUrl, fetchOptions);
113
+
114
+ // to prevent browser prompt for credentials
115
+ const newHeaders = new Headers(res.headers);
116
+ newHeaders.delete("www-authenticate");
117
+ // to disable nginx buffering
118
+ newHeaders.set("X-Accel-Buffering", "no");
119
+
120
+ return new Response(res.body, {
121
+ status: res.status,
122
+ statusText: res.statusText,
123
+ headers: newHeaders,
124
+ });
125
+ } finally {
126
+ clearTimeout(timeoutId);
127
+ }
128
+ }
app/client/api.ts ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getClientConfig } from "../config/client";
2
+ import {
3
+ ACCESS_CODE_PREFIX,
4
+ ModelProvider,
5
+ ServiceProvider,
6
+ } from "../constant";
7
+ import {
8
+ ChatMessageTool,
9
+ ChatMessage,
10
+ ModelType,
11
+ useAccessStore,
12
+ useChatStore,
13
+ } from "../store";
14
+ import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
15
+ import { GeminiProApi } from "./platforms/google";
16
+ import { ClaudeApi } from "./platforms/anthropic";
17
+ import { ErnieApi } from "./platforms/baidu";
18
+ import { DoubaoApi } from "./platforms/bytedance";
19
+ import { QwenApi } from "./platforms/alibaba";
20
+ import { HunyuanApi } from "./platforms/tencent";
21
+ import { MoonshotApi } from "./platforms/moonshot";
22
+ import { SparkApi } from "./platforms/iflytek";
23
+ import { DeepSeekApi } from "./platforms/deepseek";
24
+ import { XAIApi } from "./platforms/xai";
25
+ import { ChatGLMApi } from "./platforms/glm";
26
+ import { SiliconflowApi } from "./platforms/siliconflow";
27
+
28
+ export const ROLES = ["system", "user", "assistant"] as const;
29
+ export type MessageRole = (typeof ROLES)[number];
30
+
31
+ export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
32
+ export const TTSModels = ["tts-1", "tts-1-hd"] as const;
33
+ export type ChatModel = ModelType;
34
+
35
+ export interface MultimodalContent {
36
+ type: "text" | "image_url";
37
+ text?: string;
38
+ image_url?: {
39
+ url: string;
40
+ };
41
+ }
42
+
43
+ export interface RequestMessage {
44
+ role: MessageRole;
45
+ content: string | MultimodalContent[];
46
+ }
47
+
48
+ export interface LLMConfig {
49
+ model: string;
50
+ providerName?: string;
51
+ temperature?: number;
52
+ top_p?: number;
53
+ stream?: boolean;
54
+ presence_penalty?: number;
55
+ frequency_penalty?: number;
56
+ size?: DalleRequestPayload["size"];
57
+ quality?: DalleRequestPayload["quality"];
58
+ style?: DalleRequestPayload["style"];
59
+ }
60
+
61
+ export interface SpeechOptions {
62
+ model: string;
63
+ input: string;
64
+ voice: string;
65
+ response_format?: string;
66
+ speed?: number;
67
+ onController?: (controller: AbortController) => void;
68
+ }
69
+
70
+ export interface ChatOptions {
71
+ messages: RequestMessage[];
72
+ config: LLMConfig;
73
+
74
+ onUpdate?: (message: string, chunk: string) => void;
75
+ onFinish: (message: string, responseRes: Response) => void;
76
+ onError?: (err: Error) => void;
77
+ onController?: (controller: AbortController) => void;
78
+ onBeforeTool?: (tool: ChatMessageTool) => void;
79
+ onAfterTool?: (tool: ChatMessageTool) => void;
80
+ }
81
+
82
+ export interface LLMUsage {
83
+ used: number;
84
+ total: number;
85
+ }
86
+
87
+ export interface LLMModel {
88
+ name: string;
89
+ displayName?: string;
90
+ available: boolean;
91
+ provider: LLMModelProvider;
92
+ sorted: number;
93
+ }
94
+
95
+ export interface LLMModelProvider {
96
+ id: string;
97
+ providerName: string;
98
+ providerType: string;
99
+ sorted: number;
100
+ }
101
+
102
+ export abstract class LLMApi {
103
+ abstract chat(options: ChatOptions): Promise<void>;
104
+ abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
105
+ abstract usage(): Promise<LLMUsage>;
106
+ abstract models(): Promise<LLMModel[]>;
107
+ }
108
+
109
+ type ProviderName = "openai" | "azure" | "claude" | "palm";
110
+
111
+ interface Model {
112
+ name: string;
113
+ provider: ProviderName;
114
+ ctxlen: number;
115
+ }
116
+
117
+ interface ChatProvider {
118
+ name: ProviderName;
119
+ apiConfig: {
120
+ baseUrl: string;
121
+ apiKey: string;
122
+ summaryModel: Model;
123
+ };
124
+ models: Model[];
125
+
126
+ chat: () => void;
127
+ usage: () => void;
128
+ }
129
+
130
+ export class ClientApi {
131
+ public llm: LLMApi;
132
+
133
+ constructor(provider: ModelProvider = ModelProvider.GPT) {
134
+ switch (provider) {
135
+ case ModelProvider.GeminiPro:
136
+ this.llm = new GeminiProApi();
137
+ break;
138
+ case ModelProvider.Claude:
139
+ this.llm = new ClaudeApi();
140
+ break;
141
+ case ModelProvider.Ernie:
142
+ this.llm = new ErnieApi();
143
+ break;
144
+ case ModelProvider.Doubao:
145
+ this.llm = new DoubaoApi();
146
+ break;
147
+ case ModelProvider.Qwen:
148
+ this.llm = new QwenApi();
149
+ break;
150
+ case ModelProvider.Hunyuan:
151
+ this.llm = new HunyuanApi();
152
+ break;
153
+ case ModelProvider.Moonshot:
154
+ this.llm = new MoonshotApi();
155
+ break;
156
+ case ModelProvider.Iflytek:
157
+ this.llm = new SparkApi();
158
+ break;
159
+ case ModelProvider.DeepSeek:
160
+ this.llm = new DeepSeekApi();
161
+ break;
162
+ case ModelProvider.XAI:
163
+ this.llm = new XAIApi();
164
+ break;
165
+ case ModelProvider.ChatGLM:
166
+ this.llm = new ChatGLMApi();
167
+ break;
168
+ case ModelProvider.SiliconFlow:
169
+ this.llm = new SiliconflowApi();
170
+ break;
171
+ default:
172
+ this.llm = new ChatGPTApi();
173
+ }
174
+ }
175
+
176
+ config() {}
177
+
178
+ prompts() {}
179
+
180
+ masks() {}
181
+
182
+ async share(messages: ChatMessage[], avatarUrl: string | null = null) {
183
+ const msgs = messages
184
+ .map((m) => ({
185
+ from: m.role === "user" ? "human" : "gpt",
186
+ value: m.content,
187
+ }))
188
+ .concat([
189
+ {
190
+ from: "human",
191
+ value:
192
+ "Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web",
193
+ },
194
+ ]);
195
+ // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
196
+ // Please do not modify this message
197
+
198
+ console.log("[Share]", messages, msgs);
199
+ const clientConfig = getClientConfig();
200
+ const proxyUrl = "/sharegpt";
201
+ const rawUrl = "https://sharegpt.com/api/conversations";
202
+ const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
203
+ const res = await fetch(shareUrl, {
204
+ body: JSON.stringify({
205
+ avatarUrl,
206
+ items: msgs,
207
+ }),
208
+ headers: {
209
+ "Content-Type": "application/json",
210
+ },
211
+ method: "POST",
212
+ });
213
+
214
+ const resJson = await res.json();
215
+ console.log("[Share]", resJson);
216
+ if (resJson.id) {
217
+ return `https://shareg.pt/${resJson.id}`;
218
+ }
219
+ }
220
+ }
221
+
222
+ export function getBearerToken(
223
+ apiKey: string,
224
+ noBearer: boolean = false,
225
+ ): string {
226
+ return validString(apiKey)
227
+ ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
228
+ : "";
229
+ }
230
+
231
+ export function validString(x: string): boolean {
232
+ return x?.length > 0;
233
+ }
234
+
235
+ export function getHeaders(ignoreHeaders: boolean = false) {
236
+ const accessStore = useAccessStore.getState();
237
+ const chatStore = useChatStore.getState();
238
+ let headers: Record<string, string> = {};
239
+ if (!ignoreHeaders) {
240
+ headers = {
241
+ "Content-Type": "application/json",
242
+ Accept: "application/json",
243
+ };
244
+ }
245
+
246
+ const clientConfig = getClientConfig();
247
+
248
+ function getConfig() {
249
+ const modelConfig = chatStore.currentSession().mask.modelConfig;
250
+ const isGoogle = modelConfig.providerName === ServiceProvider.Google;
251
+ const isAzure = modelConfig.providerName === ServiceProvider.Azure;
252
+ const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
253
+ const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
254
+ const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
255
+ const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
256
+ const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
257
+ const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
258
+ const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek;
259
+ const isXAI = modelConfig.providerName === ServiceProvider.XAI;
260
+ const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
261
+ const isSiliconFlow =
262
+ modelConfig.providerName === ServiceProvider.SiliconFlow;
263
+ const isEnabledAccessControl = accessStore.enabledAccessControl();
264
+ const apiKey = isGoogle
265
+ ? accessStore.googleApiKey
266
+ : isAzure
267
+ ? accessStore.azureApiKey
268
+ : isAnthropic
269
+ ? accessStore.anthropicApiKey
270
+ : isByteDance
271
+ ? accessStore.bytedanceApiKey
272
+ : isAlibaba
273
+ ? accessStore.alibabaApiKey
274
+ : isMoonshot
275
+ ? accessStore.moonshotApiKey
276
+ : isXAI
277
+ ? accessStore.xaiApiKey
278
+ : isDeepSeek
279
+ ? accessStore.deepseekApiKey
280
+ : isChatGLM
281
+ ? accessStore.chatglmApiKey
282
+ : isSiliconFlow
283
+ ? accessStore.siliconflowApiKey
284
+ : isIflytek
285
+ ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
286
+ ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
287
+ : ""
288
+ : accessStore.openaiApiKey;
289
+ return {
290
+ isGoogle,
291
+ isAzure,
292
+ isAnthropic,
293
+ isBaidu,
294
+ isByteDance,
295
+ isAlibaba,
296
+ isMoonshot,
297
+ isIflytek,
298
+ isDeepSeek,
299
+ isXAI,
300
+ isChatGLM,
301
+ isSiliconFlow,
302
+ apiKey,
303
+ isEnabledAccessControl,
304
+ };
305
+ }
306
+
307
+ function getAuthHeader(): string {
308
+ return isAzure
309
+ ? "api-key"
310
+ : isAnthropic
311
+ ? "x-api-key"
312
+ : isGoogle
313
+ ? "x-goog-api-key"
314
+ : "Authorization";
315
+ }
316
+
317
+ const {
318
+ isGoogle,
319
+ isAzure,
320
+ isAnthropic,
321
+ isBaidu,
322
+ isByteDance,
323
+ isAlibaba,
324
+ isMoonshot,
325
+ isIflytek,
326
+ isDeepSeek,
327
+ isXAI,
328
+ isChatGLM,
329
+ isSiliconFlow,
330
+ apiKey,
331
+ isEnabledAccessControl,
332
+ } = getConfig();
333
+ // when using baidu api in app, not set auth header
334
+ if (isBaidu && clientConfig?.isApp) return headers;
335
+
336
+ const authHeader = getAuthHeader();
337
+
338
+ const bearerToken = getBearerToken(
339
+ apiKey,
340
+ isAzure || isAnthropic || isGoogle,
341
+ );
342
+
343
+ if (bearerToken) {
344
+ headers[authHeader] = bearerToken;
345
+ } else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
346
+ headers["Authorization"] = getBearerToken(
347
+ ACCESS_CODE_PREFIX + accessStore.accessCode,
348
+ );
349
+ }
350
+
351
+ return headers;
352
+ }
353
+
354
+ export function getClientApi(provider: ServiceProvider): ClientApi {
355
+ switch (provider) {
356
+ case ServiceProvider.Google:
357
+ return new ClientApi(ModelProvider.GeminiPro);
358
+ case ServiceProvider.Anthropic:
359
+ return new ClientApi(ModelProvider.Claude);
360
+ case ServiceProvider.Baidu:
361
+ return new ClientApi(ModelProvider.Ernie);
362
+ case ServiceProvider.ByteDance:
363
+ return new ClientApi(ModelProvider.Doubao);
364
+ case ServiceProvider.Alibaba:
365
+ return new ClientApi(ModelProvider.Qwen);
366
+ case ServiceProvider.Tencent:
367
+ return new ClientApi(ModelProvider.Hunyuan);
368
+ case ServiceProvider.Moonshot:
369
+ return new ClientApi(ModelProvider.Moonshot);
370
+ case ServiceProvider.Iflytek:
371
+ return new ClientApi(ModelProvider.Iflytek);
372
+ case ServiceProvider.DeepSeek:
373
+ return new ClientApi(ModelProvider.DeepSeek);
374
+ case ServiceProvider.XAI:
375
+ return new ClientApi(ModelProvider.XAI);
376
+ case ServiceProvider.ChatGLM:
377
+ return new ClientApi(ModelProvider.ChatGLM);
378
+ case ServiceProvider.SiliconFlow:
379
+ return new ClientApi(ModelProvider.SiliconFlow);
380
+ default:
381
+ return new ClientApi(ModelProvider.GPT);
382
+ }
383
+ }
app/client/controller.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // To store message streaming controller
2
+ export const ChatControllerPool = {
3
+ controllers: {} as Record<string, AbortController>,
4
+
5
+ addController(
6
+ sessionId: string,
7
+ messageId: string,
8
+ controller: AbortController,
9
+ ) {
10
+ const key = this.key(sessionId, messageId);
11
+ this.controllers[key] = controller;
12
+ return key;
13
+ },
14
+
15
+ stop(sessionId: string, messageId: string) {
16
+ const key = this.key(sessionId, messageId);
17
+ const controller = this.controllers[key];
18
+ controller?.abort();
19
+ },
20
+
21
+ stopAll() {
22
+ Object.values(this.controllers).forEach((v) => v.abort());
23
+ },
24
+
25
+ hasPending() {
26
+ return Object.values(this.controllers).length > 0;
27
+ },
28
+
29
+ remove(sessionId: string, messageId: string) {
30
+ const key = this.key(sessionId, messageId);
31
+ delete this.controllers[key];
32
+ },
33
+
34
+ key(sessionId: string, messageIndex: string) {
35
+ return `${sessionId},${messageIndex}`;
36
+ },
37
+ };
app/client/platforms/alibaba.ts ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ApiPath, Alibaba, ALIBABA_BASE_URL } from "@/app/constant";
3
+ import {
4
+ useAccessStore,
5
+ useAppConfig,
6
+ useChatStore,
7
+ ChatMessageTool,
8
+ usePluginStore,
9
+ } from "@/app/store";
10
+ import { streamWithThink } from "@/app/utils/chat";
11
+ import {
12
+ ChatOptions,
13
+ getHeaders,
14
+ LLMApi,
15
+ LLMModel,
16
+ SpeechOptions,
17
+ MultimodalContent,
18
+ } from "../api";
19
+ import { getClientConfig } from "@/app/config/client";
20
+ import {
21
+ getMessageTextContent,
22
+ getMessageTextContentWithoutThinking,
23
+ getTimeoutMSByModel,
24
+ } from "@/app/utils";
25
+ import { fetch } from "@/app/utils/stream";
26
+
27
+ export interface OpenAIListModelResponse {
28
+ object: string;
29
+ data: Array<{
30
+ id: string;
31
+ object: string;
32
+ root: string;
33
+ }>;
34
+ }
35
+
36
+ interface RequestInput {
37
+ messages: {
38
+ role: "system" | "user" | "assistant";
39
+ content: string | MultimodalContent[];
40
+ }[];
41
+ }
42
+ interface RequestParam {
43
+ result_format: string;
44
+ incremental_output?: boolean;
45
+ temperature: number;
46
+ repetition_penalty?: number;
47
+ top_p: number;
48
+ max_tokens?: number;
49
+ }
50
+ interface RequestPayload {
51
+ model: string;
52
+ input: RequestInput;
53
+ parameters: RequestParam;
54
+ }
55
+
56
+ export class QwenApi implements LLMApi {
57
+ path(path: string): string {
58
+ const accessStore = useAccessStore.getState();
59
+
60
+ let baseUrl = "";
61
+
62
+ if (accessStore.useCustomConfig) {
63
+ baseUrl = accessStore.alibabaUrl;
64
+ }
65
+
66
+ if (baseUrl.length === 0) {
67
+ const isApp = !!getClientConfig()?.isApp;
68
+ baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
69
+ }
70
+
71
+ if (baseUrl.endsWith("/")) {
72
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
73
+ }
74
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) {
75
+ baseUrl = "https://" + baseUrl;
76
+ }
77
+
78
+ console.log("[Proxy Endpoint] ", baseUrl, path);
79
+
80
+ return [baseUrl, path].join("/");
81
+ }
82
+
83
+ extractMessage(res: any) {
84
+ return res?.output?.choices?.at(0)?.message?.content ?? "";
85
+ }
86
+
87
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
88
+ throw new Error("Method not implemented.");
89
+ }
90
+
91
+ async chat(options: ChatOptions) {
92
+ const messages = options.messages.map((v) => ({
93
+ role: v.role,
94
+ content:
95
+ v.role === "assistant"
96
+ ? getMessageTextContentWithoutThinking(v)
97
+ : getMessageTextContent(v),
98
+ }));
99
+
100
+ const modelConfig = {
101
+ ...useAppConfig.getState().modelConfig,
102
+ ...useChatStore.getState().currentSession().mask.modelConfig,
103
+ ...{
104
+ model: options.config.model,
105
+ },
106
+ };
107
+
108
+ const shouldStream = !!options.config.stream;
109
+ const requestPayload: RequestPayload = {
110
+ model: modelConfig.model,
111
+ input: {
112
+ messages,
113
+ },
114
+ parameters: {
115
+ result_format: "message",
116
+ incremental_output: shouldStream,
117
+ temperature: modelConfig.temperature,
118
+ // max_tokens: modelConfig.max_tokens,
119
+ top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1
120
+ },
121
+ };
122
+
123
+ const controller = new AbortController();
124
+ options.onController?.(controller);
125
+
126
+ try {
127
+ const headers = {
128
+ ...getHeaders(),
129
+ "X-DashScope-SSE": shouldStream ? "enable" : "disable",
130
+ };
131
+
132
+ const chatPath = this.path(Alibaba.ChatPath);
133
+ const chatPayload = {
134
+ method: "POST",
135
+ body: JSON.stringify(requestPayload),
136
+ signal: controller.signal,
137
+ headers: headers,
138
+ };
139
+
140
+ // make a fetch request
141
+ const requestTimeoutId = setTimeout(
142
+ () => controller.abort(),
143
+ getTimeoutMSByModel(options.config.model),
144
+ );
145
+
146
+ if (shouldStream) {
147
+ const [tools, funcs] = usePluginStore
148
+ .getState()
149
+ .getAsTools(
150
+ useChatStore.getState().currentSession().mask?.plugin || [],
151
+ );
152
+ return streamWithThink(
153
+ chatPath,
154
+ requestPayload,
155
+ headers,
156
+ tools as any,
157
+ funcs,
158
+ controller,
159
+ // parseSSE
160
+ (text: string, runTools: ChatMessageTool[]) => {
161
+ // console.log("parseSSE", text, runTools);
162
+ const json = JSON.parse(text);
163
+ const choices = json.output.choices as Array<{
164
+ message: {
165
+ content: string | null;
166
+ tool_calls: ChatMessageTool[];
167
+ reasoning_content: string | null;
168
+ };
169
+ }>;
170
+
171
+ if (!choices?.length) return { isThinking: false, content: "" };
172
+
173
+ const tool_calls = choices[0]?.message?.tool_calls;
174
+ if (tool_calls?.length > 0) {
175
+ const index = tool_calls[0]?.index;
176
+ const id = tool_calls[0]?.id;
177
+ const args = tool_calls[0]?.function?.arguments;
178
+ if (id) {
179
+ runTools.push({
180
+ id,
181
+ type: tool_calls[0]?.type,
182
+ function: {
183
+ name: tool_calls[0]?.function?.name as string,
184
+ arguments: args,
185
+ },
186
+ });
187
+ } else {
188
+ // @ts-ignore
189
+ runTools[index]["function"]["arguments"] += args;
190
+ }
191
+ }
192
+
193
+ const reasoning = choices[0]?.message?.reasoning_content;
194
+ const content = choices[0]?.message?.content;
195
+
196
+ // Skip if both content and reasoning_content are empty or null
197
+ if (
198
+ (!reasoning || reasoning.length === 0) &&
199
+ (!content || content.length === 0)
200
+ ) {
201
+ return {
202
+ isThinking: false,
203
+ content: "",
204
+ };
205
+ }
206
+
207
+ if (reasoning && reasoning.length > 0) {
208
+ return {
209
+ isThinking: true,
210
+ content: reasoning,
211
+ };
212
+ } else if (content && content.length > 0) {
213
+ return {
214
+ isThinking: false,
215
+ content: content,
216
+ };
217
+ }
218
+
219
+ return {
220
+ isThinking: false,
221
+ content: "",
222
+ };
223
+ },
224
+ // processToolMessage, include tool_calls message and tool call results
225
+ (
226
+ requestPayload: RequestPayload,
227
+ toolCallMessage: any,
228
+ toolCallResult: any[],
229
+ ) => {
230
+ requestPayload?.input?.messages?.splice(
231
+ requestPayload?.input?.messages?.length,
232
+ 0,
233
+ toolCallMessage,
234
+ ...toolCallResult,
235
+ );
236
+ },
237
+ options,
238
+ );
239
+ } else {
240
+ const res = await fetch(chatPath, chatPayload);
241
+ clearTimeout(requestTimeoutId);
242
+
243
+ const resJson = await res.json();
244
+ const message = this.extractMessage(resJson);
245
+ options.onFinish(message, res);
246
+ }
247
+ } catch (e) {
248
+ console.log("[Request] failed to make a chat request", e);
249
+ options.onError?.(e as Error);
250
+ }
251
+ }
252
+ async usage() {
253
+ return {
254
+ used: 0,
255
+ total: 0,
256
+ };
257
+ }
258
+
259
+ async models(): Promise<LLMModel[]> {
260
+ return [];
261
+ }
262
+ }
263
+ export { Alibaba };
app/client/platforms/anthropic.ts ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Anthropic, ApiPath } from "@/app/constant";
2
+ import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
3
+ import {
4
+ useAccessStore,
5
+ useAppConfig,
6
+ useChatStore,
7
+ usePluginStore,
8
+ ChatMessageTool,
9
+ } from "@/app/store";
10
+ import { getClientConfig } from "@/app/config/client";
11
+ import { ANTHROPIC_BASE_URL } from "@/app/constant";
12
+ import { getMessageTextContent, isVisionModel } from "@/app/utils";
13
+ import { preProcessImageContent, stream } from "@/app/utils/chat";
14
+ import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
15
+ import { RequestPayload } from "./openai";
16
+ import { fetch } from "@/app/utils/stream";
17
+
18
+ export type MultiBlockContent = {
19
+ type: "image" | "text";
20
+ source?: {
21
+ type: string;
22
+ media_type: string;
23
+ data: string;
24
+ };
25
+ text?: string;
26
+ };
27
+
28
+ export type AnthropicMessage = {
29
+ role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
30
+ content: string | MultiBlockContent[];
31
+ };
32
+
33
+ export interface AnthropicChatRequest {
34
+ model: string; // The model that will complete your prompt.
35
+ messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
36
+ max_tokens: number; // The maximum number of tokens to generate before stopping.
37
+ stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
38
+ temperature?: number; // Amount of randomness injected into the response.
39
+ top_p?: number; // Use nucleus sampling.
40
+ top_k?: number; // Only sample from the top K options for each subsequent token.
41
+ metadata?: object; // An object describing metadata about the request.
42
+ stream?: boolean; // Whether to incrementally stream the response using server-sent events.
43
+ }
44
+
45
+ export interface ChatRequest {
46
+ model: string; // The model that will complete your prompt.
47
+ prompt: string; // The prompt that you want Claude to complete.
48
+ max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
49
+ stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
50
+ temperature?: number; // Amount of randomness injected into the response.
51
+ top_p?: number; // Use nucleus sampling.
52
+ top_k?: number; // Only sample from the top K options for each subsequent token.
53
+ metadata?: object; // An object describing metadata about the request.
54
+ stream?: boolean; // Whether to incrementally stream the response using server-sent events.
55
+ }
56
+
57
+ export interface ChatResponse {
58
+ completion: string;
59
+ stop_reason: "stop_sequence" | "max_tokens";
60
+ model: string;
61
+ }
62
+
63
+ export type ChatStreamResponse = ChatResponse & {
64
+ stop?: string;
65
+ log_id: string;
66
+ };
67
+
68
+ const ClaudeMapper = {
69
+ assistant: "assistant",
70
+ user: "user",
71
+ system: "user",
72
+ } as const;
73
+
74
+ const keys = ["claude-2, claude-instant-1"];
75
+
76
+ export class ClaudeApi implements LLMApi {
77
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
78
+ throw new Error("Method not implemented.");
79
+ }
80
+
81
+ extractMessage(res: any) {
82
+ console.log("[Response] claude response: ", res);
83
+
84
+ return res?.content?.[0]?.text;
85
+ }
86
+ async chat(options: ChatOptions): Promise<void> {
87
+ const visionModel = isVisionModel(options.config.model);
88
+
89
+ const accessStore = useAccessStore.getState();
90
+
91
+ const shouldStream = !!options.config.stream;
92
+
93
+ const modelConfig = {
94
+ ...useAppConfig.getState().modelConfig,
95
+ ...useChatStore.getState().currentSession().mask.modelConfig,
96
+ ...{
97
+ model: options.config.model,
98
+ },
99
+ };
100
+
101
+ // try get base64image from local cache image_url
102
+ const messages: ChatOptions["messages"] = [];
103
+ for (const v of options.messages) {
104
+ const content = await preProcessImageContent(v.content);
105
+ messages.push({ role: v.role, content });
106
+ }
107
+
108
+ const keys = ["system", "user"];
109
+
110
+ // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
111
+ for (let i = 0; i < messages.length - 1; i++) {
112
+ const message = messages[i];
113
+ const nextMessage = messages[i + 1];
114
+
115
+ if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
116
+ messages[i] = [
117
+ message,
118
+ {
119
+ role: "assistant",
120
+ content: ";",
121
+ },
122
+ ] as any;
123
+ }
124
+ }
125
+
126
+ const prompt = messages
127
+ .flat()
128
+ .filter((v) => {
129
+ if (!v.content) return false;
130
+ if (typeof v.content === "string" && !v.content.trim()) return false;
131
+ return true;
132
+ })
133
+ .map((v) => {
134
+ const { role, content } = v;
135
+ const insideRole = ClaudeMapper[role] ?? "user";
136
+
137
+ if (!visionModel || typeof content === "string") {
138
+ return {
139
+ role: insideRole,
140
+ content: getMessageTextContent(v),
141
+ };
142
+ }
143
+ return {
144
+ role: insideRole,
145
+ content: content
146
+ .filter((v) => v.image_url || v.text)
147
+ .map(({ type, text, image_url }) => {
148
+ if (type === "text") {
149
+ return {
150
+ type,
151
+ text: text!,
152
+ };
153
+ }
154
+ const { url = "" } = image_url || {};
155
+ const colonIndex = url.indexOf(":");
156
+ const semicolonIndex = url.indexOf(";");
157
+ const comma = url.indexOf(",");
158
+
159
+ const mimeType = url.slice(colonIndex + 1, semicolonIndex);
160
+ const encodeType = url.slice(semicolonIndex + 1, comma);
161
+ const data = url.slice(comma + 1);
162
+
163
+ return {
164
+ type: "image" as const,
165
+ source: {
166
+ type: encodeType,
167
+ media_type: mimeType,
168
+ data,
169
+ },
170
+ };
171
+ }),
172
+ };
173
+ });
174
+
175
+ if (prompt[0]?.role === "assistant") {
176
+ prompt.unshift({
177
+ role: "user",
178
+ content: ";",
179
+ });
180
+ }
181
+
182
+ const requestBody: AnthropicChatRequest = {
183
+ messages: prompt,
184
+ stream: shouldStream,
185
+
186
+ model: modelConfig.model,
187
+ max_tokens: modelConfig.max_tokens,
188
+ temperature: modelConfig.temperature,
189
+ top_p: modelConfig.top_p,
190
+ // top_k: modelConfig.top_k,
191
+ top_k: 5,
192
+ };
193
+
194
+ const path = this.path(Anthropic.ChatPath);
195
+
196
+ const controller = new AbortController();
197
+ options.onController?.(controller);
198
+
199
+ if (shouldStream) {
200
+ let index = -1;
201
+ const [tools, funcs] = usePluginStore
202
+ .getState()
203
+ .getAsTools(
204
+ useChatStore.getState().currentSession().mask?.plugin || [],
205
+ );
206
+ return stream(
207
+ path,
208
+ requestBody,
209
+ {
210
+ ...getHeaders(),
211
+ "anthropic-version": accessStore.anthropicApiVersion,
212
+ },
213
+ // @ts-ignore
214
+ tools.map((tool) => ({
215
+ name: tool?.function?.name,
216
+ description: tool?.function?.description,
217
+ input_schema: tool?.function?.parameters,
218
+ })),
219
+ funcs,
220
+ controller,
221
+ // parseSSE
222
+ (text: string, runTools: ChatMessageTool[]) => {
223
+ // console.log("parseSSE", text, runTools);
224
+ let chunkJson:
225
+ | undefined
226
+ | {
227
+ type: "content_block_delta" | "content_block_stop";
228
+ content_block?: {
229
+ type: "tool_use";
230
+ id: string;
231
+ name: string;
232
+ };
233
+ delta?: {
234
+ type: "text_delta" | "input_json_delta";
235
+ text?: string;
236
+ partial_json?: string;
237
+ };
238
+ index: number;
239
+ };
240
+ chunkJson = JSON.parse(text);
241
+
242
+ if (chunkJson?.content_block?.type == "tool_use") {
243
+ index += 1;
244
+ const id = chunkJson?.content_block.id;
245
+ const name = chunkJson?.content_block.name;
246
+ runTools.push({
247
+ id,
248
+ type: "function",
249
+ function: {
250
+ name,
251
+ arguments: "",
252
+ },
253
+ });
254
+ }
255
+ if (
256
+ chunkJson?.delta?.type == "input_json_delta" &&
257
+ chunkJson?.delta?.partial_json
258
+ ) {
259
+ // @ts-ignore
260
+ runTools[index]["function"]["arguments"] +=
261
+ chunkJson?.delta?.partial_json;
262
+ }
263
+ return chunkJson?.delta?.text;
264
+ },
265
+ // processToolMessage, include tool_calls message and tool call results
266
+ (
267
+ requestPayload: RequestPayload,
268
+ toolCallMessage: any,
269
+ toolCallResult: any[],
270
+ ) => {
271
+ // reset index value
272
+ index = -1;
273
+ // @ts-ignore
274
+ requestPayload?.messages?.splice(
275
+ // @ts-ignore
276
+ requestPayload?.messages?.length,
277
+ 0,
278
+ {
279
+ role: "assistant",
280
+ content: toolCallMessage.tool_calls.map(
281
+ (tool: ChatMessageTool) => ({
282
+ type: "tool_use",
283
+ id: tool.id,
284
+ name: tool?.function?.name,
285
+ input: tool?.function?.arguments
286
+ ? JSON.parse(tool?.function?.arguments)
287
+ : {},
288
+ }),
289
+ ),
290
+ },
291
+ // @ts-ignore
292
+ ...toolCallResult.map((result) => ({
293
+ role: "user",
294
+ content: [
295
+ {
296
+ type: "tool_result",
297
+ tool_use_id: result.tool_call_id,
298
+ content: result.content,
299
+ },
300
+ ],
301
+ })),
302
+ );
303
+ },
304
+ options,
305
+ );
306
+ } else {
307
+ const payload = {
308
+ method: "POST",
309
+ body: JSON.stringify(requestBody),
310
+ signal: controller.signal,
311
+ headers: {
312
+ ...getHeaders(), // get common headers
313
+ "anthropic-version": accessStore.anthropicApiVersion,
314
+ // do not send `anthropicApiKey` in browser!!!
315
+ // Authorization: getAuthKey(accessStore.anthropicApiKey),
316
+ },
317
+ };
318
+
319
+ try {
320
+ controller.signal.onabort = () =>
321
+ options.onFinish("", new Response(null, { status: 400 }));
322
+
323
+ const res = await fetch(path, payload);
324
+ const resJson = await res.json();
325
+
326
+ const message = this.extractMessage(resJson);
327
+ options.onFinish(message, res);
328
+ } catch (e) {
329
+ console.error("failed to chat", e);
330
+ options.onError?.(e as Error);
331
+ }
332
+ }
333
+ }
334
+ async usage() {
335
+ return {
336
+ used: 0,
337
+ total: 0,
338
+ };
339
+ }
340
+ async models() {
341
+ // const provider = {
342
+ // id: "anthropic",
343
+ // providerName: "Anthropic",
344
+ // providerType: "anthropic",
345
+ // };
346
+
347
+ return [
348
+ // {
349
+ // name: "claude-instant-1.2",
350
+ // available: true,
351
+ // provider,
352
+ // },
353
+ // {
354
+ // name: "claude-2.0",
355
+ // available: true,
356
+ // provider,
357
+ // },
358
+ // {
359
+ // name: "claude-2.1",
360
+ // available: true,
361
+ // provider,
362
+ // },
363
+ // {
364
+ // name: "claude-3-opus-20240229",
365
+ // available: true,
366
+ // provider,
367
+ // },
368
+ // {
369
+ // name: "claude-3-sonnet-20240229",
370
+ // available: true,
371
+ // provider,
372
+ // },
373
+ // {
374
+ // name: "claude-3-haiku-20240307",
375
+ // available: true,
376
+ // provider,
377
+ // },
378
+ ];
379
+ }
380
+ path(path: string): string {
381
+ const accessStore = useAccessStore.getState();
382
+
383
+ let baseUrl: string = "";
384
+
385
+ if (accessStore.useCustomConfig) {
386
+ baseUrl = accessStore.anthropicUrl;
387
+ }
388
+
389
+ // if endpoint is empty, use default endpoint
390
+ if (baseUrl.trim().length === 0) {
391
+ const isApp = !!getClientConfig()?.isApp;
392
+
393
+ baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
394
+ }
395
+
396
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
397
+ baseUrl = "https://" + baseUrl;
398
+ }
399
+
400
+ baseUrl = trimEnd(baseUrl, "/");
401
+
402
+ // try rebuild url, when using cloudflare ai gateway in client
403
+ return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
404
+ }
405
+ }
406
+
407
+ function trimEnd(s: string, end = " ") {
408
+ if (end.length === 0) return s;
409
+
410
+ while (s.endsWith(end)) {
411
+ s = s.slice(0, -end.length);
412
+ }
413
+
414
+ return s;
415
+ }
app/client/platforms/baidu.ts ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ApiPath, Baidu, BAIDU_BASE_URL } from "@/app/constant";
3
+ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
4
+ import { getAccessToken } from "@/app/utils/baidu";
5
+
6
+ import {
7
+ ChatOptions,
8
+ getHeaders,
9
+ LLMApi,
10
+ LLMModel,
11
+ MultimodalContent,
12
+ SpeechOptions,
13
+ } from "../api";
14
+ import Locale from "../../locales";
15
+ import {
16
+ EventStreamContentType,
17
+ fetchEventSource,
18
+ } from "@fortaine/fetch-event-source";
19
+ import { prettyObject } from "@/app/utils/format";
20
+ import { getClientConfig } from "@/app/config/client";
21
+ import { getMessageTextContent, getTimeoutMSByModel } from "@/app/utils";
22
+ import { fetch } from "@/app/utils/stream";
23
+
24
+ export interface OpenAIListModelResponse {
25
+ object: string;
26
+ data: Array<{
27
+ id: string;
28
+ object: string;
29
+ root: string;
30
+ }>;
31
+ }
32
+
33
+ interface RequestPayload {
34
+ messages: {
35
+ role: "system" | "user" | "assistant";
36
+ content: string | MultimodalContent[];
37
+ }[];
38
+ stream?: boolean;
39
+ model: string;
40
+ temperature: number;
41
+ presence_penalty: number;
42
+ frequency_penalty: number;
43
+ top_p: number;
44
+ max_tokens?: number;
45
+ }
46
+
47
+ export class ErnieApi implements LLMApi {
48
+ path(path: string): string {
49
+ const accessStore = useAccessStore.getState();
50
+
51
+ let baseUrl = "";
52
+
53
+ if (accessStore.useCustomConfig) {
54
+ baseUrl = accessStore.baiduUrl;
55
+ }
56
+
57
+ if (baseUrl.length === 0) {
58
+ const isApp = !!getClientConfig()?.isApp;
59
+ // do not use proxy for baidubce api
60
+ baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
61
+ }
62
+
63
+ if (baseUrl.endsWith("/")) {
64
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
65
+ }
66
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) {
67
+ baseUrl = "https://" + baseUrl;
68
+ }
69
+
70
+ console.log("[Proxy Endpoint] ", baseUrl, path);
71
+
72
+ return [baseUrl, path].join("/");
73
+ }
74
+
75
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
76
+ throw new Error("Method not implemented.");
77
+ }
78
+
79
+ async chat(options: ChatOptions) {
80
+ const messages = options.messages.map((v) => ({
81
+ // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
82
+ role: v.role === "system" ? "user" : v.role,
83
+ content: getMessageTextContent(v),
84
+ }));
85
+
86
+ // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
87
+ if (messages.length % 2 === 0) {
88
+ if (messages.at(0)?.role === "user") {
89
+ messages.splice(1, 0, {
90
+ role: "assistant",
91
+ content: " ",
92
+ });
93
+ } else {
94
+ messages.unshift({
95
+ role: "user",
96
+ content: " ",
97
+ });
98
+ }
99
+ }
100
+
101
+ const modelConfig = {
102
+ ...useAppConfig.getState().modelConfig,
103
+ ...useChatStore.getState().currentSession().mask.modelConfig,
104
+ ...{
105
+ model: options.config.model,
106
+ },
107
+ };
108
+
109
+ const shouldStream = !!options.config.stream;
110
+ const requestPayload: RequestPayload = {
111
+ messages,
112
+ stream: shouldStream,
113
+ model: modelConfig.model,
114
+ temperature: modelConfig.temperature,
115
+ presence_penalty: modelConfig.presence_penalty,
116
+ frequency_penalty: modelConfig.frequency_penalty,
117
+ top_p: modelConfig.top_p,
118
+ };
119
+
120
+ console.log("[Request] Baidu payload: ", requestPayload);
121
+
122
+ const controller = new AbortController();
123
+ options.onController?.(controller);
124
+
125
+ try {
126
+ let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
127
+
128
+ // getAccessToken can not run in browser, because cors error
129
+ if (!!getClientConfig()?.isApp) {
130
+ const accessStore = useAccessStore.getState();
131
+ if (accessStore.useCustomConfig) {
132
+ if (accessStore.isValidBaidu()) {
133
+ const { access_token } = await getAccessToken(
134
+ accessStore.baiduApiKey,
135
+ accessStore.baiduSecretKey,
136
+ );
137
+ chatPath = `${chatPath}${
138
+ chatPath.includes("?") ? "&" : "?"
139
+ }access_token=${access_token}`;
140
+ }
141
+ }
142
+ }
143
+ const chatPayload = {
144
+ method: "POST",
145
+ body: JSON.stringify(requestPayload),
146
+ signal: controller.signal,
147
+ headers: getHeaders(),
148
+ };
149
+
150
+ // make a fetch request
151
+ const requestTimeoutId = setTimeout(
152
+ () => controller.abort(),
153
+ getTimeoutMSByModel(options.config.model),
154
+ );
155
+
156
+ if (shouldStream) {
157
+ let responseText = "";
158
+ let remainText = "";
159
+ let finished = false;
160
+ let responseRes: Response;
161
+
162
+ // animate response to make it looks smooth
163
+ function animateResponseText() {
164
+ if (finished || controller.signal.aborted) {
165
+ responseText += remainText;
166
+ console.log("[Response Animation] finished");
167
+ if (responseText?.length === 0) {
168
+ options.onError?.(new Error("empty response from server"));
169
+ }
170
+ return;
171
+ }
172
+
173
+ if (remainText.length > 0) {
174
+ const fetchCount = Math.max(1, Math.round(remainText.length / 60));
175
+ const fetchText = remainText.slice(0, fetchCount);
176
+ responseText += fetchText;
177
+ remainText = remainText.slice(fetchCount);
178
+ options.onUpdate?.(responseText, fetchText);
179
+ }
180
+
181
+ requestAnimationFrame(animateResponseText);
182
+ }
183
+
184
+ // start animaion
185
+ animateResponseText();
186
+
187
+ const finish = () => {
188
+ if (!finished) {
189
+ finished = true;
190
+ options.onFinish(responseText + remainText, responseRes);
191
+ }
192
+ };
193
+
194
+ controller.signal.onabort = finish;
195
+
196
+ fetchEventSource(chatPath, {
197
+ fetch: fetch as any,
198
+ ...chatPayload,
199
+ async onopen(res) {
200
+ clearTimeout(requestTimeoutId);
201
+ const contentType = res.headers.get("content-type");
202
+ console.log("[Baidu] request response content type: ", contentType);
203
+ responseRes = res;
204
+ if (contentType?.startsWith("text/plain")) {
205
+ responseText = await res.clone().text();
206
+ return finish();
207
+ }
208
+
209
+ if (
210
+ !res.ok ||
211
+ !res.headers
212
+ .get("content-type")
213
+ ?.startsWith(EventStreamContentType) ||
214
+ res.status !== 200
215
+ ) {
216
+ const responseTexts = [responseText];
217
+ let extraInfo = await res.clone().text();
218
+ try {
219
+ const resJson = await res.clone().json();
220
+ extraInfo = prettyObject(resJson);
221
+ } catch {}
222
+
223
+ if (res.status === 401) {
224
+ responseTexts.push(Locale.Error.Unauthorized);
225
+ }
226
+
227
+ if (extraInfo) {
228
+ responseTexts.push(extraInfo);
229
+ }
230
+
231
+ responseText = responseTexts.join("\n\n");
232
+
233
+ return finish();
234
+ }
235
+ },
236
+ onmessage(msg) {
237
+ if (msg.data === "[DONE]" || finished) {
238
+ return finish();
239
+ }
240
+ const text = msg.data;
241
+ try {
242
+ const json = JSON.parse(text);
243
+ const delta = json?.result;
244
+ if (delta) {
245
+ remainText += delta;
246
+ }
247
+ } catch (e) {
248
+ console.error("[Request] parse error", text, msg);
249
+ }
250
+ },
251
+ onclose() {
252
+ finish();
253
+ },
254
+ onerror(e) {
255
+ options.onError?.(e);
256
+ throw e;
257
+ },
258
+ openWhenHidden: true,
259
+ });
260
+ } else {
261
+ const res = await fetch(chatPath, chatPayload);
262
+ clearTimeout(requestTimeoutId);
263
+
264
+ const resJson = await res.json();
265
+ const message = resJson?.result;
266
+ options.onFinish(message, res);
267
+ }
268
+ } catch (e) {
269
+ console.log("[Request] failed to make a chat request", e);
270
+ options.onError?.(e as Error);
271
+ }
272
+ }
273
+ async usage() {
274
+ return {
275
+ used: 0,
276
+ total: 0,
277
+ };
278
+ }
279
+
280
+ async models(): Promise<LLMModel[]> {
281
+ return [];
282
+ }
283
+ }
284
+ export { Baidu };
app/client/platforms/bytedance.ts ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ApiPath, ByteDance, BYTEDANCE_BASE_URL } from "@/app/constant";
3
+ import {
4
+ useAccessStore,
5
+ useAppConfig,
6
+ useChatStore,
7
+ ChatMessageTool,
8
+ usePluginStore,
9
+ } from "@/app/store";
10
+
11
+ import {
12
+ ChatOptions,
13
+ getHeaders,
14
+ LLMApi,
15
+ LLMModel,
16
+ MultimodalContent,
17
+ SpeechOptions,
18
+ } from "../api";
19
+
20
+ import { streamWithThink } from "@/app/utils/chat";
21
+ import { getClientConfig } from "@/app/config/client";
22
+ import { preProcessImageContent } from "@/app/utils/chat";
23
+ import {
24
+ getMessageTextContentWithoutThinking,
25
+ getTimeoutMSByModel,
26
+ } from "@/app/utils";
27
+ import { fetch } from "@/app/utils/stream";
28
+
29
+ export interface OpenAIListModelResponse {
30
+ object: string;
31
+ data: Array<{
32
+ id: string;
33
+ object: string;
34
+ root: string;
35
+ }>;
36
+ }
37
+
38
+ interface RequestPayloadForByteDance {
39
+ messages: {
40
+ role: "system" | "user" | "assistant";
41
+ content: string | MultimodalContent[];
42
+ }[];
43
+ stream?: boolean;
44
+ model: string;
45
+ temperature: number;
46
+ presence_penalty: number;
47
+ frequency_penalty: number;
48
+ top_p: number;
49
+ max_tokens?: number;
50
+ }
51
+
52
+ export class DoubaoApi implements LLMApi {
53
+ path(path: string): string {
54
+ const accessStore = useAccessStore.getState();
55
+
56
+ let baseUrl = "";
57
+
58
+ if (accessStore.useCustomConfig) {
59
+ baseUrl = accessStore.bytedanceUrl;
60
+ }
61
+
62
+ if (baseUrl.length === 0) {
63
+ const isApp = !!getClientConfig()?.isApp;
64
+ baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
65
+ }
66
+
67
+ if (baseUrl.endsWith("/")) {
68
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
69
+ }
70
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) {
71
+ baseUrl = "https://" + baseUrl;
72
+ }
73
+
74
+ console.log("[Proxy Endpoint] ", baseUrl, path);
75
+
76
+ return [baseUrl, path].join("/");
77
+ }
78
+
79
+ extractMessage(res: any) {
80
+ return res.choices?.at(0)?.message?.content ?? "";
81
+ }
82
+
83
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
84
+ throw new Error("Method not implemented.");
85
+ }
86
+
87
+ async chat(options: ChatOptions) {
88
+ const messages: ChatOptions["messages"] = [];
89
+ for (const v of options.messages) {
90
+ const content =
91
+ v.role === "assistant"
92
+ ? getMessageTextContentWithoutThinking(v)
93
+ : await preProcessImageContent(v.content);
94
+ messages.push({ role: v.role, content });
95
+ }
96
+
97
+ const modelConfig = {
98
+ ...useAppConfig.getState().modelConfig,
99
+ ...useChatStore.getState().currentSession().mask.modelConfig,
100
+ ...{
101
+ model: options.config.model,
102
+ },
103
+ };
104
+
105
+ const shouldStream = !!options.config.stream;
106
+ const requestPayload: RequestPayloadForByteDance = {
107
+ messages,
108
+ stream: shouldStream,
109
+ model: modelConfig.model,
110
+ temperature: modelConfig.temperature,
111
+ presence_penalty: modelConfig.presence_penalty,
112
+ frequency_penalty: modelConfig.frequency_penalty,
113
+ top_p: modelConfig.top_p,
114
+ };
115
+
116
+ const controller = new AbortController();
117
+ options.onController?.(controller);
118
+
119
+ try {
120
+ const chatPath = this.path(ByteDance.ChatPath);
121
+ const chatPayload = {
122
+ method: "POST",
123
+ body: JSON.stringify(requestPayload),
124
+ signal: controller.signal,
125
+ headers: getHeaders(),
126
+ };
127
+
128
+ // make a fetch request
129
+ const requestTimeoutId = setTimeout(
130
+ () => controller.abort(),
131
+ getTimeoutMSByModel(options.config.model),
132
+ );
133
+
134
+ if (shouldStream) {
135
+ const [tools, funcs] = usePluginStore
136
+ .getState()
137
+ .getAsTools(
138
+ useChatStore.getState().currentSession().mask?.plugin || [],
139
+ );
140
+ return streamWithThink(
141
+ chatPath,
142
+ requestPayload,
143
+ getHeaders(),
144
+ tools as any,
145
+ funcs,
146
+ controller,
147
+ // parseSSE
148
+ (text: string, runTools: ChatMessageTool[]) => {
149
+ // console.log("parseSSE", text, runTools);
150
+ const json = JSON.parse(text);
151
+ const choices = json.choices as Array<{
152
+ delta: {
153
+ content: string | null;
154
+ tool_calls: ChatMessageTool[];
155
+ reasoning_content: string | null;
156
+ };
157
+ }>;
158
+
159
+ if (!choices?.length) return { isThinking: false, content: "" };
160
+
161
+ const tool_calls = choices[0]?.delta?.tool_calls;
162
+ if (tool_calls?.length > 0) {
163
+ const index = tool_calls[0]?.index;
164
+ const id = tool_calls[0]?.id;
165
+ const args = tool_calls[0]?.function?.arguments;
166
+ if (id) {
167
+ runTools.push({
168
+ id,
169
+ type: tool_calls[0]?.type,
170
+ function: {
171
+ name: tool_calls[0]?.function?.name as string,
172
+ arguments: args,
173
+ },
174
+ });
175
+ } else {
176
+ // @ts-ignore
177
+ runTools[index]["function"]["arguments"] += args;
178
+ }
179
+ }
180
+ const reasoning = choices[0]?.delta?.reasoning_content;
181
+ const content = choices[0]?.delta?.content;
182
+
183
+ // Skip if both content and reasoning_content are empty or null
184
+ if (
185
+ (!reasoning || reasoning.length === 0) &&
186
+ (!content || content.length === 0)
187
+ ) {
188
+ return {
189
+ isThinking: false,
190
+ content: "",
191
+ };
192
+ }
193
+
194
+ if (reasoning && reasoning.length > 0) {
195
+ return {
196
+ isThinking: true,
197
+ content: reasoning,
198
+ };
199
+ } else if (content && content.length > 0) {
200
+ return {
201
+ isThinking: false,
202
+ content: content,
203
+ };
204
+ }
205
+
206
+ return {
207
+ isThinking: false,
208
+ content: "",
209
+ };
210
+ },
211
+ // processToolMessage, include tool_calls message and tool call results
212
+ (
213
+ requestPayload: RequestPayloadForByteDance,
214
+ toolCallMessage: any,
215
+ toolCallResult: any[],
216
+ ) => {
217
+ requestPayload?.messages?.splice(
218
+ requestPayload?.messages?.length,
219
+ 0,
220
+ toolCallMessage,
221
+ ...toolCallResult,
222
+ );
223
+ },
224
+ options,
225
+ );
226
+ } else {
227
+ const res = await fetch(chatPath, chatPayload);
228
+ clearTimeout(requestTimeoutId);
229
+
230
+ const resJson = await res.json();
231
+ const message = this.extractMessage(resJson);
232
+ options.onFinish(message, res);
233
+ }
234
+ } catch (e) {
235
+ console.log("[Request] failed to make a chat request", e);
236
+ options.onError?.(e as Error);
237
+ }
238
+ }
239
+ async usage() {
240
+ return {
241
+ used: 0,
242
+ total: 0,
243
+ };
244
+ }
245
+
246
+ async models(): Promise<LLMModel[]> {
247
+ return [];
248
+ }
249
+ }
250
+ export { ByteDance };
app/client/platforms/deepseek.ts ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ // azure and openai, using same models. so using same LLMApi.
3
+ import { ApiPath, DEEPSEEK_BASE_URL, DeepSeek } from "@/app/constant";
4
+ import {
5
+ useAccessStore,
6
+ useAppConfig,
7
+ useChatStore,
8
+ ChatMessageTool,
9
+ usePluginStore,
10
+ } from "@/app/store";
11
+ import { streamWithThink } from "@/app/utils/chat";
12
+ import {
13
+ ChatOptions,
14
+ getHeaders,
15
+ LLMApi,
16
+ LLMModel,
17
+ SpeechOptions,
18
+ } from "../api";
19
+ import { getClientConfig } from "@/app/config/client";
20
+ import {
21
+ getMessageTextContent,
22
+ getMessageTextContentWithoutThinking,
23
+ getTimeoutMSByModel,
24
+ } from "@/app/utils";
25
+ import { RequestPayload } from "./openai";
26
+ import { fetch } from "@/app/utils/stream";
27
+
28
+ export class DeepSeekApi implements LLMApi {
29
+ private disableListModels = true;
30
+
31
+ path(path: string): string {
32
+ const accessStore = useAccessStore.getState();
33
+
34
+ let baseUrl = "";
35
+
36
+ if (accessStore.useCustomConfig) {
37
+ baseUrl = accessStore.deepseekUrl;
38
+ }
39
+
40
+ if (baseUrl.length === 0) {
41
+ const isApp = !!getClientConfig()?.isApp;
42
+ const apiPath = ApiPath.DeepSeek;
43
+ baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath;
44
+ }
45
+
46
+ if (baseUrl.endsWith("/")) {
47
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
48
+ }
49
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) {
50
+ baseUrl = "https://" + baseUrl;
51
+ }
52
+
53
+ console.log("[Proxy Endpoint] ", baseUrl, path);
54
+
55
+ return [baseUrl, path].join("/");
56
+ }
57
+
58
+ extractMessage(res: any) {
59
+ return res.choices?.at(0)?.message?.content ?? "";
60
+ }
61
+
62
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
63
+ throw new Error("Method not implemented.");
64
+ }
65
+
66
+ async chat(options: ChatOptions) {
67
+ const messages: ChatOptions["messages"] = [];
68
+ for (const v of options.messages) {
69
+ if (v.role === "assistant") {
70
+ const content = getMessageTextContentWithoutThinking(v);
71
+ messages.push({ role: v.role, content });
72
+ } else {
73
+ const content = getMessageTextContent(v);
74
+ messages.push({ role: v.role, content });
75
+ }
76
+ }
77
+
78
+ const modelConfig = {
79
+ ...useAppConfig.getState().modelConfig,
80
+ ...useChatStore.getState().currentSession().mask.modelConfig,
81
+ ...{
82
+ model: options.config.model,
83
+ providerName: options.config.providerName,
84
+ },
85
+ };
86
+
87
+ const requestPayload: RequestPayload = {
88
+ messages,
89
+ stream: options.config.stream,
90
+ model: modelConfig.model,
91
+ temperature: modelConfig.temperature,
92
+ presence_penalty: modelConfig.presence_penalty,
93
+ frequency_penalty: modelConfig.frequency_penalty,
94
+ top_p: modelConfig.top_p,
95
+ // max_tokens: Math.max(modelConfig.max_tokens, 1024),
96
+ // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
97
+ };
98
+
99
+ console.log("[Request] openai payload: ", requestPayload);
100
+
101
+ const shouldStream = !!options.config.stream;
102
+ const controller = new AbortController();
103
+ options.onController?.(controller);
104
+
105
+ try {
106
+ const chatPath = this.path(DeepSeek.ChatPath);
107
+ const chatPayload = {
108
+ method: "POST",
109
+ body: JSON.stringify(requestPayload),
110
+ signal: controller.signal,
111
+ headers: getHeaders(),
112
+ };
113
+
114
+ // make a fetch request
115
+ const requestTimeoutId = setTimeout(
116
+ () => controller.abort(),
117
+ getTimeoutMSByModel(options.config.model),
118
+ );
119
+
120
+ if (shouldStream) {
121
+ const [tools, funcs] = usePluginStore
122
+ .getState()
123
+ .getAsTools(
124
+ useChatStore.getState().currentSession().mask?.plugin || [],
125
+ );
126
+ return streamWithThink(
127
+ chatPath,
128
+ requestPayload,
129
+ getHeaders(),
130
+ tools as any,
131
+ funcs,
132
+ controller,
133
+ // parseSSE
134
+ (text: string, runTools: ChatMessageTool[]) => {
135
+ // console.log("parseSSE", text, runTools);
136
+ const json = JSON.parse(text);
137
+ const choices = json.choices as Array<{
138
+ delta: {
139
+ content: string | null;
140
+ tool_calls: ChatMessageTool[];
141
+ reasoning_content: string | null;
142
+ };
143
+ }>;
144
+ const tool_calls = choices[0]?.delta?.tool_calls;
145
+ if (tool_calls?.length > 0) {
146
+ const index = tool_calls[0]?.index;
147
+ const id = tool_calls[0]?.id;
148
+ const args = tool_calls[0]?.function?.arguments;
149
+ if (id) {
150
+ runTools.push({
151
+ id,
152
+ type: tool_calls[0]?.type,
153
+ function: {
154
+ name: tool_calls[0]?.function?.name as string,
155
+ arguments: args,
156
+ },
157
+ });
158
+ } else {
159
+ // @ts-ignore
160
+ runTools[index]["function"]["arguments"] += args;
161
+ }
162
+ }
163
+ const reasoning = choices[0]?.delta?.reasoning_content;
164
+ const content = choices[0]?.delta?.content;
165
+
166
+ // Skip if both content and reasoning_content are empty or null
167
+ if (
168
+ (!reasoning || reasoning.length === 0) &&
169
+ (!content || content.length === 0)
170
+ ) {
171
+ return {
172
+ isThinking: false,
173
+ content: "",
174
+ };
175
+ }
176
+
177
+ if (reasoning && reasoning.length > 0) {
178
+ return {
179
+ isThinking: true,
180
+ content: reasoning,
181
+ };
182
+ } else if (content && content.length > 0) {
183
+ return {
184
+ isThinking: false,
185
+ content: content,
186
+ };
187
+ }
188
+
189
+ return {
190
+ isThinking: false,
191
+ content: "",
192
+ };
193
+ },
194
+ // processToolMessage, include tool_calls message and tool call results
195
+ (
196
+ requestPayload: RequestPayload,
197
+ toolCallMessage: any,
198
+ toolCallResult: any[],
199
+ ) => {
200
+ // @ts-ignore
201
+ requestPayload?.messages?.splice(
202
+ // @ts-ignore
203
+ requestPayload?.messages?.length,
204
+ 0,
205
+ toolCallMessage,
206
+ ...toolCallResult,
207
+ );
208
+ },
209
+ options,
210
+ );
211
+ } else {
212
+ const res = await fetch(chatPath, chatPayload);
213
+ clearTimeout(requestTimeoutId);
214
+
215
+ const resJson = await res.json();
216
+ const message = this.extractMessage(resJson);
217
+ options.onFinish(message, res);
218
+ }
219
+ } catch (e) {
220
+ console.log("[Request] failed to make a chat request", e);
221
+ options.onError?.(e as Error);
222
+ }
223
+ }
224
+ async usage() {
225
+ return {
226
+ used: 0,
227
+ total: 0,
228
+ };
229
+ }
230
+
231
+ async models(): Promise<LLMModel[]> {
232
+ return [];
233
+ }
234
+ }
app/client/platforms/glm.ts ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ApiPath, CHATGLM_BASE_URL, ChatGLM } from "@/app/constant";
3
+ import {
4
+ useAccessStore,
5
+ useAppConfig,
6
+ useChatStore,
7
+ ChatMessageTool,
8
+ usePluginStore,
9
+ } from "@/app/store";
10
+ import { stream } from "@/app/utils/chat";
11
+ import {
12
+ ChatOptions,
13
+ getHeaders,
14
+ LLMApi,
15
+ LLMModel,
16
+ SpeechOptions,
17
+ } from "../api";
18
+ import { getClientConfig } from "@/app/config/client";
19
+ import {
20
+ getMessageTextContent,
21
+ isVisionModel,
22
+ getTimeoutMSByModel,
23
+ } from "@/app/utils";
24
+ import { RequestPayload } from "./openai";
25
+ import { fetch } from "@/app/utils/stream";
26
+ import { preProcessImageContent } from "@/app/utils/chat";
27
+
28
+ interface BasePayload {
29
+ model: string;
30
+ }
31
+
32
+ interface ChatPayload extends BasePayload {
33
+ messages: ChatOptions["messages"];
34
+ stream?: boolean;
35
+ temperature?: number;
36
+ presence_penalty?: number;
37
+ frequency_penalty?: number;
38
+ top_p?: number;
39
+ }
40
+
41
+ interface ImageGenerationPayload extends BasePayload {
42
+ prompt: string;
43
+ size?: string;
44
+ user_id?: string;
45
+ }
46
+
47
+ interface VideoGenerationPayload extends BasePayload {
48
+ prompt: string;
49
+ duration?: number;
50
+ resolution?: string;
51
+ user_id?: string;
52
+ }
53
+
54
+ type ModelType = "chat" | "image" | "video";
55
+
56
+ export class ChatGLMApi implements LLMApi {
57
+ private disableListModels = true;
58
+
59
+ private getModelType(model: string): ModelType {
60
+ if (model.startsWith("cogview-")) return "image";
61
+ if (model.startsWith("cogvideo-")) return "video";
62
+ return "chat";
63
+ }
64
+
65
+ private getModelPath(type: ModelType): string {
66
+ switch (type) {
67
+ case "image":
68
+ return ChatGLM.ImagePath;
69
+ case "video":
70
+ return ChatGLM.VideoPath;
71
+ default:
72
+ return ChatGLM.ChatPath;
73
+ }
74
+ }
75
+
76
+ private createPayload(
77
+ messages: ChatOptions["messages"],
78
+ modelConfig: any,
79
+ options: ChatOptions,
80
+ ): BasePayload {
81
+ const modelType = this.getModelType(modelConfig.model);
82
+ const lastMessage = messages[messages.length - 1];
83
+ const prompt =
84
+ typeof lastMessage.content === "string"
85
+ ? lastMessage.content
86
+ : lastMessage.content.map((c) => c.text).join("\n");
87
+
88
+ switch (modelType) {
89
+ case "image":
90
+ return {
91
+ model: modelConfig.model,
92
+ prompt,
93
+ size: options.config.size,
94
+ } as ImageGenerationPayload;
95
+ default:
96
+ return {
97
+ messages,
98
+ stream: options.config.stream,
99
+ model: modelConfig.model,
100
+ temperature: modelConfig.temperature,
101
+ presence_penalty: modelConfig.presence_penalty,
102
+ frequency_penalty: modelConfig.frequency_penalty,
103
+ top_p: modelConfig.top_p,
104
+ } as ChatPayload;
105
+ }
106
+ }
107
+
108
+ private parseResponse(modelType: ModelType, json: any): string {
109
+ switch (modelType) {
110
+ case "image": {
111
+ const imageUrl = json.data?.[0]?.url;
112
+ return imageUrl ? `![Generated Image](${imageUrl})` : "";
113
+ }
114
+ case "video": {
115
+ const videoUrl = json.data?.[0]?.url;
116
+ return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
117
+ }
118
+ default:
119
+ return this.extractMessage(json);
120
+ }
121
+ }
122
+
123
+ path(path: string): string {
124
+ const accessStore = useAccessStore.getState();
125
+ let baseUrl = "";
126
+
127
+ if (accessStore.useCustomConfig) {
128
+ baseUrl = accessStore.chatglmUrl;
129
+ }
130
+
131
+ if (baseUrl.length === 0) {
132
+ const isApp = !!getClientConfig()?.isApp;
133
+ const apiPath = ApiPath.ChatGLM;
134
+ baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
135
+ }
136
+
137
+ if (baseUrl.endsWith("/")) {
138
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
139
+ }
140
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
141
+ baseUrl = "https://" + baseUrl;
142
+ }
143
+
144
+ console.log("[Proxy Endpoint] ", baseUrl, path);
145
+ return [baseUrl, path].join("/");
146
+ }
147
+
148
+ extractMessage(res: any) {
149
+ return res.choices?.at(0)?.message?.content ?? "";
150
+ }
151
+
152
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
153
+ throw new Error("Method not implemented.");
154
+ }
155
+
156
+ async chat(options: ChatOptions) {
157
+ const visionModel = isVisionModel(options.config.model);
158
+ const messages: ChatOptions["messages"] = [];
159
+ for (const v of options.messages) {
160
+ const content = visionModel
161
+ ? await preProcessImageContent(v.content)
162
+ : getMessageTextContent(v);
163
+ messages.push({ role: v.role, content });
164
+ }
165
+
166
+ const modelConfig = {
167
+ ...useAppConfig.getState().modelConfig,
168
+ ...useChatStore.getState().currentSession().mask.modelConfig,
169
+ ...{
170
+ model: options.config.model,
171
+ providerName: options.config.providerName,
172
+ },
173
+ };
174
+ const modelType = this.getModelType(modelConfig.model);
175
+ const requestPayload = this.createPayload(messages, modelConfig, options);
176
+ const path = this.path(this.getModelPath(modelType));
177
+
178
+ console.log(`[Request] glm ${modelType} payload: `, requestPayload);
179
+
180
+ const controller = new AbortController();
181
+ options.onController?.(controller);
182
+
183
+ try {
184
+ const chatPayload = {
185
+ method: "POST",
186
+ body: JSON.stringify(requestPayload),
187
+ signal: controller.signal,
188
+ headers: getHeaders(),
189
+ };
190
+
191
+ const requestTimeoutId = setTimeout(
192
+ () => controller.abort(),
193
+ getTimeoutMSByModel(options.config.model),
194
+ );
195
+
196
+ if (modelType === "image" || modelType === "video") {
197
+ const res = await fetch(path, chatPayload);
198
+ clearTimeout(requestTimeoutId);
199
+
200
+ const resJson = await res.json();
201
+ console.log(`[Response] glm ${modelType}:`, resJson);
202
+ const message = this.parseResponse(modelType, resJson);
203
+ options.onFinish(message, res);
204
+ return;
205
+ }
206
+
207
+ const shouldStream = !!options.config.stream;
208
+ if (shouldStream) {
209
+ const [tools, funcs] = usePluginStore
210
+ .getState()
211
+ .getAsTools(
212
+ useChatStore.getState().currentSession().mask?.plugin || [],
213
+ );
214
+ return stream(
215
+ path,
216
+ requestPayload,
217
+ getHeaders(),
218
+ tools as any,
219
+ funcs,
220
+ controller,
221
+ // parseSSE
222
+ (text: string, runTools: ChatMessageTool[]) => {
223
+ const json = JSON.parse(text);
224
+ const choices = json.choices as Array<{
225
+ delta: {
226
+ content: string;
227
+ tool_calls: ChatMessageTool[];
228
+ };
229
+ }>;
230
+ const tool_calls = choices[0]?.delta?.tool_calls;
231
+ if (tool_calls?.length > 0) {
232
+ const index = tool_calls[0]?.index;
233
+ const id = tool_calls[0]?.id;
234
+ const args = tool_calls[0]?.function?.arguments;
235
+ if (id) {
236
+ runTools.push({
237
+ id,
238
+ type: tool_calls[0]?.type,
239
+ function: {
240
+ name: tool_calls[0]?.function?.name as string,
241
+ arguments: args,
242
+ },
243
+ });
244
+ } else {
245
+ // @ts-ignore
246
+ runTools[index]["function"]["arguments"] += args;
247
+ }
248
+ }
249
+ return choices[0]?.delta?.content;
250
+ },
251
+ // processToolMessage
252
+ (
253
+ requestPayload: RequestPayload,
254
+ toolCallMessage: any,
255
+ toolCallResult: any[],
256
+ ) => {
257
+ // @ts-ignore
258
+ requestPayload?.messages?.splice(
259
+ // @ts-ignore
260
+ requestPayload?.messages?.length,
261
+ 0,
262
+ toolCallMessage,
263
+ ...toolCallResult,
264
+ );
265
+ },
266
+ options,
267
+ );
268
+ } else {
269
+ const res = await fetch(path, chatPayload);
270
+ clearTimeout(requestTimeoutId);
271
+
272
+ const resJson = await res.json();
273
+ const message = this.extractMessage(resJson);
274
+ options.onFinish(message, res);
275
+ }
276
+ } catch (e) {
277
+ console.log("[Request] failed to make a chat request", e);
278
+ options.onError?.(e as Error);
279
+ }
280
+ }
281
+
282
+ async usage() {
283
+ return {
284
+ used: 0,
285
+ total: 0,
286
+ };
287
+ }
288
+
289
+ async models(): Promise<LLMModel[]> {
290
+ return [];
291
+ }
292
+ }
app/client/platforms/google.ts ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiPath, Google } from "@/app/constant";
2
+ import {
3
+ ChatOptions,
4
+ getHeaders,
5
+ LLMApi,
6
+ LLMModel,
7
+ LLMUsage,
8
+ SpeechOptions,
9
+ } from "../api";
10
+ import {
11
+ useAccessStore,
12
+ useAppConfig,
13
+ useChatStore,
14
+ usePluginStore,
15
+ ChatMessageTool,
16
+ } from "@/app/store";
17
+ import { stream } from "@/app/utils/chat";
18
+ import { getClientConfig } from "@/app/config/client";
19
+ import { GEMINI_BASE_URL } from "@/app/constant";
20
+
21
+ import {
22
+ getMessageTextContent,
23
+ getMessageImages,
24
+ isVisionModel,
25
+ getTimeoutMSByModel,
26
+ } from "@/app/utils";
27
+ import { preProcessImageContent } from "@/app/utils/chat";
28
+ import { nanoid } from "nanoid";
29
+ import { RequestPayload } from "./openai";
30
+ import { fetch } from "@/app/utils/stream";
31
+
32
+ export class GeminiProApi implements LLMApi {
33
+ path(path: string, shouldStream = false): string {
34
+ const accessStore = useAccessStore.getState();
35
+
36
+ let baseUrl = "";
37
+ if (accessStore.useCustomConfig) {
38
+ baseUrl = accessStore.googleUrl;
39
+ }
40
+
41
+ const isApp = !!getClientConfig()?.isApp;
42
+ if (baseUrl.length === 0) {
43
+ baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
44
+ }
45
+ if (baseUrl.endsWith("/")) {
46
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
47
+ }
48
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
49
+ baseUrl = "https://" + baseUrl;
50
+ }
51
+
52
+ console.log("[Proxy Endpoint] ", baseUrl, path);
53
+
54
+ let chatPath = [baseUrl, path].join("/");
55
+ if (shouldStream) {
56
+ chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
57
+ }
58
+
59
+ return chatPath;
60
+ }
61
+ extractMessage(res: any) {
62
+ console.log("[Response] gemini-pro response: ", res);
63
+
64
+ const getTextFromParts = (parts: any[]) => {
65
+ if (!Array.isArray(parts)) return "";
66
+
67
+ return parts
68
+ .map((part) => part?.text || "")
69
+ .filter((text) => text.trim() !== "")
70
+ .join("\n\n");
71
+ };
72
+
73
+ let content = "";
74
+ if (Array.isArray(res)) {
75
+ res.map((item) => {
76
+ content += getTextFromParts(item?.candidates?.at(0)?.content?.parts);
77
+ });
78
+ }
79
+
80
+ return (
81
+ getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
82
+ content || //getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
83
+ res?.error?.message ||
84
+ ""
85
+ );
86
+ }
87
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
88
+ throw new Error("Method not implemented.");
89
+ }
90
+
91
+ async chat(options: ChatOptions): Promise<void> {
92
+ const apiClient = this;
93
+ let multimodal = false;
94
+
95
+ // try get base64image from local cache image_url
96
+ const _messages: ChatOptions["messages"] = [];
97
+ for (const v of options.messages) {
98
+ const content = await preProcessImageContent(v.content);
99
+ _messages.push({ role: v.role, content });
100
+ }
101
+ const messages = _messages.map((v) => {
102
+ let parts: any[] = [{ text: getMessageTextContent(v) }];
103
+ if (isVisionModel(options.config.model)) {
104
+ const images = getMessageImages(v);
105
+ if (images.length > 0) {
106
+ multimodal = true;
107
+ parts = parts.concat(
108
+ images.map((image) => {
109
+ const imageType = image.split(";")[0].split(":")[1];
110
+ const imageData = image.split(",")[1];
111
+ return {
112
+ inline_data: {
113
+ mime_type: imageType,
114
+ data: imageData,
115
+ },
116
+ };
117
+ }),
118
+ );
119
+ }
120
+ }
121
+ return {
122
+ role: v.role.replace("assistant", "model").replace("system", "user"),
123
+ parts: parts,
124
+ };
125
+ });
126
+
127
+ // google requires that role in neighboring messages must not be the same
128
+ for (let i = 0; i < messages.length - 1; ) {
129
+ // Check if current and next item both have the role "model"
130
+ if (messages[i].role === messages[i + 1].role) {
131
+ // Concatenate the 'parts' of the current and next item
132
+ messages[i].parts = messages[i].parts.concat(messages[i + 1].parts);
133
+ // Remove the next item
134
+ messages.splice(i + 1, 1);
135
+ } else {
136
+ // Move to the next item
137
+ i++;
138
+ }
139
+ }
140
+ // if (visionModel && messages.length > 1) {
141
+ // options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
142
+ // }
143
+
144
+ const accessStore = useAccessStore.getState();
145
+
146
+ const modelConfig = {
147
+ ...useAppConfig.getState().modelConfig,
148
+ ...useChatStore.getState().currentSession().mask.modelConfig,
149
+ ...{
150
+ model: options.config.model,
151
+ },
152
+ };
153
+ const requestPayload = {
154
+ contents: messages,
155
+ generationConfig: {
156
+ // stopSequences: [
157
+ // "Title"
158
+ // ],
159
+ temperature: modelConfig.temperature,
160
+ maxOutputTokens: modelConfig.max_tokens,
161
+ topP: modelConfig.top_p,
162
+ // "topK": modelConfig.top_k,
163
+ },
164
+ safetySettings: [
165
+ {
166
+ category: "HARM_CATEGORY_HARASSMENT",
167
+ threshold: accessStore.googleSafetySettings,
168
+ },
169
+ {
170
+ category: "HARM_CATEGORY_HATE_SPEECH",
171
+ threshold: accessStore.googleSafetySettings,
172
+ },
173
+ {
174
+ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
175
+ threshold: accessStore.googleSafetySettings,
176
+ },
177
+ {
178
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
179
+ threshold: accessStore.googleSafetySettings,
180
+ },
181
+ ],
182
+ };
183
+
184
+ let shouldStream = !!options.config.stream;
185
+ const controller = new AbortController();
186
+ options.onController?.(controller);
187
+ try {
188
+ // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
189
+ const chatPath = this.path(
190
+ Google.ChatPath(modelConfig.model),
191
+ shouldStream,
192
+ );
193
+
194
+ const chatPayload = {
195
+ method: "POST",
196
+ body: JSON.stringify(requestPayload),
197
+ signal: controller.signal,
198
+ headers: getHeaders(),
199
+ };
200
+
201
+ const isThinking = options.config.model.includes("-thinking");
202
+ // make a fetch request
203
+ const requestTimeoutId = setTimeout(
204
+ () => controller.abort(),
205
+ getTimeoutMSByModel(options.config.model),
206
+ );
207
+
208
+ if (shouldStream) {
209
+ const [tools, funcs] = usePluginStore
210
+ .getState()
211
+ .getAsTools(
212
+ useChatStore.getState().currentSession().mask?.plugin || [],
213
+ );
214
+ return stream(
215
+ chatPath,
216
+ requestPayload,
217
+ getHeaders(),
218
+ // @ts-ignore
219
+ tools.length > 0
220
+ ? // @ts-ignore
221
+ [{ functionDeclarations: tools.map((tool) => tool.function) }]
222
+ : [],
223
+ funcs,
224
+ controller,
225
+ // parseSSE
226
+ (text: string, runTools: ChatMessageTool[]) => {
227
+ // console.log("parseSSE", text, runTools);
228
+ const chunkJson = JSON.parse(text);
229
+
230
+ const functionCall = chunkJson?.candidates
231
+ ?.at(0)
232
+ ?.content.parts.at(0)?.functionCall;
233
+ if (functionCall) {
234
+ const { name, args } = functionCall;
235
+ runTools.push({
236
+ id: nanoid(),
237
+ type: "function",
238
+ function: {
239
+ name,
240
+ arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
241
+ },
242
+ });
243
+ }
244
+ return chunkJson?.candidates
245
+ ?.at(0)
246
+ ?.content.parts?.map((part: { text: string }) => part.text)
247
+ .join("\n\n");
248
+ },
249
+ // processToolMessage, include tool_calls message and tool call results
250
+ (
251
+ requestPayload: RequestPayload,
252
+ toolCallMessage: any,
253
+ toolCallResult: any[],
254
+ ) => {
255
+ // @ts-ignore
256
+ requestPayload?.contents?.splice(
257
+ // @ts-ignore
258
+ requestPayload?.contents?.length,
259
+ 0,
260
+ {
261
+ role: "model",
262
+ parts: toolCallMessage.tool_calls.map(
263
+ (tool: ChatMessageTool) => ({
264
+ functionCall: {
265
+ name: tool?.function?.name,
266
+ args: JSON.parse(tool?.function?.arguments as string),
267
+ },
268
+ }),
269
+ ),
270
+ },
271
+ // @ts-ignore
272
+ ...toolCallResult.map((result) => ({
273
+ role: "function",
274
+ parts: [
275
+ {
276
+ functionResponse: {
277
+ name: result.name,
278
+ response: {
279
+ name: result.name,
280
+ content: result.content, // TODO just text content...
281
+ },
282
+ },
283
+ },
284
+ ],
285
+ })),
286
+ );
287
+ },
288
+ options,
289
+ );
290
+ } else {
291
+ const res = await fetch(chatPath, chatPayload);
292
+ clearTimeout(requestTimeoutId);
293
+ const resJson = await res.json();
294
+ if (resJson?.promptFeedback?.blockReason) {
295
+ // being blocked
296
+ options.onError?.(
297
+ new Error(
298
+ "Message is being blocked for reason: " +
299
+ resJson.promptFeedback.blockReason,
300
+ ),
301
+ );
302
+ }
303
+ const message = apiClient.extractMessage(resJson);
304
+ options.onFinish(message, res);
305
+ }
306
+ } catch (e) {
307
+ console.log("[Request] failed to make a chat request", e);
308
+ options.onError?.(e as Error);
309
+ }
310
+ }
311
+ usage(): Promise<LLMUsage> {
312
+ throw new Error("Method not implemented.");
313
+ }
314
+ async models(): Promise<LLMModel[]> {
315
+ return [];
316
+ }
317
+ }
app/client/platforms/iflytek.ts ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import {
3
+ ApiPath,
4
+ IFLYTEK_BASE_URL,
5
+ Iflytek,
6
+ REQUEST_TIMEOUT_MS,
7
+ } from "@/app/constant";
8
+ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
9
+
10
+ import {
11
+ ChatOptions,
12
+ getHeaders,
13
+ LLMApi,
14
+ LLMModel,
15
+ SpeechOptions,
16
+ } from "../api";
17
+ import Locale from "../../locales";
18
+ import {
19
+ EventStreamContentType,
20
+ fetchEventSource,
21
+ } from "@fortaine/fetch-event-source";
22
+ import { prettyObject } from "@/app/utils/format";
23
+ import { getClientConfig } from "@/app/config/client";
24
+ import { getMessageTextContent } from "@/app/utils";
25
+ import { fetch } from "@/app/utils/stream";
26
+
27
+ import { RequestPayload } from "./openai";
28
+
29
+ export class SparkApi implements LLMApi {
30
+ private disableListModels = true;
31
+
32
+ path(path: string): string {
33
+ const accessStore = useAccessStore.getState();
34
+
35
+ let baseUrl = "";
36
+
37
+ if (accessStore.useCustomConfig) {
38
+ baseUrl = accessStore.iflytekUrl;
39
+ }
40
+
41
+ if (baseUrl.length === 0) {
42
+ const isApp = !!getClientConfig()?.isApp;
43
+ const apiPath = ApiPath.Iflytek;
44
+ baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
45
+ }
46
+
47
+ if (baseUrl.endsWith("/")) {
48
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
49
+ }
50
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
51
+ baseUrl = "https://" + baseUrl;
52
+ }
53
+
54
+ console.log("[Proxy Endpoint] ", baseUrl, path);
55
+
56
+ return [baseUrl, path].join("/");
57
+ }
58
+
59
+ extractMessage(res: any) {
60
+ return res.choices?.at(0)?.message?.content ?? "";
61
+ }
62
+
63
+ speech(options: SpeechOptions): Promise<ArrayBuffer> {
64
+ throw new Error("Method not implemented.");
65
+ }
66
+
67
+ async chat(options: ChatOptions) {
68
+ const messages: ChatOptions["messages"] = [];
69
+ for (const v of options.messages) {
70
+ const content = getMessageTextContent(v);
71
+ messages.push({ role: v.role, content });
72
+ }
73
+
74
+ const modelConfig = {
75
+ ...useAppConfig.getState().modelConfig,
76
+ ...useChatStore.getState().currentSession().mask.modelConfig,
77
+ ...{
78
+ model: options.config.model,
79
+ providerName: options.config.providerName,
80
+ },
81
+ };
82
+
83
+ const requestPayload: RequestPayload = {
84
+ messages,
85
+ stream: options.config.stream,
86
+ model: modelConfig.model,
87
+ temperature: modelConfig.temperature,
88
+ presence_penalty: modelConfig.presence_penalty,
89
+ frequency_penalty: modelConfig.frequency_penalty,
90
+ top_p: modelConfig.top_p,
91
+ // max_tokens: Math.max(modelConfig.max_tokens, 1024),
92
+ // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
93
+ };
94
+
95
+ console.log("[Request] Spark payload: ", requestPayload);
96
+
97
+ const shouldStream = !!options.config.stream;
98
+ const controller = new AbortController();
99
+ options.onController?.(controller);
100
+
101
+ try {
102
+ const chatPath = this.path(Iflytek.ChatPath);
103
+ const chatPayload = {
104
+ method: "POST",
105
+ body: JSON.stringify(requestPayload),
106
+ signal: controller.signal,
107
+ headers: getHeaders(),
108
+ };
109
+
110
+ // Make a fetch request
111
+ const requestTimeoutId = setTimeout(
112
+ () => controller.abort(),
113
+ REQUEST_TIMEOUT_MS,
114
+ );
115
+
116
+ if (shouldStream) {
117
+ let responseText = "";
118
+ let remainText = "";
119
+ let finished = false;
120
+ let responseRes: Response;
121
+
122
+ // Animate response text to make it look smooth
123
+ function animateResponseText() {
124
+ if (finished || controller.signal.aborted) {
125
+ responseText += remainText;
126
+ console.log("[Response Animation] finished");
127
+ return;
128
+ }
129
+
130
+ if (remainText.length > 0) {
131
+ const fetchCount = Math.max(1, Math.round(remainText.length / 60));
132
+ const fetchText = remainText.slice(0, fetchCount);
133
+ responseText += fetchText;
134
+ remainText = remainText.slice(fetchCount);
135
+ options.onUpdate?.(responseText, fetchText);
136
+ }
137
+
138
+ requestAnimationFrame(animateResponseText);
139
+ }
140
+
141
+ // Start animation
142
+ animateResponseText();
143
+
144
+ const finish = () => {
145
+ if (!finished) {
146
+ finished = true;
147
+ options.onFinish(responseText + remainText, responseRes);
148
+ }
149
+ };
150
+
151
+ controller.signal.onabort = finish;
152
+
153
+ fetchEventSource(chatPath, {
154
+ fetch: fetch as any,
155
+ ...chatPayload,
156
+ async onopen(res) {
157
+ clearTimeout(requestTimeoutId);
158
+ const contentType = res.headers.get("content-type");
159
+ console.log("[Spark] request response content type: ", contentType);
160
+ responseRes = res;
161
+ if (contentType?.startsWith("text/plain")) {
162
+ responseText = await res.clone().text();
163
+ return finish();
164
+ }
165
+
166
+ // Handle different error scenarios
167
+ if (
168
+ !res.ok ||
169
+ !res.headers
170
+ .get("content-type")
171
+ ?.startsWith(EventStreamContentType) ||
172
+ res.status !== 200
173
+ ) {
174
+ let extraInfo = await res.clone().text();
175
+ try {
176
+ const resJson = await res.clone().json();
177
+ extraInfo = prettyObject(resJson);
178
+ } catch {}
179
+
180
+ if (res.status === 401) {
181
+ extraInfo = Locale.Error.Unauthorized;
182
+ }
183
+
184
+ options.onError?.(
185
+ new Error(
186
+ `Request failed with status ${res.status}: ${extraInfo}`,
187
+ ),
188
+ );
189
+ return finish();
190
+ }
191
+ },
192
+ onmessage(msg) {
193
+ if (msg.data === "[DONE]" || finished) {
194
+ return finish();
195
+ }
196
+ const text = msg.data;
197
+ try {
198
+ const json = JSON.parse(text);
199
+ const choices = json.choices as Array<{
200
+ delta: { content: string };
201
+ }>;
202
+ const delta = choices[0]?.delta?.content;
203
+
204
+ if (delta) {
205
+ remainText += delta;
206
+ }
207
+ } catch (e) {
208
+ console.error("[Request] parse error", text);
209
+ options.onError?.(new Error(`Failed to parse response: ${text}`));
210
+ }
211
+ },
212
+ onclose() {
213
+ finish();
214
+ },
215
+ onerror(e) {
216
+ options.onError?.(e);
217
+ throw e;
218
+ },
219
+ openWhenHidden: true,
220
+ });
221
+ } else {
222
+ const res = await fetch(chatPath, chatPayload);
223
+ clearTimeout(requestTimeoutId);
224
+
225
+ if (!res.ok) {
226
+ const errorText = await res.text();
227
+ options.onError?.(
228
+ new Error(`Request failed with status ${res.status}: ${errorText}`),
229
+ );
230
+ return;
231
+ }
232
+
233
+ const resJson = await res.json();
234
+ const message = this.extractMessage(resJson);
235
+ options.onFinish(message, res);
236
+ }
237
+ } catch (e) {
238
+ console.log("[Request] failed to make a chat request", e);
239
+ options.onError?.(e as Error);
240
+ }
241
+ }
242
+
243
+ async usage() {
244
+ return {
245
+ used: 0,
246
+ total: 0,
247
+ };
248
+ }
249
+
250
+ async models(): Promise<LLMModel[]> {
251
+ return [];
252
+ }
253
+ }