@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 +2 -2
- package/README.md +72 -16
- package/dist/proxy.js +177 -176
- package/package.json +4 -2
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.
|
|
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
|
+
[](https://github.com/sunflower0305/claude-proxy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/sunflower0305/claude-proxy/actions/workflows/cd.yml)
|
|
5
|
+
[](https://coveralls.io/github/sunflower0305/claude-proxy?branch=master)
|
|
6
|
+
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
7
|
+
[](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
|
|
38
|
-
|
|
|
39
|
-
| `PROVIDER`
|
|
40
|
-
| `PROXY_PORT`
|
|
41
|
-
| `QWEN_API_KEY`
|
|
42
|
-
| `DEEPSEEK_API_KEY`
|
|
43
|
-
| `GLM_API_KEY`
|
|
44
|
-
| `MINIMAX_API_KEY`
|
|
45
|
-
| `KIMI_API_KEY`
|
|
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`
|
|
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
|
|
86
|
-
|
|
|
87
|
-
| `POST`
|
|
88
|
-
| `GET`
|
|
89
|
-
| `GET`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
242
|
-
no_body: true,
|
|
196
|
+
content_type: upstream.headers.get("content-type") || "",
|
|
243
197
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
203
|
+
bytes: payload.byteLength,
|
|
257
204
|
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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",
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
║
|
|
409
|
-
|
|
408
|
+
╔═══════════════════════════════════════════════════════╗
|
|
409
|
+
║ claude-proxy ║
|
|
410
|
+
╠═══════════════════════════════════════════════════════╣
|
|
410
411
|
║ http://localhost:${PORT}
|
|
411
|
-
║ Backend: ${
|
|
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.
|
|
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": "
|
|
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",
|