@sunflower0305/claude-proxy 1.1.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/README.md +48 -29
- package/dist/proxy.js +5 -5
- 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/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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)
|
|
6
7
|
[](https://github.com/sunflower0305/claude-proxy/blob/master/LICENSE)
|
|
@@ -39,17 +40,17 @@ QWEN_MODEL=qwen-plus
|
|
|
39
40
|
|
|
40
41
|
Available variables:
|
|
41
42
|
|
|
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`
|
|
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. |
|
|
51
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. |
|
|
52
|
-
| `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. |
|
|
53
54
|
|
|
54
55
|
You can use the bundled example as a starting point:
|
|
55
56
|
|
|
@@ -87,12 +88,12 @@ const client = new Anthropic({
|
|
|
87
88
|
|
|
88
89
|
## Runtime Endpoints
|
|
89
90
|
|
|
90
|
-
| Method
|
|
91
|
-
|
|
|
92
|
-
| `POST`
|
|
93
|
-
| `GET`
|
|
94
|
-
| `GET`
|
|
95
|
-
| `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 |
|
|
96
97
|
|
|
97
98
|
Health check:
|
|
98
99
|
|
|
@@ -121,20 +122,21 @@ app.listen(8080);
|
|
|
121
122
|
|
|
122
123
|
## Release Verification
|
|
123
124
|
|
|
124
|
-
`v1.
|
|
125
|
+
`v1.1.0` was verified on April 21, 2026 after publishing `@sunflower0305/claude-proxy` to npm.
|
|
125
126
|
|
|
126
127
|
Verified items:
|
|
127
128
|
|
|
129
|
+
- `npm view @sunflower0305/claude-proxy version dist-tags --json` confirmed `version: 1.1.0` and `latest: 1.1.0`
|
|
128
130
|
- `npm install @sunflower0305/claude-proxy` completed successfully in a clean temporary directory
|
|
129
131
|
- the published `claude-proxy` CLI started correctly from the installed package
|
|
130
132
|
- `GET /health` and `GET /v1/models` returned `200 OK`
|
|
131
|
-
- end-to-end proxying against a
|
|
132
|
-
- end-to-end proxying against the real Qwen Anthropic-compatible upstream passed for both non-streaming and streaming `POST /v1/messages`
|
|
133
|
+
- local end-to-end proxying against a mock Anthropic-compatible upstream passed for both non-streaming and streaming `POST /v1/messages`
|
|
133
134
|
|
|
134
135
|
Observed behavior during verification:
|
|
135
136
|
|
|
136
|
-
-
|
|
137
|
-
- the
|
|
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`
|
|
138
140
|
- the published package included the expected CLI entrypoint, `dist/` build output, `README.md`, `LICENSE`, and `.env.example`
|
|
139
141
|
|
|
140
142
|
## Development
|
|
@@ -148,15 +150,32 @@ npm run dev
|
|
|
148
150
|
|
|
149
151
|
## CI And Releases
|
|
150
152
|
|
|
151
|
-
GitHub Actions
|
|
153
|
+
GitHub Actions provides separate CI and CD workflows:
|
|
152
154
|
|
|
153
|
-
-
|
|
154
|
-
-
|
|
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
|
|
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`
|
|
158
157
|
|
|
159
|
-
|
|
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.
|
|
160
179
|
|
|
161
180
|
Build and local package verification:
|
|
162
181
|
|
|
@@ -171,7 +190,7 @@ Local integration test:
|
|
|
171
190
|
npm run test:proxy-local
|
|
172
191
|
```
|
|
173
192
|
|
|
174
|
-
Release notes for `v1.
|
|
193
|
+
Release notes for `v1.1.0` are available in [docs/releases/1.1.0.md](docs/releases/1.1.0.md).
|
|
175
194
|
|
|
176
195
|
## License
|
|
177
196
|
|
package/dist/proxy.js
CHANGED
|
@@ -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
|
-
}
|