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