@lakphy/local-router 0.5.2 → 0.5.5

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/entry.js CHANGED
@@ -4934,7 +4934,7 @@ var require_compile = __commonJS((exports) => {
4934
4934
  const schOrFunc = root2.refs[ref];
4935
4935
  if (schOrFunc)
4936
4936
  return schOrFunc;
4937
- let _sch = resolve4.call(this, root2, ref);
4937
+ let _sch = resolve5.call(this, root2, ref);
4938
4938
  if (_sch === undefined) {
4939
4939
  const schema = (_a21 = root2.localRefs) === null || _a21 === undefined ? undefined : _a21[ref];
4940
4940
  const { schemaId } = this.opts;
@@ -4961,7 +4961,7 @@ var require_compile = __commonJS((exports) => {
4961
4961
  function sameSchemaEnv(s1, s2) {
4962
4962
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
4963
4963
  }
4964
- function resolve4(root2, ref) {
4964
+ function resolve5(root2, ref) {
4965
4965
  let sch;
4966
4966
  while (typeof (sch = this.refs[ref]) == "string")
4967
4967
  ref = sch;
@@ -5491,7 +5491,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5491
5491
  }
5492
5492
  return uri;
5493
5493
  }
5494
- function resolve4(baseURI, relativeURI, options) {
5494
+ function resolve5(baseURI, relativeURI, options) {
5495
5495
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
5496
5496
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
5497
5497
  schemelessOptions.skipEscape = true;
@@ -5719,7 +5719,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5719
5719
  var fastUri = {
5720
5720
  SCHEMES,
5721
5721
  normalize,
5722
- resolve: resolve4,
5722
+ resolve: resolve5,
5723
5723
  resolveComponent,
5724
5724
  equal,
5725
5725
  serialize,
@@ -9284,8 +9284,218 @@ var require_dist2 = __commonJS((exports, module) => {
9284
9284
  });
9285
9285
 
9286
9286
  // src/index.ts
9287
- import { readFileSync as readFileSync4 } from "fs";
9288
- import { dirname as dirname2, resolve as resolve6 } from "path";
9287
+ import { readFileSync as readFileSync6 } from "fs";
9288
+ import { dirname as dirname3, resolve as resolve8 } from "path";
9289
+
9290
+ // src/cli/autostart.ts
9291
+ import { execSync } from "child_process";
9292
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
9293
+ import { homedir as homedir2, platform } from "os";
9294
+ import { dirname, join as join2 } from "path";
9295
+
9296
+ // src/cli/runtime.ts
9297
+ import { homedir } from "os";
9298
+ import { join, resolve } from "path";
9299
+ function getRuntimeDirs() {
9300
+ const override = process.env.LOCAL_ROUTER_RUNTIME_DIR;
9301
+ const root = override?.trim() ? override.trim() : join(homedir(), ".local-router");
9302
+ return {
9303
+ root,
9304
+ run: join(root, "run"),
9305
+ logs: join(root, "logs")
9306
+ };
9307
+ }
9308
+
9309
+ // src/cli/autostart.ts
9310
+ var LABEL = "com.lakphy.local-router";
9311
+ function getDaemonLogPath() {
9312
+ return getRuntimeDirs().logs + "/daemon.log";
9313
+ }
9314
+ function getLaunchAgentPath() {
9315
+ return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
9316
+ }
9317
+ function buildPlist(opts) {
9318
+ const logPath = getDaemonLogPath();
9319
+ const args = [opts.execPath, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join(`
9320
+ `);
9321
+ return `<?xml version="1.0" encoding="UTF-8"?>
9322
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9323
+ <plist version="1.0">
9324
+ <dict>
9325
+ <key>Label</key>
9326
+ <string>${escapeXml(opts.label)}</string>
9327
+ <key>ProgramArguments</key>
9328
+ <array>
9329
+ ${args}
9330
+ </array>
9331
+ <key>RunAtLoad</key>
9332
+ <true/>
9333
+ <key>KeepAlive</key>
9334
+ <false/>
9335
+ <key>StandardOutPath</key>
9336
+ <string>${escapeXml(logPath)}</string>
9337
+ <key>StandardErrorPath</key>
9338
+ <string>${escapeXml(logPath)}</string>
9339
+ </dict>
9340
+ </plist>
9341
+ `;
9342
+ }
9343
+ function escapeXml(s) {
9344
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9345
+ }
9346
+ function createMacosManager() {
9347
+ const plistPath = getLaunchAgentPath();
9348
+ return {
9349
+ platform: "macos",
9350
+ async isInstalled() {
9351
+ return existsSync(plistPath);
9352
+ },
9353
+ async install(opts) {
9354
+ const dir = dirname(plistPath);
9355
+ if (!existsSync(dir))
9356
+ mkdirSync(dir, { recursive: true });
9357
+ writeFileSync(plistPath, buildPlist(opts), "utf-8");
9358
+ try {
9359
+ execSync(`launchctl bootout gui/$(id -u) ${plistPath} 2>/dev/null`, { stdio: "ignore" });
9360
+ } catch {}
9361
+ execSync(`launchctl bootstrap gui/$(id -u) ${plistPath}`, { stdio: "ignore" });
9362
+ },
9363
+ async uninstall() {
9364
+ if (!existsSync(plistPath))
9365
+ return;
9366
+ try {
9367
+ execSync(`launchctl bootout gui/$(id -u) ${plistPath}`, { stdio: "ignore" });
9368
+ } catch {}
9369
+ rmSync(plistPath, { force: true });
9370
+ },
9371
+ getServicePath() {
9372
+ return plistPath;
9373
+ }
9374
+ };
9375
+ }
9376
+ function getSystemdUnitPath() {
9377
+ return join2(homedir2(), ".config", "systemd", "user", "local-router.service");
9378
+ }
9379
+ function buildUnit(opts) {
9380
+ const logPath = getDaemonLogPath();
9381
+ const execStart = [opts.execPath, ...opts.args].join(" ");
9382
+ return `[Unit]
9383
+ Description=Local Router API Gateway
9384
+ After=network-online.target
9385
+
9386
+ [Service]
9387
+ Type=simple
9388
+ ExecStart=${execStart}
9389
+ Restart=on-failure
9390
+ RestartSec=5
9391
+ StandardOutput=append:${logPath}
9392
+ StandardError=append:${logPath}
9393
+
9394
+ [Install]
9395
+ WantedBy=default.target
9396
+ `;
9397
+ }
9398
+ function createLinuxManager() {
9399
+ const unitPath = getSystemdUnitPath();
9400
+ return {
9401
+ platform: "linux",
9402
+ async isInstalled() {
9403
+ if (!existsSync(unitPath))
9404
+ return false;
9405
+ try {
9406
+ const out = execSync("systemctl --user is-enabled local-router 2>/dev/null", {
9407
+ encoding: "utf-8"
9408
+ }).trim();
9409
+ return out === "enabled";
9410
+ } catch {
9411
+ return false;
9412
+ }
9413
+ },
9414
+ async install(opts) {
9415
+ const dir = dirname(unitPath);
9416
+ if (!existsSync(dir))
9417
+ mkdirSync(dir, { recursive: true });
9418
+ writeFileSync(unitPath, buildUnit(opts), "utf-8");
9419
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
9420
+ execSync("systemctl --user enable local-router", { stdio: "ignore" });
9421
+ },
9422
+ async uninstall() {
9423
+ try {
9424
+ execSync("systemctl --user disable local-router", { stdio: "ignore" });
9425
+ } catch {}
9426
+ rmSync(unitPath, { force: true });
9427
+ try {
9428
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
9429
+ } catch {}
9430
+ },
9431
+ getServicePath() {
9432
+ return unitPath;
9433
+ }
9434
+ };
9435
+ }
9436
+ var WIN_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
9437
+ var WIN_REG_VALUE = "LocalRouter";
9438
+ function createWindowsManager() {
9439
+ return {
9440
+ platform: "windows",
9441
+ async isInstalled() {
9442
+ try {
9443
+ execSync(`reg query "${WIN_REG_KEY}" /v ${WIN_REG_VALUE}`, { stdio: "ignore" });
9444
+ return true;
9445
+ } catch {
9446
+ return false;
9447
+ }
9448
+ },
9449
+ async install(opts) {
9450
+ const cmd = [opts.execPath, ...opts.args].map((a) => `"${a}"`).join(" ");
9451
+ execSync(`reg add "${WIN_REG_KEY}" /v ${WIN_REG_VALUE} /t REG_SZ /d "${cmd}" /f`, {
9452
+ stdio: "ignore"
9453
+ });
9454
+ },
9455
+ async uninstall() {
9456
+ try {
9457
+ execSync(`reg delete "${WIN_REG_KEY}" /v ${WIN_REG_VALUE} /f`, { stdio: "ignore" });
9458
+ } catch {}
9459
+ },
9460
+ getServicePath() {
9461
+ return `${WIN_REG_KEY}\\${WIN_REG_VALUE}`;
9462
+ }
9463
+ };
9464
+ }
9465
+ function createUnsupportedManager() {
9466
+ return {
9467
+ platform: "unsupported",
9468
+ async isInstalled() {
9469
+ return false;
9470
+ },
9471
+ async install() {
9472
+ throw new Error("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u81EA\u542F\u52A8");
9473
+ },
9474
+ async uninstall() {
9475
+ throw new Error("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u81EA\u542F\u52A8");
9476
+ },
9477
+ getServicePath() {
9478
+ return "";
9479
+ }
9480
+ };
9481
+ }
9482
+ function createAutostartManager() {
9483
+ const p = platform();
9484
+ if (p === "darwin")
9485
+ return createMacosManager();
9486
+ if (p === "linux")
9487
+ return createLinuxManager();
9488
+ if (p === "win32")
9489
+ return createWindowsManager();
9490
+ return createUnsupportedManager();
9491
+ }
9492
+ function getAutostartExecArgs() {
9493
+ const script = process.argv[1] ?? "dist/cli.js";
9494
+ return {
9495
+ execPath: process.execPath,
9496
+ args: [script, "__run-server", "--mode", "daemon"]
9497
+ };
9498
+ }
9289
9499
 
9290
9500
  // node_modules/.bun/@ai-sdk+provider@3.0.8/node_modules/@ai-sdk/provider/dist/index.mjs
9291
9501
  var marker = "vercel.ai.error";
@@ -29133,7 +29343,7 @@ function createProviderToolFactoryWithOutputSchema({
29133
29343
  supportsDeferredResults
29134
29344
  });
29135
29345
  }
29136
- async function resolve(value) {
29346
+ async function resolve2(value) {
29137
29347
  if (typeof value === "function") {
29138
29348
  value = value();
29139
29349
  }
@@ -32078,11 +32288,11 @@ var AnthropicMessagesLanguageModel = class {
32078
32288
  betas,
32079
32289
  headers
32080
32290
  }) {
32081
- return combineHeaders(await resolve(this.config.headers), headers, betas.size > 0 ? { "anthropic-beta": Array.from(betas).join(",") } : {});
32291
+ return combineHeaders(await resolve2(this.config.headers), headers, betas.size > 0 ? { "anthropic-beta": Array.from(betas).join(",") } : {});
32082
32292
  }
32083
32293
  async getBetasFromHeaders(requestHeaders) {
32084
32294
  var _a16, _b16;
32085
- const configHeaders = await resolve(this.config.headers);
32295
+ const configHeaders = await resolve2(this.config.headers);
32086
32296
  const configBetaHeader = (_a16 = configHeaders["anthropic-beta"]) != null ? _a16 : "";
32087
32297
  const requestBetaHeader = (_b16 = requestHeaders == null ? undefined : requestHeaders["anthropic-beta"]) != null ? _b16 : "";
32088
32298
  return new Set([
@@ -41267,7 +41477,7 @@ var GatewayFetchMetadata = class {
41267
41477
  try {
41268
41478
  const { value } = await getFromApi({
41269
41479
  url: `${this.config.baseURL}/config`,
41270
- headers: await resolve(this.config.headers()),
41480
+ headers: await resolve2(this.config.headers()),
41271
41481
  successfulResponseHandler: createJsonResponseHandler(gatewayAvailableModelsResponseSchema),
41272
41482
  failedResponseHandler: createJsonErrorResponseHandler({
41273
41483
  errorSchema: exports_external.any(),
@@ -41285,7 +41495,7 @@ var GatewayFetchMetadata = class {
41285
41495
  const baseUrl = new URL(this.config.baseURL);
41286
41496
  const { value } = await getFromApi({
41287
41497
  url: `${baseUrl.origin}/v1/credits`,
41288
- headers: await resolve(this.config.headers()),
41498
+ headers: await resolve2(this.config.headers()),
41289
41499
  successfulResponseHandler: createJsonResponseHandler(gatewayCreditsResponseSchema),
41290
41500
  failedResponseHandler: createJsonErrorResponseHandler({
41291
41501
  errorSchema: exports_external.any(),
@@ -41350,7 +41560,7 @@ var GatewayLanguageModel = class {
41350
41560
  async doGenerate(options) {
41351
41561
  const { args, warnings } = await this.getArgs(options);
41352
41562
  const { abortSignal } = options;
41353
- const resolvedHeaders = await resolve(this.config.headers());
41563
+ const resolvedHeaders = await resolve2(this.config.headers());
41354
41564
  try {
41355
41565
  const {
41356
41566
  responseHeaders,
@@ -41358,7 +41568,7 @@ var GatewayLanguageModel = class {
41358
41568
  rawValue: rawResponse
41359
41569
  } = await postJsonToApi({
41360
41570
  url: this.getUrl(),
41361
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve(this.config.o11yHeaders)),
41571
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve2(this.config.o11yHeaders)),
41362
41572
  body: args,
41363
41573
  successfulResponseHandler: createJsonResponseHandler(exports_external.any()),
41364
41574
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -41381,11 +41591,11 @@ var GatewayLanguageModel = class {
41381
41591
  async doStream(options) {
41382
41592
  const { args, warnings } = await this.getArgs(options);
41383
41593
  const { abortSignal } = options;
41384
- const resolvedHeaders = await resolve(this.config.headers());
41594
+ const resolvedHeaders = await resolve2(this.config.headers());
41385
41595
  try {
41386
41596
  const { value: response, responseHeaders } = await postJsonToApi({
41387
41597
  url: this.getUrl(),
41388
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve(this.config.o11yHeaders)),
41598
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve2(this.config.o11yHeaders)),
41389
41599
  body: args,
41390
41600
  successfulResponseHandler: createEventSourceResponseHandler(exports_external.any()),
41391
41601
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -41471,7 +41681,7 @@ var GatewayEmbeddingModel = class {
41471
41681
  providerOptions
41472
41682
  }) {
41473
41683
  var _a92;
41474
- const resolvedHeaders = await resolve(this.config.headers());
41684
+ const resolvedHeaders = await resolve2(this.config.headers());
41475
41685
  try {
41476
41686
  const {
41477
41687
  responseHeaders,
@@ -41479,7 +41689,7 @@ var GatewayEmbeddingModel = class {
41479
41689
  rawValue
41480
41690
  } = await postJsonToApi({
41481
41691
  url: this.getUrl(),
41482
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve(this.config.o11yHeaders)),
41692
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve2(this.config.o11yHeaders)),
41483
41693
  body: {
41484
41694
  values,
41485
41695
  ...providerOptions ? { providerOptions } : {}
@@ -41541,7 +41751,7 @@ var GatewayImageModel = class {
41541
41751
  abortSignal
41542
41752
  }) {
41543
41753
  var _a92, _b92, _c, _d;
41544
- const resolvedHeaders = await resolve(this.config.headers());
41754
+ const resolvedHeaders = await resolve2(this.config.headers());
41545
41755
  try {
41546
41756
  const {
41547
41757
  responseHeaders,
@@ -41549,7 +41759,7 @@ var GatewayImageModel = class {
41549
41759
  rawValue
41550
41760
  } = await postJsonToApi({
41551
41761
  url: this.getUrl(),
41552
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve(this.config.o11yHeaders)),
41762
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve2(this.config.o11yHeaders)),
41553
41763
  body: {
41554
41764
  prompt,
41555
41765
  n,
@@ -41664,11 +41874,11 @@ var GatewayVideoModel = class {
41664
41874
  abortSignal
41665
41875
  }) {
41666
41876
  var _a92;
41667
- const resolvedHeaders = await resolve(this.config.headers());
41877
+ const resolvedHeaders = await resolve2(this.config.headers());
41668
41878
  try {
41669
41879
  const { responseHeaders, value: responseBody } = await postJsonToApi({
41670
41880
  url: this.getUrl(),
41671
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve(this.config.o11yHeaders), { accept: "text/event-stream" }),
41881
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve2(this.config.o11yHeaders), { accept: "text/event-stream" }),
41672
41882
  body: {
41673
41883
  prompt,
41674
41884
  n,
@@ -44539,7 +44749,7 @@ var object2 = ({
44539
44749
  const schema = asSchema(inputSchema);
44540
44750
  return {
44541
44751
  name: "object",
44542
- responseFormat: resolve(schema.jsonSchema).then((jsonSchema2) => ({
44752
+ responseFormat: resolve2(schema.jsonSchema).then((jsonSchema2) => ({
44543
44753
  type: "json",
44544
44754
  schema: jsonSchema2,
44545
44755
  ...name21 != null && { name: name21 },
@@ -44601,7 +44811,7 @@ var array2 = ({
44601
44811
  const elementSchema = asSchema(inputElementSchema);
44602
44812
  return {
44603
44813
  name: "array",
44604
- responseFormat: resolve(elementSchema.jsonSchema).then((jsonSchema2) => {
44814
+ responseFormat: resolve2(elementSchema.jsonSchema).then((jsonSchema2) => {
44605
44815
  const { $schema, ...itemSchema } = jsonSchema2;
44606
44816
  return {
44607
44817
  type: "json",
@@ -49860,7 +50070,7 @@ var Hono2 = class extends Hono {
49860
50070
 
49861
50071
  // node_modules/.bun/hono@4.12.5/node_modules/hono/dist/adapter/bun/serve-static.js
49862
50072
  import { stat } from "fs/promises";
49863
- import { join } from "path";
50073
+ import { join as join3 } from "path";
49864
50074
 
49865
50075
  // node_modules/.bun/hono@4.12.5/node_modules/hono/dist/utils/compress.js
49866
50076
  var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
@@ -49965,7 +50175,7 @@ var DEFAULT_DOCUMENT = "index.html";
49965
50175
  var serveStatic = (options) => {
49966
50176
  const root = options.root ?? "./";
49967
50177
  const optionPath = options.path;
49968
- const join = options.join ?? defaultJoin;
50178
+ const join3 = options.join ?? defaultJoin;
49969
50179
  return async (c, next) => {
49970
50180
  if (c.finalized) {
49971
50181
  return next();
@@ -49984,9 +50194,9 @@ var serveStatic = (options) => {
49984
50194
  return next();
49985
50195
  }
49986
50196
  }
49987
- let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename);
50197
+ let path = join3(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename);
49988
50198
  if (options.isDir && await options.isDir(path)) {
49989
- path = join(path, DEFAULT_DOCUMENT);
50199
+ path = join3(path, DEFAULT_DOCUMENT);
49990
50200
  }
49991
50201
  const getContent = options.getContent;
49992
50202
  let content = await getContent(path, c);
@@ -50038,7 +50248,7 @@ var serveStatic2 = (options) => {
50038
50248
  return serveStatic({
50039
50249
  ...options,
50040
50250
  getContent,
50041
- join,
50251
+ join: join3,
50042
50252
  isDir
50043
50253
  })(c, next);
50044
50254
  };
@@ -50130,9 +50340,9 @@ var upgradeWebSocket = defineWebSocketHelper((c, events) => {
50130
50340
  });
50131
50341
 
50132
50342
  // src/config.ts
50133
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
50134
- import { homedir } from "os";
50135
- import { join as join2, resolve as resolve2 } from "path";
50343
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
50344
+ import { homedir as homedir3 } from "os";
50345
+ import { join as join4, resolve as resolve3 } from "path";
50136
50346
 
50137
50347
  // node_modules/.bun/json5@2.2.3/node_modules/json5/dist/index.mjs
50138
50348
  var Space_Separator = /[\u1680\u2000-\u200A\u202F\u205F\u3000]/;
@@ -51300,8 +51510,8 @@ var DEFAULT_CONFIG = `{
51300
51510
  // },
51301
51511
  }`;
51302
51512
  function loadConfig(configPath) {
51303
- const absolutePath = resolve2(configPath);
51304
- const content = readFileSync(absolutePath, "utf-8");
51513
+ const absolutePath = resolve3(configPath);
51514
+ const content = readFileSync2(absolutePath, "utf-8");
51305
51515
  const config2 = dist_default.parse(content);
51306
51516
  for (const [routeType, modelMap] of Object.entries(config2.routes)) {
51307
51517
  if (!modelMap["*"]) {
@@ -51316,21 +51526,21 @@ function loadConfig(configPath) {
51316
51526
  return config2;
51317
51527
  }
51318
51528
  function createDefaultConfig(configPath) {
51319
- const configDir = resolve2(configPath, "..");
51320
- if (!existsSync(configDir)) {
51321
- mkdirSync(configDir, { recursive: true });
51529
+ const configDir = resolve3(configPath, "..");
51530
+ if (!existsSync2(configDir)) {
51531
+ mkdirSync2(configDir, { recursive: true });
51322
51532
  }
51323
- writeFileSync(configPath, DEFAULT_CONFIG, "utf-8");
51533
+ writeFileSync2(configPath, DEFAULT_CONFIG, "utf-8");
51324
51534
  console.log(`\u5DF2\u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E\u6587\u4EF6: ${configPath}`);
51325
51535
  }
51326
51536
  function resolveDefaultConfigPath() {
51327
51537
  const localConfig = "config.json5";
51328
- if (existsSync(localConfig)) {
51538
+ if (existsSync2(localConfig)) {
51329
51539
  return localConfig;
51330
51540
  }
51331
- const globalConfigDir = join2(homedir(), ".local-router");
51332
- const globalConfig2 = join2(globalConfigDir, "config.json5");
51333
- if (!existsSync(globalConfig2)) {
51541
+ const globalConfigDir = join4(homedir3(), ".local-router");
51542
+ const globalConfig2 = join4(globalConfigDir, "config.json5");
51543
+ if (!existsSync2(globalConfig2)) {
51334
51544
  createDefaultConfig(globalConfig2);
51335
51545
  }
51336
51546
  return globalConfig2;
@@ -51349,19 +51559,19 @@ function parseConfigPath() {
51349
51559
  }
51350
51560
  function resolveLogBaseDir(logConfig) {
51351
51561
  if (logConfig?.baseDir) {
51352
- return resolve2(logConfig.baseDir);
51562
+ return resolve3(logConfig.baseDir);
51353
51563
  }
51354
- return join2(homedir(), ".local-router", "logs");
51564
+ return join4(homedir3(), ".local-router", "logs");
51355
51565
  }
51356
51566
 
51357
51567
  // src/config-store.ts
51358
- import { writeFileSync as writeFileSync2 } from "fs";
51359
- import { resolve as resolve3 } from "path";
51568
+ import { writeFileSync as writeFileSync3 } from "fs";
51569
+ import { resolve as resolve4 } from "path";
51360
51570
  class ConfigStore {
51361
51571
  config;
51362
51572
  absolutePath;
51363
51573
  constructor(configPath) {
51364
- this.absolutePath = resolve3(configPath);
51574
+ this.absolutePath = resolve4(configPath);
51365
51575
  this.config = loadConfig(this.absolutePath);
51366
51576
  }
51367
51577
  get() {
@@ -51376,7 +51586,7 @@ class ConfigStore {
51376
51586
  }
51377
51587
  save(newConfig) {
51378
51588
  const content = dist_default.stringify(newConfig, { space: 2, quote: '"' });
51379
- writeFileSync2(this.absolutePath, content, "utf-8");
51589
+ writeFileSync3(this.absolutePath, content, "utf-8");
51380
51590
  }
51381
51591
  validate(config2) {
51382
51592
  for (const [routeType, modelMap] of Object.entries(config2.routes)) {
@@ -51395,7 +51605,7 @@ class ConfigStore {
51395
51605
  // src/config-validate.ts
51396
51606
  var import__2020 = __toESM(require_2020(), 1);
51397
51607
  var import_ajv_formats = __toESM(require_dist2(), 1);
51398
- import { readFileSync as readFileSync2 } from "fs";
51608
+ import { readFileSync as readFileSync3 } from "fs";
51399
51609
 
51400
51610
  // src/runtime-assets.ts
51401
51611
  import { fileURLToPath } from "url";
@@ -51426,7 +51636,7 @@ function validateConfigOrThrow(config2) {
51426
51636
  validateBusinessRules(config2);
51427
51637
  const ajv = new import__2020.default({ allErrors: true, strict: false });
51428
51638
  import_ajv_formats.default(ajv);
51429
- const schemaJson = JSON.parse(readFileSync2(getBundledSchemaPath(), "utf-8"));
51639
+ const schemaJson = JSON.parse(readFileSync3(getBundledSchemaPath(), "utf-8"));
51430
51640
  const validateBySchema = ajv.compile(schemaJson);
51431
51641
  const valid = validateBySchema(config2);
51432
51642
  if (!valid) {
@@ -51487,8 +51697,8 @@ class CryptoSession {
51487
51697
  }
51488
51698
 
51489
51699
  // src/log-metrics.ts
51490
- import { createReadStream, existsSync as existsSync2 } from "fs";
51491
- import { join as join3 } from "path";
51700
+ import { createReadStream, existsSync as existsSync3 } from "fs";
51701
+ import { join as join5 } from "path";
51492
51702
  import { createInterface } from "readline";
51493
51703
  var WINDOW_MS = {
51494
51704
  "1h": 60 * 60 * 1000,
@@ -51605,8 +51815,8 @@ async function getLogMetrics(options) {
51605
51815
  if (!refresh && cached2 && cached2.expiresAt > nowMs) {
51606
51816
  return cached2.value;
51607
51817
  }
51608
- const eventsDir = join3(baseDir, "events");
51609
- if (!existsSync2(eventsDir)) {
51818
+ const eventsDir = join5(baseDir, "events");
51819
+ if (!existsSync3(eventsDir)) {
51610
51820
  const empty = createEmptyMetrics(window2, nowMs, {
51611
51821
  logEnabled: true,
51612
51822
  baseDir,
@@ -51652,8 +51862,8 @@ async function getLogMetrics(options) {
51652
51862
  partial2 = true;
51653
51863
  break;
51654
51864
  }
51655
- const filePath = join3(eventsDir, `${dateStr}.jsonl`);
51656
- if (!existsSync2(filePath))
51865
+ const filePath = join5(eventsDir, `${dateStr}.jsonl`);
51866
+ if (!existsSync3(filePath))
51657
51867
  continue;
51658
51868
  filesScanned += 1;
51659
51869
  try {
@@ -51789,8 +51999,8 @@ async function getLogMetrics(options) {
51789
51999
  }
51790
52000
 
51791
52001
  // src/log-query.ts
51792
- import { createReadStream as createReadStream3, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
51793
- import { join as join5, resolve as resolve4 } from "path";
52002
+ import { createReadStream as createReadStream3, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
52003
+ import { join as join7, resolve as resolve6 } from "path";
51794
52004
  import { createInterface as createInterface2 } from "readline";
51795
52005
 
51796
52006
  // src/log-index.ts
@@ -51798,13 +52008,13 @@ import { Database } from "bun:sqlite";
51798
52008
  import {
51799
52009
  closeSync,
51800
52010
  createReadStream as createReadStream2,
51801
- existsSync as existsSync3,
51802
- mkdirSync as mkdirSync2,
52011
+ existsSync as existsSync5,
52012
+ mkdirSync as mkdirSync3,
51803
52013
  openSync,
51804
52014
  readSync,
51805
- statSync
52015
+ statSync as statSync2
51806
52016
  } from "fs";
51807
- import { join as join4 } from "path";
52017
+ import { join as join6 } from "path";
51808
52018
 
51809
52019
  // src/log-session-identity.ts
51810
52020
  var USER_SESSION_DELIMITER = "_account__session_";
@@ -51882,8 +52092,588 @@ function resolveLogSessionIdentity(requestBody) {
51882
52092
  };
51883
52093
  }
51884
52094
 
52095
+ // src/token-usage.ts
52096
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
52097
+ import { resolve as resolve5 } from "path";
52098
+ var MAX_STREAM_USAGE_BYTES = 25 * 1024 * 1024;
52099
+ function asRecord(value) {
52100
+ if (!value || typeof value !== "object" || Array.isArray(value))
52101
+ return null;
52102
+ return value;
52103
+ }
52104
+ function numeric(value) {
52105
+ if (typeof value === "number" && Number.isFinite(value))
52106
+ return value;
52107
+ if (typeof value === "string" && value.trim()) {
52108
+ const parsed = Number(value);
52109
+ if (Number.isFinite(parsed))
52110
+ return parsed;
52111
+ }
52112
+ return null;
52113
+ }
52114
+ function numberAt(value, path) {
52115
+ let current = value;
52116
+ for (const key2 of path) {
52117
+ const record2 = asRecord(current);
52118
+ if (!record2 || !(key2 in record2))
52119
+ return null;
52120
+ current = record2[key2];
52121
+ }
52122
+ return numeric(current);
52123
+ }
52124
+ function firstNumber(value, paths) {
52125
+ for (const path of paths) {
52126
+ const found = numberAt(value, path);
52127
+ if (found !== null)
52128
+ return found;
52129
+ }
52130
+ return null;
52131
+ }
52132
+ function maxNullable(...values) {
52133
+ const numbers = values.filter((value) => value !== null && value !== undefined);
52134
+ if (numbers.length === 0)
52135
+ return null;
52136
+ return Math.max(...numbers);
52137
+ }
52138
+ function sumNullable(...values) {
52139
+ let total = 0;
52140
+ let hasValue = false;
52141
+ for (const value of values) {
52142
+ if (value === null || value === undefined)
52143
+ continue;
52144
+ total += value;
52145
+ hasValue = true;
52146
+ }
52147
+ return hasValue ? total : null;
52148
+ }
52149
+ function roundPercent(numerator, denominator) {
52150
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0)
52151
+ return null;
52152
+ return Number((numerator / denominator * 100).toFixed(2));
52153
+ }
52154
+ function inferProviderStyle(usage, providerHint) {
52155
+ const hint = providerHint?.toLowerCase() ?? "";
52156
+ if (hint.includes("anthropic") || hint.includes("claude"))
52157
+ return "anthropic";
52158
+ if (hint.includes("gemini") || hint.includes("google"))
52159
+ return "gemini";
52160
+ if (hint.includes("deepseek"))
52161
+ return "deepseek";
52162
+ if (hint.includes("cohere"))
52163
+ return "cohere";
52164
+ if (hint.includes("mistral"))
52165
+ return "mistral";
52166
+ if (hint.includes("openrouter"))
52167
+ return "openrouter";
52168
+ if (hint.includes("openai") || hint.includes("gpt-"))
52169
+ return "openai";
52170
+ if ("cache_read_input_tokens" in usage || "cache_creation_input_tokens" in usage) {
52171
+ return "anthropic";
52172
+ }
52173
+ if ("prompt_cache_hit_tokens" in usage || "prompt_cache_miss_tokens" in usage) {
52174
+ return "deepseek";
52175
+ }
52176
+ if ("promptTokenCount" in usage || "usageMetadata" in usage || "cachedContentTokenCount" in usage) {
52177
+ return "gemini";
52178
+ }
52179
+ if ("billed_units" in usage || "tokens" in usage) {
52180
+ return "cohere";
52181
+ }
52182
+ if ("prompt_tokens" in usage || "completion_tokens" in usage) {
52183
+ return "openai";
52184
+ }
52185
+ if ("input_tokens" in usage || "output_tokens" in usage) {
52186
+ return "openai";
52187
+ }
52188
+ return "unknown";
52189
+ }
52190
+ function createEmptyMetrics2(input) {
52191
+ return {
52192
+ schemaVersion: 1,
52193
+ source: input.source,
52194
+ providerStyle: input.providerStyle,
52195
+ inputTokens: null,
52196
+ outputTokens: null,
52197
+ totalTokens: null,
52198
+ cachedInputTokens: null,
52199
+ cacheHitInputTokens: null,
52200
+ cacheHitRate: null,
52201
+ cacheHitRateDenominatorTokens: null,
52202
+ cacheHitRateFormula: null,
52203
+ cacheReadInputTokens: null,
52204
+ cacheCreationInputTokens: null,
52205
+ cacheCreationInputTokens5m: null,
52206
+ cacheCreationInputTokens1h: null,
52207
+ cacheWriteInputTokens: null,
52208
+ cacheMissInputTokens: null,
52209
+ reasoningTokens: null,
52210
+ audioInputTokens: null,
52211
+ audioOutputTokens: null,
52212
+ textInputTokens: null,
52213
+ textOutputTokens: null,
52214
+ acceptedPredictionTokens: null,
52215
+ rejectedPredictionTokens: null,
52216
+ toolUsePromptTokens: null,
52217
+ billableInputTokens: null,
52218
+ billableOutputTokens: null,
52219
+ creditUsage: null,
52220
+ cost: null,
52221
+ rawUsage: input.rawUsage,
52222
+ rawUsagePath: input.rawUsagePath,
52223
+ warnings: []
52224
+ };
52225
+ }
52226
+ function hasAnyTokenSignal(metrics) {
52227
+ return [
52228
+ metrics.inputTokens,
52229
+ metrics.outputTokens,
52230
+ metrics.totalTokens,
52231
+ metrics.cachedInputTokens,
52232
+ metrics.cacheHitInputTokens,
52233
+ metrics.cacheReadInputTokens,
52234
+ metrics.cacheCreationInputTokens,
52235
+ metrics.cacheMissInputTokens,
52236
+ metrics.reasoningTokens,
52237
+ metrics.billableInputTokens,
52238
+ metrics.billableOutputTokens,
52239
+ metrics.creditUsage,
52240
+ metrics.cost
52241
+ ].some((value) => value !== null);
52242
+ }
52243
+ function normalizeUsageObject(input) {
52244
+ const { usage, source: source2, rawUsagePath, providerHint } = input;
52245
+ const providerStyle = inferProviderStyle(usage, providerHint);
52246
+ const metrics = createEmptyMetrics2({
52247
+ source: source2,
52248
+ providerStyle,
52249
+ rawUsage: usage,
52250
+ rawUsagePath
52251
+ });
52252
+ const usageBody = asRecord(usage.usageMetadata) ?? usage;
52253
+ metrics.inputTokens = firstNumber(usageBody, [
52254
+ ["input_tokens"],
52255
+ ["prompt_tokens"],
52256
+ ["promptTokenCount"],
52257
+ ["tokens", "input_tokens"],
52258
+ ["billed_units", "input_tokens"]
52259
+ ]);
52260
+ metrics.outputTokens = firstNumber(usageBody, [
52261
+ ["output_tokens"],
52262
+ ["completion_tokens"],
52263
+ ["candidatesTokenCount"],
52264
+ ["tokens", "output_tokens"],
52265
+ ["billed_units", "output_tokens"]
52266
+ ]);
52267
+ const explicitTotalTokens = firstNumber(usageBody, [
52268
+ ["total_tokens"],
52269
+ ["totalTokenCount"],
52270
+ ["tokens", "total_tokens"]
52271
+ ]);
52272
+ metrics.totalTokens = explicitTotalTokens;
52273
+ const cachedTokens = firstNumber(usageBody, [
52274
+ ["input_tokens_details", "cached_tokens"],
52275
+ ["prompt_tokens_details", "cached_tokens"],
52276
+ ["cached_tokens"],
52277
+ ["cachedContentTokenCount"]
52278
+ ]);
52279
+ const cacheReadTokens = firstNumber(usageBody, [
52280
+ ["cache_read_input_tokens"],
52281
+ ["cacheReadInputTokens"],
52282
+ ["claude_cache_read_input_tokens"]
52283
+ ]);
52284
+ const promptCacheHitTokens = firstNumber(usageBody, [["prompt_cache_hit_tokens"]]);
52285
+ const promptCacheMissTokens = firstNumber(usageBody, [["prompt_cache_miss_tokens"]]);
52286
+ const cacheCreationTokens = firstNumber(usageBody, [
52287
+ ["cache_creation_input_tokens"],
52288
+ ["cacheCreationInputTokens"],
52289
+ ["cache_creation", "input_tokens"],
52290
+ ["claude_cache_creation_input_tokens"]
52291
+ ]);
52292
+ metrics.cacheCreationInputTokens5m = firstNumber(usageBody, [
52293
+ ["cache_creation", "ephemeral_5m_input_tokens"],
52294
+ ["cache_creation", "ephemeral5mInputTokens"],
52295
+ ["cache_creation_ephemeral_5m_input_tokens"],
52296
+ ["claude_cache_creation_5_m_tokens"]
52297
+ ]);
52298
+ metrics.cacheCreationInputTokens1h = firstNumber(usageBody, [
52299
+ ["cache_creation", "ephemeral_1h_input_tokens"],
52300
+ ["cache_creation", "ephemeral1hInputTokens"],
52301
+ ["cache_creation_ephemeral_1h_input_tokens"],
52302
+ ["claude_cache_creation_1_h_tokens"]
52303
+ ]);
52304
+ metrics.cacheCreationInputTokens = maxNullable(cacheCreationTokens, sumNullable(metrics.cacheCreationInputTokens5m, metrics.cacheCreationInputTokens1h));
52305
+ metrics.cacheReadInputTokens = cacheReadTokens;
52306
+ metrics.cacheHitInputTokens = maxNullable(cachedTokens, cacheReadTokens, promptCacheHitTokens);
52307
+ metrics.cachedInputTokens = metrics.cacheHitInputTokens;
52308
+ metrics.cacheMissInputTokens = promptCacheMissTokens;
52309
+ metrics.cacheWriteInputTokens = firstNumber(usageBody, [
52310
+ ["cache_write_input_tokens"],
52311
+ ["cacheWriteInputTokens"]
52312
+ ]);
52313
+ if (metrics.cacheWriteInputTokens === null && providerStyle === "anthropic") {
52314
+ metrics.cacheWriteInputTokens = metrics.cacheCreationInputTokens;
52315
+ }
52316
+ metrics.reasoningTokens = firstNumber(usageBody, [
52317
+ ["output_tokens_details", "reasoning_tokens"],
52318
+ ["completion_tokens_details", "reasoning_tokens"],
52319
+ ["reasoning_tokens"],
52320
+ ["thoughtsTokenCount"]
52321
+ ]);
52322
+ metrics.audioInputTokens = firstNumber(usageBody, [
52323
+ ["input_tokens_details", "audio_tokens"],
52324
+ ["prompt_tokens_details", "audio_tokens"],
52325
+ ["audio_input_tokens"]
52326
+ ]);
52327
+ metrics.audioOutputTokens = firstNumber(usageBody, [
52328
+ ["output_tokens_details", "audio_tokens"],
52329
+ ["completion_tokens_details", "audio_tokens"],
52330
+ ["audio_output_tokens"]
52331
+ ]);
52332
+ metrics.textInputTokens = firstNumber(usageBody, [
52333
+ ["input_tokens_details", "text_tokens"],
52334
+ ["prompt_tokens_details", "text_tokens"],
52335
+ ["text_input_tokens"]
52336
+ ]);
52337
+ metrics.textOutputTokens = firstNumber(usageBody, [
52338
+ ["output_tokens_details", "text_tokens"],
52339
+ ["completion_tokens_details", "text_tokens"],
52340
+ ["text_output_tokens"]
52341
+ ]);
52342
+ metrics.acceptedPredictionTokens = firstNumber(usageBody, [
52343
+ ["output_tokens_details", "accepted_prediction_tokens"],
52344
+ ["completion_tokens_details", "accepted_prediction_tokens"],
52345
+ ["accepted_prediction_tokens"]
52346
+ ]);
52347
+ metrics.rejectedPredictionTokens = firstNumber(usageBody, [
52348
+ ["output_tokens_details", "rejected_prediction_tokens"],
52349
+ ["completion_tokens_details", "rejected_prediction_tokens"],
52350
+ ["rejected_prediction_tokens"]
52351
+ ]);
52352
+ metrics.toolUsePromptTokens = firstNumber(usageBody, [
52353
+ ["toolUsePromptTokenCount"],
52354
+ ["tool_use_prompt_tokens"]
52355
+ ]);
52356
+ metrics.billableInputTokens = firstNumber(usageBody, [
52357
+ ["billed_units", "input_tokens"],
52358
+ ["billable_input_tokens"]
52359
+ ]);
52360
+ metrics.billableOutputTokens = firstNumber(usageBody, [
52361
+ ["billed_units", "output_tokens"],
52362
+ ["billable_output_tokens"]
52363
+ ]);
52364
+ metrics.creditUsage = firstNumber(usageBody, [["credit_usage"], ["creditUsage"]]);
52365
+ metrics.cost = firstNumber(usageBody, [["cost"], ["total_cost"], ["totalCost"]]);
52366
+ let cacheDenominator = null;
52367
+ let cacheFormula = null;
52368
+ if (providerStyle === "anthropic") {
52369
+ cacheDenominator = sumNullable(metrics.inputTokens, metrics.cacheReadInputTokens, metrics.cacheCreationInputTokens);
52370
+ cacheFormula = "cache_read_input_tokens / (input_tokens + cache_read_input_tokens + cache_creation_input_tokens)";
52371
+ } else if (providerStyle === "deepseek") {
52372
+ cacheDenominator = metrics.inputTokens ?? sumNullable(metrics.cacheHitInputTokens, metrics.cacheMissInputTokens);
52373
+ cacheFormula = "prompt_cache_hit_tokens / prompt_tokens";
52374
+ } else if (providerStyle === "gemini") {
52375
+ cacheDenominator = metrics.inputTokens;
52376
+ cacheFormula = "cachedContentTokenCount / promptTokenCount";
52377
+ } else if (providerStyle === "openai" || providerStyle === "mistral" || providerStyle === "openrouter" || providerStyle === "unknown") {
52378
+ cacheDenominator = metrics.inputTokens;
52379
+ cacheFormula = "cached_tokens / input_tokens";
52380
+ }
52381
+ if (metrics.cacheHitInputTokens !== null && cacheDenominator !== null && cacheDenominator > 0) {
52382
+ metrics.cacheHitRateDenominatorTokens = cacheDenominator;
52383
+ metrics.cacheHitRateFormula = cacheFormula;
52384
+ metrics.cacheHitRate = roundPercent(metrics.cacheHitInputTokens, cacheDenominator);
52385
+ if (metrics.cacheMissInputTokens === null) {
52386
+ metrics.cacheMissInputTokens = Math.max(0, cacheDenominator - metrics.cacheHitInputTokens);
52387
+ }
52388
+ }
52389
+ if (metrics.totalTokens === null && metrics.outputTokens !== null) {
52390
+ const effectiveInputTokens = metrics.cacheHitRateDenominatorTokens ?? metrics.inputTokens;
52391
+ if (effectiveInputTokens !== null) {
52392
+ metrics.totalTokens = effectiveInputTokens + metrics.outputTokens;
52393
+ metrics.warnings.push(metrics.cacheHitRateDenominatorTokens !== null ? "totalTokens \u7531 cacheHitRateDenominatorTokens + outputTokens \u63A8\u5BFC" : "totalTokens \u7531 inputTokens + outputTokens \u63A8\u5BFC");
52394
+ }
52395
+ }
52396
+ return hasAnyTokenSignal(metrics) ? metrics : null;
52397
+ }
52398
+ function collectUsageCandidates(value, prefix = "") {
52399
+ const record2 = asRecord(value);
52400
+ if (!record2)
52401
+ return [];
52402
+ const candidates = [];
52403
+ const candidateKeys = [
52404
+ "usage",
52405
+ "usageMetadata",
52406
+ "message.usage",
52407
+ "response.usage",
52408
+ "body.usage",
52409
+ "data.usage",
52410
+ "event.usage"
52411
+ ];
52412
+ for (const key2 of candidateKeys) {
52413
+ const path = key2.split(".");
52414
+ let current = record2;
52415
+ for (const part of path) {
52416
+ const currentRecord = asRecord(current);
52417
+ current = currentRecord?.[part];
52418
+ }
52419
+ const usage = asRecord(current);
52420
+ if (usage) {
52421
+ candidates.push({ usage, path: prefix ? `${prefix}.${key2}` : key2 });
52422
+ }
52423
+ }
52424
+ const direct = normalizeUsageObject({
52425
+ usage: record2,
52426
+ source: "response_body",
52427
+ rawUsagePath: prefix || null
52428
+ });
52429
+ if (direct) {
52430
+ candidates.push({ usage: record2, path: prefix || "$" });
52431
+ }
52432
+ return candidates;
52433
+ }
52434
+ function mergeNumber(current, incoming, strategy = "max") {
52435
+ if (incoming === null)
52436
+ return current;
52437
+ if (current === null)
52438
+ return incoming;
52439
+ return strategy === "latest" ? incoming : Math.max(current, incoming);
52440
+ }
52441
+ function mergeTokenUsageMetrics(current, incoming) {
52442
+ if (!current)
52443
+ return incoming;
52444
+ if (!incoming)
52445
+ return current;
52446
+ const merged = {
52447
+ ...current,
52448
+ source: incoming.source,
52449
+ providerStyle: current.providerStyle === "unknown" ? incoming.providerStyle : current.providerStyle,
52450
+ rawUsage: incoming.rawUsage ?? current.rawUsage,
52451
+ rawUsagePath: incoming.rawUsagePath ?? current.rawUsagePath,
52452
+ warnings: Array.from(new Set([...current.warnings, ...incoming.warnings]))
52453
+ };
52454
+ const numericKeys = [
52455
+ "inputTokens",
52456
+ "outputTokens",
52457
+ "totalTokens",
52458
+ "cachedInputTokens",
52459
+ "cacheHitInputTokens",
52460
+ "cacheHitRateDenominatorTokens",
52461
+ "cacheReadInputTokens",
52462
+ "cacheCreationInputTokens",
52463
+ "cacheCreationInputTokens5m",
52464
+ "cacheCreationInputTokens1h",
52465
+ "cacheWriteInputTokens",
52466
+ "cacheMissInputTokens",
52467
+ "reasoningTokens",
52468
+ "audioInputTokens",
52469
+ "audioOutputTokens",
52470
+ "textInputTokens",
52471
+ "textOutputTokens",
52472
+ "acceptedPredictionTokens",
52473
+ "rejectedPredictionTokens",
52474
+ "toolUsePromptTokens",
52475
+ "billableInputTokens",
52476
+ "billableOutputTokens",
52477
+ "creditUsage",
52478
+ "cost"
52479
+ ];
52480
+ for (const key2 of numericKeys) {
52481
+ const value = mergeNumber(current[key2], incoming[key2], key2 === "cost" || key2 === "creditUsage" ? "latest" : "max");
52482
+ merged[key2] = value;
52483
+ }
52484
+ if (incoming.cacheHitRate !== null && (current.cacheHitRate === null || (incoming.cacheHitRateDenominatorTokens ?? 0) >= (current.cacheHitRateDenominatorTokens ?? 0))) {
52485
+ merged.cacheHitRate = incoming.cacheHitRate;
52486
+ merged.cacheHitRateFormula = incoming.cacheHitRateFormula;
52487
+ }
52488
+ if (merged.cacheHitInputTokens !== null && merged.cacheHitRateDenominatorTokens !== null) {
52489
+ merged.cacheHitRate = roundPercent(merged.cacheHitInputTokens, merged.cacheHitRateDenominatorTokens);
52490
+ }
52491
+ if (merged.totalTokens === null && merged.outputTokens !== null) {
52492
+ const effectiveInputTokens = merged.cacheHitRateDenominatorTokens ?? merged.inputTokens;
52493
+ if (effectiveInputTokens !== null) {
52494
+ merged.totalTokens = effectiveInputTokens + merged.outputTokens;
52495
+ merged.warnings.push(merged.cacheHitRateDenominatorTokens !== null ? "totalTokens \u7531 cacheHitRateDenominatorTokens + outputTokens \u63A8\u5BFC" : "totalTokens \u7531 inputTokens + outputTokens \u63A8\u5BFC");
52496
+ merged.warnings = Array.from(new Set(merged.warnings));
52497
+ }
52498
+ }
52499
+ return merged;
52500
+ }
52501
+ function toTokenUsageSummary(metrics) {
52502
+ const { rawUsage: _rawUsage, ...summary } = metrics;
52503
+ return summary;
52504
+ }
52505
+ function extractTokenUsageFromJson(value, options) {
52506
+ let merged = null;
52507
+ const candidates = collectUsageCandidates(value, options.rawUsagePathPrefix ?? "");
52508
+ for (const candidate of candidates) {
52509
+ const metrics = normalizeUsageObject({
52510
+ usage: candidate.usage,
52511
+ source: options.source,
52512
+ rawUsagePath: candidate.path,
52513
+ providerHint: options.providerHint
52514
+ });
52515
+ merged = mergeTokenUsageMetrics(merged, metrics);
52516
+ }
52517
+ return merged;
52518
+ }
52519
+ function extractTokenUsageFromResponseText(text2, source2 = "response_body", providerHint) {
52520
+ if (!text2?.trim())
52521
+ return null;
52522
+ try {
52523
+ return extractTokenUsageFromJson(JSON.parse(text2), { source: source2, providerHint });
52524
+ } catch {
52525
+ return null;
52526
+ }
52527
+ }
52528
+ function processSseMessage(dataLines, source2, providerHint) {
52529
+ if (dataLines.length === 0)
52530
+ return null;
52531
+ const data = dataLines.join(`
52532
+ `).trim();
52533
+ if (!data || data === "[DONE]")
52534
+ return null;
52535
+ try {
52536
+ return extractTokenUsageFromJson(JSON.parse(data), {
52537
+ source: source2,
52538
+ providerHint,
52539
+ rawUsagePathPrefix: source2 === "stream_file" ? "stream" : "stream"
52540
+ });
52541
+ } catch {
52542
+ return null;
52543
+ }
52544
+ }
52545
+ function extractTokenUsageFromSseText(text2, source2 = "stream_file", providerHint) {
52546
+ if (!text2?.trim())
52547
+ return null;
52548
+ let merged = null;
52549
+ let dataLines = [];
52550
+ const flush = () => {
52551
+ merged = mergeTokenUsageMetrics(merged, processSseMessage(dataLines, source2, providerHint));
52552
+ dataLines = [];
52553
+ };
52554
+ for (const rawLine of text2.split(/\r?\n/)) {
52555
+ if (rawLine === "") {
52556
+ flush();
52557
+ continue;
52558
+ }
52559
+ if (rawLine.startsWith("data:")) {
52560
+ dataLines.push(rawLine.slice(5).trimStart());
52561
+ }
52562
+ }
52563
+ flush();
52564
+ return merged;
52565
+ }
52566
+ function createTokenUsageStreamCollector(providerHint) {
52567
+ const decoder = new TextDecoder;
52568
+ let buffer2 = "";
52569
+ let dataLines = [];
52570
+ let latest = null;
52571
+ const flushMessage = () => {
52572
+ latest = mergeTokenUsageMetrics(latest, processSseMessage(dataLines, "stream_chunk", providerHint));
52573
+ dataLines = [];
52574
+ };
52575
+ const processLine = (rawLine) => {
52576
+ const line2 = rawLine.replace(/\r$/, "");
52577
+ if (line2 === "") {
52578
+ flushMessage();
52579
+ return;
52580
+ }
52581
+ if (line2.startsWith("data:")) {
52582
+ dataLines.push(line2.slice(5).trimStart());
52583
+ }
52584
+ };
52585
+ return {
52586
+ addChunk(chunk) {
52587
+ buffer2 += decoder.decode(chunk, { stream: true });
52588
+ let newlineIndex = buffer2.indexOf(`
52589
+ `);
52590
+ while (newlineIndex >= 0) {
52591
+ processLine(buffer2.slice(0, newlineIndex));
52592
+ buffer2 = buffer2.slice(newlineIndex + 1);
52593
+ newlineIndex = buffer2.indexOf(`
52594
+ `);
52595
+ }
52596
+ },
52597
+ getUsage() {
52598
+ buffer2 += decoder.decode();
52599
+ if (buffer2) {
52600
+ processLine(buffer2);
52601
+ buffer2 = "";
52602
+ }
52603
+ flushMessage();
52604
+ return latest;
52605
+ }
52606
+ };
52607
+ }
52608
+ function safeReadStreamFile(streamFile, baseDir) {
52609
+ if (!streamFile)
52610
+ return { content: null, warning: null };
52611
+ try {
52612
+ const candidates = [streamFile];
52613
+ if (baseDir)
52614
+ candidates.push(resolve5(baseDir, streamFile));
52615
+ for (const candidate of candidates) {
52616
+ const resolved = resolve5(candidate);
52617
+ if (!resolved.endsWith(".sse.raw"))
52618
+ continue;
52619
+ if (!existsSync4(resolved))
52620
+ continue;
52621
+ const stats = statSync(resolved);
52622
+ if (stats.size > MAX_STREAM_USAGE_BYTES) {
52623
+ return {
52624
+ content: null,
52625
+ warning: `stream_file \u8D85\u8FC7 ${MAX_STREAM_USAGE_BYTES} \u5B57\u8282\uFF0C\u5DF2\u8DF3\u8FC7 token usage \u56DE\u586B`
52626
+ };
52627
+ }
52628
+ return { content: readFileSync4(resolved, "utf-8"), warning: null };
52629
+ }
52630
+ } catch (err) {
52631
+ return {
52632
+ content: null,
52633
+ warning: `stream_file token usage \u8BFB\u53D6\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
52634
+ };
52635
+ }
52636
+ return { content: null, warning: null };
52637
+ }
52638
+ function extractTokenUsageFromLogEvent(event, options = {}) {
52639
+ if (event.token_usage) {
52640
+ return {
52641
+ rawUsage: null,
52642
+ ...event.token_usage,
52643
+ source: event.token_usage.source ?? "explicit"
52644
+ };
52645
+ }
52646
+ const providerHint = [event.provider, event.route_type, event.model_in, event.model_out].filter(Boolean).join(" ");
52647
+ const responseBodyUsage = extractTokenUsageFromResponseText(event.response_body, "response_body", providerHint);
52648
+ if (responseBodyUsage)
52649
+ return responseBodyUsage;
52650
+ const responseAfterPluginsUsage = extractTokenUsageFromResponseText(event.response_body_after_plugins, "response_body_after_plugins", providerHint);
52651
+ if (responseAfterPluginsUsage)
52652
+ return responseAfterPluginsUsage;
52653
+ const responseBeforePluginsUsage = extractTokenUsageFromResponseText(event.response_body_before_plugins, "response_body_before_plugins", providerHint);
52654
+ if (responseBeforePluginsUsage)
52655
+ return responseBeforePluginsUsage;
52656
+ const streamContent = options.streamContent ?? safeReadStreamFile(event.stream_file, options.baseDir).content;
52657
+ return extractTokenUsageFromSseText(streamContent ?? undefined, "stream_file", providerHint);
52658
+ }
52659
+ function extractTokenUsageSummaryFromLogEvent(event, options = {}) {
52660
+ const metrics = extractTokenUsageFromLogEvent(event, options);
52661
+ return metrics ? toTokenUsageSummary(metrics) : null;
52662
+ }
52663
+ function enrichLogEventTokenUsage(event, options = {}) {
52664
+ if (event.token_usage)
52665
+ return event;
52666
+ const tokenUsage = extractTokenUsageFromLogEvent(event, options);
52667
+ if (!tokenUsage)
52668
+ return event;
52669
+ return {
52670
+ ...event,
52671
+ token_usage: tokenUsage
52672
+ };
52673
+ }
52674
+
51885
52675
  // src/log-index.ts
51886
- var SCHEMA_VERSION = 1;
52676
+ var SCHEMA_VERSION = 3;
51887
52677
  var MAX_INDEX_QUEUE = 20000;
51888
52678
  var INDEX_BATCH_SIZE = 250;
51889
52679
  var INDEX_FLUSH_DELAY_MS = 50;
@@ -52027,6 +52817,7 @@ function eventToRow(input) {
52027
52817
  const statusClass = getStatusClass2(event);
52028
52818
  const latencyMs = Math.max(0, event.latency_ms ?? 0);
52029
52819
  const model = event.model_out || event.model_in;
52820
+ const tokenUsage = extractTokenUsageSummaryFromLogEvent(event, { baseDir: input.baseDir });
52030
52821
  return {
52031
52822
  id: input.id,
52032
52823
  ts_ms: tsMs,
@@ -52054,8 +52845,46 @@ function eventToRow(input) {
52054
52845
  line_number: input.lineNumber,
52055
52846
  byte_offset: input.offset,
52056
52847
  byte_length: input.byteLength,
52057
- search_text: buildSearchText(event)
52058
- };
52848
+ search_text: buildSearchText(event),
52849
+ token_input: tokenUsage?.inputTokens ?? null,
52850
+ token_output: tokenUsage?.outputTokens ?? null,
52851
+ token_total: tokenUsage?.totalTokens ?? null,
52852
+ token_cached_input: tokenUsage?.cachedInputTokens ?? null,
52853
+ token_cache_hit_input: tokenUsage?.cacheHitInputTokens ?? null,
52854
+ token_cache_hit_rate: tokenUsage?.cacheHitRate ?? null,
52855
+ token_cache_hit_rate_denominator: tokenUsage?.cacheHitRateDenominatorTokens ?? null,
52856
+ token_cache_read_input: tokenUsage?.cacheReadInputTokens ?? null,
52857
+ token_cache_creation_input: tokenUsage?.cacheCreationInputTokens ?? null,
52858
+ token_cache_creation_input_5m: tokenUsage?.cacheCreationInputTokens5m ?? null,
52859
+ token_cache_creation_input_1h: tokenUsage?.cacheCreationInputTokens1h ?? null,
52860
+ token_cache_write_input: tokenUsage?.cacheWriteInputTokens ?? null,
52861
+ token_cache_miss_input: tokenUsage?.cacheMissInputTokens ?? null,
52862
+ token_reasoning: tokenUsage?.reasoningTokens ?? null,
52863
+ token_audio_input: tokenUsage?.audioInputTokens ?? null,
52864
+ token_audio_output: tokenUsage?.audioOutputTokens ?? null,
52865
+ token_text_input: tokenUsage?.textInputTokens ?? null,
52866
+ token_text_output: tokenUsage?.textOutputTokens ?? null,
52867
+ token_accepted_prediction: tokenUsage?.acceptedPredictionTokens ?? null,
52868
+ token_rejected_prediction: tokenUsage?.rejectedPredictionTokens ?? null,
52869
+ token_tool_use_prompt: tokenUsage?.toolUsePromptTokens ?? null,
52870
+ token_billable_input: tokenUsage?.billableInputTokens ?? null,
52871
+ token_billable_output: tokenUsage?.billableOutputTokens ?? null,
52872
+ token_credit_usage: tokenUsage?.creditUsage ?? null,
52873
+ token_cost: tokenUsage?.cost ?? null,
52874
+ token_source: tokenUsage?.source ?? null,
52875
+ token_provider_style: tokenUsage?.providerStyle ?? null,
52876
+ token_raw_usage_path: tokenUsage?.rawUsagePath ?? null,
52877
+ token_usage_json: tokenUsage ? JSON.stringify(tokenUsage) : null
52878
+ };
52879
+ }
52880
+ function parseTokenUsageSummary(value) {
52881
+ if (!value)
52882
+ return null;
52883
+ try {
52884
+ return JSON.parse(value);
52885
+ } catch {
52886
+ return null;
52887
+ }
52059
52888
  }
52060
52889
  function rowToSummary(row) {
52061
52890
  return {
@@ -52078,7 +52907,8 @@ function rowToSummary(row) {
52078
52907
  hasMetadata: row.has_metadata === 1,
52079
52908
  userIdRaw: row.user_id_raw,
52080
52909
  userKey: row.user_key,
52081
- sessionId: row.session_id
52910
+ sessionId: row.session_id,
52911
+ tokenUsage: parseTokenUsageSummary(row.token_usage_json)
52082
52912
  };
52083
52913
  }
52084
52914
  async function* readJsonlLinesWithOffsets(filePath) {
@@ -52148,7 +52978,22 @@ function createEmptyStats() {
52148
52978
  errorCount: 0,
52149
52979
  errorRate: 0,
52150
52980
  avgLatencyMs: 0,
52151
- p95LatencyMs: 0
52981
+ p95LatencyMs: 0,
52982
+ tokenUsageCount: 0,
52983
+ inputTokens: 0,
52984
+ outputTokens: 0,
52985
+ totalTokens: 0,
52986
+ cachedInputTokens: 0,
52987
+ cacheHitInputTokens: 0,
52988
+ cacheHitRate: 0,
52989
+ cacheHitRateDenominatorTokens: 0,
52990
+ cacheReadInputTokens: 0,
52991
+ cacheCreationInputTokens: 0,
52992
+ cacheWriteInputTokens: 0,
52993
+ cacheMissInputTokens: 0,
52994
+ reasoningTokens: 0,
52995
+ billableInputTokens: 0,
52996
+ billableOutputTokens: 0
52152
52997
  };
52153
52998
  }
52154
52999
  function createEmptyQueryResult(query, meta3 = {}) {
@@ -52231,8 +53076,8 @@ class LogIndex {
52231
53076
  constructor(baseDir, config2) {
52232
53077
  this.baseDir = baseDir;
52233
53078
  this.config = config2;
52234
- mkdirSync2(baseDir, { recursive: true });
52235
- const dbPath = join4(baseDir, "logs-index.sqlite");
53079
+ mkdirSync3(baseDir, { recursive: true });
53080
+ const dbPath = join6(baseDir, "logs-index.sqlite");
52236
53081
  this.db = new Database(dbPath, { create: true, strict: true });
52237
53082
  this.configure();
52238
53083
  this.migrate();
@@ -52241,12 +53086,28 @@ class LogIndex {
52241
53086
  id, ts_ms, ts_start, level, provider, route_type, model, model_in, model_out,
52242
53087
  path, request_id, latency_ms, upstream_status, status_class, has_error,
52243
53088
  message, error_type, has_metadata, user_id_raw, user_key, session_id,
52244
- source_date, source_file, line_number, byte_offset, byte_length, search_text
53089
+ source_date, source_file, line_number, byte_offset, byte_length, search_text,
53090
+ token_input, token_output, token_total, token_cached_input, token_cache_hit_input,
53091
+ token_cache_hit_rate, token_cache_hit_rate_denominator, token_cache_read_input,
53092
+ token_cache_creation_input, token_cache_creation_input_5m, token_cache_creation_input_1h,
53093
+ token_cache_write_input, token_cache_miss_input, token_reasoning, token_audio_input,
53094
+ token_audio_output, token_text_input, token_text_output, token_accepted_prediction,
53095
+ token_rejected_prediction, token_tool_use_prompt, token_billable_input,
53096
+ token_billable_output, token_credit_usage, token_cost, token_source,
53097
+ token_provider_style, token_raw_usage_path, token_usage_json
52245
53098
  ) VALUES (
52246
53099
  $id, $ts_ms, $ts_start, $level, $provider, $route_type, $model, $model_in, $model_out,
52247
53100
  $path, $request_id, $latency_ms, $upstream_status, $status_class, $has_error,
52248
53101
  $message, $error_type, $has_metadata, $user_id_raw, $user_key, $session_id,
52249
- $source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text
53102
+ $source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text,
53103
+ $token_input, $token_output, $token_total, $token_cached_input, $token_cache_hit_input,
53104
+ $token_cache_hit_rate, $token_cache_hit_rate_denominator, $token_cache_read_input,
53105
+ $token_cache_creation_input, $token_cache_creation_input_5m, $token_cache_creation_input_1h,
53106
+ $token_cache_write_input, $token_cache_miss_input, $token_reasoning, $token_audio_input,
53107
+ $token_audio_output, $token_text_input, $token_text_output, $token_accepted_prediction,
53108
+ $token_rejected_prediction, $token_tool_use_prompt, $token_billable_input,
53109
+ $token_billable_output, $token_credit_usage, $token_cost, $token_source,
53110
+ $token_provider_style, $token_raw_usage_path, $token_usage_json
52250
53111
  )
52251
53112
  `);
52252
53113
  this.deleteFtsStmt = this.db.prepare("DELETE FROM log_events_fts WHERE event_id = ?");
@@ -52292,13 +53153,13 @@ class LogIndex {
52292
53153
  let scannedFiles = 0;
52293
53154
  let scannedLines = 0;
52294
53155
  let parseErrors = 0;
52295
- const eventsDir = join4(this.baseDir, "events");
53156
+ const eventsDir = join6(this.baseDir, "events");
52296
53157
  const dates = listDateStrings2(fromMs, toMs);
52297
53158
  for (const date5 of dates) {
52298
- const filePath = join4(eventsDir, `${date5}.jsonl`);
52299
- if (!existsSync3(filePath))
53159
+ const filePath = join6(eventsDir, `${date5}.jsonl`);
53160
+ if (!existsSync5(filePath))
52300
53161
  continue;
52301
- const stats = statSync(filePath);
53162
+ const stats = statSync2(filePath);
52302
53163
  const fileRow = this.db.query("SELECT size_bytes, mtime_ms FROM log_index_files WHERE file_path = ?").get(filePath);
52303
53164
  const sizeBytes = stats.size;
52304
53165
  const mtimeMs = Math.trunc(stats.mtimeMs);
@@ -52333,7 +53194,7 @@ class LogIndex {
52333
53194
  e.id, e.ts_start, e.level, e.provider, e.route_type, e.model, e.model_in,
52334
53195
  e.model_out, e.path, e.request_id, e.latency_ms, e.upstream_status,
52335
53196
  e.status_class, e.has_error, e.message, e.error_type, e.has_metadata,
52336
- e.user_id_raw, e.user_key, e.session_id, e.ts_ms
53197
+ e.user_id_raw, e.user_key, e.session_id, e.ts_ms, e.token_usage_json
52337
53198
  FROM log_events e
52338
53199
  ${whereSql}
52339
53200
  ${cursorClause}
@@ -52387,8 +53248,8 @@ class LogIndex {
52387
53248
  if (!parsedId)
52388
53249
  return null;
52389
53250
  const row = this.db.query("SELECT source_date, source_file, line_number, byte_offset FROM log_events WHERE id = ?").get(id);
52390
- const filePath = row?.source_file ?? join4(this.baseDir, "events", `${parsedId.date}.jsonl`);
52391
- if (!existsSync3(filePath))
53251
+ const filePath = row?.source_file ?? join6(this.baseDir, "events", `${parsedId.date}.jsonl`);
53252
+ if (!existsSync5(filePath))
52392
53253
  return null;
52393
53254
  const line2 = readLineAtOffset(filePath, row?.byte_offset ?? parsedId.offset);
52394
53255
  if (!line2?.trim())
@@ -52456,7 +53317,36 @@ class LogIndex {
52456
53317
  line_number INTEGER,
52457
53318
  byte_offset INTEGER NOT NULL,
52458
53319
  byte_length INTEGER NOT NULL,
52459
- search_text TEXT NOT NULL
53320
+ search_text TEXT NOT NULL,
53321
+ token_input INTEGER,
53322
+ token_output INTEGER,
53323
+ token_total INTEGER,
53324
+ token_cached_input INTEGER,
53325
+ token_cache_hit_input INTEGER,
53326
+ token_cache_hit_rate REAL,
53327
+ token_cache_hit_rate_denominator INTEGER,
53328
+ token_cache_read_input INTEGER,
53329
+ token_cache_creation_input INTEGER,
53330
+ token_cache_creation_input_5m INTEGER,
53331
+ token_cache_creation_input_1h INTEGER,
53332
+ token_cache_write_input INTEGER,
53333
+ token_cache_miss_input INTEGER,
53334
+ token_reasoning INTEGER,
53335
+ token_audio_input INTEGER,
53336
+ token_audio_output INTEGER,
53337
+ token_text_input INTEGER,
53338
+ token_text_output INTEGER,
53339
+ token_accepted_prediction INTEGER,
53340
+ token_rejected_prediction INTEGER,
53341
+ token_tool_use_prompt INTEGER,
53342
+ token_billable_input INTEGER,
53343
+ token_billable_output INTEGER,
53344
+ token_credit_usage REAL,
53345
+ token_cost REAL,
53346
+ token_source TEXT,
53347
+ token_provider_style TEXT,
53348
+ token_raw_usage_path TEXT,
53349
+ token_usage_json TEXT
52460
53350
  );
52461
53351
 
52462
53352
  CREATE VIRTUAL TABLE IF NOT EXISTS log_events_fts
@@ -52474,12 +53364,67 @@ class LogIndex {
52474
53364
  CREATE INDEX IF NOT EXISTS idx_log_events_session_time ON log_events(session_id, ts_ms DESC);
52475
53365
  CREATE INDEX IF NOT EXISTS idx_log_events_file ON log_events(source_file);
52476
53366
  `);
53367
+ this.ensureTokenColumns();
53368
+ this.db.exec(`
53369
+ CREATE INDEX IF NOT EXISTS idx_log_events_token_total_time ON log_events(token_total, ts_ms DESC);
53370
+ CREATE INDEX IF NOT EXISTS idx_log_events_token_input_time ON log_events(token_input, ts_ms DESC);
53371
+ CREATE INDEX IF NOT EXISTS idx_log_events_token_cache_hit_rate_time ON log_events(token_cache_hit_rate, ts_ms DESC);
53372
+ `);
53373
+ const versionRow = this.db.query("SELECT value FROM log_index_meta WHERE key = 'schema_version'").get();
53374
+ const previousVersion = Number.parseInt(versionRow?.value ?? "0", 10) || 0;
53375
+ if (previousVersion > 0 && previousVersion < SCHEMA_VERSION) {
53376
+ this.db.exec(`
53377
+ DELETE FROM log_events_fts;
53378
+ DELETE FROM log_events;
53379
+ DELETE FROM log_index_files;
53380
+ `);
53381
+ }
52477
53382
  this.db.prepare(`
52478
53383
  INSERT INTO log_index_meta(key, value)
52479
53384
  VALUES ('schema_version', ?)
52480
53385
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
52481
53386
  `).run(String(SCHEMA_VERSION));
52482
53387
  }
53388
+ ensureTokenColumns() {
53389
+ const rows = this.db.query("PRAGMA table_info(log_events)").all();
53390
+ const existing = new Set(rows.map((row) => row.name));
53391
+ const columns = [
53392
+ { name: "token_input", type: "INTEGER" },
53393
+ { name: "token_output", type: "INTEGER" },
53394
+ { name: "token_total", type: "INTEGER" },
53395
+ { name: "token_cached_input", type: "INTEGER" },
53396
+ { name: "token_cache_hit_input", type: "INTEGER" },
53397
+ { name: "token_cache_hit_rate", type: "REAL" },
53398
+ { name: "token_cache_hit_rate_denominator", type: "INTEGER" },
53399
+ { name: "token_cache_read_input", type: "INTEGER" },
53400
+ { name: "token_cache_creation_input", type: "INTEGER" },
53401
+ { name: "token_cache_creation_input_5m", type: "INTEGER" },
53402
+ { name: "token_cache_creation_input_1h", type: "INTEGER" },
53403
+ { name: "token_cache_write_input", type: "INTEGER" },
53404
+ { name: "token_cache_miss_input", type: "INTEGER" },
53405
+ { name: "token_reasoning", type: "INTEGER" },
53406
+ { name: "token_audio_input", type: "INTEGER" },
53407
+ { name: "token_audio_output", type: "INTEGER" },
53408
+ { name: "token_text_input", type: "INTEGER" },
53409
+ { name: "token_text_output", type: "INTEGER" },
53410
+ { name: "token_accepted_prediction", type: "INTEGER" },
53411
+ { name: "token_rejected_prediction", type: "INTEGER" },
53412
+ { name: "token_tool_use_prompt", type: "INTEGER" },
53413
+ { name: "token_billable_input", type: "INTEGER" },
53414
+ { name: "token_billable_output", type: "INTEGER" },
53415
+ { name: "token_credit_usage", type: "REAL" },
53416
+ { name: "token_cost", type: "REAL" },
53417
+ { name: "token_source", type: "TEXT" },
53418
+ { name: "token_provider_style", type: "TEXT" },
53419
+ { name: "token_raw_usage_path", type: "TEXT" },
53420
+ { name: "token_usage_json", type: "TEXT" }
53421
+ ];
53422
+ for (const column2 of columns) {
53423
+ if (existing.has(column2.name))
53424
+ continue;
53425
+ this.db.exec(`ALTER TABLE log_events ADD COLUMN ${column2.name} ${column2.type}`);
53426
+ }
53427
+ }
52483
53428
  flushQueue() {
52484
53429
  if (this.queue.length === 0 || this.disposed)
52485
53430
  return;
@@ -52507,6 +53452,7 @@ class LogIndex {
52507
53452
  return;
52508
53453
  const id = encodeOffsetLogEventId(item.date, item.offset);
52509
53454
  const row = eventToRow({
53455
+ baseDir: this.baseDir,
52510
53456
  id,
52511
53457
  date: item.date,
52512
53458
  filePath: item.filePath,
@@ -52522,7 +53468,7 @@ class LogIndex {
52522
53468
  this.insertFtsStmt.run(id, row.search_text);
52523
53469
  if (!this.dirtyFiles.has(item.filePath)) {
52524
53470
  try {
52525
- const stats = statSync(item.filePath);
53471
+ const stats = statSync2(item.filePath);
52526
53472
  const indexedThrough = item.offset + item.byteLength;
52527
53473
  this.upsertFileStmt.run(item.filePath, item.date, Math.min(indexedThrough, stats.size), Math.trunc(stats.mtimeMs), Date.now());
52528
53474
  } catch {}
@@ -52549,6 +53495,7 @@ class LogIndex {
52549
53495
  continue;
52550
53496
  }
52551
53497
  const row = eventToRow({
53498
+ baseDir: this.baseDir,
52552
53499
  id: encodeOffsetLogEventId(date5, item.offset),
52553
53500
  date: date5,
52554
53501
  filePath,
@@ -52581,7 +53528,21 @@ class LogIndex {
52581
53528
  SELECT
52582
53529
  COUNT(*) AS total,
52583
53530
  COALESCE(SUM(has_error), 0) AS errorCount,
52584
- COALESCE(AVG(latency_ms), 0) AS avgLatencyMs
53531
+ COALESCE(AVG(latency_ms), 0) AS avgLatencyMs,
53532
+ COALESCE(SUM(CASE WHEN token_usage_json IS NOT NULL THEN 1 ELSE 0 END), 0) AS tokenUsageCount,
53533
+ COALESCE(SUM(token_input), 0) AS inputTokens,
53534
+ COALESCE(SUM(token_output), 0) AS outputTokens,
53535
+ COALESCE(SUM(token_total), 0) AS totalTokens,
53536
+ COALESCE(SUM(token_cached_input), 0) AS cachedInputTokens,
53537
+ COALESCE(SUM(token_cache_hit_input), 0) AS cacheHitInputTokens,
53538
+ COALESCE(SUM(token_cache_hit_rate_denominator), 0) AS cacheHitRateDenominatorTokens,
53539
+ COALESCE(SUM(token_cache_read_input), 0) AS cacheReadInputTokens,
53540
+ COALESCE(SUM(token_cache_creation_input), 0) AS cacheCreationInputTokens,
53541
+ COALESCE(SUM(token_cache_write_input), 0) AS cacheWriteInputTokens,
53542
+ COALESCE(SUM(token_cache_miss_input), 0) AS cacheMissInputTokens,
53543
+ COALESCE(SUM(token_reasoning), 0) AS reasoningTokens,
53544
+ COALESCE(SUM(token_billable_input), 0) AS billableInputTokens,
53545
+ COALESCE(SUM(token_billable_output), 0) AS billableOutputTokens
52585
53546
  FROM log_events e
52586
53547
  ${whereSql}
52587
53548
  `).get(...params);
@@ -52602,7 +53563,22 @@ class LogIndex {
52602
53563
  errorCount,
52603
53564
  errorRate: toPercent2(errorCount, total),
52604
53565
  avgLatencyMs: Math.round(Number(aggregate.avgLatencyMs) || 0),
52605
- p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0)
53566
+ p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0),
53567
+ tokenUsageCount: Number(aggregate.tokenUsageCount) || 0,
53568
+ inputTokens: Number(aggregate.inputTokens) || 0,
53569
+ outputTokens: Number(aggregate.outputTokens) || 0,
53570
+ totalTokens: Number(aggregate.totalTokens) || 0,
53571
+ cachedInputTokens: Number(aggregate.cachedInputTokens) || 0,
53572
+ cacheHitInputTokens: Number(aggregate.cacheHitInputTokens) || 0,
53573
+ cacheHitRate: toPercent2(Number(aggregate.cacheHitInputTokens) || 0, Number(aggregate.cacheHitRateDenominatorTokens) || 0),
53574
+ cacheHitRateDenominatorTokens: Number(aggregate.cacheHitRateDenominatorTokens) || 0,
53575
+ cacheReadInputTokens: Number(aggregate.cacheReadInputTokens) || 0,
53576
+ cacheCreationInputTokens: Number(aggregate.cacheCreationInputTokens) || 0,
53577
+ cacheWriteInputTokens: Number(aggregate.cacheWriteInputTokens) || 0,
53578
+ cacheMissInputTokens: Number(aggregate.cacheMissInputTokens) || 0,
53579
+ reasoningTokens: Number(aggregate.reasoningTokens) || 0,
53580
+ billableInputTokens: Number(aggregate.billableInputTokens) || 0,
53581
+ billableOutputTokens: Number(aggregate.billableOutputTokens) || 0
52606
53582
  };
52607
53583
  }
52608
53584
  }
@@ -52682,8 +53658,8 @@ function getIndexedLogEventDetail(logConfig, id) {
52682
53658
  const parsedId = decodeOffsetLogEventId(id);
52683
53659
  if (!parsedId)
52684
53660
  return null;
52685
- const filePath = join4(baseDir, "events", `${parsedId.date}.jsonl`);
52686
- if (!existsSync3(filePath))
53661
+ const filePath = join6(baseDir, "events", `${parsedId.date}.jsonl`);
53662
+ if (!existsSync5(filePath))
52687
53663
  return null;
52688
53664
  const line2 = readLineAtOffset(filePath, parsedId.offset);
52689
53665
  if (!line2?.trim())
@@ -52790,12 +53766,8 @@ function buildMessage2(event) {
52790
53766
  const status = event.upstream_status ?? 0;
52791
53767
  return `${event.method} ${event.path} -> ${status}`;
52792
53768
  }
52793
- function containsKeyword(event, q) {
52794
- if (!q)
52795
- return true;
52796
- const identity = resolveLogSessionIdentity(event.request_body);
52797
- const keyword = q.toLowerCase();
52798
- const haystack = [
53769
+ function buildKeywordText(event, identity) {
53770
+ return [
52799
53771
  event.request_id,
52800
53772
  event.path,
52801
53773
  event.provider,
@@ -52809,15 +53781,38 @@ function containsKeyword(event, q) {
52809
53781
  event.error_message ?? "",
52810
53782
  buildMessage2(event)
52811
53783
  ].join(" ").toLowerCase();
52812
- return haystack.includes(keyword);
53784
+ }
53785
+ function containsKeyword(event, q) {
53786
+ if (!q)
53787
+ return true;
53788
+ const identity = resolveLogSessionIdentity(event.request_body);
53789
+ const keyword = q.toLowerCase();
53790
+ return buildKeywordText(event, identity).includes(keyword);
52813
53791
  }
52814
53792
  function createRunningStats() {
52815
53793
  return {
52816
53794
  total: 0,
52817
53795
  errorCount: 0,
52818
53796
  latencySum: 0,
52819
- latencyCounts: new Map
52820
- };
53797
+ latencyCounts: new Map,
53798
+ tokenUsageCount: 0,
53799
+ inputTokens: 0,
53800
+ outputTokens: 0,
53801
+ totalTokens: 0,
53802
+ cachedInputTokens: 0,
53803
+ cacheHitInputTokens: 0,
53804
+ cacheHitRateDenominatorTokens: 0,
53805
+ cacheReadInputTokens: 0,
53806
+ cacheCreationInputTokens: 0,
53807
+ cacheWriteInputTokens: 0,
53808
+ cacheMissInputTokens: 0,
53809
+ reasoningTokens: 0,
53810
+ billableInputTokens: 0,
53811
+ billableOutputTokens: 0
53812
+ };
53813
+ }
53814
+ function addNullable(total, value) {
53815
+ return total + (value ?? 0);
52821
53816
  }
52822
53817
  function updateRunningStats(stats, item) {
52823
53818
  stats.total += 1;
@@ -52828,6 +53823,23 @@ function updateRunningStats(stats, item) {
52828
53823
  stats.latencySum += latency;
52829
53824
  const roundedLatency = Math.round(latency);
52830
53825
  stats.latencyCounts.set(roundedLatency, (stats.latencyCounts.get(roundedLatency) ?? 0) + 1);
53826
+ const usage = item.tokenUsage;
53827
+ if (!usage)
53828
+ return;
53829
+ stats.tokenUsageCount += 1;
53830
+ stats.inputTokens = addNullable(stats.inputTokens, usage.inputTokens);
53831
+ stats.outputTokens = addNullable(stats.outputTokens, usage.outputTokens);
53832
+ stats.totalTokens = addNullable(stats.totalTokens, usage.totalTokens);
53833
+ stats.cachedInputTokens = addNullable(stats.cachedInputTokens, usage.cachedInputTokens);
53834
+ stats.cacheHitInputTokens = addNullable(stats.cacheHitInputTokens, usage.cacheHitInputTokens);
53835
+ stats.cacheHitRateDenominatorTokens = addNullable(stats.cacheHitRateDenominatorTokens, usage.cacheHitRateDenominatorTokens);
53836
+ stats.cacheReadInputTokens = addNullable(stats.cacheReadInputTokens, usage.cacheReadInputTokens);
53837
+ stats.cacheCreationInputTokens = addNullable(stats.cacheCreationInputTokens, usage.cacheCreationInputTokens);
53838
+ stats.cacheWriteInputTokens = addNullable(stats.cacheWriteInputTokens, usage.cacheWriteInputTokens);
53839
+ stats.cacheMissInputTokens = addNullable(stats.cacheMissInputTokens, usage.cacheMissInputTokens);
53840
+ stats.reasoningTokens = addNullable(stats.reasoningTokens, usage.reasoningTokens);
53841
+ stats.billableInputTokens = addNullable(stats.billableInputTokens, usage.billableInputTokens);
53842
+ stats.billableOutputTokens = addNullable(stats.billableOutputTokens, usage.billableOutputTokens);
52831
53843
  }
52832
53844
  function percentileFromCounts(latencyCounts, total, ratio) {
52833
53845
  if (total <= 0 || latencyCounts.size === 0)
@@ -52850,7 +53862,22 @@ function finalizeStats(stats) {
52850
53862
  errorCount: 0,
52851
53863
  errorRate: 0,
52852
53864
  avgLatencyMs: 0,
52853
- p95LatencyMs: 0
53865
+ p95LatencyMs: 0,
53866
+ tokenUsageCount: 0,
53867
+ inputTokens: 0,
53868
+ outputTokens: 0,
53869
+ totalTokens: 0,
53870
+ cachedInputTokens: 0,
53871
+ cacheHitInputTokens: 0,
53872
+ cacheHitRate: 0,
53873
+ cacheHitRateDenominatorTokens: 0,
53874
+ cacheReadInputTokens: 0,
53875
+ cacheCreationInputTokens: 0,
53876
+ cacheWriteInputTokens: 0,
53877
+ cacheMissInputTokens: 0,
53878
+ reasoningTokens: 0,
53879
+ billableInputTokens: 0,
53880
+ billableOutputTokens: 0
52854
53881
  };
52855
53882
  }
52856
53883
  return {
@@ -52858,8 +53885,26 @@ function finalizeStats(stats) {
52858
53885
  errorCount: stats.errorCount,
52859
53886
  errorRate: toPercent3(stats.errorCount, stats.total),
52860
53887
  avgLatencyMs: Math.round(stats.latencySum / stats.total),
52861
- p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95)
52862
- };
53888
+ p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95),
53889
+ tokenUsageCount: stats.tokenUsageCount,
53890
+ inputTokens: stats.inputTokens,
53891
+ outputTokens: stats.outputTokens,
53892
+ totalTokens: stats.totalTokens,
53893
+ cachedInputTokens: stats.cachedInputTokens,
53894
+ cacheHitInputTokens: stats.cacheHitInputTokens,
53895
+ cacheHitRate: toPercent3(stats.cacheHitInputTokens, stats.cacheHitRateDenominatorTokens),
53896
+ cacheHitRateDenominatorTokens: stats.cacheHitRateDenominatorTokens,
53897
+ cacheReadInputTokens: stats.cacheReadInputTokens,
53898
+ cacheCreationInputTokens: stats.cacheCreationInputTokens,
53899
+ cacheWriteInputTokens: stats.cacheWriteInputTokens,
53900
+ cacheMissInputTokens: stats.cacheMissInputTokens,
53901
+ reasoningTokens: stats.reasoningTokens,
53902
+ billableInputTokens: stats.billableInputTokens,
53903
+ billableOutputTokens: stats.billableOutputTokens
53904
+ };
53905
+ }
53906
+ function createEmptyLogQueryStats() {
53907
+ return finalizeStats(createRunningStats());
52863
53908
  }
52864
53909
  function compareLocatedEvents(a, b, sort) {
52865
53910
  if (a.ts !== b.ts) {
@@ -52910,7 +53955,7 @@ function normalizeQuery(input, maxLimit = MAX_QUERY_LIMIT) {
52910
53955
  }
52911
53956
  function eventToSummary(item) {
52912
53957
  const { event } = item;
52913
- const identity = resolveLogSessionIdentity(event.request_body);
53958
+ const identity = item.identity ?? resolveLogSessionIdentity(event.request_body);
52914
53959
  return {
52915
53960
  id: item.id,
52916
53961
  ts: event.ts_start,
@@ -52931,21 +53976,43 @@ function eventToSummary(item) {
52931
53976
  hasMetadata: identity.hasMetadata,
52932
53977
  userIdRaw: identity.userIdRaw,
52933
53978
  userKey: identity.userKey,
52934
- sessionId: identity.sessionId
53979
+ sessionId: identity.sessionId,
53980
+ tokenUsage: item.tokenUsage
52935
53981
  };
52936
53982
  }
52937
- function createLogEventSummaryFromEvent(event, location) {
53983
+ function extractLogEventFacts(event) {
52938
53984
  const ts = Date.parse(event.ts_start);
53985
+ const level = getLevel2(event);
53986
+ const statusClass = getStatusClass3(event);
53987
+ const identity = resolveLogSessionIdentity(event.request_body);
53988
+ return {
53989
+ event,
53990
+ ts: Number.isFinite(ts) ? ts : Number.NaN,
53991
+ level,
53992
+ statusClass,
53993
+ model: event.model_out || event.model_in,
53994
+ identity,
53995
+ hasError: level === "error",
53996
+ keywordText: buildKeywordText(event, identity),
53997
+ tokenUsage: extractTokenUsageSummaryFromLogEvent(event)
53998
+ };
53999
+ }
54000
+ function createLogEventSummaryFromFacts(facts, location) {
52939
54001
  return eventToSummary({
52940
54002
  id: location.id,
52941
54003
  date: location.date,
52942
54004
  line: location.line ?? 0,
52943
- ts: Number.isFinite(ts) ? ts : 0,
52944
- level: getLevel2(event),
52945
- statusClass: getStatusClass3(event),
52946
- event
54005
+ ts: facts.ts,
54006
+ level: facts.level,
54007
+ statusClass: facts.statusClass,
54008
+ event: facts.event,
54009
+ identity: facts.identity,
54010
+ tokenUsage: facts.tokenUsage
52947
54011
  });
52948
54012
  }
54013
+ function createLogEventSummaryFromEvent(event, location) {
54014
+ return createLogEventSummaryFromFacts(extractLogEventFacts(event), location);
54015
+ }
52949
54016
  function logEventMatchesQuery(event, query) {
52950
54017
  if (!event.ts_start)
52951
54018
  return false;
@@ -53032,23 +54099,23 @@ function readStreamContent(baseDir, streamFile) {
53032
54099
  if (!streamFile)
53033
54100
  return { content: null, warning: null };
53034
54101
  try {
53035
- const resolvedBase = resolve4(baseDir);
53036
- const resolvedFromFile = resolve4(streamFile);
54102
+ const resolvedBase = resolve6(baseDir);
54103
+ const resolvedFromFile = resolve6(streamFile);
53037
54104
  const looksLikeStreamFile = resolvedFromFile.endsWith(".sse.raw");
53038
54105
  if (!looksLikeStreamFile) {
53039
54106
  return { content: null, warning: "stream_file \u4E0D\u662F .sse.raw \u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7\u8BFB\u53D6\u3002" };
53040
54107
  }
53041
- if (existsSync4(resolvedFromFile)) {
53042
- return { content: readFileSync3(resolvedFromFile, "utf-8"), warning: null };
54108
+ if (existsSync6(resolvedFromFile)) {
54109
+ return { content: readFileSync5(resolvedFromFile, "utf-8"), warning: null };
53043
54110
  }
53044
- const fallbackPath = resolve4(resolvedBase, streamFile);
54111
+ const fallbackPath = resolve6(resolvedBase, streamFile);
53045
54112
  if (!fallbackPath.startsWith(`${resolvedBase}/`) && fallbackPath !== resolvedBase) {
53046
54113
  return { content: null, warning: "stream_file \u8DEF\u5F84\u975E\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u8BFB\u53D6\u3002" };
53047
54114
  }
53048
- if (!existsSync4(fallbackPath)) {
54115
+ if (!existsSync6(fallbackPath)) {
53049
54116
  return { content: null, warning: "stream_file \u4E0D\u5B58\u5728\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\u3002" };
53050
54117
  }
53051
- return { content: readFileSync3(fallbackPath, "utf-8"), warning: null };
54118
+ return { content: readFileSync5(fallbackPath, "utf-8"), warning: null };
53052
54119
  } catch (err) {
53053
54120
  return {
53054
54121
  content: null,
@@ -53065,6 +54132,10 @@ async function buildLogEventDetail(id, parsed, location, context2) {
53065
54132
  const responseBodyAvailable = event.response_body !== undefined;
53066
54133
  const streamCaptured = Boolean(event.stream_file);
53067
54134
  const { content: streamContent, warning: streamWarning } = readStreamContent(resolveLogBaseDir(context2.logConfig), event.stream_file);
54135
+ const tokenUsage = extractTokenUsageSummaryFromLogEvent(event, {
54136
+ baseDir: resolveLogBaseDir(context2.logConfig),
54137
+ streamContent: streamContent ?? undefined
54138
+ }) ?? null;
53068
54139
  const hasPluginData = event.plugins_request || event.plugins_response || event.request_body_after_plugins !== undefined || event.request_url_after_plugins !== undefined || event.response_body_before_plugins !== undefined || event.response_body_after_plugins !== undefined;
53069
54140
  const pluginsSection = hasPluginData ? {
53070
54141
  request: event.plugins_request,
@@ -53090,7 +54161,16 @@ async function buildLogEventDetail(id, parsed, location, context2) {
53090
54161
  hasError: level === "error",
53091
54162
  model: event.model_out || event.model_in,
53092
54163
  modelIn: event.model_in,
53093
- modelOut: event.model_out
54164
+ modelOut: event.model_out,
54165
+ tokenUsage
54166
+ },
54167
+ usage: {
54168
+ tokenUsage,
54169
+ requestBytes: event.request_bytes ?? 0,
54170
+ responseBytes: event.response_bytes ?? null,
54171
+ streamBytes: event.stream_bytes ?? null,
54172
+ streamFileBytes: event.stream_file_bytes ?? null,
54173
+ streamFileTruncated: event.stream_file_truncated === true
53094
54174
  },
53095
54175
  request: {
53096
54176
  method: event.method,
@@ -53131,17 +54211,11 @@ async function buildLogEventDetail(id, parsed, location, context2) {
53131
54211
  };
53132
54212
  }
53133
54213
  async function scanEvents(baseDir, query) {
53134
- const eventsDir = join5(baseDir, "events");
53135
- if (!existsSync4(eventsDir)) {
54214
+ const eventsDir = join7(baseDir, "events");
54215
+ if (!existsSync6(eventsDir)) {
53136
54216
  return {
53137
54217
  items: [],
53138
- stats: {
53139
- total: 0,
53140
- errorCount: 0,
53141
- errorRate: 0,
53142
- avgLatencyMs: 0,
53143
- p95LatencyMs: 0
53144
- },
54218
+ stats: createEmptyLogQueryStats(),
53145
54219
  meta: {
53146
54220
  scannedFiles: 0,
53147
54221
  scannedLines: 0,
@@ -53164,8 +54238,8 @@ async function scanEvents(baseDir, query) {
53164
54238
  truncated = true;
53165
54239
  break;
53166
54240
  }
53167
- const filePath = join5(eventsDir, `${date5}.jsonl`);
53168
- if (!existsSync4(filePath))
54241
+ const filePath = join7(eventsDir, `${date5}.jsonl`);
54242
+ if (!existsSync6(filePath))
53169
54243
  continue;
53170
54244
  scannedFiles += 1;
53171
54245
  const stream = createReadStream3(filePath, { encoding: "utf-8" });
@@ -53227,6 +54301,7 @@ async function scanEvents(baseDir, query) {
53227
54301
  continue;
53228
54302
  if (!containsKeyword(event, query.q))
53229
54303
  continue;
54304
+ const tokenUsage = extractTokenUsageSummaryFromLogEvent(event, { baseDir });
53230
54305
  const located = {
53231
54306
  id: encodeEventId(date5, lineNumber),
53232
54307
  date: date5,
@@ -53234,7 +54309,8 @@ async function scanEvents(baseDir, query) {
53234
54309
  ts,
53235
54310
  level,
53236
54311
  statusClass,
53237
- event
54312
+ event,
54313
+ tokenUsage
53238
54314
  };
53239
54315
  updateRunningStats(runningStats, located);
53240
54316
  insertBoundedEvent(items, located, query.sort, maxKeep);
@@ -53254,6 +54330,9 @@ async function scanEvents(baseDir, query) {
53254
54330
  function isLogQueryWindow(value) {
53255
54331
  return value === "1h" || value === "6h" || value === "24h";
53256
54332
  }
54333
+ function getLogQueryWindowMs(window2) {
54334
+ return WINDOW_MS2[window2];
54335
+ }
53257
54336
  function resolveLogQueryRange(input) {
53258
54337
  const nowMs = input.nowMs ?? Date.now();
53259
54338
  const hasFrom = !!input.from;
@@ -53294,13 +54373,7 @@ async function queryLogEventsInternal(context2, input, maxLimit) {
53294
54373
  items: [],
53295
54374
  nextCursor: null,
53296
54375
  hasMore: false,
53297
- stats: {
53298
- total: 0,
53299
- errorCount: 0,
53300
- errorRate: 0,
53301
- avgLatencyMs: 0,
53302
- p95LatencyMs: 0
53303
- },
54376
+ stats: createEmptyLogQueryStats(),
53304
54377
  meta: {
53305
54378
  scannedFiles: 0,
53306
54379
  scannedLines: 0,
@@ -53351,8 +54424,8 @@ async function getLogEventDetailById(context2, id) {
53351
54424
  }
53352
54425
  const { date: date5, line: line2 } = decodeEventId(id);
53353
54426
  const baseDir = resolveLogBaseDir(context2.logConfig);
53354
- const filePath = join5(baseDir, "events", `${date5}.jsonl`);
53355
- if (!existsSync4(filePath))
54427
+ const filePath = join7(baseDir, "events", `${date5}.jsonl`);
54428
+ if (!existsSync6(filePath))
53356
54429
  return null;
53357
54430
  const stream = createReadStream3(filePath, { encoding: "utf-8" });
53358
54431
  const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
@@ -53383,6 +54456,7 @@ function escapeCsvValue(value) {
53383
54456
  return text2;
53384
54457
  }
53385
54458
  function toCsvRow(item) {
54459
+ const usage = item.tokenUsage;
53386
54460
  return [
53387
54461
  item.id,
53388
54462
  item.ts,
@@ -53403,7 +54477,36 @@ function toCsvRow(item) {
53403
54477
  item.userKey ?? "",
53404
54478
  item.sessionId ?? "",
53405
54479
  item.message,
53406
- item.errorType ?? ""
54480
+ item.errorType ?? "",
54481
+ usage?.inputTokens ?? "",
54482
+ usage?.outputTokens ?? "",
54483
+ usage?.totalTokens ?? "",
54484
+ usage?.cachedInputTokens ?? "",
54485
+ usage?.cacheHitInputTokens ?? "",
54486
+ usage?.cacheHitRate ?? "",
54487
+ usage?.cacheHitRateDenominatorTokens ?? "",
54488
+ usage?.cacheReadInputTokens ?? "",
54489
+ usage?.cacheCreationInputTokens ?? "",
54490
+ usage?.cacheCreationInputTokens5m ?? "",
54491
+ usage?.cacheCreationInputTokens1h ?? "",
54492
+ usage?.cacheWriteInputTokens ?? "",
54493
+ usage?.cacheMissInputTokens ?? "",
54494
+ usage?.reasoningTokens ?? "",
54495
+ usage?.audioInputTokens ?? "",
54496
+ usage?.audioOutputTokens ?? "",
54497
+ usage?.textInputTokens ?? "",
54498
+ usage?.textOutputTokens ?? "",
54499
+ usage?.acceptedPredictionTokens ?? "",
54500
+ usage?.rejectedPredictionTokens ?? "",
54501
+ usage?.toolUsePromptTokens ?? "",
54502
+ usage?.billableInputTokens ?? "",
54503
+ usage?.billableOutputTokens ?? "",
54504
+ usage?.creditUsage ?? "",
54505
+ usage?.cost ?? "",
54506
+ usage?.providerStyle ?? "",
54507
+ usage?.source ?? "",
54508
+ usage?.rawUsagePath ?? "",
54509
+ usage?.cacheHitRateFormula ?? ""
53407
54510
  ].map(escapeCsvValue).join(",");
53408
54511
  }
53409
54512
  function createCsvExportStream(items) {
@@ -53428,7 +54531,36 @@ function createCsvExportStream(items) {
53428
54531
  "userKey",
53429
54532
  "sessionId",
53430
54533
  "message",
53431
- "errorType"
54534
+ "errorType",
54535
+ "usage.inputTokens",
54536
+ "usage.outputTokens",
54537
+ "usage.totalTokens",
54538
+ "usage.cachedInputTokens",
54539
+ "usage.cacheHitInputTokens",
54540
+ "usage.cacheHitRate",
54541
+ "usage.cacheHitRateDenominatorTokens",
54542
+ "usage.cacheReadInputTokens",
54543
+ "usage.cacheCreationInputTokens",
54544
+ "usage.cacheCreationInputTokens5m",
54545
+ "usage.cacheCreationInputTokens1h",
54546
+ "usage.cacheWriteInputTokens",
54547
+ "usage.cacheMissInputTokens",
54548
+ "usage.reasoningTokens",
54549
+ "usage.audioInputTokens",
54550
+ "usage.audioOutputTokens",
54551
+ "usage.textInputTokens",
54552
+ "usage.textOutputTokens",
54553
+ "usage.acceptedPredictionTokens",
54554
+ "usage.rejectedPredictionTokens",
54555
+ "usage.toolUsePromptTokens",
54556
+ "usage.billableInputTokens",
54557
+ "usage.billableOutputTokens",
54558
+ "usage.creditUsage",
54559
+ "usage.cost",
54560
+ "usage.providerStyle",
54561
+ "usage.source",
54562
+ "usage.rawUsagePath",
54563
+ "usage.cacheHitRateFormula"
53432
54564
  ];
53433
54565
  return new ReadableStream({
53434
54566
  start(controller) {
@@ -53506,12 +54638,666 @@ function parseBooleanFlag(value) {
53506
54638
  throw new Error("hasError \u53C2\u6570\u4EC5\u652F\u6301 true/false/1/0");
53507
54639
  }
53508
54640
 
54641
+ // src/log-tail.ts
54642
+ var subscribers = new Set;
54643
+ function publishLogEvent(event) {
54644
+ for (const subscriber of subscribers) {
54645
+ try {
54646
+ subscriber(event);
54647
+ } catch {}
54648
+ }
54649
+ }
54650
+ function subscribeLogEvents(subscriber) {
54651
+ subscribers.add(subscriber);
54652
+ return () => {
54653
+ subscribers.delete(subscriber);
54654
+ };
54655
+ }
54656
+
54657
+ // src/network-access.ts
54658
+ var REMOTE_ADDRESS_ENV_KEY = "LOCAL_ROUTER_REMOTE_ADDRESS";
54659
+ function parseIpv4(address) {
54660
+ const parts = address.split(".");
54661
+ if (parts.length !== 4)
54662
+ return null;
54663
+ const octets = parts.map((part) => {
54664
+ if (!/^\d{1,3}$/.test(part))
54665
+ return Number.NaN;
54666
+ const value = Number.parseInt(part, 10);
54667
+ return value >= 0 && value <= 255 ? value : Number.NaN;
54668
+ });
54669
+ return octets.every(Number.isFinite) ? octets : null;
54670
+ }
54671
+ function normalizeIpAddress(raw2) {
54672
+ let address = raw2.trim().toLowerCase();
54673
+ if (address.startsWith("[")) {
54674
+ const end = address.indexOf("]");
54675
+ if (end !== -1)
54676
+ address = address.slice(1, end);
54677
+ }
54678
+ const mappedIpv4 = address.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
54679
+ if (mappedIpv4) {
54680
+ return mappedIpv4[1];
54681
+ }
54682
+ return address;
54683
+ }
54684
+ function isLoopbackAddress(raw2) {
54685
+ if (!raw2)
54686
+ return false;
54687
+ const address = normalizeIpAddress(raw2);
54688
+ const ipv43 = parseIpv4(address);
54689
+ if (ipv43)
54690
+ return ipv43[0] === 127;
54691
+ return address === "::1";
54692
+ }
54693
+ function isLanAddress(raw2) {
54694
+ if (!raw2)
54695
+ return false;
54696
+ const address = normalizeIpAddress(raw2);
54697
+ const ipv43 = parseIpv4(address);
54698
+ if (ipv43) {
54699
+ const [a, b] = ipv43;
54700
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
54701
+ }
54702
+ return address.startsWith("fc") || address.startsWith("fd") || /^fe[89ab]/.test(address);
54703
+ }
54704
+ function decideNetworkAccess(serverConfig, rawRemoteAddress) {
54705
+ const remoteAddress = rawRemoteAddress ? normalizeIpAddress(rawRemoteAddress) : null;
54706
+ if (!remoteAddress || isLoopbackAddress(remoteAddress)) {
54707
+ return { allowed: true, remoteAddress };
54708
+ }
54709
+ const lanEnabled = serverConfig?.lanAccess?.enabled === true;
54710
+ if (!lanEnabled) {
54711
+ return { allowed: false, remoteAddress, reason: "lan-disabled" };
54712
+ }
54713
+ if (!isLanAddress(remoteAddress)) {
54714
+ return { allowed: false, remoteAddress, reason: "non-lan-address" };
54715
+ }
54716
+ return { allowed: true, remoteAddress };
54717
+ }
54718
+ function getRemoteAddressFromContext(c2) {
54719
+ const env = c2.env;
54720
+ const value = env?.[REMOTE_ADDRESS_ENV_KEY];
54721
+ return typeof value === "string" && value.trim() ? value : null;
54722
+ }
54723
+ function createNetworkAccessMiddleware(store) {
54724
+ return async (c2, next) => {
54725
+ const decision = decideNetworkAccess(store.get().server, getRemoteAddressFromContext(c2));
54726
+ if (!decision.allowed) {
54727
+ return c2.json({
54728
+ error: decision.reason === "lan-disabled" ? "\u5C40\u57DF\u7F51\u670D\u52A1\u672A\u5F00\u542F\uFF0C\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u8BF7\u6C42" : "\u4EC5\u5141\u8BB8\u672C\u673A\u6216\u5C40\u57DF\u7F51\u6765\u6E90\u8BBF\u95EE",
54729
+ remoteAddress: decision.remoteAddress
54730
+ }, 403);
54731
+ }
54732
+ await next();
54733
+ };
54734
+ }
54735
+
54736
+ // src/log-realtime.ts
54737
+ var WS_PATHNAME = "/api/logs/events/ws";
54738
+ var MAX_CONNECTIONS = 64;
54739
+ var MAX_SUBSCRIPTIONS = 64;
54740
+ var MAX_CLIENT_MESSAGE_BYTES = 16 * 1024;
54741
+ var MAX_GLOBAL_EVENT_QUEUE_ITEMS = 1000;
54742
+ var MAX_GLOBAL_EVENT_QUEUE_BYTES = 16 * 1024 * 1024;
54743
+ var MAX_PENDING_BYTES_PER_CONNECTION = 1024 * 1024;
54744
+ var MAX_TOTAL_PENDING_BYTES = 32 * 1024 * 1024;
54745
+ var BACKPRESSURE_BUFFERED_BYTES = 512 * 1024;
54746
+ var HEARTBEAT_INTERVAL_MS = 15000;
54747
+ var FLUSH_BUDGET_MS = 10;
54748
+ var MAX_Q_LENGTH2 = 200;
54749
+ function jsonResponse(status, body) {
54750
+ return new Response(JSON.stringify(body), {
54751
+ status,
54752
+ headers: { "Content-Type": "application/json; charset=utf-8" }
54753
+ });
54754
+ }
54755
+ function createId(prefix) {
54756
+ return `${prefix}_${crypto.randomUUID()}`;
54757
+ }
54758
+ function estimatePublishedEventBytes(event) {
54759
+ const bodyBytes = (event.event.request_bytes ?? 0) + (event.event.response_bytes ?? 0);
54760
+ const streamBytes = event.event.stream_bytes ?? 0;
54761
+ return 1024 + Math.min(64 * 1024, Math.max(0, bodyBytes + streamBytes));
54762
+ }
54763
+ function byteLength(text2) {
54764
+ return Buffer.byteLength(text2);
54765
+ }
54766
+ function isSocketOpen(ws) {
54767
+ return typeof ws.readyState !== "number" || ws.readyState === 1;
54768
+ }
54769
+ function bufferedAmount(ws) {
54770
+ return typeof ws.bufferedAmount === "number" ? ws.bufferedAmount : 0;
54771
+ }
54772
+ function parseClientMessage(raw2) {
54773
+ let text2;
54774
+ if (typeof raw2 === "string") {
54775
+ text2 = raw2;
54776
+ } else {
54777
+ text2 = new TextDecoder().decode(raw2);
54778
+ }
54779
+ if (byteLength(text2) > MAX_CLIENT_MESSAGE_BYTES) {
54780
+ throw new Error("\u5BA2\u6237\u7AEF\u6D88\u606F\u8FC7\u5927");
54781
+ }
54782
+ const parsed = JSON.parse(text2);
54783
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
54784
+ throw new Error("\u5BA2\u6237\u7AEF\u6D88\u606F\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
54785
+ }
54786
+ const message = parsed;
54787
+ if (message.type !== "subscribe" && message.type !== "unsubscribe" && message.type !== "ping") {
54788
+ throw new Error("\u4E0D\u652F\u6301\u7684\u5B9E\u65F6\u65E5\u5FD7\u6D88\u606F\u7C7B\u578B");
54789
+ }
54790
+ return message;
54791
+ }
54792
+ function parseStringValue(value) {
54793
+ if (typeof value !== "string")
54794
+ return;
54795
+ const trimmed = value.trim();
54796
+ return trimmed || undefined;
54797
+ }
54798
+ function parseStringArrayValue(value) {
54799
+ if (Array.isArray(value)) {
54800
+ return value.flatMap((item) => typeof item === "string" ? parseCommaSeparated(item) : []);
54801
+ }
54802
+ if (typeof value === "string")
54803
+ return parseCommaSeparated(value);
54804
+ return [];
54805
+ }
54806
+ function parseBooleanValue(value) {
54807
+ if (typeof value === "boolean")
54808
+ return value;
54809
+ if (typeof value === "string")
54810
+ return parseBooleanFlag(value);
54811
+ return null;
54812
+ }
54813
+ function createQueryHash(value) {
54814
+ const text2 = JSON.stringify(value);
54815
+ let hash2 = 2166136261;
54816
+ for (let i = 0;i < text2.length; i += 1) {
54817
+ hash2 ^= text2.charCodeAt(i);
54818
+ hash2 = Math.imul(hash2, 16777619);
54819
+ }
54820
+ return `q_${(hash2 >>> 0).toString(36)}`;
54821
+ }
54822
+ function parseValidatedArray(value, validate, errorMessage) {
54823
+ const raw2 = parseStringArrayValue(value);
54824
+ const parsed = raw2.filter(validate);
54825
+ if (parsed.length !== raw2.length) {
54826
+ throw new Error(errorMessage);
54827
+ }
54828
+ return parsed;
54829
+ }
54830
+ function compileRealtimeQuery(input) {
54831
+ const query = input ?? {};
54832
+ const nowMs = Date.now();
54833
+ const windowRaw = parseStringValue(query.window) ?? "24h";
54834
+ if (!isLogQueryWindow(windowRaw)) {
54835
+ throw new Error("window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h");
54836
+ }
54837
+ const from = parseStringValue(query.from);
54838
+ const to = parseStringValue(query.to);
54839
+ const range = from || to ? {
54840
+ type: "fixed",
54841
+ ...resolveLogQueryRange({ window: windowRaw, from, to, nowMs })
54842
+ } : {
54843
+ type: "window",
54844
+ window: windowRaw,
54845
+ windowMs: getLogQueryWindowMs(windowRaw)
54846
+ };
54847
+ if (range.type === "fixed" && range.toMs <= nowMs) {
54848
+ throw new Error("\u56FA\u5B9A\u7ED3\u675F\u65F6\u95F4\u5DF2\u8FC7\u671F\uFF0C\u5B9E\u65F6\u63A8\u9001\u4EC5\u652F\u6301\u4ECD\u5728\u6EDA\u52A8\u7684\u67E5\u8BE2\u7A97\u53E3");
54849
+ }
54850
+ const sortRaw = parseStringValue(query.sort) ?? "time_desc";
54851
+ if (!validateSort(sortRaw)) {
54852
+ throw new Error("sort \u53C2\u6570\u4EC5\u652F\u6301 time_desc | time_asc");
54853
+ }
54854
+ const levels = parseValidatedArray(query.levels, validateLogLevel, "levels \u53C2\u6570\u4EC5\u652F\u6301 info,error");
54855
+ const statusClasses = parseValidatedArray(query.statusClass, validateStatusClass, "statusClass \u53C2\u6570\u4EC5\u652F\u6301 2xx,4xx,5xx,network_error");
54856
+ const q = (parseStringValue(query.q) ?? "").slice(0, MAX_Q_LENGTH2).toLowerCase();
54857
+ const normalized = {
54858
+ range,
54859
+ sort: sortRaw,
54860
+ levels,
54861
+ providers: parseStringArrayValue(query.provider),
54862
+ routeTypes: parseStringArrayValue(query.routeType),
54863
+ models: parseStringArrayValue(query.model),
54864
+ modelIns: parseStringArrayValue(query.modelIn),
54865
+ modelOuts: parseStringArrayValue(query.modelOut),
54866
+ users: parseStringArrayValue(query.user),
54867
+ sessions: parseStringArrayValue(query.session),
54868
+ statusClasses,
54869
+ hasError: parseBooleanValue(query.hasError),
54870
+ q
54871
+ };
54872
+ return {
54873
+ queryHash: createQueryHash(normalized),
54874
+ sort: sortRaw,
54875
+ range,
54876
+ levels: new Set(levels),
54877
+ providers: new Set(normalized.providers),
54878
+ routeTypes: new Set(normalized.routeTypes),
54879
+ models: new Set(normalized.models),
54880
+ modelIns: new Set(normalized.modelIns),
54881
+ modelOuts: new Set(normalized.modelOuts),
54882
+ users: new Set(normalized.users),
54883
+ sessions: new Set(normalized.sessions),
54884
+ statusClasses: new Set(statusClasses),
54885
+ hasError: normalized.hasError,
54886
+ q
54887
+ };
54888
+ }
54889
+ function setHas(set2, value) {
54890
+ return set2.size === 0 || set2.has(value);
54891
+ }
54892
+ function matchesCompiledQuery(query, facts, nowMs) {
54893
+ if (!facts.event.ts_start || !Number.isFinite(facts.ts))
54894
+ return false;
54895
+ const fromMs = query.range.type === "window" ? nowMs - query.range.windowMs : query.range.fromMs;
54896
+ const toMs = query.range.type === "window" ? nowMs : query.range.toMs;
54897
+ if (facts.ts < fromMs || facts.ts > toMs)
54898
+ return false;
54899
+ if (!setHas(query.levels, facts.level))
54900
+ return false;
54901
+ if (!setHas(query.providers, facts.event.provider))
54902
+ return false;
54903
+ if (!setHas(query.routeTypes, facts.event.route_type))
54904
+ return false;
54905
+ if (!setHas(query.models, facts.model))
54906
+ return false;
54907
+ if (!setHas(query.modelIns, facts.event.model_in))
54908
+ return false;
54909
+ if (!setHas(query.modelOuts, facts.event.model_out))
54910
+ return false;
54911
+ if (query.users.size > 0) {
54912
+ const matchedByRaw = facts.identity.userIdRaw ? query.users.has(facts.identity.userIdRaw) : false;
54913
+ const matchedByUserKey = facts.identity.userKey ? query.users.has(facts.identity.userKey) : false;
54914
+ if (!matchedByRaw && !matchedByUserKey)
54915
+ return false;
54916
+ }
54917
+ if (query.sessions.size > 0) {
54918
+ if (!facts.identity.sessionId || !query.sessions.has(facts.identity.sessionId))
54919
+ return false;
54920
+ }
54921
+ if (!setHas(query.statusClasses, facts.statusClass))
54922
+ return false;
54923
+ if (query.hasError !== null && query.hasError !== facts.hasError)
54924
+ return false;
54925
+ return !query.q || facts.keywordText.includes(query.q);
54926
+ }
54927
+ function createLogRealtimeRuntime(options) {
54928
+ const connections = new Map;
54929
+ const subscriptions = new Map;
54930
+ const eventQueue = [];
54931
+ let tailUnsubscribe = null;
54932
+ let flushTimer = null;
54933
+ let heartbeatTimer = null;
54934
+ let eventQueueBytes = 0;
54935
+ let totalPendingBytes = 0;
54936
+ let droppedUpstreamEvents = 0;
54937
+ const sendText = (connection, text2) => {
54938
+ if (!isSocketOpen(connection.ws))
54939
+ return false;
54940
+ const bytes = byteLength(text2);
54941
+ const shouldQueue = connection.queue.length > 0 || bufferedAmount(connection.ws) > BACKPRESSURE_BUFFERED_BYTES;
54942
+ if (!shouldQueue) {
54943
+ try {
54944
+ connection.ws.send(text2);
54945
+ return true;
54946
+ } catch {
54947
+ removeConnection(connection.id, "send-failed");
54948
+ return false;
54949
+ }
54950
+ }
54951
+ if (connection.pendingBytes + bytes > MAX_PENDING_BYTES_PER_CONNECTION || totalPendingBytes + bytes > MAX_TOTAL_PENDING_BYTES) {
54952
+ connection.droppedOutbound += 1;
54953
+ if (connection.droppedOutbound >= 3) {
54954
+ removeConnection(connection.id, "slow-client");
54955
+ }
54956
+ return false;
54957
+ }
54958
+ connection.queue.push({ text: text2, bytes });
54959
+ connection.pendingBytes += bytes;
54960
+ totalPendingBytes += bytes;
54961
+ return true;
54962
+ };
54963
+ const sendMessage = (connection, message) => {
54964
+ return sendText(connection, JSON.stringify(message));
54965
+ };
54966
+ const flushConnectionQueue = (connection) => {
54967
+ if (!isSocketOpen(connection.ws))
54968
+ return;
54969
+ while (connection.queue.length > 0 && bufferedAmount(connection.ws) <= BACKPRESSURE_BUFFERED_BYTES) {
54970
+ const item = connection.queue.shift();
54971
+ if (!item)
54972
+ break;
54973
+ connection.pendingBytes -= item.bytes;
54974
+ totalPendingBytes -= item.bytes;
54975
+ try {
54976
+ connection.ws.send(item.text);
54977
+ } catch {
54978
+ removeConnection(connection.id, "send-failed");
54979
+ return;
54980
+ }
54981
+ }
54982
+ };
54983
+ const sendError = (connection, error48, requestId) => {
54984
+ sendMessage(connection, { type: "error", requestId, error: error48 });
54985
+ };
54986
+ const ensureTailSubscription = () => {
54987
+ if (tailUnsubscribe || subscriptions.size === 0)
54988
+ return;
54989
+ tailUnsubscribe = subscribeLogEvents((event) => {
54990
+ if (subscriptions.size === 0)
54991
+ return;
54992
+ const bytes = estimatePublishedEventBytes(event);
54993
+ while (eventQueue.length >= MAX_GLOBAL_EVENT_QUEUE_ITEMS || eventQueueBytes + bytes > MAX_GLOBAL_EVENT_QUEUE_BYTES) {
54994
+ const dropped = eventQueue.shift();
54995
+ if (!dropped)
54996
+ break;
54997
+ eventQueueBytes -= dropped.bytes;
54998
+ droppedUpstreamEvents += 1;
54999
+ }
55000
+ eventQueue.push({ event, bytes });
55001
+ eventQueueBytes += bytes;
55002
+ scheduleFlush();
55003
+ });
55004
+ };
55005
+ const maybeStopTailSubscription = () => {
55006
+ if (subscriptions.size > 0)
55007
+ return;
55008
+ tailUnsubscribe?.();
55009
+ tailUnsubscribe = null;
55010
+ eventQueue.length = 0;
55011
+ eventQueueBytes = 0;
55012
+ droppedUpstreamEvents = 0;
55013
+ };
55014
+ const sendOverflow = (subscription, dropped, message) => {
55015
+ const connection = connections.get(subscription.connectionId);
55016
+ if (!connection)
55017
+ return;
55018
+ sendMessage(connection, {
55019
+ type: "overflow",
55020
+ subscriptionId: subscription.id,
55021
+ dropped,
55022
+ message
55023
+ });
55024
+ };
55025
+ const removeSubscription = (subscriptionId, reason, notify2 = true) => {
55026
+ const subscription = subscriptions.get(subscriptionId);
55027
+ if (!subscription)
55028
+ return;
55029
+ subscriptions.delete(subscriptionId);
55030
+ const connection = connections.get(subscription.connectionId);
55031
+ if (connection) {
55032
+ connection.subscriptions.delete(subscriptionId);
55033
+ if (notify2) {
55034
+ sendMessage(connection, {
55035
+ type: "unsubscribed",
55036
+ subscriptionId,
55037
+ reason
55038
+ });
55039
+ }
55040
+ }
55041
+ maybeStopTailSubscription();
55042
+ };
55043
+ function removeConnection(connectionId, reason) {
55044
+ const connection = connections.get(connectionId);
55045
+ if (!connection)
55046
+ return;
55047
+ connections.delete(connectionId);
55048
+ for (const subscriptionId of Array.from(connection.subscriptions)) {
55049
+ removeSubscription(subscriptionId, reason, false);
55050
+ }
55051
+ for (const item of connection.queue) {
55052
+ totalPendingBytes -= item.bytes;
55053
+ }
55054
+ connection.queue.length = 0;
55055
+ connection.pendingBytes = 0;
55056
+ try {
55057
+ connection.ws.close(1001, reason);
55058
+ } catch {}
55059
+ if (connections.size === 0 && heartbeatTimer) {
55060
+ clearInterval(heartbeatTimer);
55061
+ heartbeatTimer = null;
55062
+ }
55063
+ maybeStopTailSubscription();
55064
+ }
55065
+ const pruneExpiredSubscriptions = (nowMs) => {
55066
+ for (const subscription of Array.from(subscriptions.values())) {
55067
+ const range = subscription.query.range;
55068
+ if (range.type === "fixed" && range.toMs <= nowMs) {
55069
+ removeSubscription(subscription.id, "expired");
55070
+ }
55071
+ }
55072
+ };
55073
+ const flushEvents = () => {
55074
+ flushTimer = null;
55075
+ if (subscriptions.size === 0) {
55076
+ eventQueue.length = 0;
55077
+ eventQueueBytes = 0;
55078
+ droppedUpstreamEvents = 0;
55079
+ maybeStopTailSubscription();
55080
+ return;
55081
+ }
55082
+ const startedAt = Date.now();
55083
+ const nowMs = Date.now();
55084
+ pruneExpiredSubscriptions(nowMs);
55085
+ if (droppedUpstreamEvents > 0) {
55086
+ const dropped = droppedUpstreamEvents;
55087
+ droppedUpstreamEvents = 0;
55088
+ for (const subscription of subscriptions.values()) {
55089
+ sendOverflow(subscription, dropped, `\u5B9E\u65F6\u65E5\u5FD7\u961F\u5217\u5DF2\u4E22\u5F03 ${dropped} \u6761\u4E8B\u4EF6\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u4EE5\u8865\u9F50\u3002`);
55090
+ }
55091
+ }
55092
+ while (eventQueue.length > 0 && Date.now() - startedAt < FLUSH_BUDGET_MS) {
55093
+ const queued = eventQueue.shift();
55094
+ if (!queued)
55095
+ break;
55096
+ eventQueueBytes -= queued.bytes;
55097
+ const facts = extractLogEventFacts(queued.event.event);
55098
+ let summary = null;
55099
+ for (const subscription of subscriptions.values()) {
55100
+ if (!matchesCompiledQuery(subscription.query, facts, Date.now()))
55101
+ continue;
55102
+ const connection = connections.get(subscription.connectionId);
55103
+ if (!connection)
55104
+ continue;
55105
+ summary ??= createLogEventSummaryFromFacts(facts, {
55106
+ id: queued.event.id,
55107
+ date: queued.event.date,
55108
+ line: null
55109
+ });
55110
+ const sent = sendMessage(connection, {
55111
+ type: "log.event",
55112
+ subscriptionId: subscription.id,
55113
+ item: summary
55114
+ });
55115
+ if (!sent) {
55116
+ sendOverflow(subscription, 1, "\u5B9E\u65F6\u65E5\u5FD7\u5BA2\u6237\u7AEF\u53D1\u9001\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u4EE5\u8865\u9F50\u3002");
55117
+ }
55118
+ }
55119
+ }
55120
+ for (const connection of connections.values()) {
55121
+ flushConnectionQueue(connection);
55122
+ }
55123
+ if (eventQueue.length > 0)
55124
+ scheduleFlush();
55125
+ };
55126
+ function scheduleFlush() {
55127
+ if (flushTimer)
55128
+ return;
55129
+ flushTimer = setTimeout(flushEvents, 0);
55130
+ flushTimer.unref?.();
55131
+ }
55132
+ const ensureHeartbeat = () => {
55133
+ if (heartbeatTimer)
55134
+ return;
55135
+ heartbeatTimer = setInterval(() => {
55136
+ const ts = new Date().toISOString();
55137
+ pruneExpiredSubscriptions(Date.now());
55138
+ for (const connection of connections.values()) {
55139
+ sendMessage(connection, { type: "pong", ts });
55140
+ flushConnectionQueue(connection);
55141
+ }
55142
+ }, HEARTBEAT_INTERVAL_MS);
55143
+ heartbeatTimer.unref?.();
55144
+ };
55145
+ const subscribeConnection = (connection, requestId, query) => {
55146
+ if (subscriptions.size >= MAX_SUBSCRIPTIONS && connection.subscriptions.size === 0) {
55147
+ sendError(connection, "\u5B9E\u65F6\u65E5\u5FD7\u8BA2\u9605\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", requestId);
55148
+ return;
55149
+ }
55150
+ let compiled;
55151
+ try {
55152
+ compiled = compileRealtimeQuery(query);
55153
+ } catch (err) {
55154
+ sendError(connection, err instanceof Error ? err.message : String(err), requestId);
55155
+ return;
55156
+ }
55157
+ for (const subscriptionId2 of Array.from(connection.subscriptions)) {
55158
+ removeSubscription(subscriptionId2, "replaced");
55159
+ }
55160
+ const subscriptionId = createId("sub");
55161
+ const subscription = {
55162
+ id: subscriptionId,
55163
+ connectionId: connection.id,
55164
+ query: compiled
55165
+ };
55166
+ subscriptions.set(subscriptionId, subscription);
55167
+ connection.subscriptions.add(subscriptionId);
55168
+ ensureTailSubscription();
55169
+ sendMessage(connection, {
55170
+ type: "subscribed",
55171
+ requestId: requestId ?? null,
55172
+ subscriptionId,
55173
+ queryHash: compiled.queryHash,
55174
+ now: new Date().toISOString()
55175
+ });
55176
+ };
55177
+ const openConnection = (ws) => {
55178
+ const data = ws.data;
55179
+ const connection = {
55180
+ id: data.connectionId,
55181
+ remoteAddress: data.remoteAddress,
55182
+ ws,
55183
+ subscriptions: new Set,
55184
+ queue: [],
55185
+ pendingBytes: 0,
55186
+ droppedOutbound: 0
55187
+ };
55188
+ connections.set(connection.id, connection);
55189
+ ensureHeartbeat();
55190
+ sendMessage(connection, {
55191
+ type: "ready",
55192
+ connectionId: connection.id,
55193
+ now: new Date().toISOString()
55194
+ });
55195
+ };
55196
+ const handleMessage = (ws, raw2) => {
55197
+ const connection = connections.get(ws.data.connectionId);
55198
+ if (!connection)
55199
+ return;
55200
+ let message;
55201
+ try {
55202
+ message = parseClientMessage(raw2);
55203
+ } catch (err) {
55204
+ sendError(connection, err instanceof Error ? err.message : String(err));
55205
+ return;
55206
+ }
55207
+ if (message.type === "ping") {
55208
+ sendMessage(connection, { type: "pong", ts: message.ts ?? new Date().toISOString() });
55209
+ return;
55210
+ }
55211
+ if (message.type === "unsubscribe") {
55212
+ if (message.subscriptionId) {
55213
+ removeSubscription(message.subscriptionId, "client-unsubscribe");
55214
+ } else {
55215
+ for (const subscriptionId of Array.from(connection.subscriptions)) {
55216
+ removeSubscription(subscriptionId, "client-unsubscribe");
55217
+ }
55218
+ }
55219
+ return;
55220
+ }
55221
+ subscribeConnection(connection, message.requestId, message.query);
55222
+ };
55223
+ return {
55224
+ pathname: WS_PATHNAME,
55225
+ upgrade: (request, server, remoteAddress) => {
55226
+ const url2 = new URL(request.url);
55227
+ if (url2.pathname !== WS_PATHNAME)
55228
+ return { handled: false };
55229
+ const upgradeHeader = request.headers.get("upgrade")?.toLowerCase();
55230
+ if (upgradeHeader !== "websocket") {
55231
+ return {
55232
+ handled: true,
55233
+ response: jsonResponse(400, { error: "\u9700\u8981 WebSocket Upgrade" })
55234
+ };
55235
+ }
55236
+ const decision = decideNetworkAccess(options.store.get().server, remoteAddress);
55237
+ if (!decision.allowed) {
55238
+ return {
55239
+ handled: true,
55240
+ response: jsonResponse(403, {
55241
+ error: decision.reason === "lan-disabled" ? "\u5C40\u57DF\u7F51\u670D\u52A1\u672A\u5F00\u542F\uFF0C\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u8BF7\u6C42" : "\u4EC5\u5141\u8BB8\u672C\u673A\u6216\u5C40\u57DF\u7F51\u6765\u6E90\u8BBF\u95EE",
55242
+ remoteAddress: decision.remoteAddress
55243
+ })
55244
+ };
55245
+ }
55246
+ if (connections.size >= MAX_CONNECTIONS) {
55247
+ return {
55248
+ handled: true,
55249
+ response: jsonResponse(503, { error: "\u5B9E\u65F6\u65E5\u5FD7\u8FDE\u63A5\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" })
55250
+ };
55251
+ }
55252
+ const upgraded = server.upgrade(request, {
55253
+ data: {
55254
+ kind: "log-realtime",
55255
+ connectionId: createId("conn"),
55256
+ remoteAddress: decision.remoteAddress
55257
+ }
55258
+ });
55259
+ return upgraded ? { handled: true, upgraded: true } : { handled: true, response: jsonResponse(400, { error: "WebSocket Upgrade \u5931\u8D25" }) };
55260
+ },
55261
+ websocket: {
55262
+ open: openConnection,
55263
+ message: handleMessage,
55264
+ drain: (ws) => {
55265
+ const connection = connections.get(ws.data.connectionId);
55266
+ if (connection)
55267
+ flushConnectionQueue(connection);
55268
+ },
55269
+ close: (ws) => {
55270
+ removeConnection(ws.data.connectionId, "closed");
55271
+ }
55272
+ },
55273
+ dispose: () => {
55274
+ if (flushTimer) {
55275
+ clearTimeout(flushTimer);
55276
+ flushTimer = null;
55277
+ }
55278
+ if (heartbeatTimer) {
55279
+ clearInterval(heartbeatTimer);
55280
+ heartbeatTimer = null;
55281
+ }
55282
+ tailUnsubscribe?.();
55283
+ tailUnsubscribe = null;
55284
+ for (const connectionId of Array.from(connections.keys())) {
55285
+ removeConnection(connectionId, "server-dispose");
55286
+ }
55287
+ eventQueue.length = 0;
55288
+ eventQueueBytes = 0;
55289
+ totalPendingBytes = 0;
55290
+ droppedUpstreamEvents = 0;
55291
+ }
55292
+ };
55293
+ }
55294
+
53509
55295
  // src/log-sessions.ts
53510
- import { createReadStream as createReadStream4, existsSync as existsSync5 } from "fs";
53511
- import { join as join6 } from "path";
55296
+ import { createReadStream as createReadStream4, existsSync as existsSync7 } from "fs";
55297
+ import { join as join8 } from "path";
53512
55298
  import { createInterface as createInterface3 } from "readline";
53513
55299
  var MAX_LINES_SCANNED3 = 250000;
53514
- var MAX_Q_LENGTH2 = 200;
55300
+ var MAX_Q_LENGTH3 = 200;
53515
55301
  function toDayStart4(ms) {
53516
55302
  const date5 = new Date(ms);
53517
55303
  return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
@@ -53530,7 +55316,7 @@ function normalizeInput(input) {
53530
55316
  toMs: input.toMs,
53531
55317
  users: (input.users ?? []).map((item) => item.trim()).filter(Boolean),
53532
55318
  sessions: (input.sessions ?? []).map((item) => item.trim()).filter(Boolean),
53533
- q: qRaw.length > MAX_Q_LENGTH2 ? qRaw.slice(0, MAX_Q_LENGTH2) : qRaw
55319
+ q: qRaw.length > MAX_Q_LENGTH3 ? qRaw.slice(0, MAX_Q_LENGTH3) : qRaw
53534
55320
  };
53535
55321
  }
53536
55322
  function incrementCount(map2, key2) {
@@ -53629,8 +55415,8 @@ async function queryLogSessions(context2, input) {
53629
55415
  return createEmptyResult(normalized.fromMs, normalized.toMs);
53630
55416
  }
53631
55417
  const baseDir = resolveLogBaseDir(context2.logConfig);
53632
- const eventsDir = join6(baseDir, "events");
53633
- if (!existsSync5(eventsDir)) {
55418
+ const eventsDir = join8(baseDir, "events");
55419
+ if (!existsSync7(eventsDir)) {
53634
55420
  return createEmptyResult(normalized.fromMs, normalized.toMs);
53635
55421
  }
53636
55422
  const usersMap = new Map;
@@ -53648,8 +55434,8 @@ async function queryLogSessions(context2, input) {
53648
55434
  truncated = true;
53649
55435
  break;
53650
55436
  }
53651
- const filePath = join6(eventsDir, `${date5}.jsonl`);
53652
- if (!existsSync5(filePath))
55437
+ const filePath = join8(eventsDir, `${date5}.jsonl`);
55438
+ if (!existsSync7(filePath))
53653
55439
  continue;
53654
55440
  scannedFiles += 1;
53655
55441
  const stream = createReadStream4(filePath, { encoding: "utf-8" });
@@ -53757,8 +55543,8 @@ async function queryLogSessions(context2, input) {
53757
55543
  }
53758
55544
 
53759
55545
  // src/log-storage.ts
53760
- import { existsSync as existsSync6, promises as fsPromises } from "fs";
53761
- import { join as join7 } from "path";
55546
+ import { existsSync as existsSync8, promises as fsPromises } from "fs";
55547
+ import { join as join9 } from "path";
53762
55548
  var cachedStorage = null;
53763
55549
  var calculationPromise = null;
53764
55550
  var lastCalculationTime = 0;
@@ -53766,7 +55552,7 @@ var CACHE_TTL_MS2 = 60 * 60 * 1000;
53766
55552
  var CALCULATION_INTERVAL_MS = 60 * 60 * 1000;
53767
55553
  var MIN_CALCULATION_INTERVAL_MS = 5 * 60 * 1000;
53768
55554
  async function calculateDirSize(dirPath) {
53769
- if (!existsSync6(dirPath)) {
55555
+ if (!existsSync8(dirPath)) {
53770
55556
  return { bytes: 0, fileCount: 0 };
53771
55557
  }
53772
55558
  let bytes = 0;
@@ -53775,7 +55561,7 @@ async function calculateDirSize(dirPath) {
53775
55561
  try {
53776
55562
  const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
53777
55563
  for (const entry of entries) {
53778
- const fullPath = join7(currentPath, entry.name);
55564
+ const fullPath = join9(currentPath, entry.name);
53779
55565
  if (entry.isDirectory()) {
53780
55566
  await walk(fullPath);
53781
55567
  } else if (entry.isFile()) {
@@ -53806,8 +55592,8 @@ async function doCalculateStorage(logConfig) {
53806
55592
  }
53807
55593
  const baseDir = resolveLogBaseDir(logConfig);
53808
55594
  const [eventsResult, streamsResult] = await Promise.all([
53809
- calculateDirSize(join7(baseDir, "events")),
53810
- calculateDirSize(join7(baseDir, "streams"))
55595
+ calculateDirSize(join9(baseDir, "events")),
55596
+ calculateDirSize(join9(baseDir, "streams"))
53811
55597
  ]);
53812
55598
  const indexResult = await calculateIndexSize(baseDir);
53813
55599
  return {
@@ -53821,7 +55607,7 @@ async function doCalculateStorage(logConfig) {
53821
55607
  };
53822
55608
  }
53823
55609
  async function calculateIndexSize(baseDir) {
53824
- if (!existsSync6(baseDir)) {
55610
+ if (!existsSync8(baseDir)) {
53825
55611
  return { bytes: 0, fileCount: 0 };
53826
55612
  }
53827
55613
  let bytes = 0;
@@ -53831,7 +55617,7 @@ async function calculateIndexSize(baseDir) {
53831
55617
  for (const entry of entries) {
53832
55618
  if (!entry.isFile() || !entry.name.startsWith("logs-index.sqlite"))
53833
55619
  continue;
53834
- const stats = await fsPromises.stat(join7(baseDir, entry.name));
55620
+ const stats = await fsPromises.stat(join9(baseDir, entry.name));
53835
55621
  bytes += stats.size;
53836
55622
  fileCount += 1;
53837
55623
  }
@@ -53882,33 +55668,17 @@ function startLogStorageBackgroundTask(logConfig) {
53882
55668
  };
53883
55669
  }
53884
55670
 
53885
- // src/log-tail.ts
53886
- var subscribers = new Set;
53887
- function publishLogEvent(event) {
53888
- for (const subscriber of subscribers) {
53889
- try {
53890
- subscriber(event);
53891
- } catch {}
53892
- }
53893
- }
53894
- function subscribeLogEvents(subscriber) {
53895
- subscribers.add(subscriber);
53896
- return () => {
53897
- subscribers.delete(subscriber);
53898
- };
53899
- }
53900
-
53901
55671
  // src/logger.ts
53902
55672
  import {
53903
55673
  appendFileSync,
53904
55674
  closeSync as closeSync2,
53905
- existsSync as existsSync7,
53906
- mkdirSync as mkdirSync3,
55675
+ existsSync as existsSync9,
55676
+ mkdirSync as mkdirSync4,
53907
55677
  openSync as openSync2,
53908
- statSync as statSync2,
55678
+ statSync as statSync3,
53909
55679
  writeSync
53910
55680
  } from "fs";
53911
- import { join as join8 } from "path";
55681
+ import { join as join10 } from "path";
53912
55682
  class Logger {
53913
55683
  baseDir;
53914
55684
  eventsDir;
@@ -53923,8 +55693,8 @@ class Logger {
53923
55693
  this._bodyPolicy = config2.bodyPolicy ?? "off";
53924
55694
  this._streamsEnabled = config2.streams?.enabled !== false;
53925
55695
  this.maxStreamBytes = config2.streams?.maxBytesPerRequest ?? 10 * 1024 * 1024;
53926
- this.eventsDir = join8(baseDir, "events");
53927
- this.streamsDir = join8(baseDir, "streams");
55696
+ this.eventsDir = join10(baseDir, "events");
55697
+ this.streamsDir = join10(baseDir, "streams");
53928
55698
  if (this._enabled)
53929
55699
  this.ensureDirs();
53930
55700
  }
@@ -53936,25 +55706,26 @@ class Logger {
53936
55706
  }
53937
55707
  ensureDirs() {
53938
55708
  for (const dir of [this.baseDir, this.eventsDir, this.streamsDir]) {
53939
- if (!existsSync7(dir))
53940
- mkdirSync3(dir, { recursive: true });
55709
+ if (!existsSync9(dir))
55710
+ mkdirSync4(dir, { recursive: true });
53941
55711
  }
53942
55712
  }
53943
55713
  ensureStreamDateDir(dateStr) {
53944
- const dir = join8(this.streamsDir, dateStr);
53945
- if (!existsSync7(dir))
53946
- mkdirSync3(dir, { recursive: true });
55714
+ const dir = join10(this.streamsDir, dateStr);
55715
+ if (!existsSync9(dir))
55716
+ mkdirSync4(dir, { recursive: true });
53947
55717
  return dir;
53948
55718
  }
53949
55719
  writeEvent(event) {
53950
55720
  if (!this._enabled)
53951
55721
  return;
53952
55722
  try {
55723
+ const enrichedEvent = enrichLogEventTokenUsage(event, { baseDir: this.baseDir });
53953
55724
  this.ensureDirs();
53954
- const dateStr = event.ts_start.slice(0, 10);
53955
- const filePath = join8(this.eventsDir, `${dateStr}.jsonl`);
53956
- const offset = existsSync7(filePath) ? statSync2(filePath).size : 0;
53957
- const line2 = `${JSON.stringify(event)}
55725
+ const dateStr = enrichedEvent.ts_start.slice(0, 10);
55726
+ const filePath = join10(this.eventsDir, `${dateStr}.jsonl`);
55727
+ const offset = existsSync9(filePath) ? statSync3(filePath).size : 0;
55728
+ const line2 = `${JSON.stringify(enrichedEvent)}
53958
55729
  `;
53959
55730
  appendFileSync(filePath, line2);
53960
55731
  const id = encodeOffsetLogEventId(dateStr, offset);
@@ -53964,9 +55735,9 @@ class Logger {
53964
55735
  date: dateStr,
53965
55736
  offset,
53966
55737
  byteLength: Buffer.byteLength(line2),
53967
- event
55738
+ event: enrichedEvent
53968
55739
  });
53969
- publishLogEvent({ id, date: dateStr, filePath, offset, event });
55740
+ publishLogEvent({ id, date: dateStr, filePath, offset, event: enrichedEvent });
53970
55741
  } catch (err) {
53971
55742
  console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
53972
55743
  }
@@ -53982,7 +55753,7 @@ class Logger {
53982
55753
  let fd;
53983
55754
  try {
53984
55755
  const dir = this.ensureStreamDateDir(dateStr);
53985
- filePath = join8(dir, `${requestId}.sse.raw`);
55756
+ filePath = join10(dir, `${requestId}.sse.raw`);
53986
55757
  fd = openSync2(filePath, "a");
53987
55758
  } catch (err) {
53988
55759
  console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u6253\u5F00\u5931\u8D25:", err);
@@ -54072,85 +55843,6 @@ function normalizeUrl(rawUrl) {
54072
55843
  return rawUrl;
54073
55844
  }
54074
55845
 
54075
- // src/network-access.ts
54076
- var REMOTE_ADDRESS_ENV_KEY = "LOCAL_ROUTER_REMOTE_ADDRESS";
54077
- function parseIpv4(address) {
54078
- const parts = address.split(".");
54079
- if (parts.length !== 4)
54080
- return null;
54081
- const octets = parts.map((part) => {
54082
- if (!/^\d{1,3}$/.test(part))
54083
- return Number.NaN;
54084
- const value = Number.parseInt(part, 10);
54085
- return value >= 0 && value <= 255 ? value : Number.NaN;
54086
- });
54087
- return octets.every(Number.isFinite) ? octets : null;
54088
- }
54089
- function normalizeIpAddress(raw2) {
54090
- let address = raw2.trim().toLowerCase();
54091
- if (address.startsWith("[")) {
54092
- const end = address.indexOf("]");
54093
- if (end !== -1)
54094
- address = address.slice(1, end);
54095
- }
54096
- const mappedIpv4 = address.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
54097
- if (mappedIpv4) {
54098
- return mappedIpv4[1];
54099
- }
54100
- return address;
54101
- }
54102
- function isLoopbackAddress(raw2) {
54103
- if (!raw2)
54104
- return false;
54105
- const address = normalizeIpAddress(raw2);
54106
- const ipv43 = parseIpv4(address);
54107
- if (ipv43)
54108
- return ipv43[0] === 127;
54109
- return address === "::1";
54110
- }
54111
- function isLanAddress(raw2) {
54112
- if (!raw2)
54113
- return false;
54114
- const address = normalizeIpAddress(raw2);
54115
- const ipv43 = parseIpv4(address);
54116
- if (ipv43) {
54117
- const [a, b] = ipv43;
54118
- return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
54119
- }
54120
- return address.startsWith("fc") || address.startsWith("fd") || /^fe[89ab]/.test(address);
54121
- }
54122
- function decideNetworkAccess(serverConfig, rawRemoteAddress) {
54123
- const remoteAddress = rawRemoteAddress ? normalizeIpAddress(rawRemoteAddress) : null;
54124
- if (!remoteAddress || isLoopbackAddress(remoteAddress)) {
54125
- return { allowed: true, remoteAddress };
54126
- }
54127
- const lanEnabled = serverConfig?.lanAccess?.enabled === true;
54128
- if (!lanEnabled) {
54129
- return { allowed: false, remoteAddress, reason: "lan-disabled" };
54130
- }
54131
- if (!isLanAddress(remoteAddress)) {
54132
- return { allowed: false, remoteAddress, reason: "non-lan-address" };
54133
- }
54134
- return { allowed: true, remoteAddress };
54135
- }
54136
- function getRemoteAddressFromContext(c2) {
54137
- const env = c2.env;
54138
- const value = env?.[REMOTE_ADDRESS_ENV_KEY];
54139
- return typeof value === "string" && value.trim() ? value : null;
54140
- }
54141
- function createNetworkAccessMiddleware(store) {
54142
- return async (c2, next) => {
54143
- const decision = decideNetworkAccess(store.get().server, getRemoteAddressFromContext(c2));
54144
- if (!decision.allowed) {
54145
- return c2.json({
54146
- error: decision.reason === "lan-disabled" ? "\u5C40\u57DF\u7F51\u670D\u52A1\u672A\u5F00\u542F\uFF0C\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u8BF7\u6C42" : "\u4EC5\u5141\u8BB8\u672C\u673A\u6216\u5C40\u57DF\u7F51\u6765\u6E90\u8BBF\u95EE",
54147
- remoteAddress: decision.remoteAddress
54148
- }, 403);
54149
- }
54150
- await next();
54151
- };
54152
- }
54153
-
54154
55846
  // src/openapi.ts
54155
55847
  var openAPISpec = {
54156
55848
  openapi: "3.0.0",
@@ -55257,7 +56949,7 @@ var openAPISpec = {
55257
56949
  // src/plugin-loader.ts
55258
56950
  import { mkdtemp, rm, writeFile } from "fs/promises";
55259
56951
  import { tmpdir } from "os";
55260
- import { join as join9, resolve as resolve5 } from "path";
56952
+ import { join as join11, resolve as resolve7 } from "path";
55261
56953
  function isLocalPath(pkg) {
55262
56954
  return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
55263
56955
  }
@@ -55280,7 +56972,7 @@ var remoteTmpDir = null;
55280
56972
  var remoteTmpFiles = [];
55281
56973
  async function ensureRemoteTmpDir() {
55282
56974
  if (!remoteTmpDir) {
55283
- remoteTmpDir = await mkdtemp(join9(tmpdir(), "local-router-plugins-"));
56975
+ remoteTmpDir = await mkdtemp(join11(tmpdir(), "local-router-plugins-"));
55284
56976
  }
55285
56977
  return remoteTmpDir;
55286
56978
  }
@@ -55293,7 +56985,7 @@ async function fetchRemotePlugin(url2) {
55293
56985
  const ext = inferExtension(url2, response.headers.get("content-type"));
55294
56986
  const dir = await ensureRemoteTmpDir();
55295
56987
  const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
55296
- const filePath = join9(dir, fileName);
56988
+ const filePath = join11(dir, fileName);
55297
56989
  await writeFile(filePath, content, "utf-8");
55298
56990
  remoteTmpFiles.push(filePath);
55299
56991
  return filePath;
@@ -55313,7 +57005,7 @@ async function importPlugin(pkg, configDir) {
55313
57005
  const localPath = await fetchRemotePlugin(pkg);
55314
57006
  modulePath = `${localPath}?t=${Date.now()}`;
55315
57007
  } else if (isLocalPath(pkg)) {
55316
- const absolutePath = resolve5(configDir, pkg);
57008
+ const absolutePath = resolve7(configDir, pkg);
55317
57009
  modulePath = `${absolutePath}?t=${Date.now()}`;
55318
57010
  } else {
55319
57011
  modulePath = pkg;
@@ -55706,6 +57398,7 @@ async function proxyRequest(c2, options) {
55706
57398
  });
55707
57399
  }
55708
57400
  const capture = logger?.openStreamCapture(logMeta.requestId, dateStr) ?? null;
57401
+ const tokenUsageCollector = createTokenUsageStreamCollector(`${logMeta.provider} ${logMeta.routeType} ${logMeta.modelIn} ${logMeta.modelOut}`);
55709
57402
  let upstreamBytes = 0;
55710
57403
  let writeEventCalled = false;
55711
57404
  const finalizeAndWriteEvent = () => {
@@ -55717,6 +57410,7 @@ async function proxyRequest(c2, options) {
55717
57410
  truncated: false,
55718
57411
  filePath: null
55719
57412
  };
57413
+ const tokenUsage2 = tokenUsageCollector.getUsage();
55720
57414
  logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
55721
57415
  upstream_status: sseStatus,
55722
57416
  content_type_res: contentTypeRes,
@@ -55728,6 +57422,7 @@ async function proxyRequest(c2, options) {
55728
57422
  stream_file_bytes: captureResult.bytesWritten
55729
57423
  },
55730
57424
  ...captureResult.truncated && { stream_file_truncated: true },
57425
+ ...tokenUsage2 && { token_usage: tokenUsage2 },
55731
57426
  ...requestBody !== undefined && { request_body: requestBody },
55732
57427
  ...pluginLogOverrides
55733
57428
  }));
@@ -55743,6 +57438,7 @@ async function proxyRequest(c2, options) {
55743
57438
  return;
55744
57439
  }
55745
57440
  upstreamBytes += value.byteLength;
57441
+ tokenUsageCollector.addChunk(value);
55746
57442
  capture?.write(value);
55747
57443
  controller.enqueue(value);
55748
57444
  } catch (err) {
@@ -55793,12 +57489,14 @@ async function proxyRequest(c2, options) {
55793
57489
  });
55794
57490
  }
55795
57491
  const responseBytes = Buffer.byteLength(responseText, "utf-8");
57492
+ const tokenUsage = extractTokenUsageFromResponseText(responseText, "response_body", `${logMeta.provider} ${logMeta.routeType} ${logMeta.modelIn} ${logMeta.modelOut}`);
55796
57493
  const eventOverrides = {
55797
57494
  upstream_status: upstreamRes.status,
55798
57495
  content_type_res: contentTypeRes,
55799
57496
  response_headers: finalResponseHeaders,
55800
57497
  response_bytes: responseBytes,
55801
57498
  provider_request_id: providerRequestId,
57499
+ ...tokenUsage && { token_usage: tokenUsage },
55802
57500
  ...pluginLogOverrides
55803
57501
  };
55804
57502
  if (requestBody !== undefined) {
@@ -56081,7 +57779,7 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
56081
57779
  const CRYPTO_SESSION_TTL_MS = 2 * 60 * 1000;
56082
57780
  const CRYPTO_SESSION_MAX = 512;
56083
57781
  const schemaPath = getBundledSchemaPath();
56084
- const schemaJson = JSON.parse(readFileSync4(schemaPath, "utf-8"));
57782
+ const schemaJson = JSON.parse(readFileSync6(schemaPath, "utf-8"));
56085
57783
  const pruneExpiredCryptoSessions = (now2 = Date.now()) => {
56086
57784
  for (const [id, record2] of Array.from(cryptoSessions.entries())) {
56087
57785
  if (now2 - record2.createdAt > CRYPTO_SESSION_TTL_MS) {
@@ -56221,6 +57919,45 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
56221
57919
  return c2.json({ error: `\u8BFB\u53D6\u914D\u7F6E schema \u5931\u8D25: ${err instanceof Error ? err.message : err}` }, 500);
56222
57920
  }
56223
57921
  });
57922
+ api2.get("/autostart", async (c2) => {
57923
+ try {
57924
+ const manager = createAutostartManager();
57925
+ const systemInstalled = await manager.isInstalled();
57926
+ const config2 = store.get();
57927
+ return c2.json({
57928
+ enabled: config2.server?.autostart ?? false,
57929
+ systemInstalled,
57930
+ platform: manager.platform,
57931
+ servicePath: manager.getServicePath()
57932
+ });
57933
+ } catch (err) {
57934
+ return c2.json({ error: err instanceof Error ? err.message : String(err) }, 500);
57935
+ }
57936
+ });
57937
+ api2.put("/autostart", async (c2) => {
57938
+ try {
57939
+ const { enabled } = await c2.req.json();
57940
+ if (typeof enabled !== "boolean") {
57941
+ return c2.json({ error: "enabled \u5FC5\u987B\u662F\u5E03\u5C14\u503C" }, 400);
57942
+ }
57943
+ const manager = createAutostartManager();
57944
+ if (manager.platform === "unsupported") {
57945
+ return c2.json({ error: "\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u81EA\u542F\u52A8" }, 400);
57946
+ }
57947
+ if (enabled) {
57948
+ const { execPath, args } = getAutostartExecArgs();
57949
+ await manager.install({ execPath, args, label: "com.lakphy.local-router" });
57950
+ } else {
57951
+ await manager.uninstall();
57952
+ }
57953
+ const config2 = store.get();
57954
+ const updated = { ...config2, server: { ...config2.server, autostart: enabled } };
57955
+ store.save(updated);
57956
+ return c2.json({ ok: true, enabled });
57957
+ } catch (err) {
57958
+ return c2.json({ error: err instanceof Error ? err.message : String(err) }, 500);
57959
+ }
57960
+ });
56224
57961
  api2.post("/chat/proxy", async (c2) => {
56225
57962
  let body;
56226
57963
  try {
@@ -56686,7 +58423,7 @@ async function createApp(store, options) {
56686
58423
  }
56687
58424
  const stopLogStorageTask = startLogStorageBackgroundTask(config2.log);
56688
58425
  options?.registerCleanup?.(stopLogStorageTask);
56689
- const configDir = dirname2(resolve6(store.getPath()));
58426
+ const configDir = dirname3(resolve8(store.getPath()));
56690
58427
  const pluginManager = new PluginManager(configDir);
56691
58428
  const reloadResult = await pluginManager.reloadAll(config2.providers);
56692
58429
  if (!reloadResult.ok) {
@@ -56733,28 +58470,56 @@ async function createApp(store, options) {
56733
58470
  }
56734
58471
  return app;
56735
58472
  }
56736
- async function createDefaultAppFromProcessArgs() {
56737
- const configPath = parseConfigPath();
58473
+ async function createAppRuntimeFromConfigPath(configPath, listen) {
56738
58474
  const store = new ConfigStore(configPath);
56739
- return createApp(store, {
56740
- listen: {
56741
- host: process.env.HOST ?? "0.0.0.0",
56742
- port: Number.parseInt(process.env.PORT ?? "4099", 10)
58475
+ const cleanups = [];
58476
+ const app = await createApp(store, {
58477
+ listen,
58478
+ registerCleanup: (cleanup) => {
58479
+ cleanups.push(cleanup);
56743
58480
  }
56744
58481
  });
58482
+ const logRealtime = createLogRealtimeRuntime({ store });
58483
+ return {
58484
+ app,
58485
+ logRealtime,
58486
+ dispose: () => {
58487
+ logRealtime.dispose();
58488
+ for (const cleanup of cleanups.reverse()) {
58489
+ try {
58490
+ cleanup();
58491
+ } catch {}
58492
+ }
58493
+ }
58494
+ };
58495
+ }
58496
+ async function createDefaultAppRuntimeFromProcessArgs() {
58497
+ const configPath = parseConfigPath();
58498
+ return createAppRuntimeFromConfigPath(configPath, {
58499
+ host: process.env.HOST ?? "0.0.0.0",
58500
+ port: Number.parseInt(process.env.PORT ?? "4099", 10)
58501
+ });
56745
58502
  }
56746
58503
 
56747
58504
  // src/entry.ts
56748
- var app = await createDefaultAppFromProcessArgs();
58505
+ var runtime = await createDefaultAppRuntimeFromProcessArgs();
56749
58506
  var entry_default = {
56750
58507
  hostname: process.env.HOST ?? "0.0.0.0",
56751
58508
  port: Number.parseInt(process.env.PORT ?? "4099", 10),
56752
58509
  fetch(request, server) {
56753
58510
  const remoteAddress = server.requestIP(request)?.address ?? null;
56754
- return app.fetch(request, {
58511
+ const realtimeUpgrade = runtime.logRealtime.upgrade(request, server, remoteAddress);
58512
+ if (realtimeUpgrade.handled) {
58513
+ if (realtimeUpgrade.upgraded) {
58514
+ return;
58515
+ }
58516
+ return realtimeUpgrade.response ?? new Response("WebSocket Upgrade failed", { status: 400 });
58517
+ }
58518
+ return runtime.app.fetch(request, {
56755
58519
  [REMOTE_ADDRESS_ENV_KEY]: remoteAddress
56756
58520
  });
56757
- }
58521
+ },
58522
+ websocket: runtime.logRealtime.websocket
56758
58523
  };
56759
58524
  export {
56760
58525
  entry_default as default