@sunflower0305/claude-proxy 1.3.0 → 1.3.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/.env.example +2 -2
- package/CHANGELOG.md +12 -0
- package/README.md +22 -22
- package/dist/proxy.d.ts +6 -14
- package/dist/proxy.js +359 -405
- package/package.json +47 -39
package/.env.example
CHANGED
|
@@ -17,10 +17,10 @@ KIMI_API_KEY=your-kimi-key
|
|
|
17
17
|
MIMO_API_KEY=your-mimo-key
|
|
18
18
|
|
|
19
19
|
# Optional model overrides
|
|
20
|
-
QWEN_MODEL=
|
|
20
|
+
QWEN_MODEL=qwen3.7-plus
|
|
21
21
|
DEEPSEEK_MODEL=deepseek-v4-pro
|
|
22
22
|
GLM_MODEL=glm-5.1
|
|
23
|
-
MINIMAX_MODEL=
|
|
23
|
+
MINIMAX_MODEL=minimax-m3
|
|
24
24
|
KIMI_MODEL=kimi-k2.6
|
|
25
25
|
MIMO_MODEL=mimo-v2.5-pro
|
|
26
26
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.1] - 2026-06-03
|
|
6
|
+
|
|
7
|
+
Patch release of `@sunflower0305/claude-proxy`.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- default Qwen upstream model changed from `qwen-plus` to `qwen3.7-plus`
|
|
12
|
+
- default MiniMax upstream model changed from `minimax-m2.7-highspeed` to `minimax-m3`
|
|
13
|
+
- README and `.env.example` now document the upgraded provider defaults
|
|
14
|
+
|
|
15
|
+
Detailed release notes: [docs/releases/1.3.1.md](docs/releases/1.3.1.md)
|
|
16
|
+
|
|
5
17
|
## [1.3.0] - 2026-05-06
|
|
6
18
|
|
|
7
19
|
Minor release of `@sunflower0305/claude-proxy`.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
7
7
|
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
8
8
|
[](https://github.com/sunflower0305/claude-proxy/stargazers)
|
|
9
|
-
[](https://zread.ai/sunflower0305/claude-proxy)
|
|
10
10
|
|
|
11
11
|
`claude-proxy` is published on npm as `@sunflower0305/claude-proxy`. It is a lightweight Express proxy that lets Claude Code or the Claude Agent SDK talk to domestic Chinese LLM providers through Anthropic-compatible `/v1/messages` endpoints.
|
|
12
12
|
|
|
@@ -46,30 +46,30 @@ DEEPSEEK_MODEL=deepseek-v4-pro
|
|
|
46
46
|
|
|
47
47
|
Available variables:
|
|
48
48
|
|
|
49
|
-
| Variable
|
|
50
|
-
|
|
|
51
|
-
| `PROVIDER`
|
|
52
|
-
| `PROXY_PORT`
|
|
53
|
-
| `PROXY_API_KEY`
|
|
54
|
-
| `QWEN_API_KEY`
|
|
55
|
-
| `DEEPSEEK_API_KEY`
|
|
56
|
-
| `GLM_API_KEY`
|
|
57
|
-
| `MINIMAX_API_KEY`
|
|
58
|
-
| `KIMI_API_KEY`
|
|
59
|
-
| `MIMO_API_KEY`
|
|
60
|
-
| `QWEN_ANTHROPIC_BASE_URL`, `DEEPSEEK_ANTHROPIC_BASE_URL`, `GLM_ANTHROPIC_BASE_URL`, `MINIMAX_ANTHROPIC_BASE_URL`, `KIMI_ANTHROPIC_BASE_URL`, `MIMO_ANTHROPIC_BASE_URL` | Override the upstream Anthropic-compatible base URL for a provider.
|
|
61
|
-
| `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL`, `MIMO_MODEL`
|
|
49
|
+
| Variable | Purpose |
|
|
50
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
51
|
+
| `PROVIDER` | Active provider. Defaults to `deepseek`. |
|
|
52
|
+
| `PROXY_PORT` | Local server port. Defaults to `8080`. |
|
|
53
|
+
| `PROXY_API_KEY` | Optional local proxy token for `POST /v1/messages` and `POST /api/provider`. |
|
|
54
|
+
| `QWEN_API_KEY` | API key for Qwen. |
|
|
55
|
+
| `DEEPSEEK_API_KEY` | API key for DeepSeek. |
|
|
56
|
+
| `GLM_API_KEY` | API key for GLM. |
|
|
57
|
+
| `MINIMAX_API_KEY` | API key for MiniMax. |
|
|
58
|
+
| `KIMI_API_KEY` | API key for Kimi. |
|
|
59
|
+
| `MIMO_API_KEY` | API key for MIMO. |
|
|
60
|
+
| `QWEN_ANTHROPIC_BASE_URL`, `DEEPSEEK_ANTHROPIC_BASE_URL`, `GLM_ANTHROPIC_BASE_URL`, `MINIMAX_ANTHROPIC_BASE_URL`, `KIMI_ANTHROPIC_BASE_URL`, `MIMO_ANTHROPIC_BASE_URL` | Override the upstream Anthropic-compatible base URL for a provider. |
|
|
61
|
+
| `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL`, `MIMO_MODEL` | Override the default upstream model for a provider. |
|
|
62
62
|
|
|
63
63
|
Provider defaults:
|
|
64
64
|
|
|
65
|
-
| Provider
|
|
66
|
-
|
|
|
67
|
-
| **`deepseek` (default)** | `DEEPSEEK_MODEL` | **`deepseek-v4-pro`**
|
|
68
|
-
| `qwen`
|
|
69
|
-
| `glm`
|
|
70
|
-
| `minimax`
|
|
71
|
-
| `kimi`
|
|
72
|
-
| `mimo`
|
|
65
|
+
| Provider | Model env | Default model |
|
|
66
|
+
| ------------------------ | ---------------- | ------------------------ |
|
|
67
|
+
| **`deepseek` (default)** | `DEEPSEEK_MODEL` | **`deepseek-v4-pro`** |
|
|
68
|
+
| `qwen` | `QWEN_MODEL` | `qwen3.7-plus` |
|
|
69
|
+
| `glm` | `GLM_MODEL` | `glm-5.1` |
|
|
70
|
+
| `minimax` | `MINIMAX_MODEL` | `minimax-m3` |
|
|
71
|
+
| `kimi` | `KIMI_MODEL` | `kimi-k2.6` |
|
|
72
|
+
| `mimo` | `MIMO_MODEL` | `mimo-v2.5-pro` |
|
|
73
73
|
|
|
74
74
|
You can use the bundled example as a starting point:
|
|
75
75
|
|
package/dist/proxy.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Claude Proxy
|
|
4
|
-
*
|
|
5
|
-
* Proxies Anthropic Messages API requests to provider-native
|
|
6
|
-
* Anthropic-compatible endpoints without translating protocols.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* export ANTHROPIC_BASE_URL=http://localhost:8080
|
|
10
|
-
* export ANTHROPIC_API_KEY=any-key-works
|
|
11
|
-
* # If PROXY_API_KEY is set, use that same value instead.
|
|
12
|
-
*/
|
|
13
1
|
import express from "express";
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
|
|
3
|
+
//#region src/proxy.d.ts
|
|
4
|
+
declare function createApp(): express.Express;
|
|
5
|
+
declare const app: express.Express;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { app, createApp };
|
package/dist/proxy.js
CHANGED
|
@@ -1,451 +1,403 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Claude Proxy
|
|
4
|
-
*
|
|
5
|
-
* Proxies Anthropic Messages API requests to provider-native
|
|
6
|
-
* Anthropic-compatible endpoints without translating protocols.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* export ANTHROPIC_BASE_URL=http://localhost:8080
|
|
10
|
-
* export ANTHROPIC_API_KEY=any-key-works
|
|
11
|
-
* # If PROXY_API_KEY is set, use that same value instead.
|
|
12
|
-
*/
|
|
13
2
|
import express from "express";
|
|
14
3
|
import { existsSync, realpathSync } from "node:fs";
|
|
15
4
|
import { loadEnvFile } from "node:process";
|
|
16
5
|
import { Readable } from "node:stream";
|
|
17
6
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
//#region src/proxy.ts
|
|
8
|
+
/**
|
|
9
|
+
* Claude Proxy
|
|
10
|
+
*
|
|
11
|
+
* Proxies Anthropic Messages API requests to provider-native
|
|
12
|
+
* Anthropic-compatible endpoints without translating protocols.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* export ANTHROPIC_BASE_URL=http://localhost:8080
|
|
16
|
+
* export ANTHROPIC_API_KEY=any-key-works
|
|
17
|
+
* # If PROXY_API_KEY is set, use that same value instead.
|
|
18
|
+
*/
|
|
19
|
+
if (existsSync(".env")) loadEnvFile(".env");
|
|
20
20
|
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
21
21
|
const HOP_BY_HOP_RESPONSE_HEADERS = new Set([
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
"connection",
|
|
23
|
+
"content-encoding",
|
|
24
|
+
"content-length",
|
|
25
|
+
"keep-alive",
|
|
26
|
+
"proxy-authenticate",
|
|
27
|
+
"proxy-authorization",
|
|
28
|
+
"te",
|
|
29
|
+
"trailer",
|
|
30
|
+
"transfer-encoding",
|
|
31
|
+
"upgrade"
|
|
32
32
|
]);
|
|
33
33
|
function pickEnv(...keys) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
const value = process.env[key]?.trim();
|
|
36
|
+
if (value) return value;
|
|
37
|
+
}
|
|
40
38
|
}
|
|
41
39
|
const PROVIDERS = {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
baseUrl: pickEnv("MIMO_ANTHROPIC_BASE_URL") ||
|
|
73
|
-
"https://api.xiaomimimo.com/anthropic",
|
|
74
|
-
apiKey: process.env.MIMO_API_KEY || "",
|
|
75
|
-
model: pickEnv("MIMO_MODEL") || "mimo-v2.5-pro",
|
|
76
|
-
},
|
|
40
|
+
deepseek: {
|
|
41
|
+
baseUrl: pickEnv("DEEPSEEK_ANTHROPIC_BASE_URL") || "https://api.deepseek.com/anthropic",
|
|
42
|
+
apiKey: process.env.DEEPSEEK_API_KEY || "",
|
|
43
|
+
model: pickEnv("DEEPSEEK_MODEL") || "deepseek-v4-pro"
|
|
44
|
+
},
|
|
45
|
+
qwen: {
|
|
46
|
+
baseUrl: pickEnv("QWEN_ANTHROPIC_BASE_URL") || "https://dashscope.aliyuncs.com/apps/anthropic",
|
|
47
|
+
apiKey: process.env.QWEN_API_KEY || "",
|
|
48
|
+
model: pickEnv("QWEN_MODEL") || "qwen3.7-plus"
|
|
49
|
+
},
|
|
50
|
+
glm: {
|
|
51
|
+
baseUrl: pickEnv("GLM_ANTHROPIC_BASE_URL") || "https://open.bigmodel.cn/api/anthropic",
|
|
52
|
+
apiKey: process.env.GLM_API_KEY || "",
|
|
53
|
+
model: pickEnv("GLM_MODEL") || "glm-5.1"
|
|
54
|
+
},
|
|
55
|
+
minimax: {
|
|
56
|
+
baseUrl: pickEnv("MINIMAX_ANTHROPIC_BASE_URL") || "https://api.minimaxi.com/anthropic",
|
|
57
|
+
apiKey: process.env.MINIMAX_API_KEY || "",
|
|
58
|
+
model: pickEnv("MINIMAX_MODEL") || "minimax-m3"
|
|
59
|
+
},
|
|
60
|
+
kimi: {
|
|
61
|
+
baseUrl: pickEnv("KIMI_ANTHROPIC_BASE_URL") || "https://api.moonshot.cn/anthropic",
|
|
62
|
+
apiKey: process.env.KIMI_API_KEY || "",
|
|
63
|
+
model: pickEnv("KIMI_MODEL") || "kimi-k2.6"
|
|
64
|
+
},
|
|
65
|
+
mimo: {
|
|
66
|
+
baseUrl: pickEnv("MIMO_ANTHROPIC_BASE_URL") || "https://api.xiaomimimo.com/anthropic",
|
|
67
|
+
apiKey: process.env.MIMO_API_KEY || "",
|
|
68
|
+
model: pickEnv("MIMO_MODEL") || "mimo-v2.5-pro"
|
|
69
|
+
}
|
|
77
70
|
};
|
|
78
71
|
function isProviderKey(value) {
|
|
79
|
-
|
|
72
|
+
return Boolean(value && value in PROVIDERS);
|
|
80
73
|
}
|
|
81
74
|
function getInitialProvider() {
|
|
82
|
-
|
|
83
|
-
? process.env.PROVIDER
|
|
84
|
-
: "deepseek";
|
|
75
|
+
return isProviderKey(process.env.PROVIDER) ? process.env.PROVIDER : "deepseek";
|
|
85
76
|
}
|
|
86
77
|
function getProviderConfig(provider) {
|
|
87
|
-
|
|
78
|
+
return PROVIDERS[provider] || PROVIDERS.deepseek;
|
|
88
79
|
}
|
|
89
80
|
function getHeaderValue(value) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return value;
|
|
81
|
+
if (Array.isArray(value)) return value.join(",");
|
|
82
|
+
return value;
|
|
93
83
|
}
|
|
94
84
|
function buildUpstreamHeaders(req, stream, apiKey) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (anthropicBeta) {
|
|
105
|
-
headers["anthropic-beta"] = anthropicBeta;
|
|
106
|
-
}
|
|
107
|
-
return headers;
|
|
85
|
+
const headers = {
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
"x-api-key": apiKey,
|
|
88
|
+
"anthropic-version": getHeaderValue(req.headers["anthropic-version"]) || DEFAULT_ANTHROPIC_VERSION,
|
|
89
|
+
accept: getHeaderValue(req.headers.accept) || (stream ? "text/event-stream" : "application/json")
|
|
90
|
+
};
|
|
91
|
+
const anthropicBeta = getHeaderValue(req.headers["anthropic-beta"]);
|
|
92
|
+
if (anthropicBeta) headers["anthropic-beta"] = anthropicBeta;
|
|
93
|
+
return headers;
|
|
108
94
|
}
|
|
109
95
|
function getUpstreamUrl(baseUrl) {
|
|
110
|
-
|
|
96
|
+
return `${baseUrl.replace(/\/$/, "")}/v1/messages`;
|
|
111
97
|
}
|
|
112
98
|
function buildUpstreamBody(body, targetModel) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
normalized.model = targetModel;
|
|
117
|
-
return normalized;
|
|
99
|
+
const normalized = typeof body === "object" && body !== null ? { ...body } : {};
|
|
100
|
+
normalized.model = targetModel;
|
|
101
|
+
return normalized;
|
|
118
102
|
}
|
|
119
103
|
function copyUpstreamHeaders(upstream, res) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
104
|
+
for (const [key, value] of upstream.headers.entries()) {
|
|
105
|
+
if (HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
|
|
106
|
+
res.setHeader(key, value);
|
|
107
|
+
}
|
|
125
108
|
}
|
|
126
109
|
function createProxyError(message) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
110
|
+
return {
|
|
111
|
+
type: "error",
|
|
112
|
+
error: {
|
|
113
|
+
type: "internal_error",
|
|
114
|
+
message
|
|
115
|
+
}
|
|
116
|
+
};
|
|
134
117
|
}
|
|
135
118
|
function createAuthenticationError() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
return {
|
|
120
|
+
type: "error",
|
|
121
|
+
error: {
|
|
122
|
+
type: "authentication_error",
|
|
123
|
+
message: "Invalid API key"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
143
126
|
}
|
|
144
127
|
function getBearerToken(authorization) {
|
|
145
|
-
|
|
146
|
-
return match?.[1]?.trim() || undefined;
|
|
128
|
+
return (authorization?.match(/^Bearer\s+(.+)$/i))?.[1]?.trim() || void 0;
|
|
147
129
|
}
|
|
148
130
|
function inferProviderFromModel(model) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const key of Object.keys(PROVIDERS)) {
|
|
153
|
-
if (normalizedModel.includes(key))
|
|
154
|
-
return key;
|
|
155
|
-
}
|
|
156
|
-
return undefined;
|
|
131
|
+
if (!model) return void 0;
|
|
132
|
+
const normalizedModel = model.toLowerCase();
|
|
133
|
+
for (const key of Object.keys(PROVIDERS)) if (normalizedModel.includes(key)) return key;
|
|
157
134
|
}
|
|
158
135
|
function logTimingEvent(trace, phase, extra = {}) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
console.log(`[ProxyTiming] ${JSON.stringify({
|
|
137
|
+
request_id: trace.requestId,
|
|
138
|
+
provider: trace.provider,
|
|
139
|
+
requested_model: trace.requestedModel,
|
|
140
|
+
target_model: trace.targetModel,
|
|
141
|
+
stream: trace.stream,
|
|
142
|
+
phase,
|
|
143
|
+
elapsed_ms: Date.now() - trace.startedAt,
|
|
144
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
145
|
+
...extra
|
|
146
|
+
})}`);
|
|
170
147
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
available: Object.keys(PROVIDERS),
|
|
401
|
-
});
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const targetConfig = getConfig(targetProvider);
|
|
405
|
-
if (!targetConfig.apiKey) {
|
|
406
|
-
res.status(400).json({
|
|
407
|
-
error: `API key not set for: ${targetProvider}`,
|
|
408
|
-
});
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
const oldProvider = currentProvider;
|
|
412
|
-
currentProvider = targetProvider;
|
|
413
|
-
console.log(`Provider: ${oldProvider} -> ${currentProvider}`);
|
|
414
|
-
res.json({
|
|
415
|
-
success: true,
|
|
416
|
-
provider: currentProvider,
|
|
417
|
-
model: targetConfig.model,
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
return app;
|
|
148
|
+
function createApp() {
|
|
149
|
+
let currentProvider = getInitialProvider();
|
|
150
|
+
let requestSequence = 0;
|
|
151
|
+
const proxyApiKey = pickEnv("PROXY_API_KEY");
|
|
152
|
+
function getConfig(provider = currentProvider) {
|
|
153
|
+
return getProviderConfig(provider);
|
|
154
|
+
}
|
|
155
|
+
function getTargetModel(requestedModel) {
|
|
156
|
+
if (typeof requestedModel !== "string" || !requestedModel) return getConfig().model;
|
|
157
|
+
const normalizedModel = requestedModel.toLowerCase();
|
|
158
|
+
if (normalizedModel === "opus" || normalizedModel === "sonnet" || normalizedModel === "haiku") return getConfig().model;
|
|
159
|
+
if (normalizedModel.startsWith("claude-") && (normalizedModel.includes("-opus") || normalizedModel.includes("-sonnet") || normalizedModel.includes("-haiku"))) return getConfig().model;
|
|
160
|
+
return requestedModel;
|
|
161
|
+
}
|
|
162
|
+
function createRequestTrace(requestedModel, targetModel, stream) {
|
|
163
|
+
return {
|
|
164
|
+
requestId: `req-${++requestSequence}`,
|
|
165
|
+
provider: currentProvider,
|
|
166
|
+
requestedModel: typeof requestedModel === "string" && requestedModel ? requestedModel : targetModel,
|
|
167
|
+
targetModel,
|
|
168
|
+
stream,
|
|
169
|
+
startedAt: Date.now()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function requireProxyApiKey(req, res, next) {
|
|
173
|
+
if (!proxyApiKey) {
|
|
174
|
+
next();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if ([getHeaderValue(req.headers["x-api-key"])?.trim(), getBearerToken(getHeaderValue(req.headers.authorization))].filter((token) => Boolean(token)).includes(proxyApiKey)) {
|
|
178
|
+
next();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
res.status(401).json(createAuthenticationError());
|
|
182
|
+
}
|
|
183
|
+
async function handleNonStreamingRequest(req, res) {
|
|
184
|
+
const config = getConfig();
|
|
185
|
+
const targetModel = getTargetModel(req.body?.model);
|
|
186
|
+
const requestBody = buildUpstreamBody(req.body, targetModel);
|
|
187
|
+
const trace = createRequestTrace(req.body?.model, targetModel, false);
|
|
188
|
+
logTimingEvent(trace, "start");
|
|
189
|
+
try {
|
|
190
|
+
const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: buildUpstreamHeaders(req, false, config.apiKey),
|
|
193
|
+
body: JSON.stringify(requestBody)
|
|
194
|
+
});
|
|
195
|
+
logTimingEvent(trace, "upstream_headers", {
|
|
196
|
+
status: upstream.status,
|
|
197
|
+
content_type: upstream.headers.get("content-type") || ""
|
|
198
|
+
});
|
|
199
|
+
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
200
|
+
copyUpstreamHeaders(upstream, res);
|
|
201
|
+
res.status(upstream.status).send(payload);
|
|
202
|
+
logTimingEvent(trace, "completed", {
|
|
203
|
+
status: upstream.status,
|
|
204
|
+
bytes: payload.byteLength
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error("Request error:", error);
|
|
208
|
+
logTimingEvent(trace, "error", { message: error?.message || String(error) });
|
|
209
|
+
res.status(500).json(createProxyError(error.message));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function handleStreamingRequest(req, res) {
|
|
213
|
+
const config = getConfig();
|
|
214
|
+
const targetModel = getTargetModel(req.body?.model);
|
|
215
|
+
const requestBody = buildUpstreamBody(req.body, targetModel);
|
|
216
|
+
const trace = createRequestTrace(req.body?.model, targetModel, true);
|
|
217
|
+
const abortController = new AbortController();
|
|
218
|
+
let clientClosed = false;
|
|
219
|
+
let streamCompleted = false;
|
|
220
|
+
let sawFirstChunk = false;
|
|
221
|
+
logTimingEvent(trace, "start");
|
|
222
|
+
res.on("close", () => {
|
|
223
|
+
if (streamCompleted) return;
|
|
224
|
+
clientClosed = true;
|
|
225
|
+
abortController.abort();
|
|
226
|
+
logTimingEvent(trace, "client_aborted");
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: buildUpstreamHeaders(req, true, config.apiKey),
|
|
232
|
+
body: JSON.stringify(requestBody),
|
|
233
|
+
signal: abortController.signal
|
|
234
|
+
});
|
|
235
|
+
logTimingEvent(trace, "upstream_headers", {
|
|
236
|
+
status: upstream.status,
|
|
237
|
+
content_type: upstream.headers.get("content-type") || ""
|
|
238
|
+
});
|
|
239
|
+
copyUpstreamHeaders(upstream, res);
|
|
240
|
+
res.status(upstream.status);
|
|
241
|
+
if (!upstream.body) {
|
|
242
|
+
streamCompleted = true;
|
|
243
|
+
res.end();
|
|
244
|
+
logTimingEvent(trace, "completed", {
|
|
245
|
+
status: upstream.status,
|
|
246
|
+
bytes: 0,
|
|
247
|
+
no_body: true
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const upstreamStream = Readable.fromWeb(upstream.body);
|
|
252
|
+
upstreamStream.on("data", (chunk) => {
|
|
253
|
+
if (sawFirstChunk) return;
|
|
254
|
+
sawFirstChunk = true;
|
|
255
|
+
const chunkSize = Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(String(chunk));
|
|
256
|
+
logTimingEvent(trace, "first_chunk", {
|
|
257
|
+
status: upstream.status,
|
|
258
|
+
chunk_bytes: chunkSize
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
upstreamStream.on("error", (error) => {
|
|
262
|
+
if (clientClosed) return;
|
|
263
|
+
console.error("Upstream stream error:", error);
|
|
264
|
+
logTimingEvent(trace, "error", {
|
|
265
|
+
status: upstream.status,
|
|
266
|
+
message: error?.message || String(error)
|
|
267
|
+
});
|
|
268
|
+
if (!res.writableEnded) res.end();
|
|
269
|
+
});
|
|
270
|
+
upstreamStream.pipe(res);
|
|
271
|
+
await new Promise((resolve, reject) => {
|
|
272
|
+
upstreamStream.on("end", () => {
|
|
273
|
+
streamCompleted = true;
|
|
274
|
+
logTimingEvent(trace, "completed", { status: upstream.status });
|
|
275
|
+
resolve();
|
|
276
|
+
});
|
|
277
|
+
upstreamStream.on("error", reject);
|
|
278
|
+
res.on("close", () => resolve());
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const wasAborted = error?.name === "AbortError" || abortController.signal.aborted;
|
|
282
|
+
if (clientClosed || wasAborted) {
|
|
283
|
+
console.warn("[Proxy] Client disconnected, streaming aborted");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
console.error("Request error:", error);
|
|
287
|
+
logTimingEvent(trace, "error", { message: error?.message || String(error) });
|
|
288
|
+
if (!res.headersSent) {
|
|
289
|
+
res.status(500).json(createProxyError(error.message));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!res.writableEnded) res.end();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const app = express();
|
|
296
|
+
app.use(express.json({ limit: "32mb" }));
|
|
297
|
+
app.get("/", (_req, res) => {
|
|
298
|
+
const config = getConfig();
|
|
299
|
+
res.json({
|
|
300
|
+
name: "claude-proxy",
|
|
301
|
+
status: "running",
|
|
302
|
+
provider: currentProvider,
|
|
303
|
+
model: config.model,
|
|
304
|
+
endpoints: {
|
|
305
|
+
messages: "POST /v1/messages",
|
|
306
|
+
health: "GET /health",
|
|
307
|
+
models: "GET /v1/models",
|
|
308
|
+
provider: "GET|POST /api/provider"
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
app.post("/v1/messages", requireProxyApiKey, async (req, res) => {
|
|
313
|
+
if (req.body?.stream) {
|
|
314
|
+
await handleStreamingRequest(req, res);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await handleNonStreamingRequest(req, res);
|
|
318
|
+
});
|
|
319
|
+
app.get("/health", (_req, res) => {
|
|
320
|
+
const config = getConfig();
|
|
321
|
+
res.json({
|
|
322
|
+
status: "ok",
|
|
323
|
+
provider: currentProvider,
|
|
324
|
+
model: config.model
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
app.get("/v1/models", (_req, res) => {
|
|
328
|
+
res.json({ data: [
|
|
329
|
+
{
|
|
330
|
+
id: "claude-opus-4-7",
|
|
331
|
+
object: "model"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: "claude-sonnet-4-6",
|
|
335
|
+
object: "model"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "claude-haiku-4-5",
|
|
339
|
+
object: "model"
|
|
340
|
+
}
|
|
341
|
+
] });
|
|
342
|
+
});
|
|
343
|
+
app.get("/api/provider", (_req, res) => {
|
|
344
|
+
const config = getConfig();
|
|
345
|
+
res.json({
|
|
346
|
+
provider: currentProvider,
|
|
347
|
+
model: config.model,
|
|
348
|
+
baseUrl: config.baseUrl,
|
|
349
|
+
availableProviders: Object.keys(PROVIDERS)
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
app.post("/api/provider", requireProxyApiKey, (req, res) => {
|
|
353
|
+
const { provider, model } = req.body ?? {};
|
|
354
|
+
const targetProvider = provider ?? inferProviderFromModel(model);
|
|
355
|
+
if (!isProviderKey(targetProvider)) {
|
|
356
|
+
res.status(400).json({
|
|
357
|
+
error: `Unknown provider: ${targetProvider}`,
|
|
358
|
+
available: Object.keys(PROVIDERS)
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const targetConfig = getConfig(targetProvider);
|
|
363
|
+
if (!targetConfig.apiKey) {
|
|
364
|
+
res.status(400).json({ error: `API key not set for: ${targetProvider}` });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const oldProvider = currentProvider;
|
|
368
|
+
currentProvider = targetProvider;
|
|
369
|
+
console.log(`Provider: ${oldProvider} -> ${currentProvider}`);
|
|
370
|
+
res.json({
|
|
371
|
+
success: true,
|
|
372
|
+
provider: currentProvider,
|
|
373
|
+
model: targetConfig.model
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
return app;
|
|
421
377
|
}
|
|
422
|
-
|
|
378
|
+
const app = createApp();
|
|
423
379
|
const PORT = parseInt(process.env.PROXY_PORT || "8080", 10);
|
|
424
380
|
function isMainModule() {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
381
|
+
const entryPath = process.argv[1];
|
|
382
|
+
if (!entryPath) return false;
|
|
383
|
+
try {
|
|
384
|
+
return realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url));
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
434
388
|
}
|
|
435
389
|
if (isMainModule()) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
app.listen(PORT, () => {
|
|
448
|
-
console.log(`
|
|
390
|
+
const initialProvider = getInitialProvider();
|
|
391
|
+
const initialConfig = getProviderConfig(initialProvider);
|
|
392
|
+
const clientApiKeyHint = pickEnv("PROXY_API_KEY") ? "same value as PROXY_API_KEY" : "any-string-works";
|
|
393
|
+
if (!initialConfig.apiKey) {
|
|
394
|
+
console.warn(`Warning: API key not configured for provider: ${initialProvider}`);
|
|
395
|
+
console.warn("Please set the appropriate environment variable in .env");
|
|
396
|
+
}
|
|
397
|
+
console.log(`Using ${initialProvider} as backend`);
|
|
398
|
+
console.log(`Model: ${initialConfig.model}`);
|
|
399
|
+
app.listen(PORT, () => {
|
|
400
|
+
console.log(`
|
|
449
401
|
╔═══════════════════════════════════════════════════════╗
|
|
450
402
|
║ claude-proxy ║
|
|
451
403
|
╠═══════════════════════════════════════════════════════╣
|
|
@@ -457,5 +409,7 @@ if (isMainModule()) {
|
|
|
457
409
|
║ ANTHROPIC_API_KEY=${clientApiKeyHint}
|
|
458
410
|
╚═══════════════════════════════════════════════════════╝
|
|
459
411
|
`);
|
|
460
|
-
|
|
412
|
+
});
|
|
461
413
|
}
|
|
414
|
+
//#endregion
|
|
415
|
+
export { app, createApp };
|
package/package.json
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunflower0305/claude-proxy",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "1.3.1",
|
|
5
4
|
"description": "A proxy that lets Claude Agent SDK use domestic Chinese LLMs (DeepSeek, Qwen, GLM, MiniMax, Kimi, MIMO) as backend",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"anthropic",
|
|
7
|
+
"claude",
|
|
8
|
+
"deepseek",
|
|
9
|
+
"express",
|
|
10
|
+
"glm",
|
|
11
|
+
"kimi",
|
|
12
|
+
"llm",
|
|
13
|
+
"mimo",
|
|
14
|
+
"minimax",
|
|
15
|
+
"proxy",
|
|
16
|
+
"qwen"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/sunflower0305/claude-proxy#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/sunflower0305/claude-proxy/issues"
|
|
21
|
+
},
|
|
6
22
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/sunflower0305/claude-proxy.git"
|
|
26
|
+
},
|
|
9
27
|
"bin": {
|
|
10
28
|
"claude-proxy": "dist/proxy.js"
|
|
11
29
|
},
|
|
12
|
-
"exports": {
|
|
13
|
-
".": {
|
|
14
|
-
"types": "./dist/proxy.d.ts",
|
|
15
|
-
"import": "./dist/proxy.js"
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
30
|
"files": [
|
|
19
31
|
"dist/",
|
|
20
32
|
"README.md",
|
|
@@ -22,42 +34,33 @@
|
|
|
22
34
|
"LICENSE",
|
|
23
35
|
".env.example"
|
|
24
36
|
],
|
|
25
|
-
"
|
|
26
|
-
|
|
37
|
+
"type": "module",
|
|
38
|
+
"main": "./dist/proxy.js",
|
|
39
|
+
"types": "./dist/proxy.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/proxy.d.ts",
|
|
43
|
+
"import": "./dist/proxy.js"
|
|
44
|
+
}
|
|
27
45
|
},
|
|
28
46
|
"publishConfig": {
|
|
29
47
|
"access": "public"
|
|
30
48
|
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/sunflower0305/claude-proxy.git"
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com/sunflower0305/claude-proxy#readme",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/sunflower0305/claude-proxy/issues"
|
|
38
|
-
},
|
|
39
|
-
"keywords": [
|
|
40
|
-
"anthropic",
|
|
41
|
-
"claude",
|
|
42
|
-
"proxy",
|
|
43
|
-
"express",
|
|
44
|
-
"llm",
|
|
45
|
-
"qwen",
|
|
46
|
-
"deepseek",
|
|
47
|
-
"glm",
|
|
48
|
-
"minimax",
|
|
49
|
-
"kimi",
|
|
50
|
-
"mimo"
|
|
51
|
-
],
|
|
52
49
|
"scripts": {
|
|
53
50
|
"dev": "tsx watch src/proxy.ts",
|
|
54
|
-
"build": "
|
|
51
|
+
"build": "vp pack",
|
|
55
52
|
"start": "node dist/proxy.js",
|
|
56
53
|
"prepack": "npm run build",
|
|
57
54
|
"prepublishOnly": "npm run test:proxy-local",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
55
|
+
"check": "vp check",
|
|
56
|
+
"check:fix": "vp check --fix",
|
|
57
|
+
"lint": "vp lint",
|
|
58
|
+
"lint:fix": "vp lint --fix",
|
|
59
|
+
"format": "vp fmt . --write",
|
|
60
|
+
"format:check": "vp fmt --check",
|
|
61
|
+
"test": "vp test",
|
|
62
|
+
"test:coverage": "vp test run tests/integration/proxy-local.test.ts --coverage --coverage.reporter=lcov --coverage.reporter=text --pool forks",
|
|
63
|
+
"test:proxy-local": "vp test run tests/integration/proxy-local.test.ts",
|
|
61
64
|
"test:provider-anthropic": "node --experimental-strip-types tests/integration/provider-anthropic.ts",
|
|
62
65
|
"test:provider-cli-e2e": "node --experimental-strip-types tests/integration/provider-cli-e2e.ts",
|
|
63
66
|
"report:provider-cli-e2e": "node --experimental-strip-types tests/integration/report-provider-cli-e2e.ts"
|
|
@@ -66,11 +69,16 @@
|
|
|
66
69
|
"express": "^4.21.0"
|
|
67
70
|
},
|
|
68
71
|
"devDependencies": {
|
|
69
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
70
72
|
"@types/express": "^4.17.21",
|
|
71
73
|
"@types/node": "^22.0.0",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
72
75
|
"tsx": "^4.19.0",
|
|
73
76
|
"typescript": "^5.5.0",
|
|
74
|
-
"
|
|
77
|
+
"vite": "^8.0.11",
|
|
78
|
+
"vite-plus": "^0.1.20",
|
|
79
|
+
"vitest": "^4.1.5"
|
|
80
|
+
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=20.12"
|
|
75
83
|
}
|
|
76
84
|
}
|