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