@sunflower0305/claude-proxy 1.0.0 → 1.1.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
@@ -14,9 +14,9 @@ KIMI_API_KEY=your-kimi-key
14
14
  # Optional model overrides
15
15
  QWEN_MODEL=qwen-plus
16
16
  DEEPSEEK_MODEL=deepseek-chat
17
- GLM_MODEL=glm-5
17
+ GLM_MODEL=glm-5.1
18
18
  MINIMAX_MODEL=MiniMax-M2.7-highspeed
19
- KIMI_MODEL=kimi-k2.5
19
+ KIMI_MODEL=kimi-k2.6
20
20
 
21
21
  # Optional upstream Anthropic-compatible base URL overrides
22
22
  # QWEN_ANTHROPIC_BASE_URL=https://dashscope.aliyuncs.com/apps/anthropic
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # claude-proxy
2
2
 
3
+ [![CI](https://github.com/sunflower0305/claude-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/sunflower0305/claude-proxy/actions/workflows/ci.yml)
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
+ [![Coverage Status](https://coveralls.io/repos/github/sunflower0305/claude-proxy/badge.svg?branch=master)](https://coveralls.io/github/sunflower0305/claude-proxy?branch=master)
6
+ [![npm version](https://img.shields.io/npm/v/%40sunflower0305%2Fclaude-proxy)](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
7
+ [![License](https://img.shields.io/github/license/sunflower0305/claude-proxy)](https://github.com/sunflower0305/claude-proxy/blob/master/LICENSE)
8
+
3
9
  `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.
4
10
 
5
11
  It currently supports `qwen`, `deepseek`, `glm`, `minimax`, and `kimi`.
@@ -34,17 +40,17 @@ QWEN_MODEL=qwen-plus
34
40
 
35
41
  Available variables:
36
42
 
37
- | Variable | Purpose |
38
- | --- | --- |
39
- | `PROVIDER` | Active provider. Defaults to `qwen`. |
40
- | `PROXY_PORT` | Local server port. Defaults to `8080`. |
41
- | `QWEN_API_KEY` | API key for Qwen. |
42
- | `DEEPSEEK_API_KEY` | API key for DeepSeek. |
43
- | `GLM_API_KEY` | API key for GLM. |
44
- | `MINIMAX_API_KEY` | API key for MiniMax. |
45
- | `KIMI_API_KEY` | API key for Kimi. |
43
+ | Variable | Purpose |
44
+ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
45
+ | `PROVIDER` | Active provider. Defaults to `qwen`. |
46
+ | `PROXY_PORT` | Local server port. Defaults to `8080`. |
47
+ | `QWEN_API_KEY` | API key for Qwen. |
48
+ | `DEEPSEEK_API_KEY` | API key for DeepSeek. |
49
+ | `GLM_API_KEY` | API key for GLM. |
50
+ | `MINIMAX_API_KEY` | API key for MiniMax. |
51
+ | `KIMI_API_KEY` | API key for Kimi. |
46
52
  | `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. |
47
- | `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL` | Override the default upstream model for a provider. |
53
+ | `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL` | Override the default upstream model for a provider. |
48
54
 
49
55
  You can use the bundled example as a starting point:
50
56
 
@@ -82,12 +88,12 @@ const client = new Anthropic({
82
88
 
83
89
  ## Runtime Endpoints
84
90
 
85
- | Method | Path | Description |
86
- | --- | --- | --- |
87
- | `POST` | `/v1/messages` | Main Anthropic Messages API proxy endpoint |
88
- | `GET` | `/v1/models` | Lists supported Claude-facing model ids |
89
- | `GET` | `/health` | Health check |
90
- | `GET` / `POST` | `/api/provider` | Read or switch the active provider |
91
+ | Method | Path | Description |
92
+ | -------------- | --------------- | ------------------------------------------ |
93
+ | `POST` | `/v1/messages` | Main Anthropic Messages API proxy endpoint |
94
+ | `GET` | `/v1/models` | Lists supported Claude-facing model ids |
95
+ | `GET` | `/health` | Health check |
96
+ | `GET` / `POST` | `/api/provider` | Read or switch the active provider |
91
97
 
92
98
  Health check:
93
99
 
@@ -114,6 +120,25 @@ const app = createApp();
114
120
  app.listen(8080);
115
121
  ```
116
122
 
123
+ ## Release Verification
124
+
125
+ `v1.1.0` was verified on April 21, 2026 after publishing `@sunflower0305/claude-proxy` to npm.
126
+
127
+ Verified items:
128
+
129
+ - `npm view @sunflower0305/claude-proxy version dist-tags --json` confirmed `version: 1.1.0` and `latest: 1.1.0`
130
+ - `npm install @sunflower0305/claude-proxy` completed successfully in a clean temporary directory
131
+ - the published `claude-proxy` CLI started correctly from the installed package
132
+ - `GET /health` and `GET /v1/models` returned `200 OK`
133
+ - local end-to-end proxying against a mock Anthropic-compatible upstream passed for both non-streaming and streaming `POST /v1/messages`
134
+
135
+ Observed behavior during verification:
136
+
137
+ - smoke-test startup succeeded with `PROVIDER=qwen`
138
+ - the published artifact returned the expected `health` payload with `provider: qwen` and `model: qwen-plus`
139
+ - the published artifact returned the expected Claude-facing model list from `GET /v1/models`
140
+ - the published package included the expected CLI entrypoint, `dist/` build output, `README.md`, `LICENSE`, and `.env.example`
141
+
117
142
  ## Development
118
143
 
119
144
  From source:
@@ -123,6 +148,35 @@ npm install
123
148
  npm run dev
124
149
  ```
125
150
 
151
+ ## CI And Releases
152
+
153
+ GitHub Actions provides separate CI and CD workflows:
154
+
155
+ - `CI` runs on branch pushes and pull requests
156
+ - `CD` runs when you push a `vX.Y.Z` tag and can also be re-run manually with `workflow_dispatch`
157
+
158
+ The `CD` workflow:
159
+
160
+ - checks that the tag exactly matches `package.json.version`
161
+ - requires `docs/releases/<version>.md` to exist before publishing
162
+ - runs `npm run build`, `npm run test:proxy-local`, `npm run test:coverage`, and `npm pack --dry-run`
163
+ - fails fast if the npm version already exists
164
+ - publishes the package to npm using the `NPM_TOKEN` repository secret
165
+ - creates or updates the GitHub Release from `docs/releases/<version>.md`
166
+
167
+ Required repository secret:
168
+
169
+ - `NPM_TOKEN`: npm automation token with permission to publish `@sunflower0305/claude-proxy`
170
+
171
+ Release flow:
172
+
173
+ 1. bump `package.json`, `CHANGELOG.md`, and `docs/releases/<version>.md`
174
+ 2. commit the release prep
175
+ 3. create and push the matching `vX.Y.Z` tag
176
+ 4. let the `CD` workflow publish npm and GitHub Release
177
+
178
+ The package still relies on `prepack` and `prepublishOnly` in `package.json` to build and verify the artifact before release.
179
+
126
180
  Build and local package verification:
127
181
 
128
182
  ```bash
@@ -136,6 +190,8 @@ Local integration test:
136
190
  npm run test:proxy-local
137
191
  ```
138
192
 
193
+ Release notes for `v1.1.0` are available in [docs/releases/1.1.0.md](docs/releases/1.1.0.md).
194
+
139
195
  ## License
140
196
 
141
197
  MIT
package/dist/proxy.js CHANGED
@@ -53,7 +53,7 @@ const PROVIDERS = {
53
53
  baseUrl: pickEnv("GLM_ANTHROPIC_BASE_URL") ||
54
54
  "https://open.bigmodel.cn/api/anthropic",
55
55
  apiKey: process.env.GLM_API_KEY || "",
56
- model: pickEnv("GLM_MODEL") || "glm-5",
56
+ model: pickEnv("GLM_MODEL") || "glm-5.1",
57
57
  },
58
58
  minimax: {
59
59
  baseUrl: pickEnv("MINIMAX_ANTHROPIC_BASE_URL") ||
@@ -64,43 +64,17 @@ const PROVIDERS = {
64
64
  kimi: {
65
65
  baseUrl: pickEnv("KIMI_ANTHROPIC_BASE_URL") || "https://api.moonshot.cn/anthropic",
66
66
  apiKey: process.env.KIMI_API_KEY || "",
67
- model: pickEnv("KIMI_MODEL") || "kimi-k2.5",
67
+ model: pickEnv("KIMI_MODEL") || "kimi-k2.6",
68
68
  },
69
69
  };
70
70
  function isProviderKey(value) {
71
71
  return Boolean(value && value in PROVIDERS);
72
72
  }
73
- let currentProvider = isProviderKey(process.env.PROVIDER)
74
- ? process.env.PROVIDER
75
- : "qwen";
76
- let requestSequence = 0;
77
- function getConfig(provider = currentProvider) {
78
- return PROVIDERS[provider] || PROVIDERS.qwen;
79
- }
80
- const initialConfig = getConfig();
81
- if (!initialConfig.apiKey) {
82
- console.warn(`Warning: API key not configured for provider: ${currentProvider}`);
83
- console.warn("Please set the appropriate environment variable in .env");
73
+ function getInitialProvider() {
74
+ return isProviderKey(process.env.PROVIDER) ? process.env.PROVIDER : "qwen";
84
75
  }
85
- console.log(`Using ${currentProvider} as backend`);
86
- console.log(`Model: ${initialConfig.model}`);
87
- function getTargetModel(requestedModel) {
88
- if (typeof requestedModel !== "string" || !requestedModel) {
89
- return getConfig().model;
90
- }
91
- const normalizedModel = requestedModel.toLowerCase();
92
- if (normalizedModel === "opus" ||
93
- normalizedModel === "sonnet" ||
94
- normalizedModel === "haiku") {
95
- return getConfig().model;
96
- }
97
- if (normalizedModel.startsWith("claude-") &&
98
- (normalizedModel.includes("-opus") ||
99
- normalizedModel.includes("-sonnet") ||
100
- normalizedModel.includes("-haiku"))) {
101
- return getConfig().model;
102
- }
103
- return requestedModel;
76
+ function getProviderConfig(provider) {
77
+ return PROVIDERS[provider] || PROVIDERS.qwen;
104
78
  }
105
79
  function getHeaderValue(value) {
106
80
  if (Array.isArray(value))
@@ -148,15 +122,15 @@ function createProxyError(message) {
148
122
  },
149
123
  };
150
124
  }
151
- function createRequestTrace(requestedModel, targetModel, stream) {
152
- return {
153
- requestId: `req-${++requestSequence}`,
154
- provider: currentProvider,
155
- requestedModel: String(requestedModel || getConfig().model),
156
- targetModel,
157
- stream,
158
- startedAt: Date.now(),
159
- };
125
+ function inferProviderFromModel(model) {
126
+ if (!model)
127
+ return undefined;
128
+ const normalizedModel = model.toLowerCase();
129
+ for (const key of Object.keys(PROVIDERS)) {
130
+ if (normalizedModel.includes(key))
131
+ return key;
132
+ }
133
+ return undefined;
160
134
  }
161
135
  function logTimingEvent(trace, phase, extra = {}) {
162
136
  console.log(`[ProxyTiming] ${JSON.stringify({
@@ -171,132 +145,167 @@ function logTimingEvent(trace, phase, extra = {}) {
171
145
  ...extra,
172
146
  })}`);
173
147
  }
174
- async function handleNonStreamingRequest(req, res) {
175
- const config = getConfig();
176
- const targetModel = getTargetModel(req.body?.model);
177
- const requestBody = buildUpstreamBody(req.body, targetModel);
178
- const trace = createRequestTrace(req.body?.model, targetModel, false);
179
- console.log(`\n[${new Date().toISOString()}] ${String(req.body?.model || config.model)} -> ${targetModel} (non-streaming)`);
180
- logTimingEvent(trace, "start");
181
- try {
182
- const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
183
- method: "POST",
184
- headers: buildUpstreamHeaders(req, false, config.apiKey),
185
- body: JSON.stringify(requestBody),
186
- });
187
- logTimingEvent(trace, "upstream_headers", {
188
- status: upstream.status,
189
- content_type: upstream.headers.get("content-type") || "",
190
- });
191
- const payload = Buffer.from(await upstream.arrayBuffer());
192
- copyUpstreamHeaders(upstream, res);
193
- res.status(upstream.status).send(payload);
194
- logTimingEvent(trace, "completed", {
195
- status: upstream.status,
196
- bytes: payload.byteLength,
197
- });
148
+ export function createApp() {
149
+ let currentProvider = getInitialProvider();
150
+ let requestSequence = 0;
151
+ function getConfig(provider = currentProvider) {
152
+ return getProviderConfig(provider);
198
153
  }
199
- catch (error) {
200
- console.error("Request error:", error);
201
- logTimingEvent(trace, "error", { message: error?.message || String(error) });
202
- res.status(500).json(createProxyError(error.message));
154
+ function getTargetModel(requestedModel) {
155
+ if (typeof requestedModel !== "string" || !requestedModel) {
156
+ return getConfig().model;
157
+ }
158
+ const normalizedModel = requestedModel.toLowerCase();
159
+ if (normalizedModel === "opus" ||
160
+ normalizedModel === "sonnet" ||
161
+ normalizedModel === "haiku") {
162
+ return getConfig().model;
163
+ }
164
+ if (normalizedModel.startsWith("claude-") &&
165
+ (normalizedModel.includes("-opus") ||
166
+ normalizedModel.includes("-sonnet") ||
167
+ normalizedModel.includes("-haiku"))) {
168
+ return getConfig().model;
169
+ }
170
+ return requestedModel;
203
171
  }
204
- }
205
- async function handleStreamingRequest(req, res) {
206
- const config = getConfig();
207
- const targetModel = getTargetModel(req.body?.model);
208
- const requestBody = buildUpstreamBody(req.body, targetModel);
209
- const trace = createRequestTrace(req.body?.model, targetModel, true);
210
- const abortController = new AbortController();
211
- let clientClosed = false;
212
- let streamCompleted = false;
213
- let sawFirstChunk = false;
214
- console.log(`\n[${new Date().toISOString()}] ${String(req.body?.model || config.model)} -> ${targetModel} (streaming)`);
215
- logTimingEvent(trace, "start");
216
- res.on("close", () => {
217
- if (streamCompleted)
218
- return;
219
- clientClosed = true;
220
- abortController.abort();
221
- logTimingEvent(trace, "client_aborted");
222
- });
223
- try {
224
- const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
225
- method: "POST",
226
- headers: buildUpstreamHeaders(req, true, config.apiKey),
227
- body: JSON.stringify(requestBody),
228
- signal: abortController.signal,
229
- });
230
- logTimingEvent(trace, "upstream_headers", {
231
- status: upstream.status,
232
- content_type: upstream.headers.get("content-type") || "",
233
- });
234
- copyUpstreamHeaders(upstream, res);
235
- res.status(upstream.status);
236
- if (!upstream.body) {
237
- streamCompleted = true;
238
- res.end();
239
- logTimingEvent(trace, "completed", {
172
+ function createRequestTrace(requestedModel, targetModel, stream) {
173
+ return {
174
+ requestId: `req-${++requestSequence}`,
175
+ provider: currentProvider,
176
+ requestedModel: String(requestedModel || getConfig().model),
177
+ targetModel,
178
+ stream,
179
+ startedAt: Date.now(),
180
+ };
181
+ }
182
+ async function handleNonStreamingRequest(req, res) {
183
+ const config = getConfig();
184
+ const targetModel = getTargetModel(req.body?.model);
185
+ const requestBody = buildUpstreamBody(req.body, targetModel);
186
+ const trace = createRequestTrace(req.body?.model, targetModel, false);
187
+ logTimingEvent(trace, "start");
188
+ try {
189
+ const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
190
+ method: "POST",
191
+ headers: buildUpstreamHeaders(req, false, config.apiKey),
192
+ body: JSON.stringify(requestBody),
193
+ });
194
+ logTimingEvent(trace, "upstream_headers", {
240
195
  status: upstream.status,
241
- bytes: 0,
242
- no_body: true,
196
+ content_type: upstream.headers.get("content-type") || "",
243
197
  });
244
- return;
245
- }
246
- const upstreamStream = Readable.fromWeb(upstream.body);
247
- upstreamStream.on("data", (chunk) => {
248
- if (sawFirstChunk)
249
- return;
250
- sawFirstChunk = true;
251
- const chunkSize = Buffer.isBuffer(chunk)
252
- ? chunk.byteLength
253
- : Buffer.byteLength(String(chunk));
254
- logTimingEvent(trace, "first_chunk", {
198
+ const payload = Buffer.from(await upstream.arrayBuffer());
199
+ copyUpstreamHeaders(upstream, res);
200
+ res.status(upstream.status).send(payload);
201
+ logTimingEvent(trace, "completed", {
255
202
  status: upstream.status,
256
- chunk_bytes: chunkSize,
203
+ bytes: payload.byteLength,
257
204
  });
258
- });
259
- upstreamStream.on("error", (error) => {
260
- if (clientClosed)
261
- return;
262
- console.error("Upstream stream error:", error);
205
+ }
206
+ catch (error) {
207
+ console.error("Request error:", error);
263
208
  logTimingEvent(trace, "error", {
264
- status: upstream.status,
265
209
  message: error?.message || String(error),
266
210
  });
267
- if (!res.writableEnded)
268
- res.end();
211
+ res.status(500).json(createProxyError(error.message));
212
+ }
213
+ }
214
+ async function handleStreamingRequest(req, res) {
215
+ const config = getConfig();
216
+ const targetModel = getTargetModel(req.body?.model);
217
+ const requestBody = buildUpstreamBody(req.body, targetModel);
218
+ const trace = createRequestTrace(req.body?.model, targetModel, true);
219
+ const abortController = new AbortController();
220
+ let clientClosed = false;
221
+ let streamCompleted = false;
222
+ let sawFirstChunk = false;
223
+ logTimingEvent(trace, "start");
224
+ res.on("close", () => {
225
+ if (streamCompleted)
226
+ return;
227
+ clientClosed = true;
228
+ abortController.abort();
229
+ logTimingEvent(trace, "client_aborted");
269
230
  });
270
- upstreamStream.pipe(res);
271
- await new Promise((resolve, reject) => {
272
- upstreamStream.on("end", () => {
231
+ try {
232
+ const upstream = await fetch(getUpstreamUrl(config.baseUrl), {
233
+ method: "POST",
234
+ headers: buildUpstreamHeaders(req, true, config.apiKey),
235
+ body: JSON.stringify(requestBody),
236
+ signal: abortController.signal,
237
+ });
238
+ logTimingEvent(trace, "upstream_headers", {
239
+ status: upstream.status,
240
+ content_type: upstream.headers.get("content-type") || "",
241
+ });
242
+ copyUpstreamHeaders(upstream, res);
243
+ res.status(upstream.status);
244
+ if (!upstream.body) {
273
245
  streamCompleted = true;
246
+ res.end();
274
247
  logTimingEvent(trace, "completed", {
275
248
  status: upstream.status,
249
+ bytes: 0,
250
+ no_body: true,
251
+ });
252
+ return;
253
+ }
254
+ const upstreamStream = Readable.fromWeb(upstream.body);
255
+ upstreamStream.on("data", (chunk) => {
256
+ if (sawFirstChunk)
257
+ return;
258
+ sawFirstChunk = true;
259
+ const chunkSize = Buffer.isBuffer(chunk)
260
+ ? chunk.byteLength
261
+ : Buffer.byteLength(String(chunk));
262
+ logTimingEvent(trace, "first_chunk", {
263
+ status: upstream.status,
264
+ chunk_bytes: chunkSize,
276
265
  });
277
- resolve();
278
266
  });
279
- upstreamStream.on("error", reject);
280
- res.on("close", () => resolve());
281
- });
282
- }
283
- catch (error) {
284
- const wasAborted = error?.name === "AbortError" || abortController.signal.aborted;
285
- if (clientClosed || wasAborted) {
286
- console.warn("[Proxy] Client disconnected, streaming aborted");
287
- return;
267
+ upstreamStream.on("error", (error) => {
268
+ if (clientClosed)
269
+ return;
270
+ console.error("Upstream stream error:", error);
271
+ logTimingEvent(trace, "error", {
272
+ status: upstream.status,
273
+ message: error?.message || String(error),
274
+ });
275
+ if (!res.writableEnded)
276
+ res.end();
277
+ });
278
+ upstreamStream.pipe(res);
279
+ await new Promise((resolve, reject) => {
280
+ upstreamStream.on("end", () => {
281
+ streamCompleted = true;
282
+ logTimingEvent(trace, "completed", {
283
+ status: upstream.status,
284
+ });
285
+ resolve();
286
+ });
287
+ upstreamStream.on("error", reject);
288
+ res.on("close", () => resolve());
289
+ });
288
290
  }
289
- console.error("Request error:", error);
290
- logTimingEvent(trace, "error", { message: error?.message || String(error) });
291
- if (!res.headersSent) {
292
- res.status(500).json(createProxyError(error.message));
293
- return;
291
+ catch (error) {
292
+ const wasAborted = error?.name === "AbortError" || abortController.signal.aborted;
293
+ if (clientClosed || wasAborted) {
294
+ console.warn("[Proxy] Client disconnected, streaming aborted");
295
+ return;
296
+ }
297
+ console.error("Request error:", error);
298
+ logTimingEvent(trace, "error", {
299
+ message: error?.message || String(error),
300
+ });
301
+ if (!res.headersSent) {
302
+ res.status(500).json(createProxyError(error.message));
303
+ return;
304
+ }
305
+ if (!res.writableEnded)
306
+ res.end();
294
307
  }
295
- if (!res.writableEnded)
296
- res.end();
297
308
  }
298
- }
299
- export function createApp() {
300
309
  const app = express();
301
310
  app.use(cors());
302
311
  app.use(express.json({ limit: "50mb" }));
@@ -329,7 +338,7 @@ export function createApp() {
329
338
  app.get("/v1/models", (_req, res) => {
330
339
  res.json({
331
340
  data: [
332
- { id: "claude-opus-4-6", object: "model" },
341
+ { id: "claude-opus-4-7", object: "model" },
333
342
  { id: "claude-sonnet-4-6", object: "model" },
334
343
  { id: "claude-haiku-4-5", object: "model" },
335
344
  ],
@@ -346,22 +355,7 @@ export function createApp() {
346
355
  });
347
356
  app.post("/api/provider", (req, res) => {
348
357
  const { provider, model } = (req.body ?? {});
349
- let targetProvider = provider;
350
- if (!targetProvider && model) {
351
- const normalizedModel = model.toLowerCase();
352
- if (normalizedModel.includes("kimi")) {
353
- targetProvider = "kimi";
354
- }
355
- else if (normalizedModel.includes("qwen"))
356
- targetProvider = "qwen";
357
- else if (normalizedModel.includes("deepseek"))
358
- targetProvider = "deepseek";
359
- else if (normalizedModel.includes("glm"))
360
- targetProvider = "glm";
361
- else if (normalizedModel.includes("minimax")) {
362
- targetProvider = "minimax";
363
- }
364
- }
358
+ const targetProvider = provider ?? inferProviderFromModel(model);
365
359
  if (!isProviderKey(targetProvider)) {
366
360
  res.status(400).json({
367
361
  error: `Unknown provider: ${targetProvider}`,
@@ -394,26 +388,33 @@ function isMainModule() {
394
388
  if (!entryPath)
395
389
  return false;
396
390
  try {
397
- return realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url));
391
+ return (realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url)));
398
392
  }
399
393
  catch {
400
394
  return false;
401
395
  }
402
396
  }
403
397
  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}`);
404
406
  app.listen(PORT, () => {
405
- const cfg = getConfig();
406
407
  console.log(`
407
- ╔════════════════════════════════════════════════╗
408
- claude-proxy ║
409
- ╠════════════════════════════════════════════════╣
408
+ ╔═══════════════════════════════════════════════════════╗
409
+ claude-proxy ║
410
+ ╠═══════════════════════════════════════════════════════╣
410
411
  ║ http://localhost:${PORT}
411
- ║ Backend: ${currentProvider} (${cfg.model})
412
- ╠════════════════════════════════════════════════╣
413
- ║ Set these env vars in your app:
412
+ ║ Backend: ${initialProvider} (${initialConfig.model})
413
+ ╠═══════════════════════════════════════════════════════╣
414
+ ║ Set these env vars in your app:
414
415
  ║ ANTHROPIC_BASE_URL=http://localhost:${PORT}
415
- ║ ANTHROPIC_API_KEY=any-string-works
416
- ╚════════════════════════════════════════════════╝
416
+ ║ ANTHROPIC_API_KEY=any-string-works
417
+ ╚═══════════════════════════════════════════════════════╝
417
418
  `);
418
419
  });
419
420
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@sunflower0305/claude-proxy",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "A proxy that lets Claude Agent SDK use domestic Chinese LLMs (DeepSeek, Qwen, GLM, MiniMax) as backend",
6
6
  "license": "MIT",
7
7
  "main": "./dist/proxy.js",
8
8
  "types": "./dist/proxy.d.ts",
9
9
  "bin": {
10
- "claude-proxy": "./dist/proxy.js"
10
+ "claude-proxy": "dist/proxy.js"
11
11
  },
12
12
  "exports": {
13
13
  ".": {
@@ -54,6 +54,7 @@
54
54
  "prepack": "npm run build",
55
55
  "prepublishOnly": "npm run test:proxy-local",
56
56
  "test": "vitest run",
57
+ "test:coverage": "vitest run tests/integration/proxy-local.test.ts --coverage --coverage.reporter=lcov --coverage.reporter=text --pool threads",
57
58
  "test:proxy-local": "vitest run tests/integration/proxy-local.test.ts",
58
59
  "test:provider-anthropic": "node --experimental-strip-types tests/integration/provider-anthropic.ts",
59
60
  "test:provider-cli-e2e": "node --experimental-strip-types tests/integration/provider-cli-e2e.ts",
@@ -65,6 +66,7 @@
65
66
  "express": "^4.21.0"
66
67
  },
67
68
  "devDependencies": {
69
+ "@vitest/coverage-v8": "^3.2.4",
68
70
  "@types/cors": "^2.8.17",
69
71
  "@types/express": "^4.17.21",
70
72
  "@types/node": "^22.0.0",