@sunflower0305/claude-proxy 1.0.0

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 ADDED
@@ -0,0 +1,26 @@
1
+ # Choose your provider: qwen | deepseek | glm | minimax | kimi
2
+ PROVIDER=qwen
3
+
4
+ # Proxy server port (default: 8080)
5
+ PROXY_PORT=8080
6
+
7
+ # API keys - set the one(s) you need
8
+ QWEN_API_KEY=your-qwen-api-key
9
+ DEEPSEEK_API_KEY=your-deepseek-key
10
+ GLM_API_KEY=your-glm-key
11
+ MINIMAX_API_KEY=your-minimax-key
12
+ KIMI_API_KEY=your-kimi-key
13
+
14
+ # Optional model overrides
15
+ QWEN_MODEL=qwen-plus
16
+ DEEPSEEK_MODEL=deepseek-chat
17
+ GLM_MODEL=glm-5
18
+ MINIMAX_MODEL=MiniMax-M2.7-highspeed
19
+ KIMI_MODEL=kimi-k2.5
20
+
21
+ # Optional upstream Anthropic-compatible base URL overrides
22
+ # QWEN_ANTHROPIC_BASE_URL=https://dashscope.aliyuncs.com/apps/anthropic
23
+ # DEEPSEEK_ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
24
+ # GLM_ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
25
+ # MINIMAX_ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
26
+ # KIMI_ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sunflower0305
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # claude-proxy
2
+
3
+ `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
+
5
+ It currently supports `qwen`, `deepseek`, `glm`, `minimax`, and `kimi`.
6
+
7
+ ## Install
8
+
9
+ Run without installing:
10
+
11
+ ```bash
12
+ npx @sunflower0305/claude-proxy
13
+ ```
14
+
15
+ Or install globally:
16
+
17
+ ```bash
18
+ npm install -g @sunflower0305/claude-proxy
19
+ claude-proxy
20
+ ```
21
+
22
+ ## Configure
23
+
24
+ The proxy reads configuration from environment variables. You can export them in your shell or create a `.env` file in the directory where you run `claude-proxy`.
25
+
26
+ Example `.env`:
27
+
28
+ ```dotenv
29
+ PROVIDER=qwen
30
+ PROXY_PORT=8080
31
+ QWEN_API_KEY=your-qwen-api-key
32
+ QWEN_MODEL=qwen-plus
33
+ ```
34
+
35
+ Available variables:
36
+
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. |
46
+ | `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. |
48
+
49
+ You can use the bundled example as a starting point:
50
+
51
+ ```bash
52
+ cp node_modules/@sunflower0305/claude-proxy/.env.example .env
53
+ ```
54
+
55
+ If you installed globally, create `.env` manually or export the variables in your shell before starting the proxy.
56
+
57
+ ## Start The Proxy
58
+
59
+ ```bash
60
+ claude-proxy
61
+ ```
62
+
63
+ When the server starts, it listens on `http://localhost:8080` by default.
64
+
65
+ Point Claude Code or the Claude Agent SDK at the proxy:
66
+
67
+ ```bash
68
+ export ANTHROPIC_BASE_URL=http://localhost:8080
69
+ export ANTHROPIC_API_KEY=any-string-works
70
+ ```
71
+
72
+ Example SDK usage:
73
+
74
+ ```ts
75
+ import Anthropic from "@anthropic-ai/sdk";
76
+
77
+ const client = new Anthropic({
78
+ baseURL: "http://localhost:8080",
79
+ apiKey: "any-string",
80
+ });
81
+ ```
82
+
83
+ ## Runtime Endpoints
84
+
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
+
92
+ Health check:
93
+
94
+ ```bash
95
+ curl http://localhost:8080/health
96
+ ```
97
+
98
+ Switch provider at runtime:
99
+
100
+ ```bash
101
+ curl -X POST http://localhost:8080/api/provider \
102
+ -H "Content-Type: application/json" \
103
+ -d '{"provider":"deepseek"}'
104
+ ```
105
+
106
+ ## Library Usage
107
+
108
+ The package also keeps the programmatic Express entrypoint:
109
+
110
+ ```ts
111
+ import { createApp } from "@sunflower0305/claude-proxy";
112
+
113
+ const app = createApp();
114
+ app.listen(8080);
115
+ ```
116
+
117
+ ## Development
118
+
119
+ From source:
120
+
121
+ ```bash
122
+ npm install
123
+ npm run dev
124
+ ```
125
+
126
+ Build and local package verification:
127
+
128
+ ```bash
129
+ npm run build
130
+ env npm_config_cache=/tmp/claude-proxy-npm-cache npm pack --dry-run
131
+ ```
132
+
133
+ Local integration test:
134
+
135
+ ```bash
136
+ npm run test:proxy-local
137
+ ```
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,15 @@
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
+ import "dotenv/config";
13
+ import express from "express";
14
+ export declare function createApp(): express.Express;
15
+ export declare const app: express.Express;
package/dist/proxy.js ADDED
@@ -0,0 +1,419 @@
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
+ import "dotenv/config";
13
+ import cors from "cors";
14
+ import express from "express";
15
+ import { realpathSync } from "node:fs";
16
+ import { Readable } from "node:stream";
17
+ import { fileURLToPath } from "node:url";
18
+ const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
19
+ const HOP_BY_HOP_RESPONSE_HEADERS = new Set([
20
+ "connection",
21
+ "content-encoding",
22
+ "content-length",
23
+ "keep-alive",
24
+ "proxy-authenticate",
25
+ "proxy-authorization",
26
+ "te",
27
+ "trailer",
28
+ "transfer-encoding",
29
+ "upgrade",
30
+ ]);
31
+ function pickEnv(...keys) {
32
+ for (const key of keys) {
33
+ const value = process.env[key]?.trim();
34
+ if (value)
35
+ return value;
36
+ }
37
+ return undefined;
38
+ }
39
+ const PROVIDERS = {
40
+ deepseek: {
41
+ baseUrl: pickEnv("DEEPSEEK_ANTHROPIC_BASE_URL") ||
42
+ "https://api.deepseek.com/anthropic",
43
+ apiKey: process.env.DEEPSEEK_API_KEY || "",
44
+ model: pickEnv("DEEPSEEK_MODEL") || "deepseek-chat",
45
+ },
46
+ qwen: {
47
+ baseUrl: pickEnv("QWEN_ANTHROPIC_BASE_URL") ||
48
+ "https://dashscope.aliyuncs.com/apps/anthropic",
49
+ apiKey: process.env.QWEN_API_KEY || "",
50
+ model: pickEnv("QWEN_MODEL") || "qwen-plus",
51
+ },
52
+ glm: {
53
+ baseUrl: pickEnv("GLM_ANTHROPIC_BASE_URL") ||
54
+ "https://open.bigmodel.cn/api/anthropic",
55
+ apiKey: process.env.GLM_API_KEY || "",
56
+ model: pickEnv("GLM_MODEL") || "glm-5",
57
+ },
58
+ minimax: {
59
+ baseUrl: pickEnv("MINIMAX_ANTHROPIC_BASE_URL") ||
60
+ "https://api.minimaxi.com/anthropic",
61
+ apiKey: process.env.MINIMAX_API_KEY || "",
62
+ model: pickEnv("MINIMAX_MODEL") || "MiniMax-M2.7-highspeed",
63
+ },
64
+ kimi: {
65
+ baseUrl: pickEnv("KIMI_ANTHROPIC_BASE_URL") || "https://api.moonshot.cn/anthropic",
66
+ apiKey: process.env.KIMI_API_KEY || "",
67
+ model: pickEnv("KIMI_MODEL") || "kimi-k2.5",
68
+ },
69
+ };
70
+ function isProviderKey(value) {
71
+ return Boolean(value && value in PROVIDERS);
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");
84
+ }
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;
104
+ }
105
+ function getHeaderValue(value) {
106
+ if (Array.isArray(value))
107
+ return value.join(",");
108
+ return value;
109
+ }
110
+ function buildUpstreamHeaders(req, stream, apiKey) {
111
+ const headers = {
112
+ "content-type": "application/json",
113
+ "x-api-key": apiKey,
114
+ "anthropic-version": getHeaderValue(req.headers["anthropic-version"]) ||
115
+ DEFAULT_ANTHROPIC_VERSION,
116
+ accept: getHeaderValue(req.headers.accept) ||
117
+ (stream ? "text/event-stream" : "application/json"),
118
+ };
119
+ const anthropicBeta = getHeaderValue(req.headers["anthropic-beta"]);
120
+ if (anthropicBeta) {
121
+ headers["anthropic-beta"] = anthropicBeta;
122
+ }
123
+ return headers;
124
+ }
125
+ function getUpstreamUrl(baseUrl) {
126
+ return `${baseUrl.replace(/\/$/, "")}/v1/messages`;
127
+ }
128
+ function buildUpstreamBody(body, targetModel) {
129
+ const normalized = typeof body === "object" && body !== null
130
+ ? { ...body }
131
+ : {};
132
+ normalized.model = targetModel;
133
+ return normalized;
134
+ }
135
+ function copyUpstreamHeaders(upstream, res) {
136
+ for (const [key, value] of upstream.headers.entries()) {
137
+ if (HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase()))
138
+ continue;
139
+ res.setHeader(key, value);
140
+ }
141
+ }
142
+ function createProxyError(message) {
143
+ return {
144
+ type: "error",
145
+ error: {
146
+ type: "internal_error",
147
+ message,
148
+ },
149
+ };
150
+ }
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
+ };
160
+ }
161
+ function logTimingEvent(trace, phase, extra = {}) {
162
+ console.log(`[ProxyTiming] ${JSON.stringify({
163
+ request_id: trace.requestId,
164
+ provider: trace.provider,
165
+ requested_model: trace.requestedModel,
166
+ target_model: trace.targetModel,
167
+ stream: trace.stream,
168
+ phase,
169
+ elapsed_ms: Date.now() - trace.startedAt,
170
+ at: new Date().toISOString(),
171
+ ...extra,
172
+ })}`);
173
+ }
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
+ });
198
+ }
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));
203
+ }
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", {
240
+ status: upstream.status,
241
+ bytes: 0,
242
+ no_body: true,
243
+ });
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", {
255
+ status: upstream.status,
256
+ chunk_bytes: chunkSize,
257
+ });
258
+ });
259
+ upstreamStream.on("error", (error) => {
260
+ if (clientClosed)
261
+ return;
262
+ console.error("Upstream stream error:", error);
263
+ logTimingEvent(trace, "error", {
264
+ status: upstream.status,
265
+ message: error?.message || String(error),
266
+ });
267
+ if (!res.writableEnded)
268
+ res.end();
269
+ });
270
+ upstreamStream.pipe(res);
271
+ await new Promise((resolve, reject) => {
272
+ upstreamStream.on("end", () => {
273
+ streamCompleted = true;
274
+ logTimingEvent(trace, "completed", {
275
+ status: upstream.status,
276
+ });
277
+ resolve();
278
+ });
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;
288
+ }
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;
294
+ }
295
+ if (!res.writableEnded)
296
+ res.end();
297
+ }
298
+ }
299
+ export function createApp() {
300
+ const app = express();
301
+ app.use(cors());
302
+ app.use(express.json({ limit: "50mb" }));
303
+ app.get("/", (_req, res) => {
304
+ const config = getConfig();
305
+ res.json({
306
+ name: "claude-proxy",
307
+ status: "running",
308
+ provider: currentProvider,
309
+ model: config.model,
310
+ endpoints: {
311
+ messages: "POST /v1/messages",
312
+ health: "GET /health",
313
+ models: "GET /v1/models",
314
+ provider: "GET|POST /api/provider",
315
+ },
316
+ });
317
+ });
318
+ app.post("/v1/messages", async (req, res) => {
319
+ if (req.body?.stream) {
320
+ await handleStreamingRequest(req, res);
321
+ return;
322
+ }
323
+ await handleNonStreamingRequest(req, res);
324
+ });
325
+ app.get("/health", (_req, res) => {
326
+ const config = getConfig();
327
+ res.json({ status: "ok", provider: currentProvider, model: config.model });
328
+ });
329
+ app.get("/v1/models", (_req, res) => {
330
+ res.json({
331
+ data: [
332
+ { id: "claude-opus-4-6", object: "model" },
333
+ { id: "claude-sonnet-4-6", object: "model" },
334
+ { id: "claude-haiku-4-5", object: "model" },
335
+ ],
336
+ });
337
+ });
338
+ app.get("/api/provider", (_req, res) => {
339
+ const config = getConfig();
340
+ res.json({
341
+ provider: currentProvider,
342
+ model: config.model,
343
+ baseUrl: config.baseUrl,
344
+ availableProviders: Object.keys(PROVIDERS),
345
+ });
346
+ });
347
+ app.post("/api/provider", (req, res) => {
348
+ 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
+ }
365
+ if (!isProviderKey(targetProvider)) {
366
+ res.status(400).json({
367
+ error: `Unknown provider: ${targetProvider}`,
368
+ available: Object.keys(PROVIDERS),
369
+ });
370
+ return;
371
+ }
372
+ const targetConfig = getConfig(targetProvider);
373
+ if (!targetConfig.apiKey) {
374
+ res.status(400).json({
375
+ error: `API key not set for: ${targetProvider}`,
376
+ });
377
+ return;
378
+ }
379
+ const oldProvider = currentProvider;
380
+ currentProvider = targetProvider;
381
+ console.log(`Provider: ${oldProvider} -> ${currentProvider}`);
382
+ res.json({
383
+ success: true,
384
+ provider: currentProvider,
385
+ model: targetConfig.model,
386
+ });
387
+ });
388
+ return app;
389
+ }
390
+ export const app = createApp();
391
+ const PORT = parseInt(process.env.PROXY_PORT || "8080", 10);
392
+ function isMainModule() {
393
+ const entryPath = process.argv[1];
394
+ if (!entryPath)
395
+ return false;
396
+ try {
397
+ return realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url));
398
+ }
399
+ catch {
400
+ return false;
401
+ }
402
+ }
403
+ if (isMainModule()) {
404
+ app.listen(PORT, () => {
405
+ const cfg = getConfig();
406
+ console.log(`
407
+ ╔════════════════════════════════════════════════╗
408
+ ║ claude-proxy ║
409
+ ╠════════════════════════════════════════════════╣
410
+ ║ http://localhost:${PORT}
411
+ ║ Backend: ${currentProvider} (${cfg.model})
412
+ ╠════════════════════════════════════════════════╣
413
+ ║ Set these env vars in your app: ║
414
+ ║ ANTHROPIC_BASE_URL=http://localhost:${PORT}
415
+ ║ ANTHROPIC_API_KEY=any-string-works ║
416
+ ╚════════════════════════════════════════════════╝
417
+ `);
418
+ });
419
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@sunflower0305/claude-proxy",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "A proxy that lets Claude Agent SDK use domestic Chinese LLMs (DeepSeek, Qwen, GLM, MiniMax) as backend",
6
+ "license": "MIT",
7
+ "main": "./dist/proxy.js",
8
+ "types": "./dist/proxy.d.ts",
9
+ "bin": {
10
+ "claude-proxy": "./dist/proxy.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/proxy.d.ts",
15
+ "import": "./dist/proxy.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist/",
20
+ "README.md",
21
+ "LICENSE",
22
+ ".env.example"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/sunflower0305/claude-proxy.git"
33
+ },
34
+ "homepage": "https://github.com/sunflower0305/claude-proxy#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/sunflower0305/claude-proxy/issues"
37
+ },
38
+ "keywords": [
39
+ "anthropic",
40
+ "claude",
41
+ "proxy",
42
+ "express",
43
+ "llm",
44
+ "qwen",
45
+ "deepseek",
46
+ "glm",
47
+ "minimax",
48
+ "kimi"
49
+ ],
50
+ "scripts": {
51
+ "dev": "tsx watch src/proxy.ts",
52
+ "build": "tsc",
53
+ "start": "node dist/proxy.js",
54
+ "prepack": "npm run build",
55
+ "prepublishOnly": "npm run test:proxy-local",
56
+ "test": "vitest run",
57
+ "test:proxy-local": "vitest run tests/integration/proxy-local.test.ts",
58
+ "test:provider-anthropic": "node --experimental-strip-types tests/integration/provider-anthropic.ts",
59
+ "test:provider-cli-e2e": "node --experimental-strip-types tests/integration/provider-cli-e2e.ts",
60
+ "report:provider-cli-e2e": "node --experimental-strip-types tests/integration/report-provider-cli-e2e.ts"
61
+ },
62
+ "dependencies": {
63
+ "cors": "^2.8.5",
64
+ "dotenv": "^16.4.5",
65
+ "express": "^4.21.0"
66
+ },
67
+ "devDependencies": {
68
+ "@types/cors": "^2.8.17",
69
+ "@types/express": "^4.17.21",
70
+ "@types/node": "^22.0.0",
71
+ "tsx": "^4.19.0",
72
+ "typescript": "^5.5.0",
73
+ "vitest": "^3.2.4"
74
+ }
75
+ }