@antseed/cli 0.1.132 → 0.1.133
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 +12 -1
- package/dist/cli/commands/buyer/channel-withdraw.d.ts +7 -0
- package/dist/cli/commands/buyer/channel-withdraw.d.ts.map +1 -0
- package/dist/cli/commands/buyer/channel-withdraw.js +178 -0
- package/dist/cli/commands/buyer/channel-withdraw.js.map +1 -0
- package/dist/cli/commands/buyer/channel-withdraw.test.d.ts +2 -0
- package/dist/cli/commands/buyer/channel-withdraw.test.d.ts.map +1 -0
- package/dist/cli/commands/buyer/channel-withdraw.test.js +45 -0
- package/dist/cli/commands/buyer/channel-withdraw.test.js.map +1 -0
- package/dist/cli/commands/buyer/channels.d.ts.map +1 -1
- package/dist/cli/commands/buyer/channels.js +60 -52
- package/dist/cli/commands/buyer/channels.js.map +1 -1
- package/dist/cli/commands/buyer/emissions.d.ts +3 -0
- package/dist/cli/commands/buyer/emissions.d.ts.map +1 -0
- package/dist/cli/commands/buyer/emissions.js +5 -0
- package/dist/cli/commands/buyer/emissions.js.map +1 -0
- package/dist/cli/commands/buyer/index.d.ts.map +1 -1
- package/dist/cli/commands/buyer/index.js +4 -0
- package/dist/cli/commands/buyer/index.js.map +1 -1
- package/dist/cli/commands/buyer/start.d.ts +1 -0
- package/dist/cli/commands/buyer/start.d.ts.map +1 -1
- package/dist/cli/commands/buyer/start.js +16 -0
- package/dist/cli/commands/buyer/start.js.map +1 -1
- package/dist/cli/commands/buyer/start.test.js +2 -0
- package/dist/cli/commands/buyer/start.test.js.map +1 -1
- package/dist/cli/commands/buyer/sweep.d.ts +3 -0
- package/dist/cli/commands/buyer/sweep.d.ts.map +1 -0
- package/dist/cli/commands/buyer/sweep.js +332 -0
- package/dist/cli/commands/buyer/sweep.js.map +1 -0
- package/dist/cli/commands/buyer/sweep.test.d.ts +2 -0
- package/dist/cli/commands/buyer/sweep.test.d.ts.map +1 -0
- package/dist/cli/commands/buyer/sweep.test.js +46 -0
- package/dist/cli/commands/buyer/sweep.test.js.map +1 -0
- package/dist/cli/commands/emissions.d.ts +10 -0
- package/dist/cli/commands/emissions.d.ts.map +1 -0
- package/dist/cli/commands/emissions.js +91 -0
- package/dist/cli/commands/emissions.js.map +1 -0
- package/dist/cli/commands/emissions.test.d.ts +2 -0
- package/dist/cli/commands/emissions.test.d.ts.map +1 -0
- package/dist/cli/commands/emissions.test.js +16 -0
- package/dist/cli/commands/emissions.test.js.map +1 -0
- package/dist/cli/commands/seller/emissions.d.ts.map +1 -1
- package/dist/cli/commands/seller/emissions.js +2 -77
- package/dist/cli/commands/seller/emissions.js.map +1 -1
- package/dist/cli/commands/seller/start.d.ts.map +1 -1
- package/dist/cli/commands/seller/start.js +5 -0
- package/dist/cli/commands/seller/start.js.map +1 -1
- package/dist/cli/commands/system-proxy/index.d.ts +3 -0
- package/dist/cli/commands/system-proxy/index.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/index.js +14 -0
- package/dist/cli/commands/system-proxy/index.js.map +1 -0
- package/dist/cli/commands/system-proxy/install-ca.d.ts +3 -0
- package/dist/cli/commands/system-proxy/install-ca.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/install-ca.js +43 -0
- package/dist/cli/commands/system-proxy/install-ca.js.map +1 -0
- package/dist/cli/commands/system-proxy/start.d.ts +3 -0
- package/dist/cli/commands/system-proxy/start.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/start.js +151 -0
- package/dist/cli/commands/system-proxy/start.js.map +1 -0
- package/dist/cli/commands/system-proxy/status.d.ts +3 -0
- package/dist/cli/commands/system-proxy/status.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/status.js +60 -0
- package/dist/cli/commands/system-proxy/status.js.map +1 -0
- package/dist/cli/commands/system-proxy/stop.d.ts +3 -0
- package/dist/cli/commands/system-proxy/stop.d.ts.map +1 -0
- package/dist/cli/commands/system-proxy/stop.js +71 -0
- package/dist/cli/commands/system-proxy/stop.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/payment-utils.d.ts +7 -1
- package/dist/cli/payment-utils.d.ts.map +1 -1
- package/dist/cli/payment-utils.js +20 -1
- package/dist/cli/payment-utils.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effective.d.ts +1 -0
- package/dist/config/effective.d.ts.map +1 -1
- package/dist/config/effective.js +18 -0
- package/dist/config/effective.js.map +1 -1
- package/dist/config/effective.test.js +10 -0
- package/dist/config/effective.test.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +9 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.test.js +31 -0
- package/dist/config/loader.test.js.map +1 -1
- package/dist/config/types.d.ts +20 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +17 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +2 -0
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +89 -16
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +190 -1
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/request-utils.d.ts +1 -1
- package/dist/proxy/request-utils.d.ts.map +1 -1
- package/dist/proxy/request-utils.js +9 -3
- package/dist/proxy/request-utils.js.map +1 -1
- package/dist/proxy/service-api-adapter.d.ts +2 -2
- package/dist/proxy/service-api-adapter.d.ts.map +1 -1
- package/dist/proxy/service-api-adapter.js +1 -1
- package/dist/proxy/service-api-adapter.js.map +1 -1
- package/dist/proxy/service-api-adapter.test.js +21 -12
- package/dist/proxy/service-api-adapter.test.js.map +1 -1
- package/dist/system-proxy/ca-manager.d.ts +21 -0
- package/dist/system-proxy/ca-manager.d.ts.map +1 -0
- package/dist/system-proxy/ca-manager.js +112 -0
- package/dist/system-proxy/ca-manager.js.map +1 -0
- package/dist/system-proxy/ca-manager.test.d.ts +2 -0
- package/dist/system-proxy/ca-manager.test.d.ts.map +1 -0
- package/dist/system-proxy/ca-manager.test.js +64 -0
- package/dist/system-proxy/ca-manager.test.js.map +1 -0
- package/dist/system-proxy/cert-cache.d.ts +16 -0
- package/dist/system-proxy/cert-cache.d.ts.map +1 -0
- package/dist/system-proxy/cert-cache.js +69 -0
- package/dist/system-proxy/cert-cache.js.map +1 -0
- package/dist/system-proxy/cert-cache.test.d.ts +2 -0
- package/dist/system-proxy/cert-cache.test.d.ts.map +1 -0
- package/dist/system-proxy/cert-cache.test.js +52 -0
- package/dist/system-proxy/cert-cache.test.js.map +1 -0
- package/dist/system-proxy/index.d.ts +11 -0
- package/dist/system-proxy/index.d.ts.map +1 -0
- package/dist/system-proxy/index.js +7 -0
- package/dist/system-proxy/index.js.map +1 -0
- package/dist/system-proxy/node-hook.d.ts +3 -0
- package/dist/system-proxy/node-hook.d.ts.map +1 -0
- package/dist/system-proxy/node-hook.js +27 -0
- package/dist/system-proxy/node-hook.js.map +1 -0
- package/dist/system-proxy/profiles.d.ts +13 -0
- package/dist/system-proxy/profiles.d.ts.map +1 -0
- package/dist/system-proxy/profiles.js +150 -0
- package/dist/system-proxy/profiles.js.map +1 -0
- package/dist/system-proxy/profiles.test.d.ts +2 -0
- package/dist/system-proxy/profiles.test.d.ts.map +1 -0
- package/dist/system-proxy/profiles.test.js +66 -0
- package/dist/system-proxy/profiles.test.js.map +1 -0
- package/dist/system-proxy/proxy-server.d.ts +98 -0
- package/dist/system-proxy/proxy-server.d.ts.map +1 -0
- package/dist/system-proxy/proxy-server.js +977 -0
- package/dist/system-proxy/proxy-server.js.map +1 -0
- package/dist/system-proxy/proxy-server.test.d.ts +2 -0
- package/dist/system-proxy/proxy-server.test.d.ts.map +1 -0
- package/dist/system-proxy/proxy-server.test.js +778 -0
- package/dist/system-proxy/proxy-server.test.js.map +1 -0
- package/dist/system-proxy/system-proxy.d.ts +25 -0
- package/dist/system-proxy/system-proxy.d.ts.map +1 -0
- package/dist/system-proxy/system-proxy.js +152 -0
- package/dist/system-proxy/system-proxy.js.map +1 -0
- package/dist/system-proxy/types.d.ts +69 -0
- package/dist/system-proxy/types.d.ts.map +1 -0
- package/dist/system-proxy/types.js +2 -0
- package/dist/system-proxy/types.js.map +1 -0
- package/package.json +7 -5
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as https from 'node:https';
|
|
3
|
+
import * as net from 'node:net';
|
|
4
|
+
import * as tls from 'node:tls';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
const HOP_BY_HOP = new Set([
|
|
7
|
+
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
|
8
|
+
'te', 'trailers', 'transfer-encoding', 'upgrade', 'proxy-connection',
|
|
9
|
+
]);
|
|
10
|
+
const ROUTED_SENSITIVE_HEADERS = new Set([
|
|
11
|
+
'authorization',
|
|
12
|
+
'cookie',
|
|
13
|
+
'x-csrf-token',
|
|
14
|
+
]);
|
|
15
|
+
function log(msg) {
|
|
16
|
+
process.stdout.write(`[system-proxy] ${msg}\n`);
|
|
17
|
+
}
|
|
18
|
+
export class SystemProxyServer {
|
|
19
|
+
config;
|
|
20
|
+
outerServer = null;
|
|
21
|
+
innerNetServer = null;
|
|
22
|
+
innerTlsServer = null;
|
|
23
|
+
sockets = new Set();
|
|
24
|
+
innerPort = 0;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
this.innerTlsServer = https.createServer({
|
|
30
|
+
maxHeaderSize: 256 * 1024,
|
|
31
|
+
SNICallback: (hostname, cb) => {
|
|
32
|
+
this.config.certCache.get(hostname)
|
|
33
|
+
.then(({ certPem, privateKeyPem }) => {
|
|
34
|
+
cb(null, tls.createSecureContext({ cert: certPem, key: privateKeyPem }));
|
|
35
|
+
})
|
|
36
|
+
.catch((err) => {
|
|
37
|
+
log(`cert error for ${hostname}: ${err.message}`);
|
|
38
|
+
cb(err);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
}, (req, res) => {
|
|
42
|
+
this.handleDecrypted(req, res);
|
|
43
|
+
});
|
|
44
|
+
this.innerTlsServer.on('tlsClientError', (err) => {
|
|
45
|
+
log(`TLS client error: ${err.message}`);
|
|
46
|
+
if (err.message.includes('certificate unknown')) {
|
|
47
|
+
log('TLS trust failure: restart the client with NODE_EXTRA_CA_CERTS pointing at the AntSeed CA, or install/trust the CA for that client.');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.innerNetServer = net.createServer((socket) => {
|
|
51
|
+
this.trackSocket(socket);
|
|
52
|
+
this.innerTlsServer.emit('connection', socket);
|
|
53
|
+
});
|
|
54
|
+
await new Promise((resolve) => {
|
|
55
|
+
this.innerNetServer.listen(0, '127.0.0.1', () => resolve());
|
|
56
|
+
});
|
|
57
|
+
this.innerPort = this.innerNetServer.address().port;
|
|
58
|
+
this.outerServer = http.createServer({ maxHeaderSize: 64 * 1024 }, (_req, res) => {
|
|
59
|
+
res.writeHead(400).end('AntSeed system-proxy: use HTTPS via CONNECT.');
|
|
60
|
+
});
|
|
61
|
+
this.outerServer.on('connect', (req, socket, head) => {
|
|
62
|
+
this.handleConnect(req, socket, head);
|
|
63
|
+
});
|
|
64
|
+
this.outerServer.on('connection', (socket) => {
|
|
65
|
+
this.trackSocket(socket);
|
|
66
|
+
});
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
this.outerServer.listen(this.config.port, '127.0.0.1', () => resolve());
|
|
69
|
+
});
|
|
70
|
+
log(`listening on :${this.port}`);
|
|
71
|
+
log(`proxying: ${[...this.config.proxiedDomains].join(', ')}`);
|
|
72
|
+
log(`routes: ${formatSystemProxyRoutes(this.config.proxiedPathPrefixes)}`);
|
|
73
|
+
log(`peer: ${this.config.defaultPeerId}`);
|
|
74
|
+
if (this.config.defaultModel) {
|
|
75
|
+
log(`default model: ${this.config.defaultModel}`);
|
|
76
|
+
}
|
|
77
|
+
log(`buyer proxy: localhost:${this.config.buyerProxyPort}`);
|
|
78
|
+
log(`env HTTPS_PROXY=${process.env['HTTPS_PROXY'] ?? '(not set)'}`);
|
|
79
|
+
log(`env NODE_EXTRA_CA_CERTS=${process.env['NODE_EXTRA_CA_CERTS'] ?? '(not set)'}`);
|
|
80
|
+
}
|
|
81
|
+
get port() {
|
|
82
|
+
return this.outerServer?.address()?.port ?? 0;
|
|
83
|
+
}
|
|
84
|
+
get innerTlsPort() {
|
|
85
|
+
return this.innerNetServer?.address()?.port ?? 0;
|
|
86
|
+
}
|
|
87
|
+
async stop() {
|
|
88
|
+
for (const socket of this.sockets) {
|
|
89
|
+
socket.destroy();
|
|
90
|
+
}
|
|
91
|
+
await Promise.all([
|
|
92
|
+
closeServer(this.outerServer),
|
|
93
|
+
closeServer(this.innerNetServer),
|
|
94
|
+
closeServer(this.innerTlsServer),
|
|
95
|
+
]);
|
|
96
|
+
this.outerServer = null;
|
|
97
|
+
this.innerNetServer = null;
|
|
98
|
+
this.innerTlsServer = null;
|
|
99
|
+
}
|
|
100
|
+
trackSocket(socket) {
|
|
101
|
+
this.sockets.add(socket);
|
|
102
|
+
socket.on('close', () => this.sockets.delete(socket));
|
|
103
|
+
}
|
|
104
|
+
handleConnect(req, clientSocket, head) {
|
|
105
|
+
const target = req.url ?? '';
|
|
106
|
+
const colonIdx = target.lastIndexOf(':');
|
|
107
|
+
const host = colonIdx >= 0 ? target.slice(0, colonIdx) : target;
|
|
108
|
+
const port = colonIdx >= 0 ? (parseInt(target.slice(colonIdx + 1), 10) || 443) : 443;
|
|
109
|
+
if (this.config.proxiedDomains.has(host)) {
|
|
110
|
+
log(`CONNECT ${host}:${port} → proxy`);
|
|
111
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
112
|
+
this.trackSocket(clientSocket);
|
|
113
|
+
const conn = net.connect(this.innerPort, '127.0.0.1', () => {
|
|
114
|
+
this.trackSocket(conn);
|
|
115
|
+
if (head.length > 0)
|
|
116
|
+
conn.write(head);
|
|
117
|
+
clientSocket.pipe(conn);
|
|
118
|
+
conn.pipe(clientSocket);
|
|
119
|
+
});
|
|
120
|
+
conn.on('close', (hadError) => {
|
|
121
|
+
if (!hadError) {
|
|
122
|
+
log(`CONNECT ${host}:${port} closed before HTTPS request`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
conn.on('error', (err) => {
|
|
126
|
+
log(`inner conn error for ${host}: ${err.message}`);
|
|
127
|
+
clientSocket.destroy();
|
|
128
|
+
});
|
|
129
|
+
clientSocket.on('error', () => conn.destroy());
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
log(`CONNECT ${host}:${port} → tunnel (not proxied)`);
|
|
133
|
+
this.trackSocket(clientSocket);
|
|
134
|
+
const conn = net.connect(port, host, () => {
|
|
135
|
+
this.trackSocket(conn);
|
|
136
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
137
|
+
if (head.length > 0)
|
|
138
|
+
conn.write(head);
|
|
139
|
+
clientSocket.pipe(conn);
|
|
140
|
+
conn.pipe(clientSocket);
|
|
141
|
+
});
|
|
142
|
+
conn.on('error', () => {
|
|
143
|
+
if (!clientSocket.destroyed) {
|
|
144
|
+
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
145
|
+
clientSocket.destroy();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
clientSocket.on('error', () => conn.destroy());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
handleDecrypted(req, res) {
|
|
152
|
+
const host = req.headers['host']?.split(':')[0] ?? 'unknown';
|
|
153
|
+
const isJson = (req.headers['content-type'] ?? '').includes('application/json');
|
|
154
|
+
const hasAuth = Boolean(req.headers['authorization']);
|
|
155
|
+
const forwardRule = getSystemProxyForwardRule(host, req.url ?? '/', this.config.proxiedPathPrefixes, this.config.systemProxyForwardRules);
|
|
156
|
+
if (req.method === 'OPTIONS' && forwardRule?.cors) {
|
|
157
|
+
log(`→ OPTIONS ${host}${req.url ?? '/'} | cors preflight | source:${forwardRule.source}`);
|
|
158
|
+
res.writeHead(204, buildCorsHeaders(req, forwardRule.cors, true));
|
|
159
|
+
res.end();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const chunks = [];
|
|
163
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
164
|
+
req.on('error', (err) => {
|
|
165
|
+
log(`request error from ${host}: ${err.message}`);
|
|
166
|
+
if (!res.headersSent)
|
|
167
|
+
res.writeHead(400).end(err.message);
|
|
168
|
+
});
|
|
169
|
+
req.on('end', () => {
|
|
170
|
+
const rawBody = Buffer.concat(chunks);
|
|
171
|
+
const buyerRequest = buildBuyerForwardRequest({
|
|
172
|
+
host,
|
|
173
|
+
path: req.url ?? '/',
|
|
174
|
+
rawBody,
|
|
175
|
+
isJson,
|
|
176
|
+
peerId: this.config.defaultPeerId,
|
|
177
|
+
defaultModel: this.config.defaultModel,
|
|
178
|
+
servedModels: this.config.servedModels,
|
|
179
|
+
prefixesByHost: this.config.proxiedPathPrefixes,
|
|
180
|
+
forwardRulesByHost: this.config.systemProxyForwardRules,
|
|
181
|
+
});
|
|
182
|
+
const body = buyerRequest?.body ?? rawBody;
|
|
183
|
+
// Log the request details
|
|
184
|
+
let modelBefore = '';
|
|
185
|
+
let modelAfter = '';
|
|
186
|
+
if (isJson && rawBody.length > 0) {
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(rawBody.toString('utf8'));
|
|
189
|
+
modelBefore = typeof parsed['model'] === 'string' ? parsed['model'] : '';
|
|
190
|
+
}
|
|
191
|
+
catch { /* ignore */ }
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(body.toString('utf8'));
|
|
194
|
+
modelAfter = typeof parsed['model'] === 'string' ? parsed['model'] : '';
|
|
195
|
+
}
|
|
196
|
+
catch { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
if (!buyerRequest) {
|
|
199
|
+
log(`→ ${req.method ?? 'GET'} ${host}${req.url ?? '/'} | pass-through`);
|
|
200
|
+
this.forwardToUpstream(host, req, res, body);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const authNote = hasAuth ? 'auth:stripped' : 'auth:none';
|
|
204
|
+
const modelNote = modelBefore
|
|
205
|
+
? (modelBefore !== modelAfter ? `model: ${modelBefore} → ${modelAfter}` : `model: ${modelBefore}`)
|
|
206
|
+
: 'model: -';
|
|
207
|
+
const pathNote = buyerRequest.path === (req.url ?? '/')
|
|
208
|
+
? req.url ?? '/'
|
|
209
|
+
: `${req.url ?? '/'} → ${buyerRequest.path}`;
|
|
210
|
+
log(`→ ${req.method ?? 'GET'} ${host}${pathNote} | ${modelNote} | ${authNote} | source:${buyerRequest.source}`);
|
|
211
|
+
const forwardHeaders = {};
|
|
212
|
+
const strippedHeaders = new Set([
|
|
213
|
+
...ROUTED_SENSITIVE_HEADERS,
|
|
214
|
+
...(forwardRule?.stripHeaders ?? []).map((header) => header.toLowerCase()),
|
|
215
|
+
]);
|
|
216
|
+
if (forwardRule?.headers === 'browser-sse') {
|
|
217
|
+
forwardHeaders['accept'] = 'text/event-stream';
|
|
218
|
+
forwardHeaders['content-type'] = 'application/json';
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
222
|
+
if (strippedHeaders.has(k.toLowerCase()))
|
|
223
|
+
continue;
|
|
224
|
+
if (k === 'host')
|
|
225
|
+
continue;
|
|
226
|
+
if (HOP_BY_HOP.has(k))
|
|
227
|
+
continue;
|
|
228
|
+
forwardHeaders[k] = v;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
forwardHeaders['host'] = `127.0.0.1:${this.config.buyerProxyPort}`;
|
|
232
|
+
forwardHeaders['connection'] = 'close';
|
|
233
|
+
if (body.length > 0) {
|
|
234
|
+
forwardHeaders['content-length'] = String(body.length);
|
|
235
|
+
}
|
|
236
|
+
const proxyReq = http.request({
|
|
237
|
+
hostname: '127.0.0.1',
|
|
238
|
+
port: this.config.buyerProxyPort,
|
|
239
|
+
path: buyerRequest.path,
|
|
240
|
+
method: req.method ?? 'GET',
|
|
241
|
+
headers: forwardHeaders,
|
|
242
|
+
}, (proxyRes) => {
|
|
243
|
+
log(`← ${proxyRes.statusCode ?? '?'} ${host} | source:${buyerRequest.source} | from buyer proxy`);
|
|
244
|
+
if (buyerRequest.response?.kind === 'conversation-sse' && buyerRequest.conversation) {
|
|
245
|
+
this.forwardConversationResponse(proxyRes, res, buyerRequest.conversation);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (buyerRequest.response?.kind === 'responses-sse') {
|
|
249
|
+
this.forwardResponsesSse(proxyRes, res, req, buyerRequest.response, forwardRule);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const resHeaders = buildResponseHeaders(proxyRes, req, forwardRule);
|
|
253
|
+
res.writeHead(proxyRes.statusCode ?? 200, resHeaders);
|
|
254
|
+
proxyRes.pipe(res);
|
|
255
|
+
});
|
|
256
|
+
proxyReq.on('error', (err) => {
|
|
257
|
+
log(`← 502 ${host} | source:${buyerRequest.source} | forward error: ${err.message}`);
|
|
258
|
+
if (!res.headersSent) {
|
|
259
|
+
res.writeHead(502).end(`SystemProxy forward error: ${err.message}`);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (body.length > 0) {
|
|
263
|
+
proxyReq.end(body);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
proxyReq.end();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
forwardConversationResponse(proxyRes, res, context) {
|
|
271
|
+
res.writeHead(proxyRes.statusCode && proxyRes.statusCode >= 400 ? proxyRes.statusCode : 200, {
|
|
272
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
273
|
+
'cache-control': 'no-cache',
|
|
274
|
+
'connection': 'close',
|
|
275
|
+
});
|
|
276
|
+
const adapter = new ConversationSseAdapter(context);
|
|
277
|
+
const contentType = String(proxyRes.headers['content-type'] ?? '').toLowerCase();
|
|
278
|
+
const chunks = [];
|
|
279
|
+
proxyRes.on('data', (chunk) => {
|
|
280
|
+
if (contentType.includes('text/event-stream')) {
|
|
281
|
+
for (const out of adapter.feed(chunk, false)) {
|
|
282
|
+
res.write(out);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
chunks.push(chunk);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
proxyRes.on('end', () => {
|
|
290
|
+
const out = contentType.includes('text/event-stream')
|
|
291
|
+
? adapter.feed(Buffer.alloc(0), true)
|
|
292
|
+
: adapter.fromJsonResponse(Buffer.concat(chunks));
|
|
293
|
+
for (const chunk of out) {
|
|
294
|
+
res.write(chunk);
|
|
295
|
+
}
|
|
296
|
+
res.end();
|
|
297
|
+
});
|
|
298
|
+
proxyRes.on('error', (err) => {
|
|
299
|
+
log(`conversation response error: ${err.message}`);
|
|
300
|
+
for (const out of adapter.error(err.message)) {
|
|
301
|
+
res.write(out);
|
|
302
|
+
}
|
|
303
|
+
res.end();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
forwardResponsesSse(proxyRes, res, originalReq, transform, forwardRule) {
|
|
307
|
+
const resHeaders = buildResponseHeaders(proxyRes, originalReq, forwardRule);
|
|
308
|
+
const contentType = String(proxyRes.headers['content-type'] ?? '').toLowerCase();
|
|
309
|
+
if (!contentType.includes('text/event-stream')) {
|
|
310
|
+
res.writeHead(proxyRes.statusCode ?? 200, resHeaders);
|
|
311
|
+
proxyRes.pipe(res);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
resHeaders['content-type'] = 'text/event-stream; charset=utf-8';
|
|
315
|
+
delete resHeaders['content-length'];
|
|
316
|
+
res.writeHead(proxyRes.statusCode ?? 200, resHeaders);
|
|
317
|
+
const adapter = new ResponsesSseNormalizer(transform);
|
|
318
|
+
proxyRes.on('data', (chunk) => {
|
|
319
|
+
for (const out of adapter.feed(chunk, false)) {
|
|
320
|
+
res.write(out);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
proxyRes.on('end', () => {
|
|
324
|
+
for (const out of adapter.feed(Buffer.alloc(0), true)) {
|
|
325
|
+
res.write(out);
|
|
326
|
+
}
|
|
327
|
+
res.end();
|
|
328
|
+
});
|
|
329
|
+
proxyRes.on('error', (err) => {
|
|
330
|
+
log(`responses sse error: ${err.message}`);
|
|
331
|
+
res.end();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
forwardToUpstream(host, req, res, body) {
|
|
335
|
+
const forwardHeaders = {};
|
|
336
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
337
|
+
if (HOP_BY_HOP.has(k))
|
|
338
|
+
continue;
|
|
339
|
+
forwardHeaders[k] = v;
|
|
340
|
+
}
|
|
341
|
+
forwardHeaders['host'] = host;
|
|
342
|
+
forwardHeaders['connection'] = 'close';
|
|
343
|
+
if (body.length > 0) {
|
|
344
|
+
forwardHeaders['content-length'] = String(body.length);
|
|
345
|
+
}
|
|
346
|
+
const upstream = this.config.upstreamOverrides?.get(host);
|
|
347
|
+
const upstreamReq = https.request({
|
|
348
|
+
hostname: upstream?.host ?? host,
|
|
349
|
+
port: upstream?.port ?? 443,
|
|
350
|
+
servername: host,
|
|
351
|
+
path: req.url ?? '/',
|
|
352
|
+
method: req.method ?? 'GET',
|
|
353
|
+
headers: forwardHeaders,
|
|
354
|
+
}, (upstreamRes) => {
|
|
355
|
+
log(`← ${upstreamRes.statusCode ?? '?'} from upstream ${host}`);
|
|
356
|
+
const resHeaders = {};
|
|
357
|
+
for (const [k, v] of Object.entries(upstreamRes.headers)) {
|
|
358
|
+
if (!HOP_BY_HOP.has(k) && v !== undefined) {
|
|
359
|
+
resHeaders[k] = v;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
resHeaders['connection'] = 'close';
|
|
363
|
+
res.writeHead(upstreamRes.statusCode ?? 200, resHeaders);
|
|
364
|
+
upstreamRes.pipe(res);
|
|
365
|
+
});
|
|
366
|
+
upstreamReq.on('error', (err) => {
|
|
367
|
+
log(`upstream error → ${host}: ${err.message}`);
|
|
368
|
+
if (!res.headersSent) {
|
|
369
|
+
res.writeHead(502).end(`SystemProxy upstream error: ${err.message}`);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (body.length > 0)
|
|
373
|
+
upstreamReq.end(body);
|
|
374
|
+
else
|
|
375
|
+
upstreamReq.end();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function buildResponseHeaders(proxyRes, originalReq, forwardRule) {
|
|
379
|
+
const resHeaders = {};
|
|
380
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
381
|
+
if (!HOP_BY_HOP.has(k) && v !== undefined) {
|
|
382
|
+
resHeaders[k] = v;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (forwardRule?.cors) {
|
|
386
|
+
Object.assign(resHeaders, buildCorsHeaders(originalReq, forwardRule.cors, false));
|
|
387
|
+
}
|
|
388
|
+
resHeaders['connection'] = 'close';
|
|
389
|
+
return resHeaders;
|
|
390
|
+
}
|
|
391
|
+
function getSystemProxyForwardRule(host, path, prefixesByHost, forwardRulesByHost) {
|
|
392
|
+
if (!shouldSystemProxyRequest(host, path, prefixesByHost)) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return forwardRulesByHost?.get(host);
|
|
396
|
+
}
|
|
397
|
+
function buildCorsHeaders(req, rule, preflight) {
|
|
398
|
+
const headers = {};
|
|
399
|
+
const origin = resolveCorsOrigin(req.headers['origin'], rule.allowOrigin);
|
|
400
|
+
if (origin) {
|
|
401
|
+
headers['access-control-allow-origin'] = origin;
|
|
402
|
+
headers['vary'] = 'Origin';
|
|
403
|
+
}
|
|
404
|
+
if (rule.allowCredentials) {
|
|
405
|
+
headers['access-control-allow-credentials'] = 'true';
|
|
406
|
+
}
|
|
407
|
+
if (preflight) {
|
|
408
|
+
headers['access-control-allow-methods'] = (rule.allowMethods ?? ['GET', 'POST', 'OPTIONS']).join(', ');
|
|
409
|
+
const requestHeaders = typeof req.headers['access-control-request-headers'] === 'string'
|
|
410
|
+
? req.headers['access-control-request-headers']
|
|
411
|
+
: '';
|
|
412
|
+
const allowHeaders = rule.allowHeaders === 'request'
|
|
413
|
+
? requestHeaders
|
|
414
|
+
: (rule.allowHeaders ?? ['content-type', 'authorization']).join(', ');
|
|
415
|
+
if (allowHeaders.length > 0) {
|
|
416
|
+
headers['access-control-allow-headers'] = allowHeaders;
|
|
417
|
+
}
|
|
418
|
+
headers['access-control-max-age'] = String(rule.maxAgeSeconds ?? 600);
|
|
419
|
+
}
|
|
420
|
+
if (rule.exposeHeaders && rule.exposeHeaders.length > 0) {
|
|
421
|
+
headers['access-control-expose-headers'] = rule.exposeHeaders.join(', ');
|
|
422
|
+
}
|
|
423
|
+
return headers;
|
|
424
|
+
}
|
|
425
|
+
function resolveCorsOrigin(requestOrigin, allowOrigin = 'request') {
|
|
426
|
+
if (allowOrigin === '*')
|
|
427
|
+
return '*';
|
|
428
|
+
if (allowOrigin === 'request') {
|
|
429
|
+
return typeof requestOrigin === 'string' && requestOrigin.length > 0 ? requestOrigin : undefined;
|
|
430
|
+
}
|
|
431
|
+
return allowOrigin;
|
|
432
|
+
}
|
|
433
|
+
export function buildBuyerForwardRequest(opts) {
|
|
434
|
+
if (!shouldSystemProxyRequest(opts.host, opts.path, opts.prefixesByHost)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const forwardRule = opts.forwardRulesByHost?.get(opts.host);
|
|
438
|
+
if (forwardRule?.request) {
|
|
439
|
+
if (!opts.isJson)
|
|
440
|
+
return null;
|
|
441
|
+
const body = transformRequestBody(opts.rawBody, opts.peerId, forwardRule.request, {
|
|
442
|
+
defaultModel: opts.defaultModel,
|
|
443
|
+
servedModels: opts.servedModels,
|
|
444
|
+
});
|
|
445
|
+
if (!body)
|
|
446
|
+
return null;
|
|
447
|
+
return {
|
|
448
|
+
path: forwardRule?.targetPath ?? opts.path,
|
|
449
|
+
body,
|
|
450
|
+
source: forwardRule.source,
|
|
451
|
+
response: forwardRule.response,
|
|
452
|
+
conversation: forwardRule.response?.kind === 'conversation-sse'
|
|
453
|
+
? extractConversationForwardContext(opts.rawBody, body, forwardRule.response)
|
|
454
|
+
: undefined,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (forwardRule) {
|
|
458
|
+
return {
|
|
459
|
+
path: forwardRule.targetPath ?? opts.path,
|
|
460
|
+
body: opts.isJson
|
|
461
|
+
? rewriteRequestBody(opts.rawBody, opts.peerId, {
|
|
462
|
+
defaultModel: opts.defaultModel,
|
|
463
|
+
servedModels: opts.servedModels,
|
|
464
|
+
})
|
|
465
|
+
: opts.rawBody,
|
|
466
|
+
source: forwardRule.source,
|
|
467
|
+
response: forwardRule.response,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
path: opts.path,
|
|
472
|
+
body: opts.isJson
|
|
473
|
+
? rewriteRequestBody(opts.rawBody, opts.peerId, {
|
|
474
|
+
defaultModel: opts.defaultModel,
|
|
475
|
+
servedModels: opts.servedModels,
|
|
476
|
+
})
|
|
477
|
+
: opts.rawBody,
|
|
478
|
+
source: 'standard',
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
export function rewriteRequestBody(raw, peerId, modelDefaults = {}) {
|
|
482
|
+
if (raw.length === 0)
|
|
483
|
+
return raw;
|
|
484
|
+
try {
|
|
485
|
+
const json = JSON.parse(raw.toString('utf8'));
|
|
486
|
+
const rawModel = typeof json['model'] === 'string' ? json['model'] : '';
|
|
487
|
+
const model = resolveSystemProxyModel(rawModel, modelDefaults);
|
|
488
|
+
if (model && peerId) {
|
|
489
|
+
json['model'] = `${peerId}@${model}`;
|
|
490
|
+
}
|
|
491
|
+
return Buffer.from(JSON.stringify(json), 'utf8');
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return raw;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
export function transformRequestBody(raw, peerId, transform, modelDefaults = {}) {
|
|
498
|
+
if (raw.length === 0)
|
|
499
|
+
return null;
|
|
500
|
+
try {
|
|
501
|
+
const json = JSON.parse(raw.toString('utf8'));
|
|
502
|
+
const messages = extractProfileMessages(getPath(json, transform.messagesPath));
|
|
503
|
+
if (messages.length === 0) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
const rawModelValue = transform.modelPath ? getPath(json, transform.modelPath) : undefined;
|
|
507
|
+
const rawModel = typeof rawModelValue === 'string' ? rawModelValue : '';
|
|
508
|
+
if (transform.kind === 'chat-completions-from-messages') {
|
|
509
|
+
const resolvedModel = (resolveSystemProxyModel(rawModel || transform.missingModel || '', modelDefaults)
|
|
510
|
+
?? (rawModel.trim() || transform.missingModel || '')).trim();
|
|
511
|
+
if (resolvedModel.length === 0)
|
|
512
|
+
return null;
|
|
513
|
+
return Buffer.from(JSON.stringify({
|
|
514
|
+
model: peerId ? `${peerId}@${resolvedModel}` : resolvedModel,
|
|
515
|
+
messages,
|
|
516
|
+
stream: transform.stream ?? true,
|
|
517
|
+
}), 'utf8');
|
|
518
|
+
}
|
|
519
|
+
const resolvedModel = resolveSystemProxyModel(rawModel, modelDefaults);
|
|
520
|
+
if (!resolvedModel)
|
|
521
|
+
return null;
|
|
522
|
+
return Buffer.from(JSON.stringify({
|
|
523
|
+
model: peerId ? `${peerId}@${resolvedModel}` : resolvedModel,
|
|
524
|
+
input: messages,
|
|
525
|
+
stream: transform.stream ?? true,
|
|
526
|
+
}), 'utf8');
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
export class ConversationSseAdapter {
|
|
533
|
+
context;
|
|
534
|
+
buffer = '';
|
|
535
|
+
text = '';
|
|
536
|
+
done = false;
|
|
537
|
+
createdAt;
|
|
538
|
+
constructor(context) {
|
|
539
|
+
this.context = context;
|
|
540
|
+
this.createdAt = context.assistantCreateTime ?? Date.now() / 1000;
|
|
541
|
+
}
|
|
542
|
+
feed(chunk, done) {
|
|
543
|
+
const out = [];
|
|
544
|
+
if (chunk.length > 0) {
|
|
545
|
+
this.buffer += chunk.toString('utf8');
|
|
546
|
+
}
|
|
547
|
+
const normalized = this.buffer.replace(/\r\n/g, '\n');
|
|
548
|
+
const blocks = normalized.split('\n\n');
|
|
549
|
+
this.buffer = blocks.pop() ?? '';
|
|
550
|
+
for (const block of blocks) {
|
|
551
|
+
const data = block
|
|
552
|
+
.split('\n')
|
|
553
|
+
.filter((line) => line.startsWith('data: '))
|
|
554
|
+
.map((line) => line.slice('data: '.length))
|
|
555
|
+
.join('\n');
|
|
556
|
+
if (!data)
|
|
557
|
+
continue;
|
|
558
|
+
if (data === '[DONE]') {
|
|
559
|
+
out.push(this.finish());
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const parsed = parseJsonObject(data);
|
|
563
|
+
if (!parsed)
|
|
564
|
+
continue;
|
|
565
|
+
const delta = extractChatTextDelta(parsed);
|
|
566
|
+
if (delta.length > 0) {
|
|
567
|
+
this.text += delta;
|
|
568
|
+
out.push(this.event('in_progress', false));
|
|
569
|
+
}
|
|
570
|
+
if (chatFinished(parsed)) {
|
|
571
|
+
out.push(this.finish());
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (done) {
|
|
575
|
+
out.push(this.finish());
|
|
576
|
+
}
|
|
577
|
+
return out.filter((item) => item.length > 0);
|
|
578
|
+
}
|
|
579
|
+
fromJsonResponse(body) {
|
|
580
|
+
const parsed = parseJsonObject(body);
|
|
581
|
+
if (!parsed) {
|
|
582
|
+
return this.error('Invalid response from buyer proxy.');
|
|
583
|
+
}
|
|
584
|
+
const error = parsed['error'];
|
|
585
|
+
if (error && typeof error === 'object' && !Array.isArray(error)) {
|
|
586
|
+
const message = typeof error['message'] === 'string'
|
|
587
|
+
? String(error['message'])
|
|
588
|
+
: 'Buyer proxy returned an error.';
|
|
589
|
+
return this.error(message);
|
|
590
|
+
}
|
|
591
|
+
const text = extractChatMessageText(parsed);
|
|
592
|
+
if (text.length === 0) {
|
|
593
|
+
return this.error('Buyer proxy returned an empty response.');
|
|
594
|
+
}
|
|
595
|
+
this.text = text;
|
|
596
|
+
return [this.event('in_progress', false), this.finish()].filter((item) => item.length > 0);
|
|
597
|
+
}
|
|
598
|
+
error(message) {
|
|
599
|
+
if (this.done)
|
|
600
|
+
return [];
|
|
601
|
+
this.done = true;
|
|
602
|
+
const payload = { message: null, conversation_id: this.context.conversationId, error: message };
|
|
603
|
+
return [Buffer.from(`data: ${JSON.stringify(payload)}\n\ndata: [DONE]\n\n`, 'utf8')];
|
|
604
|
+
}
|
|
605
|
+
finish() {
|
|
606
|
+
if (this.done)
|
|
607
|
+
return Buffer.alloc(0);
|
|
608
|
+
this.done = true;
|
|
609
|
+
return Buffer.from(`${this.encodeEvent('finished_successfully', true)}data: [DONE]\n\n`, 'utf8');
|
|
610
|
+
}
|
|
611
|
+
event(status, endTurn) {
|
|
612
|
+
if (this.done)
|
|
613
|
+
return Buffer.alloc(0);
|
|
614
|
+
return Buffer.from(this.encodeEvent(status, endTurn), 'utf8');
|
|
615
|
+
}
|
|
616
|
+
encodeEvent(status, endTurn) {
|
|
617
|
+
const metadata = {
|
|
618
|
+
message_type: 'next',
|
|
619
|
+
parent_id: this.context.assistantParent === 'user-message'
|
|
620
|
+
? this.context.userMessageId
|
|
621
|
+
: this.context.parentMessageId,
|
|
622
|
+
};
|
|
623
|
+
if (this.context.modelSlug) {
|
|
624
|
+
metadata['model_slug'] = this.context.modelSlug;
|
|
625
|
+
metadata['default_model_slug'] = this.context.modelSlug;
|
|
626
|
+
}
|
|
627
|
+
if (endTurn) {
|
|
628
|
+
metadata['finish_details'] = { type: 'stop' };
|
|
629
|
+
metadata['is_complete'] = true;
|
|
630
|
+
}
|
|
631
|
+
const payload = {
|
|
632
|
+
message: {
|
|
633
|
+
id: this.context.assistantMessageId,
|
|
634
|
+
author: { role: 'assistant', name: null, metadata: {} },
|
|
635
|
+
create_time: this.createdAt,
|
|
636
|
+
update_time: null,
|
|
637
|
+
content: { content_type: 'text', parts: [this.text] },
|
|
638
|
+
status,
|
|
639
|
+
end_turn: endTurn,
|
|
640
|
+
weight: 1,
|
|
641
|
+
metadata,
|
|
642
|
+
recipient: 'all',
|
|
643
|
+
channel: null,
|
|
644
|
+
},
|
|
645
|
+
conversation_id: this.context.conversationId,
|
|
646
|
+
error: null,
|
|
647
|
+
content_references: [],
|
|
648
|
+
};
|
|
649
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
export class ResponsesSseNormalizer {
|
|
653
|
+
options;
|
|
654
|
+
buffer = '';
|
|
655
|
+
responseId = null;
|
|
656
|
+
messageItemId = null;
|
|
657
|
+
constructor(options) {
|
|
658
|
+
this.options = options;
|
|
659
|
+
}
|
|
660
|
+
feed(chunk, done) {
|
|
661
|
+
const out = [];
|
|
662
|
+
if (chunk.length > 0) {
|
|
663
|
+
this.buffer += chunk.toString('utf8');
|
|
664
|
+
}
|
|
665
|
+
const normalized = this.buffer.replace(/\r\n/g, '\n');
|
|
666
|
+
const blocks = normalized.split('\n\n');
|
|
667
|
+
this.buffer = blocks.pop() ?? '';
|
|
668
|
+
for (const block of blocks) {
|
|
669
|
+
out.push(Buffer.from(this.normalizeBlock(block), 'utf8'));
|
|
670
|
+
}
|
|
671
|
+
if (done && this.buffer.trim().length > 0) {
|
|
672
|
+
out.push(Buffer.from(this.normalizeBlock(this.buffer), 'utf8'));
|
|
673
|
+
this.buffer = '';
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
normalizeBlock(block) {
|
|
678
|
+
const lines = block.split('\n');
|
|
679
|
+
const event = lines
|
|
680
|
+
.find((line) => line.startsWith('event: '))
|
|
681
|
+
?.slice('event: '.length)
|
|
682
|
+
.trim();
|
|
683
|
+
const dataLines = lines
|
|
684
|
+
.filter((line) => line.startsWith('data: '))
|
|
685
|
+
.map((line) => line.slice('data: '.length));
|
|
686
|
+
if (!event || dataLines.length === 0) {
|
|
687
|
+
return `${block}\n\n`;
|
|
688
|
+
}
|
|
689
|
+
const data = dataLines.join('\n');
|
|
690
|
+
if (data === '[DONE]') {
|
|
691
|
+
return `data: [DONE]\n\n`;
|
|
692
|
+
}
|
|
693
|
+
const parsed = parseJsonObject(data);
|
|
694
|
+
if (!parsed) {
|
|
695
|
+
return `${block}\n\n`;
|
|
696
|
+
}
|
|
697
|
+
const response = parsed['response'];
|
|
698
|
+
if (response && typeof response === 'object' && !Array.isArray(response)) {
|
|
699
|
+
const id = response['id'];
|
|
700
|
+
if (typeof id === 'string' && id.length > 0) {
|
|
701
|
+
this.responseId = id;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (this.options.injectResponseId && this.responseId && shouldAttachResponseId(event, parsed)) {
|
|
705
|
+
parsed['response_id'] = this.responseId;
|
|
706
|
+
}
|
|
707
|
+
if (this.options.stripDataType) {
|
|
708
|
+
delete parsed['type'];
|
|
709
|
+
}
|
|
710
|
+
this.normalizeMessageItem(event, parsed);
|
|
711
|
+
if (this.options.emptyContentPartDoneText && event === 'response.content_part.done') {
|
|
712
|
+
const part = parsed['part'];
|
|
713
|
+
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
714
|
+
parsed['part'] = { ...part, text: '' };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (this.options.emptyOutputItemDoneContent && event === 'response.output_item.done') {
|
|
718
|
+
const item = parsed['item'];
|
|
719
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
720
|
+
parsed['item'] = { ...item, content: [] };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return `event: ${event}\ndata: ${JSON.stringify(parsed)}\n\n`;
|
|
724
|
+
}
|
|
725
|
+
normalizeMessageItem(event, parsed) {
|
|
726
|
+
if (event === 'response.output_item.added' || event === 'response.output_item.done') {
|
|
727
|
+
const item = parsed['item'];
|
|
728
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
729
|
+
return;
|
|
730
|
+
const itemObject = item;
|
|
731
|
+
if (itemObject['type'] !== 'message')
|
|
732
|
+
return;
|
|
733
|
+
const id = typeof itemObject['id'] === 'string' && itemObject['id'].length > 0
|
|
734
|
+
? itemObject['id']
|
|
735
|
+
: null;
|
|
736
|
+
if (id && !this.messageItemId) {
|
|
737
|
+
this.messageItemId = id;
|
|
738
|
+
}
|
|
739
|
+
if (this.options.messageOutputIndex !== undefined) {
|
|
740
|
+
parsed['output_index'] = this.options.messageOutputIndex;
|
|
741
|
+
}
|
|
742
|
+
if (this.options.messageItemId) {
|
|
743
|
+
itemObject['id'] = this.options.messageItemId;
|
|
744
|
+
}
|
|
745
|
+
if (this.options.stripItemPhase) {
|
|
746
|
+
delete itemObject['phase'];
|
|
747
|
+
}
|
|
748
|
+
parsed['item'] = itemObject;
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const itemId = typeof parsed['item_id'] === 'string' ? parsed['item_id'] : '';
|
|
752
|
+
if (!this.messageItemId || itemId !== this.messageItemId)
|
|
753
|
+
return;
|
|
754
|
+
if (this.options.messageOutputIndex !== undefined) {
|
|
755
|
+
parsed['output_index'] = this.options.messageOutputIndex;
|
|
756
|
+
}
|
|
757
|
+
if (this.options.messageItemId) {
|
|
758
|
+
parsed['item_id'] = this.options.messageItemId;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function resolveSystemProxyModel(rawModel, opts) {
|
|
763
|
+
const requested = rawModel.trim();
|
|
764
|
+
const fallback = opts.defaultModel?.trim();
|
|
765
|
+
if (!fallback) {
|
|
766
|
+
return requested.length > 0 ? requested : null;
|
|
767
|
+
}
|
|
768
|
+
if (requested.length === 0 || requested.toLowerCase() === 'auto') {
|
|
769
|
+
return fallback;
|
|
770
|
+
}
|
|
771
|
+
const served = opts.servedModels ? normalizeServedModels(opts.servedModels) : new Set();
|
|
772
|
+
if (served.size > 0 && !served.has(requested.toLowerCase())) {
|
|
773
|
+
return fallback;
|
|
774
|
+
}
|
|
775
|
+
return requested;
|
|
776
|
+
}
|
|
777
|
+
function normalizeServedModels(models) {
|
|
778
|
+
const normalized = new Set();
|
|
779
|
+
for (const model of models) {
|
|
780
|
+
const value = model.trim().toLowerCase();
|
|
781
|
+
if (value.length > 0)
|
|
782
|
+
normalized.add(value);
|
|
783
|
+
}
|
|
784
|
+
return normalized;
|
|
785
|
+
}
|
|
786
|
+
function extractConversationForwardContext(raw, routedBody, responseTransform) {
|
|
787
|
+
const parsed = parseJsonObject(raw);
|
|
788
|
+
const routed = routedBody ? parseJsonObject(routedBody) : null;
|
|
789
|
+
const conversationId = typeof parsed?.['conversation_id'] === 'string' && parsed['conversation_id'].length > 0
|
|
790
|
+
? parsed['conversation_id']
|
|
791
|
+
: randomUUID();
|
|
792
|
+
const parentMessageId = typeof parsed?.['parent_message_id'] === 'string' && parsed['parent_message_id'].length > 0
|
|
793
|
+
? parsed['parent_message_id']
|
|
794
|
+
: 'client-created-root';
|
|
795
|
+
const messages = Array.isArray(parsed?.['messages']) ? parsed['messages'] : [];
|
|
796
|
+
const firstMessage = messages[0] && typeof messages[0] === 'object' && !Array.isArray(messages[0])
|
|
797
|
+
? messages[0]
|
|
798
|
+
: null;
|
|
799
|
+
const userMessageId = typeof firstMessage?.['id'] === 'string' && firstMessage['id'].length > 0
|
|
800
|
+
? firstMessage['id']
|
|
801
|
+
: randomUUID();
|
|
802
|
+
const userCreateTime = typeof firstMessage?.['create_time'] === 'number'
|
|
803
|
+
? firstMessage['create_time']
|
|
804
|
+
: null;
|
|
805
|
+
const now = Date.now() / 1000;
|
|
806
|
+
return {
|
|
807
|
+
conversationId,
|
|
808
|
+
parentMessageId,
|
|
809
|
+
userMessageId,
|
|
810
|
+
assistantMessageId: randomUUID(),
|
|
811
|
+
assistantParent: responseTransform?.assistantParent,
|
|
812
|
+
assistantCreateTime: userCreateTime ? Math.max(now, userCreateTime + 0.001) : now,
|
|
813
|
+
modelSlug: extractRoutedModelSlug(routed?.['model']),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function shouldAttachResponseId(event, parsed) {
|
|
817
|
+
if (parsed['response_id'] !== undefined)
|
|
818
|
+
return false;
|
|
819
|
+
if (event === 'response.created' || event === 'response.in_progress' || event === 'response.completed') {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
return event.startsWith('response.');
|
|
823
|
+
}
|
|
824
|
+
function extractRoutedModelSlug(value) {
|
|
825
|
+
if (typeof value !== 'string')
|
|
826
|
+
return undefined;
|
|
827
|
+
const model = value.trim();
|
|
828
|
+
if (model.length === 0)
|
|
829
|
+
return undefined;
|
|
830
|
+
const at = model.indexOf('@');
|
|
831
|
+
return at >= 0 ? model.slice(at + 1) || undefined : model;
|
|
832
|
+
}
|
|
833
|
+
function parseJsonObject(value) {
|
|
834
|
+
try {
|
|
835
|
+
const parsed = JSON.parse(typeof value === 'string' ? value : value.toString('utf8'));
|
|
836
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
837
|
+
? parsed
|
|
838
|
+
: null;
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function extractChatTextDelta(parsed) {
|
|
845
|
+
const choices = Array.isArray(parsed['choices']) ? parsed['choices'] : [];
|
|
846
|
+
const choice = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
|
|
847
|
+
? choices[0]
|
|
848
|
+
: null;
|
|
849
|
+
const delta = choice?.['delta'] && typeof choice['delta'] === 'object' && !Array.isArray(choice['delta'])
|
|
850
|
+
? choice['delta']
|
|
851
|
+
: null;
|
|
852
|
+
return typeof delta?.['content'] === 'string' ? delta['content'] : '';
|
|
853
|
+
}
|
|
854
|
+
function extractChatMessageText(parsed) {
|
|
855
|
+
const choices = Array.isArray(parsed['choices']) ? parsed['choices'] : [];
|
|
856
|
+
const choice = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
|
|
857
|
+
? choices[0]
|
|
858
|
+
: null;
|
|
859
|
+
const message = choice?.['message'] && typeof choice['message'] === 'object' && !Array.isArray(choice['message'])
|
|
860
|
+
? choice['message']
|
|
861
|
+
: null;
|
|
862
|
+
return typeof message?.['content'] === 'string' ? message['content'] : '';
|
|
863
|
+
}
|
|
864
|
+
function chatFinished(parsed) {
|
|
865
|
+
const choices = Array.isArray(parsed['choices']) ? parsed['choices'] : [];
|
|
866
|
+
const choice = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
|
|
867
|
+
? choices[0]
|
|
868
|
+
: null;
|
|
869
|
+
return typeof choice?.['finish_reason'] === 'string' && choice['finish_reason'].length > 0;
|
|
870
|
+
}
|
|
871
|
+
function extractProfileMessages(messagesRaw) {
|
|
872
|
+
if (!Array.isArray(messagesRaw))
|
|
873
|
+
return [];
|
|
874
|
+
const messages = [];
|
|
875
|
+
for (const entry of messagesRaw) {
|
|
876
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
877
|
+
continue;
|
|
878
|
+
const object = entry;
|
|
879
|
+
const author = object['author'];
|
|
880
|
+
const role = author && typeof author === 'object' && !Array.isArray(author)
|
|
881
|
+
&& typeof author['role'] === 'string'
|
|
882
|
+
? String(author['role'])
|
|
883
|
+
: typeof object['role'] === 'string'
|
|
884
|
+
? object['role']
|
|
885
|
+
: 'user';
|
|
886
|
+
const content = extractProfileMessageContent(object['content']);
|
|
887
|
+
if (content.length === 0)
|
|
888
|
+
continue;
|
|
889
|
+
messages.push({ role: normalizeConversationRole(role), content });
|
|
890
|
+
}
|
|
891
|
+
return messages;
|
|
892
|
+
}
|
|
893
|
+
function extractProfileMessageContent(contentRaw) {
|
|
894
|
+
if (typeof contentRaw === 'string')
|
|
895
|
+
return contentRaw.trim();
|
|
896
|
+
if (Array.isArray(contentRaw)) {
|
|
897
|
+
return contentRaw
|
|
898
|
+
.map((part) => {
|
|
899
|
+
if (typeof part === 'string')
|
|
900
|
+
return part.trim();
|
|
901
|
+
if (!part || typeof part !== 'object' || Array.isArray(part))
|
|
902
|
+
return '';
|
|
903
|
+
const object = part;
|
|
904
|
+
return typeof object['text'] === 'string' ? object['text'].trim() : '';
|
|
905
|
+
})
|
|
906
|
+
.filter((part) => part.length > 0)
|
|
907
|
+
.join('\n');
|
|
908
|
+
}
|
|
909
|
+
if (!contentRaw || typeof contentRaw !== 'object' || Array.isArray(contentRaw))
|
|
910
|
+
return '';
|
|
911
|
+
const content = contentRaw;
|
|
912
|
+
const parts = content['parts'];
|
|
913
|
+
if (Array.isArray(parts)) {
|
|
914
|
+
return parts
|
|
915
|
+
.filter((part) => typeof part === 'string')
|
|
916
|
+
.map((part) => part.trim())
|
|
917
|
+
.filter((part) => part.length > 0)
|
|
918
|
+
.join('\n');
|
|
919
|
+
}
|
|
920
|
+
if (typeof content['text'] === 'string') {
|
|
921
|
+
return content['text'].trim();
|
|
922
|
+
}
|
|
923
|
+
return '';
|
|
924
|
+
}
|
|
925
|
+
function getPath(value, path) {
|
|
926
|
+
let current = value;
|
|
927
|
+
for (const segment of path) {
|
|
928
|
+
if (!current || typeof current !== 'object' || Array.isArray(current))
|
|
929
|
+
return undefined;
|
|
930
|
+
current = current[segment];
|
|
931
|
+
}
|
|
932
|
+
return current;
|
|
933
|
+
}
|
|
934
|
+
function normalizeConversationRole(role) {
|
|
935
|
+
const normalized = role.trim().toLowerCase();
|
|
936
|
+
if (normalized === 'assistant' || normalized === 'system' || normalized === 'developer') {
|
|
937
|
+
return normalized;
|
|
938
|
+
}
|
|
939
|
+
return 'user';
|
|
940
|
+
}
|
|
941
|
+
export function shouldSystemProxyRequest(host, path, prefixesByHost) {
|
|
942
|
+
const prefixes = prefixesByHost.get(host);
|
|
943
|
+
if (!prefixes || prefixes.size === 0)
|
|
944
|
+
return false;
|
|
945
|
+
const pathname = normalizeRequestPath(path);
|
|
946
|
+
for (const prefix of prefixes) {
|
|
947
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
function normalizeRequestPath(path) {
|
|
954
|
+
try {
|
|
955
|
+
return new URL(path, 'https://local.invalid').pathname;
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return path.split('?')[0] || '/';
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function formatSystemProxyRoutes(prefixesByHost) {
|
|
962
|
+
const parts = [];
|
|
963
|
+
for (const [host, prefixes] of prefixesByHost.entries()) {
|
|
964
|
+
parts.push(`${host}(${[...prefixes].join(',')})`);
|
|
965
|
+
}
|
|
966
|
+
return parts.join('; ') || '(none)';
|
|
967
|
+
}
|
|
968
|
+
function closeServer(server) {
|
|
969
|
+
return new Promise((resolve) => {
|
|
970
|
+
if (!server) {
|
|
971
|
+
resolve();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
server.close(() => resolve());
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
//# sourceMappingURL=proxy-server.js.map
|