@lakphy/local-router 0.3.3 → 0.4.0-beta.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.
package/dist/cli.js CHANGED
@@ -4935,7 +4935,7 @@ var require_compile = __commonJS((exports) => {
4935
4935
  const schOrFunc = root2.refs[ref];
4936
4936
  if (schOrFunc)
4937
4937
  return schOrFunc;
4938
- let _sch = resolve5.call(this, root2, ref);
4938
+ let _sch = resolve6.call(this, root2, ref);
4939
4939
  if (_sch === undefined) {
4940
4940
  const schema = (_a21 = root2.localRefs) === null || _a21 === undefined ? undefined : _a21[ref];
4941
4941
  const { schemaId } = this.opts;
@@ -4962,7 +4962,7 @@ var require_compile = __commonJS((exports) => {
4962
4962
  function sameSchemaEnv(s1, s2) {
4963
4963
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
4964
4964
  }
4965
- function resolve5(root2, ref) {
4965
+ function resolve6(root2, ref) {
4966
4966
  let sch;
4967
4967
  while (typeof (sch = this.refs[ref]) == "string")
4968
4968
  ref = sch;
@@ -5492,7 +5492,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5492
5492
  }
5493
5493
  return uri;
5494
5494
  }
5495
- function resolve5(baseURI, relativeURI, options) {
5495
+ function resolve6(baseURI, relativeURI, options) {
5496
5496
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
5497
5497
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
5498
5498
  schemelessOptions.skipEscape = true;
@@ -5720,7 +5720,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5720
5720
  var fastUri = {
5721
5721
  SCHEMES,
5722
5722
  normalize,
5723
- resolve: resolve5,
5723
+ resolve: resolve6,
5724
5724
  resolveComponent,
5725
5725
  equal,
5726
5726
  serialize,
@@ -10518,6 +10518,7 @@ import { parseArgs } from "util";
10518
10518
 
10519
10519
  // src/index.ts
10520
10520
  import { readFileSync as readFileSync4 } from "fs";
10521
+ import { dirname as dirname2, resolve as resolve6 } from "path";
10521
10522
 
10522
10523
  // node_modules/.bun/@ai-sdk+provider@3.0.8/node_modules/@ai-sdk/provider/dist/index.mjs
10523
10524
  var marker = "vercel.ai.error";
@@ -52118,6 +52119,14 @@ async function buildLogEventDetail(id, parsed, location, context2) {
52118
52119
  const responseBodyAvailable = event.response_body !== undefined;
52119
52120
  const streamCaptured = Boolean(event.stream_file);
52120
52121
  const { content: streamContent, warning: streamWarning } = readStreamContent(resolveLogBaseDir(context2.logConfig), event.stream_file);
52122
+ const hasPluginData = event.plugins_request || event.plugins_response || event.request_body_after_plugins !== undefined || event.request_url_after_plugins !== undefined || event.response_body_after_plugins !== undefined;
52123
+ const pluginsSection = hasPluginData ? {
52124
+ request: event.plugins_request,
52125
+ response: event.plugins_response,
52126
+ requestBodyAfterPlugins: event.request_body_after_plugins,
52127
+ requestUrlAfterPlugins: event.request_url_after_plugins,
52128
+ responseBodyAfterPlugins: event.response_body_after_plugins
52129
+ } : undefined;
52121
52130
  return {
52122
52131
  id,
52123
52132
  summary: {
@@ -52169,6 +52178,7 @@ async function buildLogEventDetail(id, parsed, location, context2) {
52169
52178
  ...streamWarning ? [streamWarning] : []
52170
52179
  ]
52171
52180
  },
52181
+ ...pluginsSection && { plugins: pluginsSection },
52172
52182
  rawEvent: event,
52173
52183
  location
52174
52184
  };
@@ -54081,10 +54091,301 @@ var openAPISpec = {
54081
54091
  }
54082
54092
  };
54083
54093
 
54094
+ // src/plugin-loader.ts
54095
+ import { resolve as resolve5, join as join8 } from "path";
54096
+ import { tmpdir } from "os";
54097
+ import { mkdtemp, writeFile, rm } from "fs/promises";
54098
+ function isLocalPath(pkg) {
54099
+ return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
54100
+ }
54101
+ function isRemoteUrl(pkg) {
54102
+ return pkg.startsWith("http://") || pkg.startsWith("https://");
54103
+ }
54104
+ function inferExtension(url2, contentType) {
54105
+ const pathname = new URL(url2).pathname;
54106
+ if (pathname.endsWith(".ts") || pathname.endsWith(".tsx"))
54107
+ return ".ts";
54108
+ if (pathname.endsWith(".mjs"))
54109
+ return ".mjs";
54110
+ if (pathname.endsWith(".cjs"))
54111
+ return ".cjs";
54112
+ if (contentType?.includes("typescript"))
54113
+ return ".ts";
54114
+ return ".js";
54115
+ }
54116
+ var remoteTmpDir = null;
54117
+ var remoteTmpFiles = [];
54118
+ async function ensureRemoteTmpDir() {
54119
+ if (!remoteTmpDir) {
54120
+ remoteTmpDir = await mkdtemp(join8(tmpdir(), "local-router-plugins-"));
54121
+ }
54122
+ return remoteTmpDir;
54123
+ }
54124
+ async function fetchRemotePlugin(url2) {
54125
+ const response = await fetch(url2);
54126
+ if (!response.ok) {
54127
+ throw new Error(`\u4E0B\u8F7D\u8FDC\u7A0B\u63D2\u4EF6\u5931\u8D25: HTTP ${response.status} ${response.statusText} (${url2})`);
54128
+ }
54129
+ const content = await response.text();
54130
+ const ext = inferExtension(url2, response.headers.get("content-type"));
54131
+ const dir = await ensureRemoteTmpDir();
54132
+ const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
54133
+ const filePath = join8(dir, fileName);
54134
+ await writeFile(filePath, content, "utf-8");
54135
+ remoteTmpFiles.push(filePath);
54136
+ return filePath;
54137
+ }
54138
+ async function cleanupRemoteTmpFiles() {
54139
+ if (remoteTmpDir) {
54140
+ try {
54141
+ await rm(remoteTmpDir, { recursive: true, force: true });
54142
+ } catch {}
54143
+ remoteTmpDir = null;
54144
+ remoteTmpFiles.length = 0;
54145
+ }
54146
+ }
54147
+ async function importPlugin(pkg, configDir) {
54148
+ let modulePath;
54149
+ if (isRemoteUrl(pkg)) {
54150
+ const localPath = await fetchRemotePlugin(pkg);
54151
+ modulePath = `${localPath}?t=${Date.now()}`;
54152
+ } else if (isLocalPath(pkg)) {
54153
+ const absolutePath = resolve5(configDir, pkg);
54154
+ modulePath = `${absolutePath}?t=${Date.now()}`;
54155
+ } else {
54156
+ modulePath = pkg;
54157
+ }
54158
+ const mod = await import(modulePath);
54159
+ const definition = mod.default ?? mod;
54160
+ if (!definition || typeof definition.name !== "string" || typeof definition.create !== "function") {
54161
+ throw new Error(`\u63D2\u4EF6 "${pkg}" \u5BFC\u51FA\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u9700\u5BFC\u51FA\u5305\u542B name \u548C create \u7684 PluginDefinition`);
54162
+ }
54163
+ return definition;
54164
+ }
54165
+
54166
+ class PluginManager {
54167
+ plugins = new Map;
54168
+ configDir;
54169
+ constructor(configDir) {
54170
+ this.configDir = configDir;
54171
+ }
54172
+ async loadPluginsForProvider(providerName, pluginConfigs) {
54173
+ const loaded = [];
54174
+ const failures = [];
54175
+ for (const config2 of pluginConfigs) {
54176
+ try {
54177
+ const definition = await importPlugin(config2.package, this.configDir);
54178
+ const instance2 = await definition.create(config2.params ?? {});
54179
+ loaded.push({ config: config2, definition, instance: instance2 });
54180
+ } catch (err) {
54181
+ const errorMsg = err instanceof Error ? err.message : String(err);
54182
+ console.error(`[plugin] \u52A0\u8F7D\u63D2\u4EF6 "${config2.package}" \u5931\u8D25 (provider: ${providerName}):`, errorMsg);
54183
+ failures.push({ provider: providerName, package: config2.package, error: errorMsg });
54184
+ }
54185
+ }
54186
+ return { loaded, failures };
54187
+ }
54188
+ async reloadAll(providers) {
54189
+ const newPlugins = new Map;
54190
+ const allFailures = [];
54191
+ const oldPluginsToDispose = [];
54192
+ for (const [providerName, providerConfig] of Object.entries(providers)) {
54193
+ if (providerConfig.plugins && providerConfig.plugins.length > 0) {
54194
+ const { loaded, failures } = await this.loadPluginsForProvider(providerName, providerConfig.plugins);
54195
+ allFailures.push(...failures);
54196
+ if (failures.length > 0) {
54197
+ console.warn(`[plugin] provider "${providerName}" \u6709\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u4FDD\u7559\u65E7\u63D2\u4EF6\u94FE`);
54198
+ const oldLoaded = this.plugins.get(providerName);
54199
+ if (oldLoaded) {
54200
+ newPlugins.set(providerName, oldLoaded);
54201
+ }
54202
+ for (const { instance: instance2, config: config2 } of loaded) {
54203
+ try {
54204
+ await instance2.dispose?.();
54205
+ } catch (err) {
54206
+ console.error(`[plugin] \u56DE\u6EDA\u9500\u6BC1\u63D2\u4EF6 "${config2.package}" \u5931\u8D25:`, err instanceof Error ? err.message : err);
54207
+ }
54208
+ }
54209
+ } else {
54210
+ newPlugins.set(providerName, loaded);
54211
+ const oldLoaded = this.plugins.get(providerName);
54212
+ if (oldLoaded) {
54213
+ oldPluginsToDispose.push(...oldLoaded);
54214
+ }
54215
+ }
54216
+ }
54217
+ }
54218
+ for (const [providerName, oldLoaded] of this.plugins) {
54219
+ if (!newPlugins.has(providerName)) {
54220
+ oldPluginsToDispose.push(...oldLoaded);
54221
+ }
54222
+ }
54223
+ this.plugins = newPlugins;
54224
+ if (oldPluginsToDispose.length > 0) {
54225
+ setTimeout(() => {
54226
+ this.disposePluginList(oldPluginsToDispose).catch((err) => {
54227
+ console.error("[plugin] \u65E7\u63D2\u4EF6\u9500\u6BC1\u5931\u8D25:", err);
54228
+ });
54229
+ }, 5000);
54230
+ }
54231
+ if (allFailures.length > 0) {
54232
+ console.warn(`[plugin] \u70ED\u91CD\u8F7D\u5B8C\u6210\uFF0C\u4F46\u6709 ${allFailures.length} \u4E2A\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25:`, allFailures.map((f) => `${f.provider}/${f.package}`).join(", "));
54233
+ }
54234
+ return { ok: allFailures.length === 0, failures: allFailures };
54235
+ }
54236
+ getPlugins(providerName) {
54237
+ const loaded = this.plugins.get(providerName);
54238
+ if (!loaded)
54239
+ return [];
54240
+ return loaded.map((l) => l.instance);
54241
+ }
54242
+ getLoadedPlugins(providerName) {
54243
+ return this.plugins.get(providerName) ?? [];
54244
+ }
54245
+ async disposeAll() {
54246
+ const allPlugins = [];
54247
+ for (const [, loadedPlugins] of this.plugins) {
54248
+ allPlugins.push(...loadedPlugins);
54249
+ }
54250
+ this.plugins.clear();
54251
+ await this.disposePluginList(allPlugins);
54252
+ await cleanupRemoteTmpFiles();
54253
+ }
54254
+ async disposePluginList(plugins) {
54255
+ for (const { instance: instance2, config: config2 } of plugins) {
54256
+ try {
54257
+ await instance2.dispose?.();
54258
+ } catch (err) {
54259
+ console.error(`[plugin] \u9500\u6BC1\u63D2\u4EF6 "${config2.package}" \u5931\u8D25:`, err instanceof Error ? err.message : err);
54260
+ }
54261
+ }
54262
+ }
54263
+ async disposePluginMap(pluginMap) {
54264
+ const allPlugins = [];
54265
+ for (const [, loadedPlugins] of pluginMap) {
54266
+ allPlugins.push(...loadedPlugins);
54267
+ }
54268
+ await this.disposePluginList(allPlugins);
54269
+ }
54270
+ }
54271
+
54084
54272
  // src/proxy.ts
54085
54273
  import { appendFile, readFile, unlink } from "fs/promises";
54086
- import { join as join8 } from "path";
54087
- import { tmpdir } from "os";
54274
+ import { join as join9 } from "path";
54275
+ import { tmpdir as tmpdir2 } from "os";
54276
+
54277
+ // src/plugin-engine.ts
54278
+ async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
54279
+ let currentUrl = url2;
54280
+ let currentHeaders = headers;
54281
+ let currentBody = body;
54282
+ for (const plugin of plugins) {
54283
+ if (!plugin.onRequest)
54284
+ continue;
54285
+ try {
54286
+ const result = await plugin.onRequest({
54287
+ ctx,
54288
+ url: currentUrl,
54289
+ headers: currentHeaders,
54290
+ body: currentBody
54291
+ });
54292
+ if (result) {
54293
+ if (result.url !== undefined)
54294
+ currentUrl = result.url;
54295
+ if (result.headers !== undefined)
54296
+ currentHeaders = result.headers;
54297
+ if (result.body !== undefined)
54298
+ currentBody = result.body;
54299
+ }
54300
+ } catch (err) {
54301
+ const error48 = err instanceof Error ? err : new Error(String(err));
54302
+ try {
54303
+ await plugin.onError?.({ ctx, phase: "request", error: error48 });
54304
+ } catch {}
54305
+ }
54306
+ }
54307
+ return { url: currentUrl, headers: currentHeaders, body: currentBody };
54308
+ }
54309
+ async function executeJsonResponsePlugins(plugins, ctx, status, headers, body) {
54310
+ let currentStatus = status;
54311
+ let currentHeaders = headers;
54312
+ let currentBody = body;
54313
+ for (let i = plugins.length - 1;i >= 0; i--) {
54314
+ const plugin = plugins[i];
54315
+ if (!plugin.onResponse)
54316
+ continue;
54317
+ try {
54318
+ const result = await plugin.onResponse({
54319
+ ctx,
54320
+ status: currentStatus,
54321
+ headers: currentHeaders,
54322
+ body: currentBody
54323
+ });
54324
+ if (result) {
54325
+ if (result.status !== undefined)
54326
+ currentStatus = result.status;
54327
+ if (result.headers !== undefined)
54328
+ currentHeaders = result.headers;
54329
+ if (result.body !== undefined)
54330
+ currentBody = result.body;
54331
+ }
54332
+ } catch (err) {
54333
+ const error48 = err instanceof Error ? err : new Error(String(err));
54334
+ try {
54335
+ await plugin.onError?.({ ctx, phase: "response", error: error48 });
54336
+ } catch {}
54337
+ }
54338
+ }
54339
+ return { status: currentStatus, headers: currentHeaders, body: currentBody };
54340
+ }
54341
+ async function createSSEPluginTransform(plugins, ctx, status, headers) {
54342
+ let currentStatus = status;
54343
+ let currentHeaders = headers;
54344
+ const transforms = [];
54345
+ for (let i = plugins.length - 1;i >= 0; i--) {
54346
+ const plugin = plugins[i];
54347
+ if (!plugin.onSSEResponse)
54348
+ continue;
54349
+ try {
54350
+ const result = await plugin.onSSEResponse({
54351
+ ctx,
54352
+ status: currentStatus,
54353
+ headers: currentHeaders
54354
+ });
54355
+ if (result) {
54356
+ if (result.status !== undefined)
54357
+ currentStatus = result.status;
54358
+ if (result.headers !== undefined)
54359
+ currentHeaders = result.headers;
54360
+ if (result.transform)
54361
+ transforms.push(result.transform);
54362
+ }
54363
+ } catch (err) {
54364
+ const error48 = err instanceof Error ? err : new Error(String(err));
54365
+ try {
54366
+ await plugin.onError?.({ ctx, phase: "response", error: error48 });
54367
+ } catch {}
54368
+ }
54369
+ }
54370
+ if (transforms.length === 0) {
54371
+ return { status: currentStatus, headers: currentHeaders, transform: null };
54372
+ }
54373
+ if (transforms.length === 1) {
54374
+ return { status: currentStatus, headers: currentHeaders, transform: transforms[0] };
54375
+ }
54376
+ const entry = new TransformStream;
54377
+ let stream = entry.readable;
54378
+ for (const t of transforms) {
54379
+ stream = stream.pipeThrough(t);
54380
+ }
54381
+ return {
54382
+ status: currentStatus,
54383
+ headers: currentHeaders,
54384
+ transform: { writable: entry.writable, readable: stream }
54385
+ };
54386
+ }
54387
+
54388
+ // src/proxy.ts
54088
54389
  var HOP_BY_HOP_HEADERS = new Set([
54089
54390
  "connection",
54090
54391
  "keep-alive",
@@ -54163,7 +54464,7 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
54163
54464
  };
54164
54465
  }
54165
54466
  function createTempStreamCapturePath(requestId) {
54166
- return join8(tmpdir(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
54467
+ return join9(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
54167
54468
  }
54168
54469
  async function appendTempStreamCapture(filePath, chunk) {
54169
54470
  await appendFile(filePath, chunk);
@@ -54185,33 +54486,63 @@ async function flushTempCaptureToLogger(tempPath, requestId, dateStr, logger) {
54185
54486
  }
54186
54487
  }
54187
54488
  async function proxyRequest(c2, options) {
54188
- const { logMeta } = options;
54489
+ const { logMeta, plugins, pluginConfigs } = options;
54189
54490
  const logger = getLogger();
54190
54491
  const shouldLog = logger?.enabled ?? false;
54191
- const headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
54492
+ const hasPlugins = plugins && plugins.length > 0;
54493
+ let targetUrl = options.targetUrl;
54494
+ let headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
54495
+ let bodyStr = options.body;
54496
+ const pluginLogOverrides = {};
54497
+ if (hasPlugins) {
54498
+ const bodyObj = JSON.parse(bodyStr);
54499
+ const ctx = {
54500
+ requestId: logMeta.requestId,
54501
+ provider: logMeta.provider,
54502
+ modelIn: logMeta.modelIn,
54503
+ modelOut: logMeta.modelOut,
54504
+ routeType: logMeta.routeType,
54505
+ isStream: logMeta.isStream
54506
+ };
54507
+ const result = await executeRequestPlugins(plugins, ctx, targetUrl, headers, bodyObj);
54508
+ if (pluginConfigs) {
54509
+ pluginLogOverrides.plugins_request = pluginConfigs;
54510
+ }
54511
+ if (result.url !== targetUrl) {
54512
+ targetUrl = result.url;
54513
+ pluginLogOverrides.request_url_after_plugins = targetUrl;
54514
+ }
54515
+ headers = result.headers;
54516
+ const newBodyStr = JSON.stringify(result.body);
54517
+ if (newBodyStr !== bodyStr) {
54518
+ bodyStr = newBodyStr;
54519
+ pluginLogOverrides.request_body_after_plugins = result.body;
54520
+ }
54521
+ }
54192
54522
  const requestBody = shouldLog && logger?.bodyPolicy !== "off" ? JSON.parse(options.body) : undefined;
54193
54523
  const proxy = options.proxy?.trim() ? options.proxy.trim() : undefined;
54194
54524
  let upstreamRes;
54195
54525
  try {
54196
- upstreamRes = await fetch(options.targetUrl, {
54526
+ upstreamRes = await fetch(targetUrl, {
54197
54527
  method: c2.req.method,
54198
54528
  headers,
54199
- body: options.body,
54529
+ body: bodyStr,
54200
54530
  ...proxy ? { proxy } : {},
54201
54531
  decompress: true
54202
54532
  });
54203
54533
  } catch (err) {
54204
54534
  if (shouldLog) {
54205
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), {
54535
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
54206
54536
  error_type: err instanceof Error ? err.constructor.name : "UnknownError",
54207
54537
  error_message: err instanceof Error ? err.message : String(err),
54208
- ...requestBody !== undefined && { request_body: requestBody }
54538
+ ...requestBody !== undefined && { request_body: requestBody },
54539
+ ...pluginLogOverrides
54209
54540
  }));
54210
54541
  }
54211
54542
  throw err;
54212
54543
  }
54213
54544
  const responseHeaders = buildResponseHeaders(upstreamRes.headers);
54214
- if (!shouldLog) {
54545
+ if (!shouldLog && !hasPlugins) {
54215
54546
  return new Response(upstreamRes.body, {
54216
54547
  status: upstreamRes.status,
54217
54548
  headers: responseHeaders
@@ -54221,6 +54552,33 @@ async function proxyRequest(c2, options) {
54221
54552
  const providerRequestId = extractProviderRequestId(upstreamRes.headers);
54222
54553
  const dateStr = new Date(logMeta.tsStart).toISOString().slice(0, 10);
54223
54554
  if (logMeta.isStream && upstreamRes.body) {
54555
+ let sseStatus = upstreamRes.status;
54556
+ let sseHeaders = responseHeaders;
54557
+ let sseTransform = null;
54558
+ if (hasPlugins) {
54559
+ const ctx = {
54560
+ requestId: logMeta.requestId,
54561
+ provider: logMeta.provider,
54562
+ modelIn: logMeta.modelIn,
54563
+ modelOut: logMeta.modelOut,
54564
+ routeType: logMeta.routeType,
54565
+ isStream: logMeta.isStream
54566
+ };
54567
+ const sseResult = await createSSEPluginTransform(plugins, ctx, upstreamRes.status, responseHeaders);
54568
+ sseStatus = sseResult.status;
54569
+ sseHeaders = sseResult.headers;
54570
+ sseTransform = sseResult.transform;
54571
+ if (pluginConfigs) {
54572
+ pluginLogOverrides.plugins_response = pluginConfigs;
54573
+ }
54574
+ }
54575
+ if (!shouldLog) {
54576
+ const outputBody2 = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
54577
+ return new Response(outputBody2, {
54578
+ status: sseStatus,
54579
+ headers: sseHeaders
54580
+ });
54581
+ }
54224
54582
  const [clientStream, logStream] = upstreamRes.body.tee();
54225
54583
  (async () => {
54226
54584
  const tempPath = createTempStreamCapturePath(logMeta.requestId);
@@ -54242,30 +54600,61 @@ async function proxyRequest(c2, options) {
54242
54600
  });
54243
54601
  console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5904\u7406\u5931\u8D25:", err);
54244
54602
  } finally {
54245
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), {
54246
- upstream_status: upstreamRes.status,
54603
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
54604
+ upstream_status: sseStatus,
54247
54605
  content_type_res: contentTypeRes,
54248
- response_headers: responseHeaders,
54606
+ response_headers: sseHeaders,
54249
54607
  stream_bytes: streamBytes,
54250
54608
  provider_request_id: providerRequestId,
54251
54609
  ...streamFile != null && { stream_file: streamFile },
54252
- ...requestBody !== undefined && { request_body: requestBody }
54610
+ ...requestBody !== undefined && { request_body: requestBody },
54611
+ ...pluginLogOverrides
54253
54612
  }));
54254
54613
  }
54255
54614
  })();
54256
- return new Response(clientStream, {
54257
- status: upstreamRes.status,
54258
- headers: responseHeaders
54615
+ const outputBody = sseTransform ? clientStream.pipeThrough(sseTransform) : clientStream;
54616
+ return new Response(outputBody, {
54617
+ status: sseStatus,
54618
+ headers: sseHeaders
54619
+ });
54620
+ }
54621
+ let responseText = await upstreamRes.text();
54622
+ let responseStatus = upstreamRes.status;
54623
+ let finalResponseHeaders = responseHeaders;
54624
+ if (hasPlugins) {
54625
+ const ctx = {
54626
+ requestId: logMeta.requestId,
54627
+ provider: logMeta.provider,
54628
+ modelIn: logMeta.modelIn,
54629
+ modelOut: logMeta.modelOut,
54630
+ routeType: logMeta.routeType,
54631
+ isStream: logMeta.isStream
54632
+ };
54633
+ const result = await executeJsonResponsePlugins(plugins, ctx, upstreamRes.status, responseHeaders, responseText);
54634
+ if (pluginConfigs) {
54635
+ pluginLogOverrides.plugins_response = pluginConfigs;
54636
+ }
54637
+ if (result.body !== responseText) {
54638
+ pluginLogOverrides.response_body_after_plugins = result.body;
54639
+ }
54640
+ responseStatus = result.status;
54641
+ finalResponseHeaders = result.headers;
54642
+ responseText = result.body;
54643
+ }
54644
+ if (!shouldLog) {
54645
+ return new Response(responseText, {
54646
+ status: responseStatus,
54647
+ headers: finalResponseHeaders
54259
54648
  });
54260
54649
  }
54261
- const responseText = await upstreamRes.text();
54262
54650
  const responseBytes = Buffer.byteLength(responseText, "utf-8");
54263
54651
  const eventOverrides = {
54264
54652
  upstream_status: upstreamRes.status,
54265
54653
  content_type_res: contentTypeRes,
54266
- response_headers: responseHeaders,
54654
+ response_headers: finalResponseHeaders,
54267
54655
  response_bytes: responseBytes,
54268
- provider_request_id: providerRequestId
54656
+ provider_request_id: providerRequestId,
54657
+ ...pluginLogOverrides
54269
54658
  };
54270
54659
  if (requestBody !== undefined) {
54271
54660
  eventOverrides.request_body = requestBody;
@@ -54273,10 +54662,10 @@ async function proxyRequest(c2, options) {
54273
54662
  if (logger?.bodyPolicy !== "off") {
54274
54663
  eventOverrides.response_body = responseText;
54275
54664
  }
54276
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), eventOverrides));
54665
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), eventOverrides));
54277
54666
  return new Response(responseText, {
54278
- status: upstreamRes.status,
54279
- headers: responseHeaders
54667
+ status: responseStatus,
54668
+ headers: finalResponseHeaders
54280
54669
  });
54281
54670
  }
54282
54671
 
@@ -54291,7 +54680,7 @@ function resolveRoute(modelMap, incomingModel) {
54291
54680
  return;
54292
54681
  }
54293
54682
  function createModelRoutingHandler(options) {
54294
- const { routeType, store, authType, buildTargetUrl } = options;
54683
+ const { routeType, store, authType, buildTargetUrl, pluginManager } = options;
54295
54684
  return async (c2) => {
54296
54685
  const config2 = store.get();
54297
54686
  const modelMap = config2.routes[routeType];
@@ -54333,49 +54722,60 @@ function createModelRoutingHandler(options) {
54333
54722
  requestBytes: Buffer.byteLength(body, "utf-8"),
54334
54723
  requestHeaders: collectHeaders(c2.req.raw.headers)
54335
54724
  };
54725
+ const plugins = pluginManager?.getPlugins(target.provider) ?? [];
54726
+ const pluginConfigs = pluginManager?.getLoadedPlugins(target.provider) ?? [];
54336
54727
  return proxyRequest(c2, {
54337
54728
  targetUrl,
54338
54729
  apiKey: provider.apiKey,
54339
54730
  proxy: provider.proxy,
54340
54731
  authType,
54341
54732
  body,
54342
- logMeta
54733
+ logMeta,
54734
+ plugins: plugins.length > 0 ? plugins : undefined,
54735
+ pluginConfigs: pluginConfigs.length > 0 ? pluginConfigs.map((lp) => ({
54736
+ name: lp.definition.name,
54737
+ package: lp.config.package,
54738
+ params: lp.config.params ?? {}
54739
+ })) : undefined
54343
54740
  });
54344
54741
  };
54345
54742
  }
54346
54743
 
54347
54744
  // src/routes/anthropic-messages.ts
54348
- function createAnthropicMessagesRoutes(routeType, store) {
54745
+ function createAnthropicMessagesRoutes(routeType, store, pluginManager) {
54349
54746
  const routes = new Hono2;
54350
54747
  routes.post("/v1/messages", createModelRoutingHandler({
54351
54748
  routeType,
54352
54749
  store,
54353
54750
  authType: "x-api-key",
54354
- buildTargetUrl: (base) => `${base}/v1/messages`
54751
+ buildTargetUrl: (base) => `${base}/v1/messages`,
54752
+ pluginManager
54355
54753
  }));
54356
54754
  return routes;
54357
54755
  }
54358
54756
 
54359
54757
  // src/routes/openai-completions.ts
54360
- function createOpenaiCompletionsRoutes(routeType, store) {
54758
+ function createOpenaiCompletionsRoutes(routeType, store, pluginManager) {
54361
54759
  const routes = new Hono2;
54362
54760
  routes.post("/v1/chat/completions", createModelRoutingHandler({
54363
54761
  routeType,
54364
54762
  store,
54365
54763
  authType: "bearer",
54366
- buildTargetUrl: (base) => `${base}/v1/chat/completions`
54764
+ buildTargetUrl: (base) => `${base}/v1/chat/completions`,
54765
+ pluginManager
54367
54766
  }));
54368
54767
  return routes;
54369
54768
  }
54370
54769
 
54371
54770
  // src/routes/openai-responses.ts
54372
- function createOpenaiResponsesRoutes(routeType, store) {
54771
+ function createOpenaiResponsesRoutes(routeType, store, pluginManager) {
54373
54772
  const routes = new Hono2;
54374
54773
  routes.post("/v1/responses", createModelRoutingHandler({
54375
54774
  routeType,
54376
54775
  store,
54377
54776
  authType: "bearer",
54378
- buildTargetUrl: (base) => `${base}/v1/responses`
54777
+ buildTargetUrl: (base) => `${base}/v1/responses`,
54778
+ pluginManager
54379
54779
  }));
54380
54780
  return routes;
54381
54781
  }
@@ -54536,7 +54936,7 @@ function createChatProxyModel(providerName, providerConfig, model) {
54536
54936
  throw new Error(`\u6682\u4E0D\u652F\u6301\u7684 provider \u7C7B\u578B: ${providerConfig.type}`);
54537
54937
  }
54538
54938
  }
54539
- function createAdminApiRoutes(store, registerCleanup) {
54939
+ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
54540
54940
  const api2 = new Hono2;
54541
54941
  const cryptoSessions = new Map;
54542
54942
  const CRYPTO_SESSION_TTL_MS = 2 * 60 * 1000;
@@ -54647,18 +55047,22 @@ function createAdminApiRoutes(store, registerCleanup) {
54647
55047
  session.dispose();
54648
55048
  }
54649
55049
  });
54650
- api2.post("/config/apply", (_c) => {
55050
+ api2.post("/config/apply", async (_c) => {
54651
55051
  try {
54652
55052
  const config2 = store.reload();
54653
55053
  if (config2.log) {
54654
55054
  const logBaseDir = resolveLogBaseDir(config2.log);
54655
55055
  initLogger(logBaseDir, config2.log);
54656
55056
  }
55057
+ const pluginResult = await pluginManager.reloadAll(config2.providers);
54657
55058
  return _c.json({
54658
55059
  ok: true,
54659
55060
  summary: {
54660
55061
  providers: Object.keys(config2.providers).length,
54661
55062
  routes: Object.keys(config2.routes).length
55063
+ },
55064
+ ...pluginResult.failures.length > 0 && {
55065
+ pluginWarnings: pluginResult.failures
54662
55066
  }
54663
55067
  });
54664
55068
  } catch (err) {
@@ -55083,7 +55487,7 @@ async function proxyAdminToDevServer(c2, origin) {
55083
55487
  headers: buildProxyResponseHeaders(upstreamRes.headers)
55084
55488
  });
55085
55489
  }
55086
- function createApp(store, options) {
55490
+ async function createApp(store, options) {
55087
55491
  const config2 = store.get();
55088
55492
  console.log(`\u5DF2\u52A0\u8F7D\u914D\u7F6E: ${store.getPath()}`);
55089
55493
  if (config2.log) {
@@ -55094,15 +55498,24 @@ function createApp(store, options) {
55094
55498
  }
55095
55499
  const stopLogStorageTask = startLogStorageBackgroundTask(config2.log);
55096
55500
  options?.registerCleanup?.(stopLogStorageTask);
55501
+ const configDir = dirname2(resolve6(store.getPath()));
55502
+ const pluginManager = new PluginManager(configDir);
55503
+ const reloadResult = await pluginManager.reloadAll(config2.providers);
55504
+ if (!reloadResult.ok) {
55505
+ console.warn(`[plugin] \u63D2\u4EF6\u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u4F46\u6709 ${reloadResult.failures.length} \u4E2A\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25`);
55506
+ }
55507
+ options?.registerCleanup?.(() => {
55508
+ pluginManager.disposeAll().catch(() => {});
55509
+ });
55097
55510
  printIntegrationGuide(config2);
55098
55511
  const app = new Hono2;
55099
55512
  app.get("/", (c2) => c2.text("local-router is running"));
55100
55513
  for (const [routeType, entry] of Object.entries(ROUTE_REGISTRY)) {
55101
- const subApp = entry.create(routeType, store);
55514
+ const subApp = entry.create(routeType, store, pluginManager);
55102
55515
  app.route(entry.mountPrefix, subApp);
55103
55516
  console.log(`\u5DF2\u6CE8\u518C\u8DEF\u7531: ${routeType} -> ${entry.mountPrefix}`);
55104
55517
  }
55105
- app.route("/api", createAdminApiRoutes(store, options?.registerCleanup));
55518
+ app.route("/api", createAdminApiRoutes(store, pluginManager, options?.registerCleanup));
55106
55519
  console.log("\u5DF2\u6CE8\u518C\u7BA1\u7406 API: /api");
55107
55520
  app.get("/api/docs", middleware({ url: "/api/openapi.json" }));
55108
55521
  app.get("/api/openapi.json", (c2) => c2.json(openAPISpec));
@@ -55131,10 +55544,10 @@ function createApp(store, options) {
55131
55544
  }
55132
55545
  return app;
55133
55546
  }
55134
- function createAppRuntimeFromConfigPath(configPath) {
55547
+ async function createAppRuntimeFromConfigPath(configPath) {
55135
55548
  const store = new ConfigStore(configPath);
55136
55549
  const cleanups = [];
55137
- const app = createApp(store, {
55550
+ const app = await createApp(store, {
55138
55551
  registerCleanup: (cleanup) => {
55139
55552
  cleanups.push(cleanup);
55140
55553
  }
@@ -55152,7 +55565,7 @@ function createAppRuntimeFromConfigPath(configPath) {
55152
55565
  }
55153
55566
 
55154
55567
  // src/server.ts
55155
- var DEFAULT_IDLE_TIMEOUT_SECONDS = 600;
55568
+ var DEFAULT_IDLE_TIMEOUT_SECONDS = 0;
55156
55569
  function resolveIdleTimeoutSeconds(explicit) {
55157
55570
  if (typeof explicit === "number" && Number.isFinite(explicit) && explicit >= 0) {
55158
55571
  return explicit;
@@ -55167,8 +55580,8 @@ function resolveIdleTimeoutSeconds(explicit) {
55167
55580
  }
55168
55581
  return DEFAULT_IDLE_TIMEOUT_SECONDS;
55169
55582
  }
55170
- function startServer(options) {
55171
- const runtime = createAppRuntimeFromConfigPath(options.configPath);
55583
+ async function startServer(options) {
55584
+ const runtime = await createAppRuntimeFromConfigPath(options.configPath);
55172
55585
  const idleTimeout = resolveIdleTimeoutSeconds(options.idleTimeoutSeconds);
55173
55586
  const server = Bun.serve({
55174
55587
  fetch: runtime.app.fetch,
@@ -55193,21 +55606,21 @@ function startServer(options) {
55193
55606
  // src/cli/runtime.ts
55194
55607
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
55195
55608
  import { homedir as homedir2 } from "os";
55196
- import { join as join9, resolve as resolve5 } from "path";
55609
+ import { join as join10, resolve as resolve7 } from "path";
55197
55610
  function getRuntimeDirs() {
55198
- const root2 = join9(homedir2(), ".local-router");
55611
+ const root2 = join10(homedir2(), ".local-router");
55199
55612
  return {
55200
55613
  root: root2,
55201
- run: join9(root2, "run"),
55202
- logs: join9(root2, "logs")
55614
+ run: join10(root2, "run"),
55615
+ logs: join10(root2, "logs")
55203
55616
  };
55204
55617
  }
55205
55618
  function getRuntimeFiles() {
55206
55619
  const dirs = getRuntimeDirs();
55207
55620
  return {
55208
- pid: join9(dirs.run, "local-router.pid"),
55209
- state: join9(dirs.run, "status.json"),
55210
- daemonLog: join9(dirs.logs, "daemon.log")
55621
+ pid: join10(dirs.run, "local-router.pid"),
55622
+ state: join10(dirs.run, "status.json"),
55623
+ daemonLog: join10(dirs.logs, "daemon.log")
55211
55624
  };
55212
55625
  }
55213
55626
  function ensureRuntimeDirs() {
@@ -55240,7 +55653,7 @@ function clearRuntimeFiles() {
55240
55653
  rmSync(files.state, { force: true });
55241
55654
  }
55242
55655
  function resolveConfigArgPath(pathValue) {
55243
- return resolve5(pathValue);
55656
+ return resolve7(pathValue);
55244
55657
  }
55245
55658
 
55246
55659
  // src/cli/process.ts
@@ -55319,7 +55732,7 @@ async function runServerProcess(opts) {
55319
55732
  const idleTimeoutSeconds = opts.idleTimeoutSeconds ?? Number.parseInt(process.env.LOCAL_ROUTER_IDLE_TIMEOUT ?? "", 10);
55320
55733
  let running;
55321
55734
  try {
55322
- running = startServer({
55735
+ running = await startServer({
55323
55736
  configPath: ensured.path,
55324
55737
  host,
55325
55738
  port,
@@ -55471,7 +55884,7 @@ function readLogDelta(filePath, offset) {
55471
55884
  // src/cli/config-command.ts
55472
55885
  import { createInterface as createInterface4 } from "readline/promises";
55473
55886
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
55474
- import { dirname as dirname2, join as join10 } from "path";
55887
+ import { dirname as dirname3, join as join11 } from "path";
55475
55888
  import { parseArgs as parseArgs2 } from "util";
55476
55889
  function readConfig(configArg) {
55477
55890
  const path = resolveConfigPath(configArg);
@@ -55479,9 +55892,9 @@ function readConfig(configArg) {
55479
55892
  }
55480
55893
  function saveConfig(path, config2) {
55481
55894
  validateConfigOrThrow(config2);
55482
- const backupDir = join10(dirname2(path), ".backups");
55895
+ const backupDir = join11(dirname3(path), ".backups");
55483
55896
  mkdirSync4(backupDir, { recursive: true });
55484
- const backupPath = join10(backupDir, `config-${Date.now()}.json5`);
55897
+ const backupPath = join11(backupDir, `config-${Date.now()}.json5`);
55485
55898
  writeFileSync5(backupPath, readFileSync7(path, "utf-8"), "utf-8");
55486
55899
  const content = dist_default.stringify(config2, { space: 2, quote: '"' });
55487
55900
  writeFileSync5(path, content, "utf-8");