@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 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=qwen-plus
20
+ QWEN_MODEL=qwen3.7-plus
21
21
  DEEPSEEK_MODEL=deepseek-v4-pro
22
22
  GLM_MODEL=glm-5.1
23
- MINIMAX_MODEL=MiniMax-M2.7-highspeed
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
  [![npm version](https://img.shields.io/npm/v/%40sunflower0305%2Fclaude-proxy)](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/%40sunflower0305%2Fclaude-proxy?cacheSeconds=60)](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
8
8
  [![GitHub stars](https://img.shields.io/github/stars/sunflower0305/claude-proxy?cacheSeconds=60)](https://github.com/sunflower0305/claude-proxy/stargazers)
9
- [![License](https://img.shields.io/github/license/sunflower0305/claude-proxy)](https://github.com/sunflower0305/claude-proxy/blob/master/LICENSE)
9
+ [![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logoColor=ffffff)](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 | 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. |
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 | Model env | Default model |
66
- | --- | --- | --- |
67
- | **`deepseek` (default)** | `DEEPSEEK_MODEL` | **`deepseek-v4-pro`** |
68
- | `qwen` | `QWEN_MODEL` | `qwen-plus` |
69
- | `glm` | `GLM_MODEL` | `glm-5.1` |
70
- | `minimax` | `MINIMAX_MODEL` | `MiniMax-M2.7-highspeed` |
71
- | `kimi` | `KIMI_MODEL` | `kimi-k2.6` |
72
- | `mimo` | `MIMO_MODEL` | `mimo-v2.5-pro` |
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
- export declare function createApp(): express.Express;
15
- export declare const app: express.Express;
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
- if (existsSync(".env"))
19
- loadEnvFile(".env");
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
- "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",
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
- for (const key of keys) {
35
- const value = process.env[key]?.trim();
36
- if (value)
37
- return value;
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
- deepseek: {
43
- baseUrl: pickEnv("DEEPSEEK_ANTHROPIC_BASE_URL") ||
44
- "https://api.deepseek.com/anthropic",
45
- apiKey: process.env.DEEPSEEK_API_KEY || "",
46
- model: pickEnv("DEEPSEEK_MODEL") || "deepseek-v4-pro",
47
- },
48
- qwen: {
49
- baseUrl: pickEnv("QWEN_ANTHROPIC_BASE_URL") ||
50
- "https://dashscope.aliyuncs.com/apps/anthropic",
51
- apiKey: process.env.QWEN_API_KEY || "",
52
- model: pickEnv("QWEN_MODEL") || "qwen-plus",
53
- },
54
- glm: {
55
- baseUrl: pickEnv("GLM_ANTHROPIC_BASE_URL") ||
56
- "https://open.bigmodel.cn/api/anthropic",
57
- apiKey: process.env.GLM_API_KEY || "",
58
- model: pickEnv("GLM_MODEL") || "glm-5.1",
59
- },
60
- minimax: {
61
- baseUrl: pickEnv("MINIMAX_ANTHROPIC_BASE_URL") ||
62
- "https://api.minimaxi.com/anthropic",
63
- apiKey: process.env.MINIMAX_API_KEY || "",
64
- model: pickEnv("MINIMAX_MODEL") || "MiniMax-M2.7-highspeed",
65
- },
66
- kimi: {
67
- baseUrl: pickEnv("KIMI_ANTHROPIC_BASE_URL") || "https://api.moonshot.cn/anthropic",
68
- apiKey: process.env.KIMI_API_KEY || "",
69
- model: pickEnv("KIMI_MODEL") || "kimi-k2.6",
70
- },
71
- mimo: {
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
- return Boolean(value && value in PROVIDERS);
72
+ return Boolean(value && value in PROVIDERS);
80
73
  }
81
74
  function getInitialProvider() {
82
- return isProviderKey(process.env.PROVIDER)
83
- ? process.env.PROVIDER
84
- : "deepseek";
75
+ return isProviderKey(process.env.PROVIDER) ? process.env.PROVIDER : "deepseek";
85
76
  }
86
77
  function getProviderConfig(provider) {
87
- return PROVIDERS[provider] || PROVIDERS.deepseek;
78
+ return PROVIDERS[provider] || PROVIDERS.deepseek;
88
79
  }
89
80
  function getHeaderValue(value) {
90
- if (Array.isArray(value))
91
- return value.join(",");
92
- return value;
81
+ if (Array.isArray(value)) return value.join(",");
82
+ return value;
93
83
  }
94
84
  function buildUpstreamHeaders(req, stream, apiKey) {
95
- const headers = {
96
- "content-type": "application/json",
97
- "x-api-key": apiKey,
98
- "anthropic-version": getHeaderValue(req.headers["anthropic-version"]) ||
99
- DEFAULT_ANTHROPIC_VERSION,
100
- accept: getHeaderValue(req.headers.accept) ||
101
- (stream ? "text/event-stream" : "application/json"),
102
- };
103
- const anthropicBeta = getHeaderValue(req.headers["anthropic-beta"]);
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
- return `${baseUrl.replace(/\/$/, "")}/v1/messages`;
96
+ return `${baseUrl.replace(/\/$/, "")}/v1/messages`;
111
97
  }
112
98
  function buildUpstreamBody(body, targetModel) {
113
- const normalized = typeof body === "object" && body !== null
114
- ? { ...body }
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
- for (const [key, value] of upstream.headers.entries()) {
121
- if (HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase()))
122
- continue;
123
- res.setHeader(key, value);
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
- return {
128
- type: "error",
129
- error: {
130
- type: "internal_error",
131
- message,
132
- },
133
- };
110
+ return {
111
+ type: "error",
112
+ error: {
113
+ type: "internal_error",
114
+ message
115
+ }
116
+ };
134
117
  }
135
118
  function createAuthenticationError() {
136
- return {
137
- type: "error",
138
- error: {
139
- type: "authentication_error",
140
- message: "Invalid API key",
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
- const match = authorization?.match(/^Bearer\s+(.+)$/i);
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
- if (!model)
150
- return undefined;
151
- const normalizedModel = model.toLowerCase();
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
- console.log(`[ProxyTiming] ${JSON.stringify({
160
- request_id: trace.requestId,
161
- provider: trace.provider,
162
- requested_model: trace.requestedModel,
163
- target_model: trace.targetModel,
164
- stream: trace.stream,
165
- phase,
166
- elapsed_ms: Date.now() - trace.startedAt,
167
- at: new Date().toISOString(),
168
- ...extra,
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
- export function createApp() {
172
- let currentProvider = getInitialProvider();
173
- let requestSequence = 0;
174
- const proxyApiKey = pickEnv("PROXY_API_KEY");
175
- function getConfig(provider = currentProvider) {
176
- return getProviderConfig(provider);
177
- }
178
- function getTargetModel(requestedModel) {
179
- if (typeof requestedModel !== "string" || !requestedModel) {
180
- return getConfig().model;
181
- }
182
- const normalizedModel = requestedModel.toLowerCase();
183
- if (normalizedModel === "opus" ||
184
- normalizedModel === "sonnet" ||
185
- normalizedModel === "haiku") {
186
- return getConfig().model;
187
- }
188
- if (normalizedModel.startsWith("claude-") &&
189
- (normalizedModel.includes("-opus") ||
190
- normalizedModel.includes("-sonnet") ||
191
- normalizedModel.includes("-haiku"))) {
192
- return getConfig().model;
193
- }
194
- return requestedModel;
195
- }
196
- function createRequestTrace(requestedModel, targetModel, stream) {
197
- return {
198
- requestId: `req-${++requestSequence}`,
199
- provider: currentProvider,
200
- requestedModel: String(requestedModel || getConfig().model),
201
- targetModel,
202
- stream,
203
- startedAt: Date.now(),
204
- };
205
- }
206
- function requireProxyApiKey(req, res, next) {
207
- if (!proxyApiKey) {
208
- next();
209
- return;
210
- }
211
- const clientTokens = [
212
- getHeaderValue(req.headers["x-api-key"])?.trim(),
213
- getBearerToken(getHeaderValue(req.headers.authorization)),
214
- ].filter((token) => Boolean(token));
215
- if (clientTokens.includes(proxyApiKey)) {
216
- next();
217
- return;
218
- }
219
- res.status(401).json(createAuthenticationError());
220
- }
221
- async function handleNonStreamingRequest(req, res) {
222
- const config = getConfig();
223
- const targetModel = getTargetModel(req.body?.model);
224
- const requestBody = buildUpstreamBody(req.body, targetModel);
225
- const trace = createRequestTrace(req.body?.model, targetModel, false);
226
- logTimingEvent(trace, "start");
227
- try {
228
- const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
229
- method: "POST",
230
- headers: buildUpstreamHeaders(req, false, config.apiKey),
231
- body: JSON.stringify(requestBody),
232
- });
233
- logTimingEvent(trace, "upstream_headers", {
234
- status: upstream.status,
235
- content_type: upstream.headers.get("content-type") || "",
236
- });
237
- const payload = Buffer.from(await upstream.arrayBuffer());
238
- copyUpstreamHeaders(upstream, res);
239
- res.status(upstream.status).send(payload);
240
- logTimingEvent(trace, "completed", {
241
- status: upstream.status,
242
- bytes: payload.byteLength,
243
- });
244
- }
245
- catch (error) {
246
- console.error("Request error:", error);
247
- logTimingEvent(trace, "error", {
248
- message: error?.message || String(error),
249
- });
250
- res.status(500).json(createProxyError(error.message));
251
- }
252
- }
253
- async function handleStreamingRequest(req, res) {
254
- const config = getConfig();
255
- const targetModel = getTargetModel(req.body?.model);
256
- const requestBody = buildUpstreamBody(req.body, targetModel);
257
- const trace = createRequestTrace(req.body?.model, targetModel, true);
258
- const abortController = new AbortController();
259
- let clientClosed = false;
260
- let streamCompleted = false;
261
- let sawFirstChunk = false;
262
- logTimingEvent(trace, "start");
263
- res.on("close", () => {
264
- if (streamCompleted)
265
- return;
266
- clientClosed = true;
267
- abortController.abort();
268
- logTimingEvent(trace, "client_aborted");
269
- });
270
- try {
271
- const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
272
- method: "POST",
273
- headers: buildUpstreamHeaders(req, true, config.apiKey),
274
- body: JSON.stringify(requestBody),
275
- signal: abortController.signal,
276
- });
277
- logTimingEvent(trace, "upstream_headers", {
278
- status: upstream.status,
279
- content_type: upstream.headers.get("content-type") || "",
280
- });
281
- copyUpstreamHeaders(upstream, res);
282
- res.status(upstream.status);
283
- if (!upstream.body) {
284
- streamCompleted = true;
285
- res.end();
286
- logTimingEvent(trace, "completed", {
287
- status: upstream.status,
288
- bytes: 0,
289
- no_body: true,
290
- });
291
- return;
292
- }
293
- const upstreamStream = Readable.fromWeb(upstream.body);
294
- upstreamStream.on("data", (chunk) => {
295
- if (sawFirstChunk)
296
- return;
297
- sawFirstChunk = true;
298
- const chunkSize = Buffer.isBuffer(chunk)
299
- ? chunk.byteLength
300
- : Buffer.byteLength(String(chunk));
301
- logTimingEvent(trace, "first_chunk", {
302
- status: upstream.status,
303
- chunk_bytes: chunkSize,
304
- });
305
- });
306
- upstreamStream.on("error", (error) => {
307
- if (clientClosed)
308
- return;
309
- console.error("Upstream stream error:", error);
310
- logTimingEvent(trace, "error", {
311
- status: upstream.status,
312
- message: error?.message || String(error),
313
- });
314
- if (!res.writableEnded)
315
- res.end();
316
- });
317
- upstreamStream.pipe(res);
318
- await new Promise((resolve, reject) => {
319
- upstreamStream.on("end", () => {
320
- streamCompleted = true;
321
- logTimingEvent(trace, "completed", {
322
- status: upstream.status,
323
- });
324
- resolve();
325
- });
326
- upstreamStream.on("error", reject);
327
- res.on("close", () => resolve());
328
- });
329
- }
330
- catch (error) {
331
- const wasAborted = error?.name === "AbortError" || abortController.signal.aborted;
332
- if (clientClosed || wasAborted) {
333
- console.warn("[Proxy] Client disconnected, streaming aborted");
334
- return;
335
- }
336
- console.error("Request error:", error);
337
- logTimingEvent(trace, "error", {
338
- message: error?.message || String(error),
339
- });
340
- if (!res.headersSent) {
341
- res.status(500).json(createProxyError(error.message));
342
- return;
343
- }
344
- if (!res.writableEnded)
345
- res.end();
346
- }
347
- }
348
- const app = express();
349
- app.use(express.json({ limit: "32mb" }));
350
- app.get("/", (_req, res) => {
351
- const config = getConfig();
352
- res.json({
353
- name: "claude-proxy",
354
- status: "running",
355
- provider: currentProvider,
356
- model: config.model,
357
- endpoints: {
358
- messages: "POST /v1/messages",
359
- health: "GET /health",
360
- models: "GET /v1/models",
361
- provider: "GET|POST /api/provider",
362
- },
363
- });
364
- });
365
- app.post("/v1/messages", requireProxyApiKey, async (req, res) => {
366
- if (req.body?.stream) {
367
- await handleStreamingRequest(req, res);
368
- return;
369
- }
370
- await handleNonStreamingRequest(req, res);
371
- });
372
- app.get("/health", (_req, res) => {
373
- const config = getConfig();
374
- res.json({ status: "ok", provider: currentProvider, model: config.model });
375
- });
376
- app.get("/v1/models", (_req, res) => {
377
- res.json({
378
- data: [
379
- { id: "claude-opus-4-7", object: "model" },
380
- { id: "claude-sonnet-4-6", object: "model" },
381
- { id: "claude-haiku-4-5", object: "model" },
382
- ],
383
- });
384
- });
385
- app.get("/api/provider", (_req, res) => {
386
- const config = getConfig();
387
- res.json({
388
- provider: currentProvider,
389
- model: config.model,
390
- baseUrl: config.baseUrl,
391
- availableProviders: Object.keys(PROVIDERS),
392
- });
393
- });
394
- app.post("/api/provider", requireProxyApiKey, (req, res) => {
395
- const { provider, model } = (req.body ?? {});
396
- const targetProvider = provider ?? inferProviderFromModel(model);
397
- if (!isProviderKey(targetProvider)) {
398
- res.status(400).json({
399
- error: `Unknown provider: ${targetProvider}`,
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
- export const app = createApp();
378
+ const app = createApp();
423
379
  const PORT = parseInt(process.env.PROXY_PORT || "8080", 10);
424
380
  function isMainModule() {
425
- const entryPath = process.argv[1];
426
- if (!entryPath)
427
- return false;
428
- try {
429
- return (realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url)));
430
- }
431
- catch {
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
- const initialProvider = getInitialProvider();
437
- const initialConfig = getProviderConfig(initialProvider);
438
- const clientApiKeyHint = pickEnv("PROXY_API_KEY")
439
- ? "same value as PROXY_API_KEY"
440
- : "any-string-works";
441
- if (!initialConfig.apiKey) {
442
- console.warn(`Warning: API key not configured for provider: ${initialProvider}`);
443
- console.warn("Please set the appropriate environment variable in .env");
444
- }
445
- console.log(`Using ${initialProvider} as backend`);
446
- console.log(`Model: ${initialConfig.model}`);
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.0",
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
- "main": "./dist/proxy.js",
8
- "types": "./dist/proxy.d.ts",
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
- "engines": {
26
- "node": ">=20.12"
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": "tsc",
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
- "test": "vitest run",
59
- "test:coverage": "vitest run tests/integration/proxy-local.test.ts --coverage --coverage.reporter=lcov --coverage.reporter=text --pool forks",
60
- "test:proxy-local": "vitest run tests/integration/proxy-local.test.ts",
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
- "vitest": "^3.2.4"
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
  }