devinfra-bridge 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +110 -18
- package/dist/agents/types.d.ts +5 -2
- package/dist/auth/bytedcli-provider.d.ts +7 -0
- package/dist/auth/credential-provider.d.ts +3 -0
- package/dist/daemon/daemon.d.ts +7 -3
- package/dist/daemon/single-instance.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +366 -174
- package/dist/main.js +366 -174
- package/dist/proxy/errors.d.ts +2 -1
- package/dist/proxy/forward-model-request.d.ts +4 -0
- package/dist/proxy/headers.d.ts +3 -2
- package/dist/proxy/jwt-expired.d.ts +6 -0
- package/dist/util/logger.d.ts +6 -2
- package/docs/acceptance.md +14 -0
- package/docs/development.md +22 -10
- package/package.json +2 -2
- package/dist/proxy/forward-messages.d.ts +0 -3
package/dist/index.js
CHANGED
|
@@ -184,6 +184,8 @@ var BytedcliProvider = class {
|
|
|
184
184
|
controller = new AbortController();
|
|
185
185
|
pending;
|
|
186
186
|
cached;
|
|
187
|
+
latestJwt;
|
|
188
|
+
recovery;
|
|
187
189
|
timeoutMs;
|
|
188
190
|
outputBytes;
|
|
189
191
|
cacheMs;
|
|
@@ -191,24 +193,50 @@ var BytedcliProvider = class {
|
|
|
191
193
|
if (this.controller.signal.aborted || signal?.aborted) {
|
|
192
194
|
return Promise.reject(new CredentialError("AUTH_CANCELLED"));
|
|
193
195
|
}
|
|
196
|
+
if (this.pending) return this.wait(this.pending, signal);
|
|
194
197
|
if (this.cached && this.cached.until > Date.now()) return Promise.resolve(this.cached.jwt);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
return this.wait(this.track(this.acquire()), signal);
|
|
199
|
+
}
|
|
200
|
+
refreshJwt(failedJwt, { signal } = {}) {
|
|
201
|
+
if (this.controller.signal.aborted || signal?.aborted) {
|
|
202
|
+
return Promise.reject(new CredentialError("AUTH_CANCELLED"));
|
|
203
|
+
}
|
|
204
|
+
if (this.latestJwt && this.latestJwt !== failedJwt) {
|
|
205
|
+
return this.getJwt({ signal });
|
|
199
206
|
}
|
|
200
|
-
|
|
207
|
+
if (this.recovery?.failedJwt === failedJwt) return this.wait(this.recovery.promise, signal);
|
|
208
|
+
this.cached = void 0;
|
|
209
|
+
const earlier = this.pending;
|
|
210
|
+
const recovery = (async () => {
|
|
211
|
+
await earlier?.catch(() => {
|
|
212
|
+
});
|
|
213
|
+
if (this.controller.signal.aborted) throw new CredentialError("AUTH_CANCELLED");
|
|
214
|
+
if (this.latestJwt && this.latestJwt !== failedJwt) return this.latestJwt;
|
|
215
|
+
return this.acquire(true);
|
|
216
|
+
})();
|
|
217
|
+
this.recovery = { failedJwt, promise: recovery };
|
|
218
|
+
return this.wait(this.track(recovery), signal);
|
|
219
|
+
}
|
|
220
|
+
track(acquisition) {
|
|
221
|
+
const pending = acquisition.finally(() => {
|
|
222
|
+
if (this.pending === pending) this.pending = void 0;
|
|
223
|
+
});
|
|
224
|
+
this.pending = pending;
|
|
225
|
+
return pending;
|
|
226
|
+
}
|
|
227
|
+
wait(pending, signal) {
|
|
201
228
|
return new Promise((resolve, reject) => {
|
|
202
229
|
const onAbort = () => reject(new CredentialError("AUTH_CANCELLED"));
|
|
203
230
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
204
231
|
pending.then(resolve, reject).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
232
|
+
if (signal?.aborted) onAbort();
|
|
205
233
|
});
|
|
206
234
|
}
|
|
207
|
-
async acquire() {
|
|
235
|
+
async acquire(forceRefresh = false) {
|
|
208
236
|
const script = [".js", ".mjs", ".cjs"].includes(extname2(this.executable));
|
|
209
237
|
const stdout = await runCredentialCommand(
|
|
210
238
|
script ? process.execPath : this.executable,
|
|
211
|
-
script ? [this.executable, ...JWT_ARGS] : JWT_ARGS,
|
|
239
|
+
script ? [this.executable, ...JWT_ARGS, ...forceRefresh ? ["--force-refresh"] : []] : [...JWT_ARGS, ...forceRefresh ? ["--force-refresh"] : []],
|
|
212
240
|
{ signal: this.controller.signal, timeoutMs: this.timeoutMs, outputBytes: this.outputBytes }
|
|
213
241
|
);
|
|
214
242
|
let result;
|
|
@@ -227,7 +255,8 @@ var BytedcliProvider = class {
|
|
|
227
255
|
}
|
|
228
256
|
} catch {
|
|
229
257
|
}
|
|
230
|
-
|
|
258
|
+
this.latestJwt = jwt;
|
|
259
|
+
this.cached = until > Date.now() ? { jwt, until } : void 0;
|
|
231
260
|
return jwt;
|
|
232
261
|
}
|
|
233
262
|
async close() {
|
|
@@ -235,6 +264,8 @@ var BytedcliProvider = class {
|
|
|
235
264
|
this.controller.abort();
|
|
236
265
|
await this.pending?.catch(() => {
|
|
237
266
|
});
|
|
267
|
+
this.latestJwt = void 0;
|
|
268
|
+
this.recovery = void 0;
|
|
238
269
|
}
|
|
239
270
|
};
|
|
240
271
|
|
|
@@ -298,36 +329,6 @@ import { randomUUID } from "node:crypto";
|
|
|
298
329
|
import { createServer } from "node:http";
|
|
299
330
|
import { Agent } from "node:https";
|
|
300
331
|
|
|
301
|
-
// src/proxy/forward-messages.ts
|
|
302
|
-
import { request } from "node:https";
|
|
303
|
-
import { pipeline } from "node:stream/promises";
|
|
304
|
-
|
|
305
|
-
// src/proxy/errors.ts
|
|
306
|
-
var ProxyError = class extends Error {
|
|
307
|
-
constructor(status, code, message) {
|
|
308
|
-
super(message);
|
|
309
|
-
this.status = status;
|
|
310
|
-
this.code = code;
|
|
311
|
-
}
|
|
312
|
-
status;
|
|
313
|
-
code;
|
|
314
|
-
};
|
|
315
|
-
function sendError(res, error) {
|
|
316
|
-
if (res.destroyed) return;
|
|
317
|
-
if (res.headersSent) {
|
|
318
|
-
res.destroy();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const type = error.status === 429 ? "rate_limit_error" : error.status === 413 ? "request_too_large" : error.status >= 500 ? "api_error" : "invalid_request_error";
|
|
322
|
-
const body = JSON.stringify({ type: "error", error: { type, message: error.message } });
|
|
323
|
-
res.writeHead(error.status, {
|
|
324
|
-
"content-type": "application/json",
|
|
325
|
-
"content-length": Buffer.byteLength(body),
|
|
326
|
-
connection: "close"
|
|
327
|
-
});
|
|
328
|
-
res.end(body);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
332
|
// src/agents/identity.ts
|
|
332
333
|
var AGENT_IDENTITIES = [
|
|
333
334
|
{ id: "cc", client: /^claude-cli(?:\/|\s|$)/i },
|
|
@@ -355,6 +356,120 @@ function identifyAgent(headers) {
|
|
|
355
356
|
return "x";
|
|
356
357
|
}
|
|
357
358
|
|
|
359
|
+
// src/proxy/forward-model-request.ts
|
|
360
|
+
import { request } from "node:https";
|
|
361
|
+
import { pipeline } from "node:stream/promises";
|
|
362
|
+
|
|
363
|
+
// src/util/logger.ts
|
|
364
|
+
import {
|
|
365
|
+
constants as constants3,
|
|
366
|
+
closeSync,
|
|
367
|
+
existsSync,
|
|
368
|
+
lstatSync,
|
|
369
|
+
openSync,
|
|
370
|
+
renameSync,
|
|
371
|
+
unlinkSync,
|
|
372
|
+
writeSync
|
|
373
|
+
} from "node:fs";
|
|
374
|
+
import { join as join2 } from "node:path";
|
|
375
|
+
var silentLogger = { write() {
|
|
376
|
+
} };
|
|
377
|
+
function safeTraceId(value) {
|
|
378
|
+
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
|
|
379
|
+
}
|
|
380
|
+
function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
381
|
+
const path = join2(stateRoot, "daemon.log");
|
|
382
|
+
const safeFile = (file) => {
|
|
383
|
+
const info = lstatSync(file);
|
|
384
|
+
if (!info.isFile() || info.isSymbolicLink() || process.platform !== "win32" && info.uid !== process.getuid?.())
|
|
385
|
+
throw new Error();
|
|
386
|
+
return info;
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
write(record) {
|
|
390
|
+
try {
|
|
391
|
+
const line = `${JSON.stringify({
|
|
392
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
393
|
+
event: record.event,
|
|
394
|
+
request_id: record.request_id,
|
|
395
|
+
gateway_logid: safeTraceId(record.gateway_logid),
|
|
396
|
+
gateway_request_id: safeTraceId(record.gateway_request_id),
|
|
397
|
+
gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
|
|
398
|
+
path: record.path,
|
|
399
|
+
protocol: record.protocol,
|
|
400
|
+
agent: record.agent,
|
|
401
|
+
duration_ms: record.duration_ms,
|
|
402
|
+
status: record.status,
|
|
403
|
+
code: record.code
|
|
404
|
+
})}
|
|
405
|
+
`;
|
|
406
|
+
if (existsSync(path) && safeFile(path).size + Buffer.byteLength(line) > maxBytes) {
|
|
407
|
+
for (let i = 2; i >= 1; i--) {
|
|
408
|
+
const from = `${path}.${i}`;
|
|
409
|
+
const to = `${path}.${i + 1}`;
|
|
410
|
+
if (existsSync(from)) {
|
|
411
|
+
safeFile(from);
|
|
412
|
+
if (existsSync(to)) {
|
|
413
|
+
safeFile(to);
|
|
414
|
+
unlinkSync(to);
|
|
415
|
+
}
|
|
416
|
+
if (i === 2) unlinkSync(from);
|
|
417
|
+
else renameSync(from, to);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
renameSync(path, `${path}.1`);
|
|
421
|
+
}
|
|
422
|
+
const fd = openSync(
|
|
423
|
+
path,
|
|
424
|
+
constants3.O_WRONLY | constants3.O_APPEND | constants3.O_CREAT | constants3.O_NOFOLLOW,
|
|
425
|
+
384
|
|
426
|
+
);
|
|
427
|
+
try {
|
|
428
|
+
writeSync(fd, line);
|
|
429
|
+
} finally {
|
|
430
|
+
closeSync(fd);
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/proxy/errors.ts
|
|
439
|
+
var ProxyError = class extends Error {
|
|
440
|
+
constructor(status, code, message) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.status = status;
|
|
443
|
+
this.code = code;
|
|
444
|
+
}
|
|
445
|
+
status;
|
|
446
|
+
code;
|
|
447
|
+
};
|
|
448
|
+
function sendError(res, error, protocol = "messages") {
|
|
449
|
+
if (res.destroyed) return;
|
|
450
|
+
if (res.headersSent) {
|
|
451
|
+
res.destroy();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const type = error.status === 429 ? "rate_limit_error" : error.status === 413 ? "request_too_large" : error.status >= 500 ? "api_error" : "invalid_request_error";
|
|
455
|
+
const body = JSON.stringify(
|
|
456
|
+
protocol === "responses" ? {
|
|
457
|
+
error: {
|
|
458
|
+
type: error.status >= 500 ? "server_error" : type,
|
|
459
|
+
message: error.message,
|
|
460
|
+
code: error.code,
|
|
461
|
+
param: null
|
|
462
|
+
}
|
|
463
|
+
} : { type: "error", error: { type, message: error.message } }
|
|
464
|
+
);
|
|
465
|
+
res.writeHead(error.status, {
|
|
466
|
+
"content-type": "application/json",
|
|
467
|
+
"content-length": Buffer.byteLength(body),
|
|
468
|
+
connection: "close"
|
|
469
|
+
});
|
|
470
|
+
res.end(body);
|
|
471
|
+
}
|
|
472
|
+
|
|
358
473
|
// src/proxy/headers.ts
|
|
359
474
|
var HOP = /* @__PURE__ */ new Set([
|
|
360
475
|
"connection",
|
|
@@ -368,14 +483,31 @@ var HOP = /* @__PURE__ */ new Set([
|
|
|
368
483
|
"proxy-connection"
|
|
369
484
|
]);
|
|
370
485
|
var REQUEST = /* @__PURE__ */ new Set([
|
|
371
|
-
"anthropic-version",
|
|
372
|
-
"anthropic-beta",
|
|
373
486
|
"content-type",
|
|
374
487
|
"accept",
|
|
375
488
|
"accept-encoding",
|
|
376
489
|
"x-request-id",
|
|
377
|
-
"request-id"
|
|
490
|
+
"request-id",
|
|
491
|
+
"user-agent"
|
|
378
492
|
]);
|
|
493
|
+
var REQUEST_MESSAGES = /* @__PURE__ */ new Set(["anthropic-version", "anthropic-beta"]);
|
|
494
|
+
var REQUEST_RESPONSES = /* @__PURE__ */ new Set([
|
|
495
|
+
"originator",
|
|
496
|
+
"session-id",
|
|
497
|
+
"thread-id",
|
|
498
|
+
"session_id",
|
|
499
|
+
"conversation_id",
|
|
500
|
+
"x-client-request-id",
|
|
501
|
+
"openai-beta",
|
|
502
|
+
"x-codex-installation-id",
|
|
503
|
+
"x-codex-turn-state",
|
|
504
|
+
"x-codex-turn-metadata",
|
|
505
|
+
"x-codex-parent-thread-id",
|
|
506
|
+
"x-codex-window-id",
|
|
507
|
+
"x-codex-beta-features",
|
|
508
|
+
"x-openai-subagent"
|
|
509
|
+
]);
|
|
510
|
+
var RESPONSE_RESPONSES = /* @__PURE__ */ new Set(["x-codex-turn-state", "openai-model", "x-reasoning-included"]);
|
|
379
511
|
var RESPONSE = /* @__PURE__ */ new Set([
|
|
380
512
|
"x-tt-logid",
|
|
381
513
|
"content-type",
|
|
@@ -391,11 +523,13 @@ function blocked(headers) {
|
|
|
391
523
|
set.add(name.trim().toLowerCase());
|
|
392
524
|
return set;
|
|
393
525
|
}
|
|
394
|
-
function requestHeaders(incoming, jwt, bytes, agent) {
|
|
526
|
+
function requestHeaders(incoming, jwt, bytes, agent, protocol = "messages") {
|
|
395
527
|
const result = {};
|
|
396
528
|
const deny = blocked(incoming);
|
|
529
|
+
const native = protocol === "responses" ? REQUEST_RESPONSES : REQUEST_MESSAGES;
|
|
397
530
|
for (const [name, value] of Object.entries(incoming)) {
|
|
398
|
-
if (REQUEST.has(name) && !deny.has(name) && value !== void 0)
|
|
531
|
+
if ((REQUEST.has(name) || native.has(name)) && !deny.has(name) && value !== void 0)
|
|
532
|
+
result[name] = value;
|
|
399
533
|
}
|
|
400
534
|
result["content-length"] = bytes;
|
|
401
535
|
result["x-identity-token"] = jwt;
|
|
@@ -407,17 +541,81 @@ function requestHeaders(incoming, jwt, bytes, agent) {
|
|
|
407
541
|
result["x-tt-env"] = "ppe_devinfra_bridge";
|
|
408
542
|
return result;
|
|
409
543
|
}
|
|
410
|
-
function responseHeaders(incoming) {
|
|
544
|
+
function responseHeaders(incoming, protocol = "messages") {
|
|
411
545
|
const result = {};
|
|
412
546
|
const deny = blocked(incoming);
|
|
413
547
|
for (const [name, value] of Object.entries(incoming)) {
|
|
414
|
-
if (!deny.has(name) && value !== void 0 && (RESPONSE.has(name) || /^anthropic-ratelimit-[a-z0-9-]+$/.test(name)))
|
|
548
|
+
if (!deny.has(name) && value !== void 0 && (RESPONSE.has(name) || (protocol === "messages" ? /^anthropic-ratelimit-[a-z0-9-]+$/.test(name) : RESPONSE_RESPONSES.has(name) || /^x-ratelimit-[a-z0-9-]+$/.test(name) || /^x-codex-(?:primary|secondary)-(?:used-percent|window-minutes|reset-at)$/.test(name))))
|
|
415
549
|
result[name] = value;
|
|
416
550
|
}
|
|
417
551
|
return result;
|
|
418
552
|
}
|
|
419
553
|
|
|
420
|
-
// src/proxy/
|
|
554
|
+
// src/proxy/jwt-expired.ts
|
|
555
|
+
import { Readable } from "node:stream";
|
|
556
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
557
|
+
var ERROR_BYTES = 64 * 1024;
|
|
558
|
+
async function probeJwtExpired(response, idleMs) {
|
|
559
|
+
const type = String(response.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
560
|
+
const encoding = String(response.headers["content-encoding"] ?? "identity").toLowerCase();
|
|
561
|
+
if (type !== "application/json" && !type?.endsWith("+json") || !["identity", "gzip", "deflate", "br"].includes(encoding)) {
|
|
562
|
+
return { source: response, expired: false };
|
|
563
|
+
}
|
|
564
|
+
const iterator = response.iterator({ destroyOnReturn: false });
|
|
565
|
+
const chunks = [];
|
|
566
|
+
let bytes = 0;
|
|
567
|
+
let timer;
|
|
568
|
+
try {
|
|
569
|
+
while (true) {
|
|
570
|
+
timer = setTimeout(
|
|
571
|
+
() => response.destroy(
|
|
572
|
+
new ProxyError(504, "UPSTREAM_STREAM_TIMEOUT", "Gateway stream timed out.")
|
|
573
|
+
),
|
|
574
|
+
idleMs
|
|
575
|
+
);
|
|
576
|
+
const next = await iterator.next();
|
|
577
|
+
clearTimeout(timer);
|
|
578
|
+
if (next.done) break;
|
|
579
|
+
const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
|
|
580
|
+
chunks.push(chunk);
|
|
581
|
+
bytes += chunk.length;
|
|
582
|
+
if (bytes > ERROR_BYTES) {
|
|
583
|
+
return {
|
|
584
|
+
expired: false,
|
|
585
|
+
source: Readable.from(
|
|
586
|
+
(async function* () {
|
|
587
|
+
try {
|
|
588
|
+
yield* chunks;
|
|
589
|
+
while (true) {
|
|
590
|
+
const next2 = await iterator.next();
|
|
591
|
+
if (next2.done) break;
|
|
592
|
+
yield next2.value;
|
|
593
|
+
}
|
|
594
|
+
} finally {
|
|
595
|
+
await iterator.return?.();
|
|
596
|
+
}
|
|
597
|
+
})()
|
|
598
|
+
)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} finally {
|
|
603
|
+
clearTimeout(timer);
|
|
604
|
+
}
|
|
605
|
+
const body = Buffer.concat(chunks);
|
|
606
|
+
let expired = false;
|
|
607
|
+
try {
|
|
608
|
+
const options = { maxOutputLength: ERROR_BYTES };
|
|
609
|
+
const decoded = encoding === "gzip" ? gunzipSync(body, options) : encoding === "deflate" ? inflateSync(body, options) : encoding === "br" ? brotliDecompressSync(body, options) : encoding === "identity" ? body : void 0;
|
|
610
|
+
if (!decoded) throw new Error();
|
|
611
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
|
|
612
|
+
expired = value?.error?.code === "JWT_EXPIRED";
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
return { source: Readable.from([body]), expired };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/proxy/forward-model-request.ts
|
|
421
619
|
async function readBody(req, context, signal) {
|
|
422
620
|
const { bodyBytes, bodyIdleMs } = context.limits;
|
|
423
621
|
const length = req.headers["content-length"];
|
|
@@ -466,16 +664,8 @@ async function readBody(req, context, signal) {
|
|
|
466
664
|
if (signal.aborted) onAbort();
|
|
467
665
|
});
|
|
468
666
|
}
|
|
469
|
-
async function
|
|
667
|
+
async function forwardModelRequest(req, res, context, signal, agent, protocol, onUpstreamHeaders, requestId) {
|
|
470
668
|
const body = await readBody(req, context, signal);
|
|
471
|
-
try {
|
|
472
|
-
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
|
|
473
|
-
const parsed = JSON.parse(text);
|
|
474
|
-
if (!parsed || Array.isArray(parsed) || typeof parsed.model !== "string" || !parsed.model.trim())
|
|
475
|
-
throw new Error();
|
|
476
|
-
} catch {
|
|
477
|
-
throw new ProxyError(400, "INVALID_BODY", "Expected a JSON object with a nonempty model.");
|
|
478
|
-
}
|
|
479
669
|
let jwt;
|
|
480
670
|
try {
|
|
481
671
|
jwt = await context.credentialProvider.getJwt({ signal });
|
|
@@ -487,6 +677,53 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
|
|
|
487
677
|
);
|
|
488
678
|
}
|
|
489
679
|
if (signal.aborted) return;
|
|
680
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
681
|
+
let nextJwt;
|
|
682
|
+
const retry = await forwardAttempt(
|
|
683
|
+
req,
|
|
684
|
+
res,
|
|
685
|
+
context,
|
|
686
|
+
signal,
|
|
687
|
+
agent,
|
|
688
|
+
protocol,
|
|
689
|
+
body,
|
|
690
|
+
jwt,
|
|
691
|
+
onUpstreamHeaders,
|
|
692
|
+
attempt === 0 && context.credentialProvider.refreshJwt ? async (headers) => {
|
|
693
|
+
const log = (event, code) => context.logger.write({
|
|
694
|
+
event,
|
|
695
|
+
request_id: requestId,
|
|
696
|
+
path: protocol === "messages" ? "/v1/messages" : "/v1/responses",
|
|
697
|
+
protocol,
|
|
698
|
+
agent: identifyAgent(req.headers),
|
|
699
|
+
code,
|
|
700
|
+
gateway_logid: safeTraceId(headers["x-tt-logid"]),
|
|
701
|
+
gateway_request_id: safeTraceId(headers["request-id"]),
|
|
702
|
+
gateway_x_request_id: safeTraceId(headers["x-request-id"])
|
|
703
|
+
});
|
|
704
|
+
log("jwt_refresh_started");
|
|
705
|
+
try {
|
|
706
|
+
nextJwt = await context.credentialProvider.refreshJwt?.(jwt, { signal });
|
|
707
|
+
if (signal.aborted) return false;
|
|
708
|
+
log("jwt_refresh_succeeded");
|
|
709
|
+
log("query_retry", "JWT_EXPIRED");
|
|
710
|
+
return true;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (!signal.aborted)
|
|
713
|
+
log(
|
|
714
|
+
"jwt_refresh_failed",
|
|
715
|
+
error instanceof CredentialError ? error.code : "AUTH_UNAVAILABLE"
|
|
716
|
+
);
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
} : void 0
|
|
720
|
+
);
|
|
721
|
+
if (!retry || !nextJwt || signal.aborted) return;
|
|
722
|
+
jwt = nextJwt;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function forwardAttempt(req, res, context, signal, agent, protocol, body, jwt, onUpstreamHeaders, recover) {
|
|
726
|
+
if (signal.aborted) return false;
|
|
490
727
|
const transfer = new AbortController();
|
|
491
728
|
const abort = () => transfer.abort(signal.reason);
|
|
492
729
|
signal.addEventListener("abort", abort, { once: true });
|
|
@@ -494,10 +731,10 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
|
|
|
494
731
|
let idleTimer;
|
|
495
732
|
let cleanupStreamTimer;
|
|
496
733
|
let upstreamResponse;
|
|
497
|
-
const target = new URL(req.url ?? "/
|
|
734
|
+
const target = new URL(req.url ?? "/", context.config.gatewayOrigin);
|
|
498
735
|
const upstream = request(target, {
|
|
499
736
|
method: "POST",
|
|
500
|
-
headers: requestHeaders(req.headers, jwt, body.length, agent),
|
|
737
|
+
headers: requestHeaders(req.headers, jwt, body.length, agent, protocol),
|
|
501
738
|
agent: context.httpsAgent,
|
|
502
739
|
signal: transfer.signal
|
|
503
740
|
});
|
|
@@ -520,6 +757,14 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
|
|
|
520
757
|
response.destroy();
|
|
521
758
|
throw new ProxyError(502, "UPSTREAM_REDIRECT", "Gateway redirection is not supported.");
|
|
522
759
|
}
|
|
760
|
+
let source = response;
|
|
761
|
+
if (response.statusCode === 401 && recover) {
|
|
762
|
+
const inspected = await probeJwtExpired(response, context.limits.streamIdleMs);
|
|
763
|
+
if (signal.aborted) return false;
|
|
764
|
+
if (inspected.expired && await recover(response.headers)) return true;
|
|
765
|
+
if (signal.aborted) return false;
|
|
766
|
+
source = inspected.source;
|
|
767
|
+
}
|
|
523
768
|
let backpressured = false;
|
|
524
769
|
let streamEnded = false;
|
|
525
770
|
const resetIdle = () => {
|
|
@@ -546,25 +791,26 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
|
|
|
546
791
|
streamEnded = true;
|
|
547
792
|
clearTimeout(idleTimer);
|
|
548
793
|
};
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
794
|
+
source.on("pause", onPause);
|
|
795
|
+
source.on("resume", onResume);
|
|
796
|
+
source.once("end", onEnd);
|
|
797
|
+
source.on("data", resetIdle);
|
|
553
798
|
cleanupStreamTimer = () => {
|
|
554
799
|
clearTimeout(idleTimer);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
800
|
+
source.off("pause", onPause);
|
|
801
|
+
source.off("resume", onResume);
|
|
802
|
+
source.off("end", onEnd);
|
|
803
|
+
source.off("data", resetIdle);
|
|
559
804
|
};
|
|
560
805
|
resetIdle();
|
|
561
|
-
res.writeHead(response.statusCode ?? 502, responseHeaders(response.headers));
|
|
806
|
+
res.writeHead(response.statusCode ?? 502, responseHeaders(response.headers, protocol));
|
|
562
807
|
res.flushHeaders();
|
|
563
|
-
await pipeline(
|
|
808
|
+
await pipeline(source, res, { signal: transfer.signal });
|
|
809
|
+
return false;
|
|
564
810
|
} catch (error) {
|
|
565
811
|
if (transfer.signal.reason instanceof ProxyError) throw transfer.signal.reason;
|
|
566
812
|
if (error instanceof ProxyError) throw error;
|
|
567
|
-
if (signal.aborted) return;
|
|
813
|
+
if (signal.aborted) return false;
|
|
568
814
|
throw new ProxyError(502, "UPSTREAM_CONNECTION", "Gateway connection failed.");
|
|
569
815
|
} finally {
|
|
570
816
|
clearTimeout(headerTimer);
|
|
@@ -578,82 +824,24 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
|
|
|
578
824
|
|
|
579
825
|
// src/agents/registry.ts
|
|
580
826
|
var AGENT_ROUTES = Object.freeze([
|
|
581
|
-
{
|
|
827
|
+
{
|
|
828
|
+
agent: "claude-code",
|
|
829
|
+
protocol: "messages",
|
|
830
|
+
path: "/v1/messages",
|
|
831
|
+
method: "POST",
|
|
832
|
+
allowedQueries: ["", "?beta=true"],
|
|
833
|
+
forward: forwardModelRequest
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
agent: "codex",
|
|
837
|
+
protocol: "responses",
|
|
838
|
+
path: "/v1/responses",
|
|
839
|
+
method: "POST",
|
|
840
|
+
allowedQueries: [""],
|
|
841
|
+
forward: forwardModelRequest
|
|
842
|
+
}
|
|
582
843
|
]);
|
|
583
844
|
|
|
584
|
-
// src/util/logger.ts
|
|
585
|
-
import {
|
|
586
|
-
constants as constants3,
|
|
587
|
-
closeSync,
|
|
588
|
-
existsSync,
|
|
589
|
-
lstatSync,
|
|
590
|
-
openSync,
|
|
591
|
-
renameSync,
|
|
592
|
-
unlinkSync,
|
|
593
|
-
writeSync
|
|
594
|
-
} from "node:fs";
|
|
595
|
-
import { join as join2 } from "node:path";
|
|
596
|
-
var silentLogger = { write() {
|
|
597
|
-
} };
|
|
598
|
-
function safeTraceId(value) {
|
|
599
|
-
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
|
|
600
|
-
}
|
|
601
|
-
function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
|
|
602
|
-
const path = join2(stateRoot, "daemon.log");
|
|
603
|
-
const safeFile = (file) => {
|
|
604
|
-
const info = lstatSync(file);
|
|
605
|
-
if (!info.isFile() || info.isSymbolicLink() || process.platform !== "win32" && info.uid !== process.getuid?.())
|
|
606
|
-
throw new Error();
|
|
607
|
-
return info;
|
|
608
|
-
};
|
|
609
|
-
return {
|
|
610
|
-
write(record) {
|
|
611
|
-
try {
|
|
612
|
-
const line = `${JSON.stringify({
|
|
613
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
614
|
-
event: record.event,
|
|
615
|
-
request_id: record.request_id,
|
|
616
|
-
gateway_logid: safeTraceId(record.gateway_logid),
|
|
617
|
-
gateway_request_id: safeTraceId(record.gateway_request_id),
|
|
618
|
-
gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
|
|
619
|
-
path: record.path,
|
|
620
|
-
duration_ms: record.duration_ms,
|
|
621
|
-
status: record.status,
|
|
622
|
-
code: record.code
|
|
623
|
-
})}
|
|
624
|
-
`;
|
|
625
|
-
if (existsSync(path) && safeFile(path).size + Buffer.byteLength(line) > maxBytes) {
|
|
626
|
-
for (let i = 2; i >= 1; i--) {
|
|
627
|
-
const from = `${path}.${i}`;
|
|
628
|
-
const to = `${path}.${i + 1}`;
|
|
629
|
-
if (existsSync(from)) {
|
|
630
|
-
safeFile(from);
|
|
631
|
-
if (existsSync(to)) {
|
|
632
|
-
safeFile(to);
|
|
633
|
-
unlinkSync(to);
|
|
634
|
-
}
|
|
635
|
-
if (i === 2) unlinkSync(from);
|
|
636
|
-
else renameSync(from, to);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
renameSync(path, `${path}.1`);
|
|
640
|
-
}
|
|
641
|
-
const fd = openSync(
|
|
642
|
-
path,
|
|
643
|
-
constants3.O_WRONLY | constants3.O_APPEND | constants3.O_CREAT | constants3.O_NOFOLLOW,
|
|
644
|
-
384
|
|
645
|
-
);
|
|
646
|
-
try {
|
|
647
|
-
writeSync(fd, line);
|
|
648
|
-
} finally {
|
|
649
|
-
closeSync(fd);
|
|
650
|
-
}
|
|
651
|
-
} catch {
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
845
|
// src/proxy/server.ts
|
|
658
846
|
function createProxyContext(config, credentialProvider, options = {}) {
|
|
659
847
|
return {
|
|
@@ -671,28 +859,19 @@ function createProxyHandler(context) {
|
|
|
671
859
|
void (async () => {
|
|
672
860
|
const raw = req.url ?? "";
|
|
673
861
|
const route = AGENT_ROUTES.find((item) => item.path === raw.split("?")[0]);
|
|
674
|
-
|
|
675
|
-
|
|
862
|
+
const protocol = route?.protocol ?? (raw.split("?")[0]?.startsWith("/v1/responses") ? "responses" : "messages");
|
|
863
|
+
const fail2 = (error) => sendError(res, error, protocol);
|
|
864
|
+
if (!route) return fail2(new ProxyError(404, "UNKNOWN_ROUTE", "Route is not supported."));
|
|
676
865
|
if (req.method !== route.method) {
|
|
677
866
|
res.setHeader("allow", route.method);
|
|
678
|
-
return
|
|
679
|
-
res,
|
|
680
|
-
new ProxyError(405, "METHOD_NOT_ALLOWED", "Method is not supported.")
|
|
681
|
-
);
|
|
867
|
+
return fail2(new ProxyError(405, "METHOD_NOT_ALLOWED", "Method is not supported."));
|
|
682
868
|
}
|
|
683
|
-
if (
|
|
684
|
-
return
|
|
685
|
-
res,
|
|
686
|
-
new ProxyError(400, "INVALID_QUERY", "Query parameters are not supported.")
|
|
687
|
-
);
|
|
869
|
+
if (!route.allowedQueries.some((query) => raw === `${route.path}${query}`)) {
|
|
870
|
+
return fail2(new ProxyError(400, "INVALID_QUERY", "Query parameters are not supported."));
|
|
688
871
|
}
|
|
689
|
-
if (context.closing)
|
|
690
|
-
return sendError(res, new ProxyError(503, "SHUTTING_DOWN", "Daemon is stopping."));
|
|
872
|
+
if (context.closing) return fail2(new ProxyError(503, "SHUTTING_DOWN", "Daemon is stopping."));
|
|
691
873
|
if (context.active.size >= context.limits.concurrency) {
|
|
692
|
-
return
|
|
693
|
-
res,
|
|
694
|
-
new ProxyError(429, "CONCURRENCY_LIMIT", "Too many active requests.")
|
|
695
|
-
);
|
|
874
|
+
return fail2(new ProxyError(429, "CONCURRENCY_LIMIT", "Too many active requests."));
|
|
696
875
|
}
|
|
697
876
|
const controller = new AbortController();
|
|
698
877
|
context.active.add(controller);
|
|
@@ -707,13 +886,22 @@ function createProxyHandler(context) {
|
|
|
707
886
|
req.once("aborted", onAbort);
|
|
708
887
|
res.once("close", onClose);
|
|
709
888
|
try {
|
|
710
|
-
await route.forward(
|
|
711
|
-
|
|
712
|
-
|
|
889
|
+
await route.forward(
|
|
890
|
+
req,
|
|
891
|
+
res,
|
|
892
|
+
context,
|
|
893
|
+
controller.signal,
|
|
894
|
+
route.agent,
|
|
895
|
+
route.protocol,
|
|
896
|
+
(headers) => {
|
|
897
|
+
upstreamHeaders = headers;
|
|
898
|
+
},
|
|
899
|
+
requestId
|
|
900
|
+
);
|
|
713
901
|
} catch (error) {
|
|
714
902
|
const failure = error instanceof ProxyError ? error : new ProxyError(502, "PROXY_FAILURE", "Request forwarding failed.");
|
|
715
903
|
code = failure.code;
|
|
716
|
-
|
|
904
|
+
fail2(failure);
|
|
717
905
|
} finally {
|
|
718
906
|
req.off("aborted", onAbort);
|
|
719
907
|
res.off("close", onClose);
|
|
@@ -724,7 +912,9 @@ function createProxyHandler(context) {
|
|
|
724
912
|
gateway_logid: safeTraceId(upstreamHeaders?.["x-tt-logid"]),
|
|
725
913
|
gateway_request_id: safeTraceId(upstreamHeaders?.["request-id"]),
|
|
726
914
|
gateway_x_request_id: safeTraceId(upstreamHeaders?.["x-request-id"]),
|
|
727
|
-
path:
|
|
915
|
+
path: route.path,
|
|
916
|
+
protocol: route.protocol,
|
|
917
|
+
agent: identifyAgent(req.headers),
|
|
728
918
|
duration_ms: Date.now() - started,
|
|
729
919
|
status: res.statusCode,
|
|
730
920
|
code
|
|
@@ -771,29 +961,30 @@ async function prepareStateRoot(path) {
|
|
|
771
961
|
}
|
|
772
962
|
|
|
773
963
|
// src/version.ts
|
|
774
|
-
var VERSION = true ? "0.1.0-alpha.
|
|
964
|
+
var VERSION = true ? "0.1.0-alpha.3" : "0.1.0-dev";
|
|
775
965
|
|
|
776
966
|
// src/daemon/single-instance.ts
|
|
777
967
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
778
968
|
import { readFile as readFile2, rename, unlink, writeFile } from "node:fs/promises";
|
|
779
969
|
import { join as join3 } from "node:path";
|
|
780
|
-
async function bindSingleInstance(server) {
|
|
970
|
+
async function bindSingleInstance(server, port = PORT) {
|
|
781
971
|
await new Promise((resolve, reject) => {
|
|
782
|
-
const onError = () => reject(new Error(
|
|
972
|
+
const onError = () => reject(new Error(`Cannot bind ${HOST}:${port}; port is occupied or unavailable.`));
|
|
783
973
|
server.once("error", onError);
|
|
784
|
-
server.listen({ host: HOST, port
|
|
974
|
+
server.listen({ host: HOST, port, exclusive: true }, () => {
|
|
785
975
|
server.off("error", onError);
|
|
786
976
|
resolve();
|
|
787
977
|
});
|
|
788
978
|
});
|
|
979
|
+
return server.address().port;
|
|
789
980
|
}
|
|
790
|
-
async function writeInstance(stateRoot, id, version) {
|
|
981
|
+
async function writeInstance(stateRoot, id, version, port = PORT) {
|
|
791
982
|
const path = join3(stateRoot, "instance.json");
|
|
792
983
|
const temp = join3(stateRoot, `.instance-${randomUUID2()}.tmp`);
|
|
793
984
|
try {
|
|
794
985
|
await writeFile(
|
|
795
986
|
temp,
|
|
796
|
-
`${JSON.stringify({ instance_id: id, pid: process.pid, host: HOST, port
|
|
987
|
+
`${JSON.stringify({ instance_id: id, pid: process.pid, host: HOST, port, version })}
|
|
797
988
|
`,
|
|
798
989
|
{ flag: "wx", mode: 384 }
|
|
799
990
|
);
|
|
@@ -811,7 +1002,7 @@ async function writeInstance(stateRoot, id, version) {
|
|
|
811
1002
|
}
|
|
812
1003
|
|
|
813
1004
|
// src/daemon/daemon.ts
|
|
814
|
-
async function startDaemon(config) {
|
|
1005
|
+
async function startDaemon(config, { port: requestedPort = PORT } = {}) {
|
|
815
1006
|
const stateRoot = await prepareStateRoot(config.stateRoot);
|
|
816
1007
|
const logger = createLogger(stateRoot);
|
|
817
1008
|
const provider = createCredentialProvider(config.auth);
|
|
@@ -824,9 +1015,10 @@ async function startDaemon(config) {
|
|
|
824
1015
|
});
|
|
825
1016
|
const instanceId = randomUUID3();
|
|
826
1017
|
let cleanup;
|
|
1018
|
+
let port;
|
|
827
1019
|
try {
|
|
828
|
-
await bindSingleInstance(server);
|
|
829
|
-
cleanup = await writeInstance(stateRoot, instanceId, VERSION);
|
|
1020
|
+
port = await bindSingleInstance(server, requestedPort);
|
|
1021
|
+
cleanup = await writeInstance(stateRoot, instanceId, VERSION, port);
|
|
830
1022
|
} catch (error) {
|
|
831
1023
|
context.closing = true;
|
|
832
1024
|
for (const controller of context.active) controller.abort();
|
|
@@ -871,7 +1063,7 @@ async function startDaemon(config) {
|
|
|
871
1063
|
process.on("SIGINT", onSignal);
|
|
872
1064
|
process.on("SIGTERM", onSignal);
|
|
873
1065
|
logger.write({ event: "ready" });
|
|
874
|
-
return { instanceId, host: HOST, port
|
|
1066
|
+
return { instanceId, host: HOST, port, close };
|
|
875
1067
|
}
|
|
876
1068
|
export {
|
|
877
1069
|
CC_PLACEHOLDER_TOKEN,
|