skalpel 2.0.6 → 2.0.7

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.cjs CHANGED
@@ -509,42 +509,84 @@ async function createSkalpelAnthropic(options) {
509
509
  var import_node_http = __toESM(require("http"), 1);
510
510
 
511
511
  // src/proxy/streaming.ts
512
- async function handleStreamingRequest(_req, res, _config, _source, body, forwardUrl, forwardHeaders, logger) {
513
- const HOP_BY_HOP = /* @__PURE__ */ new Set([
514
- "connection",
515
- "keep-alive",
516
- "proxy-authenticate",
517
- "proxy-authorization",
518
- "te",
519
- "trailer",
520
- "transfer-encoding",
521
- "upgrade"
522
- ]);
523
- let response;
524
- try {
525
- response = await fetch(forwardUrl, {
526
- method: "POST",
527
- headers: forwardHeaders,
528
- body
529
- });
530
- } catch (err) {
531
- logger.error(`streaming fetch failed: ${err.message}`);
512
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
513
+ "connection",
514
+ "keep-alive",
515
+ "proxy-authenticate",
516
+ "proxy-authorization",
517
+ "te",
518
+ "trailer",
519
+ "transfer-encoding",
520
+ "upgrade"
521
+ ]);
522
+ var STRIP_HEADERS = /* @__PURE__ */ new Set([
523
+ ...HOP_BY_HOP,
524
+ "content-encoding",
525
+ "content-length"
526
+ ]);
527
+ function stripSkalpelHeaders(headers) {
528
+ const cleaned = { ...headers };
529
+ delete cleaned["X-Skalpel-API-Key"];
530
+ delete cleaned["X-Skalpel-Source"];
531
+ delete cleaned["X-Skalpel-Agent-Type"];
532
+ delete cleaned["X-Skalpel-SDK-Version"];
533
+ return cleaned;
534
+ }
535
+ function isSkalpelBackendFailure(response, err) {
536
+ if (err) return true;
537
+ if (!response) return true;
538
+ if (response.status >= 500) return true;
539
+ return false;
540
+ }
541
+ async function doStreamingFetch(url, body, headers) {
542
+ return fetch(url, { method: "POST", headers, body });
543
+ }
544
+ async function handleStreamingRequest(_req, res, _config, _source, body, skalpelUrl, directUrl, useSkalpel, forwardHeaders, logger) {
545
+ let response = null;
546
+ let fetchError = null;
547
+ let usedFallback = false;
548
+ if (useSkalpel) {
549
+ try {
550
+ response = await doStreamingFetch(skalpelUrl, body, forwardHeaders);
551
+ } catch (err) {
552
+ fetchError = err;
553
+ }
554
+ if (isSkalpelBackendFailure(response, fetchError)) {
555
+ logger.warn(`streaming: Skalpel backend failed (${fetchError ? fetchError.message : `status ${response?.status}`}), falling back to direct Anthropic API`);
556
+ usedFallback = true;
557
+ response = null;
558
+ fetchError = null;
559
+ const directHeaders = stripSkalpelHeaders(forwardHeaders);
560
+ try {
561
+ response = await doStreamingFetch(directUrl, body, directHeaders);
562
+ } catch (err) {
563
+ fetchError = err;
564
+ }
565
+ }
566
+ } else {
567
+ try {
568
+ response = await doStreamingFetch(directUrl, body, forwardHeaders);
569
+ } catch (err) {
570
+ fetchError = err;
571
+ }
572
+ }
573
+ if (!response || fetchError) {
574
+ const errMsg = fetchError ? fetchError.message : "no response from upstream";
575
+ logger.error(`streaming fetch failed: ${errMsg}`);
532
576
  res.writeHead(502, {
533
577
  "Content-Type": "text/event-stream",
534
578
  "Cache-Control": "no-cache"
535
579
  });
536
580
  res.write(`event: error
537
- data: ${JSON.stringify({ error: err.message })}
581
+ data: ${JSON.stringify({ error: errMsg })}
538
582
 
539
583
  `);
540
584
  res.end();
541
585
  return;
542
586
  }
543
- const STRIP_HEADERS = /* @__PURE__ */ new Set([
544
- ...HOP_BY_HOP,
545
- "content-encoding",
546
- "content-length"
547
- ]);
587
+ if (usedFallback) {
588
+ logger.info("streaming: using direct Anthropic API fallback");
589
+ }
548
590
  if (response.status >= 300) {
549
591
  const errorBody = Buffer.from(await response.arrayBuffer());
550
592
  logger.error(`streaming upstream error: status=${response.status} body=${errorBody.toString().slice(0, 500)}`);
@@ -610,6 +652,67 @@ function shouldRouteToSkalpel(path4, source) {
610
652
  const pathname = path4.split("?")[0];
611
653
  return SKALPEL_EXACT_PATHS.has(pathname);
612
654
  }
655
+ function isSkalpelBackendFailure2(response, err) {
656
+ if (err) return true;
657
+ if (!response) return true;
658
+ if (response.status >= 500) return true;
659
+ return false;
660
+ }
661
+ function buildForwardHeaders(req, config, source, useSkalpel) {
662
+ const forwardHeaders = {};
663
+ for (const [key, value] of Object.entries(req.headers)) {
664
+ if (value !== void 0) {
665
+ forwardHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
666
+ }
667
+ }
668
+ delete forwardHeaders["host"];
669
+ delete forwardHeaders["connection"];
670
+ if (useSkalpel) {
671
+ forwardHeaders["X-Skalpel-API-Key"] = config.apiKey;
672
+ forwardHeaders["X-Skalpel-Source"] = source;
673
+ forwardHeaders["X-Skalpel-Agent-Type"] = source;
674
+ forwardHeaders["X-Skalpel-SDK-Version"] = "proxy-1.0.0";
675
+ if (source === "claude-code" && !forwardHeaders["x-api-key"]) {
676
+ const authHeader = forwardHeaders["authorization"] ?? "";
677
+ if (authHeader.toLowerCase().startsWith("bearer ")) {
678
+ const token = authHeader.slice(7).trim();
679
+ if (token.startsWith("sk-ant-")) {
680
+ forwardHeaders["x-api-key"] = token;
681
+ }
682
+ }
683
+ }
684
+ }
685
+ return forwardHeaders;
686
+ }
687
+ function stripSkalpelHeaders2(headers) {
688
+ const cleaned = { ...headers };
689
+ delete cleaned["X-Skalpel-API-Key"];
690
+ delete cleaned["X-Skalpel-Source"];
691
+ delete cleaned["X-Skalpel-Agent-Type"];
692
+ delete cleaned["X-Skalpel-SDK-Version"];
693
+ return cleaned;
694
+ }
695
+ var STRIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
696
+ "connection",
697
+ "keep-alive",
698
+ "proxy-authenticate",
699
+ "proxy-authorization",
700
+ "te",
701
+ "trailer",
702
+ "transfer-encoding",
703
+ "upgrade",
704
+ "content-encoding",
705
+ "content-length"
706
+ ]);
707
+ function extractResponseHeaders(response) {
708
+ const headers = {};
709
+ for (const [key, value] of response.headers.entries()) {
710
+ if (!STRIP_RESPONSE_HEADERS.has(key)) {
711
+ headers[key] = value;
712
+ }
713
+ }
714
+ return headers;
715
+ }
613
716
  async function handleRequest(req, res, config, source, logger) {
614
717
  const start = Date.now();
615
718
  const method = req.method ?? "GET";
@@ -617,30 +720,7 @@ async function handleRequest(req, res, config, source, logger) {
617
720
  try {
618
721
  const body = await collectBody(req);
619
722
  const useSkalpel = shouldRouteToSkalpel(path4, source);
620
- const forwardUrl = `${useSkalpel ? config.remoteBaseUrl : config.anthropicDirectUrl}${path4}`;
621
- const forwardHeaders = {};
622
- for (const [key, value] of Object.entries(req.headers)) {
623
- if (value !== void 0) {
624
- forwardHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
625
- }
626
- }
627
- delete forwardHeaders["host"];
628
- delete forwardHeaders["connection"];
629
- if (useSkalpel) {
630
- forwardHeaders["X-Skalpel-API-Key"] = config.apiKey;
631
- forwardHeaders["X-Skalpel-Source"] = source;
632
- forwardHeaders["X-Skalpel-Agent-Type"] = source;
633
- forwardHeaders["X-Skalpel-SDK-Version"] = "proxy-1.0.0";
634
- if (source === "claude-code" && !forwardHeaders["x-api-key"]) {
635
- const authHeader = forwardHeaders["authorization"] ?? "";
636
- if (authHeader.toLowerCase().startsWith("bearer ")) {
637
- const token = authHeader.slice(7).trim();
638
- if (token.startsWith("sk-ant-")) {
639
- forwardHeaders["x-api-key"] = token;
640
- }
641
- }
642
- }
643
- }
723
+ const forwardHeaders = buildForwardHeaders(req, config, source, useSkalpel);
644
724
  let isStreaming = false;
645
725
  if (body) {
646
726
  try {
@@ -650,38 +730,52 @@ async function handleRequest(req, res, config, source, logger) {
650
730
  }
651
731
  }
652
732
  if (isStreaming) {
653
- await handleStreamingRequest(req, res, config, source, body, forwardUrl, forwardHeaders, logger);
733
+ const skalpelUrl2 = `${config.remoteBaseUrl}${path4}`;
734
+ const directUrl2 = `${config.anthropicDirectUrl}${path4}`;
735
+ await handleStreamingRequest(req, res, config, source, body, skalpelUrl2, directUrl2, useSkalpel, forwardHeaders, logger);
654
736
  logger.info(`${method} ${path4} source=${source} streaming latency=${Date.now() - start}ms`);
655
737
  return;
656
738
  }
657
- const response = await fetch(forwardUrl, {
658
- method,
659
- headers: forwardHeaders,
660
- body: method !== "GET" && method !== "HEAD" ? body : void 0
661
- });
662
- const STRIP_HEADERS = /* @__PURE__ */ new Set([
663
- "connection",
664
- "keep-alive",
665
- "proxy-authenticate",
666
- "proxy-authorization",
667
- "te",
668
- "trailer",
669
- "transfer-encoding",
670
- "upgrade",
671
- "content-encoding",
672
- "content-length"
673
- ]);
674
- const responseHeaders = {};
675
- for (const [key, value] of response.headers.entries()) {
676
- if (!STRIP_HEADERS.has(key)) {
677
- responseHeaders[key] = value;
739
+ const skalpelUrl = `${config.remoteBaseUrl}${path4}`;
740
+ const directUrl = `${config.anthropicDirectUrl}${path4}`;
741
+ const fetchBody = method !== "GET" && method !== "HEAD" ? body : void 0;
742
+ let response = null;
743
+ let fetchError = null;
744
+ let usedFallback = false;
745
+ if (useSkalpel) {
746
+ try {
747
+ response = await fetch(skalpelUrl, { method, headers: forwardHeaders, body: fetchBody });
748
+ } catch (err) {
749
+ fetchError = err;
750
+ }
751
+ if (isSkalpelBackendFailure2(response, fetchError)) {
752
+ logger.warn(`${method} ${path4} Skalpel backend failed (${fetchError ? fetchError.message : `status ${response?.status}`}), falling back to direct Anthropic API`);
753
+ usedFallback = true;
754
+ response = null;
755
+ fetchError = null;
756
+ const directHeaders = stripSkalpelHeaders2(forwardHeaders);
757
+ try {
758
+ response = await fetch(directUrl, { method, headers: directHeaders, body: fetchBody });
759
+ } catch (err) {
760
+ fetchError = err;
761
+ }
762
+ }
763
+ } else {
764
+ try {
765
+ response = await fetch(directUrl, { method, headers: forwardHeaders, body: fetchBody });
766
+ } catch (err) {
767
+ fetchError = err;
678
768
  }
679
769
  }
770
+ if (!response || fetchError) {
771
+ throw fetchError ?? new Error("no response from upstream");
772
+ }
773
+ const responseHeaders = extractResponseHeaders(response);
680
774
  const responseBody = Buffer.from(await response.arrayBuffer());
681
775
  responseHeaders["content-length"] = String(responseBody.length);
682
776
  res.writeHead(response.status, responseHeaders);
683
777
  res.end(responseBody);
684
- logger.info(`${method} ${path4} source=${source} status=${response.status} latency=${Date.now() - start}ms`);
778
+ logger.info(`${method} ${path4} source=${source} status=${response.status}${usedFallback ? " (fallback)" : ""} latency=${Date.now() - start}ms`);
685
779
  } catch (err) {
686
780
  logger.error(`${method} ${path4} source=${source} error=${err.message}`);
687
781
  if (!res.headersSent) {
@@ -815,6 +909,24 @@ function startProxy(config) {
815
909
  }
816
910
  handleRequest(req, res, config, "codex", logger);
817
911
  });
912
+ anthropicServer.on("error", (err) => {
913
+ if (err.code === "EADDRINUSE") {
914
+ logger.error(`Port ${config.anthropicPort} is already in use. Another Skalpel proxy or process may be running.`);
915
+ } else {
916
+ logger.error(`Anthropic proxy failed to bind port ${config.anthropicPort}: ${err.message}`);
917
+ }
918
+ removePid(config.pidFile);
919
+ process.exit(1);
920
+ });
921
+ openaiServer.on("error", (err) => {
922
+ if (err.code === "EADDRINUSE") {
923
+ logger.error(`Port ${config.openaiPort} is already in use. Another Skalpel proxy or process may be running.`);
924
+ } else {
925
+ logger.error(`OpenAI proxy failed to bind port ${config.openaiPort}: ${err.message}`);
926
+ }
927
+ removePid(config.pidFile);
928
+ process.exit(1);
929
+ });
818
930
  anthropicServer.listen(config.anthropicPort, () => {
819
931
  logger.info(`Anthropic proxy listening on port ${config.anthropicPort}`);
820
932
  });
@@ -832,6 +944,16 @@ function startProxy(config) {
832
944
  };
833
945
  process.on("SIGTERM", cleanup);
834
946
  process.on("SIGINT", cleanup);
947
+ process.on("uncaughtException", (err) => {
948
+ logger.error(`Uncaught exception: ${err.message}`);
949
+ removePid(config.pidFile);
950
+ process.exit(1);
951
+ });
952
+ process.on("unhandledRejection", (reason) => {
953
+ logger.error(`Unhandled rejection: ${reason}`);
954
+ removePid(config.pidFile);
955
+ process.exit(1);
956
+ });
835
957
  return { anthropicServer, openaiServer };
836
958
  }
837
959
  function stopProxy(config) {