@linxiraos/pi-stats 1.1.22 → 1.1.24
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/CHANGELOG.md +4 -0
- package/README.md +5 -5
- package/THIRD-PARTY-NOTICES.txt +3 -3
- package/build.ts +2 -2
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +35 -35
- package/dist/types/index.d.ts +7 -2
- package/dist/types/port-conflict.d.ts +2 -2
- package/package.json +5 -5
- package/src/client/api.ts +1 -1
- package/src/client/app/Shell.tsx +13 -3
- package/src/client/routes/FrustrationRoute.tsx +1 -1
- package/src/client/routes/OverviewRoute.tsx +4 -1
- package/src/client/routes/ProvidersRoute.tsx +2 -1
- package/src/client/routes/RequestsRoute.tsx +1 -1
- package/src/client/routes/ToolsRoute.tsx +1 -1
- package/src/client/styles.css +2 -2
- package/src/client/useSystemTheme.ts +1 -1
- package/src/index.ts +18 -20
- package/src/port-conflict.ts +23 -7
- package/src/server.ts +2 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -7,7 +7,12 @@ export { getGainDashboardStats } from "./gain-aggregator.js";
|
|
|
7
7
|
export { formatStatsDashboardUrl, type StartServerOptions, startServer } from "./server.js";
|
|
8
8
|
export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint } from "./shared-types.js";
|
|
9
9
|
export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Print the dashboard summary to the console. Shared by `zeta-c stats --summary`
|
|
12
|
+
* and the standalone `zeta-stats --sync`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function printStatsSummary(): Promise<void>;
|
|
15
|
+
/** Parsed arguments for the standalone `zeta-stats` entry point. */
|
|
11
16
|
export interface StandaloneStatsArgs {
|
|
12
17
|
port: number;
|
|
13
18
|
host: string;
|
|
@@ -15,5 +20,5 @@ export interface StandaloneStatsArgs {
|
|
|
15
20
|
sync: boolean;
|
|
16
21
|
help: boolean;
|
|
17
22
|
}
|
|
18
|
-
/** Parse the standalone `
|
|
23
|
+
/** Parse the standalone `zeta-stats` arguments used by the production entry point. */
|
|
19
24
|
export declare function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
2
|
-
export declare const STATS_DASHBOARD_HEADER = "x-
|
|
2
|
+
export declare const STATS_DASHBOARD_HEADER = "x-zeta-stats-dashboard";
|
|
3
3
|
/** Header recording the server's requested bind host so reuse cannot change its exposure scope. */
|
|
4
|
-
export declare const STATS_DASHBOARD_HOSTNAME_HEADER = "x-
|
|
4
|
+
export declare const STATS_DASHBOARD_HOSTNAME_HEADER = "x-zeta-stats-hostname";
|
|
5
5
|
/** Identity-header value for dashboards enforcing an explicit bind host and same-origin access. */
|
|
6
6
|
export declare const STATS_DASHBOARD_SECURITY_VERSION = "3";
|
|
7
7
|
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-stats",
|
|
4
|
-
"version": "1.1.
|
|
5
|
-
"description": "Local observability dashboard for
|
|
4
|
+
"version": "1.1.24",
|
|
5
|
+
"description": "Local observability dashboard for zeta AI usage statistics",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
8
8
|
"license": "MIT",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@linxiraos/pi-ai": "1.1.
|
|
43
|
-
"@linxiraos/pi-catalog": "1.1.
|
|
44
|
-
"@linxiraos/pi-utils": "1.1.
|
|
42
|
+
"@linxiraos/pi-ai": "1.1.24",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.1.24",
|
|
44
|
+
"@linxiraos/pi-utils": "1.1.24",
|
|
45
45
|
"lucide-react": "^1.24.0",
|
|
46
46
|
"react": "19.2.7",
|
|
47
47
|
"react-dom": "19.2.7"
|
package/src/client/api.ts
CHANGED
|
@@ -57,7 +57,7 @@ async function fetchJson<T>(endpoint: string, options?: RequestInit): Promise<T>
|
|
|
57
57
|
* Spend-incurring actions carry this custom header. It forces a CORS preflight
|
|
58
58
|
* the server never approves, so a cross-site page cannot trigger them.
|
|
59
59
|
*/
|
|
60
|
-
const ACTION_HEADERS = { "X-
|
|
60
|
+
const ACTION_HEADERS = { "X-Zeta-Stats-Action": "1" };
|
|
61
61
|
|
|
62
62
|
export async function getOverviewStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<OverviewStats> {
|
|
63
63
|
return fetchJson<OverviewStats>(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`, {
|
package/src/client/app/Shell.tsx
CHANGED
|
@@ -58,15 +58,25 @@ export function Shell({ section, onSectionChange, range, onRangeChange, children
|
|
|
58
58
|
<div className="topbar-brand">
|
|
59
59
|
<svg className="topbar-mark" viewBox="0 0 64 64" width="22" height="22" aria-hidden="true">
|
|
60
60
|
<defs>
|
|
61
|
-
<linearGradient id="
|
|
61
|
+
<linearGradient id="zeta-mark-grad" x1="0" y1="0" x2="1" y2="1">
|
|
62
62
|
<stop offset="0" stopColor="oklch(0.7 0.24 340)" />
|
|
63
63
|
<stop offset=".5" stopColor="oklch(0.62 0.21 295)" />
|
|
64
64
|
<stop offset="1" stopColor="oklch(0.81 0.14 200)" />
|
|
65
65
|
</linearGradient>
|
|
66
66
|
</defs>
|
|
67
|
-
<
|
|
67
|
+
<text
|
|
68
|
+
x="32"
|
|
69
|
+
y="47"
|
|
70
|
+
textAnchor="middle"
|
|
71
|
+
fontSize="52"
|
|
72
|
+
fontWeight="700"
|
|
73
|
+
fontFamily="Georgia, 'Times New Roman', serif"
|
|
74
|
+
fill="url(#zeta-mark-grad)"
|
|
75
|
+
>
|
|
76
|
+
ζ
|
|
77
|
+
</text>
|
|
68
78
|
</svg>
|
|
69
|
-
<span>
|
|
79
|
+
<span>zeta</span>
|
|
70
80
|
<span className="topbar-slash">/</span>
|
|
71
81
|
<span className="topbar-title">stats</span>
|
|
72
82
|
</div>
|
|
@@ -189,7 +189,7 @@ interface JudgePanelProps {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
const UNAVAILABLE_HINT =
|
|
192
|
-
"Judging needs the
|
|
192
|
+
"Judging needs the zeta host process: open the dashboard with zeta-c stats and configure a judge model. Until then messages are classified by regex signals.";
|
|
193
193
|
|
|
194
194
|
function JudgePanel({ active, range, judgeAvailable, job, onRunStarted, onRunChanged }: JudgePanelProps) {
|
|
195
195
|
const [modalOpen, setModalOpen] = useState(false);
|
|
@@ -94,7 +94,10 @@ export function OverviewRoute({ active, range, onRequestClick }: OverviewRoutePr
|
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<div className="page">
|
|
97
|
-
<PageHeader
|
|
97
|
+
<PageHeader
|
|
98
|
+
title="Overview"
|
|
99
|
+
description={`Everything zeta did across your sessions in ${meta.windowLabel}.`}
|
|
100
|
+
/>
|
|
98
101
|
|
|
99
102
|
<QueryView query={overview} skeleton={<ChartSkeleton height={112} />}>
|
|
100
103
|
{({ overall }) => (
|
|
@@ -66,7 +66,8 @@ const TOKEN_MIX = [
|
|
|
66
66
|
{ key: "output", label: "Output", color: "var(--chart-secondary)" },
|
|
67
67
|
] as const;
|
|
68
68
|
|
|
69
|
-
const SNAPSHOT_HINT =
|
|
69
|
+
const SNAPSHOT_HINT =
|
|
70
|
+
"Usage snapshots accumulate whenever usage limits are fetched (TUI footer, /usage, zeta-c usage).";
|
|
70
71
|
|
|
71
72
|
interface WindowRef {
|
|
72
73
|
provider: string;
|
|
@@ -106,7 +106,7 @@ export function RequestsRoute({ active, range, onRequestClick }: RequestsRoutePr
|
|
|
106
106
|
<div className="page">
|
|
107
107
|
<PageHeader
|
|
108
108
|
title="Requests"
|
|
109
|
-
description={`Every model call
|
|
109
|
+
description={`Every model call zeta made in ${meta.windowLabel}, newest first. Open a row for its full payload.`}
|
|
110
110
|
/>
|
|
111
111
|
|
|
112
112
|
<QueryView query={log} skeleton={<Skeleton height={96} style={{ borderRadius: 12 }} />}>
|
|
@@ -73,7 +73,7 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
|
|
|
73
73
|
<div className="page">
|
|
74
74
|
<PageHeader
|
|
75
75
|
title="Tools"
|
|
76
|
-
description={`Which tools
|
|
76
|
+
description={`Which tools zeta called in ${meta.windowLabel}, how often they failed, and what they cost.`}
|
|
77
77
|
/>
|
|
78
78
|
|
|
79
79
|
<QueryView query={tools} skeleton={<ChartSkeleton height={112} />}>
|
package/src/client/styles.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
|
|
2
|
+
zeta stats — design system
|
|
3
3
|
Pure-black chassis, one inset panel, hairline edges, one accent gradient.
|
|
4
4
|
Page-specific styles live next to their route (`routes/*.css`).
|
|
5
5
|
========================================================================== */
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
--ink-4: #46464c;
|
|
31
31
|
--ink-inverse: #0a0a0b;
|
|
32
32
|
|
|
33
|
-
/* Accent:
|
|
33
|
+
/* Accent: zeta cyan → pink. Solid `--accent` for text/strokes, gradient for hero marks. */
|
|
34
34
|
--accent-a: #5ad8e6;
|
|
35
35
|
--accent-b: #ed4abf;
|
|
36
36
|
--accent: #f068c8;
|
|
@@ -3,7 +3,7 @@ import { useSyncExternalStore } from "react";
|
|
|
3
3
|
export type SystemTheme = "light" | "dark";
|
|
4
4
|
export type ThemePreference = "system" | "light" | "dark";
|
|
5
5
|
|
|
6
|
-
const STORAGE_KEY = "
|
|
6
|
+
const STORAGE_KEY = "zeta-stats-theme";
|
|
7
7
|
const DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
|
|
8
8
|
|
|
9
9
|
function readStoredPreference(): ThemePreference {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { formatDuration, formatNumber, formatPercent } from "@linxiraos/pi-utils";
|
|
4
|
+
import { formatDuration, formatNumber, formatPercent, normalizePremiumRequests } from "@linxiraos/pi-utils";
|
|
5
|
+
import chalk from "@linxiraos/pi-utils/chalk";
|
|
5
6
|
import { getDashboardStats, getTotalMessageCount, syncAllSessions } from "./aggregator";
|
|
6
7
|
import { closeDb } from "./db";
|
|
7
8
|
import { refreshRollups } from "./rollup";
|
|
@@ -45,20 +46,17 @@ function formatCost(n: number, unpricedRequests = 0): string {
|
|
|
45
46
|
return `$${n.toFixed(2)}`;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function normalizePremiumRequests(n: number): number {
|
|
49
|
-
return Math.round((n + Number.EPSILON) * 100) / 100;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
|
-
* Print
|
|
50
|
+
* Print the dashboard summary to the console. Shared by `zeta-c stats --summary`
|
|
51
|
+
* and the standalone `zeta-stats --sync`.
|
|
54
52
|
*/
|
|
55
|
-
async function
|
|
53
|
+
export async function printStatsSummary(): Promise<void> {
|
|
56
54
|
const stats = await getDashboardStats();
|
|
57
55
|
const { overall, byModel, byFolder } = stats;
|
|
58
56
|
|
|
59
|
-
console.log("\n=== AI Usage Statistics ===\n");
|
|
57
|
+
console.log(chalk.bold("\n=== AI Usage Statistics ===\n"));
|
|
60
58
|
|
|
61
|
-
console.log("Overall:");
|
|
59
|
+
console.log(chalk.bold("Overall:"));
|
|
62
60
|
console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`);
|
|
63
61
|
console.log(` Error Rate: ${formatPercent(overall.errorRate)}`);
|
|
64
62
|
console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
|
|
@@ -75,7 +73,7 @@ async function printStats(): Promise<void> {
|
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
if (byModel.length > 0) {
|
|
78
|
-
console.log("\nBy Model (API-equivalent estimates):");
|
|
76
|
+
console.log(chalk.bold("\nBy Model (API-equivalent estimates):"));
|
|
79
77
|
for (const m of byModel.slice(0, 10)) {
|
|
80
78
|
console.log(
|
|
81
79
|
` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost, m.unpricedRequests)}, ${formatPercent(m.cacheRate)} cache rate, ${formatPercent(m.cacheSavings)} cache savings`,
|
|
@@ -84,7 +82,7 @@ async function printStats(): Promise<void> {
|
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
if (byFolder.length > 0) {
|
|
87
|
-
console.log("\nBy Folder (API-equivalent estimates):");
|
|
85
|
+
console.log(chalk.bold("\nBy Folder (API-equivalent estimates):"));
|
|
88
86
|
for (const f of byFolder.slice(0, 10)) {
|
|
89
87
|
console.log(
|
|
90
88
|
` ${f.folder}: ${formatNumber(f.totalRequests)} reqs, ${formatCost(f.totalCost, f.unpricedRequests)}`,
|
|
@@ -95,7 +93,7 @@ async function printStats(): Promise<void> {
|
|
|
95
93
|
console.log("");
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
/** Parsed arguments for the standalone `
|
|
96
|
+
/** Parsed arguments for the standalone `zeta-stats` entry point. */
|
|
99
97
|
export interface StandaloneStatsArgs {
|
|
100
98
|
port: number;
|
|
101
99
|
host: string;
|
|
@@ -104,7 +102,7 @@ export interface StandaloneStatsArgs {
|
|
|
104
102
|
help: boolean;
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
/** Parse the standalone `
|
|
105
|
+
/** Parse the standalone `zeta-stats` arguments used by the production entry point. */
|
|
108
106
|
export function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs {
|
|
109
107
|
const { values } = parseArgs({
|
|
110
108
|
args,
|
|
@@ -134,10 +132,10 @@ async function main(): Promise<void> {
|
|
|
134
132
|
|
|
135
133
|
if (values.help) {
|
|
136
134
|
console.log(`
|
|
137
|
-
|
|
135
|
+
zeta-stats - AI Usage Statistics Dashboard
|
|
138
136
|
|
|
139
137
|
Usage:
|
|
140
|
-
|
|
138
|
+
zeta-stats [options]
|
|
141
139
|
|
|
142
140
|
Options:
|
|
143
141
|
-p, --port <port> Port for the dashboard server (default: 3847)
|
|
@@ -147,10 +145,10 @@ Options:
|
|
|
147
145
|
-h, --help Show this help message
|
|
148
146
|
|
|
149
147
|
Examples:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
zeta-stats # Start dashboard server
|
|
149
|
+
zeta-stats --json # Print stats as JSON
|
|
150
|
+
zeta-stats --host 0.0.0.0 # Explicitly expose on all IPv4 interfaces
|
|
151
|
+
zeta-stats --sync # Sync and show summary
|
|
154
152
|
`);
|
|
155
153
|
return;
|
|
156
154
|
}
|
|
@@ -199,7 +197,7 @@ Examples:
|
|
|
199
197
|
if (values.json) {
|
|
200
198
|
console.log(JSON.stringify(await getDashboardStats(), null, 2));
|
|
201
199
|
} else {
|
|
202
|
-
await
|
|
200
|
+
await printStatsSummary();
|
|
203
201
|
}
|
|
204
202
|
} catch (error) {
|
|
205
203
|
console.error("Error:", error);
|
package/src/port-conflict.ts
CHANGED
|
@@ -7,7 +7,18 @@ import { $ } from "bun";
|
|
|
7
7
|
const STATS_PROBE_TIMEOUT_MS = 500;
|
|
8
8
|
const PROCESS_EXIT_POLL_MS = 50;
|
|
9
9
|
const PROCESS_EXIT_POLLS = 10;
|
|
10
|
-
const STATS_RUNTIME_IMAGES: Record<string, true> = {
|
|
10
|
+
const STATS_RUNTIME_IMAGES: Record<string, true> = {
|
|
11
|
+
bun: true,
|
|
12
|
+
node: true,
|
|
13
|
+
// Zeta runtime images: the compiled CLI and the standalone npm bin.
|
|
14
|
+
"zeta-c": true,
|
|
15
|
+
"zeta-cli": true,
|
|
16
|
+
zetacode: true,
|
|
17
|
+
"zeta-stats": true,
|
|
18
|
+
// Upstream OMP images kept so a leftover upstream dashboard is still reclaimable.
|
|
19
|
+
omp: true,
|
|
20
|
+
"omp-stats": true,
|
|
21
|
+
};
|
|
11
22
|
|
|
12
23
|
interface PortHolder {
|
|
13
24
|
pid: number;
|
|
@@ -16,9 +27,9 @@ interface PortHolder {
|
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
19
|
-
export const STATS_DASHBOARD_HEADER = "x-
|
|
30
|
+
export const STATS_DASHBOARD_HEADER = "x-zeta-stats-dashboard";
|
|
20
31
|
/** Header recording the server's requested bind host so reuse cannot change its exposure scope. */
|
|
21
|
-
export const STATS_DASHBOARD_HOSTNAME_HEADER = "x-
|
|
32
|
+
export const STATS_DASHBOARD_HOSTNAME_HEADER = "x-zeta-stats-hostname";
|
|
22
33
|
|
|
23
34
|
/** Identity-header value for dashboards enforcing an explicit bind host and same-origin access. */
|
|
24
35
|
export const STATS_DASHBOARD_SECURITY_VERSION = "3";
|
|
@@ -242,14 +253,19 @@ async function reclaimStatsPort(port: number, hasDashboardIdentity = false): Pro
|
|
|
242
253
|
.replace(/ \(deleted\)$/, "");
|
|
243
254
|
const normalizedCommand = holder.commandLine.toLowerCase().replaceAll("\\", "/");
|
|
244
255
|
const hasStatsIdentity =
|
|
256
|
+
normalizedImage === "zeta-stats" ||
|
|
245
257
|
normalizedImage === "omp-stats" ||
|
|
246
|
-
/(?:^|[/"'\s])omp-stats(?:\.exe)?(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
258
|
+
/(?:^|[/"'\s])(?:zeta|omp)-stats(?:\.exe)?(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
247
259
|
/\/packages\/stats\/src\/index\.ts(?:["'\s]|$)/.test(normalizedCommand) ||
|
|
248
|
-
(normalizedImage === "
|
|
249
|
-
|
|
260
|
+
((normalizedImage === "zeta-c" ||
|
|
261
|
+
normalizedImage === "zeta-cli" ||
|
|
262
|
+
normalizedImage === "zetacode" ||
|
|
263
|
+
normalizedImage === "omp") &&
|
|
264
|
+
/(?:^|\s)stats(?:\s|$)/.test(normalizedCommand)) ||
|
|
265
|
+
/(?:^|\/)(?:zeta-c|zeta-cli|zetacode|omp)(?:\.exe)?["'\s]+stats(?:["'\s]|$)/.test(normalizedCommand);
|
|
250
266
|
if (!STATS_RUNTIME_IMAGES[normalizedImage] || (!hasStatsIdentity && !hasDashboardIdentity)) {
|
|
251
267
|
throw new Error(
|
|
252
|
-
`Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as
|
|
268
|
+
`Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as a zeta stats dashboard; refusing to stop it.`,
|
|
253
269
|
);
|
|
254
270
|
}
|
|
255
271
|
|
package/src/server.ts
CHANGED
|
@@ -69,7 +69,7 @@ async function getEmbeddedClientFiles(): Promise<Map<string, Blob>> {
|
|
|
69
69
|
|
|
70
70
|
if (!EMBEDDED_CLIENT_ARCHIVE) {
|
|
71
71
|
throw new Error(
|
|
72
|
-
"Embedded stats client bundle missing. Rebuild the
|
|
72
|
+
"Embedded stats client bundle missing. Rebuild the zeta binary or npm bundle with embedded stats assets.",
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -157,7 +157,7 @@ const ensureClientBuild = async () => {
|
|
|
157
157
|
* preflight, which this server never approves, so a hostile page cannot
|
|
158
158
|
* trigger a paid judge run through a cross-site form or `fetch`.
|
|
159
159
|
*/
|
|
160
|
-
const STATS_ACTION_HEADER = "X-
|
|
160
|
+
const STATS_ACTION_HEADER = "X-Zeta-Stats-Action";
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* Handle API requests.
|