@lakphy/local-router 0.5.5 → 0.5.6

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
@@ -51697,978 +51697,1029 @@ class CryptoSession {
51697
51697
  }
51698
51698
 
51699
51699
  // src/log-metrics.ts
51700
- import { createReadStream, existsSync as existsSync3 } from "fs";
51700
+ import { createReadStream, existsSync as existsSync4 } from "fs";
51701
51701
  import { join as join5 } from "path";
51702
51702
  import { createInterface } from "readline";
51703
- var WINDOW_MS = {
51704
- "1h": 60 * 60 * 1000,
51705
- "6h": 6 * 60 * 60 * 1000,
51706
- "24h": 24 * 60 * 60 * 1000
51707
- };
51708
- var BUCKET_MS = {
51709
- "1h": 5 * 60 * 1000,
51710
- "6h": 15 * 60 * 1000,
51711
- "24h": 30 * 60 * 1000
51712
- };
51713
- var TOP_LIMIT = 5;
51714
- var MAX_LINES_SCANNED = 250000;
51715
- var CACHE_TTL_MS = 15000;
51716
- var metricsCache = new Map;
51717
- function isLogMetricsWindow(value) {
51718
- return value === "1h" || value === "6h" || value === "24h";
51719
- }
51720
- function toPercent(numerator, denominator) {
51721
- if (denominator <= 0)
51722
- return 0;
51723
- return Number((numerator / denominator * 100).toFixed(2));
51703
+
51704
+ // src/token-usage.ts
51705
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "fs";
51706
+ import { resolve as resolve5 } from "path";
51707
+ var MAX_STREAM_USAGE_BYTES = 25 * 1024 * 1024;
51708
+ function asRecord(value) {
51709
+ if (!value || typeof value !== "object" || Array.isArray(value))
51710
+ return null;
51711
+ return value;
51724
51712
  }
51725
- function toDayStart(ms) {
51726
- const date5 = new Date(ms);
51727
- return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
51713
+ function numeric(value) {
51714
+ if (typeof value === "number" && Number.isFinite(value))
51715
+ return value;
51716
+ if (typeof value === "string" && value.trim()) {
51717
+ const parsed = Number(value);
51718
+ if (Number.isFinite(parsed))
51719
+ return parsed;
51720
+ }
51721
+ return null;
51728
51722
  }
51729
- function listDateStrings(fromMs, toMs) {
51730
- const result = [];
51731
- for (let day = toDayStart(fromMs);day <= toDayStart(toMs); day += 24 * 60 * 60 * 1000) {
51732
- result.push(new Date(day).toISOString().slice(0, 10));
51723
+ function numberAt(value, path) {
51724
+ let current = value;
51725
+ for (const key2 of path) {
51726
+ const record2 = asRecord(current);
51727
+ if (!record2 || !(key2 in record2))
51728
+ return null;
51729
+ current = record2[key2];
51733
51730
  }
51734
- return result;
51731
+ return numeric(current);
51735
51732
  }
51736
- function getStatusClass(event) {
51737
- if (event.error_type)
51738
- return "network_error";
51739
- const status = event.upstream_status ?? 0;
51740
- if (status >= 200 && status < 300)
51741
- return "2xx";
51742
- if (status >= 400 && status < 500)
51743
- return "4xx";
51744
- if (status >= 500)
51745
- return "5xx";
51746
- return "network_error";
51733
+ function firstNumber(value, paths) {
51734
+ for (const path of paths) {
51735
+ const found = numberAt(value, path);
51736
+ if (found !== null)
51737
+ return found;
51738
+ }
51739
+ return null;
51747
51740
  }
51748
- function isErrorEvent(event) {
51749
- if (event.error_type)
51750
- return true;
51751
- const status = event.upstream_status ?? 0;
51752
- return status < 200 || status >= 400;
51741
+ function maxNullable(...values) {
51742
+ const numbers = values.filter((value) => value !== null && value !== undefined);
51743
+ if (numbers.length === 0)
51744
+ return null;
51745
+ return Math.max(...numbers);
51753
51746
  }
51754
- function percentile(sortedNumbers, ratio) {
51755
- if (sortedNumbers.length === 0)
51756
- return 0;
51757
- const index = Math.min(sortedNumbers.length - 1, Math.ceil(sortedNumbers.length * ratio) - 1);
51758
- return Math.round(sortedNumbers[index]);
51747
+ function sumNullable(...values) {
51748
+ let total = 0;
51749
+ let hasValue = false;
51750
+ for (const value of values) {
51751
+ if (value === null || value === undefined)
51752
+ continue;
51753
+ total += value;
51754
+ hasValue = true;
51755
+ }
51756
+ return hasValue ? total : null;
51759
51757
  }
51760
- function createEmptyMetrics(window2, nowMs, source2, warnings = []) {
51761
- const fromMs = nowMs - WINDOW_MS[window2];
51762
- const bucketMs = BUCKET_MS[window2];
51763
- const bucketCount = Math.max(1, Math.ceil((nowMs - fromMs) / bucketMs));
51764
- const series = Array.from({ length: bucketCount }, (_, i) => ({
51765
- ts: new Date(fromMs + i * bucketMs).toISOString(),
51766
- requests: 0,
51767
- errors: 0,
51768
- avgLatencyMs: 0
51769
- }));
51770
- return {
51771
- window: window2,
51772
- from: new Date(fromMs).toISOString(),
51773
- to: new Date(nowMs).toISOString(),
51774
- generatedAt: new Date(nowMs).toISOString(),
51775
- source: source2,
51776
- summary: {
51777
- totalRequests: 0,
51778
- successRequests: 0,
51779
- errorRequests: 0,
51780
- successRate: 0,
51781
- avgLatencyMs: 0,
51782
- p95LatencyMs: 0,
51783
- totalRequestBytes: 0,
51784
- totalResponseBytes: 0
51785
- },
51786
- series,
51787
- topProviders: [],
51788
- topRouteTypes: [],
51789
- statusClasses: {
51790
- "2xx": 0,
51791
- "4xx": 0,
51792
- "5xx": 0,
51793
- network_error: 0
51794
- },
51795
- warnings
51796
- };
51758
+ function roundPercent(numerator, denominator) {
51759
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0)
51760
+ return null;
51761
+ return Number((numerator / denominator * 100).toFixed(2));
51797
51762
  }
51798
- async function getLogMetrics(options) {
51799
- const window2 = options.window ?? "24h";
51800
- const refresh = options.refresh === true;
51801
- const nowMs = options.nowMs ?? Date.now();
51802
- const logEnabled = options.logConfig?.enabled !== false && !!options.logConfig;
51803
- if (!logEnabled) {
51804
- return createEmptyMetrics(window2, nowMs, {
51805
- logEnabled: false,
51806
- baseDir: null,
51807
- filesScanned: 0,
51808
- linesScanned: 0,
51809
- partial: false
51810
- }, ["\u65E5\u5FD7\u672A\u542F\u7528"]);
51763
+ function inferProviderStyle(usage, providerHint) {
51764
+ const hint = providerHint?.toLowerCase() ?? "";
51765
+ if (hint.includes("anthropic") || hint.includes("claude"))
51766
+ return "anthropic";
51767
+ if (hint.includes("gemini") || hint.includes("google"))
51768
+ return "gemini";
51769
+ if (hint.includes("deepseek"))
51770
+ return "deepseek";
51771
+ if (hint.includes("cohere"))
51772
+ return "cohere";
51773
+ if (hint.includes("mistral"))
51774
+ return "mistral";
51775
+ if (hint.includes("openrouter"))
51776
+ return "openrouter";
51777
+ if (hint.includes("openai") || hint.includes("gpt-"))
51778
+ return "openai";
51779
+ if ("cache_read_input_tokens" in usage || "cache_creation_input_tokens" in usage) {
51780
+ return "anthropic";
51811
51781
  }
51812
- const baseDir = resolveLogBaseDir(options.logConfig);
51813
- const cacheKey = `${baseDir}:${window2}`;
51814
- const cached2 = metricsCache.get(cacheKey);
51815
- if (!refresh && cached2 && cached2.expiresAt > nowMs) {
51816
- return cached2.value;
51782
+ if ("prompt_cache_hit_tokens" in usage || "prompt_cache_miss_tokens" in usage) {
51783
+ return "deepseek";
51817
51784
  }
51818
- const eventsDir = join5(baseDir, "events");
51819
- if (!existsSync3(eventsDir)) {
51820
- const empty = createEmptyMetrics(window2, nowMs, {
51821
- logEnabled: true,
51822
- baseDir,
51823
- filesScanned: 0,
51824
- linesScanned: 0,
51825
- partial: false
51826
- }, ["\u65E5\u5FD7\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u6682\u65E0\u53EF\u5206\u6790\u6570\u636E"]);
51827
- metricsCache.set(cacheKey, { expiresAt: nowMs + CACHE_TTL_MS, value: empty });
51828
- return empty;
51785
+ if ("promptTokenCount" in usage || "usageMetadata" in usage || "cachedContentTokenCount" in usage) {
51786
+ return "gemini";
51829
51787
  }
51830
- const fromMs = nowMs - WINDOW_MS[window2];
51831
- const bucketMs = BUCKET_MS[window2];
51832
- const bucketCount = Math.max(1, Math.ceil((nowMs - fromMs) / bucketMs));
51833
- const buckets = Array.from({ length: bucketCount }, () => ({
51834
- requests: 0,
51835
- errors: 0,
51836
- latencySum: 0,
51837
- latencyCount: 0
51838
- }));
51839
- const providerAgg = new Map;
51840
- const routeTypeAgg = new Map;
51841
- const latencies = [];
51842
- const statusClasses = {
51843
- "2xx": 0,
51844
- "4xx": 0,
51845
- "5xx": 0,
51846
- network_error: 0
51788
+ if ("billed_units" in usage || "tokens" in usage) {
51789
+ return "cohere";
51790
+ }
51791
+ if ("prompt_tokens" in usage || "completion_tokens" in usage) {
51792
+ return "openai";
51793
+ }
51794
+ if ("input_tokens" in usage || "output_tokens" in usage) {
51795
+ return "openai";
51796
+ }
51797
+ return "unknown";
51798
+ }
51799
+ function createEmptyMetrics(input) {
51800
+ return {
51801
+ schemaVersion: 1,
51802
+ source: input.source,
51803
+ providerStyle: input.providerStyle,
51804
+ inputTokens: null,
51805
+ outputTokens: null,
51806
+ totalTokens: null,
51807
+ cachedInputTokens: null,
51808
+ cacheHitInputTokens: null,
51809
+ cacheHitRate: null,
51810
+ cacheHitRateDenominatorTokens: null,
51811
+ cacheHitRateFormula: null,
51812
+ cacheReadInputTokens: null,
51813
+ cacheCreationInputTokens: null,
51814
+ cacheCreationInputTokens5m: null,
51815
+ cacheCreationInputTokens1h: null,
51816
+ cacheWriteInputTokens: null,
51817
+ cacheMissInputTokens: null,
51818
+ reasoningTokens: null,
51819
+ audioInputTokens: null,
51820
+ audioOutputTokens: null,
51821
+ textInputTokens: null,
51822
+ textOutputTokens: null,
51823
+ acceptedPredictionTokens: null,
51824
+ rejectedPredictionTokens: null,
51825
+ toolUsePromptTokens: null,
51826
+ billableInputTokens: null,
51827
+ billableOutputTokens: null,
51828
+ creditUsage: null,
51829
+ cost: null,
51830
+ rawUsage: input.rawUsage,
51831
+ rawUsagePath: input.rawUsagePath,
51832
+ warnings: []
51847
51833
  };
51848
- let filesScanned = 0;
51849
- let linesScanned = 0;
51850
- let parseErrors = 0;
51851
- let partial2 = false;
51852
- let totalRequests = 0;
51853
- let successRequests = 0;
51854
- let errorRequests = 0;
51855
- let totalLatency = 0;
51856
- let totalRequestBytes = 0;
51857
- let totalResponseBytes = 0;
51858
- const warnings = [];
51859
- const dateStrings = listDateStrings(fromMs, nowMs);
51860
- for (const dateStr of dateStrings) {
51861
- if (linesScanned >= MAX_LINES_SCANNED) {
51862
- partial2 = true;
51863
- break;
51834
+ }
51835
+ function hasAnyTokenSignal(metrics) {
51836
+ return [
51837
+ metrics.inputTokens,
51838
+ metrics.outputTokens,
51839
+ metrics.totalTokens,
51840
+ metrics.cachedInputTokens,
51841
+ metrics.cacheHitInputTokens,
51842
+ metrics.cacheReadInputTokens,
51843
+ metrics.cacheCreationInputTokens,
51844
+ metrics.cacheMissInputTokens,
51845
+ metrics.reasoningTokens,
51846
+ metrics.billableInputTokens,
51847
+ metrics.billableOutputTokens,
51848
+ metrics.creditUsage,
51849
+ metrics.cost
51850
+ ].some((value) => value !== null);
51851
+ }
51852
+ function normalizeUsageObject(input) {
51853
+ const { usage, source: source2, rawUsagePath, providerHint } = input;
51854
+ const providerStyle = inferProviderStyle(usage, providerHint);
51855
+ const metrics = createEmptyMetrics({
51856
+ source: source2,
51857
+ providerStyle,
51858
+ rawUsage: usage,
51859
+ rawUsagePath
51860
+ });
51861
+ const usageBody = asRecord(usage.usageMetadata) ?? usage;
51862
+ metrics.inputTokens = firstNumber(usageBody, [
51863
+ ["input_tokens"],
51864
+ ["prompt_tokens"],
51865
+ ["promptTokenCount"],
51866
+ ["tokens", "input_tokens"],
51867
+ ["billed_units", "input_tokens"]
51868
+ ]);
51869
+ metrics.outputTokens = firstNumber(usageBody, [
51870
+ ["output_tokens"],
51871
+ ["completion_tokens"],
51872
+ ["candidatesTokenCount"],
51873
+ ["tokens", "output_tokens"],
51874
+ ["billed_units", "output_tokens"]
51875
+ ]);
51876
+ const explicitTotalTokens = firstNumber(usageBody, [
51877
+ ["total_tokens"],
51878
+ ["totalTokenCount"],
51879
+ ["tokens", "total_tokens"]
51880
+ ]);
51881
+ metrics.totalTokens = explicitTotalTokens;
51882
+ const cachedTokens = firstNumber(usageBody, [
51883
+ ["input_tokens_details", "cached_tokens"],
51884
+ ["prompt_tokens_details", "cached_tokens"],
51885
+ ["cached_tokens"],
51886
+ ["cachedContentTokenCount"]
51887
+ ]);
51888
+ const cacheReadTokens = firstNumber(usageBody, [
51889
+ ["cache_read_input_tokens"],
51890
+ ["cacheReadInputTokens"],
51891
+ ["claude_cache_read_input_tokens"]
51892
+ ]);
51893
+ const promptCacheHitTokens = firstNumber(usageBody, [["prompt_cache_hit_tokens"]]);
51894
+ const promptCacheMissTokens = firstNumber(usageBody, [["prompt_cache_miss_tokens"]]);
51895
+ const cacheCreationTokens = firstNumber(usageBody, [
51896
+ ["cache_creation_input_tokens"],
51897
+ ["cacheCreationInputTokens"],
51898
+ ["cache_creation", "input_tokens"],
51899
+ ["claude_cache_creation_input_tokens"]
51900
+ ]);
51901
+ metrics.cacheCreationInputTokens5m = firstNumber(usageBody, [
51902
+ ["cache_creation", "ephemeral_5m_input_tokens"],
51903
+ ["cache_creation", "ephemeral5mInputTokens"],
51904
+ ["cache_creation_ephemeral_5m_input_tokens"],
51905
+ ["claude_cache_creation_5_m_tokens"]
51906
+ ]);
51907
+ metrics.cacheCreationInputTokens1h = firstNumber(usageBody, [
51908
+ ["cache_creation", "ephemeral_1h_input_tokens"],
51909
+ ["cache_creation", "ephemeral1hInputTokens"],
51910
+ ["cache_creation_ephemeral_1h_input_tokens"],
51911
+ ["claude_cache_creation_1_h_tokens"]
51912
+ ]);
51913
+ metrics.cacheCreationInputTokens = maxNullable(cacheCreationTokens, sumNullable(metrics.cacheCreationInputTokens5m, metrics.cacheCreationInputTokens1h));
51914
+ metrics.cacheReadInputTokens = cacheReadTokens;
51915
+ metrics.cacheHitInputTokens = maxNullable(cachedTokens, cacheReadTokens, promptCacheHitTokens);
51916
+ metrics.cachedInputTokens = metrics.cacheHitInputTokens;
51917
+ metrics.cacheMissInputTokens = promptCacheMissTokens;
51918
+ metrics.cacheWriteInputTokens = firstNumber(usageBody, [
51919
+ ["cache_write_input_tokens"],
51920
+ ["cacheWriteInputTokens"]
51921
+ ]);
51922
+ if (metrics.cacheWriteInputTokens === null && providerStyle === "anthropic") {
51923
+ metrics.cacheWriteInputTokens = metrics.cacheCreationInputTokens;
51924
+ }
51925
+ metrics.reasoningTokens = firstNumber(usageBody, [
51926
+ ["output_tokens_details", "reasoning_tokens"],
51927
+ ["completion_tokens_details", "reasoning_tokens"],
51928
+ ["reasoning_tokens"],
51929
+ ["thoughtsTokenCount"]
51930
+ ]);
51931
+ metrics.audioInputTokens = firstNumber(usageBody, [
51932
+ ["input_tokens_details", "audio_tokens"],
51933
+ ["prompt_tokens_details", "audio_tokens"],
51934
+ ["audio_input_tokens"]
51935
+ ]);
51936
+ metrics.audioOutputTokens = firstNumber(usageBody, [
51937
+ ["output_tokens_details", "audio_tokens"],
51938
+ ["completion_tokens_details", "audio_tokens"],
51939
+ ["audio_output_tokens"]
51940
+ ]);
51941
+ metrics.textInputTokens = firstNumber(usageBody, [
51942
+ ["input_tokens_details", "text_tokens"],
51943
+ ["prompt_tokens_details", "text_tokens"],
51944
+ ["text_input_tokens"]
51945
+ ]);
51946
+ metrics.textOutputTokens = firstNumber(usageBody, [
51947
+ ["output_tokens_details", "text_tokens"],
51948
+ ["completion_tokens_details", "text_tokens"],
51949
+ ["text_output_tokens"]
51950
+ ]);
51951
+ metrics.acceptedPredictionTokens = firstNumber(usageBody, [
51952
+ ["output_tokens_details", "accepted_prediction_tokens"],
51953
+ ["completion_tokens_details", "accepted_prediction_tokens"],
51954
+ ["accepted_prediction_tokens"]
51955
+ ]);
51956
+ metrics.rejectedPredictionTokens = firstNumber(usageBody, [
51957
+ ["output_tokens_details", "rejected_prediction_tokens"],
51958
+ ["completion_tokens_details", "rejected_prediction_tokens"],
51959
+ ["rejected_prediction_tokens"]
51960
+ ]);
51961
+ metrics.toolUsePromptTokens = firstNumber(usageBody, [
51962
+ ["toolUsePromptTokenCount"],
51963
+ ["tool_use_prompt_tokens"]
51964
+ ]);
51965
+ metrics.billableInputTokens = firstNumber(usageBody, [
51966
+ ["billed_units", "input_tokens"],
51967
+ ["billable_input_tokens"]
51968
+ ]);
51969
+ metrics.billableOutputTokens = firstNumber(usageBody, [
51970
+ ["billed_units", "output_tokens"],
51971
+ ["billable_output_tokens"]
51972
+ ]);
51973
+ metrics.creditUsage = firstNumber(usageBody, [["credit_usage"], ["creditUsage"]]);
51974
+ metrics.cost = firstNumber(usageBody, [["cost"], ["total_cost"], ["totalCost"]]);
51975
+ let cacheDenominator = null;
51976
+ let cacheFormula = null;
51977
+ if (providerStyle === "anthropic") {
51978
+ cacheDenominator = sumNullable(metrics.inputTokens, metrics.cacheReadInputTokens, metrics.cacheCreationInputTokens);
51979
+ cacheFormula = "cache_read_input_tokens / (input_tokens + cache_read_input_tokens + cache_creation_input_tokens)";
51980
+ } else if (providerStyle === "deepseek") {
51981
+ cacheDenominator = metrics.inputTokens ?? sumNullable(metrics.cacheHitInputTokens, metrics.cacheMissInputTokens);
51982
+ cacheFormula = "prompt_cache_hit_tokens / prompt_tokens";
51983
+ } else if (providerStyle === "gemini") {
51984
+ cacheDenominator = metrics.inputTokens;
51985
+ cacheFormula = "cachedContentTokenCount / promptTokenCount";
51986
+ } else if (providerStyle === "openai" || providerStyle === "mistral" || providerStyle === "openrouter" || providerStyle === "unknown") {
51987
+ cacheDenominator = metrics.inputTokens;
51988
+ cacheFormula = "cached_tokens / input_tokens";
51989
+ }
51990
+ if (metrics.cacheHitInputTokens !== null && cacheDenominator !== null && cacheDenominator > 0) {
51991
+ metrics.cacheHitRateDenominatorTokens = cacheDenominator;
51992
+ metrics.cacheHitRateFormula = cacheFormula;
51993
+ metrics.cacheHitRate = roundPercent(metrics.cacheHitInputTokens, cacheDenominator);
51994
+ if (metrics.cacheMissInputTokens === null) {
51995
+ metrics.cacheMissInputTokens = Math.max(0, cacheDenominator - metrics.cacheHitInputTokens);
51864
51996
  }
51865
- const filePath = join5(eventsDir, `${dateStr}.jsonl`);
51866
- if (!existsSync3(filePath))
51867
- continue;
51868
- filesScanned += 1;
51869
- try {
51870
- const stream = createReadStream(filePath, { encoding: "utf-8" });
51871
- const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
51872
- for await (const line2 of rl) {
51873
- if (linesScanned >= MAX_LINES_SCANNED) {
51874
- partial2 = true;
51875
- rl.close();
51876
- stream.destroy();
51877
- break;
51878
- }
51879
- linesScanned += 1;
51880
- if (!line2.trim())
51881
- continue;
51882
- let event;
51883
- try {
51884
- event = JSON.parse(line2);
51885
- } catch {
51886
- parseErrors += 1;
51887
- continue;
51888
- }
51889
- if (!event.ts_start)
51890
- continue;
51891
- const ts = Date.parse(event.ts_start);
51892
- if (!Number.isFinite(ts) || ts < fromMs || ts > nowMs)
51893
- continue;
51894
- totalRequests += 1;
51895
- const isError = isErrorEvent(event);
51896
- if (isError) {
51897
- errorRequests += 1;
51898
- } else {
51899
- successRequests += 1;
51900
- }
51901
- const latency = Number.isFinite(event.latency_ms) ? Math.max(0, event.latency_ms ?? 0) : 0;
51902
- totalLatency += latency;
51903
- latencies.push(latency);
51904
- totalRequestBytes += Math.max(0, event.request_bytes ?? 0);
51905
- totalResponseBytes += Math.max(0, event.response_bytes ?? 0) + Math.max(0, event.stream_bytes ?? 0);
51906
- const bucketIndex = Math.min(bucketCount - 1, Math.max(0, Math.floor((ts - fromMs) / bucketMs)));
51907
- const bucket = buckets[bucketIndex];
51908
- bucket.requests += 1;
51909
- bucket.latencySum += latency;
51910
- bucket.latencyCount += 1;
51911
- if (isError)
51912
- bucket.errors += 1;
51913
- const providerKey = event.provider || "unknown";
51914
- const providerRow = providerAgg.get(providerKey) ?? {
51915
- requests: 0,
51916
- errors: 0,
51917
- latencySum: 0
51918
- };
51919
- providerRow.requests += 1;
51920
- providerRow.latencySum += latency;
51921
- if (isError)
51922
- providerRow.errors += 1;
51923
- providerAgg.set(providerKey, providerRow);
51924
- const routeTypeKey = event.route_type || "unknown";
51925
- const routeTypeRow = routeTypeAgg.get(routeTypeKey) ?? {
51926
- requests: 0,
51927
- errors: 0,
51928
- latencySum: 0
51929
- };
51930
- routeTypeRow.requests += 1;
51931
- routeTypeRow.latencySum += latency;
51932
- if (isError)
51933
- routeTypeRow.errors += 1;
51934
- routeTypeAgg.set(routeTypeKey, routeTypeRow);
51935
- statusClasses[getStatusClass(event)] += 1;
51936
- }
51937
- } catch (err) {
51938
- warnings.push(`\u8BFB\u53D6\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25: ${filePath} (${err instanceof Error ? err.message : String(err)})`);
51939
- partial2 = true;
51997
+ }
51998
+ if (metrics.totalTokens === null && metrics.outputTokens !== null) {
51999
+ const effectiveInputTokens = metrics.cacheHitRateDenominatorTokens ?? metrics.inputTokens;
52000
+ if (effectiveInputTokens !== null) {
52001
+ metrics.totalTokens = effectiveInputTokens + metrics.outputTokens;
52002
+ metrics.warnings.push(metrics.cacheHitRateDenominatorTokens !== null ? "totalTokens \u7531 cacheHitRateDenominatorTokens + outputTokens \u63A8\u5BFC" : "totalTokens \u7531 inputTokens + outputTokens \u63A8\u5BFC");
51940
52003
  }
51941
52004
  }
51942
- if (parseErrors > 0) {
51943
- warnings.push(`\u5DF2\u8DF3\u8FC7 ${parseErrors} \u884C\u65E0\u6548 JSON \u65E5\u5FD7`);
52005
+ return hasAnyTokenSignal(metrics) ? metrics : null;
52006
+ }
52007
+ function collectUsageCandidates(value, prefix = "") {
52008
+ const record2 = asRecord(value);
52009
+ if (!record2)
52010
+ return [];
52011
+ const candidates = [];
52012
+ const candidateKeys = [
52013
+ "usage",
52014
+ "usageMetadata",
52015
+ "message.usage",
52016
+ "response.usage",
52017
+ "body.usage",
52018
+ "data.usage",
52019
+ "event.usage"
52020
+ ];
52021
+ for (const key2 of candidateKeys) {
52022
+ const path = key2.split(".");
52023
+ let current = record2;
52024
+ for (const part of path) {
52025
+ const currentRecord = asRecord(current);
52026
+ current = currentRecord?.[part];
52027
+ }
52028
+ const usage = asRecord(current);
52029
+ if (usage) {
52030
+ candidates.push({ usage, path: prefix ? `${prefix}.${key2}` : key2 });
52031
+ }
52032
+ }
52033
+ const direct = normalizeUsageObject({
52034
+ usage: record2,
52035
+ source: "response_body",
52036
+ rawUsagePath: prefix || null
52037
+ });
52038
+ if (direct) {
52039
+ candidates.push({ usage: record2, path: prefix || "$" });
52040
+ }
52041
+ return candidates;
52042
+ }
52043
+ function mergeNumber(current, incoming, strategy = "max") {
52044
+ if (incoming === null)
52045
+ return current;
52046
+ if (current === null)
52047
+ return incoming;
52048
+ return strategy === "latest" ? incoming : Math.max(current, incoming);
52049
+ }
52050
+ function mergeTokenUsageMetrics(current, incoming) {
52051
+ if (!current)
52052
+ return incoming;
52053
+ if (!incoming)
52054
+ return current;
52055
+ const merged = {
52056
+ ...current,
52057
+ source: incoming.source,
52058
+ providerStyle: current.providerStyle === "unknown" ? incoming.providerStyle : current.providerStyle,
52059
+ rawUsage: incoming.rawUsage ?? current.rawUsage,
52060
+ rawUsagePath: incoming.rawUsagePath ?? current.rawUsagePath,
52061
+ warnings: Array.from(new Set([...current.warnings, ...incoming.warnings]))
52062
+ };
52063
+ const numericKeys = [
52064
+ "inputTokens",
52065
+ "outputTokens",
52066
+ "totalTokens",
52067
+ "cachedInputTokens",
52068
+ "cacheHitInputTokens",
52069
+ "cacheHitRateDenominatorTokens",
52070
+ "cacheReadInputTokens",
52071
+ "cacheCreationInputTokens",
52072
+ "cacheCreationInputTokens5m",
52073
+ "cacheCreationInputTokens1h",
52074
+ "cacheWriteInputTokens",
52075
+ "cacheMissInputTokens",
52076
+ "reasoningTokens",
52077
+ "audioInputTokens",
52078
+ "audioOutputTokens",
52079
+ "textInputTokens",
52080
+ "textOutputTokens",
52081
+ "acceptedPredictionTokens",
52082
+ "rejectedPredictionTokens",
52083
+ "toolUsePromptTokens",
52084
+ "billableInputTokens",
52085
+ "billableOutputTokens",
52086
+ "creditUsage",
52087
+ "cost"
52088
+ ];
52089
+ for (const key2 of numericKeys) {
52090
+ const value = mergeNumber(current[key2], incoming[key2], key2 === "cost" || key2 === "creditUsage" ? "latest" : "max");
52091
+ merged[key2] = value;
52092
+ }
52093
+ if (incoming.cacheHitRate !== null && (current.cacheHitRate === null || (incoming.cacheHitRateDenominatorTokens ?? 0) >= (current.cacheHitRateDenominatorTokens ?? 0))) {
52094
+ merged.cacheHitRate = incoming.cacheHitRate;
52095
+ merged.cacheHitRateFormula = incoming.cacheHitRateFormula;
52096
+ }
52097
+ if (merged.cacheHitInputTokens !== null && merged.cacheHitRateDenominatorTokens !== null) {
52098
+ merged.cacheHitRate = roundPercent(merged.cacheHitInputTokens, merged.cacheHitRateDenominatorTokens);
51944
52099
  }
51945
- if (partial2) {
51946
- warnings.push("\u65E5\u5FD7\u626B\u63CF\u5DF2\u90E8\u5206\u622A\u65AD\uFF0C\u7ED3\u679C\u53EF\u80FD\u4E0D\u5B8C\u6574");
52100
+ if (merged.totalTokens === null && merged.outputTokens !== null) {
52101
+ const effectiveInputTokens = merged.cacheHitRateDenominatorTokens ?? merged.inputTokens;
52102
+ if (effectiveInputTokens !== null) {
52103
+ merged.totalTokens = effectiveInputTokens + merged.outputTokens;
52104
+ merged.warnings.push(merged.cacheHitRateDenominatorTokens !== null ? "totalTokens \u7531 cacheHitRateDenominatorTokens + outputTokens \u63A8\u5BFC" : "totalTokens \u7531 inputTokens + outputTokens \u63A8\u5BFC");
52105
+ merged.warnings = Array.from(new Set(merged.warnings));
52106
+ }
51947
52107
  }
51948
- latencies.sort((a, b) => a - b);
51949
- const series = buckets.map((bucket, index) => ({
51950
- ts: new Date(fromMs + index * bucketMs).toISOString(),
51951
- requests: bucket.requests,
51952
- errors: bucket.errors,
51953
- avgLatencyMs: bucket.latencyCount > 0 ? Math.round(bucket.latencySum / bucket.latencyCount) : 0
51954
- }));
51955
- const topProviders = Array.from(providerAgg.entries()).map(([key2, row]) => ({
51956
- key: key2,
51957
- requests: row.requests,
51958
- errorRate: toPercent(row.errors, row.requests),
51959
- avgLatencyMs: row.requests > 0 ? Math.round(row.latencySum / row.requests) : 0
51960
- })).sort((a, b) => b.requests - a.requests).slice(0, TOP_LIMIT);
51961
- const topRouteTypes = Array.from(routeTypeAgg.entries()).map(([key2, row]) => ({
51962
- key: key2,
51963
- requests: row.requests,
51964
- errorRate: toPercent(row.errors, row.requests)
51965
- })).sort((a, b) => b.requests - a.requests).slice(0, TOP_LIMIT);
51966
- const response = {
51967
- window: window2,
51968
- from: new Date(fromMs).toISOString(),
51969
- to: new Date(nowMs).toISOString(),
51970
- generatedAt: new Date(nowMs).toISOString(),
51971
- source: {
51972
- logEnabled: true,
51973
- baseDir,
51974
- filesScanned,
51975
- linesScanned,
51976
- partial: partial2
51977
- },
51978
- summary: {
51979
- totalRequests,
51980
- successRequests,
51981
- errorRequests,
51982
- successRate: toPercent(successRequests, totalRequests),
51983
- avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
51984
- p95LatencyMs: percentile(latencies, 0.95),
51985
- totalRequestBytes,
51986
- totalResponseBytes
51987
- },
51988
- series,
51989
- topProviders,
51990
- topRouteTypes,
51991
- statusClasses,
51992
- warnings
51993
- };
51994
- metricsCache.set(cacheKey, {
51995
- expiresAt: nowMs + CACHE_TTL_MS,
51996
- value: response
51997
- });
51998
- return response;
52108
+ return merged;
51999
52109
  }
52000
-
52001
- // src/log-query.ts
52002
- import { createReadStream as createReadStream3, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
52003
- import { join as join7, resolve as resolve6 } from "path";
52004
- import { createInterface as createInterface2 } from "readline";
52005
-
52006
- // src/log-index.ts
52007
- import { Database } from "bun:sqlite";
52008
- import {
52009
- closeSync,
52010
- createReadStream as createReadStream2,
52011
- existsSync as existsSync5,
52012
- mkdirSync as mkdirSync3,
52013
- openSync,
52014
- readSync,
52015
- statSync as statSync2
52016
- } from "fs";
52017
- import { join as join6 } from "path";
52018
-
52019
- // src/log-session-identity.ts
52020
- var USER_SESSION_DELIMITER = "_account__session_";
52021
- function toRecord(value) {
52022
- if (!value || typeof value !== "object" || Array.isArray(value))
52023
- return null;
52024
- return value;
52110
+ function toTokenUsageSummary(metrics) {
52111
+ const { rawUsage: _rawUsage, ...summary } = metrics;
52112
+ return summary;
52025
52113
  }
52026
- function extractUserIdRawFromRequestBody(requestBody) {
52027
- const requestBodyRecord = toRecord(requestBody);
52028
- const metadata = toRecord(requestBodyRecord?.metadata);
52029
- if (!metadata) {
52030
- return {
52031
- hasMetadata: false,
52032
- userIdRaw: null
52033
- };
52034
- }
52035
- const userId = metadata.user_id;
52036
- if (typeof userId !== "string" || userId.trim() === "") {
52037
- return {
52038
- hasMetadata: true,
52039
- userIdRaw: null
52040
- };
52114
+ function extractTokenUsageFromJson(value, options) {
52115
+ let merged = null;
52116
+ const candidates = collectUsageCandidates(value, options.rawUsagePathPrefix ?? "");
52117
+ for (const candidate of candidates) {
52118
+ const metrics = normalizeUsageObject({
52119
+ usage: candidate.usage,
52120
+ source: options.source,
52121
+ rawUsagePath: candidate.path,
52122
+ providerHint: options.providerHint
52123
+ });
52124
+ merged = mergeTokenUsageMetrics(merged, metrics);
52041
52125
  }
52042
- return {
52043
- hasMetadata: true,
52044
- userIdRaw: userId
52045
- };
52126
+ return merged;
52046
52127
  }
52047
- function parseUserSessionFromJsonFormat(userIdRaw) {
52048
- let parsed;
52128
+ function extractTokenUsageFromResponseText(text2, source2 = "response_body", providerHint) {
52129
+ if (!text2?.trim())
52130
+ return null;
52049
52131
  try {
52050
- parsed = JSON.parse(userIdRaw);
52132
+ return extractTokenUsageFromJson(JSON.parse(text2), { source: source2, providerHint });
52051
52133
  } catch {
52052
52134
  return null;
52053
52135
  }
52054
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
52136
+ }
52137
+ function processSseMessage(dataLines, source2, providerHint) {
52138
+ if (dataLines.length === 0)
52055
52139
  return null;
52056
- const obj = parsed;
52057
- const sessionId = typeof obj.session_id === "string" ? obj.session_id.trim() : "";
52058
- if (!sessionId)
52140
+ const data = dataLines.join(`
52141
+ `).trim();
52142
+ if (!data || data === "[DONE]")
52059
52143
  return null;
52060
- const userKey = (typeof obj.account_uuid === "string" ? obj.account_uuid.trim() : "") || (typeof obj.device_id === "string" ? obj.device_id.trim() : "");
52061
- return { userKey: userKey || sessionId, sessionId };
52062
- }
52063
- function parseUserSessionFromUserIdRaw(userIdRaw) {
52064
- if (userIdRaw.trimStart().startsWith("{")) {
52065
- return parseUserSessionFromJsonFormat(userIdRaw);
52066
- }
52067
- const index = userIdRaw.indexOf(USER_SESSION_DELIMITER);
52068
- if (index <= 0)
52144
+ try {
52145
+ return extractTokenUsageFromJson(JSON.parse(data), {
52146
+ source: source2,
52147
+ providerHint,
52148
+ rawUsagePathPrefix: source2 === "stream_file" ? "stream" : "stream"
52149
+ });
52150
+ } catch {
52069
52151
  return null;
52070
- const userKey = userIdRaw.slice(0, index).trim();
52071
- const sessionId = userIdRaw.slice(index + USER_SESSION_DELIMITER.length).trim();
52072
- if (!userKey || !sessionId)
52152
+ }
52153
+ }
52154
+ function extractTokenUsageFromSseText(text2, source2 = "stream_file", providerHint) {
52155
+ if (!text2?.trim())
52073
52156
  return null;
52074
- return { userKey, sessionId };
52157
+ let merged = null;
52158
+ let dataLines = [];
52159
+ const flush = () => {
52160
+ merged = mergeTokenUsageMetrics(merged, processSseMessage(dataLines, source2, providerHint));
52161
+ dataLines = [];
52162
+ };
52163
+ for (const rawLine of text2.split(/\r?\n/)) {
52164
+ if (rawLine === "") {
52165
+ flush();
52166
+ continue;
52167
+ }
52168
+ if (rawLine.startsWith("data:")) {
52169
+ dataLines.push(rawLine.slice(5).trimStart());
52170
+ }
52171
+ }
52172
+ flush();
52173
+ return merged;
52075
52174
  }
52076
- function resolveLogSessionIdentity(requestBody) {
52077
- const { hasMetadata, userIdRaw } = extractUserIdRawFromRequestBody(requestBody);
52078
- if (!userIdRaw) {
52175
+ function createTokenUsageStreamCollector(providerHint) {
52176
+ const decoder = new TextDecoder;
52177
+ let buffer2 = "";
52178
+ let dataLines = [];
52179
+ let latest = null;
52180
+ const flushMessage = () => {
52181
+ latest = mergeTokenUsageMetrics(latest, processSseMessage(dataLines, "stream_chunk", providerHint));
52182
+ dataLines = [];
52183
+ };
52184
+ const processLine = (rawLine) => {
52185
+ const line2 = rawLine.replace(/\r$/, "");
52186
+ if (line2 === "") {
52187
+ flushMessage();
52188
+ return;
52189
+ }
52190
+ if (line2.startsWith("data:")) {
52191
+ dataLines.push(line2.slice(5).trimStart());
52192
+ }
52193
+ };
52194
+ return {
52195
+ addChunk(chunk) {
52196
+ buffer2 += decoder.decode(chunk, { stream: true });
52197
+ let newlineIndex = buffer2.indexOf(`
52198
+ `);
52199
+ while (newlineIndex >= 0) {
52200
+ processLine(buffer2.slice(0, newlineIndex));
52201
+ buffer2 = buffer2.slice(newlineIndex + 1);
52202
+ newlineIndex = buffer2.indexOf(`
52203
+ `);
52204
+ }
52205
+ },
52206
+ getUsage() {
52207
+ buffer2 += decoder.decode();
52208
+ if (buffer2) {
52209
+ processLine(buffer2);
52210
+ buffer2 = "";
52211
+ }
52212
+ flushMessage();
52213
+ return latest;
52214
+ }
52215
+ };
52216
+ }
52217
+ function safeReadStreamFile(streamFile, baseDir) {
52218
+ if (!streamFile)
52219
+ return { content: null, warning: null };
52220
+ try {
52221
+ const candidates = [streamFile];
52222
+ if (baseDir)
52223
+ candidates.push(resolve5(baseDir, streamFile));
52224
+ for (const candidate of candidates) {
52225
+ const resolved = resolve5(candidate);
52226
+ if (!resolved.endsWith(".sse.raw"))
52227
+ continue;
52228
+ if (!existsSync3(resolved))
52229
+ continue;
52230
+ const stats = statSync(resolved);
52231
+ if (stats.size > MAX_STREAM_USAGE_BYTES) {
52232
+ return {
52233
+ content: null,
52234
+ warning: `stream_file \u8D85\u8FC7 ${MAX_STREAM_USAGE_BYTES} \u5B57\u8282\uFF0C\u5DF2\u8DF3\u8FC7 token usage \u56DE\u586B`
52235
+ };
52236
+ }
52237
+ return { content: readFileSync4(resolved, "utf-8"), warning: null };
52238
+ }
52239
+ } catch (err) {
52079
52240
  return {
52080
- hasMetadata,
52081
- userIdRaw: null,
52082
- userKey: null,
52083
- sessionId: null
52241
+ content: null,
52242
+ warning: `stream_file token usage \u8BFB\u53D6\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
52243
+ };
52244
+ }
52245
+ return { content: null, warning: null };
52246
+ }
52247
+ function extractTokenUsageFromLogEvent(event, options = {}) {
52248
+ if (event.token_usage) {
52249
+ return {
52250
+ rawUsage: null,
52251
+ ...event.token_usage,
52252
+ source: event.token_usage.source ?? "explicit"
52084
52253
  };
52085
52254
  }
52086
- const parsed = parseUserSessionFromUserIdRaw(userIdRaw);
52255
+ const providerHint = [event.provider, event.route_type, event.model_in, event.model_out].filter(Boolean).join(" ");
52256
+ const responseBodyUsage = extractTokenUsageFromResponseText(event.response_body, "response_body", providerHint);
52257
+ if (responseBodyUsage)
52258
+ return responseBodyUsage;
52259
+ const responseAfterPluginsUsage = extractTokenUsageFromResponseText(event.response_body_after_plugins, "response_body_after_plugins", providerHint);
52260
+ if (responseAfterPluginsUsage)
52261
+ return responseAfterPluginsUsage;
52262
+ const responseBeforePluginsUsage = extractTokenUsageFromResponseText(event.response_body_before_plugins, "response_body_before_plugins", providerHint);
52263
+ if (responseBeforePluginsUsage)
52264
+ return responseBeforePluginsUsage;
52265
+ const streamContent = options.streamContent ?? safeReadStreamFile(event.stream_file, options.baseDir).content;
52266
+ return extractTokenUsageFromSseText(streamContent ?? undefined, "stream_file", providerHint);
52267
+ }
52268
+ function extractTokenUsageSummaryFromLogEvent(event, options = {}) {
52269
+ const metrics = extractTokenUsageFromLogEvent(event, options);
52270
+ return metrics ? toTokenUsageSummary(metrics) : null;
52271
+ }
52272
+ function enrichLogEventTokenUsage(event, options = {}) {
52273
+ if (event.token_usage)
52274
+ return event;
52275
+ const tokenUsage = extractTokenUsageFromLogEvent(event, options);
52276
+ if (!tokenUsage)
52277
+ return event;
52087
52278
  return {
52088
- hasMetadata,
52089
- userIdRaw,
52090
- userKey: parsed?.userKey ?? null,
52091
- sessionId: parsed?.sessionId ?? null
52279
+ ...event,
52280
+ token_usage: tokenUsage
52092
52281
  };
52093
52282
  }
52094
52283
 
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;
52284
+ // src/log-metrics.ts
52285
+ var WINDOW_MS = {
52286
+ "1h": 60 * 60 * 1000,
52287
+ "6h": 6 * 60 * 60 * 1000,
52288
+ "24h": 24 * 60 * 60 * 1000
52289
+ };
52290
+ var BUCKET_MS = {
52291
+ "1h": 5 * 60 * 1000,
52292
+ "6h": 15 * 60 * 1000,
52293
+ "24h": 30 * 60 * 1000
52294
+ };
52295
+ var TOP_LIMIT = 5;
52296
+ var MAX_LINES_SCANNED = 250000;
52297
+ var CACHE_TTL_MS = 15000;
52298
+ var metricsCache = new Map;
52299
+ function isLogMetricsWindow(value) {
52300
+ return value === "1h" || value === "6h" || value === "24h";
52103
52301
  }
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;
52302
+ function toPercent(numerator, denominator) {
52303
+ if (denominator <= 0)
52304
+ return 0;
52305
+ return Number((numerator / denominator * 100).toFixed(2));
52113
52306
  }
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);
52307
+ function toDayStart(ms) {
52308
+ const date5 = new Date(ms);
52309
+ return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
52123
52310
  }
52124
- function firstNumber(value, paths) {
52125
- for (const path of paths) {
52126
- const found = numberAt(value, path);
52127
- if (found !== null)
52128
- return found;
52311
+ function listDateStrings(fromMs, toMs) {
52312
+ const result = [];
52313
+ for (let day = toDayStart(fromMs);day <= toDayStart(toMs); day += 24 * 60 * 60 * 1000) {
52314
+ result.push(new Date(day).toISOString().slice(0, 10));
52129
52315
  }
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);
52316
+ return result;
52137
52317
  }
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;
52318
+ function getStatusClass(event) {
52319
+ if (event.error_type)
52320
+ return "network_error";
52321
+ const status = event.upstream_status ?? 0;
52322
+ if (status >= 200 && status < 300)
52323
+ return "2xx";
52324
+ if (status >= 400 && status < 500)
52325
+ return "4xx";
52326
+ if (status >= 500)
52327
+ return "5xx";
52328
+ return "network_error";
52148
52329
  }
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));
52330
+ function isErrorEvent(event) {
52331
+ if (event.error_type)
52332
+ return true;
52333
+ const status = event.upstream_status ?? 0;
52334
+ return status < 200 || status >= 400;
52153
52335
  }
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";
52336
+ function percentile(sortedNumbers, ratio) {
52337
+ if (sortedNumbers.length === 0)
52338
+ return 0;
52339
+ const index = Math.min(sortedNumbers.length - 1, Math.ceil(sortedNumbers.length * ratio) - 1);
52340
+ return Math.round(sortedNumbers[index]);
52189
52341
  }
52190
- function createEmptyMetrics2(input) {
52342
+ function createEmptyMetrics2(window2, nowMs, source2, warnings = []) {
52343
+ const fromMs = nowMs - WINDOW_MS[window2];
52344
+ const bucketMs = BUCKET_MS[window2];
52345
+ const bucketCount = Math.max(1, Math.ceil((nowMs - fromMs) / bucketMs));
52346
+ const series = Array.from({ length: bucketCount }, (_, i) => ({
52347
+ ts: new Date(fromMs + i * bucketMs).toISOString(),
52348
+ requests: 0,
52349
+ errors: 0,
52350
+ avgLatencyMs: 0
52351
+ }));
52191
52352
  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);
52353
+ window: window2,
52354
+ from: new Date(fromMs).toISOString(),
52355
+ to: new Date(nowMs).toISOString(),
52356
+ generatedAt: new Date(nowMs).toISOString(),
52357
+ source: source2,
52358
+ summary: {
52359
+ totalRequests: 0,
52360
+ successRequests: 0,
52361
+ errorRequests: 0,
52362
+ successRate: 0,
52363
+ avgLatencyMs: 0,
52364
+ p95LatencyMs: 0,
52365
+ totalRequestBytes: 0,
52366
+ totalResponseBytes: 0
52367
+ },
52368
+ tokens: {
52369
+ usageCount: 0,
52370
+ inputTokens: 0,
52371
+ outputTokens: 0,
52372
+ totalTokens: 0,
52373
+ cachedInputTokens: 0,
52374
+ cacheHitInputTokens: 0,
52375
+ cacheHitRateDenominatorTokens: 0,
52376
+ cacheHitRate: 0,
52377
+ reasoningTokens: 0,
52378
+ cost: null
52379
+ },
52380
+ series,
52381
+ topProviders: [],
52382
+ topRouteTypes: [],
52383
+ statusClasses: {
52384
+ "2xx": 0,
52385
+ "4xx": 0,
52386
+ "5xx": 0,
52387
+ network_error: 0
52388
+ },
52389
+ warnings
52390
+ };
52242
52391
  }
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;
52392
+ async function getLogMetrics(options) {
52393
+ const window2 = options.window ?? "24h";
52394
+ const refresh = options.refresh === true;
52395
+ const nowMs = options.nowMs ?? Date.now();
52396
+ const logEnabled = options.logConfig?.enabled !== false && !!options.logConfig;
52397
+ if (!logEnabled) {
52398
+ return createEmptyMetrics2(window2, nowMs, {
52399
+ logEnabled: false,
52400
+ baseDir: null,
52401
+ filesScanned: 0,
52402
+ linesScanned: 0,
52403
+ partial: false
52404
+ }, ["\u65E5\u5FD7\u672A\u542F\u7528"]);
52315
52405
  }
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";
52406
+ const baseDir = resolveLogBaseDir(options.logConfig);
52407
+ const cacheKey = `${baseDir}:${window2}`;
52408
+ const cached2 = metricsCache.get(cacheKey);
52409
+ if (!refresh && cached2 && cached2.expiresAt > nowMs) {
52410
+ return cached2.value;
52380
52411
  }
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
- }
52412
+ const eventsDir = join5(baseDir, "events");
52413
+ if (!existsSync4(eventsDir)) {
52414
+ const empty = createEmptyMetrics2(window2, nowMs, {
52415
+ logEnabled: true,
52416
+ baseDir,
52417
+ filesScanned: 0,
52418
+ linesScanned: 0,
52419
+ partial: false
52420
+ }, ["\u65E5\u5FD7\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u6682\u65E0\u53EF\u5206\u6790\u6570\u636E"]);
52421
+ metricsCache.set(cacheKey, { expiresAt: nowMs + CACHE_TTL_MS, value: empty });
52422
+ return empty;
52388
52423
  }
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");
52424
+ const fromMs = nowMs - WINDOW_MS[window2];
52425
+ const bucketMs = BUCKET_MS[window2];
52426
+ const bucketCount = Math.max(1, Math.ceil((nowMs - fromMs) / bucketMs));
52427
+ const buckets = Array.from({ length: bucketCount }, () => ({
52428
+ requests: 0,
52429
+ errors: 0,
52430
+ latencySum: 0,
52431
+ latencyCount: 0
52432
+ }));
52433
+ const providerAgg = new Map;
52434
+ const routeTypeAgg = new Map;
52435
+ const latencies = [];
52436
+ const statusClasses = {
52437
+ "2xx": 0,
52438
+ "4xx": 0,
52439
+ "5xx": 0,
52440
+ network_error: 0
52441
+ };
52442
+ let filesScanned = 0;
52443
+ let linesScanned = 0;
52444
+ let parseErrors = 0;
52445
+ let partial2 = false;
52446
+ let totalRequests = 0;
52447
+ let successRequests = 0;
52448
+ let errorRequests = 0;
52449
+ let totalLatency = 0;
52450
+ let totalRequestBytes = 0;
52451
+ let totalResponseBytes = 0;
52452
+ let tokenUsageCount = 0;
52453
+ let tokenInput = 0;
52454
+ let tokenOutput = 0;
52455
+ let tokenTotal = 0;
52456
+ let tokenCachedInput = 0;
52457
+ let tokenCacheHitInput = 0;
52458
+ let tokenCacheHitDenominator = 0;
52459
+ let tokenReasoning = 0;
52460
+ let tokenCost = 0;
52461
+ let tokenCostSeen = false;
52462
+ const warnings = [];
52463
+ const dateStrings = listDateStrings(fromMs, nowMs);
52464
+ for (const dateStr of dateStrings) {
52465
+ if (linesScanned >= MAX_LINES_SCANNED) {
52466
+ partial2 = true;
52467
+ break;
52468
+ }
52469
+ const filePath = join5(eventsDir, `${dateStr}.jsonl`);
52470
+ if (!existsSync4(filePath))
52471
+ continue;
52472
+ filesScanned += 1;
52473
+ try {
52474
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
52475
+ const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52476
+ for await (const line2 of rl) {
52477
+ if (linesScanned >= MAX_LINES_SCANNED) {
52478
+ partial2 = true;
52479
+ rl.close();
52480
+ stream.destroy();
52481
+ break;
52482
+ }
52483
+ linesScanned += 1;
52484
+ if (!line2.trim())
52485
+ continue;
52486
+ let event;
52487
+ try {
52488
+ event = JSON.parse(line2);
52489
+ } catch {
52490
+ parseErrors += 1;
52491
+ continue;
52492
+ }
52493
+ if (!event.ts_start)
52494
+ continue;
52495
+ const ts = Date.parse(event.ts_start);
52496
+ if (!Number.isFinite(ts) || ts < fromMs || ts > nowMs)
52497
+ continue;
52498
+ totalRequests += 1;
52499
+ const isError = isErrorEvent(event);
52500
+ if (isError) {
52501
+ errorRequests += 1;
52502
+ } else {
52503
+ successRequests += 1;
52504
+ }
52505
+ const latency = Number.isFinite(event.latency_ms) ? Math.max(0, event.latency_ms ?? 0) : 0;
52506
+ totalLatency += latency;
52507
+ latencies.push(latency);
52508
+ totalRequestBytes += Math.max(0, event.request_bytes ?? 0);
52509
+ totalResponseBytes += Math.max(0, event.response_bytes ?? 0) + Math.max(0, event.stream_bytes ?? 0);
52510
+ const bucketIndex = Math.min(bucketCount - 1, Math.max(0, Math.floor((ts - fromMs) / bucketMs)));
52511
+ const bucket = buckets[bucketIndex];
52512
+ bucket.requests += 1;
52513
+ bucket.latencySum += latency;
52514
+ bucket.latencyCount += 1;
52515
+ if (isError)
52516
+ bucket.errors += 1;
52517
+ const providerKey = event.provider || "unknown";
52518
+ const providerRow = providerAgg.get(providerKey) ?? {
52519
+ requests: 0,
52520
+ errors: 0,
52521
+ latencySum: 0
52522
+ };
52523
+ providerRow.requests += 1;
52524
+ providerRow.latencySum += latency;
52525
+ if (isError)
52526
+ providerRow.errors += 1;
52527
+ providerAgg.set(providerKey, providerRow);
52528
+ const routeTypeKey = event.route_type || "unknown";
52529
+ const routeTypeRow = routeTypeAgg.get(routeTypeKey) ?? {
52530
+ requests: 0,
52531
+ errors: 0,
52532
+ latencySum: 0
52533
+ };
52534
+ routeTypeRow.requests += 1;
52535
+ routeTypeRow.latencySum += latency;
52536
+ if (isError)
52537
+ routeTypeRow.errors += 1;
52538
+ routeTypeAgg.set(routeTypeKey, routeTypeRow);
52539
+ statusClasses[getStatusClass(event)] += 1;
52540
+ const usage = extractTokenUsageSummaryFromLogEvent(event, { baseDir });
52541
+ if (usage) {
52542
+ tokenUsageCount += 1;
52543
+ tokenInput += Math.max(0, usage.inputTokens ?? 0);
52544
+ tokenOutput += Math.max(0, usage.outputTokens ?? 0);
52545
+ tokenTotal += Math.max(0, usage.totalTokens ?? 0);
52546
+ tokenCachedInput += Math.max(0, usage.cachedInputTokens ?? 0);
52547
+ tokenCacheHitInput += Math.max(0, usage.cacheHitInputTokens ?? 0);
52548
+ tokenCacheHitDenominator += Math.max(0, usage.cacheHitRateDenominatorTokens ?? 0);
52549
+ tokenReasoning += Math.max(0, usage.reasoningTokens ?? 0);
52550
+ if (typeof usage.cost === "number" && Number.isFinite(usage.cost)) {
52551
+ tokenCost += usage.cost;
52552
+ tokenCostSeen = true;
52553
+ }
52554
+ }
52555
+ }
52556
+ } catch (err) {
52557
+ warnings.push(`\u8BFB\u53D6\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25: ${filePath} (${err instanceof Error ? err.message : String(err)})`);
52558
+ partial2 = true;
52394
52559
  }
52395
52560
  }
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
- }
52561
+ if (parseErrors > 0) {
52562
+ warnings.push(`\u5DF2\u8DF3\u8FC7 ${parseErrors} \u884C\u65E0\u6548 JSON \u65E5\u5FD7`);
52423
52563
  }
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 || "$" });
52564
+ if (partial2) {
52565
+ warnings.push("\u65E5\u5FD7\u626B\u63CF\u5DF2\u90E8\u5206\u622A\u65AD\uFF0C\u7ED3\u679C\u53EF\u80FD\u4E0D\u5B8C\u6574");
52431
52566
  }
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]))
52567
+ latencies.sort((a, b) => a - b);
52568
+ const series = buckets.map((bucket, index) => ({
52569
+ ts: new Date(fromMs + index * bucketMs).toISOString(),
52570
+ requests: bucket.requests,
52571
+ errors: bucket.errors,
52572
+ avgLatencyMs: bucket.latencyCount > 0 ? Math.round(bucket.latencySum / bucket.latencyCount) : 0
52573
+ }));
52574
+ const topProviders = Array.from(providerAgg.entries()).map(([key2, row]) => ({
52575
+ key: key2,
52576
+ requests: row.requests,
52577
+ errorRate: toPercent(row.errors, row.requests),
52578
+ avgLatencyMs: row.requests > 0 ? Math.round(row.latencySum / row.requests) : 0
52579
+ })).sort((a, b) => b.requests - a.requests).slice(0, TOP_LIMIT);
52580
+ const topRouteTypes = Array.from(routeTypeAgg.entries()).map(([key2, row]) => ({
52581
+ key: key2,
52582
+ requests: row.requests,
52583
+ errorRate: toPercent(row.errors, row.requests)
52584
+ })).sort((a, b) => b.requests - a.requests).slice(0, TOP_LIMIT);
52585
+ const response = {
52586
+ window: window2,
52587
+ from: new Date(fromMs).toISOString(),
52588
+ to: new Date(nowMs).toISOString(),
52589
+ generatedAt: new Date(nowMs).toISOString(),
52590
+ source: {
52591
+ logEnabled: true,
52592
+ baseDir,
52593
+ filesScanned,
52594
+ linesScanned,
52595
+ partial: partial2
52596
+ },
52597
+ summary: {
52598
+ totalRequests,
52599
+ successRequests,
52600
+ errorRequests,
52601
+ successRate: toPercent(successRequests, totalRequests),
52602
+ avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
52603
+ p95LatencyMs: percentile(latencies, 0.95),
52604
+ totalRequestBytes,
52605
+ totalResponseBytes
52606
+ },
52607
+ tokens: {
52608
+ usageCount: tokenUsageCount,
52609
+ inputTokens: tokenInput,
52610
+ outputTokens: tokenOutput,
52611
+ totalTokens: tokenTotal,
52612
+ cachedInputTokens: tokenCachedInput,
52613
+ cacheHitInputTokens: tokenCacheHitInput,
52614
+ cacheHitRateDenominatorTokens: tokenCacheHitDenominator,
52615
+ cacheHitRate: toPercent(tokenCacheHitInput, tokenCacheHitDenominator),
52616
+ reasoningTokens: tokenReasoning,
52617
+ cost: tokenCostSeen ? Number(tokenCost.toFixed(6)) : null
52618
+ },
52619
+ series,
52620
+ topProviders,
52621
+ topRouteTypes,
52622
+ statusClasses,
52623
+ warnings
52453
52624
  };
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;
52625
+ metricsCache.set(cacheKey, {
52626
+ expiresAt: nowMs + CACHE_TTL_MS,
52627
+ value: response
52628
+ });
52629
+ return response;
52500
52630
  }
52501
- function toTokenUsageSummary(metrics) {
52502
- const { rawUsage: _rawUsage, ...summary } = metrics;
52503
- return summary;
52631
+
52632
+ // src/log-query.ts
52633
+ import { createReadStream as createReadStream3, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
52634
+ import { join as join7, resolve as resolve6 } from "path";
52635
+ import { createInterface as createInterface2 } from "readline";
52636
+
52637
+ // src/log-index.ts
52638
+ import { Database } from "bun:sqlite";
52639
+ import {
52640
+ closeSync,
52641
+ createReadStream as createReadStream2,
52642
+ existsSync as existsSync5,
52643
+ mkdirSync as mkdirSync3,
52644
+ openSync,
52645
+ readSync,
52646
+ statSync as statSync2
52647
+ } from "fs";
52648
+ import { join as join6 } from "path";
52649
+
52650
+ // src/log-session-identity.ts
52651
+ var USER_SESSION_DELIMITER = "_account__session_";
52652
+ function toRecord(value) {
52653
+ if (!value || typeof value !== "object" || Array.isArray(value))
52654
+ return null;
52655
+ return value;
52504
52656
  }
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);
52657
+ function extractUserIdRawFromRequestBody(requestBody) {
52658
+ const requestBodyRecord = toRecord(requestBody);
52659
+ const metadata = toRecord(requestBodyRecord?.metadata);
52660
+ if (!metadata) {
52661
+ return {
52662
+ hasMetadata: false,
52663
+ userIdRaw: null
52664
+ };
52516
52665
  }
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;
52666
+ const userId = metadata.user_id;
52667
+ if (typeof userId !== "string" || userId.trim() === "") {
52668
+ return {
52669
+ hasMetadata: true,
52670
+ userIdRaw: null
52671
+ };
52526
52672
  }
52673
+ return {
52674
+ hasMetadata: true,
52675
+ userIdRaw: userId
52676
+ };
52527
52677
  }
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;
52678
+ function parseUserSessionFromJsonFormat(userIdRaw) {
52679
+ let parsed;
52535
52680
  try {
52536
- return extractTokenUsageFromJson(JSON.parse(data), {
52537
- source: source2,
52538
- providerHint,
52539
- rawUsagePathPrefix: source2 === "stream_file" ? "stream" : "stream"
52540
- });
52681
+ parsed = JSON.parse(userIdRaw);
52541
52682
  } catch {
52542
52683
  return null;
52543
52684
  }
52544
- }
52545
- function extractTokenUsageFromSseText(text2, source2 = "stream_file", providerHint) {
52546
- if (!text2?.trim())
52685
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
52547
52686
  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
- };
52687
+ const obj = parsed;
52688
+ const sessionId = typeof obj.session_id === "string" ? obj.session_id.trim() : "";
52689
+ if (!sessionId)
52690
+ return null;
52691
+ const userKey = (typeof obj.account_uuid === "string" ? obj.account_uuid.trim() : "") || (typeof obj.device_id === "string" ? obj.device_id.trim() : "");
52692
+ return { userKey: userKey || sessionId, sessionId };
52607
52693
  }
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
- };
52694
+ function parseUserSessionFromUserIdRaw(userIdRaw) {
52695
+ if (userIdRaw.trimStart().startsWith("{")) {
52696
+ return parseUserSessionFromJsonFormat(userIdRaw);
52635
52697
  }
52636
- return { content: null, warning: null };
52698
+ const index = userIdRaw.indexOf(USER_SESSION_DELIMITER);
52699
+ if (index <= 0)
52700
+ return null;
52701
+ const userKey = userIdRaw.slice(0, index).trim();
52702
+ const sessionId = userIdRaw.slice(index + USER_SESSION_DELIMITER.length).trim();
52703
+ if (!userKey || !sessionId)
52704
+ return null;
52705
+ return { userKey, sessionId };
52637
52706
  }
52638
- function extractTokenUsageFromLogEvent(event, options = {}) {
52639
- if (event.token_usage) {
52707
+ function resolveLogSessionIdentity(requestBody) {
52708
+ const { hasMetadata, userIdRaw } = extractUserIdRawFromRequestBody(requestBody);
52709
+ if (!userIdRaw) {
52640
52710
  return {
52641
- rawUsage: null,
52642
- ...event.token_usage,
52643
- source: event.token_usage.source ?? "explicit"
52711
+ hasMetadata,
52712
+ userIdRaw: null,
52713
+ userKey: null,
52714
+ sessionId: null
52644
52715
  };
52645
52716
  }
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;
52717
+ const parsed = parseUserSessionFromUserIdRaw(userIdRaw);
52669
52718
  return {
52670
- ...event,
52671
- token_usage: tokenUsage
52719
+ hasMetadata,
52720
+ userIdRaw,
52721
+ userKey: parsed?.userKey ?? null,
52722
+ sessionId: parsed?.sessionId ?? null
52672
52723
  };
52673
52724
  }
52674
52725
 
@@ -53059,6 +53110,31 @@ function buildWhereClause(query, options = {}) {
53059
53110
  usesFts
53060
53111
  };
53061
53112
  }
53113
+ function buildSessionsWhereClause(query) {
53114
+ const pseudo = {
53115
+ fromMs: query.fromMs,
53116
+ toMs: query.toMs,
53117
+ levels: [],
53118
+ providers: [],
53119
+ routeTypes: [],
53120
+ models: [],
53121
+ modelIns: [],
53122
+ modelOuts: [],
53123
+ users: query.users,
53124
+ sessions: query.sessions,
53125
+ statusClasses: [],
53126
+ hasError: null,
53127
+ q: query.q,
53128
+ sort: "time_desc",
53129
+ limit: 1,
53130
+ cursor: null
53131
+ };
53132
+ const { whereSql, params } = buildWhereClause(pseudo);
53133
+ return { whereSql, params };
53134
+ }
53135
+ function sortIndexedCountItems(map2) {
53136
+ return Array.from(map2.entries()).map(([key2, count]) => ({ key: key2, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
53137
+ }
53062
53138
 
53063
53139
  class LogIndex {
53064
53140
  baseDir;
@@ -53266,6 +53342,163 @@ class LogIndex {
53266
53342
  }
53267
53343
  };
53268
53344
  }
53345
+ querySessions(query) {
53346
+ const startedAt = performance.now();
53347
+ const { whereSql, params } = buildSessionsWhereClause(query);
53348
+ const aggregatedWhere = `${whereSql} AND e.user_key IS NOT NULL AND e.session_id IS NOT NULL`;
53349
+ const summaryRow = this.db.query(`
53350
+ SELECT
53351
+ COUNT(*) AS totalRequests,
53352
+ COALESCE(SUM(has_metadata), 0) AS metadataRequests,
53353
+ COUNT(DISTINCT user_key) AS uniqueUsers,
53354
+ COUNT(DISTINCT CASE
53355
+ WHEN user_key IS NOT NULL AND session_id IS NOT NULL
53356
+ THEN user_key || ' ' || session_id
53357
+ END) AS uniqueSessions
53358
+ FROM log_events e
53359
+ ${whereSql}
53360
+ `).get(...params);
53361
+ const userRows = this.db.query(`
53362
+ SELECT
53363
+ user_key AS userKey,
53364
+ COUNT(*) AS requestCount,
53365
+ MIN(ts_ms) AS firstMs,
53366
+ MAX(ts_ms) AS lastMs,
53367
+ COUNT(DISTINCT session_id) AS sessionCount
53368
+ FROM log_events e
53369
+ ${aggregatedWhere}
53370
+ GROUP BY user_key
53371
+ `).all(...params);
53372
+ const sessionRows = this.db.query(`
53373
+ SELECT
53374
+ user_key AS userKey,
53375
+ session_id AS sessionId,
53376
+ COUNT(*) AS requestCount,
53377
+ MIN(ts_ms) AS firstMs,
53378
+ MAX(ts_ms) AS lastMs
53379
+ FROM log_events e
53380
+ ${aggregatedWhere}
53381
+ GROUP BY user_key, session_id
53382
+ `).all(...params);
53383
+ const userModelRows = this.db.query(`
53384
+ SELECT user_key AS userKey, model AS key, COUNT(*) AS count
53385
+ FROM log_events e
53386
+ ${aggregatedWhere}
53387
+ GROUP BY user_key, model
53388
+ `).all(...params);
53389
+ const userProviderRows = this.db.query(`
53390
+ SELECT user_key AS userKey, provider AS key, COUNT(*) AS count
53391
+ FROM log_events e
53392
+ ${aggregatedWhere}
53393
+ GROUP BY user_key, provider
53394
+ `).all(...params);
53395
+ const userRouteRows = this.db.query(`
53396
+ SELECT user_key AS userKey, route_type AS key, COUNT(*) AS count
53397
+ FROM log_events e
53398
+ ${aggregatedWhere}
53399
+ GROUP BY user_key, route_type
53400
+ `).all(...params);
53401
+ const sessionModelRows = this.db.query(`
53402
+ SELECT user_key AS userKey, session_id AS sessionId, model AS key, COUNT(*) AS count
53403
+ FROM log_events e
53404
+ ${aggregatedWhere}
53405
+ GROUP BY user_key, session_id, model
53406
+ `).all(...params);
53407
+ const latestRows = this.db.query(`
53408
+ SELECT userKey, sessionId, request_id AS latestRequestId
53409
+ FROM (
53410
+ SELECT
53411
+ user_key AS userKey,
53412
+ session_id AS sessionId,
53413
+ request_id,
53414
+ ROW_NUMBER() OVER (
53415
+ PARTITION BY user_key, session_id ORDER BY ts_ms DESC, id DESC
53416
+ ) AS rn
53417
+ FROM log_events e
53418
+ ${aggregatedWhere}
53419
+ )
53420
+ WHERE rn = 1
53421
+ `).all(...params);
53422
+ const userModels = new Map;
53423
+ const userProviders = new Map;
53424
+ const userRoutes = new Map;
53425
+ const sessionModels = new Map;
53426
+ const latestBySession = new Map;
53427
+ const addCount = (target, groupKey, key2, count) => {
53428
+ if (!key2)
53429
+ return;
53430
+ let inner = target.get(groupKey);
53431
+ if (!inner) {
53432
+ inner = new Map;
53433
+ target.set(groupKey, inner);
53434
+ }
53435
+ inner.set(key2, count);
53436
+ };
53437
+ for (const row of userModelRows)
53438
+ addCount(userModels, row.userKey, row.key, row.count);
53439
+ for (const row of userProviderRows)
53440
+ addCount(userProviders, row.userKey, row.key, row.count);
53441
+ for (const row of userRouteRows)
53442
+ addCount(userRoutes, row.userKey, row.key, row.count);
53443
+ for (const row of sessionModelRows) {
53444
+ addCount(sessionModels, `${row.userKey}\x00${row.sessionId}`, row.key, row.count);
53445
+ }
53446
+ for (const row of latestRows) {
53447
+ latestBySession.set(`${row.userKey}\x00${row.sessionId}`, row.latestRequestId);
53448
+ }
53449
+ const sessionsByUser = new Map;
53450
+ for (const row of sessionRows) {
53451
+ const sessionKey = `${row.userKey}\x00${row.sessionId}`;
53452
+ const session = {
53453
+ sessionId: row.sessionId,
53454
+ requestCount: row.requestCount,
53455
+ firstSeenAt: new Date(row.firstMs).toISOString(),
53456
+ lastSeenAt: new Date(row.lastMs).toISOString(),
53457
+ models: sortIndexedCountItems(sessionModels.get(sessionKey) ?? new Map),
53458
+ latestRequestId: latestBySession.get(sessionKey) ?? ""
53459
+ };
53460
+ const list = sessionsByUser.get(row.userKey);
53461
+ if (list) {
53462
+ list.push(session);
53463
+ } else {
53464
+ sessionsByUser.set(row.userKey, [session]);
53465
+ }
53466
+ }
53467
+ const users = userRows.map((row) => {
53468
+ const sessions = (sessionsByUser.get(row.userKey) ?? []).sort((a, b) => {
53469
+ if (a.requestCount !== b.requestCount)
53470
+ return b.requestCount - a.requestCount;
53471
+ return Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt);
53472
+ });
53473
+ return {
53474
+ userKey: row.userKey,
53475
+ requestCount: row.requestCount,
53476
+ sessionCount: row.sessionCount,
53477
+ firstSeenAt: new Date(row.firstMs).toISOString(),
53478
+ lastSeenAt: new Date(row.lastMs).toISOString(),
53479
+ models: sortIndexedCountItems(userModels.get(row.userKey) ?? new Map),
53480
+ providers: sortIndexedCountItems(userProviders.get(row.userKey) ?? new Map),
53481
+ routeTypes: sortIndexedCountItems(userRoutes.get(row.userKey) ?? new Map),
53482
+ sessions
53483
+ };
53484
+ }).sort((a, b) => {
53485
+ if (a.requestCount !== b.requestCount)
53486
+ return b.requestCount - a.requestCount;
53487
+ return Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt);
53488
+ });
53489
+ return {
53490
+ from: new Date(query.fromMs).toISOString(),
53491
+ to: new Date(query.toMs).toISOString(),
53492
+ summary: {
53493
+ totalRequests: Number(summaryRow.totalRequests) || 0,
53494
+ metadataRequests: Number(summaryRow.metadataRequests) || 0,
53495
+ uniqueUsers: Number(summaryRow.uniqueUsers) || 0,
53496
+ uniqueSessions: Number(summaryRow.uniqueSessions) || 0
53497
+ },
53498
+ users,
53499
+ queryMs: Math.round((performance.now() - startedAt) * 100) / 100
53500
+ };
53501
+ }
53269
53502
  configure() {
53270
53503
  this.db.exec(`
53271
53504
  PRAGMA journal_mode = WAL;
@@ -53648,6 +53881,65 @@ async function queryIndexedLogEvents(logConfig, query) {
53648
53881
  };
53649
53882
  }
53650
53883
  }
53884
+ async function queryIndexedLogSessions(logConfig, query) {
53885
+ if (!logConfig || logConfig.enabled === false) {
53886
+ return {
53887
+ from: new Date(query.fromMs).toISOString(),
53888
+ to: new Date(query.toMs).toISOString(),
53889
+ summary: { totalRequests: 0, metadataRequests: 0, uniqueUsers: 0, uniqueSessions: 0 },
53890
+ users: [],
53891
+ meta: {
53892
+ scannedFiles: 0,
53893
+ scannedLines: 0,
53894
+ parseErrors: 0,
53895
+ truncated: false,
53896
+ indexUsed: true,
53897
+ indexFresh: true,
53898
+ queryMs: 0
53899
+ }
53900
+ };
53901
+ }
53902
+ const baseDir = resolveLogBaseDir(logConfig);
53903
+ const index = getLogIndex(baseDir);
53904
+ if (!index)
53905
+ return null;
53906
+ try {
53907
+ const freshness = await index.ensureRangeIndexed(query.fromMs, query.toMs);
53908
+ const result = index.querySessions(query);
53909
+ return {
53910
+ from: result.from,
53911
+ to: result.to,
53912
+ summary: result.summary,
53913
+ users: result.users,
53914
+ meta: {
53915
+ scannedFiles: freshness.scannedFiles,
53916
+ scannedLines: freshness.scannedLines,
53917
+ parseErrors: freshness.parseErrors,
53918
+ truncated: false,
53919
+ indexUsed: true,
53920
+ indexFresh: true,
53921
+ queryMs: result.queryMs
53922
+ }
53923
+ };
53924
+ } catch (err) {
53925
+ return {
53926
+ from: new Date(query.fromMs).toISOString(),
53927
+ to: new Date(query.toMs).toISOString(),
53928
+ summary: { totalRequests: 0, metadataRequests: 0, uniqueUsers: 0, uniqueSessions: 0 },
53929
+ users: [],
53930
+ meta: {
53931
+ scannedFiles: 0,
53932
+ scannedLines: 0,
53933
+ parseErrors: 0,
53934
+ truncated: false,
53935
+ indexUsed: false,
53936
+ indexFresh: false,
53937
+ queryMs: 0,
53938
+ fallbackReason: err instanceof Error ? err.message : String(err)
53939
+ }
53940
+ };
53941
+ }
53942
+ }
53651
53943
  function getIndexedLogEventDetail(logConfig, id) {
53652
53944
  if (!logConfig || logConfig.enabled === false)
53653
53945
  return null;
@@ -53680,7 +53972,10 @@ function getIndexedLogEventDetail(logConfig, id) {
53680
53972
  var WINDOW_MS2 = {
53681
53973
  "1h": 60 * 60 * 1000,
53682
53974
  "6h": 6 * 60 * 60 * 1000,
53683
- "24h": 24 * 60 * 60 * 1000
53975
+ "24h": 24 * 60 * 60 * 1000,
53976
+ "7d": 7 * 24 * 60 * 60 * 1000,
53977
+ "1mo": 30 * 24 * 60 * 60 * 1000,
53978
+ "1y": 365 * 24 * 60 * 60 * 1000
53684
53979
  };
53685
53980
  var MAX_LINES_SCANNED2 = 250000;
53686
53981
  var MAX_QUERY_LIMIT = 200;
@@ -54328,7 +54623,7 @@ async function scanEvents(baseDir, query) {
54328
54623
  };
54329
54624
  }
54330
54625
  function isLogQueryWindow(value) {
54331
- return value === "1h" || value === "6h" || value === "24h";
54626
+ return value === "1h" || value === "6h" || value === "24h" || value === "7d" || value === "1mo" || value === "1y";
54332
54627
  }
54333
54628
  function getLogQueryWindowMs(window2) {
54334
54629
  return WINDOW_MS2[window2];
@@ -54832,7 +55127,7 @@ function compileRealtimeQuery(input) {
54832
55127
  const nowMs = Date.now();
54833
55128
  const windowRaw = parseStringValue(query.window) ?? "24h";
54834
55129
  if (!isLogQueryWindow(windowRaw)) {
54835
- throw new Error("window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h");
55130
+ throw new Error("window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h | 7d | 1mo | 1y");
54836
55131
  }
54837
55132
  const from = parseStringValue(query.from);
54838
55133
  const to = parseStringValue(query.to);
@@ -55414,6 +55709,23 @@ async function queryLogSessions(context2, input) {
55414
55709
  if (!logEnabled) {
55415
55710
  return createEmptyResult(normalized.fromMs, normalized.toMs);
55416
55711
  }
55712
+ const indexed = await queryIndexedLogSessions(context2.logConfig, {
55713
+ fromMs: normalized.fromMs,
55714
+ toMs: normalized.toMs,
55715
+ users: normalized.users,
55716
+ sessions: normalized.sessions,
55717
+ q: normalized.q
55718
+ });
55719
+ if (indexed?.meta.indexUsed) {
55720
+ return {
55721
+ from: indexed.from,
55722
+ to: indexed.to,
55723
+ summary: indexed.summary,
55724
+ users: indexed.users,
55725
+ meta: indexed.meta
55726
+ };
55727
+ }
55728
+ const fallbackReason = indexed?.meta.fallbackReason;
55417
55729
  const baseDir = resolveLogBaseDir(context2.logConfig);
55418
55730
  const eventsDir = join8(baseDir, "events");
55419
55731
  if (!existsSync7(eventsDir)) {
@@ -55537,7 +55849,9 @@ async function queryLogSessions(context2, input) {
55537
55849
  scannedFiles,
55538
55850
  scannedLines,
55539
55851
  parseErrors,
55540
- truncated
55852
+ truncated,
55853
+ indexUsed: false,
55854
+ ...fallbackReason ? { fallbackReason } : {}
55541
55855
  }
55542
55856
  };
55543
55857
  }
@@ -56061,7 +56375,7 @@ var openAPISpec = {
56061
56375
  required: false,
56062
56376
  schema: {
56063
56377
  type: "string",
56064
- enum: ["1h", "6h", "24h"],
56378
+ enum: ["1h", "6h", "24h", "7d", "1mo", "1y"],
56065
56379
  default: "24h"
56066
56380
  },
56067
56381
  description: "\u65F6\u95F4\u7A97\u53E3\uFF08\u5F53\u672A\u63D0\u4F9B from/to \u65F6\u751F\u6548\uFF09"
@@ -56391,7 +56705,7 @@ var openAPISpec = {
56391
56705
  required: false,
56392
56706
  schema: {
56393
56707
  type: "string",
56394
- enum: ["1h", "6h", "24h"],
56708
+ enum: ["1h", "6h", "24h", "7d", "1mo", "1y"],
56395
56709
  default: "24h"
56396
56710
  },
56397
56711
  description: "\u65F6\u95F4\u7A97\u53E3"
@@ -56512,7 +56826,7 @@ var openAPISpec = {
56512
56826
  required: false,
56513
56827
  schema: {
56514
56828
  type: "string",
56515
- enum: ["1h", "6h", "24h"],
56829
+ enum: ["1h", "6h", "24h", "7d", "1mo", "1y"],
56516
56830
  default: "1h"
56517
56831
  }
56518
56832
  },
@@ -58034,7 +58348,7 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
58034
58348
  try {
58035
58349
  const windowRaw = c2.req.query("window") ?? "24h";
58036
58350
  if (!isLogQueryWindow(windowRaw)) {
58037
- return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h" }, 400);
58351
+ return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h | 7d | 1mo | 1y" }, 400);
58038
58352
  }
58039
58353
  const range = resolveLogQueryRange({
58040
58354
  window: windowRaw,
@@ -58085,7 +58399,7 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
58085
58399
  try {
58086
58400
  const windowRaw = c2.req.query("window") ?? "24h";
58087
58401
  if (!isLogQueryWindow(windowRaw)) {
58088
- return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h" }, 400);
58402
+ return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h | 7d | 1mo | 1y" }, 400);
58089
58403
  }
58090
58404
  const range = resolveLogQueryRange({
58091
58405
  window: windowRaw,
@@ -58124,7 +58438,7 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
58124
58438
  try {
58125
58439
  const windowRaw = c2.req.query("window") ?? "24h";
58126
58440
  if (!isLogQueryWindow(windowRaw)) {
58127
- return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h" }, 400);
58441
+ return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h | 7d | 1mo | 1y" }, 400);
58128
58442
  }
58129
58443
  const range = resolveLogQueryRange({
58130
58444
  window: windowRaw,
@@ -58174,7 +58488,7 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
58174
58488
  const target = c2.req.raw;
58175
58489
  const windowRaw = c2.req.query("window") ?? "1h";
58176
58490
  if (!isLogQueryWindow(windowRaw)) {
58177
- return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h" }, 400);
58491
+ return c2.json({ error: "window \u53C2\u6570\u4EC5\u652F\u6301 1h | 6h | 24h | 7d | 1mo | 1y" }, 400);
58178
58492
  }
58179
58493
  const sortRaw = c2.req.query("sort") ?? "time_desc";
58180
58494
  if (!validateSort(sortRaw)) {