@lobehub/chat 1.19.36 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/Dockerfile +38 -49
- package/Dockerfile.database +38 -53
- package/package.json +1 -1
- package/scripts/serverLauncher/startServer.js +181 -0
- package/src/app/(main)/settings/llm/ProviderList/providers.tsx +2 -0
- package/src/app/api/chat/agentRuntime.ts +7 -0
- package/src/config/llm.ts +8 -0
- package/src/config/modelProviders/hunyuan.ts +137 -0
- package/src/config/modelProviders/index.ts +4 -0
- package/src/const/settings/llm.ts +5 -0
- package/src/libs/agent-runtime/AgentRuntime.ts +7 -0
- package/src/libs/agent-runtime/hunyuan/index.test.ts +255 -0
- package/src/libs/agent-runtime/hunyuan/index.ts +10 -0
- package/src/libs/agent-runtime/types/type.ts +1 -0
- package/src/server/globalConfig/index.ts +12 -0
- package/src/types/user/settings/keyVaults.ts +1 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,48 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.20.1](https://github.com/lobehub/lobe-chat/compare/v1.20.0...v1.20.1)
|
6
|
+
|
7
|
+
<sup>Released on **2024-09-27**</sup>
|
8
|
+
|
9
|
+
<br/>
|
10
|
+
|
11
|
+
<details>
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
13
|
+
|
14
|
+
</details>
|
15
|
+
|
16
|
+
<div align="right">
|
17
|
+
|
18
|
+
[](#readme-top)
|
19
|
+
|
20
|
+
</div>
|
21
|
+
|
22
|
+
## [Version 1.20.0](https://github.com/lobehub/lobe-chat/compare/v1.19.36...v1.20.0)
|
23
|
+
|
24
|
+
<sup>Released on **2024-09-27**</sup>
|
25
|
+
|
26
|
+
#### ✨ Features
|
27
|
+
|
28
|
+
- **misc**: Add Hunyuan(Tencent) model provider.
|
29
|
+
|
30
|
+
<br/>
|
31
|
+
|
32
|
+
<details>
|
33
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
34
|
+
|
35
|
+
#### What's improved
|
36
|
+
|
37
|
+
- **misc**: Add Hunyuan(Tencent) model provider, closes [#4147](https://github.com/lobehub/lobe-chat/issues/4147) ([8ddb41b](https://github.com/lobehub/lobe-chat/commit/8ddb41b))
|
38
|
+
|
39
|
+
</details>
|
40
|
+
|
41
|
+
<div align="right">
|
42
|
+
|
43
|
+
[](#readme-top)
|
44
|
+
|
45
|
+
</div>
|
46
|
+
|
5
47
|
### [Version 1.19.36](https://github.com/lobehub/lobe-chat/compare/v1.19.35...v1.19.36)
|
6
48
|
|
7
49
|
<sup>Released on **2024-09-27**</sup>
|
package/Dockerfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
## Base image for all
|
1
|
+
## Base image for all building stages
|
2
2
|
FROM node:20-slim AS base
|
3
3
|
|
4
4
|
ARG USE_CN_MIRROR
|
@@ -10,19 +10,22 @@ RUN \
|
|
10
10
|
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
|
11
11
|
sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \
|
12
12
|
fi \
|
13
|
-
# Add required package
|
13
|
+
# Add required package
|
14
14
|
&& apt update \
|
15
|
-
&& apt install
|
16
|
-
|
17
|
-
&&
|
18
|
-
|
19
|
-
|
20
|
-
&&
|
21
|
-
|
22
|
-
&&
|
23
|
-
|
24
|
-
|
25
|
-
&&
|
15
|
+
&& apt install ca-certificates proxychains-ng -qy \
|
16
|
+
# Prepare required package to distroless
|
17
|
+
&& mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \
|
18
|
+
# Copy proxychains to distroless
|
19
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \
|
20
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \
|
21
|
+
&& cp /usr/bin/proxychains4 /distroless/bin/proxychains \
|
22
|
+
&& cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \
|
23
|
+
# Copy node to distroless
|
24
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \
|
25
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \
|
26
|
+
&& cp /usr/local/bin/node /distroless/bin/node \
|
27
|
+
# Copy CA certificates to distroless
|
28
|
+
&& cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \
|
26
29
|
# Cleanup temp files
|
27
30
|
&& rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/*
|
28
31
|
|
@@ -80,7 +83,9 @@ COPY . .
|
|
80
83
|
RUN npm run build:docker
|
81
84
|
|
82
85
|
## Application image, copy all the files for production
|
83
|
-
FROM
|
86
|
+
FROM busybox:latest AS app
|
87
|
+
|
88
|
+
COPY --from=base /distroless/ /
|
84
89
|
|
85
90
|
COPY --from=builder /app/public /app/public
|
86
91
|
|
@@ -90,13 +95,25 @@ COPY --from=builder /app/.next/standalone /app/
|
|
90
95
|
COPY --from=builder /app/.next/static /app/.next/static
|
91
96
|
COPY --from=builder /deps/node_modules/.pnpm /app/node_modules/.pnpm
|
92
97
|
|
98
|
+
# Copy server launcher
|
99
|
+
COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js
|
100
|
+
|
101
|
+
RUN \
|
102
|
+
# Add nextjs:nodejs to run the app
|
103
|
+
addgroup -S -g 1001 nodejs \
|
104
|
+
&& adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \
|
105
|
+
# Set permission for nextjs:nodejs
|
106
|
+
&& chown -R nextjs:nodejs /app /etc/proxychains4.conf
|
107
|
+
|
93
108
|
## Production image, copy all the files and run next
|
94
|
-
FROM
|
109
|
+
FROM scratch
|
95
110
|
|
96
111
|
# Copy all the files from app, set the correct permission for prerender cache
|
97
|
-
COPY --from=app
|
112
|
+
COPY --from=app / /
|
98
113
|
|
99
114
|
ENV NODE_ENV="production" \
|
115
|
+
NODE_OPTIONS="--use-openssl-ca" \
|
116
|
+
NODE_EXTRA_CA_CERTS="/etc/ssl/certs/ca-certificates.crt"
|
100
117
|
NODE_TLS_REJECT_UNAUTHORIZED=""
|
101
118
|
|
102
119
|
# set hostname to localhost
|
@@ -135,6 +152,8 @@ ENV \
|
|
135
152
|
GOOGLE_API_KEY="" GOOGLE_PROXY_URL="" \
|
136
153
|
# Groq
|
137
154
|
GROQ_API_KEY="" GROQ_MODEL_LIST="" GROQ_PROXY_URL="" \
|
155
|
+
# Hunyuan
|
156
|
+
HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \
|
138
157
|
# Minimax
|
139
158
|
MINIMAX_API_KEY="" \
|
140
159
|
# Mistral
|
@@ -174,36 +193,6 @@ USER nextjs
|
|
174
193
|
|
175
194
|
EXPOSE 3210/tcp
|
176
195
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
IP_REGEX="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$"; \
|
181
|
-
# Set proxychains command
|
182
|
-
PROXYCHAINS="proxychains -q"; \
|
183
|
-
# Parse the proxy URL
|
184
|
-
host_with_port="${PROXY_URL#*//}"; \
|
185
|
-
host="${host_with_port%%:*}"; \
|
186
|
-
port="${PROXY_URL##*:}"; \
|
187
|
-
protocol="${PROXY_URL%%://*}"; \
|
188
|
-
# Resolve to IP address if the host is a domain
|
189
|
-
if ! [[ "$host" =~ "$IP_REGEX" ]]; then \
|
190
|
-
nslookup=$(nslookup -q="A" "$host" | tail -n +3 | grep 'Address:'); \
|
191
|
-
if [ -n "$nslookup" ]; then \
|
192
|
-
host=$(echo "$nslookup" | tail -n 1 | awk '{print $2}'); \
|
193
|
-
fi; \
|
194
|
-
fi; \
|
195
|
-
# Generate proxychains configuration file
|
196
|
-
printf "%s\n" \
|
197
|
-
'localnet 127.0.0.0/255.0.0.0' \
|
198
|
-
'localnet ::1/128' \
|
199
|
-
'proxy_dns' \
|
200
|
-
'remote_dns_subnet 224' \
|
201
|
-
'strict_chain' \
|
202
|
-
'tcp_connect_time_out 8000' \
|
203
|
-
'tcp_read_time_out 15000' \
|
204
|
-
'[ProxyList]' \
|
205
|
-
"$protocol $host $port" \
|
206
|
-
> "/etc/proxychains4.conf"; \
|
207
|
-
fi; \
|
208
|
-
# Run the server
|
209
|
-
${PROXYCHAINS} node "/app/server.js";
|
196
|
+
ENTRYPOINT ["/bin/node"]
|
197
|
+
|
198
|
+
CMD ["/app/startServer.js"]
|
package/Dockerfile.database
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
## Base image for all
|
1
|
+
## Base image for all building stages
|
2
2
|
FROM node:20-slim AS base
|
3
3
|
|
4
4
|
ARG USE_CN_MIRROR
|
@@ -10,19 +10,22 @@ RUN \
|
|
10
10
|
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
|
11
11
|
sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \
|
12
12
|
fi \
|
13
|
-
# Add required package
|
13
|
+
# Add required package
|
14
14
|
&& apt update \
|
15
|
-
&& apt install
|
16
|
-
|
17
|
-
&&
|
18
|
-
|
19
|
-
|
20
|
-
&&
|
21
|
-
|
22
|
-
&&
|
23
|
-
|
24
|
-
|
25
|
-
&&
|
15
|
+
&& apt install ca-certificates proxychains-ng -qy \
|
16
|
+
# Prepare required package to distroless
|
17
|
+
&& mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \
|
18
|
+
# Copy proxychains to distroless
|
19
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \
|
20
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \
|
21
|
+
&& cp /usr/bin/proxychains4 /distroless/bin/proxychains \
|
22
|
+
&& cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \
|
23
|
+
# Copy node to distroless
|
24
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \
|
25
|
+
&& cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \
|
26
|
+
&& cp /usr/local/bin/node /distroless/bin/node \
|
27
|
+
# Copy CA certificates to distroless
|
28
|
+
&& cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \
|
26
29
|
# Cleanup temp files
|
27
30
|
&& rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/*
|
28
31
|
|
@@ -84,7 +87,9 @@ COPY . .
|
|
84
87
|
RUN npm run build:docker
|
85
88
|
|
86
89
|
## Application image, copy all the files for production
|
87
|
-
FROM
|
90
|
+
FROM busybox:latest AS app
|
91
|
+
|
92
|
+
COPY --from=base /distroless/ /
|
88
93
|
|
89
94
|
COPY --from=builder /app/public /app/public
|
90
95
|
|
@@ -103,13 +108,25 @@ COPY --from=builder /app/src/database/server/migrations /app/migrations
|
|
103
108
|
COPY --from=builder /app/scripts/migrateServerDB/docker.cjs /app/docker.cjs
|
104
109
|
COPY --from=builder /app/scripts/migrateServerDB/errorHint.js /app/errorHint.js
|
105
110
|
|
111
|
+
# Copy server launcher
|
112
|
+
COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js
|
113
|
+
|
114
|
+
RUN \
|
115
|
+
# Add nextjs:nodejs to run the app
|
116
|
+
addgroup -S -g 1001 nodejs \
|
117
|
+
&& adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \
|
118
|
+
# Set permission for nextjs:nodejs
|
119
|
+
&& chown -R nextjs:nodejs /app /etc/proxychains4.conf
|
120
|
+
|
106
121
|
## Production image, copy all the files and run next
|
107
|
-
FROM
|
122
|
+
FROM scratch
|
108
123
|
|
109
124
|
# Copy all the files from app, set the correct permission for prerender cache
|
110
|
-
COPY --from=app
|
125
|
+
COPY --from=app / /
|
111
126
|
|
112
127
|
ENV NODE_ENV="production" \
|
128
|
+
NODE_OPTIONS="--use-openssl-ca" \
|
129
|
+
NODE_EXTRA_CA_CERTS="/etc/ssl/certs/ca-certificates.crt"
|
113
130
|
NODE_TLS_REJECT_UNAUTHORIZED=""
|
114
131
|
|
115
132
|
# set hostname to localhost
|
@@ -167,6 +184,8 @@ ENV \
|
|
167
184
|
GOOGLE_API_KEY="" GOOGLE_PROXY_URL="" \
|
168
185
|
# Groq
|
169
186
|
GROQ_API_KEY="" GROQ_MODEL_LIST="" GROQ_PROXY_URL="" \
|
187
|
+
# Hunyuan
|
188
|
+
HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \
|
170
189
|
# Minimax
|
171
190
|
MINIMAX_API_KEY="" \
|
172
191
|
# Mistral
|
@@ -206,40 +225,6 @@ USER nextjs
|
|
206
225
|
|
207
226
|
EXPOSE 3210/tcp
|
208
227
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
IP_REGEX="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$"; \
|
213
|
-
# Set proxychains command
|
214
|
-
PROXYCHAINS="proxychains -q"; \
|
215
|
-
# Parse the proxy URL
|
216
|
-
host_with_port="${PROXY_URL#*//}"; \
|
217
|
-
host="${host_with_port%%:*}"; \
|
218
|
-
port="${PROXY_URL##*:}"; \
|
219
|
-
protocol="${PROXY_URL%%://*}"; \
|
220
|
-
# Resolve to IP address if the host is a domain
|
221
|
-
if ! [[ "$host" =~ "$IP_REGEX" ]]; then \
|
222
|
-
nslookup=$(nslookup -q="A" "$host" | tail -n +3 | grep 'Address:'); \
|
223
|
-
if [ -n "$nslookup" ]; then \
|
224
|
-
host=$(echo "$nslookup" | tail -n 1 | awk '{print $2}'); \
|
225
|
-
fi; \
|
226
|
-
fi; \
|
227
|
-
# Generate proxychains configuration file
|
228
|
-
printf "%s\n" \
|
229
|
-
'localnet 127.0.0.0/255.0.0.0' \
|
230
|
-
'localnet ::1/128' \
|
231
|
-
'proxy_dns' \
|
232
|
-
'remote_dns_subnet 224' \
|
233
|
-
'strict_chain' \
|
234
|
-
'tcp_connect_time_out 8000' \
|
235
|
-
'tcp_read_time_out 15000' \
|
236
|
-
'[ProxyList]' \
|
237
|
-
"$protocol $host $port" \
|
238
|
-
> "/etc/proxychains4.conf"; \
|
239
|
-
fi; \
|
240
|
-
# Run migration
|
241
|
-
node "/app/docker.cjs"; \
|
242
|
-
if [ "$?" -eq "0" ]; then \
|
243
|
-
# Run the server
|
244
|
-
${PROXYCHAINS} node "/app/server.js"; \
|
245
|
-
fi;
|
228
|
+
ENTRYPOINT ["/bin/node"]
|
229
|
+
|
230
|
+
CMD ["/app/startServer.js"]
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.20.1",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -0,0 +1,181 @@
|
|
1
|
+
const dns = require('dns').promises;
|
2
|
+
const fs = require('fs');
|
3
|
+
const tls = require('tls');
|
4
|
+
const { spawn } = require('child_process');
|
5
|
+
|
6
|
+
// Set file paths
|
7
|
+
const DB_MIGRATION_SCRIPT_PATH = '/app/docker.cjs';
|
8
|
+
const SERVER_SCRIPT_PATH = '/app/server.js';
|
9
|
+
const PROXYCHAINS_CONF_PATH = '/etc/proxychains4.conf';
|
10
|
+
|
11
|
+
// Function to check if a string is a valid IP address
|
12
|
+
const isValidIP = (ip, version = 4) => {
|
13
|
+
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
|
14
|
+
const ipv6Regex = /^(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
15
|
+
|
16
|
+
switch (version) {
|
17
|
+
case 4:
|
18
|
+
return ipv4Regex.test(ip);
|
19
|
+
case 6:
|
20
|
+
return ipv6Regex.test(ip);
|
21
|
+
default:
|
22
|
+
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
// Function to check TLS validity of a URL
|
27
|
+
const isValidTLS = (url = '') => {
|
28
|
+
if (!url) {
|
29
|
+
console.log('⚠️ TLS Check: No URL provided. Skipping TLS check. Ensure correct setting ENV.');
|
30
|
+
console.log('-------------------------------------');
|
31
|
+
return Promise.resolve();
|
32
|
+
}
|
33
|
+
|
34
|
+
const { protocol, host, port } = parseUrl(url);
|
35
|
+
if (protocol !== 'https') {
|
36
|
+
console.log(`⚠️ TLS Check: Non-HTTPS protocol (${protocol}). Skipping TLS check for ${url}.`);
|
37
|
+
console.log('-------------------------------------');
|
38
|
+
return Promise.resolve();
|
39
|
+
}
|
40
|
+
|
41
|
+
const options = { host, port, servername: host };
|
42
|
+
return new Promise((resolve, reject) => {
|
43
|
+
const socket = tls.connect(options, () => {
|
44
|
+
if (socket.authorized) {
|
45
|
+
console.log(`✅ TLS Check: Valid certificate for ${host}:${port}.`);
|
46
|
+
console.log('-------------------------------------');
|
47
|
+
resolve();
|
48
|
+
}
|
49
|
+
socket.end();
|
50
|
+
});
|
51
|
+
|
52
|
+
socket.on('error', (err) => {
|
53
|
+
const errMsg = `❌ TLS Check: Error for ${host}:${port}. Details:`;
|
54
|
+
switch (err.code) {
|
55
|
+
case 'CERT_HAS_EXPIRED':
|
56
|
+
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
57
|
+
console.error(`${errMsg} Certificate is not valid. Consider setting NODE_TLS_REJECT_UNAUTHORIZED="0" or mapping /etc/ssl/certs/ca-certificates.crt.`);
|
58
|
+
break;
|
59
|
+
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
|
60
|
+
console.error(`${errMsg} Unable to verify issuer. Ensure correct mapping of /etc/ssl/certs/ca-certificates.crt.`);
|
61
|
+
break;
|
62
|
+
default:
|
63
|
+
console.error(`${errMsg} Network issue. Check firewall or DNS.`);
|
64
|
+
break;
|
65
|
+
}
|
66
|
+
reject(err);
|
67
|
+
});
|
68
|
+
});
|
69
|
+
};
|
70
|
+
|
71
|
+
// Function to check TLS connections for OSS and Auth Issuer
|
72
|
+
const checkTLSConnections = async () => {
|
73
|
+
await Promise.all([
|
74
|
+
isValidTLS(process.env.S3_ENDPOINT),
|
75
|
+
isValidTLS(process.env.S3_PUBLIC_DOMAIN),
|
76
|
+
isValidTLS(getEnvVarsByKeyword('_ISSUER')),
|
77
|
+
]);
|
78
|
+
};
|
79
|
+
|
80
|
+
// Function to get environment variable by keyword
|
81
|
+
const getEnvVarsByKeyword = (keyword) => {
|
82
|
+
return Object.entries(process.env)
|
83
|
+
.filter(([key, value]) => key.includes(keyword) && value)
|
84
|
+
.map(([, value]) => value)[0] || null;
|
85
|
+
};
|
86
|
+
|
87
|
+
// Function to parse protocol, host and port from a URL
|
88
|
+
const parseUrl = (url) => {
|
89
|
+
const { protocol, hostname: host, port } = new URL(url);
|
90
|
+
return { protocol: protocol.replace(':', ''), host, port: port || 443 };
|
91
|
+
};
|
92
|
+
|
93
|
+
// Function to resolve host IP via DNS
|
94
|
+
const resolveHostIP = async (host, version = 4) => {
|
95
|
+
try {
|
96
|
+
const { address } = await dns.lookup(host, { family: version });
|
97
|
+
|
98
|
+
if (!isValidIP(address, version)) {
|
99
|
+
console.error(`❌ DNS Error: Invalid resolved IP: ${address}. IP address must be IPv${version}.`);
|
100
|
+
process.exit(1);
|
101
|
+
}
|
102
|
+
|
103
|
+
return address;
|
104
|
+
} catch (err) {
|
105
|
+
console.error(`❌ DNS Error: Could not resolve ${host}. Check DNS server.`, err);
|
106
|
+
process.exit(1);
|
107
|
+
}
|
108
|
+
};
|
109
|
+
|
110
|
+
// Function to generate proxychains configuration
|
111
|
+
const runProxyChainsConfGenerator = async (url) => {
|
112
|
+
const { protocol, host, port } = parseUrl(url);
|
113
|
+
|
114
|
+
if (!['http', 'socks4', 'socks5'].includes(protocol)) {
|
115
|
+
console.error(`❌ ProxyChains: Invalid protocol (${protocol}). Protocol must be 'http', 'socks4' and 'socks5'.`);
|
116
|
+
process.exit(1);
|
117
|
+
}
|
118
|
+
|
119
|
+
const validPort = parseInt(port, 10);
|
120
|
+
if (isNaN(validPort) || validPort <= 0 || validPort > 65535) {
|
121
|
+
console.error(`❌ ProxyChains: Invalid port (${port}). Port must be a number between 1 and 65535.`);
|
122
|
+
process.exit(1);
|
123
|
+
}
|
124
|
+
|
125
|
+
let ip = isValidIP(host, 4) ? host : await resolveHostIP(host, 4);
|
126
|
+
|
127
|
+
const configContent = `
|
128
|
+
localnet 127.0.0.0/255.0.0.0
|
129
|
+
localnet ::1/128
|
130
|
+
proxy_dns
|
131
|
+
remote_dns_subnet 224
|
132
|
+
strict_chain
|
133
|
+
tcp_connect_time_out 8000
|
134
|
+
tcp_read_time_out 15000
|
135
|
+
[ProxyList]
|
136
|
+
${protocol} ${ip} ${port}
|
137
|
+
`.trim();
|
138
|
+
|
139
|
+
fs.writeFileSync(PROXYCHAINS_CONF_PATH, configContent);
|
140
|
+
console.log(`✅ ProxyChains: All outgoing traffic routed via ${protocol}://${ip}:${port}.`);
|
141
|
+
console.log('-------------------------------------');
|
142
|
+
};
|
143
|
+
|
144
|
+
// Function to execute a script with child process spawn
|
145
|
+
const runScript = (scriptPath, useProxy = false) => {
|
146
|
+
const command = useProxy ? ['/bin/proxychains', '-q', '/bin/node', scriptPath] : ['/bin/node', scriptPath];
|
147
|
+
return new Promise((resolve, reject) => {
|
148
|
+
const process = spawn(command.shift(), command, { stdio: 'inherit' });
|
149
|
+
process.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`🔴 Process exited with code ${code}`))));
|
150
|
+
});
|
151
|
+
};
|
152
|
+
|
153
|
+
// Main function to run the server with optional proxy
|
154
|
+
const runServer = async () => {
|
155
|
+
const PROXY_URL = process.env.PROXY_URL || ''; // Default empty string to avoid undefined errors
|
156
|
+
|
157
|
+
if (PROXY_URL) {
|
158
|
+
await runProxyChainsConfGenerator(PROXY_URL);
|
159
|
+
return runScript(SERVER_SCRIPT_PATH, true);
|
160
|
+
}
|
161
|
+
return runScript(SERVER_SCRIPT_PATH);
|
162
|
+
};
|
163
|
+
|
164
|
+
// Main execution block
|
165
|
+
(async () => {
|
166
|
+
console.log('🌐 DNS Server:', dns.getServers());
|
167
|
+
console.log('-------------------------------------');
|
168
|
+
|
169
|
+
if (process.env.DATABASE_DRIVER) {
|
170
|
+
try {
|
171
|
+
await runScript(DB_MIGRATION_SCRIPT_PATH);
|
172
|
+
await checkTLSConnections();
|
173
|
+
} catch (err) {
|
174
|
+
console.error('❌ Error during DB migration or TLS connection check:', err);
|
175
|
+
process.exit(1);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
// Run the server in either database or non-database mode
|
180
|
+
await runServer();
|
181
|
+
})();
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
FireworksAIProviderCard,
|
10
10
|
GoogleProviderCard,
|
11
11
|
GroqProviderCard,
|
12
|
+
HunyuanProviderCard,
|
12
13
|
MinimaxProviderCard,
|
13
14
|
MistralProviderCard,
|
14
15
|
MoonshotProviderCard,
|
@@ -60,6 +61,7 @@ export const useProviderList = (): ProviderItem[] => {
|
|
60
61
|
Ai21ProviderCard,
|
61
62
|
UpstageProviderCard,
|
62
63
|
QwenProviderCard,
|
64
|
+
HunyuanProviderCard,
|
63
65
|
SparkProviderCard,
|
64
66
|
ZhiPuProviderCard,
|
65
67
|
ZeroOneProviderCard,
|
@@ -244,6 +244,13 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
|
|
244
244
|
|
245
245
|
const apiKey = apiKeyManager.pick(payload?.apiKey || AI21_API_KEY);
|
246
246
|
|
247
|
+
return { apiKey };
|
248
|
+
}
|
249
|
+
case ModelProvider.Hunyuan: {
|
250
|
+
const { HUNYUAN_API_KEY } = getLLMConfig();
|
251
|
+
|
252
|
+
const apiKey = apiKeyManager.pick(payload?.apiKey || HUNYUAN_API_KEY);
|
253
|
+
|
247
254
|
return { apiKey };
|
248
255
|
}
|
249
256
|
}
|
package/src/config/llm.ts
CHANGED
@@ -117,6 +117,10 @@ export const getLLMConfig = () => {
|
|
117
117
|
|
118
118
|
ENABLED_AI21: z.boolean(),
|
119
119
|
AI21_API_KEY: z.string().optional(),
|
120
|
+
|
121
|
+
ENABLED_HUNYUAN: z.boolean(),
|
122
|
+
HUNYUAN_API_KEY: z.string().optional(),
|
123
|
+
HUNYUAN_MODEL_LIST: z.string().optional(),
|
120
124
|
},
|
121
125
|
runtimeEnv: {
|
122
126
|
API_KEY_SELECT_MODE: process.env.API_KEY_SELECT_MODE,
|
@@ -231,6 +235,10 @@ export const getLLMConfig = () => {
|
|
231
235
|
|
232
236
|
ENABLED_AI21: !!process.env.AI21_API_KEY,
|
233
237
|
AI21_API_KEY: process.env.AI21_API_KEY,
|
238
|
+
|
239
|
+
ENABLED_HUNYUAN: !!process.env.HUNYUAN_API_KEY,
|
240
|
+
HUNYUAN_API_KEY: process.env.HUNYUAN_API_KEY,
|
241
|
+
HUNYUAN_MODEL_LIST: process.env.HUNYUAN_MODEL_LIST,
|
234
242
|
},
|
235
243
|
});
|
236
244
|
};
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import { ModelProviderCard } from '@/types/llm';
|
2
|
+
|
3
|
+
// ref https://cloud.tencent.com/document/product/1729/104753
|
4
|
+
const Hunyuan: ModelProviderCard = {
|
5
|
+
chatModels: [
|
6
|
+
{
|
7
|
+
description: '升级为 MOE 结构,上下文窗口为 256k ,在 NLP,代码,数学,行业等多项评测集上领先众多开源模型。',
|
8
|
+
displayName: 'Hunyuan Lite',
|
9
|
+
enabled: true,
|
10
|
+
id: 'hunyuan-lite',
|
11
|
+
maxOutput: 6000,
|
12
|
+
pricing: {
|
13
|
+
currency: 'CNY',
|
14
|
+
input: 0,
|
15
|
+
output: 0,
|
16
|
+
},
|
17
|
+
tokens: 256_000,
|
18
|
+
},
|
19
|
+
{
|
20
|
+
description: '采用更优的路由策略,同时缓解了负载均衡和专家趋同的问题。长文方面,大海捞针指标达到99.9%。MOE-32K 性价比相对更高,在平衡效果、价格的同时,可对实现对长文本输入的处理。',
|
21
|
+
displayName: 'Hunyuan Standard',
|
22
|
+
enabled: true,
|
23
|
+
id: 'hunyuan-standard',
|
24
|
+
maxOutput: 2000,
|
25
|
+
pricing: {
|
26
|
+
currency: 'CNY',
|
27
|
+
input: 4.5,
|
28
|
+
output: 5,
|
29
|
+
},
|
30
|
+
tokens: 32_000,
|
31
|
+
},
|
32
|
+
{
|
33
|
+
description: '采用更优的路由策略,同时缓解了负载均衡和专家趋同的问题。长文方面,大海捞针指标达到99.9%。MOE-256K 在长度和效果上进一步突破,极大的扩展了可输入长度。',
|
34
|
+
displayName: 'Hunyuan Standard 256K',
|
35
|
+
enabled: true,
|
36
|
+
id: 'hunyuan-standard-256K',
|
37
|
+
maxOutput: 6000,
|
38
|
+
pricing: {
|
39
|
+
currency: 'CNY',
|
40
|
+
input: 15,
|
41
|
+
output: 60,
|
42
|
+
},
|
43
|
+
tokens: 256_000,
|
44
|
+
},
|
45
|
+
{
|
46
|
+
description: '混元全新一代大语言模型的预览版,采用全新的混合专家模型(MoE)结构,相比hunyuan-pro推理效率更快,效果表现更强。',
|
47
|
+
displayName: 'Hunyuan Turbo',
|
48
|
+
enabled: true,
|
49
|
+
functionCall: true,
|
50
|
+
id: 'hunyuan-turbo',
|
51
|
+
maxOutput: 4000,
|
52
|
+
pricing: {
|
53
|
+
currency: 'CNY',
|
54
|
+
input: 15,
|
55
|
+
output: 50,
|
56
|
+
},
|
57
|
+
tokens: 32_000,
|
58
|
+
},
|
59
|
+
{
|
60
|
+
description: '万亿级参数规模 MOE-32K 长文模型。在各种 benchmark 上达到绝对领先的水平,复杂指令和推理,具备复杂数学能力,支持 functioncall,在多语言翻译、金融法律医疗等领域应用重点优化。',
|
61
|
+
displayName: 'Hunyuan Pro',
|
62
|
+
enabled: true,
|
63
|
+
functionCall: true,
|
64
|
+
id: 'hunyuan-pro',
|
65
|
+
maxOutput: 4000,
|
66
|
+
pricing: {
|
67
|
+
currency: 'CNY',
|
68
|
+
input: 30,
|
69
|
+
output: 100,
|
70
|
+
},
|
71
|
+
tokens: 32_000,
|
72
|
+
},
|
73
|
+
{
|
74
|
+
description: '混元最新代码生成模型,经过 200B 高质量代码数据增训基座模型,迭代半年高质量 SFT 数据训练,上下文长窗口长度增大到 8K,五大语言代码生成自动评测指标上位居前列;五大语言10项考量各方面综合代码任务人工高质量评测上,性能处于第一梯队',
|
75
|
+
displayName: 'Hunyuan Code',
|
76
|
+
enabled: true,
|
77
|
+
id: 'hunyuan-code',
|
78
|
+
maxOutput: 4000,
|
79
|
+
pricing: {
|
80
|
+
currency: 'CNY',
|
81
|
+
input: 4,
|
82
|
+
output: 8,
|
83
|
+
},
|
84
|
+
tokens: 8000,
|
85
|
+
},
|
86
|
+
{
|
87
|
+
description: '混元最新多模态模型,支持图片+文本输入生成文本内容。',
|
88
|
+
displayName: 'Hunyuan Vision',
|
89
|
+
enabled: true,
|
90
|
+
id: 'hunyuan-vision',
|
91
|
+
maxOutput: 4000,
|
92
|
+
pricing: {
|
93
|
+
currency: 'CNY',
|
94
|
+
input: 18,
|
95
|
+
output: 18,
|
96
|
+
},
|
97
|
+
tokens: 8000,
|
98
|
+
vision: true,
|
99
|
+
},
|
100
|
+
{
|
101
|
+
description: '混元最新 MOE 架构 FunctionCall 模型,经过高质量的 FunctionCall 数据训练,上下文窗口达 32K,在多个维度的评测指标上处于领先。',
|
102
|
+
displayName: 'Hunyuan FunctionCall',
|
103
|
+
functionCall: true,
|
104
|
+
id: 'hunyuan-functioncall',
|
105
|
+
maxOutput: 4000,
|
106
|
+
pricing: {
|
107
|
+
currency: 'CNY',
|
108
|
+
input: 4,
|
109
|
+
output: 8,
|
110
|
+
},
|
111
|
+
tokens: 32_000,
|
112
|
+
},
|
113
|
+
{
|
114
|
+
description: '混元最新版角色扮演模型,混元官方精调训练推出的角色扮演模型,基于混元模型结合角色扮演场景数据集进行增训,在角色扮演场景具有更好的基础效果。',
|
115
|
+
displayName: 'Hunyuan Role',
|
116
|
+
id: 'hunyuan-role',
|
117
|
+
maxOutput: 4000,
|
118
|
+
pricing: {
|
119
|
+
currency: 'CNY',
|
120
|
+
input: 4,
|
121
|
+
output: 8,
|
122
|
+
},
|
123
|
+
tokens: 8000,
|
124
|
+
},
|
125
|
+
],
|
126
|
+
checkModel: 'hunyuan-lite',
|
127
|
+
description:
|
128
|
+
'由腾讯研发的大语言模型,具备强大的中文创作能力,复杂语境下的逻辑推理能力,以及可靠的任务执行能力',
|
129
|
+
disableBrowserRequest: true,
|
130
|
+
id: 'hunyuan',
|
131
|
+
modelList: { showModelFetcher: true },
|
132
|
+
modelsUrl: 'https://cloud.tencent.com/document/product/1729/104753',
|
133
|
+
name: 'Hunyuan',
|
134
|
+
url: 'https://hunyuan.tencent.com',
|
135
|
+
};
|
136
|
+
|
137
|
+
export default Hunyuan;
|
@@ -11,6 +11,7 @@ import FireworksAIProvider from './fireworksai';
|
|
11
11
|
import GithubProvider from './github';
|
12
12
|
import GoogleProvider from './google';
|
13
13
|
import GroqProvider from './groq';
|
14
|
+
import HunyuanProvider from './hunyuan';
|
14
15
|
import MinimaxProvider from './minimax';
|
15
16
|
import MistralProvider from './mistral';
|
16
17
|
import MoonshotProvider from './moonshot';
|
@@ -57,6 +58,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
|
|
57
58
|
UpstageProvider.chatModels,
|
58
59
|
SparkProvider.chatModels,
|
59
60
|
Ai21Provider.chatModels,
|
61
|
+
HunyuanProvider.chatModels,
|
60
62
|
].flat();
|
61
63
|
|
62
64
|
export const DEFAULT_MODEL_PROVIDER_LIST = [
|
@@ -78,6 +80,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
|
|
78
80
|
Ai21Provider,
|
79
81
|
UpstageProvider,
|
80
82
|
QwenProvider,
|
83
|
+
HunyuanProvider,
|
81
84
|
SparkProvider,
|
82
85
|
ZhiPuProvider,
|
83
86
|
ZeroOneProvider,
|
@@ -110,6 +113,7 @@ export { default as FireworksAIProviderCard } from './fireworksai';
|
|
110
113
|
export { default as GithubProviderCard } from './github';
|
111
114
|
export { default as GoogleProviderCard } from './google';
|
112
115
|
export { default as GroqProviderCard } from './groq';
|
116
|
+
export { default as HunyuanProviderCard } from './hunyuan';
|
113
117
|
export { default as MinimaxProviderCard } from './minimax';
|
114
118
|
export { default as MistralProviderCard } from './mistral';
|
115
119
|
export { default as MoonshotProviderCard } from './moonshot';
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
GithubProviderCard,
|
10
10
|
GoogleProviderCard,
|
11
11
|
GroqProviderCard,
|
12
|
+
HunyuanProviderCard,
|
12
13
|
MinimaxProviderCard,
|
13
14
|
MistralProviderCard,
|
14
15
|
MoonshotProviderCard,
|
@@ -75,6 +76,10 @@ export const DEFAULT_LLM_CONFIG: UserModelProviderConfig = {
|
|
75
76
|
enabled: false,
|
76
77
|
enabledModels: filterEnabledModels(GroqProviderCard),
|
77
78
|
},
|
79
|
+
hunyuan: {
|
80
|
+
enabled: false,
|
81
|
+
enabledModels: filterEnabledModels(HunyuanProviderCard),
|
82
|
+
},
|
78
83
|
minimax: {
|
79
84
|
enabled: false,
|
80
85
|
enabledModels: filterEnabledModels(MinimaxProviderCard),
|
@@ -14,6 +14,7 @@ import { LobeFireworksAI } from './fireworksai';
|
|
14
14
|
import { LobeGithubAI } from './github';
|
15
15
|
import { LobeGoogleAI } from './google';
|
16
16
|
import { LobeGroq } from './groq';
|
17
|
+
import { LobeHunyuanAI } from './hunyuan';
|
17
18
|
import { LobeMinimaxAI } from './minimax';
|
18
19
|
import { LobeMistralAI } from './mistral';
|
19
20
|
import { LobeMoonshotAI } from './moonshot';
|
@@ -133,6 +134,7 @@ class AgentRuntime {
|
|
133
134
|
github: Partial<ClientOptions>;
|
134
135
|
google: { apiKey?: string; baseURL?: string };
|
135
136
|
groq: Partial<ClientOptions>;
|
137
|
+
hunyuan: Partial<ClientOptions>;
|
136
138
|
minimax: Partial<ClientOptions>;
|
137
139
|
mistral: Partial<ClientOptions>;
|
138
140
|
moonshot: Partial<ClientOptions>;
|
@@ -300,6 +302,11 @@ class AgentRuntime {
|
|
300
302
|
runtimeModel = new LobeAi21AI(params.ai21);
|
301
303
|
break;
|
302
304
|
}
|
305
|
+
|
306
|
+
case ModelProvider.Hunyuan: {
|
307
|
+
runtimeModel = new LobeHunyuanAI(params.hunyuan);
|
308
|
+
break;
|
309
|
+
}
|
303
310
|
}
|
304
311
|
|
305
312
|
return new AgentRuntime(runtimeModel);
|
@@ -0,0 +1,255 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import OpenAI from 'openai';
|
3
|
+
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import {
|
6
|
+
ChatStreamCallbacks,
|
7
|
+
LobeOpenAICompatibleRuntime,
|
8
|
+
ModelProvider,
|
9
|
+
} from '@/libs/agent-runtime';
|
10
|
+
|
11
|
+
import * as debugStreamModule from '../utils/debugStream';
|
12
|
+
import { LobeHunyuanAI } from './index';
|
13
|
+
|
14
|
+
const provider = ModelProvider.Hunyuan;
|
15
|
+
const defaultBaseURL = 'https://api.hunyuan.cloud.tencent.com/v1';
|
16
|
+
|
17
|
+
const bizErrorType = 'ProviderBizError';
|
18
|
+
const invalidErrorType = 'InvalidProviderAPIKey';
|
19
|
+
|
20
|
+
// Mock the console.error to avoid polluting test output
|
21
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
22
|
+
|
23
|
+
let instance: LobeOpenAICompatibleRuntime;
|
24
|
+
|
25
|
+
beforeEach(() => {
|
26
|
+
instance = new LobeHunyuanAI({ apiKey: 'test' });
|
27
|
+
|
28
|
+
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
29
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
30
|
+
new ReadableStream() as any,
|
31
|
+
);
|
32
|
+
});
|
33
|
+
|
34
|
+
afterEach(() => {
|
35
|
+
vi.clearAllMocks();
|
36
|
+
});
|
37
|
+
|
38
|
+
describe('LobeHunyuanAI', () => {
|
39
|
+
describe('init', () => {
|
40
|
+
it('should correctly initialize with an API key', async () => {
|
41
|
+
const instance = new LobeHunyuanAI({ apiKey: 'test_api_key' });
|
42
|
+
expect(instance).toBeInstanceOf(LobeHunyuanAI);
|
43
|
+
expect(instance.baseURL).toEqual(defaultBaseURL);
|
44
|
+
});
|
45
|
+
});
|
46
|
+
|
47
|
+
describe('chat', () => {
|
48
|
+
describe('Error', () => {
|
49
|
+
it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
50
|
+
// Arrange
|
51
|
+
const apiError = new OpenAI.APIError(
|
52
|
+
400,
|
53
|
+
{
|
54
|
+
status: 400,
|
55
|
+
error: {
|
56
|
+
message: 'Bad Request',
|
57
|
+
},
|
58
|
+
},
|
59
|
+
'Error message',
|
60
|
+
{},
|
61
|
+
);
|
62
|
+
|
63
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
64
|
+
|
65
|
+
// Act
|
66
|
+
try {
|
67
|
+
await instance.chat({
|
68
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
69
|
+
model: 'hunyuan-lite',
|
70
|
+
temperature: 0,
|
71
|
+
});
|
72
|
+
} catch (e) {
|
73
|
+
expect(e).toEqual({
|
74
|
+
endpoint: defaultBaseURL,
|
75
|
+
error: {
|
76
|
+
error: { message: 'Bad Request' },
|
77
|
+
status: 400,
|
78
|
+
},
|
79
|
+
errorType: bizErrorType,
|
80
|
+
provider,
|
81
|
+
});
|
82
|
+
}
|
83
|
+
});
|
84
|
+
|
85
|
+
it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => {
|
86
|
+
try {
|
87
|
+
new LobeHunyuanAI({});
|
88
|
+
} catch (e) {
|
89
|
+
expect(e).toEqual({ errorType: invalidErrorType });
|
90
|
+
}
|
91
|
+
});
|
92
|
+
|
93
|
+
it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
|
94
|
+
// Arrange
|
95
|
+
const errorInfo = {
|
96
|
+
stack: 'abc',
|
97
|
+
cause: {
|
98
|
+
message: 'api is undefined',
|
99
|
+
},
|
100
|
+
};
|
101
|
+
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
102
|
+
|
103
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
104
|
+
|
105
|
+
// Act
|
106
|
+
try {
|
107
|
+
await instance.chat({
|
108
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
109
|
+
model: 'hunyuan-lite',
|
110
|
+
temperature: 0,
|
111
|
+
});
|
112
|
+
} catch (e) {
|
113
|
+
expect(e).toEqual({
|
114
|
+
endpoint: defaultBaseURL,
|
115
|
+
error: {
|
116
|
+
cause: { message: 'api is undefined' },
|
117
|
+
stack: 'abc',
|
118
|
+
},
|
119
|
+
errorType: bizErrorType,
|
120
|
+
provider,
|
121
|
+
});
|
122
|
+
}
|
123
|
+
});
|
124
|
+
|
125
|
+
it('should return OpenAIBizError with an cause response with desensitize Url', async () => {
|
126
|
+
// Arrange
|
127
|
+
const errorInfo = {
|
128
|
+
stack: 'abc',
|
129
|
+
cause: { message: 'api is undefined' },
|
130
|
+
};
|
131
|
+
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
132
|
+
|
133
|
+
instance = new LobeHunyuanAI({
|
134
|
+
apiKey: 'test',
|
135
|
+
|
136
|
+
baseURL: 'https://api.abc.com/v1',
|
137
|
+
});
|
138
|
+
|
139
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
140
|
+
|
141
|
+
// Act
|
142
|
+
try {
|
143
|
+
await instance.chat({
|
144
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
145
|
+
model: 'hunyuan-lite',
|
146
|
+
temperature: 0,
|
147
|
+
});
|
148
|
+
} catch (e) {
|
149
|
+
expect(e).toEqual({
|
150
|
+
endpoint: 'https://api.***.com/v1',
|
151
|
+
error: {
|
152
|
+
cause: { message: 'api is undefined' },
|
153
|
+
stack: 'abc',
|
154
|
+
},
|
155
|
+
errorType: bizErrorType,
|
156
|
+
provider,
|
157
|
+
});
|
158
|
+
}
|
159
|
+
});
|
160
|
+
|
161
|
+
it('should throw an InvalidHunyuanAPIKey error type on 401 status code', async () => {
|
162
|
+
// Mock the API call to simulate a 401 error
|
163
|
+
const error = new Error('Unauthorized') as any;
|
164
|
+
error.status = 401;
|
165
|
+
vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
|
166
|
+
|
167
|
+
try {
|
168
|
+
await instance.chat({
|
169
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
170
|
+
model: 'hunyuan-lite',
|
171
|
+
temperature: 0,
|
172
|
+
});
|
173
|
+
} catch (e) {
|
174
|
+
// Expect the chat method to throw an error with InvalidHunyuanAPIKey
|
175
|
+
expect(e).toEqual({
|
176
|
+
endpoint: defaultBaseURL,
|
177
|
+
error: new Error('Unauthorized'),
|
178
|
+
errorType: invalidErrorType,
|
179
|
+
provider,
|
180
|
+
});
|
181
|
+
}
|
182
|
+
});
|
183
|
+
|
184
|
+
it('should return AgentRuntimeError for non-OpenAI errors', async () => {
|
185
|
+
// Arrange
|
186
|
+
const genericError = new Error('Generic Error');
|
187
|
+
|
188
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
|
189
|
+
|
190
|
+
// Act
|
191
|
+
try {
|
192
|
+
await instance.chat({
|
193
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
194
|
+
model: 'hunyuan-lite',
|
195
|
+
temperature: 0,
|
196
|
+
});
|
197
|
+
} catch (e) {
|
198
|
+
expect(e).toEqual({
|
199
|
+
endpoint: defaultBaseURL,
|
200
|
+
errorType: 'AgentRuntimeError',
|
201
|
+
provider,
|
202
|
+
error: {
|
203
|
+
name: genericError.name,
|
204
|
+
cause: genericError.cause,
|
205
|
+
message: genericError.message,
|
206
|
+
stack: genericError.stack,
|
207
|
+
},
|
208
|
+
});
|
209
|
+
}
|
210
|
+
});
|
211
|
+
});
|
212
|
+
|
213
|
+
describe('DEBUG', () => {
|
214
|
+
it('should call debugStream and return StreamingTextResponse when DEBUG_HUNYUAN_CHAT_COMPLETION is 1', async () => {
|
215
|
+
// Arrange
|
216
|
+
const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
|
217
|
+
const mockDebugStream = new ReadableStream({
|
218
|
+
start(controller) {
|
219
|
+
controller.enqueue('Debug stream content');
|
220
|
+
controller.close();
|
221
|
+
},
|
222
|
+
}) as any;
|
223
|
+
mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
|
224
|
+
|
225
|
+
// 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
|
226
|
+
(instance['client'].chat.completions.create as Mock).mockResolvedValue({
|
227
|
+
tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
|
228
|
+
});
|
229
|
+
|
230
|
+
// 保存原始环境变量值
|
231
|
+
const originalDebugValue = process.env.DEBUG_HUNYUAN_CHAT_COMPLETION;
|
232
|
+
|
233
|
+
// 模拟环境变量
|
234
|
+
process.env.DEBUG_HUNYUAN_CHAT_COMPLETION = '1';
|
235
|
+
vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
|
236
|
+
|
237
|
+
// 执行测试
|
238
|
+
// 运行你的测试函数,确保它会在条件满足时调用 debugStream
|
239
|
+
// 假设的测试函数调用,你可能需要根据实际情况调整
|
240
|
+
await instance.chat({
|
241
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
242
|
+
model: 'hunyuan-lite',
|
243
|
+
stream: true,
|
244
|
+
temperature: 0,
|
245
|
+
});
|
246
|
+
|
247
|
+
// 验证 debugStream 被调用
|
248
|
+
expect(debugStreamModule.debugStream).toHaveBeenCalled();
|
249
|
+
|
250
|
+
// 恢复原始环境变量值
|
251
|
+
process.env.DEBUG_HUNYUAN_CHAT_COMPLETION = originalDebugValue;
|
252
|
+
});
|
253
|
+
});
|
254
|
+
});
|
255
|
+
});
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { ModelProvider } from '../types';
|
2
|
+
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
3
|
+
|
4
|
+
export const LobeHunyuanAI = LobeOpenAICompatibleFactory({
|
5
|
+
baseURL: 'https://api.hunyuan.cloud.tencent.com/v1',
|
6
|
+
debug: {
|
7
|
+
chatCompletion: () => process.env.DEBUG_HUNYUAN_CHAT_COMPLETION === '1',
|
8
|
+
},
|
9
|
+
provider: ModelProvider.Hunyuan,
|
10
|
+
});
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
GithubProviderCard,
|
10
10
|
GoogleProviderCard,
|
11
11
|
GroqProviderCard,
|
12
|
+
HunyuanProviderCard,
|
12
13
|
NovitaProviderCard,
|
13
14
|
OllamaProviderCard,
|
14
15
|
OpenAIProviderCard,
|
@@ -49,6 +50,9 @@ export const getServerGlobalConfig = () => {
|
|
49
50
|
ENABLED_GITHUB,
|
50
51
|
GITHUB_MODEL_LIST,
|
51
52
|
|
53
|
+
ENABLED_HUNYUAN,
|
54
|
+
HUNYUAN_MODEL_LIST,
|
55
|
+
|
52
56
|
ENABLED_DEEPSEEK,
|
53
57
|
ENABLED_PERPLEXITY,
|
54
58
|
ENABLED_ANTHROPIC,
|
@@ -160,6 +164,14 @@ export const getServerGlobalConfig = () => {
|
|
160
164
|
modelString: GROQ_MODEL_LIST,
|
161
165
|
}),
|
162
166
|
},
|
167
|
+
hunyuan: {
|
168
|
+
enabled: ENABLED_HUNYUAN,
|
169
|
+
enabledModels: extractEnabledModels(HUNYUAN_MODEL_LIST),
|
170
|
+
serverModelCards: transformToChatModelCards({
|
171
|
+
defaultChatModels: HunyuanProviderCard.chatModels,
|
172
|
+
modelString: HUNYUAN_MODEL_LIST,
|
173
|
+
}),
|
174
|
+
},
|
163
175
|
minimax: { enabled: ENABLED_MINIMAX },
|
164
176
|
mistral: { enabled: ENABLED_MISTRAL },
|
165
177
|
moonshot: { enabled: ENABLED_MOONSHOT },
|
@@ -28,6 +28,7 @@ export interface UserKeyVaults {
|
|
28
28
|
github?: OpenAICompatibleKeyVault;
|
29
29
|
google?: OpenAICompatibleKeyVault;
|
30
30
|
groq?: OpenAICompatibleKeyVault;
|
31
|
+
hunyuan?: OpenAICompatibleKeyVault;
|
31
32
|
lobehub?: any;
|
32
33
|
minimax?: OpenAICompatibleKeyVault;
|
33
34
|
mistral?: OpenAICompatibleKeyVault;
|