@sunflower0305/claude-proxy 1.1.0 → 1.1.2
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 +33 -65
- package/dist/proxy.js +8 -8
- package/package.json +1 -1
- package/dist/app.d.ts +0 -3
- package/dist/app.js +0 -79
- package/dist/messages.d.ts +0 -3
- package/dist/messages.js +0 -221
- package/dist/providers.d.ts +0 -11
- package/dist/providers.js +0 -53
- package/dist/runtime.d.ts +0 -27
- package/dist/runtime.js +0 -83
package/.env.example
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Choose your provider: qwen | deepseek | glm | minimax | kimi
|
|
2
|
-
PROVIDER=
|
|
2
|
+
PROVIDER=deepseek
|
|
3
3
|
|
|
4
4
|
# Proxy server port (default: 8080)
|
|
5
5
|
PROXY_PORT=8080
|
|
@@ -13,7 +13,7 @@ KIMI_API_KEY=your-kimi-key
|
|
|
13
13
|
|
|
14
14
|
# Optional model overrides
|
|
15
15
|
QWEN_MODEL=qwen-plus
|
|
16
|
-
DEEPSEEK_MODEL=deepseek-
|
|
16
|
+
DEEPSEEK_MODEL=deepseek-v4-pro
|
|
17
17
|
GLM_MODEL=glm-5.1
|
|
18
18
|
MINIMAX_MODEL=MiniMax-M2.7-highspeed
|
|
19
19
|
KIMI_MODEL=kimi-k2.6
|
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# claude-proxy
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sunflower0305/claude-proxy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/sunflower0305/claude-proxy/actions/workflows/cd.yml)
|
|
4
5
|
[](https://coveralls.io/github/sunflower0305/claude-proxy?branch=master)
|
|
5
6
|
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
7
|
+
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
8
|
+
[](https://github.com/sunflower0305/claude-proxy/stargazers)
|
|
6
9
|
[](https://github.com/sunflower0305/claude-proxy/blob/master/LICENSE)
|
|
7
10
|
|
|
8
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.
|
|
@@ -31,25 +34,35 @@ The proxy reads configuration from environment variables. You can export them in
|
|
|
31
34
|
Example `.env`:
|
|
32
35
|
|
|
33
36
|
```dotenv
|
|
34
|
-
PROVIDER=
|
|
37
|
+
PROVIDER=deepseek
|
|
35
38
|
PROXY_PORT=8080
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
DEEPSEEK_API_KEY=your-deepseek-api-key
|
|
40
|
+
DEEPSEEK_MODEL=deepseek-v4-pro
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
Available variables:
|
|
41
44
|
|
|
42
|
-
| Variable
|
|
43
|
-
|
|
|
44
|
-
| `PROVIDER`
|
|
45
|
-
| `PROXY_PORT`
|
|
46
|
-
| `QWEN_API_KEY`
|
|
47
|
-
| `DEEPSEEK_API_KEY`
|
|
48
|
-
| `GLM_API_KEY`
|
|
49
|
-
| `MINIMAX_API_KEY`
|
|
50
|
-
| `KIMI_API_KEY`
|
|
45
|
+
| Variable | Purpose |
|
|
46
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
47
|
+
| `PROVIDER` | Active provider. Defaults to `deepseek`. |
|
|
48
|
+
| `PROXY_PORT` | Local server port. Defaults to `8080`. |
|
|
49
|
+
| `QWEN_API_KEY` | API key for Qwen. |
|
|
50
|
+
| `DEEPSEEK_API_KEY` | API key for DeepSeek. |
|
|
51
|
+
| `GLM_API_KEY` | API key for GLM. |
|
|
52
|
+
| `MINIMAX_API_KEY` | API key for MiniMax. |
|
|
53
|
+
| `KIMI_API_KEY` | API key for Kimi. |
|
|
51
54
|
| `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. |
|
|
52
|
-
| `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL`
|
|
55
|
+
| `QWEN_MODEL`, `DEEPSEEK_MODEL`, `GLM_MODEL`, `MINIMAX_MODEL`, `KIMI_MODEL` | Override the default upstream model for a provider. |
|
|
56
|
+
|
|
57
|
+
Provider defaults:
|
|
58
|
+
|
|
59
|
+
| Provider | Model env | Default model |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| **`deepseek` (default)** | `DEEPSEEK_MODEL` | **`deepseek-v4-pro`** |
|
|
62
|
+
| `qwen` | `QWEN_MODEL` | `qwen-plus` |
|
|
63
|
+
| `glm` | `GLM_MODEL` | `glm-5.1` |
|
|
64
|
+
| `minimax` | `MINIMAX_MODEL` | `MiniMax-M2.7-highspeed` |
|
|
65
|
+
| `kimi` | `KIMI_MODEL` | `kimi-k2.6` |
|
|
53
66
|
|
|
54
67
|
You can use the bundled example as a starting point:
|
|
55
68
|
|
|
@@ -87,12 +100,12 @@ const client = new Anthropic({
|
|
|
87
100
|
|
|
88
101
|
## Runtime Endpoints
|
|
89
102
|
|
|
90
|
-
| Method
|
|
91
|
-
|
|
|
92
|
-
| `POST`
|
|
93
|
-
| `GET`
|
|
94
|
-
| `GET`
|
|
95
|
-
| `GET` / `POST` | `/api/provider` | Read or switch the active provider
|
|
103
|
+
| Method | Path | Description |
|
|
104
|
+
| -------------- | --------------- | ------------------------------------------ |
|
|
105
|
+
| `POST` | `/v1/messages` | Main Anthropic Messages API proxy endpoint |
|
|
106
|
+
| `GET` | `/v1/models` | Lists supported Claude-facing model ids |
|
|
107
|
+
| `GET` | `/health` | Health check |
|
|
108
|
+
| `GET` / `POST` | `/api/provider` | Read or switch the active provider |
|
|
96
109
|
|
|
97
110
|
Health check:
|
|
98
111
|
|
|
@@ -105,7 +118,7 @@ Switch provider at runtime:
|
|
|
105
118
|
```bash
|
|
106
119
|
curl -X POST http://localhost:8080/api/provider \
|
|
107
120
|
-H "Content-Type: application/json" \
|
|
108
|
-
-d '{"provider":"
|
|
121
|
+
-d '{"provider":"qwen"}'
|
|
109
122
|
```
|
|
110
123
|
|
|
111
124
|
## Library Usage
|
|
@@ -119,24 +132,6 @@ const app = createApp();
|
|
|
119
132
|
app.listen(8080);
|
|
120
133
|
```
|
|
121
134
|
|
|
122
|
-
## Release Verification
|
|
123
|
-
|
|
124
|
-
`v1.0.0` was verified on April 15, 2026 after publishing `@sunflower0305/claude-proxy` to npm.
|
|
125
|
-
|
|
126
|
-
Verified items:
|
|
127
|
-
|
|
128
|
-
- `npm install @sunflower0305/claude-proxy` completed successfully in a clean temporary directory
|
|
129
|
-
- the published `claude-proxy` CLI started correctly from the installed package
|
|
130
|
-
- `GET /health` and `GET /v1/models` returned `200 OK`
|
|
131
|
-
- end-to-end proxying against a local mock Anthropic-compatible upstream passed for both non-streaming and streaming `POST /v1/messages`
|
|
132
|
-
- end-to-end proxying against the real Qwen Anthropic-compatible upstream passed for both non-streaming and streaming `POST /v1/messages`
|
|
133
|
-
|
|
134
|
-
Observed behavior during verification:
|
|
135
|
-
|
|
136
|
-
- model remapping worked as expected, including `claude-sonnet-4-6 -> qwen-plus`
|
|
137
|
-
- the real Qwen verification returned a valid assistant response for both buffered JSON and SSE streaming modes
|
|
138
|
-
- the published package included the expected CLI entrypoint, `dist/` build output, `README.md`, `LICENSE`, and `.env.example`
|
|
139
|
-
|
|
140
135
|
## Development
|
|
141
136
|
|
|
142
137
|
From source:
|
|
@@ -146,33 +141,6 @@ npm install
|
|
|
146
141
|
npm run dev
|
|
147
142
|
```
|
|
148
143
|
|
|
149
|
-
## CI And Releases
|
|
150
|
-
|
|
151
|
-
GitHub Actions currently provides a CI baseline only:
|
|
152
|
-
|
|
153
|
-
- install dependencies with `pnpm`
|
|
154
|
-
- run `npm run build`
|
|
155
|
-
- run `npm run test:proxy-local`
|
|
156
|
-
- run `npm run test:coverage`
|
|
157
|
-
- upload `coverage/lcov.info` to Coveralls without blocking the workflow if the upload service is temporarily unavailable
|
|
158
|
-
|
|
159
|
-
Publishing to npm remains a manual step. The package still relies on `prepack` and `prepublishOnly` in `package.json` to build and verify the artifact before release.
|
|
160
|
-
|
|
161
|
-
Build and local package verification:
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
npm run build
|
|
165
|
-
env npm_config_cache=/tmp/claude-proxy-npm-cache npm pack --dry-run
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Local integration test:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
npm run test:proxy-local
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Release notes for `v1.0.0` are available in [docs/releases/1.0.0.md](/Users/joe/ai/claude-proxy/docs/releases/1.0.0.md).
|
|
175
|
-
|
|
176
144
|
## License
|
|
177
145
|
|
|
178
146
|
MIT
|
package/dist/proxy.js
CHANGED
|
@@ -41,7 +41,7 @@ const PROVIDERS = {
|
|
|
41
41
|
baseUrl: pickEnv("DEEPSEEK_ANTHROPIC_BASE_URL") ||
|
|
42
42
|
"https://api.deepseek.com/anthropic",
|
|
43
43
|
apiKey: process.env.DEEPSEEK_API_KEY || "",
|
|
44
|
-
model: pickEnv("DEEPSEEK_MODEL") || "deepseek-
|
|
44
|
+
model: pickEnv("DEEPSEEK_MODEL") || "deepseek-v4-pro",
|
|
45
45
|
},
|
|
46
46
|
qwen: {
|
|
47
47
|
baseUrl: pickEnv("QWEN_ANTHROPIC_BASE_URL") ||
|
|
@@ -71,10 +71,10 @@ function isProviderKey(value) {
|
|
|
71
71
|
return Boolean(value && value in PROVIDERS);
|
|
72
72
|
}
|
|
73
73
|
function getInitialProvider() {
|
|
74
|
-
return isProviderKey(process.env.PROVIDER) ? process.env.PROVIDER : "
|
|
74
|
+
return isProviderKey(process.env.PROVIDER) ? process.env.PROVIDER : "deepseek";
|
|
75
75
|
}
|
|
76
76
|
function getProviderConfig(provider) {
|
|
77
|
-
return PROVIDERS[provider] || PROVIDERS.
|
|
77
|
+
return PROVIDERS[provider] || PROVIDERS.deepseek;
|
|
78
78
|
}
|
|
79
79
|
function getHeaderValue(value) {
|
|
80
80
|
if (Array.isArray(value))
|
|
@@ -338,7 +338,7 @@ export function createApp() {
|
|
|
338
338
|
app.get("/v1/models", (_req, res) => {
|
|
339
339
|
res.json({
|
|
340
340
|
data: [
|
|
341
|
-
{ id: "claude-opus-4-
|
|
341
|
+
{ id: "claude-opus-4-7", object: "model" },
|
|
342
342
|
{ id: "claude-sonnet-4-6", object: "model" },
|
|
343
343
|
{ id: "claude-haiku-4-5", object: "model" },
|
|
344
344
|
],
|
|
@@ -406,13 +406,13 @@ if (isMainModule()) {
|
|
|
406
406
|
app.listen(PORT, () => {
|
|
407
407
|
console.log(`
|
|
408
408
|
╔═══════════════════════════════════════════════════════╗
|
|
409
|
-
║
|
|
409
|
+
║ claude-proxy ║
|
|
410
410
|
╠═══════════════════════════════════════════════════════╣
|
|
411
|
-
║ http://localhost:${PORT}
|
|
412
|
-
║ Backend: ${initialProvider} (${initialConfig.model})
|
|
411
|
+
║ http://localhost:${PORT}
|
|
412
|
+
║ Backend: ${initialProvider} (${initialConfig.model})
|
|
413
413
|
╠═══════════════════════════════════════════════════════╣
|
|
414
414
|
║ Set these env vars in your app: ║
|
|
415
|
-
║ ANTHROPIC_BASE_URL=http://localhost:${PORT}
|
|
415
|
+
║ ANTHROPIC_BASE_URL=http://localhost:${PORT}
|
|
416
416
|
║ ANTHROPIC_API_KEY=any-string-works ║
|
|
417
417
|
╚═══════════════════════════════════════════════════════╝
|
|
418
418
|
`);
|
package/package.json
CHANGED
package/dist/app.d.ts
DELETED
package/dist/app.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import cors from "cors";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import { handleMessagesRequest } from "./messages";
|
|
4
|
-
import { isProviderKey } from "./providers";
|
|
5
|
-
import { createRuntime } from "./runtime";
|
|
6
|
-
const SUPPORTED_MODELS = [
|
|
7
|
-
{ id: "claude-opus-4-6", object: "model" },
|
|
8
|
-
{ id: "claude-sonnet-4-6", object: "model" },
|
|
9
|
-
{ id: "claude-haiku-4-5", object: "model" },
|
|
10
|
-
];
|
|
11
|
-
export function createApp(runtime = createRuntime()) {
|
|
12
|
-
const app = express();
|
|
13
|
-
app.use(cors());
|
|
14
|
-
app.use(express.json({ limit: "50mb" }));
|
|
15
|
-
app.get("/", (_req, res) => {
|
|
16
|
-
const config = runtime.getCurrentConfig();
|
|
17
|
-
res.json({
|
|
18
|
-
name: "claude-proxy",
|
|
19
|
-
status: "running",
|
|
20
|
-
provider: runtime.getCurrentProvider(),
|
|
21
|
-
model: config.model,
|
|
22
|
-
endpoints: {
|
|
23
|
-
messages: "POST /v1/messages",
|
|
24
|
-
health: "GET /health",
|
|
25
|
-
models: "GET /v1/models",
|
|
26
|
-
provider: "GET|POST /api/provider",
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
app.post("/v1/messages", async (req, res) => {
|
|
31
|
-
await handleMessagesRequest(req, res, runtime);
|
|
32
|
-
});
|
|
33
|
-
app.get("/health", (_req, res) => {
|
|
34
|
-
const config = runtime.getCurrentConfig();
|
|
35
|
-
res.json({
|
|
36
|
-
status: "ok",
|
|
37
|
-
provider: runtime.getCurrentProvider(),
|
|
38
|
-
model: config.model,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
app.get("/v1/models", (_req, res) => {
|
|
42
|
-
res.json({ data: SUPPORTED_MODELS });
|
|
43
|
-
});
|
|
44
|
-
app.get("/api/provider", (_req, res) => {
|
|
45
|
-
const config = runtime.getCurrentConfig();
|
|
46
|
-
res.json({
|
|
47
|
-
provider: runtime.getCurrentProvider(),
|
|
48
|
-
model: config.model,
|
|
49
|
-
baseUrl: config.baseUrl,
|
|
50
|
-
availableProviders: [...runtime.providerKeys],
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
app.post("/api/provider", (req, res) => {
|
|
54
|
-
const { provider, model } = (req.body ?? {});
|
|
55
|
-
const targetProvider = provider ?? runtime.inferProviderFromModel(model);
|
|
56
|
-
if (!isProviderKey(targetProvider)) {
|
|
57
|
-
res.status(400).json({
|
|
58
|
-
error: `Unknown provider: ${targetProvider}`,
|
|
59
|
-
available: [...runtime.providerKeys],
|
|
60
|
-
});
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const targetConfig = runtime.getConfig(targetProvider);
|
|
64
|
-
if (!targetConfig.apiKey) {
|
|
65
|
-
res.status(400).json({
|
|
66
|
-
error: `API key not set for: ${targetProvider}`,
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const switchResult = runtime.setCurrentProvider(targetProvider);
|
|
71
|
-
console.log(`Provider: ${switchResult.previousProvider} -> ${switchResult.provider}`);
|
|
72
|
-
res.json({
|
|
73
|
-
success: true,
|
|
74
|
-
provider: switchResult.provider,
|
|
75
|
-
model: switchResult.config.model,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
return app;
|
|
79
|
-
}
|
package/dist/messages.d.ts
DELETED
package/dist/messages.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
3
|
-
const HOP_BY_HOP_RESPONSE_HEADERS = new Set([
|
|
4
|
-
"connection",
|
|
5
|
-
"content-encoding",
|
|
6
|
-
"content-length",
|
|
7
|
-
"keep-alive",
|
|
8
|
-
"proxy-authenticate",
|
|
9
|
-
"proxy-authorization",
|
|
10
|
-
"te",
|
|
11
|
-
"trailer",
|
|
12
|
-
"transfer-encoding",
|
|
13
|
-
"upgrade",
|
|
14
|
-
]);
|
|
15
|
-
function getHeaderValue(value) {
|
|
16
|
-
if (Array.isArray(value))
|
|
17
|
-
return value.join(",");
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
function buildUpstreamHeaders(req, stream, apiKey) {
|
|
21
|
-
const headers = {
|
|
22
|
-
"content-type": "application/json",
|
|
23
|
-
"x-api-key": apiKey,
|
|
24
|
-
"anthropic-version": getHeaderValue(req.headers["anthropic-version"]) ||
|
|
25
|
-
DEFAULT_ANTHROPIC_VERSION,
|
|
26
|
-
accept: getHeaderValue(req.headers.accept) ||
|
|
27
|
-
(stream ? "text/event-stream" : "application/json"),
|
|
28
|
-
};
|
|
29
|
-
const anthropicBeta = getHeaderValue(req.headers["anthropic-beta"]);
|
|
30
|
-
if (anthropicBeta) {
|
|
31
|
-
headers["anthropic-beta"] = anthropicBeta;
|
|
32
|
-
}
|
|
33
|
-
return headers;
|
|
34
|
-
}
|
|
35
|
-
function getUpstreamUrl(baseUrl) {
|
|
36
|
-
return `${baseUrl.replace(/\/$/, "")}/v1/messages`;
|
|
37
|
-
}
|
|
38
|
-
function buildUpstreamBody(body, targetModel) {
|
|
39
|
-
const normalized = typeof body === "object" && body !== null
|
|
40
|
-
? { ...body }
|
|
41
|
-
: {};
|
|
42
|
-
normalized.model = targetModel;
|
|
43
|
-
return normalized;
|
|
44
|
-
}
|
|
45
|
-
function copyUpstreamHeaders(upstream, res) {
|
|
46
|
-
for (const [key, value] of upstream.headers.entries()) {
|
|
47
|
-
if (HOP_BY_HOP_RESPONSE_HEADERS.has(key.toLowerCase()))
|
|
48
|
-
continue;
|
|
49
|
-
res.setHeader(key, value);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function createProxyError(message) {
|
|
53
|
-
return {
|
|
54
|
-
type: "error",
|
|
55
|
-
error: {
|
|
56
|
-
type: "internal_error",
|
|
57
|
-
message,
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function toErrorMessage(error) {
|
|
62
|
-
if (error instanceof Error && error.message)
|
|
63
|
-
return error.message;
|
|
64
|
-
return String(error);
|
|
65
|
-
}
|
|
66
|
-
function logTimingEvent(trace, phase, extra = {}) {
|
|
67
|
-
console.log(`[ProxyTiming] ${JSON.stringify({
|
|
68
|
-
request_id: trace.requestId,
|
|
69
|
-
provider: trace.provider,
|
|
70
|
-
requested_model: trace.requestedModel,
|
|
71
|
-
target_model: trace.targetModel,
|
|
72
|
-
stream: trace.stream,
|
|
73
|
-
phase,
|
|
74
|
-
elapsed_ms: Date.now() - trace.startedAt,
|
|
75
|
-
at: new Date().toISOString(),
|
|
76
|
-
...extra,
|
|
77
|
-
})}`);
|
|
78
|
-
}
|
|
79
|
-
function prepareUpstreamRequest(req, runtime, stream) {
|
|
80
|
-
const config = runtime.getCurrentConfig();
|
|
81
|
-
const targetModel = runtime.getTargetModel(req.body?.model);
|
|
82
|
-
return {
|
|
83
|
-
apiKey: config.apiKey,
|
|
84
|
-
providerModel: config.model,
|
|
85
|
-
requestBody: buildUpstreamBody(req.body, targetModel),
|
|
86
|
-
requestedModel: req.body?.model,
|
|
87
|
-
targetModel,
|
|
88
|
-
trace: runtime.createRequestTrace(req.body?.model, targetModel, stream),
|
|
89
|
-
upstreamUrl: getUpstreamUrl(config.baseUrl),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
async function handleNonStreamingRequest(req, res, runtime) {
|
|
93
|
-
const prepared = prepareUpstreamRequest(req, runtime, false);
|
|
94
|
-
console.log(`\n[${new Date().toISOString()}] ${String(prepared.requestedModel || prepared.providerModel)} -> ${prepared.targetModel} (non-streaming)`);
|
|
95
|
-
logTimingEvent(prepared.trace, "start");
|
|
96
|
-
try {
|
|
97
|
-
const upstream = await fetch(prepared.upstreamUrl, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: buildUpstreamHeaders(req, false, prepared.apiKey),
|
|
100
|
-
body: JSON.stringify(prepared.requestBody),
|
|
101
|
-
});
|
|
102
|
-
logTimingEvent(prepared.trace, "upstream_headers", {
|
|
103
|
-
status: upstream.status,
|
|
104
|
-
content_type: upstream.headers.get("content-type") || "",
|
|
105
|
-
});
|
|
106
|
-
const payload = Buffer.from(await upstream.arrayBuffer());
|
|
107
|
-
copyUpstreamHeaders(upstream, res);
|
|
108
|
-
res.status(upstream.status).send(payload);
|
|
109
|
-
logTimingEvent(prepared.trace, "completed", {
|
|
110
|
-
status: upstream.status,
|
|
111
|
-
bytes: payload.byteLength,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
const message = toErrorMessage(error);
|
|
116
|
-
console.error("Request error:", error);
|
|
117
|
-
logTimingEvent(prepared.trace, "error", { message });
|
|
118
|
-
res.status(500).json(createProxyError(message));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
async function handleStreamingRequest(req, res, runtime) {
|
|
122
|
-
const prepared = prepareUpstreamRequest(req, runtime, true);
|
|
123
|
-
const abortController = new AbortController();
|
|
124
|
-
let clientClosed = false;
|
|
125
|
-
let streamCompleted = false;
|
|
126
|
-
let sawFirstChunk = false;
|
|
127
|
-
console.log(`\n[${new Date().toISOString()}] ${String(prepared.requestedModel || prepared.providerModel)} -> ${prepared.targetModel} (streaming)`);
|
|
128
|
-
logTimingEvent(prepared.trace, "start");
|
|
129
|
-
res.on("close", () => {
|
|
130
|
-
if (streamCompleted)
|
|
131
|
-
return;
|
|
132
|
-
clientClosed = true;
|
|
133
|
-
abortController.abort();
|
|
134
|
-
logTimingEvent(prepared.trace, "client_aborted");
|
|
135
|
-
});
|
|
136
|
-
try {
|
|
137
|
-
const upstream = await fetch(prepared.upstreamUrl, {
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: buildUpstreamHeaders(req, true, prepared.apiKey),
|
|
140
|
-
body: JSON.stringify(prepared.requestBody),
|
|
141
|
-
signal: abortController.signal,
|
|
142
|
-
});
|
|
143
|
-
logTimingEvent(prepared.trace, "upstream_headers", {
|
|
144
|
-
status: upstream.status,
|
|
145
|
-
content_type: upstream.headers.get("content-type") || "",
|
|
146
|
-
});
|
|
147
|
-
copyUpstreamHeaders(upstream, res);
|
|
148
|
-
res.status(upstream.status);
|
|
149
|
-
if (!upstream.body) {
|
|
150
|
-
streamCompleted = true;
|
|
151
|
-
res.end();
|
|
152
|
-
logTimingEvent(prepared.trace, "completed", {
|
|
153
|
-
status: upstream.status,
|
|
154
|
-
bytes: 0,
|
|
155
|
-
no_body: true,
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const upstreamStream = Readable.fromWeb(upstream.body);
|
|
160
|
-
upstreamStream.on("data", (chunk) => {
|
|
161
|
-
if (sawFirstChunk)
|
|
162
|
-
return;
|
|
163
|
-
sawFirstChunk = true;
|
|
164
|
-
const chunkSize = Buffer.isBuffer(chunk)
|
|
165
|
-
? chunk.byteLength
|
|
166
|
-
: Buffer.byteLength(String(chunk));
|
|
167
|
-
logTimingEvent(prepared.trace, "first_chunk", {
|
|
168
|
-
status: upstream.status,
|
|
169
|
-
chunk_bytes: chunkSize,
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
upstreamStream.on("error", (error) => {
|
|
173
|
-
if (clientClosed)
|
|
174
|
-
return;
|
|
175
|
-
console.error("Upstream stream error:", error);
|
|
176
|
-
logTimingEvent(prepared.trace, "error", {
|
|
177
|
-
status: upstream.status,
|
|
178
|
-
message: toErrorMessage(error),
|
|
179
|
-
});
|
|
180
|
-
if (!res.writableEnded)
|
|
181
|
-
res.end();
|
|
182
|
-
});
|
|
183
|
-
upstreamStream.pipe(res);
|
|
184
|
-
await new Promise((resolve, reject) => {
|
|
185
|
-
upstreamStream.on("end", () => {
|
|
186
|
-
streamCompleted = true;
|
|
187
|
-
logTimingEvent(prepared.trace, "completed", {
|
|
188
|
-
status: upstream.status,
|
|
189
|
-
});
|
|
190
|
-
resolve();
|
|
191
|
-
});
|
|
192
|
-
upstreamStream.on("error", reject);
|
|
193
|
-
res.on("close", () => resolve());
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
const wasAborted = error instanceof Error && error.name === "AbortError"
|
|
198
|
-
? true
|
|
199
|
-
: abortController.signal.aborted;
|
|
200
|
-
if (clientClosed || wasAborted) {
|
|
201
|
-
console.warn("[Proxy] Client disconnected, streaming aborted");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const message = toErrorMessage(error);
|
|
205
|
-
console.error("Request error:", error);
|
|
206
|
-
logTimingEvent(prepared.trace, "error", { message });
|
|
207
|
-
if (!res.headersSent) {
|
|
208
|
-
res.status(500).json(createProxyError(message));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!res.writableEnded)
|
|
212
|
-
res.end();
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
export async function handleMessagesRequest(req, res, runtime) {
|
|
216
|
-
if (req.body?.stream) {
|
|
217
|
-
await handleStreamingRequest(req, res, runtime);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
await handleNonStreamingRequest(req, res, runtime);
|
|
221
|
-
}
|
package/dist/providers.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface ProviderConfig {
|
|
2
|
-
baseUrl: string;
|
|
3
|
-
apiKey: string;
|
|
4
|
-
model: string;
|
|
5
|
-
}
|
|
6
|
-
export declare const PROVIDER_KEYS: readonly ["deepseek", "qwen", "glm", "minimax", "kimi"];
|
|
7
|
-
export type ProviderKey = (typeof PROVIDER_KEYS)[number];
|
|
8
|
-
export type ProviderMap = Record<ProviderKey, ProviderConfig>;
|
|
9
|
-
export declare const DEFAULT_PROVIDER: ProviderKey;
|
|
10
|
-
export declare function isProviderKey(value: string | undefined): value is ProviderKey;
|
|
11
|
-
export declare function loadProviders(env?: NodeJS.ProcessEnv): ProviderMap;
|
package/dist/providers.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export const PROVIDER_KEYS = [
|
|
2
|
-
"deepseek",
|
|
3
|
-
"qwen",
|
|
4
|
-
"glm",
|
|
5
|
-
"minimax",
|
|
6
|
-
"kimi",
|
|
7
|
-
];
|
|
8
|
-
export const DEFAULT_PROVIDER = "qwen";
|
|
9
|
-
function pickEnv(env, ...keys) {
|
|
10
|
-
for (const key of keys) {
|
|
11
|
-
const value = env[key]?.trim();
|
|
12
|
-
if (value)
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
export function isProviderKey(value) {
|
|
18
|
-
return Boolean(value && PROVIDER_KEYS.includes(value));
|
|
19
|
-
}
|
|
20
|
-
export function loadProviders(env = process.env) {
|
|
21
|
-
return {
|
|
22
|
-
deepseek: {
|
|
23
|
-
baseUrl: pickEnv(env, "DEEPSEEK_ANTHROPIC_BASE_URL") ||
|
|
24
|
-
"https://api.deepseek.com/anthropic",
|
|
25
|
-
apiKey: env.DEEPSEEK_API_KEY || "",
|
|
26
|
-
model: pickEnv(env, "DEEPSEEK_MODEL") || "deepseek-chat",
|
|
27
|
-
},
|
|
28
|
-
qwen: {
|
|
29
|
-
baseUrl: pickEnv(env, "QWEN_ANTHROPIC_BASE_URL") ||
|
|
30
|
-
"https://dashscope.aliyuncs.com/apps/anthropic",
|
|
31
|
-
apiKey: env.QWEN_API_KEY || "",
|
|
32
|
-
model: pickEnv(env, "QWEN_MODEL") || "qwen-plus",
|
|
33
|
-
},
|
|
34
|
-
glm: {
|
|
35
|
-
baseUrl: pickEnv(env, "GLM_ANTHROPIC_BASE_URL") ||
|
|
36
|
-
"https://open.bigmodel.cn/api/anthropic",
|
|
37
|
-
apiKey: env.GLM_API_KEY || "",
|
|
38
|
-
model: pickEnv(env, "GLM_MODEL") || "glm-5",
|
|
39
|
-
},
|
|
40
|
-
minimax: {
|
|
41
|
-
baseUrl: pickEnv(env, "MINIMAX_ANTHROPIC_BASE_URL") ||
|
|
42
|
-
"https://api.minimaxi.com/anthropic",
|
|
43
|
-
apiKey: env.MINIMAX_API_KEY || "",
|
|
44
|
-
model: pickEnv(env, "MINIMAX_MODEL") || "MiniMax-M2.7-highspeed",
|
|
45
|
-
},
|
|
46
|
-
kimi: {
|
|
47
|
-
baseUrl: pickEnv(env, "KIMI_ANTHROPIC_BASE_URL") ||
|
|
48
|
-
"https://api.moonshot.cn/anthropic",
|
|
49
|
-
apiKey: env.KIMI_API_KEY || "",
|
|
50
|
-
model: pickEnv(env, "KIMI_MODEL") || "kimi-k2.5",
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
package/dist/runtime.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type ProviderConfig, type ProviderKey, type ProviderMap } from "./providers";
|
|
2
|
-
export interface RequestTrace {
|
|
3
|
-
requestId: string;
|
|
4
|
-
provider: ProviderKey;
|
|
5
|
-
requestedModel: string;
|
|
6
|
-
targetModel: string;
|
|
7
|
-
stream: boolean;
|
|
8
|
-
startedAt: number;
|
|
9
|
-
}
|
|
10
|
-
export interface ProxyRuntime {
|
|
11
|
-
readonly providerKeys: readonly ProviderKey[];
|
|
12
|
-
getCurrentProvider(): ProviderKey;
|
|
13
|
-
getCurrentConfig(): ProviderConfig;
|
|
14
|
-
getConfig(provider?: ProviderKey): ProviderConfig;
|
|
15
|
-
getTargetModel(requestedModel: unknown): string;
|
|
16
|
-
inferProviderFromModel(model: string | undefined): ProviderKey | undefined;
|
|
17
|
-
setCurrentProvider(provider: ProviderKey): {
|
|
18
|
-
previousProvider: ProviderKey;
|
|
19
|
-
provider: ProviderKey;
|
|
20
|
-
config: ProviderConfig;
|
|
21
|
-
};
|
|
22
|
-
createRequestTrace(requestedModel: unknown, targetModel: string, stream: boolean): RequestTrace;
|
|
23
|
-
getStartupWarning(): string | undefined;
|
|
24
|
-
}
|
|
25
|
-
export declare function resolveTargetModel(requestedModel: unknown, currentModel: string): string;
|
|
26
|
-
export declare function inferProviderFromModel(model: string | undefined): ProviderKey | undefined;
|
|
27
|
-
export declare function createRuntime(env?: NodeJS.ProcessEnv, providers?: ProviderMap): ProxyRuntime;
|
package/dist/runtime.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_PROVIDER, PROVIDER_KEYS, isProviderKey, loadProviders, } from "./providers";
|
|
2
|
-
export function resolveTargetModel(requestedModel, currentModel) {
|
|
3
|
-
if (typeof requestedModel !== "string" || !requestedModel) {
|
|
4
|
-
return currentModel;
|
|
5
|
-
}
|
|
6
|
-
const normalizedModel = requestedModel.toLowerCase();
|
|
7
|
-
if (normalizedModel === "opus" ||
|
|
8
|
-
normalizedModel === "sonnet" ||
|
|
9
|
-
normalizedModel === "haiku") {
|
|
10
|
-
return currentModel;
|
|
11
|
-
}
|
|
12
|
-
if (normalizedModel.startsWith("claude-") &&
|
|
13
|
-
(normalizedModel.includes("-opus") ||
|
|
14
|
-
normalizedModel.includes("-sonnet") ||
|
|
15
|
-
normalizedModel.includes("-haiku"))) {
|
|
16
|
-
return currentModel;
|
|
17
|
-
}
|
|
18
|
-
return requestedModel;
|
|
19
|
-
}
|
|
20
|
-
export function inferProviderFromModel(model) {
|
|
21
|
-
const normalizedModel = model?.toLowerCase();
|
|
22
|
-
if (!normalizedModel)
|
|
23
|
-
return undefined;
|
|
24
|
-
if (normalizedModel.includes("kimi"))
|
|
25
|
-
return "kimi";
|
|
26
|
-
if (normalizedModel.includes("qwen"))
|
|
27
|
-
return "qwen";
|
|
28
|
-
if (normalizedModel.includes("deepseek"))
|
|
29
|
-
return "deepseek";
|
|
30
|
-
if (normalizedModel.includes("glm"))
|
|
31
|
-
return "glm";
|
|
32
|
-
if (normalizedModel.includes("minimax"))
|
|
33
|
-
return "minimax";
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
export function createRuntime(env = process.env, providers = loadProviders(env)) {
|
|
37
|
-
let currentProvider = isProviderKey(env.PROVIDER)
|
|
38
|
-
? env.PROVIDER
|
|
39
|
-
: DEFAULT_PROVIDER;
|
|
40
|
-
let requestSequence = 0;
|
|
41
|
-
function getConfig(provider = currentProvider) {
|
|
42
|
-
return providers[provider] || providers[DEFAULT_PROVIDER];
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
providerKeys: PROVIDER_KEYS,
|
|
46
|
-
getCurrentProvider() {
|
|
47
|
-
return currentProvider;
|
|
48
|
-
},
|
|
49
|
-
getCurrentConfig() {
|
|
50
|
-
return getConfig();
|
|
51
|
-
},
|
|
52
|
-
getConfig,
|
|
53
|
-
getTargetModel(requestedModel) {
|
|
54
|
-
return resolveTargetModel(requestedModel, getConfig().model);
|
|
55
|
-
},
|
|
56
|
-
inferProviderFromModel,
|
|
57
|
-
setCurrentProvider(provider) {
|
|
58
|
-
const previousProvider = currentProvider;
|
|
59
|
-
currentProvider = provider;
|
|
60
|
-
return {
|
|
61
|
-
previousProvider,
|
|
62
|
-
provider: currentProvider,
|
|
63
|
-
config: getConfig(),
|
|
64
|
-
};
|
|
65
|
-
},
|
|
66
|
-
createRequestTrace(requestedModel, targetModel, stream) {
|
|
67
|
-
const config = getConfig();
|
|
68
|
-
return {
|
|
69
|
-
requestId: `req-${++requestSequence}`,
|
|
70
|
-
provider: currentProvider,
|
|
71
|
-
requestedModel: String(requestedModel || config.model),
|
|
72
|
-
targetModel,
|
|
73
|
-
stream,
|
|
74
|
-
startedAt: Date.now(),
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
getStartupWarning() {
|
|
78
|
-
if (getConfig().apiKey)
|
|
79
|
-
return undefined;
|
|
80
|
-
return `Warning: API key not configured for provider: ${currentProvider}`;
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|