@lakphy/local-router 0.5.0 → 0.5.2

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.
Files changed (4) hide show
  1. package/README.md +83 -0
  2. package/dist/cli.js +7369 -3743
  3. package/dist/entry.js +153 -110
  4. package/package.json +1 -1
package/dist/entry.js CHANGED
@@ -53899,7 +53899,15 @@ function subscribeLogEvents(subscriber) {
53899
53899
  }
53900
53900
 
53901
53901
  // src/logger.ts
53902
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
53902
+ import {
53903
+ appendFileSync,
53904
+ closeSync as closeSync2,
53905
+ existsSync as existsSync7,
53906
+ mkdirSync as mkdirSync3,
53907
+ openSync as openSync2,
53908
+ statSync as statSync2,
53909
+ writeSync
53910
+ } from "fs";
53903
53911
  import { join as join8 } from "path";
53904
53912
  class Logger {
53905
53913
  baseDir;
@@ -53963,22 +53971,73 @@ class Logger {
53963
53971
  console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
53964
53972
  }
53965
53973
  }
53966
- writeStreamFile(requestId, dateStr, content) {
53967
- if (!this._enabled || !this._streamsEnabled)
53968
- return null;
53974
+ openStreamCapture(requestId, dateStr) {
53975
+ if (!this._enabled || !this._streamsEnabled) {
53976
+ return makeNoopStreamCaptureHandle();
53977
+ }
53978
+ const maxStreamBytes = this.maxStreamBytes;
53979
+ const truncationMarker = Buffer.from(`
53980
+ [TRUNCATED]`);
53981
+ let filePath;
53982
+ let fd;
53969
53983
  try {
53970
53984
  const dir = this.ensureStreamDateDir(dateStr);
53971
- const filePath = join8(dir, `${requestId}.sse.raw`);
53972
- const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
53973
- [TRUNCATED]` : content;
53974
- writeFileSync3(filePath, toWrite);
53975
- return filePath;
53985
+ filePath = join8(dir, `${requestId}.sse.raw`);
53986
+ fd = openSync2(filePath, "a");
53976
53987
  } catch (err) {
53977
- console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
53978
- return null;
53988
+ console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u6253\u5F00\u5931\u8D25:", err);
53989
+ return makeNoopStreamCaptureHandle();
53979
53990
  }
53991
+ let bytes = 0;
53992
+ let truncated = false;
53993
+ let finalized = false;
53994
+ return {
53995
+ filePath,
53996
+ write(chunk) {
53997
+ if (finalized || truncated || fd == null)
53998
+ return;
53999
+ try {
54000
+ if (bytes + chunk.byteLength > maxStreamBytes) {
54001
+ const remaining = Math.max(0, maxStreamBytes - bytes);
54002
+ if (remaining > 0) {
54003
+ writeSync(fd, chunk.subarray(0, remaining));
54004
+ bytes += remaining;
54005
+ }
54006
+ writeSync(fd, truncationMarker);
54007
+ truncated = true;
54008
+ return;
54009
+ }
54010
+ writeSync(fd, chunk);
54011
+ bytes += chunk.byteLength;
54012
+ } catch (err) {
54013
+ console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
54014
+ truncated = true;
54015
+ }
54016
+ },
54017
+ finalize() {
54018
+ if (finalized)
54019
+ return { bytesWritten: bytes, truncated, filePath };
54020
+ finalized = true;
54021
+ if (fd != null) {
54022
+ try {
54023
+ closeSync2(fd);
54024
+ } catch {}
54025
+ fd = null;
54026
+ }
54027
+ return { bytesWritten: bytes, truncated, filePath };
54028
+ }
54029
+ };
53980
54030
  }
53981
54031
  }
54032
+ function makeNoopStreamCaptureHandle() {
54033
+ return {
54034
+ filePath: null,
54035
+ write() {},
54036
+ finalize() {
54037
+ return { bytesWritten: 0, truncated: false, filePath: null };
54038
+ }
54039
+ };
54040
+ }
53982
54041
  var instance = null;
53983
54042
  function initLogger(baseDir, config2) {
53984
54043
  instance = new Logger(baseDir, config2);
@@ -55370,11 +55429,6 @@ class PluginManager {
55370
55429
  }
55371
55430
  }
55372
55431
 
55373
- // src/proxy.ts
55374
- import { appendFile, readFile, unlink } from "fs/promises";
55375
- import { tmpdir as tmpdir2 } from "os";
55376
- import { join as join10 } from "path";
55377
-
55378
55432
  // src/plugin-engine.ts
55379
55433
  async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
55380
55434
  let currentUrl = url2;
@@ -55564,28 +55618,6 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
55564
55618
  ...overrides
55565
55619
  };
55566
55620
  }
55567
- function createTempStreamCapturePath(requestId) {
55568
- return join10(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
55569
- }
55570
- async function appendTempStreamCapture(filePath, chunk) {
55571
- await appendFile(filePath, chunk);
55572
- }
55573
- async function flushTempCaptureToLogger(tempPath, requestId, dateStr, logger) {
55574
- if (!logger)
55575
- return null;
55576
- try {
55577
- const text2 = await readFile(tempPath, "utf-8").catch((err) => {
55578
- if (err.code === "ENOENT")
55579
- return "";
55580
- throw err;
55581
- });
55582
- return logger.writeStreamFile(requestId, dateStr, text2);
55583
- } finally {
55584
- await unlink(tempPath).catch(() => {
55585
- return;
55586
- });
55587
- }
55588
- }
55589
55621
  async function proxyRequest(c2, options) {
55590
55622
  const { logMeta, plugins, pluginConfigs } = options;
55591
55623
  const logger = getLogger();
@@ -55593,19 +55625,20 @@ async function proxyRequest(c2, options) {
55593
55625
  const hasPlugins = plugins && plugins.length > 0;
55594
55626
  let targetUrl = options.targetUrl;
55595
55627
  let headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
55596
- let bodyStr = options.body;
55628
+ let currentBody = options.body;
55629
+ const pluginCtx = Object.freeze({
55630
+ requestId: logMeta.requestId,
55631
+ provider: logMeta.provider,
55632
+ modelIn: logMeta.modelIn,
55633
+ modelOut: logMeta.modelOut,
55634
+ routeType: logMeta.routeType,
55635
+ isStream: logMeta.isStream
55636
+ });
55637
+ const wantsBodyLog = shouldLog && logger?.bodyPolicy !== "off";
55638
+ const requestBodySnapshot = wantsBodyLog ? JSON.parse(JSON.stringify(options.body)) : undefined;
55597
55639
  const pluginLogOverrides = {};
55598
55640
  if (hasPlugins) {
55599
- const bodyObj = JSON.parse(bodyStr);
55600
- const ctx = {
55601
- requestId: logMeta.requestId,
55602
- provider: logMeta.provider,
55603
- modelIn: logMeta.modelIn,
55604
- modelOut: logMeta.modelOut,
55605
- routeType: logMeta.routeType,
55606
- isStream: logMeta.isStream
55607
- };
55608
- const result = await executeRequestPlugins(plugins, ctx, targetUrl, headers, bodyObj);
55641
+ const result = await executeRequestPlugins(plugins, pluginCtx, targetUrl, headers, currentBody);
55609
55642
  if (pluginConfigs) {
55610
55643
  pluginLogOverrides.plugins_request = pluginConfigs;
55611
55644
  }
@@ -55614,20 +55647,20 @@ async function proxyRequest(c2, options) {
55614
55647
  pluginLogOverrides.request_url_after_plugins = targetUrl;
55615
55648
  }
55616
55649
  headers = result.headers;
55617
- const newBodyStr = JSON.stringify(result.body);
55618
- if (newBodyStr !== bodyStr) {
55619
- bodyStr = newBodyStr;
55650
+ if (result.body !== currentBody) {
55651
+ currentBody = result.body;
55620
55652
  pluginLogOverrides.request_body_after_plugins = result.body;
55621
55653
  }
55622
55654
  }
55623
- const requestBody = shouldLog && logger?.bodyPolicy !== "off" ? JSON.parse(options.body) : undefined;
55655
+ const wireBody = JSON.stringify(currentBody);
55656
+ const requestBody = requestBodySnapshot;
55624
55657
  const proxy = options.proxy?.trim() ? options.proxy.trim() : undefined;
55625
55658
  let upstreamRes;
55626
55659
  try {
55627
55660
  upstreamRes = await fetch(targetUrl, {
55628
55661
  method: c2.req.method,
55629
55662
  headers,
55630
- body: bodyStr,
55663
+ body: wireBody,
55631
55664
  ...proxy ? { proxy } : {},
55632
55665
  decompress: true
55633
55666
  });
@@ -55657,15 +55690,7 @@ async function proxyRequest(c2, options) {
55657
55690
  let sseHeaders = responseHeaders;
55658
55691
  let sseTransform = null;
55659
55692
  if (hasPlugins) {
55660
- const ctx = {
55661
- requestId: logMeta.requestId,
55662
- provider: logMeta.provider,
55663
- modelIn: logMeta.modelIn,
55664
- modelOut: logMeta.modelOut,
55665
- routeType: logMeta.routeType,
55666
- isStream: logMeta.isStream
55667
- };
55668
- const sseResult = await createSSEPluginTransform(plugins, ctx, upstreamRes.status, responseHeaders);
55693
+ const sseResult = await createSSEPluginTransform(plugins, pluginCtx, upstreamRes.status, responseHeaders);
55669
55694
  sseStatus = sseResult.status;
55670
55695
  sseHeaders = sseResult.headers;
55671
55696
  sseTransform = sseResult.transform;
@@ -55674,47 +55699,71 @@ async function proxyRequest(c2, options) {
55674
55699
  }
55675
55700
  }
55676
55701
  if (!shouldLog) {
55677
- const outputBody2 = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
55678
- return new Response(outputBody2, {
55702
+ const outputBody = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
55703
+ return new Response(outputBody, {
55679
55704
  status: sseStatus,
55680
55705
  headers: sseHeaders
55681
55706
  });
55682
55707
  }
55683
- const [clientStream, logStream] = upstreamRes.body.tee();
55684
- (async () => {
55685
- const tempPath = createTempStreamCapturePath(logMeta.requestId);
55686
- let streamBytes = 0;
55687
- let streamFile = null;
55688
- try {
55689
- const reader = logStream.getReader();
55690
- while (true) {
55691
- const { done, value } = await reader.read();
55692
- if (done)
55693
- break;
55694
- streamBytes += value.byteLength;
55695
- await appendTempStreamCapture(tempPath, value);
55708
+ const capture = logger?.openStreamCapture(logMeta.requestId, dateStr) ?? null;
55709
+ let upstreamBytes = 0;
55710
+ let writeEventCalled = false;
55711
+ const finalizeAndWriteEvent = () => {
55712
+ if (writeEventCalled)
55713
+ return;
55714
+ writeEventCalled = true;
55715
+ const captureResult = capture?.finalize() ?? {
55716
+ bytesWritten: 0,
55717
+ truncated: false,
55718
+ filePath: null
55719
+ };
55720
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
55721
+ upstream_status: sseStatus,
55722
+ content_type_res: contentTypeRes,
55723
+ response_headers: sseHeaders,
55724
+ stream_bytes: upstreamBytes,
55725
+ provider_request_id: providerRequestId,
55726
+ ...captureResult.filePath != null && { stream_file: captureResult.filePath },
55727
+ ...captureResult.bytesWritten > 0 && {
55728
+ stream_file_bytes: captureResult.bytesWritten
55729
+ },
55730
+ ...captureResult.truncated && { stream_file_truncated: true },
55731
+ ...requestBody !== undefined && { request_body: requestBody },
55732
+ ...pluginLogOverrides
55733
+ }));
55734
+ };
55735
+ const upstreamReader = upstreamRes.body.getReader();
55736
+ const tappedStream = new ReadableStream({
55737
+ async pull(controller) {
55738
+ try {
55739
+ const { done, value } = await upstreamReader.read();
55740
+ if (done) {
55741
+ controller.close();
55742
+ finalizeAndWriteEvent();
55743
+ return;
55744
+ }
55745
+ upstreamBytes += value.byteLength;
55746
+ capture?.write(value);
55747
+ controller.enqueue(value);
55748
+ } catch (err) {
55749
+ finalizeAndWriteEvent();
55750
+ controller.error(err);
55751
+ }
55752
+ },
55753
+ cancel(reason) {
55754
+ try {
55755
+ upstreamReader.cancel(reason).catch(() => {
55756
+ return;
55757
+ });
55758
+ } finally {
55759
+ finalizeAndWriteEvent();
55696
55760
  }
55697
- streamFile = await flushTempCaptureToLogger(tempPath, logMeta.requestId, dateStr, logger);
55698
- } catch (err) {
55699
- await unlink(tempPath).catch(() => {
55700
- return;
55701
- });
55702
- console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5904\u7406\u5931\u8D25:", err);
55703
- } finally {
55704
- logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
55705
- upstream_status: sseStatus,
55706
- content_type_res: contentTypeRes,
55707
- response_headers: sseHeaders,
55708
- stream_bytes: streamBytes,
55709
- provider_request_id: providerRequestId,
55710
- ...streamFile != null && { stream_file: streamFile },
55711
- ...requestBody !== undefined && { request_body: requestBody },
55712
- ...pluginLogOverrides
55713
- }));
55714
55761
  }
55715
- })();
55716
- const outputBody = sseTransform ? clientStream.pipeThrough(sseTransform) : clientStream;
55717
- return new Response(outputBody, {
55762
+ });
55763
+ let stream = tappedStream;
55764
+ if (sseTransform)
55765
+ stream = stream.pipeThrough(sseTransform);
55766
+ return new Response(stream, {
55718
55767
  status: sseStatus,
55719
55768
  headers: sseHeaders
55720
55769
  });
@@ -55723,15 +55772,7 @@ async function proxyRequest(c2, options) {
55723
55772
  let responseStatus = upstreamRes.status;
55724
55773
  let finalResponseHeaders = responseHeaders;
55725
55774
  if (hasPlugins) {
55726
- const ctx = {
55727
- requestId: logMeta.requestId,
55728
- provider: logMeta.provider,
55729
- modelIn: logMeta.modelIn,
55730
- modelOut: logMeta.modelOut,
55731
- routeType: logMeta.routeType,
55732
- isStream: logMeta.isStream
55733
- };
55734
- const result = await executeJsonResponsePlugins(plugins, ctx, upstreamRes.status, responseHeaders, responseText);
55775
+ const result = await executeJsonResponsePlugins(plugins, pluginCtx, upstreamRes.status, responseHeaders, responseText);
55735
55776
  if (pluginConfigs) {
55736
55777
  pluginLogOverrides.plugins_response = pluginConfigs;
55737
55778
  }
@@ -55808,8 +55849,10 @@ function createModelRoutingHandler(options) {
55808
55849
  return c2.json({ error: `provider "${target.provider}" \u672A\u5728\u914D\u7F6E\u4E2D\u5B9A\u4E49` }, 500);
55809
55850
  }
55810
55851
  payload.model = target.model;
55811
- const body = JSON.stringify(payload);
55812
55852
  const targetUrl = buildTargetUrl(provider.base);
55853
+ const contentLengthHeader = c2.req.header("content-length");
55854
+ const parsedContentLength = contentLengthHeader ? Number(contentLengthHeader) : NaN;
55855
+ const requestBytes = Number.isInteger(parsedContentLength) && parsedContentLength >= 0 ? parsedContentLength : Buffer.byteLength(JSON.stringify(payload), "utf-8");
55813
55856
  const logMeta = {
55814
55857
  requestId: crypto.randomUUID(),
55815
55858
  tsStart: Date.now(),
@@ -55823,7 +55866,7 @@ function createModelRoutingHandler(options) {
55823
55866
  path: c2.req.path,
55824
55867
  contentTypeReq: c2.req.header("content-type") ?? null,
55825
55868
  userAgent: c2.req.header("user-agent") ?? null,
55826
- requestBytes: Buffer.byteLength(body, "utf-8"),
55869
+ requestBytes,
55827
55870
  requestHeaders: collectHeaders(c2.req.raw.headers)
55828
55871
  };
55829
55872
  const plugins = pluginManager?.getPlugins(target.provider) ?? [];
@@ -55833,7 +55876,7 @@ function createModelRoutingHandler(options) {
55833
55876
  apiKey: provider.apiKey,
55834
55877
  proxy: provider.proxy,
55835
55878
  authType,
55836
- body,
55879
+ body: payload,
55837
55880
  logMeta,
55838
55881
  plugins: plugins.length > 0 ? plugins : undefined,
55839
55882
  pluginConfigs: pluginConfigs.length > 0 ? pluginConfigs.map((lp) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakphy/local-router",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "packageManager": "bun@1.2.0",
5
5
  "workspaces": [
6
6
  "web"