@cyuanxv/openrouter-analytics-mcp 0.6.0-alpha.2 → 0.6.0-alpha.3
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/README.md +4 -4
- package/dist/index.js +116 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
完整的个人费用分析、报告、成本实验和模型预设工具,通过 MCP stdio 在本机运行,使用 SQLite 保存记录。包含与 Sites 版相同的 28 个工具。由 cyuanxv 维护:[X / Twitter @cyuanxv](https://x.com/cyuanxv)。第三方项目,非 OpenRouter 官方产品。
|
|
4
4
|
|
|
5
|
-
0.6.0-alpha.
|
|
5
|
+
0.6.0-alpha.3 alpha 测试版。本地版不附带网页、后台任务或自动 Telegram 发送;保留报告生成、通知认领和真实回执记录能力,不执行付费推理、充值、Key 修改或生产路由切换。
|
|
6
6
|
|
|
7
7
|
## 安装(中文)
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
在新的空目录中,从 npm 安装固定版本(也提供 GitHub Release tarball):
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install --ignore-scripts @cyuanxv/openrouter-analytics-mcp@0.6.0-alpha.
|
|
14
|
+
npm install --ignore-scripts @cyuanxv/openrouter-analytics-mcp@0.6.0-alpha.3
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
在支持本地 MCP stdio 的客户端中新增如下配置。Node 和安装文件都使用自己电脑的绝对路径。通过客户端安全环境设置填写 Key,不把真实密钥粘贴进聊天、Git 或终端命令。
|
|
@@ -33,7 +33,7 @@ npm install --ignore-scripts @cyuanxv/openrouter-analytics-mcp@0.6.0-alpha.2
|
|
|
33
33
|
|
|
34
34
|
数据目录必须是绝对路径、专用于本工具;不要指定根目录、家目录本身或源码目录。启动时会设目录权限为 0700、数据库文件为 0600(Windows 需使用者确保 ACL 私有)。数据库文件名 `workbench.sqlite`。符号链接目录、符号链接或硬链接数据库会被拒绝。缺少目录配置、权限不足或迁移失败时启动失败,不显示配置值。
|
|
35
35
|
|
|
36
|
-
先调用 `routerlens_status`:mode=local-stdio,adapter_version=0.6.0-alpha.
|
|
36
|
+
先调用 `routerlens_status`:mode=local-stdio,adapter_version=0.6.0-alpha.3,version 保留共享核心版本 0.5.0。authentication_verified=false 表示没有 Sites 云端身份认证;connectivity_verified=false 表示未测试上游连接。实际账户工具成功才证明自己的 Key 可用。Key 缺失时仍能管理本地记录和访问公开模型目录,账单查询会报错。保留 routerlens_status 等工具 ID 以兼容现有调用。
|
|
37
37
|
|
|
38
38
|
## 安装与配置自检
|
|
39
39
|
|
|
@@ -83,7 +83,7 @@ npm pack --ignore-scripts
|
|
|
83
83
|
|
|
84
84
|
A local stdio MCP server with all 28 tools from the personal Sites edition: OpenRouter cost analytics, call metadata, anomaly investigation, saved reports, experiments and versioned routing presets. Requires Node.js 22.22+ (22.x) or 24+, your own OpenRouter account and, for account queries, your own Management Key.
|
|
85
85
|
|
|
86
|
-
Install the pinned alpha into an empty directory using `npm install --ignore-scripts @cyuanxv/openrouter-analytics-mcp@0.6.0-alpha.
|
|
86
|
+
Install the pinned alpha into an empty directory using `npm install --ignore-scripts @cyuanxv/openrouter-analytics-mcp@0.6.0-alpha.3`. Configure a local MCP client to run the installed dist/stdio.js with an absolute Node.js path. Set OPENROUTER_WORKBENCH_DATA_DIR to a dedicated absolute private directory; provide OPENROUTER_MANAGEMENT_KEY through client secret settings. You can also install a downloaded release tarball with npm.
|
|
87
87
|
|
|
88
88
|
SQLite records survive process restarts and package removal. Back up the data directory while all instances are stopped. There is no dashboard, HTTP listener, Sites identity, built-in scheduler or automatic Telegram delivery. This is not a ChatGPT cloud connector. No paid inference, key writes or production routing changes are performed. Returned data is also subject to your MCP client's policies.
|
|
89
89
|
|
package/dist/index.js
CHANGED
|
@@ -1806,6 +1806,110 @@ async function explainCostAnomaly(input, run, now = /* @__PURE__ */ new Date())
|
|
|
1806
1806
|
return { date: day, timezone: zone2, baseline_days: days, target_range: target, baseline_range: baseline, status: apps.available ? "complete" : "partial", account: { current: publicTotals(current), baseline_daily: Object.fromEntries(METRICS.map((k) => [k, Number(prior[k]) / days])), increase_usd: increase, spend_ratio: ratio(current.total_usage, prior.total_usage / days) }, drivers: drivers.slice(0, 10).map(({ model_id, ...d }) => d), apps, upstream_query_count: queryCount, summary_lines: lines, summary_text: lines.join("\n"), evidence_scope: "complete_returned_hourly_records_reconciled_to_account_controls", decomposition_policy: "Volume first at baseline cost per request; remainder is unit cost change. Not causal attribution.", privacy: "No prompt, completion content or credential is read." };
|
|
1807
1807
|
}
|
|
1808
1808
|
|
|
1809
|
+
// ../../services/routerlens/lib/reportSummary.ts
|
|
1810
|
+
var money2 = (n) => (n < 0 ? "-" : "") + (Math.abs(n) > 0 && Math.abs(n) < 1e-4 ? "\u4E0D\u8DB3 $0.0001" : "$" + Math.abs(n).toFixed(4));
|
|
1811
|
+
var label2 = (value) => value.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 96);
|
|
1812
|
+
var complete = (e) => e.available && e.data?.totals_complete === true && Array.isArray(e.data.rows) && e.data.rows.length > 0;
|
|
1813
|
+
function modelChanges(input) {
|
|
1814
|
+
const unavailable = { available: false, items: [], reason: "\u4E24\u671F\u6A21\u578B\u660E\u7EC6\u4E0D\u5B8C\u6574\u6216\u672A\u4E0E\u8D26\u6237\u603B\u989D\u6838\u5BF9\u4E00\u81F4\uFF0C\u6682\u4E0D\u80FD\u5224\u65AD\u4E3B\u8981\u53D8\u5316\u6765\u81EA\u54EA\u4E2A\u6A21\u578B\u3002" };
|
|
1815
|
+
if (input.spend.current === null || input.spend.previous === null || ![input.current, input.prior, input.models, input.prior_models].every(complete)) return unavailable;
|
|
1816
|
+
try {
|
|
1817
|
+
const aggregate = (e) => {
|
|
1818
|
+
const values = /* @__PURE__ */ new Map();
|
|
1819
|
+
for (const row of e.data.rows) {
|
|
1820
|
+
if (typeof row.model !== "string" || !row.model || decimalCost(row.total_usage).coefficient < 0n) throw new Error("INVALID_MODEL");
|
|
1821
|
+
values.set(row.model, [...values.get(row.model) ?? [], row.total_usage]);
|
|
1822
|
+
}
|
|
1823
|
+
return new Map([...values].map(([key, costs]) => [key, sumDecimalCosts(costs)]));
|
|
1824
|
+
};
|
|
1825
|
+
for (const [models, account] of [[input.models, input.current], [input.prior_models, input.prior]]) {
|
|
1826
|
+
const costs = (e) => sumDecimalCosts(e.data.rows.map((r) => {
|
|
1827
|
+
const n = decimalCost(r.total_usage);
|
|
1828
|
+
if (n.coefficient < 0n) throw new Error("INVALID_COST");
|
|
1829
|
+
return r.total_usage;
|
|
1830
|
+
}));
|
|
1831
|
+
if (compareDecimalCost(costs(models), costs(account)) !== 0) return unavailable;
|
|
1832
|
+
}
|
|
1833
|
+
const current = aggregate(input.models), previous = aggregate(input.prior_models), zero = decimalCost(0);
|
|
1834
|
+
const items = [.../* @__PURE__ */ new Set([...current.keys(), ...previous.keys()])].map((model2) => {
|
|
1835
|
+
const a = current.get(model2) ?? zero, b = previous.get(model2) ?? zero;
|
|
1836
|
+
return { model: label2(model2), current_usd: decimalCostNumber(a), previous_usd: decimalCostNumber(b), change_usd: decimalCostNumber(subtractDecimalCost(a, b)) };
|
|
1837
|
+
}).filter((row) => row.change_usd !== 0).sort((a, b) => Math.abs(b.change_usd) - Math.abs(a.change_usd)).slice(0, 3);
|
|
1838
|
+
return { available: true, items, reason: "\u4E24\u671F\u6A21\u578B\u660E\u7EC6\u5DF2\u4E0E\u5404\u671F\u8D26\u6237\u8D39\u7528\u6838\u5BF9\uFF1B\u6309\u53D8\u5316\u91D1\u989D\u7EDD\u5BF9\u503C\u5217\u51FA\u6700\u591A 3 \u4E2A\u6A21\u578B\u3002\u4E0D\u662F\u5168\u90E8\u6A21\u578B\u6E05\u5355\uFF0C\u4E5F\u4E0D\u8BC1\u660E\u7528\u6237\u884C\u4E3A\u6216\u4EF7\u683C\u53D8\u5316\u7684\u539F\u56E0\u3002" };
|
|
1839
|
+
} catch {
|
|
1840
|
+
return unavailable;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function summarizeCostReport(input) {
|
|
1844
|
+
const { spend, anomalies, assessment } = input;
|
|
1845
|
+
const headline = spend.current === null ? "\u672C\u671F\u82B1\u4E86\u591A\u5C11\uFF1A\u672A\u77E5\uFF0C\u8D39\u7528\u6570\u636E\u4E0D\u8DB3\uFF0C\u6682\u4E0D\u80FD\u786E\u8BA4\u603B\u989D\u3002" : `\u672C\u671F\u82B1\u4E86\u591A\u5C11\uFF1A${money2(spend.current)}\u3002`;
|
|
1846
|
+
let comparison;
|
|
1847
|
+
if (spend.current === null || spend.previous === null || spend.increase === null) comparison = "\u6BD4\u4E0A\u671F\u600E\u6837\uFF1A\u6682\u4E0D\u80FD\u6BD4\u8F83\uFF0C\u4E24\u671F\u8D39\u7528\u5C1A\u672A\u90FD\u786E\u8BA4\u3002";
|
|
1848
|
+
else if (spend.increase === 0) comparison = `\u6BD4\u4E0A\u671F\u600E\u6837\uFF1A\u4E0E\u4E0A\u671F\u6301\u5E73\uFF08\u4E0A\u671F ${money2(spend.previous)}\uFF09\u3002`;
|
|
1849
|
+
else {
|
|
1850
|
+
const percent = spend.previous === 0 ? "\u4E0A\u671F\u4E3A\u96F6\uFF0C\u4E0D\u8BA1\u7B97\u6DA8\u8DCC\u5E45" : spend.percent === null || !Number.isFinite(spend.percent) ? "\u6DA8\u8DCC\u5E45\u6682\u4E0D\u53EF\u8BA1\u7B97" : `${Math.abs(spend.percent).toFixed(1)}%`;
|
|
1851
|
+
comparison = `\u6BD4\u4E0A\u671F\u600E\u6837\uFF1A${spend.increase > 0 ? "\u591A\u82B1" : "\u5C11\u82B1"} ${money2(Math.abs(spend.increase))}\uFF08${percent}\uFF1B\u4E0A\u671F ${money2(spend.previous)}\uFF09\u3002`;
|
|
1852
|
+
}
|
|
1853
|
+
const changes = modelChanges(input);
|
|
1854
|
+
const known = [], unknown = [], next = [];
|
|
1855
|
+
(spend.current === null ? unknown : known).push(spend.current === null ? "\u672C\u671F\u603B\u8D39\u7528\u5C1A\u672A\u786E\u8BA4\u3002" : `\u672C\u671F\u8D39\u7528\u53EF\u6838\u5BF9\uFF1A${money2(spend.current)}\u3002`);
|
|
1856
|
+
(spend.previous === null ? unknown : known).push(spend.previous === null ? "\u4E0A\u671F\u603B\u8D39\u7528\u5C1A\u672A\u786E\u8BA4\uFF0C\u4E0D\u80FD\u8BA1\u7B97\u53D8\u5316\u3002" : `\u4E0A\u671F\u8D39\u7528\u53EF\u6838\u5BF9\uFF1A${money2(spend.previous)}\u3002`);
|
|
1857
|
+
if (changes.available) known.push("\u6A21\u578B\u53D8\u5316\u5DF2\u4E0E\u4E24\u671F\u8D26\u6237\u8D39\u7528\u6838\u5BF9\u4E00\u81F4\u3002");
|
|
1858
|
+
else unknown.push(changes.reason);
|
|
1859
|
+
const evaluated = anomalies.available && Number.isInteger(anomalies.data?.evaluated_days) ? anomalies.data.evaluated_days : 0;
|
|
1860
|
+
const skipped = anomalies.available && Number.isInteger(anomalies.data?.skipped_days) ? anomalies.data.skipped_days : null;
|
|
1861
|
+
let anomaly;
|
|
1862
|
+
if (assessment === "candidate") anomaly = "\u5F02\u5E38\u68C0\u67E5\uFF1A\u53D1\u73B0\u8D39\u7528\u4E0A\u6DA8\u7EBF\u7D22\uFF0C\u9700\u8981\u8FDB\u4E00\u6B65\u6392\u67E5\uFF1B\u8FD9\u4E0D\u662F\u5DF2\u786E\u8BA4\u7684\u6545\u969C\u6216\u6D6A\u8D39\u3002";
|
|
1863
|
+
else if (assessment === "no_candidates" && evaluated > 0) anomaly = `\u5F02\u5E38\u68C0\u67E5\uFF1A\u5DF2\u8BC4\u4F30\u7684 ${evaluated} \u5929\u672A\u53D1\u73B0\u8D39\u7528\u4E0A\u6DA8\u5019\u9009\u3002`;
|
|
1864
|
+
else anomaly = "\u5F02\u5E38\u68C0\u67E5\uFF1A\u6570\u636E\u4E0D\u8DB3\uFF0C\u6682\u4E0D\u80FD\u5224\u65AD\u662F\u5426\u5B58\u5728\u5F02\u5E38\u3002";
|
|
1865
|
+
if (!anomalies.available || evaluated === 0) unknown.push("\u5F02\u5E38\u5224\u65AD\u5C1A\u65E0\u8DB3\u591F\u8BC1\u636E\uFF1B\u4E0D\u4EE3\u8868\u6CA1\u6709\u5F02\u5E38\u3002");
|
|
1866
|
+
if (skipped !== null && skipped > 0) unknown.push(`\u6709 ${skipped} \u5929\u672A\u80FD\u8BC4\u4F30\uFF0C\u4E0D\u80FD\u5C06\u5DF2\u8BC4\u4F30\u65E5\u671F\u7684\u7ED3\u679C\u63A8\u5E7F\u5230\u6574\u4E2A\u62A5\u544A\u671F\u3002`);
|
|
1867
|
+
for (const item of input.attributions) {
|
|
1868
|
+
if (!item.available) unknown.push(`${item.date} \u5F52\u56E0\u6570\u636E\u4E0D\u8DB3\uFF0C\u5F02\u5E38\u5019\u9009\u4ECD\u9700\u6392\u67E5\u3002`);
|
|
1869
|
+
else if (item.data?.status === "partial") unknown.push(`${item.date} \u5E94\u7528\u5C42\u5F52\u56E0\u4E0D\u8DB3\uFF0C\u4E0D\u80FD\u786E\u5B9A\u5177\u4F53\u5E94\u7528\u6765\u6E90\u3002`);
|
|
1870
|
+
}
|
|
1871
|
+
if (input.attribution_omitted_days > 0) unknown.push(`\u53E6\u6709 ${input.attribution_omitted_days} \u4E2A\u5F02\u5E38\u65E5\u671F\u5C1A\u672A\u5C55\u5F00\u5F52\u56E0\uFF0C\u53EF\u6309\u65E5\u671F\u7EE7\u7EED\u67E5\u8BE2\u3002`);
|
|
1872
|
+
if (unknown.length) next.push("\u5148\u6838\u5BF9\u6570\u636E\u6743\u9650\u3001\u62A5\u544A\u65E5\u671F\u4E0E\u5B8C\u6574\u6027\u8BF4\u660E\uFF1B\u4E0D\u8981\u628A\u7F3A\u5931\u6570\u636E\u5F53\u6210\u96F6\u3002");
|
|
1873
|
+
if (assessment === "candidate") next.push("\u6309\u5F02\u5E38\u65E5\u671F\u67E5\u770B\u8BF7\u6C42\u91CF\u53CA\u8F93\u5165\u3001\u8F93\u51FA\u53D8\u5316\uFF1B\u8D26\u5355\u8D21\u732E\u53EA\u63D0\u4F9B\u6392\u67E5\u7EBF\u7D22\uFF0C\u4E0D\u8BC1\u660E\u539F\u56E0\u3002");
|
|
1874
|
+
else if (changes.available && changes.items.length) next.push("\u4F18\u5148\u67E5\u770B\u53D8\u5316\u91D1\u989D\u6700\u5927\u7684\u6A21\u578B\uFF0C\u6838\u5BF9\u5BF9\u5E94\u8BF7\u6C42\u8BB0\u5F55\u3002");
|
|
1875
|
+
else if (!unknown.length) next.push("\u4FDD\u7559\u672C\u6B21\u62A5\u544A\uFF0C\u540E\u7EED\u5728\u76F8\u540C\u53E3\u5F84\u4E0B\u6BD4\u8F83\uFF1B\u65E0\u9700\u4EC5\u51ED\u672C\u62A5\u544A\u5207\u6362\u6A21\u578B\u3002");
|
|
1876
|
+
const lines = [
|
|
1877
|
+
`OpenRouter \u5DE5\u4F5C\u53F0 ${input.kind === "weekly" ? "\u8D39\u7528\u5468\u62A5" : "\u5F02\u5E38\u68C0\u67E5"}`,
|
|
1878
|
+
`${input.range.start_date} \u81F3 ${input.range.end_date}\uFF08\u7ED3\u675F\u65E5\u4E0D\u542B\uFF0C${input.range.timezone}\uFF1B\u91D1\u989D\u4E3A\u7F8E\u5143\uFF09`,
|
|
1879
|
+
headline,
|
|
1880
|
+
comparison,
|
|
1881
|
+
`\u5BF9\u6BD4\u671F\uFF1A${input.comparison_range.start_date} \u81F3 ${input.comparison_range.end_date}\uFF08\u7ED3\u675F\u65E5\u4E0D\u542B\uFF09\u3002`,
|
|
1882
|
+
"\u4E3B\u8981\u53D8\u5316\u6765\u81EA\u54EA\u91CC\uFF1A",
|
|
1883
|
+
...changes.available ? changes.items.length ? changes.items.map((row) => `${row.model}\uFF1A${row.change_usd > 0 ? "\u591A\u82B1" : "\u5C11\u82B1"} ${money2(Math.abs(row.change_usd))}\uFF08\u672C\u671F ${money2(row.current_usd)}\uFF0C\u4E0A\u671F ${money2(row.previous_usd)}\uFF09\u3002`) : ["\u4E24\u671F\u6A21\u578B\u8D39\u7528\u6CA1\u6709\u53D8\u5316\u3002"] : [changes.reason],
|
|
1884
|
+
...changes.available ? ["\u4EC5\u5217\u53D8\u5316\u91D1\u989D\u6700\u5927\u7684\u6700\u591A 3 \u9879\uFF1B\u5404\u6A21\u578B\u53EF\u80FD\u76F8\u4E92\u62B5\u6D88\uFF0C\u4E0D\u628A\u8D39\u7528\u6392\u540D\u5F53\u4F5C\u53D8\u5316\u539F\u56E0\u3002"] : [],
|
|
1885
|
+
anomaly,
|
|
1886
|
+
"\u73B0\u5728\u80FD\u786E\u8BA4\uFF1A",
|
|
1887
|
+
...known,
|
|
1888
|
+
"\u8FD8\u4E0D\u80FD\u786E\u8BA4\uFF1A",
|
|
1889
|
+
...unknown.length ? unknown : ["\u672C\u6B21\u67E5\u8BE2\u672A\u53D1\u73B0\u4E0A\u8FF0\u6570\u636E\u7F3A\u53E3\uFF1B\u8D26\u5355\u53D8\u5316\u4ECD\u4E0D\u8BC1\u660E\u5177\u4F53\u884C\u4E3A\u6216\u4EF7\u683C\u53D8\u5316\u539F\u56E0\u3002"],
|
|
1890
|
+
"\u5EFA\u8BAE\u4E0B\u4E00\u6B65\uFF1A",
|
|
1891
|
+
...next,
|
|
1892
|
+
"\u7F3A\u5931\u6570\u636E\u4E0D\u89C6\u4E3A\u96F6\uFF1B\u91D1\u989D\u4E3A\u67E5\u8BE2\u65F6\u89C2\u5BDF\u503C\uFF0C\u975E\u6700\u7EC8\u7ED3\u7B97\u3002\u751F\u6210\u62A5\u544A\u4E0D\u4EE3\u8868\u901A\u77E5\u5DF2\u53D1\u9001\u3002"
|
|
1893
|
+
];
|
|
1894
|
+
return { version: 1, headline, comparison, model_changes: changes, known, unknown, next_steps: next, text: lines.join("\n") };
|
|
1895
|
+
}
|
|
1896
|
+
function reportMessage(summary, attributions) {
|
|
1897
|
+
const tail = "\n\u7F3A\u5931\u6570\u636E\u4E0D\u89C6\u4E3A\u96F6\uFF1B\u91D1\u989D\u4E3A\u67E5\u8BE2\u65F6\u89C2\u5BDF\u503C\uFF0C\u975E\u6700\u7EC8\u7ED3\u7B97\u3002\u751F\u6210\u62A5\u544A\u4E0D\u4EE3\u8868\u901A\u77E5\u5DF2\u53D1\u9001\u3002";
|
|
1898
|
+
const budget = 3500 - tail.length;
|
|
1899
|
+
let text = summary.text.slice(0, -tail.length);
|
|
1900
|
+
if (text.length > budget) return text.slice(0, budget - 18) + "\n\u90E8\u5206\u8BF4\u660E\u7701\u7565\uFF0C\u8BF7\u8BFB\u53D6\u5B8C\u6574\u62A5\u544A\u3002" + tail;
|
|
1901
|
+
for (const item of attributions) {
|
|
1902
|
+
if (!item.available || typeof item.data?.summary_text !== "string") continue;
|
|
1903
|
+
const detail = "\n" + item.data.summary_text;
|
|
1904
|
+
if (text.length + detail.length > budget) {
|
|
1905
|
+
text += "\n\u8BE6\u7EC6\u5F52\u56E0\u8BF7\u8BFB\u53D6\u5B8C\u6574\u62A5\u544A\u3002".slice(0, budget - text.length);
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
text += detail;
|
|
1909
|
+
}
|
|
1910
|
+
return text + tail;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1809
1913
|
// ../../services/routerlens/lib/operations.ts
|
|
1810
1914
|
var zone = z3.enum(["UTC", "Asia/Shanghai"]).default("Asia/Shanghai");
|
|
1811
1915
|
var date = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine((v) => {
|
|
@@ -1841,9 +1945,6 @@ function total(x, metric) {
|
|
|
1841
1945
|
function change(current, previous) {
|
|
1842
1946
|
return { current, previous, increase: current === null || previous === null ? null : current - previous, percent: current === null || previous === null || previous <= 0 ? null : (current - previous) / previous * 100 };
|
|
1843
1947
|
}
|
|
1844
|
-
function money2(v) {
|
|
1845
|
-
return typeof v === "number" ? "$" + v.toFixed(4) : "\u672A\u77E5";
|
|
1846
|
-
}
|
|
1847
1948
|
function normalizeGeneration(response, id2) {
|
|
1848
1949
|
const row = response?.data ?? response;
|
|
1849
1950
|
if (!row || row.id !== id2) throw new PublicError("INVALID_RESPONSE");
|
|
@@ -1911,7 +2012,7 @@ function createOperations(client, db, now = () => /* @__PURE__ */ new Date()) {
|
|
|
1911
2012
|
const existing = await store.report(rid);
|
|
1912
2013
|
if (existing) return existing;
|
|
1913
2014
|
const previous = completedRange(i.kind === "weekly" ? 7 : 1, i.timezone, range.start_date, now());
|
|
1914
|
-
const [current, prior, models, providers, anomalies] = await Promise.all([optional(() => query(range)), optional(() => query(previous)), optional(() => query(range, ["model"],
|
|
2015
|
+
const [current, prior, models, priorModels, providers, anomalies] = await Promise.all([optional(() => query(range)), optional(() => query(previous)), optional(() => query(range, ["model"])), optional(() => query(previous, ["model"])), optional(() => invoke("query_usage", { time_range: { start: range.start, end: range.end }, granularity: "hour", dimensions: ["provider"], metrics: ["total_usage", "request_count"], limit: 100, order_by: { field: "total_usage", direction: "desc" } })), optional(() => invoke("analyze_cost_anomalies", { timezone: i.timezone, days: i.kind === "weekly" ? 7 : 1, baseline_days: 7 }))]);
|
|
1915
2016
|
const anomalyEvidence = range.end_date === dateStringInZone(now(), i.timezone) ? anomalies : { available: false, reason: "Historical anomaly window not queried." };
|
|
1916
2017
|
const spend = change(current.available ? total(current.data, "total_usage") : null, prior.available ? total(prior.data, "total_usage") : null);
|
|
1917
2018
|
const assessment = !anomalyEvidence.available || !anomalyEvidence.data.evaluated_days ? "unknown" : anomalyEvidence.data.anomalies.length ? "candidate" : "no_candidates";
|
|
@@ -1922,16 +2023,9 @@ function createOperations(client, db, now = () => /* @__PURE__ */ new Date()) {
|
|
|
1922
2023
|
}
|
|
1923
2024
|
const priorAlert = i.kind === "daily_alert" ? await store.report(`daily_alert:${i.timezone.replace("/", "_")}:${range.start_date}`) : null;
|
|
1924
2025
|
const notify = i.kind === "weekly" || assessment === "candidate" || (assessment === "unknown" || spend.current === null) && priorAlert?.payload?.assessment !== "unknown";
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
for (const row of models.data.rows.slice(0, 3)) lines.push(`${row.model}: ${money2(row.total_usage)}`);
|
|
1929
|
-
}
|
|
1930
|
-
if (anomalyEvidence.available) lines.push(`\u5DF2\u8BC4\u4F30 ${anomalyEvidence.data.evaluated_days} \u5929\uFF0C\u8DF3\u8FC7 ${anomalyEvidence.data.skipped_days} \u5929\u3002`);
|
|
1931
|
-
for (const item of attributions) lines.push(item.available ? item.data.summary_text : `${item.date} \u5F52\u56E0\u6570\u636E\u4E0D\u8DB3\uFF0C\u5F02\u5E38\u5019\u9009\u4ECD\u9700\u6392\u67E5\u3002`);
|
|
1932
|
-
if (candidates.length > 3) lines.push(`\u53E6\u6709 ${candidates.length - 3} \u4E2A\u5F02\u5E38\u65E5\u671F\uFF0C\u53EF\u6309\u65E5\u671F\u67E5\u8BE2\u5F52\u56E0\u3002`);
|
|
1933
|
-
lines.push("\u7F3A\u5931\u6570\u636E\u4E0D\u89C6\u4E3A\u96F6\uFF1B\u91D1\u989D\u4E3A\u67E5\u8BE2\u65F6\u89C2\u5BDF\u503C\uFF0C\u975E\u6700\u7EC8\u7ED3\u7B97\u3002");
|
|
1934
|
-
return store.saveReport(rid, i.kind, { range, comparison_range: previous, spend, current, prior, models, providers, anomalies: anomalyEvidence, attributions, attribution_omitted_days: Math.max(0, candidates.length - 3), assessment, should_notify: notify, telegram_text: lines.join("\n").slice(0, 3500), generated_at: now().toISOString() });
|
|
2026
|
+
const payload = { range, comparison_range: previous, spend, current, prior, models, prior_models: priorModels, providers, anomalies: anomalyEvidence, attributions, attribution_omitted_days: Math.max(0, candidates.length - 3), assessment, should_notify: notify, generated_at: now().toISOString() };
|
|
2027
|
+
const summary = summarizeCostReport({ kind: i.kind, ...payload });
|
|
2028
|
+
return store.saveReport(rid, i.kind, { ...payload, summary, telegram_text: reportMessage(summary, attributions) });
|
|
1935
2029
|
}, true);
|
|
1936
2030
|
add("list_cost_reports", "List saved reports and delivery states. No query or notification is executed.", { limit: z3.number().int().min(1).max(50).default(20) }, async (i) => ({ reports: await store.reports(i.limit) }));
|
|
1937
2031
|
add("get_cost_report", "Read a saved report including its completeness and delivery evidence.", { report_id: id }, async (i) => {
|
|
@@ -1983,7 +2077,12 @@ function createOperations(client, db, now = () => /* @__PURE__ */ new Date()) {
|
|
|
1983
2077
|
if (!t) return { isError: true, content: [{ type: "text", text: "Unknown operation." }] };
|
|
1984
2078
|
try {
|
|
1985
2079
|
const data = await t.run(z3.object(t.inputSchema).strict().parse(args));
|
|
1986
|
-
|
|
2080
|
+
const report = ["generate_cost_report", "get_cost_report"].includes(name) && typeof data?.payload?.summary?.text === "string";
|
|
2081
|
+
const states = { pending: "\u5C1A\u672A\u53D1\u9001", sending: "\u53D1\u9001\u5904\u7406\u4E2D\uFF0C\u52FF\u91CD\u590D\u53D1\u9001", sent: "\u5DF2\u786E\u8BA4\u53D1\u9001", unknown: "\u7ED3\u679C\u4E0D\u786E\u5B9A\uFF0C\u9700\u4EBA\u5DE5\u6838\u5BF9" };
|
|
2082
|
+
const text = report ? `\u62A5\u544A ID\uFF1A${data.id}
|
|
2083
|
+
${data.payload.summary.text}
|
|
2084
|
+
\u53D1\u9001\u72B6\u6001\uFF1A${states[data.delivery_state] ?? "\u672A\u786E\u8BA4"}` : JSON.stringify(data);
|
|
2085
|
+
return { content: [{ type: "text", text }], structuredContent: data };
|
|
1987
2086
|
} catch (e) {
|
|
1988
2087
|
return errorResult(e);
|
|
1989
2088
|
}
|
|
@@ -2156,8 +2255,8 @@ function createLocalServer({ dataDir, key, transport = fetch }) {
|
|
|
2156
2255
|
if (key && /\s/.test(key)) throw new Error("INVALID_KEY_FORMAT");
|
|
2157
2256
|
const db = openDatabase(dataDir);
|
|
2158
2257
|
const read = makeReadClient(key, transport);
|
|
2159
|
-
const client = { ...read, status: () => ({ management_key_configured: Boolean(key), mode: "local-stdio", adapter_version: "0.6.0-alpha.
|
|
2160
|
-
const server = new McpServer({ name: "OpenRouter \u5DE5\u4F5C\u53F0", version: "0.6.0-alpha.
|
|
2258
|
+
const client = { ...read, status: () => ({ management_key_configured: Boolean(key), mode: "local-stdio", adapter_version: "0.6.0-alpha.3", authentication_verified: false }) };
|
|
2259
|
+
const server = new McpServer({ name: "OpenRouter \u5DE5\u4F5C\u53F0", version: "0.6.0-alpha.3" });
|
|
2161
2260
|
try {
|
|
2162
2261
|
registerRouterLensTools(server, client);
|
|
2163
2262
|
registerOperations(server, createOperations(client, db));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyuanxv/openrouter-analytics-mcp",
|
|
3
|
-
"version": "0.6.0-alpha.
|
|
3
|
+
"version": "0.6.0-alpha.3",
|
|
4
4
|
"description": "Local OpenRouter cost analytics, reports, experiments and routing presets over MCP stdio.",
|
|
5
5
|
"mcpName": "io.github.cyuanxv/openrouter-analytics-mcp",
|
|
6
6
|
"type": "module",
|