skalpel 2.0.21 → 2.0.23

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
@@ -203,6 +203,37 @@ var init_fetch_error = __esm({
203
203
  }
204
204
  });
205
205
 
206
+ // src/proxy/trace-context.ts
207
+ import { randomBytes } from "crypto";
208
+ function parseTraceparent(header) {
209
+ if (!header) return null;
210
+ const trimmed = header.trim().toLowerCase();
211
+ const match = trimmed.match(TRACEPARENT_REGEX);
212
+ if (!match) return null;
213
+ const [, traceId, spanId, traceFlags] = match;
214
+ if (traceId === "00000000000000000000000000000000") return null;
215
+ if (spanId === "0000000000000000") return null;
216
+ return { traceId, spanId, traceFlags };
217
+ }
218
+ function generateTraceContext() {
219
+ return {
220
+ traceId: randomBytes(16).toString("hex"),
221
+ spanId: randomBytes(8).toString("hex"),
222
+ traceFlags: "01"
223
+ // sampled
224
+ };
225
+ }
226
+ function formatTraceparent(ctx) {
227
+ return `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags}`;
228
+ }
229
+ var TRACEPARENT_REGEX;
230
+ var init_trace_context = __esm({
231
+ "src/proxy/trace-context.ts"() {
232
+ "use strict";
233
+ TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
234
+ }
235
+ });
236
+
206
237
  // src/proxy/streaming.ts
207
238
  function parseRetryAfter(header) {
208
239
  if (!header) return void 0;
@@ -229,7 +260,7 @@ function stripSkalpelHeaders(headers) {
229
260
  async function doStreamingFetch(url, body, headers) {
230
261
  return fetch(url, { method: "POST", headers, body, dispatcher: skalpelDispatcher });
231
262
  }
232
- async function handleStreamingRequest(_req, res, _config, _source, body, skalpelUrl, directUrl, useSkalpel, forwardHeaders, logger) {
263
+ async function handleStreamingRequest(_req, res, _config, _source, body, skalpelUrl, directUrl, useSkalpel, forwardHeaders, logger, traceCtx) {
233
264
  let response = null;
234
265
  let fetchError = null;
235
266
  let usedFallback = false;
@@ -350,6 +381,9 @@ data: ${JSON.stringify(envelope)}
350
381
  }
351
382
  sseHeaders["Content-Type"] = "text/event-stream";
352
383
  sseHeaders["Cache-Control"] = "no-cache";
384
+ if (traceCtx) {
385
+ sseHeaders["traceparent"] = formatTraceparent(traceCtx);
386
+ }
353
387
  res.writeHead(response.status, sseHeaders);
354
388
  if (!response.body) {
355
389
  res.write(`event: error
@@ -401,6 +435,7 @@ var init_streaming = __esm({
401
435
  init_envelope();
402
436
  init_recovery();
403
437
  init_fetch_error();
438
+ init_trace_context();
404
439
  TIMEOUT_CODES2 = /* @__PURE__ */ new Set(["ETIMEDOUT", "TIMEOUT", "UND_ERR_HEADERS_TIMEOUT"]);
405
440
  HTTP_BAD_GATEWAY = 502;
406
441
  HOP_BY_HOP = /* @__PURE__ */ new Set([
@@ -505,6 +540,7 @@ var init_ws_client = __esm({
505
540
  "src/proxy/ws-client.ts"() {
506
541
  "use strict";
507
542
  init_codex_oauth();
543
+ init_trace_context();
508
544
  WS_SUBPROTOCOL = "skalpel-codex-v1";
509
545
  MAX_RECONNECTS = 5;
510
546
  MAX_BACKOFF_MS = 6e4;
@@ -528,13 +564,17 @@ var init_ws_client = __esm({
528
564
  const freshToken = getFreshAccessToken();
529
565
  const bearer = freshToken ?? this.opts.oauthToken;
530
566
  return new Promise((resolve, reject) => {
567
+ const headers = {
568
+ "X-Skalpel-API-Key": this.opts.apiKey,
569
+ Authorization: `Bearer ${bearer}`,
570
+ "x-skalpel-source": this.opts.source
571
+ };
572
+ if (this.opts.traceCtx) {
573
+ headers["traceparent"] = formatTraceparent(this.opts.traceCtx);
574
+ }
531
575
  const ws = new WebSocket(this.opts.url, [WS_SUBPROTOCOL], {
532
576
  agent: pickAgent(this.opts.url),
533
- headers: {
534
- "X-Skalpel-API-Key": this.opts.apiKey,
535
- Authorization: `Bearer ${bearer}`,
536
- "x-skalpel-source": this.opts.source
537
- }
577
+ headers
538
578
  });
539
579
  this.ws = ws;
540
580
  ws.once("unexpected-response", (_req, res) => {
@@ -689,13 +729,16 @@ async function isSkalpelBackendFailure(response, err, logger) {
689
729
  return true;
690
730
  }
691
731
  }
692
- function buildForwardHeaders(req, config, source, useSkalpel) {
732
+ function buildForwardHeaders(req, config, source, useSkalpel, traceCtx) {
693
733
  const forwardHeaders = {};
694
734
  for (const [key, value] of Object.entries(req.headers)) {
695
735
  if (value === void 0) continue;
696
736
  if (FORWARD_HEADER_STRIP.has(key.toLowerCase())) continue;
697
737
  forwardHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
698
738
  }
739
+ if (traceCtx) {
740
+ forwardHeaders["traceparent"] = formatTraceparent(traceCtx);
741
+ }
699
742
  if (useSkalpel) {
700
743
  forwardHeaders["X-Skalpel-API-Key"] = config.apiKey;
701
744
  forwardHeaders["X-Skalpel-Source"] = source;
@@ -760,9 +803,13 @@ async function handleRequest(req, res, config, source, logger) {
760
803
  try {
761
804
  const body = await collectBody(req);
762
805
  logger.info(`body collected bytes=${body.length}`);
806
+ const inboundTraceparent = req.headers["traceparent"];
807
+ const traceCtx = parseTraceparent(
808
+ typeof inboundTraceparent === "string" ? inboundTraceparent : null
809
+ ) ?? generateTraceContext();
763
810
  const useSkalpel = shouldRouteToSkalpel(path4, source);
764
- logger.info(`routing useSkalpel=${useSkalpel}`);
765
- const forwardHeaders = buildForwardHeaders(req, config, source, useSkalpel);
811
+ logger.info(`routing useSkalpel=${useSkalpel} traceId=${traceCtx.traceId}`);
812
+ const forwardHeaders = buildForwardHeaders(req, config, source, useSkalpel, traceCtx);
766
813
  logger.debug(`headers built skalpelHeaders=${useSkalpel} authConverted=${!forwardHeaders["authorization"] && !!forwardHeaders["x-api-key"]}`);
767
814
  let isStreaming = false;
768
815
  if (body) {
@@ -776,7 +823,7 @@ async function handleRequest(req, res, config, source, logger) {
776
823
  if (isStreaming) {
777
824
  const skalpelUrl2 = `${config.remoteBaseUrl}${path4}`;
778
825
  const directUrl2 = source === "claude-code" ? `${config.anthropicDirectUrl}${path4}` : source === "cursor" ? `${config.cursorDirectUrl}${path4}` : `${config.openaiDirectUrl}${path4}`;
779
- await handleStreamingRequest(req, res, config, source, body, skalpelUrl2, directUrl2, useSkalpel, forwardHeaders, logger);
826
+ await handleStreamingRequest(req, res, config, source, body, skalpelUrl2, directUrl2, useSkalpel, forwardHeaders, logger, traceCtx);
780
827
  logger.info(`${method} ${path4} source=${source} streaming latency=${Date.now() - start}ms`);
781
828
  return;
782
829
  }
@@ -871,6 +918,7 @@ async function handleRequest(req, res, config, source, logger) {
871
918
  const responseHeaders = extractResponseHeaders(response);
872
919
  const responseBody = Buffer.from(await response.arrayBuffer());
873
920
  responseHeaders["content-length"] = String(responseBody.length);
921
+ responseHeaders["traceparent"] = formatTraceparent(traceCtx);
874
922
  logger.info(`response forwarding status=${response.status} bodyBytes=${responseBody.length}`);
875
923
  res.writeHead(response.status, responseHeaders);
876
924
  res.end(responseBody);
@@ -1068,7 +1116,8 @@ async function fallbackToHttp(clientWs, config, source, logger, requestBody, inb
1068
1116
  Authorization: authHeader,
1069
1117
  Accept: "text/event-stream"
1070
1118
  },
1071
- body: requestBody
1119
+ body: requestBody,
1120
+ dispatcher: skalpelDispatcher
1072
1121
  });
1073
1122
  if (!resp.ok) {
1074
1123
  let errorBody = "";
@@ -1156,6 +1205,7 @@ var init_handler = __esm({
1156
1205
  init_codex_oauth();
1157
1206
  init_recovery();
1158
1207
  init_fetch_error();
1208
+ init_trace_context();
1159
1209
  TIMEOUT_CODES3 = /* @__PURE__ */ new Set(["ETIMEDOUT", "TIMEOUT", "UND_ERR_HEADERS_TIMEOUT"]);
1160
1210
  HTTP_BAD_GATEWAY2 = 502;
1161
1211
  SKALPEL_EXACT_PATHS = /* @__PURE__ */ new Set(["/v1/messages"]);
@@ -1855,10 +1905,12 @@ var Logger = class _Logger {
1855
1905
  this.log("error", msg);
1856
1906
  }
1857
1907
  /** Returns a new Logger that writes to the same file but prefixes every
1858
- * emitted line with `[conn=<connId>] `. The parent logger continues to
1859
- * work unchanged. IPv6 colons should already be sanitized by the caller. */
1860
- child(connId) {
1861
- const child = new _Logger(this.logFile, this.level, `[conn=${connId}] `);
1908
+ * emitted line with `[conn=<connId>] ` or `[conn=<connId> trace=<traceId>] `.
1909
+ * The parent logger continues to work unchanged. IPv6 colons should already
1910
+ * be sanitized by the caller. */
1911
+ child(connId, traceId) {
1912
+ const prefix = traceId ? `[conn=${connId} trace=${traceId}] ` : `[conn=${connId}] `;
1913
+ const child = new _Logger(this.logFile, this.level, prefix);
1862
1914
  return child;
1863
1915
  }
1864
1916
  log(level, msg) {