@sunflower0305/claude-proxy 1.0.0 → 1.1.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 +2 -2
- package/README.md +37 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +79 -0
- package/dist/messages.d.ts +3 -0
- package/dist/messages.js +221 -0
- package/dist/providers.d.ts +11 -0
- package/dist/providers.js +53 -0
- package/dist/proxy.js +178 -177
- package/dist/runtime.d.ts +27 -0
- package/dist/runtime.js +83 -0
- 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,10 @@
|
|
|
1
1
|
# claude-proxy
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sunflower0305/claude-proxy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://coveralls.io/github/sunflower0305/claude-proxy?branch=master)
|
|
5
|
+
[](https://www.npmjs.com/package/@sunflower0305/claude-proxy)
|
|
6
|
+
[](https://github.com/sunflower0305/claude-proxy/blob/master/LICENSE)
|
|
7
|
+
|
|
3
8
|
`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
9
|
|
|
5
10
|
It currently supports `qwen`, `deepseek`, `glm`, `minimax`, and `kimi`.
|
|
@@ -114,6 +119,24 @@ const app = createApp();
|
|
|
114
119
|
app.listen(8080);
|
|
115
120
|
```
|
|
116
121
|
|
|
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
|
+
|
|
117
140
|
## Development
|
|
118
141
|
|
|
119
142
|
From source:
|
|
@@ -123,6 +146,18 @@ npm install
|
|
|
123
146
|
npm run dev
|
|
124
147
|
```
|
|
125
148
|
|
|
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
|
+
|
|
126
161
|
Build and local package verification:
|
|
127
162
|
|
|
128
163
|
```bash
|
|
@@ -136,6 +171,8 @@ Local integration test:
|
|
|
136
171
|
npm run test:proxy-local
|
|
137
172
|
```
|
|
138
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
|
+
|
|
139
176
|
## License
|
|
140
177
|
|
|
141
178
|
MIT
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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;
|
|
@@ -0,0 +1,53 @@
|
|
|
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/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" }));
|
|
@@ -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
|
-
|
|
410
|
-
║ http://localhost:${PORT}
|
|
411
|
-
║ Backend: ${
|
|
412
|
-
|
|
413
|
-
║ Set these env vars in your app:
|
|
414
|
-
║ ANTHROPIC_BASE_URL=http://localhost:${PORT}
|
|
415
|
-
║ ANTHROPIC_API_KEY=any-string-works
|
|
416
|
-
|
|
408
|
+
╔═══════════════════════════════════════════════════════╗
|
|
409
|
+
║ claude-proxy ║
|
|
410
|
+
╠═══════════════════════════════════════════════════════╣
|
|
411
|
+
║ http://localhost:${PORT} ║
|
|
412
|
+
║ Backend: ${initialProvider} (${initialConfig.model}) ║
|
|
413
|
+
╠═══════════════════════════════════════════════════════╣
|
|
414
|
+
║ Set these env vars in your app: ║
|
|
415
|
+
║ ANTHROPIC_BASE_URL=http://localhost:${PORT} ║
|
|
416
|
+
║ ANTHROPIC_API_KEY=any-string-works ║
|
|
417
|
+
╚═══════════════════════════════════════════════════════╝
|
|
417
418
|
`);
|
|
418
419
|
});
|
|
419
420
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunflower0305/claude-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|