codexmate 0.0.30 → 0.0.32
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 +363 -421
- package/README.zh.md +371 -354
- package/cli/agents-files.js +224 -224
- package/cli/archive-helpers.js +446 -446
- package/cli/auth-profiles.js +375 -375
- package/cli/builtin-proxy.js +1725 -1725
- package/cli/claude-proxy.js +1022 -1022
- package/cli/config-bootstrap.js +402 -402
- package/cli/config-health.js +454 -454
- package/cli/doctor-core.js +903 -903
- package/cli/import-skills-url.js +356 -356
- package/cli/local-bridge.js +324 -324
- package/cli/openai-bridge.js +1653 -1653
- package/cli/openclaw-config.js +629 -629
- package/cli/session-convert-args.js +69 -65
- package/cli/session-convert-io.js +82 -82
- package/cli/session-convert.js +150 -43
- package/cli/session-usage.concurrent.js +28 -28
- package/cli/session-usage.js +118 -118
- package/cli/session-usage.models.js +176 -176
- package/cli/skills.js +1141 -1141
- package/cli/zip-commands.js +510 -510
- package/cli.js +15829 -15481
- package/lib/automation.js +404 -404
- package/lib/cli-file-utils.js +151 -151
- package/lib/cli-models-utils.js +440 -440
- package/lib/cli-network-utils.js +190 -190
- package/lib/cli-path-utils.js +85 -85
- package/lib/cli-session-utils.js +121 -121
- package/lib/cli-sessions.js +426 -417
- package/lib/cli-utils.js +155 -155
- package/lib/cli-webhook.js +126 -126
- package/lib/download-artifacts.js +92 -92
- package/lib/mcp-stdio.js +453 -453
- package/lib/task-orchestrator.js +869 -869
- package/lib/text-diff.js +303 -303
- package/lib/workflow-engine.js +340 -340
- package/package.json +76 -76
- package/plugins/README.md +20 -20
- package/plugins/README.zh-CN.md +20 -20
- package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
- package/plugins/prompt-templates/computed.mjs +253 -253
- package/plugins/prompt-templates/index.mjs +8 -8
- package/plugins/prompt-templates/manifest.mjs +15 -15
- package/plugins/prompt-templates/methods.mjs +553 -553
- package/plugins/prompt-templates/overview.mjs +91 -91
- package/plugins/prompt-templates/ownership.mjs +19 -19
- package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
- package/plugins/prompt-templates/storage.mjs +64 -64
- package/plugins/registry.mjs +16 -16
- package/web-ui/app.js +647 -645
- package/web-ui/index.html +36 -36
- package/web-ui/logic.agents-diff.mjs +386 -386
- package/web-ui/logic.claude.mjs +168 -168
- package/web-ui/logic.codex.mjs +69 -69
- package/web-ui/logic.mjs +5 -5
- package/web-ui/logic.runtime.mjs +128 -128
- package/web-ui/logic.session-convert.mjs +70 -70
- package/web-ui/logic.sessions.mjs +781 -765
- package/web-ui/modules/api.mjs +90 -90
- package/web-ui/modules/app.computed.dashboard.mjs +248 -248
- package/web-ui/modules/app.computed.index.mjs +17 -17
- package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
- package/web-ui/modules/app.computed.session.mjs +693 -999
- package/web-ui/modules/app.constants.mjs +15 -15
- package/web-ui/modules/app.methods.agents.mjs +651 -632
- package/web-ui/modules/app.methods.claude-config.mjs +200 -200
- package/web-ui/modules/app.methods.codex-config.mjs +861 -917
- package/web-ui/modules/app.methods.index.mjs +94 -94
- package/web-ui/modules/app.methods.install.mjs +205 -205
- package/web-ui/modules/app.methods.navigation.mjs +774 -774
- package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
- package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
- package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
- package/web-ui/modules/app.methods.providers.mjs +529 -529
- package/web-ui/modules/app.methods.runtime.mjs +345 -345
- package/web-ui/modules/app.methods.session-actions.mjs +591 -593
- package/web-ui/modules/app.methods.session-browser.mjs +1012 -984
- package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
- package/web-ui/modules/app.methods.session-trash.mjs +438 -438
- package/web-ui/modules/app.methods.startup-claude.mjs +537 -534
- package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
- package/web-ui/modules/app.methods.webhook.mjs +79 -79
- package/web-ui/modules/config-mode.computed.mjs +124 -124
- package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
- package/web-ui/modules/i18n.dict.mjs +3177 -3146
- package/web-ui/modules/i18n.mjs +62 -62
- package/web-ui/modules/plugins.computed.mjs +3 -3
- package/web-ui/modules/plugins.methods.mjs +3 -3
- package/web-ui/modules/plugins.storage.mjs +11 -11
- package/web-ui/modules/provider-url-display.mjs +17 -17
- package/web-ui/modules/sessions-filters-url.mjs +85 -85
- package/web-ui/modules/skills.computed.mjs +107 -107
- package/web-ui/modules/skills.methods.mjs +482 -482
- package/web-ui/partials/index/layout-footer.html +13 -13
- package/web-ui/partials/index/layout-header.html +503 -500
- package/web-ui/partials/index/modal-config-template-agents.html +185 -174
- package/web-ui/partials/index/modal-confirm-toast.html +32 -32
- package/web-ui/partials/index/modal-health-check.html +45 -45
- package/web-ui/partials/index/modal-openclaw-config.html +280 -280
- package/web-ui/partials/index/modal-skills.html +200 -200
- package/web-ui/partials/index/modals-basic.html +162 -162
- package/web-ui/partials/index/panel-config-claude.html +136 -194
- package/web-ui/partials/index/panel-config-codex.html +197 -337
- package/web-ui/partials/index/panel-config-openclaw.html +83 -83
- package/web-ui/partials/index/panel-dashboard.html +219 -219
- package/web-ui/partials/index/panel-docs.html +115 -147
- package/web-ui/partials/index/panel-market.html +177 -177
- package/web-ui/partials/index/panel-orchestration.html +391 -391
- package/web-ui/partials/index/panel-plugins.html +253 -253
- package/web-ui/partials/index/panel-sessions.html +313 -308
- package/web-ui/partials/index/panel-settings.html +190 -190
- package/web-ui/partials/index/panel-trash.html +83 -88
- package/web-ui/partials/index/panel-usage.html +138 -371
- package/web-ui/res/json5.min.js +1 -1
- package/web-ui/res/vue.global.prod.js +13 -13
- package/web-ui/session-helpers.mjs +591 -594
- package/web-ui/source-bundle.cjs +233 -233
- package/web-ui/styles/base-theme.css +281 -281
- package/web-ui/styles/bridge-pool.css +197 -0
- package/web-ui/styles/controls-forms.css +433 -422
- package/web-ui/styles/dashboard.css +406 -406
- package/web-ui/styles/docs-panel.css +245 -271
- package/web-ui/styles/feedback.css +108 -108
- package/web-ui/styles/health-check-dialog.css +144 -144
- package/web-ui/styles/layout-shell.css +638 -626
- package/web-ui/styles/modals-core.css +466 -466
- package/web-ui/styles/navigation-panels.css +391 -391
- package/web-ui/styles/openclaw-structured.css +266 -266
- package/web-ui/styles/plugins-panel.css +564 -564
- package/web-ui/styles/responsive.css +392 -454
- package/web-ui/styles/sessions-list.css +647 -417
- package/web-ui/styles/sessions-preview.css +407 -407
- package/web-ui/styles/sessions-toolbar-trash.css +518 -334
- package/web-ui/styles/sessions-usage.css +588 -1040
- package/web-ui/styles/settings-panel.css +349 -349
- package/web-ui/styles/skills-list.css +305 -305
- package/web-ui/styles/skills-market.css +429 -429
- package/web-ui/styles/task-orchestration.css +822 -822
- package/web-ui/styles/titles-cards.css +472 -472
- package/web-ui/styles/trash-panel.css +90 -90
- package/web-ui/styles/webhook.css +81 -81
- package/web-ui/styles.css +24 -23
- package/web-ui.html +17 -17
package/cli/claude-proxy.js
CHANGED
|
@@ -1,1022 +1,1022 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const https = require('https');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
|
|
5
|
-
const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
|
|
6
|
-
const {
|
|
7
|
-
extractModelNames,
|
|
8
|
-
extractModelResponseText,
|
|
9
|
-
normalizeWireApi
|
|
10
|
-
} = require('../lib/cli-models-utils');
|
|
11
|
-
const { toIsoTime } = require('../lib/cli-session-utils');
|
|
12
|
-
|
|
13
|
-
function isPlainObject(value) {
|
|
14
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readNonNegativeInteger(value) {
|
|
18
|
-
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
19
|
-
return Math.floor(value);
|
|
20
|
-
}
|
|
21
|
-
if (typeof value === 'string' && value.trim()) {
|
|
22
|
-
const parsed = Number.parseInt(value.trim(), 10);
|
|
23
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
24
|
-
}
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function formatHostForUrl(host) {
|
|
29
|
-
const value = typeof host === 'string' ? host.trim() : '';
|
|
30
|
-
if (!value) return '';
|
|
31
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
if (value.includes(':')) {
|
|
35
|
-
return `[${value}]`;
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeAnthropicContentBlocks(content) {
|
|
41
|
-
if (typeof content === 'string') {
|
|
42
|
-
return content.trim() ? [{ type: 'text', text: content }] : [];
|
|
43
|
-
}
|
|
44
|
-
if (Array.isArray(content)) {
|
|
45
|
-
return content.flatMap((item) => normalizeAnthropicContentBlocks(item));
|
|
46
|
-
}
|
|
47
|
-
if (content && typeof content === 'object') {
|
|
48
|
-
if (typeof content.type === 'string') {
|
|
49
|
-
return [content];
|
|
50
|
-
}
|
|
51
|
-
if (typeof content.text === 'string') {
|
|
52
|
-
return [{ type: 'text', text: content.text }];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function collectAnthropicTextContent(content) {
|
|
59
|
-
const pieces = [];
|
|
60
|
-
for (const block of normalizeAnthropicContentBlocks(content)) {
|
|
61
|
-
if (block && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
62
|
-
pieces.push(block.text.trim());
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return pieces.join('\n\n').trim();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function safeJsonStringify(value) {
|
|
69
|
-
try {
|
|
70
|
-
return JSON.stringify(value);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
return JSON.stringify(String(value));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function stringifyAnthropicToolResultContent(content) {
|
|
77
|
-
if (typeof content === 'string') {
|
|
78
|
-
return content;
|
|
79
|
-
}
|
|
80
|
-
const text = collectAnthropicTextContent(content);
|
|
81
|
-
if (text) {
|
|
82
|
-
return text;
|
|
83
|
-
}
|
|
84
|
-
return safeJsonStringify(content);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function appendAnthropicMessageToResponsesInput(target, message) {
|
|
88
|
-
if (!message || typeof message !== 'object') return;
|
|
89
|
-
const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
90
|
-
const role = roleRaw === 'assistant' ? 'assistant' : 'user';
|
|
91
|
-
const textType = role === 'assistant' ? 'output_text' : 'input_text';
|
|
92
|
-
let buffered = [];
|
|
93
|
-
|
|
94
|
-
const flushBuffered = () => {
|
|
95
|
-
if (!buffered.length) return;
|
|
96
|
-
target.push({ role, content: buffered });
|
|
97
|
-
buffered = [];
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
for (const block of normalizeAnthropicContentBlocks(message.content)) {
|
|
101
|
-
if (!block || typeof block !== 'object') continue;
|
|
102
|
-
if (block.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
103
|
-
buffered.push({ type: textType, text: block.text });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (block.type === 'tool_use' && typeof block.name === 'string' && block.name.trim()) {
|
|
107
|
-
flushBuffered();
|
|
108
|
-
target.push({
|
|
109
|
-
type: 'function_call',
|
|
110
|
-
call_id: typeof block.id === 'string' && block.id.trim()
|
|
111
|
-
? block.id.trim()
|
|
112
|
-
: `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
113
|
-
name: block.name.trim(),
|
|
114
|
-
arguments: safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {})
|
|
115
|
-
});
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
|
|
119
|
-
flushBuffered();
|
|
120
|
-
target.push({
|
|
121
|
-
type: 'function_call_output',
|
|
122
|
-
call_id: block.tool_use_id.trim(),
|
|
123
|
-
output: stringifyAnthropicToolResultContent(block.content)
|
|
124
|
-
});
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
buffered.push({
|
|
128
|
-
type: textType,
|
|
129
|
-
text: `[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
flushBuffered();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function mapAnthropicToolChoiceToResponses(toolChoice) {
|
|
137
|
-
if (!toolChoice) return undefined;
|
|
138
|
-
if (typeof toolChoice === 'string') {
|
|
139
|
-
if (toolChoice === 'auto') return 'auto';
|
|
140
|
-
if (toolChoice === 'any') return 'required';
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
if (!toolChoice || typeof toolChoice !== 'object') return undefined;
|
|
144
|
-
const type = typeof toolChoice.type === 'string' ? toolChoice.type.trim().toLowerCase() : '';
|
|
145
|
-
if (type === 'auto') return 'auto';
|
|
146
|
-
if (type === 'any') return 'required';
|
|
147
|
-
if (type === 'tool' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
|
|
148
|
-
return {
|
|
149
|
-
type: 'function',
|
|
150
|
-
name: toolChoice.name.trim()
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function buildBuiltinClaudeResponsesRequest(payload = {}) {
|
|
157
|
-
const model = typeof payload.model === 'string' ? payload.model.trim() : '';
|
|
158
|
-
if (!model) {
|
|
159
|
-
throw new Error('Anthropic messages 请求缺少 model');
|
|
160
|
-
}
|
|
161
|
-
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
162
|
-
if (!messages.length) {
|
|
163
|
-
throw new Error('Anthropic messages 请求缺少 messages');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const maxTokens = parseInt(String(payload.max_tokens), 10);
|
|
167
|
-
const requestBody = {
|
|
168
|
-
model,
|
|
169
|
-
input: [],
|
|
170
|
-
max_output_tokens: Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : 1024
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const instructions = collectAnthropicTextContent(payload.system);
|
|
174
|
-
if (instructions) {
|
|
175
|
-
requestBody.instructions = instructions;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (const message of messages) {
|
|
179
|
-
appendAnthropicMessageToResponsesInput(requestBody.input, message);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (Number.isFinite(payload.temperature)) {
|
|
183
|
-
requestBody.temperature = Number(payload.temperature);
|
|
184
|
-
}
|
|
185
|
-
if (Number.isFinite(payload.top_p)) {
|
|
186
|
-
requestBody.top_p = Number(payload.top_p);
|
|
187
|
-
}
|
|
188
|
-
if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
|
|
189
|
-
requestBody.stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
|
|
190
|
-
}
|
|
191
|
-
if (isPlainObject(payload.metadata)) {
|
|
192
|
-
requestBody.metadata = payload.metadata;
|
|
193
|
-
}
|
|
194
|
-
if (Array.isArray(payload.tools) && payload.tools.length) {
|
|
195
|
-
requestBody.tools = payload.tools
|
|
196
|
-
.map((tool) => {
|
|
197
|
-
if (!tool || typeof tool !== 'object') return null;
|
|
198
|
-
const name = typeof tool.name === 'string' ? tool.name.trim() : '';
|
|
199
|
-
if (!name) return null;
|
|
200
|
-
return {
|
|
201
|
-
type: 'function',
|
|
202
|
-
name,
|
|
203
|
-
description: typeof tool.description === 'string' ? tool.description : '',
|
|
204
|
-
parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
|
|
205
|
-
};
|
|
206
|
-
})
|
|
207
|
-
.filter(Boolean);
|
|
208
|
-
if (!requestBody.tools.length) {
|
|
209
|
-
delete requestBody.tools;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const toolChoice = mapAnthropicToolChoiceToResponses(payload.tool_choice);
|
|
214
|
-
if (toolChoice !== undefined) {
|
|
215
|
-
requestBody.tool_choice = toolChoice;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return requestBody;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function parseJsonObjectLoose(value) {
|
|
222
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
223
|
-
return value;
|
|
224
|
-
}
|
|
225
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
226
|
-
return {};
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(value);
|
|
230
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
231
|
-
} catch (e) {
|
|
232
|
-
return {};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function readResponsesUsageValue(value) {
|
|
237
|
-
const parsed = readNonNegativeInteger(value);
|
|
238
|
-
return parsed > 0 ? parsed : 0;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function buildAnthropicUsageFromResponses(payload) {
|
|
242
|
-
const usage = payload && payload.usage && typeof payload.usage === 'object' ? payload.usage : {};
|
|
243
|
-
return {
|
|
244
|
-
input_tokens: readResponsesUsageValue(usage.input_tokens),
|
|
245
|
-
output_tokens: readResponsesUsageValue(usage.output_tokens)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function collectAnthropicContentFromResponsesOutput(payload) {
|
|
250
|
-
const content = [];
|
|
251
|
-
const output = Array.isArray(payload && payload.output) ? payload.output : [];
|
|
252
|
-
for (const item of output) {
|
|
253
|
-
if (!item || typeof item !== 'object') continue;
|
|
254
|
-
if (item.type === 'function_call') {
|
|
255
|
-
content.push({
|
|
256
|
-
type: 'tool_use',
|
|
257
|
-
id: typeof item.call_id === 'string' && item.call_id.trim()
|
|
258
|
-
? item.call_id.trim()
|
|
259
|
-
: (typeof item.id === 'string' && item.id.trim()
|
|
260
|
-
? item.id.trim()
|
|
261
|
-
: `toolu_${crypto.randomBytes(8).toString('hex')}`),
|
|
262
|
-
name: typeof item.name === 'string' ? item.name : '',
|
|
263
|
-
input: parseJsonObjectLoose(item.arguments)
|
|
264
|
-
});
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
268
|
-
for (const block of item.content) {
|
|
269
|
-
if (!block || typeof block !== 'object') continue;
|
|
270
|
-
if ((block.type === 'output_text' || block.type === 'text' || block.type === 'input_text')
|
|
271
|
-
&& typeof block.text === 'string' && block.text) {
|
|
272
|
-
content.push({ type: 'text', text: block.text });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (!content.length) {
|
|
278
|
-
const fallbackText = extractModelResponseText(payload);
|
|
279
|
-
if (fallbackText) {
|
|
280
|
-
content.push({ type: 'text', text: fallbackText });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return content;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function buildAnthropicStopReasonFromResponses(payload, content) {
|
|
287
|
-
if (Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')) {
|
|
288
|
-
return 'tool_use';
|
|
289
|
-
}
|
|
290
|
-
const incompleteReason = payload && payload.incomplete_details && typeof payload.incomplete_details.reason === 'string'
|
|
291
|
-
? payload.incomplete_details.reason
|
|
292
|
-
: '';
|
|
293
|
-
if (incompleteReason === 'max_output_tokens') {
|
|
294
|
-
return 'max_tokens';
|
|
295
|
-
}
|
|
296
|
-
return 'end_turn';
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function buildAnthropicMessageFromResponses(payload, requestPayload = {}) {
|
|
300
|
-
const content = collectAnthropicContentFromResponsesOutput(payload);
|
|
301
|
-
const usage = buildAnthropicUsageFromResponses(payload);
|
|
302
|
-
return {
|
|
303
|
-
id: typeof payload.id === 'string' && payload.id.trim()
|
|
304
|
-
? payload.id.trim()
|
|
305
|
-
: `msg_${crypto.randomBytes(8).toString('hex')}`,
|
|
306
|
-
type: 'message',
|
|
307
|
-
role: 'assistant',
|
|
308
|
-
model: typeof payload.model === 'string' && payload.model.trim()
|
|
309
|
-
? payload.model.trim()
|
|
310
|
-
: (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
|
|
311
|
-
content,
|
|
312
|
-
stop_reason: buildAnthropicStopReasonFromResponses(payload, content),
|
|
313
|
-
stop_sequence: null,
|
|
314
|
-
usage
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function buildAnthropicStreamEvents(message) {
|
|
319
|
-
const usage = message && message.usage && typeof message.usage === 'object' ? message.usage : {};
|
|
320
|
-
const startUsage = {
|
|
321
|
-
input_tokens: readResponsesUsageValue(usage.input_tokens),
|
|
322
|
-
output_tokens: 0
|
|
323
|
-
};
|
|
324
|
-
const events = [{
|
|
325
|
-
event: 'message_start',
|
|
326
|
-
data: {
|
|
327
|
-
type: 'message_start',
|
|
328
|
-
message: {
|
|
329
|
-
...message,
|
|
330
|
-
content: [],
|
|
331
|
-
stop_reason: null,
|
|
332
|
-
stop_sequence: null,
|
|
333
|
-
usage: startUsage
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}];
|
|
337
|
-
|
|
338
|
-
const blocks = Array.isArray(message && message.content) ? message.content : [];
|
|
339
|
-
blocks.forEach((block, index) => {
|
|
340
|
-
if (!block || typeof block !== 'object') return;
|
|
341
|
-
if (block.type === 'text') {
|
|
342
|
-
events.push({
|
|
343
|
-
event: 'content_block_start',
|
|
344
|
-
data: {
|
|
345
|
-
type: 'content_block_start',
|
|
346
|
-
index,
|
|
347
|
-
content_block: { type: 'text', text: '' }
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
if (typeof block.text === 'string' && block.text) {
|
|
351
|
-
events.push({
|
|
352
|
-
event: 'content_block_delta',
|
|
353
|
-
data: {
|
|
354
|
-
type: 'content_block_delta',
|
|
355
|
-
index,
|
|
356
|
-
delta: { type: 'text_delta', text: block.text }
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (block.type === 'tool_use') {
|
|
364
|
-
events.push({
|
|
365
|
-
event: 'content_block_start',
|
|
366
|
-
data: {
|
|
367
|
-
type: 'content_block_start',
|
|
368
|
-
index,
|
|
369
|
-
content_block: {
|
|
370
|
-
type: 'tool_use',
|
|
371
|
-
id: block.id,
|
|
372
|
-
name: block.name,
|
|
373
|
-
input: {}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
const partialJson = safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {});
|
|
378
|
-
if (partialJson && partialJson !== '{}') {
|
|
379
|
-
events.push({
|
|
380
|
-
event: 'content_block_delta',
|
|
381
|
-
data: {
|
|
382
|
-
type: 'content_block_delta',
|
|
383
|
-
index,
|
|
384
|
-
delta: { type: 'input_json_delta', partial_json: partialJson }
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
events.push({
|
|
393
|
-
event: 'message_delta',
|
|
394
|
-
data: {
|
|
395
|
-
type: 'message_delta',
|
|
396
|
-
delta: {
|
|
397
|
-
stop_reason: message && message.stop_reason ? message.stop_reason : 'end_turn',
|
|
398
|
-
stop_sequence: message && Object.prototype.hasOwnProperty.call(message, 'stop_sequence')
|
|
399
|
-
? message.stop_sequence
|
|
400
|
-
: null
|
|
401
|
-
},
|
|
402
|
-
usage: {
|
|
403
|
-
output_tokens: readResponsesUsageValue(usage.output_tokens)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
events.push({ event: 'message_stop', data: { type: 'message_stop' } });
|
|
408
|
-
return events;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function buildAnthropicModelsPayload(upstreamPayload) {
|
|
412
|
-
const ids = extractModelNames(upstreamPayload);
|
|
413
|
-
return {
|
|
414
|
-
data: ids.map((id) => ({
|
|
415
|
-
type: 'model',
|
|
416
|
-
id,
|
|
417
|
-
display_name: id,
|
|
418
|
-
created_at: '1970-01-01T00:00:00Z'
|
|
419
|
-
})),
|
|
420
|
-
first_id: ids[0] || null,
|
|
421
|
-
last_id: ids.length ? ids[ids.length - 1] : null,
|
|
422
|
-
has_more: false
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function createBuiltinClaudeProxyRuntimeController(deps = {}) {
|
|
427
|
-
const {
|
|
428
|
-
BUILTIN_CLAUDE_PROXY_SETTINGS_FILE,
|
|
429
|
-
DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
|
|
430
|
-
BUILTIN_PROXY_PROVIDER_NAME,
|
|
431
|
-
MAX_API_BODY_SIZE,
|
|
432
|
-
HTTP_KEEP_ALIVE_AGENT,
|
|
433
|
-
HTTPS_KEEP_ALIVE_AGENT,
|
|
434
|
-
readConfigOrVirtualDefault,
|
|
435
|
-
resolveBuiltinProxyProviderName,
|
|
436
|
-
resolveAuthTokenFromCurrentProfile
|
|
437
|
-
} = deps;
|
|
438
|
-
|
|
439
|
-
if (!BUILTIN_CLAUDE_PROXY_SETTINGS_FILE) {
|
|
440
|
-
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 BUILTIN_CLAUDE_PROXY_SETTINGS_FILE');
|
|
441
|
-
}
|
|
442
|
-
if (!DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS !== 'object') {
|
|
443
|
-
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS');
|
|
444
|
-
}
|
|
445
|
-
if (typeof readConfigOrVirtualDefault !== 'function') {
|
|
446
|
-
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 readConfigOrVirtualDefault');
|
|
447
|
-
}
|
|
448
|
-
if (typeof resolveBuiltinProxyProviderName !== 'function') {
|
|
449
|
-
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveBuiltinProxyProviderName');
|
|
450
|
-
}
|
|
451
|
-
if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
|
|
452
|
-
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
let runtime = null;
|
|
456
|
-
|
|
457
|
-
function normalizeBuiltinClaudeProxySettings(raw) {
|
|
458
|
-
const merged = {
|
|
459
|
-
...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
|
|
460
|
-
...(isPlainObject(raw) ? raw : {})
|
|
461
|
-
};
|
|
462
|
-
const host = typeof merged.host === 'string' ? merged.host.trim() : '';
|
|
463
|
-
const port = parseInt(String(merged.port), 10);
|
|
464
|
-
const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
|
|
465
|
-
const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
|
|
466
|
-
const timeoutMs = parseInt(String(merged.timeoutMs), 10);
|
|
467
|
-
const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' || authSourceRaw === 'request'
|
|
468
|
-
? authSourceRaw
|
|
469
|
-
: 'provider';
|
|
470
|
-
|
|
471
|
-
return {
|
|
472
|
-
enabled: merged.enabled !== false,
|
|
473
|
-
host: host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
|
|
474
|
-
port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.port,
|
|
475
|
-
provider,
|
|
476
|
-
authSource,
|
|
477
|
-
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
|
|
478
|
-
? timeoutMs
|
|
479
|
-
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function readBuiltinClaudeProxySettings() {
|
|
484
|
-
const parsed = readJsonFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, null);
|
|
485
|
-
return normalizeBuiltinClaudeProxySettings(parsed);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function saveBuiltinClaudeProxySettings(payload = {}, options = {}) {
|
|
489
|
-
const current = readBuiltinClaudeProxySettings();
|
|
490
|
-
const merged = normalizeBuiltinClaudeProxySettings({
|
|
491
|
-
...current,
|
|
492
|
-
...(isPlainObject(payload) ? payload : {})
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
if (!merged.host) {
|
|
496
|
-
return { error: 'Claude 兼容代理 host 不能为空' };
|
|
497
|
-
}
|
|
498
|
-
if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
|
|
499
|
-
return { error: 'Claude 兼容代理端口无效(1-65535)' };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const { config } = readConfigOrVirtualDefault();
|
|
503
|
-
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
504
|
-
const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
505
|
-
const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
|
|
506
|
-
const normalized = {
|
|
507
|
-
...merged,
|
|
508
|
-
provider: finalProvider
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
if (!options.skipWrite) {
|
|
512
|
-
writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, normalized);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return {
|
|
516
|
-
success: true,
|
|
517
|
-
settings: normalized
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function buildBuiltinClaudeProxyListenUrl(settings) {
|
|
522
|
-
const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host);
|
|
523
|
-
return `http://${host}:${settings.port}`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function resolveBuiltinClaudeProxyUpstream(settings) {
|
|
527
|
-
const { config } = readConfigOrVirtualDefault();
|
|
528
|
-
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
529
|
-
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
530
|
-
const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
|
|
531
|
-
if (!providerName) {
|
|
532
|
-
return { error: '未找到可用的上游 provider,请先添加 responses provider' };
|
|
533
|
-
}
|
|
534
|
-
if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
|
|
535
|
-
return { error: `Claude 兼容代理的上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
|
|
536
|
-
}
|
|
537
|
-
const provider = providers[providerName];
|
|
538
|
-
if (!provider || !isPlainObject(provider)) {
|
|
539
|
-
return { error: `上游 provider 不存在: ${providerName}` };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const wireApi = normalizeWireApi(provider.wire_api);
|
|
543
|
-
if (wireApi !== 'responses') {
|
|
544
|
-
return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
548
|
-
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
|
|
549
|
-
return { error: `上游 provider base_url 无效: ${providerName}` };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
let token = '';
|
|
553
|
-
if (settings.authSource === 'profile') {
|
|
554
|
-
token = resolveAuthTokenFromCurrentProfile();
|
|
555
|
-
} else if (settings.authSource === 'provider') {
|
|
556
|
-
token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
|
|
557
|
-
if (!token) {
|
|
558
|
-
token = resolveAuthTokenFromCurrentProfile();
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
let authHeader = '';
|
|
563
|
-
if (token) {
|
|
564
|
-
authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return {
|
|
568
|
-
providerName,
|
|
569
|
-
baseUrl: normalizeBaseUrl(baseUrl),
|
|
570
|
-
authHeader
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream) {
|
|
575
|
-
if (settings && settings.authSource === 'request') {
|
|
576
|
-
const apiKey = typeof req.headers['x-api-key'] === 'string'
|
|
577
|
-
? req.headers['x-api-key'].trim()
|
|
578
|
-
: '';
|
|
579
|
-
if (!apiKey) {
|
|
580
|
-
return { error: '缺少 x-api-key,无法转发到上游 responses provider', statusCode: 401 };
|
|
581
|
-
}
|
|
582
|
-
return {
|
|
583
|
-
authHeader: /^bearer\s+/i.test(apiKey) ? apiKey : `Bearer ${apiKey}`
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
return { authHeader: upstream.authHeader || '' };
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function readJsonRequestBody(req, options = {}) {
|
|
590
|
-
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
591
|
-
? Math.floor(options.maxBytes)
|
|
592
|
-
: MAX_API_BODY_SIZE;
|
|
593
|
-
return new Promise((resolve, reject) => {
|
|
594
|
-
const chunks = [];
|
|
595
|
-
let total = 0;
|
|
596
|
-
req.on('data', (chunk) => {
|
|
597
|
-
total += chunk.length;
|
|
598
|
-
if (total > maxBytes) {
|
|
599
|
-
reject(new Error(`request body too large (${maxBytes} bytes max)`));
|
|
600
|
-
try { req.destroy(); } catch (_) {}
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
chunks.push(chunk);
|
|
604
|
-
});
|
|
605
|
-
req.on('error', reject);
|
|
606
|
-
req.on('end', () => {
|
|
607
|
-
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
608
|
-
if (!raw) {
|
|
609
|
-
resolve({});
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
try {
|
|
613
|
-
resolve(JSON.parse(raw));
|
|
614
|
-
} catch (e) {
|
|
615
|
-
reject(new Error(`invalid JSON body: ${e.message}`));
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function extractProxyErrorMessage(payload, fallback = '') {
|
|
622
|
-
if (!payload || typeof payload !== 'object') {
|
|
623
|
-
return fallback || 'upstream request failed';
|
|
624
|
-
}
|
|
625
|
-
if (payload.error && typeof payload.error === 'object') {
|
|
626
|
-
if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
|
|
627
|
-
return payload.error.message.trim();
|
|
628
|
-
}
|
|
629
|
-
if (typeof payload.error.error === 'string' && payload.error.error.trim()) {
|
|
630
|
-
return payload.error.error.trim();
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (typeof payload.message === 'string' && payload.message.trim()) {
|
|
634
|
-
return payload.message.trim();
|
|
635
|
-
}
|
|
636
|
-
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
637
|
-
return payload.error.trim();
|
|
638
|
-
}
|
|
639
|
-
return fallback || 'upstream request failed';
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function writeAnthropicProxyError(res, statusCode, message, type = 'api_error') {
|
|
643
|
-
const body = JSON.stringify({
|
|
644
|
-
type: 'error',
|
|
645
|
-
error: {
|
|
646
|
-
type,
|
|
647
|
-
message: typeof message === 'string' && message.trim() ? message.trim() : 'request failed'
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
res.writeHead(statusCode, {
|
|
651
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
652
|
-
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
653
|
-
});
|
|
654
|
-
res.end(body, 'utf-8');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function requestBuiltinClaudeProxyUpstream(upstream, requestOptions = {}) {
|
|
658
|
-
const pathSuffix = typeof requestOptions.pathSuffix === 'string' ? requestOptions.pathSuffix : '';
|
|
659
|
-
const targetBase = joinApiUrl(upstream.baseUrl, pathSuffix);
|
|
660
|
-
if (!targetBase) {
|
|
661
|
-
return Promise.reject(new Error('failed to build upstream URL'));
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
let targetUrl;
|
|
665
|
-
try {
|
|
666
|
-
targetUrl = new URL(targetBase);
|
|
667
|
-
} catch (e) {
|
|
668
|
-
return Promise.reject(new Error(`invalid upstream URL: ${e.message}`));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const bodyText = requestOptions.body === undefined ? '' : JSON.stringify(requestOptions.body);
|
|
672
|
-
const headers = {
|
|
673
|
-
Accept: 'application/json',
|
|
674
|
-
...(isPlainObject(requestOptions.headers) ? requestOptions.headers : {})
|
|
675
|
-
};
|
|
676
|
-
if (bodyText) {
|
|
677
|
-
headers['Content-Type'] = 'application/json';
|
|
678
|
-
headers['Content-Length'] = Buffer.byteLength(bodyText);
|
|
679
|
-
}
|
|
680
|
-
if (requestOptions.authHeader) {
|
|
681
|
-
headers.authorization = requestOptions.authHeader;
|
|
682
|
-
}
|
|
683
|
-
headers['x-codexmate-claude-proxy'] = '1';
|
|
684
|
-
|
|
685
|
-
const transport = targetUrl.protocol === 'https:' ? https : http;
|
|
686
|
-
const timeoutMs = Number.isFinite(requestOptions.timeoutMs) && requestOptions.timeoutMs > 0
|
|
687
|
-
? requestOptions.timeoutMs
|
|
688
|
-
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs;
|
|
689
|
-
|
|
690
|
-
return new Promise((resolve, reject) => {
|
|
691
|
-
const upstreamReq = transport.request({
|
|
692
|
-
protocol: targetUrl.protocol,
|
|
693
|
-
hostname: targetUrl.hostname,
|
|
694
|
-
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
|
695
|
-
method: requestOptions.method || 'POST',
|
|
696
|
-
path: `${targetUrl.pathname}${targetUrl.search}`,
|
|
697
|
-
headers,
|
|
698
|
-
agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
|
|
699
|
-
}, (upstreamRes) => {
|
|
700
|
-
const chunks = [];
|
|
701
|
-
let total = 0;
|
|
702
|
-
upstreamRes.on('data', (chunk) => {
|
|
703
|
-
total += chunk.length;
|
|
704
|
-
if (total > MAX_API_BODY_SIZE) {
|
|
705
|
-
upstreamReq.destroy(new Error(`upstream body too large (${MAX_API_BODY_SIZE} bytes max)`));
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
chunks.push(chunk);
|
|
709
|
-
});
|
|
710
|
-
upstreamRes.on('error', reject);
|
|
711
|
-
upstreamRes.on('end', () => {
|
|
712
|
-
const rawBody = Buffer.concat(chunks).toString('utf-8');
|
|
713
|
-
let payload = null;
|
|
714
|
-
if (rawBody.trim()) {
|
|
715
|
-
try {
|
|
716
|
-
payload = JSON.parse(rawBody);
|
|
717
|
-
} catch (_) {
|
|
718
|
-
payload = null;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
resolve({
|
|
722
|
-
statusCode: upstreamRes.statusCode || 502,
|
|
723
|
-
headers: upstreamRes.headers,
|
|
724
|
-
rawBody,
|
|
725
|
-
payload
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
upstreamReq.setTimeout(timeoutMs, () => {
|
|
731
|
-
upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
|
|
732
|
-
});
|
|
733
|
-
upstreamReq.on('error', reject);
|
|
734
|
-
if (bodyText) {
|
|
735
|
-
upstreamReq.write(bodyText, 'utf-8');
|
|
736
|
-
}
|
|
737
|
-
upstreamReq.end();
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function writeAnthropicStreamEvents(res, message) {
|
|
742
|
-
const events = buildAnthropicStreamEvents(message);
|
|
743
|
-
res.writeHead(200, {
|
|
744
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
745
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
746
|
-
Connection: 'keep-alive',
|
|
747
|
-
'X-Accel-Buffering': 'no'
|
|
748
|
-
});
|
|
749
|
-
for (const event of events) {
|
|
750
|
-
if (event && event.event) {
|
|
751
|
-
res.write(`event: ${event.event}\n`);
|
|
752
|
-
}
|
|
753
|
-
res.write(`data: ${JSON.stringify(event && event.data ? event.data : {})}\n\n`);
|
|
754
|
-
}
|
|
755
|
-
res.end();
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
async function handleBuiltinClaudeProxyRequest(req, res, settings, upstream) {
|
|
759
|
-
let parsedIncoming;
|
|
760
|
-
try {
|
|
761
|
-
parsedIncoming = new URL(req.url || '/', 'http://localhost');
|
|
762
|
-
} catch (e) {
|
|
763
|
-
writeAnthropicProxyError(res, 400, 'invalid request path', 'invalid_request_error');
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const incomingPath = parsedIncoming.pathname || '/';
|
|
768
|
-
if (incomingPath === '/health' || incomingPath === '/status') {
|
|
769
|
-
const body = JSON.stringify({
|
|
770
|
-
ok: true,
|
|
771
|
-
upstreamProvider: upstream.providerName,
|
|
772
|
-
upstreamBaseUrl: upstream.baseUrl,
|
|
773
|
-
mode: 'anthropic-to-responses'
|
|
774
|
-
});
|
|
775
|
-
res.writeHead(200, {
|
|
776
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
777
|
-
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
778
|
-
});
|
|
779
|
-
res.end(body, 'utf-8');
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const authResult = buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream);
|
|
784
|
-
if (authResult.error) {
|
|
785
|
-
writeAnthropicProxyError(res, authResult.statusCode || 401, authResult.error, 'authentication_error');
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (incomingPath === '/v1/models') {
|
|
790
|
-
if ((req.method || 'GET').toUpperCase() !== 'GET') {
|
|
791
|
-
res.writeHead(405, { Allow: 'GET' });
|
|
792
|
-
res.end();
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
|
|
796
|
-
method: 'GET',
|
|
797
|
-
pathSuffix: 'models',
|
|
798
|
-
authHeader: authResult.authHeader,
|
|
799
|
-
timeoutMs: settings.timeoutMs
|
|
800
|
-
});
|
|
801
|
-
if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
|
|
802
|
-
writeAnthropicProxyError(
|
|
803
|
-
res,
|
|
804
|
-
upstreamResponse.statusCode,
|
|
805
|
-
extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
|
|
806
|
-
'api_error'
|
|
807
|
-
);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
const body = JSON.stringify(buildAnthropicModelsPayload(upstreamResponse.payload));
|
|
811
|
-
res.writeHead(200, {
|
|
812
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
813
|
-
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
814
|
-
});
|
|
815
|
-
res.end(body, 'utf-8');
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (incomingPath !== '/v1/messages') {
|
|
820
|
-
writeAnthropicProxyError(res, 404, 'Claude 兼容代理仅支持 /v1/messages 与 /v1/models', 'not_found_error');
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if ((req.method || 'POST').toUpperCase() !== 'POST') {
|
|
825
|
-
res.writeHead(405, { Allow: 'POST' });
|
|
826
|
-
res.end();
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const payload = await readJsonRequestBody(req);
|
|
831
|
-
const upstreamRequestBody = buildBuiltinClaudeResponsesRequest(payload);
|
|
832
|
-
const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
|
|
833
|
-
method: 'POST',
|
|
834
|
-
pathSuffix: 'responses',
|
|
835
|
-
body: upstreamRequestBody,
|
|
836
|
-
authHeader: authResult.authHeader,
|
|
837
|
-
timeoutMs: settings.timeoutMs
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
|
|
841
|
-
writeAnthropicProxyError(
|
|
842
|
-
res,
|
|
843
|
-
upstreamResponse.statusCode,
|
|
844
|
-
extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
|
|
845
|
-
'api_error'
|
|
846
|
-
);
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const anthropicMessage = buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload);
|
|
851
|
-
if (payload.stream === true) {
|
|
852
|
-
writeAnthropicStreamEvents(res, anthropicMessage);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const body = JSON.stringify(anthropicMessage);
|
|
857
|
-
res.writeHead(200, {
|
|
858
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
859
|
-
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
860
|
-
});
|
|
861
|
-
res.end(body, 'utf-8');
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function createBuiltinClaudeProxyServer(settings, upstream) {
|
|
865
|
-
const connections = new Set();
|
|
866
|
-
const server = http.createServer((req, res) => {
|
|
867
|
-
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
868
|
-
const isLoopback = !remoteAddr
|
|
869
|
-
|| remoteAddr === '127.0.0.1'
|
|
870
|
-
|| remoteAddr === '::1'
|
|
871
|
-
|| remoteAddr === '::ffff:127.0.0.1';
|
|
872
|
-
if (!isLoopback) {
|
|
873
|
-
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
|
|
874
|
-
? process.env.CODEXMATE_HTTP_TOKEN.trim()
|
|
875
|
-
: '';
|
|
876
|
-
if (!expected) {
|
|
877
|
-
writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
881
|
-
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
882
|
-
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
883
|
-
const actual = match && match[1]
|
|
884
|
-
? match[1].trim()
|
|
885
|
-
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
886
|
-
if (!actual || actual !== expected) {
|
|
887
|
-
writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
|
|
892
|
-
if (res.headersSent) {
|
|
893
|
-
try { res.destroy(err); } catch (_) {}
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
writeAnthropicProxyError(res, 502, `claude proxy request failed: ${err.message}`, 'api_error');
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
server.on('connection', (socket) => {
|
|
901
|
-
connections.add(socket);
|
|
902
|
-
socket.on('close', () => connections.delete(socket));
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
return new Promise((resolve, reject) => {
|
|
906
|
-
server.once('error', reject);
|
|
907
|
-
server.listen(settings.port, settings.host, () => {
|
|
908
|
-
server.removeListener('error', reject);
|
|
909
|
-
resolve({
|
|
910
|
-
server,
|
|
911
|
-
connections,
|
|
912
|
-
settings,
|
|
913
|
-
upstream,
|
|
914
|
-
startedAt: toIsoTime(Date.now()),
|
|
915
|
-
listenUrl: buildBuiltinClaudeProxyListenUrl(settings)
|
|
916
|
-
});
|
|
917
|
-
});
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
async function startBuiltinClaudeProxyRuntime(payload = {}) {
|
|
922
|
-
if (runtime) {
|
|
923
|
-
return {
|
|
924
|
-
error: 'Claude 兼容代理已在运行',
|
|
925
|
-
runtime: {
|
|
926
|
-
listenUrl: runtime.listenUrl,
|
|
927
|
-
upstreamProvider: runtime.upstream.providerName
|
|
928
|
-
}
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const saveResult = saveBuiltinClaudeProxySettings(payload);
|
|
933
|
-
if (saveResult.error) {
|
|
934
|
-
return { error: saveResult.error };
|
|
935
|
-
}
|
|
936
|
-
const settings = saveResult.settings;
|
|
937
|
-
const upstream = resolveBuiltinClaudeProxyUpstream(settings);
|
|
938
|
-
if (upstream.error) {
|
|
939
|
-
return { error: upstream.error };
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
try {
|
|
943
|
-
runtime = await createBuiltinClaudeProxyServer(settings, upstream);
|
|
944
|
-
return {
|
|
945
|
-
success: true,
|
|
946
|
-
running: true,
|
|
947
|
-
listenUrl: runtime.listenUrl,
|
|
948
|
-
upstreamProvider: upstream.providerName,
|
|
949
|
-
mode: 'anthropic-to-responses',
|
|
950
|
-
settings
|
|
951
|
-
};
|
|
952
|
-
} catch (e) {
|
|
953
|
-
return { error: `启动 Claude 兼容代理失败: ${e.message}` };
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
async function stopBuiltinClaudeProxyRuntime() {
|
|
958
|
-
if (!runtime) {
|
|
959
|
-
return { success: true, running: false };
|
|
960
|
-
}
|
|
961
|
-
const currentRuntime = runtime;
|
|
962
|
-
runtime = null;
|
|
963
|
-
|
|
964
|
-
await new Promise((resolve) => {
|
|
965
|
-
let settled = false;
|
|
966
|
-
const finish = () => {
|
|
967
|
-
if (settled) return;
|
|
968
|
-
settled = true;
|
|
969
|
-
resolve();
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
currentRuntime.server.close(() => finish());
|
|
973
|
-
setTimeout(() => finish(), 1000);
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
for (const socket of currentRuntime.connections) {
|
|
977
|
-
try { socket.destroy(); } catch (_) {}
|
|
978
|
-
}
|
|
979
|
-
currentRuntime.connections.clear();
|
|
980
|
-
|
|
981
|
-
return {
|
|
982
|
-
success: true,
|
|
983
|
-
running: false
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function getBuiltinClaudeProxyStatus() {
|
|
988
|
-
const settings = readBuiltinClaudeProxySettings();
|
|
989
|
-
return {
|
|
990
|
-
running: !!runtime,
|
|
991
|
-
settings,
|
|
992
|
-
runtime: runtime
|
|
993
|
-
? {
|
|
994
|
-
startedAt: runtime.startedAt,
|
|
995
|
-
listenUrl: runtime.listenUrl,
|
|
996
|
-
upstreamProvider: runtime.upstream.providerName,
|
|
997
|
-
upstreamBaseUrl: runtime.upstream.baseUrl,
|
|
998
|
-
mode: 'anthropic-to-responses'
|
|
999
|
-
}
|
|
1000
|
-
: null
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
return {
|
|
1005
|
-
normalizeBuiltinClaudeProxySettings,
|
|
1006
|
-
readBuiltinClaudeProxySettings,
|
|
1007
|
-
saveBuiltinClaudeProxySettings,
|
|
1008
|
-
buildBuiltinClaudeProxyListenUrl,
|
|
1009
|
-
resolveBuiltinClaudeProxyUpstream,
|
|
1010
|
-
startBuiltinClaudeProxyRuntime,
|
|
1011
|
-
stopBuiltinClaudeProxyRuntime,
|
|
1012
|
-
getBuiltinClaudeProxyStatus
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
module.exports = {
|
|
1017
|
-
createBuiltinClaudeProxyRuntimeController,
|
|
1018
|
-
buildBuiltinClaudeResponsesRequest,
|
|
1019
|
-
buildAnthropicMessageFromResponses,
|
|
1020
|
-
buildAnthropicStreamEvents,
|
|
1021
|
-
buildAnthropicModelsPayload
|
|
1022
|
-
};
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
|
|
5
|
+
const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
|
|
6
|
+
const {
|
|
7
|
+
extractModelNames,
|
|
8
|
+
extractModelResponseText,
|
|
9
|
+
normalizeWireApi
|
|
10
|
+
} = require('../lib/cli-models-utils');
|
|
11
|
+
const { toIsoTime } = require('../lib/cli-session-utils');
|
|
12
|
+
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readNonNegativeInteger(value) {
|
|
18
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
19
|
+
return Math.floor(value);
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'string' && value.trim()) {
|
|
22
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
23
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatHostForUrl(host) {
|
|
29
|
+
const value = typeof host === 'string' ? host.trim() : '';
|
|
30
|
+
if (!value) return '';
|
|
31
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (value.includes(':')) {
|
|
35
|
+
return `[${value}]`;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeAnthropicContentBlocks(content) {
|
|
41
|
+
if (typeof content === 'string') {
|
|
42
|
+
return content.trim() ? [{ type: 'text', text: content }] : [];
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(content)) {
|
|
45
|
+
return content.flatMap((item) => normalizeAnthropicContentBlocks(item));
|
|
46
|
+
}
|
|
47
|
+
if (content && typeof content === 'object') {
|
|
48
|
+
if (typeof content.type === 'string') {
|
|
49
|
+
return [content];
|
|
50
|
+
}
|
|
51
|
+
if (typeof content.text === 'string') {
|
|
52
|
+
return [{ type: 'text', text: content.text }];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function collectAnthropicTextContent(content) {
|
|
59
|
+
const pieces = [];
|
|
60
|
+
for (const block of normalizeAnthropicContentBlocks(content)) {
|
|
61
|
+
if (block && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
62
|
+
pieces.push(block.text.trim());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return pieces.join('\n\n').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeJsonStringify(value) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.stringify(value);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return JSON.stringify(String(value));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stringifyAnthropicToolResultContent(content) {
|
|
77
|
+
if (typeof content === 'string') {
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
const text = collectAnthropicTextContent(content);
|
|
81
|
+
if (text) {
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
return safeJsonStringify(content);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendAnthropicMessageToResponsesInput(target, message) {
|
|
88
|
+
if (!message || typeof message !== 'object') return;
|
|
89
|
+
const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
90
|
+
const role = roleRaw === 'assistant' ? 'assistant' : 'user';
|
|
91
|
+
const textType = role === 'assistant' ? 'output_text' : 'input_text';
|
|
92
|
+
let buffered = [];
|
|
93
|
+
|
|
94
|
+
const flushBuffered = () => {
|
|
95
|
+
if (!buffered.length) return;
|
|
96
|
+
target.push({ role, content: buffered });
|
|
97
|
+
buffered = [];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const block of normalizeAnthropicContentBlocks(message.content)) {
|
|
101
|
+
if (!block || typeof block !== 'object') continue;
|
|
102
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
103
|
+
buffered.push({ type: textType, text: block.text });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (block.type === 'tool_use' && typeof block.name === 'string' && block.name.trim()) {
|
|
107
|
+
flushBuffered();
|
|
108
|
+
target.push({
|
|
109
|
+
type: 'function_call',
|
|
110
|
+
call_id: typeof block.id === 'string' && block.id.trim()
|
|
111
|
+
? block.id.trim()
|
|
112
|
+
: `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
113
|
+
name: block.name.trim(),
|
|
114
|
+
arguments: safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {})
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
|
|
119
|
+
flushBuffered();
|
|
120
|
+
target.push({
|
|
121
|
+
type: 'function_call_output',
|
|
122
|
+
call_id: block.tool_use_id.trim(),
|
|
123
|
+
output: stringifyAnthropicToolResultContent(block.content)
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
buffered.push({
|
|
128
|
+
type: textType,
|
|
129
|
+
text: `[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
flushBuffered();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mapAnthropicToolChoiceToResponses(toolChoice) {
|
|
137
|
+
if (!toolChoice) return undefined;
|
|
138
|
+
if (typeof toolChoice === 'string') {
|
|
139
|
+
if (toolChoice === 'auto') return 'auto';
|
|
140
|
+
if (toolChoice === 'any') return 'required';
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (!toolChoice || typeof toolChoice !== 'object') return undefined;
|
|
144
|
+
const type = typeof toolChoice.type === 'string' ? toolChoice.type.trim().toLowerCase() : '';
|
|
145
|
+
if (type === 'auto') return 'auto';
|
|
146
|
+
if (type === 'any') return 'required';
|
|
147
|
+
if (type === 'tool' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
|
|
148
|
+
return {
|
|
149
|
+
type: 'function',
|
|
150
|
+
name: toolChoice.name.trim()
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildBuiltinClaudeResponsesRequest(payload = {}) {
|
|
157
|
+
const model = typeof payload.model === 'string' ? payload.model.trim() : '';
|
|
158
|
+
if (!model) {
|
|
159
|
+
throw new Error('Anthropic messages 请求缺少 model');
|
|
160
|
+
}
|
|
161
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
162
|
+
if (!messages.length) {
|
|
163
|
+
throw new Error('Anthropic messages 请求缺少 messages');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const maxTokens = parseInt(String(payload.max_tokens), 10);
|
|
167
|
+
const requestBody = {
|
|
168
|
+
model,
|
|
169
|
+
input: [],
|
|
170
|
+
max_output_tokens: Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : 1024
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const instructions = collectAnthropicTextContent(payload.system);
|
|
174
|
+
if (instructions) {
|
|
175
|
+
requestBody.instructions = instructions;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const message of messages) {
|
|
179
|
+
appendAnthropicMessageToResponsesInput(requestBody.input, message);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Number.isFinite(payload.temperature)) {
|
|
183
|
+
requestBody.temperature = Number(payload.temperature);
|
|
184
|
+
}
|
|
185
|
+
if (Number.isFinite(payload.top_p)) {
|
|
186
|
+
requestBody.top_p = Number(payload.top_p);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
|
|
189
|
+
requestBody.stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
|
|
190
|
+
}
|
|
191
|
+
if (isPlainObject(payload.metadata)) {
|
|
192
|
+
requestBody.metadata = payload.metadata;
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(payload.tools) && payload.tools.length) {
|
|
195
|
+
requestBody.tools = payload.tools
|
|
196
|
+
.map((tool) => {
|
|
197
|
+
if (!tool || typeof tool !== 'object') return null;
|
|
198
|
+
const name = typeof tool.name === 'string' ? tool.name.trim() : '';
|
|
199
|
+
if (!name) return null;
|
|
200
|
+
return {
|
|
201
|
+
type: 'function',
|
|
202
|
+
name,
|
|
203
|
+
description: typeof tool.description === 'string' ? tool.description : '',
|
|
204
|
+
parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
.filter(Boolean);
|
|
208
|
+
if (!requestBody.tools.length) {
|
|
209
|
+
delete requestBody.tools;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const toolChoice = mapAnthropicToolChoiceToResponses(payload.tool_choice);
|
|
214
|
+
if (toolChoice !== undefined) {
|
|
215
|
+
requestBody.tool_choice = toolChoice;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return requestBody;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseJsonObjectLoose(value) {
|
|
222
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(value);
|
|
230
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readResponsesUsageValue(value) {
|
|
237
|
+
const parsed = readNonNegativeInteger(value);
|
|
238
|
+
return parsed > 0 ? parsed : 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildAnthropicUsageFromResponses(payload) {
|
|
242
|
+
const usage = payload && payload.usage && typeof payload.usage === 'object' ? payload.usage : {};
|
|
243
|
+
return {
|
|
244
|
+
input_tokens: readResponsesUsageValue(usage.input_tokens),
|
|
245
|
+
output_tokens: readResponsesUsageValue(usage.output_tokens)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function collectAnthropicContentFromResponsesOutput(payload) {
|
|
250
|
+
const content = [];
|
|
251
|
+
const output = Array.isArray(payload && payload.output) ? payload.output : [];
|
|
252
|
+
for (const item of output) {
|
|
253
|
+
if (!item || typeof item !== 'object') continue;
|
|
254
|
+
if (item.type === 'function_call') {
|
|
255
|
+
content.push({
|
|
256
|
+
type: 'tool_use',
|
|
257
|
+
id: typeof item.call_id === 'string' && item.call_id.trim()
|
|
258
|
+
? item.call_id.trim()
|
|
259
|
+
: (typeof item.id === 'string' && item.id.trim()
|
|
260
|
+
? item.id.trim()
|
|
261
|
+
: `toolu_${crypto.randomBytes(8).toString('hex')}`),
|
|
262
|
+
name: typeof item.name === 'string' ? item.name : '',
|
|
263
|
+
input: parseJsonObjectLoose(item.arguments)
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
268
|
+
for (const block of item.content) {
|
|
269
|
+
if (!block || typeof block !== 'object') continue;
|
|
270
|
+
if ((block.type === 'output_text' || block.type === 'text' || block.type === 'input_text')
|
|
271
|
+
&& typeof block.text === 'string' && block.text) {
|
|
272
|
+
content.push({ type: 'text', text: block.text });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!content.length) {
|
|
278
|
+
const fallbackText = extractModelResponseText(payload);
|
|
279
|
+
if (fallbackText) {
|
|
280
|
+
content.push({ type: 'text', text: fallbackText });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return content;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildAnthropicStopReasonFromResponses(payload, content) {
|
|
287
|
+
if (Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')) {
|
|
288
|
+
return 'tool_use';
|
|
289
|
+
}
|
|
290
|
+
const incompleteReason = payload && payload.incomplete_details && typeof payload.incomplete_details.reason === 'string'
|
|
291
|
+
? payload.incomplete_details.reason
|
|
292
|
+
: '';
|
|
293
|
+
if (incompleteReason === 'max_output_tokens') {
|
|
294
|
+
return 'max_tokens';
|
|
295
|
+
}
|
|
296
|
+
return 'end_turn';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildAnthropicMessageFromResponses(payload, requestPayload = {}) {
|
|
300
|
+
const content = collectAnthropicContentFromResponsesOutput(payload);
|
|
301
|
+
const usage = buildAnthropicUsageFromResponses(payload);
|
|
302
|
+
return {
|
|
303
|
+
id: typeof payload.id === 'string' && payload.id.trim()
|
|
304
|
+
? payload.id.trim()
|
|
305
|
+
: `msg_${crypto.randomBytes(8).toString('hex')}`,
|
|
306
|
+
type: 'message',
|
|
307
|
+
role: 'assistant',
|
|
308
|
+
model: typeof payload.model === 'string' && payload.model.trim()
|
|
309
|
+
? payload.model.trim()
|
|
310
|
+
: (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
|
|
311
|
+
content,
|
|
312
|
+
stop_reason: buildAnthropicStopReasonFromResponses(payload, content),
|
|
313
|
+
stop_sequence: null,
|
|
314
|
+
usage
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildAnthropicStreamEvents(message) {
|
|
319
|
+
const usage = message && message.usage && typeof message.usage === 'object' ? message.usage : {};
|
|
320
|
+
const startUsage = {
|
|
321
|
+
input_tokens: readResponsesUsageValue(usage.input_tokens),
|
|
322
|
+
output_tokens: 0
|
|
323
|
+
};
|
|
324
|
+
const events = [{
|
|
325
|
+
event: 'message_start',
|
|
326
|
+
data: {
|
|
327
|
+
type: 'message_start',
|
|
328
|
+
message: {
|
|
329
|
+
...message,
|
|
330
|
+
content: [],
|
|
331
|
+
stop_reason: null,
|
|
332
|
+
stop_sequence: null,
|
|
333
|
+
usage: startUsage
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}];
|
|
337
|
+
|
|
338
|
+
const blocks = Array.isArray(message && message.content) ? message.content : [];
|
|
339
|
+
blocks.forEach((block, index) => {
|
|
340
|
+
if (!block || typeof block !== 'object') return;
|
|
341
|
+
if (block.type === 'text') {
|
|
342
|
+
events.push({
|
|
343
|
+
event: 'content_block_start',
|
|
344
|
+
data: {
|
|
345
|
+
type: 'content_block_start',
|
|
346
|
+
index,
|
|
347
|
+
content_block: { type: 'text', text: '' }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
if (typeof block.text === 'string' && block.text) {
|
|
351
|
+
events.push({
|
|
352
|
+
event: 'content_block_delta',
|
|
353
|
+
data: {
|
|
354
|
+
type: 'content_block_delta',
|
|
355
|
+
index,
|
|
356
|
+
delta: { type: 'text_delta', text: block.text }
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (block.type === 'tool_use') {
|
|
364
|
+
events.push({
|
|
365
|
+
event: 'content_block_start',
|
|
366
|
+
data: {
|
|
367
|
+
type: 'content_block_start',
|
|
368
|
+
index,
|
|
369
|
+
content_block: {
|
|
370
|
+
type: 'tool_use',
|
|
371
|
+
id: block.id,
|
|
372
|
+
name: block.name,
|
|
373
|
+
input: {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
const partialJson = safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {});
|
|
378
|
+
if (partialJson && partialJson !== '{}') {
|
|
379
|
+
events.push({
|
|
380
|
+
event: 'content_block_delta',
|
|
381
|
+
data: {
|
|
382
|
+
type: 'content_block_delta',
|
|
383
|
+
index,
|
|
384
|
+
delta: { type: 'input_json_delta', partial_json: partialJson }
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
events.push({
|
|
393
|
+
event: 'message_delta',
|
|
394
|
+
data: {
|
|
395
|
+
type: 'message_delta',
|
|
396
|
+
delta: {
|
|
397
|
+
stop_reason: message && message.stop_reason ? message.stop_reason : 'end_turn',
|
|
398
|
+
stop_sequence: message && Object.prototype.hasOwnProperty.call(message, 'stop_sequence')
|
|
399
|
+
? message.stop_sequence
|
|
400
|
+
: null
|
|
401
|
+
},
|
|
402
|
+
usage: {
|
|
403
|
+
output_tokens: readResponsesUsageValue(usage.output_tokens)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
events.push({ event: 'message_stop', data: { type: 'message_stop' } });
|
|
408
|
+
return events;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function buildAnthropicModelsPayload(upstreamPayload) {
|
|
412
|
+
const ids = extractModelNames(upstreamPayload);
|
|
413
|
+
return {
|
|
414
|
+
data: ids.map((id) => ({
|
|
415
|
+
type: 'model',
|
|
416
|
+
id,
|
|
417
|
+
display_name: id,
|
|
418
|
+
created_at: '1970-01-01T00:00:00Z'
|
|
419
|
+
})),
|
|
420
|
+
first_id: ids[0] || null,
|
|
421
|
+
last_id: ids.length ? ids[ids.length - 1] : null,
|
|
422
|
+
has_more: false
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createBuiltinClaudeProxyRuntimeController(deps = {}) {
|
|
427
|
+
const {
|
|
428
|
+
BUILTIN_CLAUDE_PROXY_SETTINGS_FILE,
|
|
429
|
+
DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
|
|
430
|
+
BUILTIN_PROXY_PROVIDER_NAME,
|
|
431
|
+
MAX_API_BODY_SIZE,
|
|
432
|
+
HTTP_KEEP_ALIVE_AGENT,
|
|
433
|
+
HTTPS_KEEP_ALIVE_AGENT,
|
|
434
|
+
readConfigOrVirtualDefault,
|
|
435
|
+
resolveBuiltinProxyProviderName,
|
|
436
|
+
resolveAuthTokenFromCurrentProfile
|
|
437
|
+
} = deps;
|
|
438
|
+
|
|
439
|
+
if (!BUILTIN_CLAUDE_PROXY_SETTINGS_FILE) {
|
|
440
|
+
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 BUILTIN_CLAUDE_PROXY_SETTINGS_FILE');
|
|
441
|
+
}
|
|
442
|
+
if (!DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS !== 'object') {
|
|
443
|
+
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS');
|
|
444
|
+
}
|
|
445
|
+
if (typeof readConfigOrVirtualDefault !== 'function') {
|
|
446
|
+
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 readConfigOrVirtualDefault');
|
|
447
|
+
}
|
|
448
|
+
if (typeof resolveBuiltinProxyProviderName !== 'function') {
|
|
449
|
+
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveBuiltinProxyProviderName');
|
|
450
|
+
}
|
|
451
|
+
if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
|
|
452
|
+
throw new Error('createBuiltinClaudeProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let runtime = null;
|
|
456
|
+
|
|
457
|
+
function normalizeBuiltinClaudeProxySettings(raw) {
|
|
458
|
+
const merged = {
|
|
459
|
+
...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
|
|
460
|
+
...(isPlainObject(raw) ? raw : {})
|
|
461
|
+
};
|
|
462
|
+
const host = typeof merged.host === 'string' ? merged.host.trim() : '';
|
|
463
|
+
const port = parseInt(String(merged.port), 10);
|
|
464
|
+
const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
|
|
465
|
+
const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
|
|
466
|
+
const timeoutMs = parseInt(String(merged.timeoutMs), 10);
|
|
467
|
+
const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' || authSourceRaw === 'request'
|
|
468
|
+
? authSourceRaw
|
|
469
|
+
: 'provider';
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
enabled: merged.enabled !== false,
|
|
473
|
+
host: host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
|
|
474
|
+
port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.port,
|
|
475
|
+
provider,
|
|
476
|
+
authSource,
|
|
477
|
+
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
|
|
478
|
+
? timeoutMs
|
|
479
|
+
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function readBuiltinClaudeProxySettings() {
|
|
484
|
+
const parsed = readJsonFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, null);
|
|
485
|
+
return normalizeBuiltinClaudeProxySettings(parsed);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function saveBuiltinClaudeProxySettings(payload = {}, options = {}) {
|
|
489
|
+
const current = readBuiltinClaudeProxySettings();
|
|
490
|
+
const merged = normalizeBuiltinClaudeProxySettings({
|
|
491
|
+
...current,
|
|
492
|
+
...(isPlainObject(payload) ? payload : {})
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (!merged.host) {
|
|
496
|
+
return { error: 'Claude 兼容代理 host 不能为空' };
|
|
497
|
+
}
|
|
498
|
+
if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
|
|
499
|
+
return { error: 'Claude 兼容代理端口无效(1-65535)' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const { config } = readConfigOrVirtualDefault();
|
|
503
|
+
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
504
|
+
const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
505
|
+
const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
|
|
506
|
+
const normalized = {
|
|
507
|
+
...merged,
|
|
508
|
+
provider: finalProvider
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
if (!options.skipWrite) {
|
|
512
|
+
writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, normalized);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
settings: normalized
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function buildBuiltinClaudeProxyListenUrl(settings) {
|
|
522
|
+
const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host);
|
|
523
|
+
return `http://${host}:${settings.port}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveBuiltinClaudeProxyUpstream(settings) {
|
|
527
|
+
const { config } = readConfigOrVirtualDefault();
|
|
528
|
+
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
529
|
+
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
530
|
+
const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
|
|
531
|
+
if (!providerName) {
|
|
532
|
+
return { error: '未找到可用的上游 provider,请先添加 responses provider' };
|
|
533
|
+
}
|
|
534
|
+
if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
|
|
535
|
+
return { error: `Claude 兼容代理的上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
|
|
536
|
+
}
|
|
537
|
+
const provider = providers[providerName];
|
|
538
|
+
if (!provider || !isPlainObject(provider)) {
|
|
539
|
+
return { error: `上游 provider 不存在: ${providerName}` };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const wireApi = normalizeWireApi(provider.wire_api);
|
|
543
|
+
if (wireApi !== 'responses') {
|
|
544
|
+
return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
548
|
+
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
|
|
549
|
+
return { error: `上游 provider base_url 无效: ${providerName}` };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let token = '';
|
|
553
|
+
if (settings.authSource === 'profile') {
|
|
554
|
+
token = resolveAuthTokenFromCurrentProfile();
|
|
555
|
+
} else if (settings.authSource === 'provider') {
|
|
556
|
+
token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
|
|
557
|
+
if (!token) {
|
|
558
|
+
token = resolveAuthTokenFromCurrentProfile();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let authHeader = '';
|
|
563
|
+
if (token) {
|
|
564
|
+
authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
providerName,
|
|
569
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
570
|
+
authHeader
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream) {
|
|
575
|
+
if (settings && settings.authSource === 'request') {
|
|
576
|
+
const apiKey = typeof req.headers['x-api-key'] === 'string'
|
|
577
|
+
? req.headers['x-api-key'].trim()
|
|
578
|
+
: '';
|
|
579
|
+
if (!apiKey) {
|
|
580
|
+
return { error: '缺少 x-api-key,无法转发到上游 responses provider', statusCode: 401 };
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
authHeader: /^bearer\s+/i.test(apiKey) ? apiKey : `Bearer ${apiKey}`
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return { authHeader: upstream.authHeader || '' };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function readJsonRequestBody(req, options = {}) {
|
|
590
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
591
|
+
? Math.floor(options.maxBytes)
|
|
592
|
+
: MAX_API_BODY_SIZE;
|
|
593
|
+
return new Promise((resolve, reject) => {
|
|
594
|
+
const chunks = [];
|
|
595
|
+
let total = 0;
|
|
596
|
+
req.on('data', (chunk) => {
|
|
597
|
+
total += chunk.length;
|
|
598
|
+
if (total > maxBytes) {
|
|
599
|
+
reject(new Error(`request body too large (${maxBytes} bytes max)`));
|
|
600
|
+
try { req.destroy(); } catch (_) {}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
chunks.push(chunk);
|
|
604
|
+
});
|
|
605
|
+
req.on('error', reject);
|
|
606
|
+
req.on('end', () => {
|
|
607
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
608
|
+
if (!raw) {
|
|
609
|
+
resolve({});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
resolve(JSON.parse(raw));
|
|
614
|
+
} catch (e) {
|
|
615
|
+
reject(new Error(`invalid JSON body: ${e.message}`));
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function extractProxyErrorMessage(payload, fallback = '') {
|
|
622
|
+
if (!payload || typeof payload !== 'object') {
|
|
623
|
+
return fallback || 'upstream request failed';
|
|
624
|
+
}
|
|
625
|
+
if (payload.error && typeof payload.error === 'object') {
|
|
626
|
+
if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
|
|
627
|
+
return payload.error.message.trim();
|
|
628
|
+
}
|
|
629
|
+
if (typeof payload.error.error === 'string' && payload.error.error.trim()) {
|
|
630
|
+
return payload.error.error.trim();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (typeof payload.message === 'string' && payload.message.trim()) {
|
|
634
|
+
return payload.message.trim();
|
|
635
|
+
}
|
|
636
|
+
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
637
|
+
return payload.error.trim();
|
|
638
|
+
}
|
|
639
|
+
return fallback || 'upstream request failed';
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function writeAnthropicProxyError(res, statusCode, message, type = 'api_error') {
|
|
643
|
+
const body = JSON.stringify({
|
|
644
|
+
type: 'error',
|
|
645
|
+
error: {
|
|
646
|
+
type,
|
|
647
|
+
message: typeof message === 'string' && message.trim() ? message.trim() : 'request failed'
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
res.writeHead(statusCode, {
|
|
651
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
652
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
653
|
+
});
|
|
654
|
+
res.end(body, 'utf-8');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function requestBuiltinClaudeProxyUpstream(upstream, requestOptions = {}) {
|
|
658
|
+
const pathSuffix = typeof requestOptions.pathSuffix === 'string' ? requestOptions.pathSuffix : '';
|
|
659
|
+
const targetBase = joinApiUrl(upstream.baseUrl, pathSuffix);
|
|
660
|
+
if (!targetBase) {
|
|
661
|
+
return Promise.reject(new Error('failed to build upstream URL'));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let targetUrl;
|
|
665
|
+
try {
|
|
666
|
+
targetUrl = new URL(targetBase);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return Promise.reject(new Error(`invalid upstream URL: ${e.message}`));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const bodyText = requestOptions.body === undefined ? '' : JSON.stringify(requestOptions.body);
|
|
672
|
+
const headers = {
|
|
673
|
+
Accept: 'application/json',
|
|
674
|
+
...(isPlainObject(requestOptions.headers) ? requestOptions.headers : {})
|
|
675
|
+
};
|
|
676
|
+
if (bodyText) {
|
|
677
|
+
headers['Content-Type'] = 'application/json';
|
|
678
|
+
headers['Content-Length'] = Buffer.byteLength(bodyText);
|
|
679
|
+
}
|
|
680
|
+
if (requestOptions.authHeader) {
|
|
681
|
+
headers.authorization = requestOptions.authHeader;
|
|
682
|
+
}
|
|
683
|
+
headers['x-codexmate-claude-proxy'] = '1';
|
|
684
|
+
|
|
685
|
+
const transport = targetUrl.protocol === 'https:' ? https : http;
|
|
686
|
+
const timeoutMs = Number.isFinite(requestOptions.timeoutMs) && requestOptions.timeoutMs > 0
|
|
687
|
+
? requestOptions.timeoutMs
|
|
688
|
+
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs;
|
|
689
|
+
|
|
690
|
+
return new Promise((resolve, reject) => {
|
|
691
|
+
const upstreamReq = transport.request({
|
|
692
|
+
protocol: targetUrl.protocol,
|
|
693
|
+
hostname: targetUrl.hostname,
|
|
694
|
+
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
|
695
|
+
method: requestOptions.method || 'POST',
|
|
696
|
+
path: `${targetUrl.pathname}${targetUrl.search}`,
|
|
697
|
+
headers,
|
|
698
|
+
agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
|
|
699
|
+
}, (upstreamRes) => {
|
|
700
|
+
const chunks = [];
|
|
701
|
+
let total = 0;
|
|
702
|
+
upstreamRes.on('data', (chunk) => {
|
|
703
|
+
total += chunk.length;
|
|
704
|
+
if (total > MAX_API_BODY_SIZE) {
|
|
705
|
+
upstreamReq.destroy(new Error(`upstream body too large (${MAX_API_BODY_SIZE} bytes max)`));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
chunks.push(chunk);
|
|
709
|
+
});
|
|
710
|
+
upstreamRes.on('error', reject);
|
|
711
|
+
upstreamRes.on('end', () => {
|
|
712
|
+
const rawBody = Buffer.concat(chunks).toString('utf-8');
|
|
713
|
+
let payload = null;
|
|
714
|
+
if (rawBody.trim()) {
|
|
715
|
+
try {
|
|
716
|
+
payload = JSON.parse(rawBody);
|
|
717
|
+
} catch (_) {
|
|
718
|
+
payload = null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
resolve({
|
|
722
|
+
statusCode: upstreamRes.statusCode || 502,
|
|
723
|
+
headers: upstreamRes.headers,
|
|
724
|
+
rawBody,
|
|
725
|
+
payload
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
upstreamReq.setTimeout(timeoutMs, () => {
|
|
731
|
+
upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
|
|
732
|
+
});
|
|
733
|
+
upstreamReq.on('error', reject);
|
|
734
|
+
if (bodyText) {
|
|
735
|
+
upstreamReq.write(bodyText, 'utf-8');
|
|
736
|
+
}
|
|
737
|
+
upstreamReq.end();
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function writeAnthropicStreamEvents(res, message) {
|
|
742
|
+
const events = buildAnthropicStreamEvents(message);
|
|
743
|
+
res.writeHead(200, {
|
|
744
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
745
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
746
|
+
Connection: 'keep-alive',
|
|
747
|
+
'X-Accel-Buffering': 'no'
|
|
748
|
+
});
|
|
749
|
+
for (const event of events) {
|
|
750
|
+
if (event && event.event) {
|
|
751
|
+
res.write(`event: ${event.event}\n`);
|
|
752
|
+
}
|
|
753
|
+
res.write(`data: ${JSON.stringify(event && event.data ? event.data : {})}\n\n`);
|
|
754
|
+
}
|
|
755
|
+
res.end();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function handleBuiltinClaudeProxyRequest(req, res, settings, upstream) {
|
|
759
|
+
let parsedIncoming;
|
|
760
|
+
try {
|
|
761
|
+
parsedIncoming = new URL(req.url || '/', 'http://localhost');
|
|
762
|
+
} catch (e) {
|
|
763
|
+
writeAnthropicProxyError(res, 400, 'invalid request path', 'invalid_request_error');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const incomingPath = parsedIncoming.pathname || '/';
|
|
768
|
+
if (incomingPath === '/health' || incomingPath === '/status') {
|
|
769
|
+
const body = JSON.stringify({
|
|
770
|
+
ok: true,
|
|
771
|
+
upstreamProvider: upstream.providerName,
|
|
772
|
+
upstreamBaseUrl: upstream.baseUrl,
|
|
773
|
+
mode: 'anthropic-to-responses'
|
|
774
|
+
});
|
|
775
|
+
res.writeHead(200, {
|
|
776
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
777
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
778
|
+
});
|
|
779
|
+
res.end(body, 'utf-8');
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const authResult = buildBuiltinClaudeProxyRequestAuthHeader(req, settings, upstream);
|
|
784
|
+
if (authResult.error) {
|
|
785
|
+
writeAnthropicProxyError(res, authResult.statusCode || 401, authResult.error, 'authentication_error');
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (incomingPath === '/v1/models') {
|
|
790
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') {
|
|
791
|
+
res.writeHead(405, { Allow: 'GET' });
|
|
792
|
+
res.end();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
|
|
796
|
+
method: 'GET',
|
|
797
|
+
pathSuffix: 'models',
|
|
798
|
+
authHeader: authResult.authHeader,
|
|
799
|
+
timeoutMs: settings.timeoutMs
|
|
800
|
+
});
|
|
801
|
+
if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
|
|
802
|
+
writeAnthropicProxyError(
|
|
803
|
+
res,
|
|
804
|
+
upstreamResponse.statusCode,
|
|
805
|
+
extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
|
|
806
|
+
'api_error'
|
|
807
|
+
);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const body = JSON.stringify(buildAnthropicModelsPayload(upstreamResponse.payload));
|
|
811
|
+
res.writeHead(200, {
|
|
812
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
813
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
814
|
+
});
|
|
815
|
+
res.end(body, 'utf-8');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (incomingPath !== '/v1/messages') {
|
|
820
|
+
writeAnthropicProxyError(res, 404, 'Claude 兼容代理仅支持 /v1/messages 与 /v1/models', 'not_found_error');
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if ((req.method || 'POST').toUpperCase() !== 'POST') {
|
|
825
|
+
res.writeHead(405, { Allow: 'POST' });
|
|
826
|
+
res.end();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const payload = await readJsonRequestBody(req);
|
|
831
|
+
const upstreamRequestBody = buildBuiltinClaudeResponsesRequest(payload);
|
|
832
|
+
const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
pathSuffix: 'responses',
|
|
835
|
+
body: upstreamRequestBody,
|
|
836
|
+
authHeader: authResult.authHeader,
|
|
837
|
+
timeoutMs: settings.timeoutMs
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
|
|
841
|
+
writeAnthropicProxyError(
|
|
842
|
+
res,
|
|
843
|
+
upstreamResponse.statusCode,
|
|
844
|
+
extractProxyErrorMessage(upstreamResponse.payload, upstreamResponse.rawBody),
|
|
845
|
+
'api_error'
|
|
846
|
+
);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const anthropicMessage = buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload);
|
|
851
|
+
if (payload.stream === true) {
|
|
852
|
+
writeAnthropicStreamEvents(res, anthropicMessage);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const body = JSON.stringify(anthropicMessage);
|
|
857
|
+
res.writeHead(200, {
|
|
858
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
859
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
860
|
+
});
|
|
861
|
+
res.end(body, 'utf-8');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function createBuiltinClaudeProxyServer(settings, upstream) {
|
|
865
|
+
const connections = new Set();
|
|
866
|
+
const server = http.createServer((req, res) => {
|
|
867
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
868
|
+
const isLoopback = !remoteAddr
|
|
869
|
+
|| remoteAddr === '127.0.0.1'
|
|
870
|
+
|| remoteAddr === '::1'
|
|
871
|
+
|| remoteAddr === '::ffff:127.0.0.1';
|
|
872
|
+
if (!isLoopback) {
|
|
873
|
+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
|
|
874
|
+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
|
|
875
|
+
: '';
|
|
876
|
+
if (!expected) {
|
|
877
|
+
writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
881
|
+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
882
|
+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
883
|
+
const actual = match && match[1]
|
|
884
|
+
? match[1].trim()
|
|
885
|
+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
886
|
+
if (!actual || actual !== expected) {
|
|
887
|
+
writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
|
|
892
|
+
if (res.headersSent) {
|
|
893
|
+
try { res.destroy(err); } catch (_) {}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
writeAnthropicProxyError(res, 502, `claude proxy request failed: ${err.message}`, 'api_error');
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
server.on('connection', (socket) => {
|
|
901
|
+
connections.add(socket);
|
|
902
|
+
socket.on('close', () => connections.delete(socket));
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return new Promise((resolve, reject) => {
|
|
906
|
+
server.once('error', reject);
|
|
907
|
+
server.listen(settings.port, settings.host, () => {
|
|
908
|
+
server.removeListener('error', reject);
|
|
909
|
+
resolve({
|
|
910
|
+
server,
|
|
911
|
+
connections,
|
|
912
|
+
settings,
|
|
913
|
+
upstream,
|
|
914
|
+
startedAt: toIsoTime(Date.now()),
|
|
915
|
+
listenUrl: buildBuiltinClaudeProxyListenUrl(settings)
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function startBuiltinClaudeProxyRuntime(payload = {}) {
|
|
922
|
+
if (runtime) {
|
|
923
|
+
return {
|
|
924
|
+
error: 'Claude 兼容代理已在运行',
|
|
925
|
+
runtime: {
|
|
926
|
+
listenUrl: runtime.listenUrl,
|
|
927
|
+
upstreamProvider: runtime.upstream.providerName
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const saveResult = saveBuiltinClaudeProxySettings(payload);
|
|
933
|
+
if (saveResult.error) {
|
|
934
|
+
return { error: saveResult.error };
|
|
935
|
+
}
|
|
936
|
+
const settings = saveResult.settings;
|
|
937
|
+
const upstream = resolveBuiltinClaudeProxyUpstream(settings);
|
|
938
|
+
if (upstream.error) {
|
|
939
|
+
return { error: upstream.error };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
runtime = await createBuiltinClaudeProxyServer(settings, upstream);
|
|
944
|
+
return {
|
|
945
|
+
success: true,
|
|
946
|
+
running: true,
|
|
947
|
+
listenUrl: runtime.listenUrl,
|
|
948
|
+
upstreamProvider: upstream.providerName,
|
|
949
|
+
mode: 'anthropic-to-responses',
|
|
950
|
+
settings
|
|
951
|
+
};
|
|
952
|
+
} catch (e) {
|
|
953
|
+
return { error: `启动 Claude 兼容代理失败: ${e.message}` };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function stopBuiltinClaudeProxyRuntime() {
|
|
958
|
+
if (!runtime) {
|
|
959
|
+
return { success: true, running: false };
|
|
960
|
+
}
|
|
961
|
+
const currentRuntime = runtime;
|
|
962
|
+
runtime = null;
|
|
963
|
+
|
|
964
|
+
await new Promise((resolve) => {
|
|
965
|
+
let settled = false;
|
|
966
|
+
const finish = () => {
|
|
967
|
+
if (settled) return;
|
|
968
|
+
settled = true;
|
|
969
|
+
resolve();
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
currentRuntime.server.close(() => finish());
|
|
973
|
+
setTimeout(() => finish(), 1000);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
for (const socket of currentRuntime.connections) {
|
|
977
|
+
try { socket.destroy(); } catch (_) {}
|
|
978
|
+
}
|
|
979
|
+
currentRuntime.connections.clear();
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
success: true,
|
|
983
|
+
running: false
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function getBuiltinClaudeProxyStatus() {
|
|
988
|
+
const settings = readBuiltinClaudeProxySettings();
|
|
989
|
+
return {
|
|
990
|
+
running: !!runtime,
|
|
991
|
+
settings,
|
|
992
|
+
runtime: runtime
|
|
993
|
+
? {
|
|
994
|
+
startedAt: runtime.startedAt,
|
|
995
|
+
listenUrl: runtime.listenUrl,
|
|
996
|
+
upstreamProvider: runtime.upstream.providerName,
|
|
997
|
+
upstreamBaseUrl: runtime.upstream.baseUrl,
|
|
998
|
+
mode: 'anthropic-to-responses'
|
|
999
|
+
}
|
|
1000
|
+
: null
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
normalizeBuiltinClaudeProxySettings,
|
|
1006
|
+
readBuiltinClaudeProxySettings,
|
|
1007
|
+
saveBuiltinClaudeProxySettings,
|
|
1008
|
+
buildBuiltinClaudeProxyListenUrl,
|
|
1009
|
+
resolveBuiltinClaudeProxyUpstream,
|
|
1010
|
+
startBuiltinClaudeProxyRuntime,
|
|
1011
|
+
stopBuiltinClaudeProxyRuntime,
|
|
1012
|
+
getBuiltinClaudeProxyStatus
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
module.exports = {
|
|
1017
|
+
createBuiltinClaudeProxyRuntimeController,
|
|
1018
|
+
buildBuiltinClaudeResponsesRequest,
|
|
1019
|
+
buildAnthropicMessageFromResponses,
|
|
1020
|
+
buildAnthropicStreamEvents,
|
|
1021
|
+
buildAnthropicModelsPayload
|
|
1022
|
+
};
|