@lakphy/local-router 0.5.1 → 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 (3) hide show
  1. package/dist/cli.js +177 -134
  2. package/dist/entry.js +153 -110
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11167,7 +11167,7 @@ __export(exports_config_extra, {
11167
11167
  configBackupsList: () => configBackupsList
11168
11168
  });
11169
11169
  import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
11170
- import { dirname as dirname4, join as join13 } from "path";
11170
+ import { dirname as dirname4, join as join12 } from "path";
11171
11171
  import { parseArgs as parseArgs2 } from "util";
11172
11172
  function maskApiKey(k) {
11173
11173
  if (k.length <= 8)
@@ -11252,7 +11252,7 @@ async function configDiff(args, flags) {
11252
11252
  const against = parsed.values.against;
11253
11253
  let beforePath;
11254
11254
  if (!against) {
11255
- const backupDir = join13(dirname4(path), ".backups");
11255
+ const backupDir = join12(dirname4(path), ".backups");
11256
11256
  if (!existsSync9(backupDir)) {
11257
11257
  throw new CliError("CONFIG_NOT_FOUND", "\u6CA1\u6709\u5907\u4EFD\u53EF\u5BF9\u6BD4", {
11258
11258
  hint: "\u6307\u5B9A --against <path|backup-id>"
@@ -11262,16 +11262,16 @@ async function configDiff(args, flags) {
11262
11262
  if (files.length === 0) {
11263
11263
  throw new CliError("CONFIG_NOT_FOUND", "\u5907\u4EFD\u76EE\u5F55\u4E3A\u7A7A");
11264
11264
  }
11265
- beforePath = join13(backupDir, files[0]);
11265
+ beforePath = join12(backupDir, files[0]);
11266
11266
  } else if (existsSync9(against)) {
11267
11267
  beforePath = against;
11268
11268
  } else {
11269
- const backupDir = join13(dirname4(path), ".backups");
11270
- const candidate = join13(backupDir, `config-${against}.json5`);
11269
+ const backupDir = join12(dirname4(path), ".backups");
11270
+ const candidate = join12(backupDir, `config-${against}.json5`);
11271
11271
  if (existsSync9(candidate))
11272
11272
  beforePath = candidate;
11273
- else if (existsSync9(join13(backupDir, against)))
11274
- beforePath = join13(backupDir, against);
11273
+ else if (existsSync9(join12(backupDir, against)))
11274
+ beforePath = join12(backupDir, against);
11275
11275
  else {
11276
11276
  throw new CliError("CONFIG_NOT_FOUND", `--against \u4E0D\u5B58\u5728: ${against}`);
11277
11277
  }
@@ -11426,11 +11426,11 @@ ${result.diff}`
11426
11426
  });
11427
11427
  }
11428
11428
  function listBackups(configPath) {
11429
- const dir = join13(dirname4(configPath), ".backups");
11429
+ const dir = join12(dirname4(configPath), ".backups");
11430
11430
  if (!existsSync9(dir))
11431
11431
  return [];
11432
11432
  return readdirSync(dir).filter((f) => f.startsWith("config-") && f.endsWith(".json5")).map((f) => {
11433
- const full = join13(dir, f);
11433
+ const full = join12(dir, f);
11434
11434
  const st = statSync4(full);
11435
11435
  return {
11436
11436
  id: f.replace(/^config-/, "").replace(/\.json5$/, ""),
@@ -11677,7 +11677,7 @@ import { parseArgs as parseArgs3 } from "util";
11677
11677
 
11678
11678
  // src/cli/process.ts
11679
11679
  init_config();
11680
- import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
11680
+ import { closeSync as closeSync3, openSync as openSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
11681
11681
  import { setTimeout as sleep } from "timers/promises";
11682
11682
  import { parseArgs } from "util";
11683
11683
 
@@ -55041,7 +55041,15 @@ function subscribeLogEvents(subscriber) {
55041
55041
  }
55042
55042
 
55043
55043
  // src/logger.ts
55044
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
55044
+ import {
55045
+ appendFileSync,
55046
+ closeSync as closeSync2,
55047
+ existsSync as existsSync7,
55048
+ mkdirSync as mkdirSync4,
55049
+ openSync as openSync2,
55050
+ statSync as statSync2,
55051
+ writeSync
55052
+ } from "fs";
55045
55053
  import { join as join9 } from "path";
55046
55054
  class Logger {
55047
55055
  baseDir;
@@ -55105,22 +55113,73 @@ class Logger {
55105
55113
  console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
55106
55114
  }
55107
55115
  }
55108
- writeStreamFile(requestId, dateStr, content) {
55109
- if (!this._enabled || !this._streamsEnabled)
55110
- return null;
55116
+ openStreamCapture(requestId, dateStr) {
55117
+ if (!this._enabled || !this._streamsEnabled) {
55118
+ return makeNoopStreamCaptureHandle();
55119
+ }
55120
+ const maxStreamBytes = this.maxStreamBytes;
55121
+ const truncationMarker = Buffer.from(`
55122
+ [TRUNCATED]`);
55123
+ let filePath;
55124
+ let fd;
55111
55125
  try {
55112
55126
  const dir = this.ensureStreamDateDir(dateStr);
55113
- const filePath = join9(dir, `${requestId}.sse.raw`);
55114
- const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
55115
- [TRUNCATED]` : content;
55116
- writeFileSync4(filePath, toWrite);
55117
- return filePath;
55127
+ filePath = join9(dir, `${requestId}.sse.raw`);
55128
+ fd = openSync2(filePath, "a");
55118
55129
  } catch (err) {
55119
- console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
55120
- return null;
55130
+ console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u6253\u5F00\u5931\u8D25:", err);
55131
+ return makeNoopStreamCaptureHandle();
55121
55132
  }
55133
+ let bytes = 0;
55134
+ let truncated = false;
55135
+ let finalized = false;
55136
+ return {
55137
+ filePath,
55138
+ write(chunk) {
55139
+ if (finalized || truncated || fd == null)
55140
+ return;
55141
+ try {
55142
+ if (bytes + chunk.byteLength > maxStreamBytes) {
55143
+ const remaining = Math.max(0, maxStreamBytes - bytes);
55144
+ if (remaining > 0) {
55145
+ writeSync(fd, chunk.subarray(0, remaining));
55146
+ bytes += remaining;
55147
+ }
55148
+ writeSync(fd, truncationMarker);
55149
+ truncated = true;
55150
+ return;
55151
+ }
55152
+ writeSync(fd, chunk);
55153
+ bytes += chunk.byteLength;
55154
+ } catch (err) {
55155
+ console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
55156
+ truncated = true;
55157
+ }
55158
+ },
55159
+ finalize() {
55160
+ if (finalized)
55161
+ return { bytesWritten: bytes, truncated, filePath };
55162
+ finalized = true;
55163
+ if (fd != null) {
55164
+ try {
55165
+ closeSync2(fd);
55166
+ } catch {}
55167
+ fd = null;
55168
+ }
55169
+ return { bytesWritten: bytes, truncated, filePath };
55170
+ }
55171
+ };
55122
55172
  }
55123
55173
  }
55174
+ function makeNoopStreamCaptureHandle() {
55175
+ return {
55176
+ filePath: null,
55177
+ write() {},
55178
+ finalize() {
55179
+ return { bytesWritten: 0, truncated: false, filePath: null };
55180
+ }
55181
+ };
55182
+ }
55124
55183
  var instance = null;
55125
55184
  function initLogger(baseDir, config2) {
55126
55185
  instance = new Logger(baseDir, config2);
@@ -56512,11 +56571,6 @@ class PluginManager {
56512
56571
  }
56513
56572
  }
56514
56573
 
56515
- // src/proxy.ts
56516
- import { appendFile, readFile, unlink } from "fs/promises";
56517
- import { tmpdir as tmpdir2 } from "os";
56518
- import { join as join11 } from "path";
56519
-
56520
56574
  // src/plugin-engine.ts
56521
56575
  async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
56522
56576
  let currentUrl = url2;
@@ -56706,28 +56760,6 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
56706
56760
  ...overrides
56707
56761
  };
56708
56762
  }
56709
- function createTempStreamCapturePath(requestId) {
56710
- return join11(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
56711
- }
56712
- async function appendTempStreamCapture(filePath, chunk) {
56713
- await appendFile(filePath, chunk);
56714
- }
56715
- async function flushTempCaptureToLogger(tempPath, requestId, dateStr, logger) {
56716
- if (!logger)
56717
- return null;
56718
- try {
56719
- const text2 = await readFile(tempPath, "utf-8").catch((err) => {
56720
- if (err.code === "ENOENT")
56721
- return "";
56722
- throw err;
56723
- });
56724
- return logger.writeStreamFile(requestId, dateStr, text2);
56725
- } finally {
56726
- await unlink(tempPath).catch(() => {
56727
- return;
56728
- });
56729
- }
56730
- }
56731
56763
  async function proxyRequest(c2, options) {
56732
56764
  const { logMeta, plugins, pluginConfigs } = options;
56733
56765
  const logger = getLogger();
@@ -56735,19 +56767,20 @@ async function proxyRequest(c2, options) {
56735
56767
  const hasPlugins = plugins && plugins.length > 0;
56736
56768
  let targetUrl = options.targetUrl;
56737
56769
  let headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
56738
- let bodyStr = options.body;
56770
+ let currentBody = options.body;
56771
+ const pluginCtx = Object.freeze({
56772
+ requestId: logMeta.requestId,
56773
+ provider: logMeta.provider,
56774
+ modelIn: logMeta.modelIn,
56775
+ modelOut: logMeta.modelOut,
56776
+ routeType: logMeta.routeType,
56777
+ isStream: logMeta.isStream
56778
+ });
56779
+ const wantsBodyLog = shouldLog && logger?.bodyPolicy !== "off";
56780
+ const requestBodySnapshot = wantsBodyLog ? JSON.parse(JSON.stringify(options.body)) : undefined;
56739
56781
  const pluginLogOverrides = {};
56740
56782
  if (hasPlugins) {
56741
- const bodyObj = JSON.parse(bodyStr);
56742
- const ctx = {
56743
- requestId: logMeta.requestId,
56744
- provider: logMeta.provider,
56745
- modelIn: logMeta.modelIn,
56746
- modelOut: logMeta.modelOut,
56747
- routeType: logMeta.routeType,
56748
- isStream: logMeta.isStream
56749
- };
56750
- const result = await executeRequestPlugins(plugins, ctx, targetUrl, headers, bodyObj);
56783
+ const result = await executeRequestPlugins(plugins, pluginCtx, targetUrl, headers, currentBody);
56751
56784
  if (pluginConfigs) {
56752
56785
  pluginLogOverrides.plugins_request = pluginConfigs;
56753
56786
  }
@@ -56756,20 +56789,20 @@ async function proxyRequest(c2, options) {
56756
56789
  pluginLogOverrides.request_url_after_plugins = targetUrl;
56757
56790
  }
56758
56791
  headers = result.headers;
56759
- const newBodyStr = JSON.stringify(result.body);
56760
- if (newBodyStr !== bodyStr) {
56761
- bodyStr = newBodyStr;
56792
+ if (result.body !== currentBody) {
56793
+ currentBody = result.body;
56762
56794
  pluginLogOverrides.request_body_after_plugins = result.body;
56763
56795
  }
56764
56796
  }
56765
- const requestBody = shouldLog && logger?.bodyPolicy !== "off" ? JSON.parse(options.body) : undefined;
56797
+ const wireBody = JSON.stringify(currentBody);
56798
+ const requestBody = requestBodySnapshot;
56766
56799
  const proxy = options.proxy?.trim() ? options.proxy.trim() : undefined;
56767
56800
  let upstreamRes;
56768
56801
  try {
56769
56802
  upstreamRes = await fetch(targetUrl, {
56770
56803
  method: c2.req.method,
56771
56804
  headers,
56772
- body: bodyStr,
56805
+ body: wireBody,
56773
56806
  ...proxy ? { proxy } : {},
56774
56807
  decompress: true
56775
56808
  });
@@ -56799,15 +56832,7 @@ async function proxyRequest(c2, options) {
56799
56832
  let sseHeaders = responseHeaders;
56800
56833
  let sseTransform = null;
56801
56834
  if (hasPlugins) {
56802
- const ctx = {
56803
- requestId: logMeta.requestId,
56804
- provider: logMeta.provider,
56805
- modelIn: logMeta.modelIn,
56806
- modelOut: logMeta.modelOut,
56807
- routeType: logMeta.routeType,
56808
- isStream: logMeta.isStream
56809
- };
56810
- const sseResult = await createSSEPluginTransform(plugins, ctx, upstreamRes.status, responseHeaders);
56835
+ const sseResult = await createSSEPluginTransform(plugins, pluginCtx, upstreamRes.status, responseHeaders);
56811
56836
  sseStatus = sseResult.status;
56812
56837
  sseHeaders = sseResult.headers;
56813
56838
  sseTransform = sseResult.transform;
@@ -56816,47 +56841,71 @@ async function proxyRequest(c2, options) {
56816
56841
  }
56817
56842
  }
56818
56843
  if (!shouldLog) {
56819
- const outputBody2 = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
56820
- return new Response(outputBody2, {
56844
+ const outputBody = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
56845
+ return new Response(outputBody, {
56821
56846
  status: sseStatus,
56822
56847
  headers: sseHeaders
56823
56848
  });
56824
56849
  }
56825
- const [clientStream, logStream] = upstreamRes.body.tee();
56826
- (async () => {
56827
- const tempPath = createTempStreamCapturePath(logMeta.requestId);
56828
- let streamBytes = 0;
56829
- let streamFile = null;
56830
- try {
56831
- const reader = logStream.getReader();
56832
- while (true) {
56833
- const { done, value } = await reader.read();
56834
- if (done)
56835
- break;
56836
- streamBytes += value.byteLength;
56837
- await appendTempStreamCapture(tempPath, value);
56850
+ const capture = logger?.openStreamCapture(logMeta.requestId, dateStr) ?? null;
56851
+ let upstreamBytes = 0;
56852
+ let writeEventCalled = false;
56853
+ const finalizeAndWriteEvent = () => {
56854
+ if (writeEventCalled)
56855
+ return;
56856
+ writeEventCalled = true;
56857
+ const captureResult = capture?.finalize() ?? {
56858
+ bytesWritten: 0,
56859
+ truncated: false,
56860
+ filePath: null
56861
+ };
56862
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
56863
+ upstream_status: sseStatus,
56864
+ content_type_res: contentTypeRes,
56865
+ response_headers: sseHeaders,
56866
+ stream_bytes: upstreamBytes,
56867
+ provider_request_id: providerRequestId,
56868
+ ...captureResult.filePath != null && { stream_file: captureResult.filePath },
56869
+ ...captureResult.bytesWritten > 0 && {
56870
+ stream_file_bytes: captureResult.bytesWritten
56871
+ },
56872
+ ...captureResult.truncated && { stream_file_truncated: true },
56873
+ ...requestBody !== undefined && { request_body: requestBody },
56874
+ ...pluginLogOverrides
56875
+ }));
56876
+ };
56877
+ const upstreamReader = upstreamRes.body.getReader();
56878
+ const tappedStream = new ReadableStream({
56879
+ async pull(controller) {
56880
+ try {
56881
+ const { done, value } = await upstreamReader.read();
56882
+ if (done) {
56883
+ controller.close();
56884
+ finalizeAndWriteEvent();
56885
+ return;
56886
+ }
56887
+ upstreamBytes += value.byteLength;
56888
+ capture?.write(value);
56889
+ controller.enqueue(value);
56890
+ } catch (err) {
56891
+ finalizeAndWriteEvent();
56892
+ controller.error(err);
56893
+ }
56894
+ },
56895
+ cancel(reason) {
56896
+ try {
56897
+ upstreamReader.cancel(reason).catch(() => {
56898
+ return;
56899
+ });
56900
+ } finally {
56901
+ finalizeAndWriteEvent();
56838
56902
  }
56839
- streamFile = await flushTempCaptureToLogger(tempPath, logMeta.requestId, dateStr, logger);
56840
- } catch (err) {
56841
- await unlink(tempPath).catch(() => {
56842
- return;
56843
- });
56844
- console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5904\u7406\u5931\u8D25:", err);
56845
- } finally {
56846
- logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
56847
- upstream_status: sseStatus,
56848
- content_type_res: contentTypeRes,
56849
- response_headers: sseHeaders,
56850
- stream_bytes: streamBytes,
56851
- provider_request_id: providerRequestId,
56852
- ...streamFile != null && { stream_file: streamFile },
56853
- ...requestBody !== undefined && { request_body: requestBody },
56854
- ...pluginLogOverrides
56855
- }));
56856
56903
  }
56857
- })();
56858
- const outputBody = sseTransform ? clientStream.pipeThrough(sseTransform) : clientStream;
56859
- return new Response(outputBody, {
56904
+ });
56905
+ let stream = tappedStream;
56906
+ if (sseTransform)
56907
+ stream = stream.pipeThrough(sseTransform);
56908
+ return new Response(stream, {
56860
56909
  status: sseStatus,
56861
56910
  headers: sseHeaders
56862
56911
  });
@@ -56865,15 +56914,7 @@ async function proxyRequest(c2, options) {
56865
56914
  let responseStatus = upstreamRes.status;
56866
56915
  let finalResponseHeaders = responseHeaders;
56867
56916
  if (hasPlugins) {
56868
- const ctx = {
56869
- requestId: logMeta.requestId,
56870
- provider: logMeta.provider,
56871
- modelIn: logMeta.modelIn,
56872
- modelOut: logMeta.modelOut,
56873
- routeType: logMeta.routeType,
56874
- isStream: logMeta.isStream
56875
- };
56876
- const result = await executeJsonResponsePlugins(plugins, ctx, upstreamRes.status, responseHeaders, responseText);
56917
+ const result = await executeJsonResponsePlugins(plugins, pluginCtx, upstreamRes.status, responseHeaders, responseText);
56877
56918
  if (pluginConfigs) {
56878
56919
  pluginLogOverrides.plugins_response = pluginConfigs;
56879
56920
  }
@@ -56950,8 +56991,10 @@ function createModelRoutingHandler(options) {
56950
56991
  return c2.json({ error: `provider "${target.provider}" \u672A\u5728\u914D\u7F6E\u4E2D\u5B9A\u4E49` }, 500);
56951
56992
  }
56952
56993
  payload.model = target.model;
56953
- const body = JSON.stringify(payload);
56954
56994
  const targetUrl = buildTargetUrl(provider.base);
56995
+ const contentLengthHeader = c2.req.header("content-length");
56996
+ const parsedContentLength = contentLengthHeader ? Number(contentLengthHeader) : NaN;
56997
+ const requestBytes = Number.isInteger(parsedContentLength) && parsedContentLength >= 0 ? parsedContentLength : Buffer.byteLength(JSON.stringify(payload), "utf-8");
56955
56998
  const logMeta = {
56956
56999
  requestId: crypto.randomUUID(),
56957
57000
  tsStart: Date.now(),
@@ -56965,7 +57008,7 @@ function createModelRoutingHandler(options) {
56965
57008
  path: c2.req.path,
56966
57009
  contentTypeReq: c2.req.header("content-type") ?? null,
56967
57010
  userAgent: c2.req.header("user-agent") ?? null,
56968
- requestBytes: Buffer.byteLength(body, "utf-8"),
57011
+ requestBytes,
56969
57012
  requestHeaders: collectHeaders(c2.req.raw.headers)
56970
57013
  };
56971
57014
  const plugins = pluginManager?.getPlugins(target.provider) ?? [];
@@ -56975,7 +57018,7 @@ function createModelRoutingHandler(options) {
56975
57018
  apiKey: provider.apiKey,
56976
57019
  proxy: provider.proxy,
56977
57020
  authType,
56978
- body,
57021
+ body: payload,
56979
57022
  logMeta,
56980
57023
  plugins: plugins.length > 0 ? plugins : undefined,
56981
57024
  pluginConfigs: pluginConfigs.length > 0 ? pluginConfigs.map((lp) => ({
@@ -57904,24 +57947,24 @@ async function startServer(options) {
57904
57947
  }
57905
57948
 
57906
57949
  // src/cli/runtime.ts
57907
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync5 } from "fs";
57950
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
57908
57951
  import { homedir as homedir2 } from "os";
57909
- import { join as join12, resolve as resolve7 } from "path";
57952
+ import { join as join11, resolve as resolve7 } from "path";
57910
57953
  function getRuntimeDirs() {
57911
57954
  const override = process.env.LOCAL_ROUTER_RUNTIME_DIR;
57912
- const root2 = override?.trim() ? override.trim() : join12(homedir2(), ".local-router");
57955
+ const root2 = override?.trim() ? override.trim() : join11(homedir2(), ".local-router");
57913
57956
  return {
57914
57957
  root: root2,
57915
- run: join12(root2, "run"),
57916
- logs: join12(root2, "logs")
57958
+ run: join11(root2, "run"),
57959
+ logs: join11(root2, "logs")
57917
57960
  };
57918
57961
  }
57919
57962
  function getRuntimeFiles() {
57920
57963
  const dirs = getRuntimeDirs();
57921
57964
  return {
57922
- pid: join12(dirs.run, "local-router.pid"),
57923
- state: join12(dirs.run, "status.json"),
57924
- daemonLog: join12(dirs.logs, "daemon.log")
57965
+ pid: join11(dirs.run, "local-router.pid"),
57966
+ state: join11(dirs.run, "status.json"),
57967
+ daemonLog: join11(dirs.logs, "daemon.log")
57925
57968
  };
57926
57969
  }
57927
57970
  function ensureRuntimeDirs() {
@@ -57933,9 +57976,9 @@ function ensureRuntimeDirs() {
57933
57976
  function writeRuntimeState(state) {
57934
57977
  ensureRuntimeDirs();
57935
57978
  const files = getRuntimeFiles();
57936
- writeFileSync5(files.pid, `${state.pid}
57979
+ writeFileSync4(files.pid, `${state.pid}
57937
57980
  `, "utf-8");
57938
- writeFileSync5(files.state, JSON.stringify(state, null, 2), "utf-8");
57981
+ writeFileSync4(files.state, JSON.stringify(state, null, 2), "utf-8");
57939
57982
  }
57940
57983
  function readRuntimeState() {
57941
57984
  const files = getRuntimeFiles();
@@ -58094,8 +58137,8 @@ async function startDaemon(flags) {
58094
58137
  }
58095
58138
  ensureRuntimeDirs();
58096
58139
  const files = getRuntimeFiles();
58097
- const stdoutFd = openSync2(files.daemonLog, "a");
58098
- const stderrFd = openSync2(files.daemonLog, "a");
58140
+ const stdoutFd = openSync3(files.daemonLog, "a");
58141
+ const stderrFd = openSync3(files.daemonLog, "a");
58099
58142
  const childArgs = [process.argv[1] ?? "src/cli.ts", "__run-server", "--mode", "daemon"];
58100
58143
  if (flags.config) {
58101
58144
  childArgs.push("--config", resolveConfigArgPath(flags.config));
@@ -58116,8 +58159,8 @@ async function startDaemon(flags) {
58116
58159
  stderr: stderrFd,
58117
58160
  detached: true
58118
58161
  });
58119
- closeSync2(stdoutFd);
58120
- closeSync2(stderrFd);
58162
+ closeSync3(stdoutFd);
58163
+ closeSync3(stderrFd);
58121
58164
  child.unref();
58122
58165
  for (let i = 0;i < 24; i += 1) {
58123
58166
  await sleep(250);
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.1",
3
+ "version": "0.5.2",
4
4
  "packageManager": "bun@1.2.0",
5
5
  "workspaces": [
6
6
  "web"