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/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
- if (!this.pending) {
196
- this.pending = this.acquire().finally(() => {
197
- this.pending = void 0;
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
- const pending = this.pending;
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
- if (until > Date.now()) this.cached = { jwt, until };
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) result[name] = value;
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/forward-messages.ts
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 forwardMessages(req, res, context, signal, agent, onUpstreamHeaders) {
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 ?? "/v1/messages", context.config.gatewayOrigin);
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
- response.on("pause", onPause);
550
- response.on("resume", onResume);
551
- response.once("end", onEnd);
552
- response.on("data", resetIdle);
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
- response.off("pause", onPause);
556
- response.off("resume", onResume);
557
- response.off("end", onEnd);
558
- response.off("data", resetIdle);
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(response, res, { signal: transfer.signal });
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
- { agent: "claude-code", path: "/v1/messages", method: "POST", forward: forwardMessages }
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
- if (!route)
675
- return sendError(res, new ProxyError(404, "UNKNOWN_ROUTE", "Route is not supported."));
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 sendError(
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 (raw !== route.path && raw !== `${route.path}?beta=true`) {
684
- return sendError(
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 sendError(
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(req, res, context, controller.signal, route.agent, (headers) => {
711
- upstreamHeaders = headers;
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
- sendError(res, failure);
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: "/v1/messages",
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.2" : "0.1.0-dev";
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("Cannot bind 127.0.0.1:18765; port is occupied or unavailable."));
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: PORT, exclusive: true }, () => {
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: PORT, version })}
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: PORT, close };
1066
+ return { instanceId, host: HOST, port, close };
875
1067
  }
876
1068
  export {
877
1069
  CC_PLACEHOLDER_TOKEN,