@argosvix/mcp-server 0.22.0-alpha.1 → 0.22.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/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +433 -0
- package/dist/tools.js.map +1 -1
- package/dist/tools.test.js +164 -1
- package/dist/tools.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAkJ/D,eAAO,MAAM,KAAK,EAAE,IAAI,EA2jDvB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAChE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC,CA6tCD"}
|
package/dist/tools.js
CHANGED
|
@@ -148,6 +148,15 @@ const TOOL_ARG_ALLOWLIST = {
|
|
|
148
148
|
// percentiles / llm-budget / audit) を 並列 fetch + 1 narrative response に
|
|
149
149
|
// 圧縮。 window 軸のみ 受け、 backend 新 endpoint 不要 (= 純 read aggregator)。
|
|
150
150
|
get_account_health: ["window"],
|
|
151
|
+
// 2026-06-05 axis 4 Tier 1 = propose_alert_rules (= AI が baseline 統計から
|
|
152
|
+
// 推奨 alert rule を 提案、 適用は customer 確認後 create_alert 別 step)。
|
|
153
|
+
// 既存 4 endpoint (= aggregate ×2 + percentiles + list_alerts) を fetch +
|
|
154
|
+
// 推奨 JSON を 返す。 backend 新 endpoint 不要。
|
|
155
|
+
propose_alert_rules: ["lookbackDays"],
|
|
156
|
+
// 2026-06-05 axis 4 Tier 1 = detect_anomaly (= 現 window vs baseline window
|
|
157
|
+
// 比較で cost / latency / error_rate / call_volume の 4 軸 異常検出)。
|
|
158
|
+
// pure MCP-side aggregator、 backend 変更ゼロ。
|
|
159
|
+
detect_anomaly: ["window", "threshold"],
|
|
151
160
|
// 2026-06-02 Codex round 2 🔴 fix = idempotencyKey 必須 path (= AI agent が
|
|
152
161
|
// retry した時に backend で dedup)、 client が opaque string 64 char で carry。
|
|
153
162
|
run_eval: ["name", "recentCount", "label", "promptRegistryId", "idempotencyKey"],
|
|
@@ -1334,6 +1343,49 @@ export const tools = [
|
|
|
1334
1343
|
},
|
|
1335
1344
|
},
|
|
1336
1345
|
},
|
|
1346
|
+
{
|
|
1347
|
+
name: "detect_anomaly",
|
|
1348
|
+
description: "現 window と baseline window (= 同 length の 1 期前) を 比較して cost / latency / error_rate / call_volume の 4 軸で 異常を検出 (= axis 4 Tier 1 = AI が 1 prompt で 「何か変なことが起きてないか」 把握)。 " +
|
|
1349
|
+
"threshold で 感度 調整可能: sensitive (= 1.5×) / normal (= 2×、 default) / conservative (= 3×)。 detection 数 0-4 件、 各 anomaly は narrative 付き。 " +
|
|
1350
|
+
"返却 = { window, threshold, current: {...}, baseline: {...}, anomalies: [{ axis, severity: 'minor'|'major'|'critical', current, baseline, ratio, narrative }] } 形式。 baseline data 不足 (= 期間 record < 10) は anomalies: [] + warning narrative で carry。",
|
|
1351
|
+
inputSchema: {
|
|
1352
|
+
type: "object",
|
|
1353
|
+
additionalProperties: false,
|
|
1354
|
+
properties: {
|
|
1355
|
+
window: {
|
|
1356
|
+
type: "string",
|
|
1357
|
+
description: "観測 window (= '1h' / '24h' / '7d'、 default '24h')",
|
|
1358
|
+
enum: ["1h", "24h", "7d"],
|
|
1359
|
+
default: "24h",
|
|
1360
|
+
},
|
|
1361
|
+
threshold: {
|
|
1362
|
+
type: "string",
|
|
1363
|
+
description: "感度 (= 'sensitive' 1.5× / 'normal' 2× / 'conservative' 3×、 default 'normal')",
|
|
1364
|
+
enum: ["sensitive", "normal", "conservative"],
|
|
1365
|
+
default: "normal",
|
|
1366
|
+
},
|
|
1367
|
+
},
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
name: "propose_alert_rules",
|
|
1372
|
+
description: "過去 lookbackDays (= 7-30、 default 14) の calls pattern を 分析して、 cost / latency / error_rate / anomaly の 推奨 alert rule を JSON 提案 (= axis 4 Tier 1 = AI が baseline 設計)。 " +
|
|
1373
|
+
"適用は customer 確認後 create_alert で別 step (= propose のみ、 副作用ゼロ)。 既存 alerts と被らない rule のみ 提案 (= list_alerts で 既存 type を fetch)。 " +
|
|
1374
|
+
"返却 = { lookbackDays, baseline: {meanDailyCost, p95Latency, errorRate, dailyCalls}, proposals: [{ name, alertType, thresholdValue, windowMinutes, reasoning }], skipped: [{ alertType, reason }] } 形式。 大手 dashboard が UI で 表示する 軸を MCP-first で 1 prompt 完結。",
|
|
1375
|
+
inputSchema: {
|
|
1376
|
+
type: "object",
|
|
1377
|
+
additionalProperties: false,
|
|
1378
|
+
properties: {
|
|
1379
|
+
lookbackDays: {
|
|
1380
|
+
type: "integer",
|
|
1381
|
+
description: "baseline 算出の lookback 日数 (= 7-30、 default 14)",
|
|
1382
|
+
minimum: 7,
|
|
1383
|
+
maximum: 30,
|
|
1384
|
+
default: 14,
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1337
1389
|
{
|
|
1338
1390
|
name: "get_account_health",
|
|
1339
1391
|
description: "自社 LLM infra の健康状態 サマリを 1 call で取得 (= axis 4 Tier 1 = 自律 AI ops)。 既存 4 endpoint (= aggregate_calls / get_percentiles / get_llm_budget / list_audit_log) を 並列 fetch して 1 narrative response に圧縮。 " +
|
|
@@ -2058,6 +2110,387 @@ export async function dispatchTool(input) {
|
|
|
2058
2110
|
}
|
|
2059
2111
|
return await callApi(apiBase, `/v1/projects/${encodeURIComponent(projectId)}`, {}, apiKey, { method: "DELETE" });
|
|
2060
2112
|
}
|
|
2113
|
+
case "detect_anomaly": {
|
|
2114
|
+
// 2026-06-05 axis 4 Tier 1 = 現 window vs baseline window (= 同 length
|
|
2115
|
+
// の 1 期前) 比較で 4 軸 異常検出。 pure MCP-side aggregator、 backend
|
|
2116
|
+
// 変更ゼロ。 threshold 感度 = 1.5× (sensitive) / 2× (normal) / 3×
|
|
2117
|
+
// (conservative)。 baseline data 不足は anomalies: [] + warning。
|
|
2118
|
+
const winRaw = typeof safeArgs["window"] === "string" ? safeArgs["window"] : "24h";
|
|
2119
|
+
const window = winRaw === "1h" || winRaw === "24h" || winRaw === "7d" ? winRaw : "24h";
|
|
2120
|
+
const thresholdRaw = typeof safeArgs["threshold"] === "string" ? safeArgs["threshold"] : "normal";
|
|
2121
|
+
const threshold = thresholdRaw === "sensitive" ||
|
|
2122
|
+
thresholdRaw === "normal" ||
|
|
2123
|
+
thresholdRaw === "conservative"
|
|
2124
|
+
? thresholdRaw
|
|
2125
|
+
: "normal";
|
|
2126
|
+
const multiplier = threshold === "sensitive" ? 1.5 : threshold === "conservative" ? 3 : 2;
|
|
2127
|
+
const windowMs = window === "1h"
|
|
2128
|
+
? 60 * 60 * 1000
|
|
2129
|
+
: window === "24h"
|
|
2130
|
+
? 24 * 60 * 60 * 1000
|
|
2131
|
+
: 7 * 24 * 60 * 60 * 1000;
|
|
2132
|
+
const now = Date.now();
|
|
2133
|
+
const currentEnd = new Date(now).toISOString();
|
|
2134
|
+
const currentStart = new Date(now - windowMs).toISOString();
|
|
2135
|
+
const baselineEnd = currentStart;
|
|
2136
|
+
const baselineStart = new Date(now - 2 * windowMs).toISOString();
|
|
2137
|
+
const fetchWindow = async (startTime, endTime) => {
|
|
2138
|
+
const [costRes, errorRes, countRes, percentileRes] = await Promise.allSettled([
|
|
2139
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2140
|
+
method: "POST",
|
|
2141
|
+
jsonBody: { startTime, endTime, groupBy: "provider", metric: "cost" },
|
|
2142
|
+
}),
|
|
2143
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2144
|
+
method: "POST",
|
|
2145
|
+
jsonBody: { startTime, endTime, groupBy: "provider", metric: "error_rate" },
|
|
2146
|
+
}),
|
|
2147
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2148
|
+
method: "POST",
|
|
2149
|
+
jsonBody: { startTime, endTime, groupBy: "provider", metric: "count" },
|
|
2150
|
+
}),
|
|
2151
|
+
callApi(apiBase, "/v1/query/percentiles", {}, apiKey, {
|
|
2152
|
+
method: "POST",
|
|
2153
|
+
jsonBody: { startTime, endTime, metric: "latency" },
|
|
2154
|
+
}),
|
|
2155
|
+
]);
|
|
2156
|
+
const extractJson = (r) => {
|
|
2157
|
+
if (r.status !== "fulfilled" || r.value.isError)
|
|
2158
|
+
return null;
|
|
2159
|
+
try {
|
|
2160
|
+
return JSON.parse(r.value.content[0]?.text ?? "");
|
|
2161
|
+
}
|
|
2162
|
+
catch {
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const cost = extractJson(costRes);
|
|
2167
|
+
const errors = extractJson(errorRes);
|
|
2168
|
+
const counts = extractJson(countRes);
|
|
2169
|
+
const percentiles = extractJson(percentileRes);
|
|
2170
|
+
return {
|
|
2171
|
+
costUsd: cost?.total?.value ?? 0,
|
|
2172
|
+
errorRate: errors?.total?.value ?? null,
|
|
2173
|
+
calls: counts?.total?.value ?? 0,
|
|
2174
|
+
p95Latency: percentiles?.p95 ?? null,
|
|
2175
|
+
records: percentiles?.total ?? counts?.total?.count ?? 0,
|
|
2176
|
+
};
|
|
2177
|
+
};
|
|
2178
|
+
const [current, baseline] = await Promise.all([
|
|
2179
|
+
fetchWindow(currentStart, currentEnd),
|
|
2180
|
+
fetchWindow(baselineStart, baselineEnd),
|
|
2181
|
+
]);
|
|
2182
|
+
if (baseline.records < 10) {
|
|
2183
|
+
return {
|
|
2184
|
+
content: [
|
|
2185
|
+
{
|
|
2186
|
+
type: "text",
|
|
2187
|
+
text: JSON.stringify({
|
|
2188
|
+
window,
|
|
2189
|
+
threshold,
|
|
2190
|
+
current,
|
|
2191
|
+
baseline,
|
|
2192
|
+
anomalies: [],
|
|
2193
|
+
warning: `baseline window に records ${baseline.records} 件 (< 10 件)、 anomaly 検出 統計強度 不足`,
|
|
2194
|
+
}),
|
|
2195
|
+
},
|
|
2196
|
+
],
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const anomalies = [];
|
|
2200
|
+
const severityFromRatio = (ratio) => {
|
|
2201
|
+
if (ratio >= multiplier * 2)
|
|
2202
|
+
return "critical";
|
|
2203
|
+
if (ratio >= multiplier * 1.3)
|
|
2204
|
+
return "major";
|
|
2205
|
+
return "minor";
|
|
2206
|
+
};
|
|
2207
|
+
// 1. cost spike (= current cost > baseline cost × multiplier)
|
|
2208
|
+
if (baseline.costUsd > 0 && current.costUsd > baseline.costUsd * multiplier) {
|
|
2209
|
+
const ratio = current.costUsd / baseline.costUsd;
|
|
2210
|
+
anomalies.push({
|
|
2211
|
+
axis: "cost",
|
|
2212
|
+
severity: severityFromRatio(ratio),
|
|
2213
|
+
current: Math.round(current.costUsd * 100) / 100,
|
|
2214
|
+
baseline: Math.round(baseline.costUsd * 100) / 100,
|
|
2215
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
2216
|
+
narrative: `cost が baseline の ${ratio.toFixed(1)}× ($${current.costUsd.toFixed(2)} vs $${baseline.costUsd.toFixed(2)})。 threshold=${threshold} (= ${multiplier}×) を 超過。`,
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
// 2. latency p95 spike
|
|
2220
|
+
if (baseline.p95Latency !== null &&
|
|
2221
|
+
baseline.p95Latency > 0 &&
|
|
2222
|
+
current.p95Latency !== null &&
|
|
2223
|
+
current.p95Latency > baseline.p95Latency * multiplier) {
|
|
2224
|
+
const ratio = current.p95Latency / baseline.p95Latency;
|
|
2225
|
+
anomalies.push({
|
|
2226
|
+
axis: "latency",
|
|
2227
|
+
severity: severityFromRatio(ratio),
|
|
2228
|
+
current: Math.round(current.p95Latency),
|
|
2229
|
+
baseline: Math.round(baseline.p95Latency),
|
|
2230
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
2231
|
+
narrative: `p95 latency が baseline の ${ratio.toFixed(1)}× (${Math.round(current.p95Latency)} ms vs ${Math.round(baseline.p95Latency)} ms)。 LLM provider 側 劣化 / 自社 prompt 改変 / network 等 軸。`,
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
// 3. error_rate spike (= current > max(baseline × multiplier, +5pp))
|
|
2235
|
+
if (baseline.errorRate !== null &&
|
|
2236
|
+
current.errorRate !== null &&
|
|
2237
|
+
current.errorRate > Math.max(baseline.errorRate * multiplier, baseline.errorRate + 0.05)) {
|
|
2238
|
+
const ratio = baseline.errorRate > 0 ? current.errorRate / baseline.errorRate : 999;
|
|
2239
|
+
anomalies.push({
|
|
2240
|
+
axis: "error_rate",
|
|
2241
|
+
severity: severityFromRatio(ratio === 999 ? multiplier * 2 : ratio),
|
|
2242
|
+
current: Math.round(current.errorRate * 10000) / 10000,
|
|
2243
|
+
baseline: Math.round(baseline.errorRate * 10000) / 10000,
|
|
2244
|
+
ratio: ratio === 999 ? 999 : Math.round(ratio * 100) / 100,
|
|
2245
|
+
narrative: `error rate が ${(current.errorRate * 100).toFixed(2)}% (baseline ${(baseline.errorRate * 100).toFixed(2)}%)。 ratio ${ratio === 999 ? "∞" : ratio.toFixed(1)}× で threshold=${threshold} 超過。`,
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
// 4. call volume spike or drop (= current calls > baseline × multiplier OR < baseline / multiplier)
|
|
2249
|
+
if (baseline.calls > 0) {
|
|
2250
|
+
const ratio = current.calls / baseline.calls;
|
|
2251
|
+
if (ratio >= multiplier) {
|
|
2252
|
+
anomalies.push({
|
|
2253
|
+
axis: "call_volume",
|
|
2254
|
+
severity: severityFromRatio(ratio),
|
|
2255
|
+
current: current.calls,
|
|
2256
|
+
baseline: baseline.calls,
|
|
2257
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
2258
|
+
narrative: `call volume が baseline の ${ratio.toFixed(1)}× (${current.calls} vs ${baseline.calls})。 traffic spike / retry storm / new user onboarding 等 軸。`,
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
else if (ratio > 0 && 1 / ratio >= multiplier) {
|
|
2262
|
+
const dropRatio = 1 / ratio;
|
|
2263
|
+
anomalies.push({
|
|
2264
|
+
axis: "call_volume",
|
|
2265
|
+
severity: severityFromRatio(dropRatio),
|
|
2266
|
+
current: current.calls,
|
|
2267
|
+
baseline: baseline.calls,
|
|
2268
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
2269
|
+
narrative: `call volume が baseline の 1/${dropRatio.toFixed(1)} に 急減 (${current.calls} vs ${baseline.calls})。 SDK side outage / user drop-off / 自社 feature flag 等 軸。`,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return {
|
|
2274
|
+
content: [
|
|
2275
|
+
{
|
|
2276
|
+
type: "text",
|
|
2277
|
+
text: JSON.stringify({
|
|
2278
|
+
window,
|
|
2279
|
+
threshold,
|
|
2280
|
+
multiplier,
|
|
2281
|
+
current,
|
|
2282
|
+
baseline,
|
|
2283
|
+
anomalies,
|
|
2284
|
+
}),
|
|
2285
|
+
},
|
|
2286
|
+
],
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
case "propose_alert_rules": {
|
|
2290
|
+
// 2026-06-05 axis 4 Tier 1 = baseline 統計 + 推奨 alert rule 提案。
|
|
2291
|
+
// propose のみ、 適用は別 step (= create_alert)。 既存 alert と被る
|
|
2292
|
+
// type は skipped 配列に reason 付きで carry (= duplicate 防御)。
|
|
2293
|
+
const lookbackRaw = safeArgs["lookbackDays"];
|
|
2294
|
+
const lookbackDays = typeof lookbackRaw === "number" &&
|
|
2295
|
+
Number.isFinite(lookbackRaw) &&
|
|
2296
|
+
lookbackRaw >= 7 &&
|
|
2297
|
+
lookbackRaw <= 30
|
|
2298
|
+
? Math.floor(lookbackRaw)
|
|
2299
|
+
: 14;
|
|
2300
|
+
const now = Date.now();
|
|
2301
|
+
const windowMs = lookbackDays * 24 * 60 * 60 * 1000;
|
|
2302
|
+
const startTime = new Date(now - windowMs).toISOString();
|
|
2303
|
+
const endTime = new Date(now).toISOString();
|
|
2304
|
+
const dailyCostBody = {
|
|
2305
|
+
startTime,
|
|
2306
|
+
endTime,
|
|
2307
|
+
groupBy: "day",
|
|
2308
|
+
metric: "cost",
|
|
2309
|
+
};
|
|
2310
|
+
const errorRateBody = {
|
|
2311
|
+
startTime,
|
|
2312
|
+
endTime,
|
|
2313
|
+
groupBy: "provider",
|
|
2314
|
+
metric: "error_rate",
|
|
2315
|
+
};
|
|
2316
|
+
const dailyCountBody = {
|
|
2317
|
+
startTime,
|
|
2318
|
+
endTime,
|
|
2319
|
+
groupBy: "day",
|
|
2320
|
+
metric: "count",
|
|
2321
|
+
};
|
|
2322
|
+
const percentileBody = { startTime, endTime, metric: "latency" };
|
|
2323
|
+
const [dailyCostRes, errorRateRes, dailyCountRes, percentileRes, alertsRes] = await Promise.allSettled([
|
|
2324
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2325
|
+
method: "POST",
|
|
2326
|
+
jsonBody: dailyCostBody,
|
|
2327
|
+
}),
|
|
2328
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2329
|
+
method: "POST",
|
|
2330
|
+
jsonBody: errorRateBody,
|
|
2331
|
+
}),
|
|
2332
|
+
callApi(apiBase, "/v1/query/aggregate", {}, apiKey, {
|
|
2333
|
+
method: "POST",
|
|
2334
|
+
jsonBody: dailyCountBody,
|
|
2335
|
+
}),
|
|
2336
|
+
callApi(apiBase, "/v1/query/percentiles", {}, apiKey, {
|
|
2337
|
+
method: "POST",
|
|
2338
|
+
jsonBody: percentileBody,
|
|
2339
|
+
}),
|
|
2340
|
+
callApi(apiBase, "/v1/alerts", {}, apiKey),
|
|
2341
|
+
]);
|
|
2342
|
+
const extractJson = (r) => {
|
|
2343
|
+
if (r.status !== "fulfilled" || r.value.isError)
|
|
2344
|
+
return null;
|
|
2345
|
+
const txt = r.value.content[0]?.text ?? "";
|
|
2346
|
+
try {
|
|
2347
|
+
return JSON.parse(txt);
|
|
2348
|
+
}
|
|
2349
|
+
catch {
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
const dailyCost = extractJson(dailyCostRes);
|
|
2354
|
+
const errorRate = extractJson(errorRateRes);
|
|
2355
|
+
const dailyCount = extractJson(dailyCountRes);
|
|
2356
|
+
const percentiles = extractJson(percentileRes);
|
|
2357
|
+
const alerts = extractJson(alertsRes);
|
|
2358
|
+
const dailyCostValues = (dailyCost?.groups ?? [])
|
|
2359
|
+
.map((g) => Number(g.value))
|
|
2360
|
+
.filter((v) => Number.isFinite(v));
|
|
2361
|
+
const meanDailyCost = dailyCostValues.length > 0
|
|
2362
|
+
? dailyCostValues.reduce((a, b) => a + b, 0) / dailyCostValues.length
|
|
2363
|
+
: 0;
|
|
2364
|
+
const maxDailyCost = dailyCostValues.length > 0 ? Math.max(...dailyCostValues) : 0;
|
|
2365
|
+
const dailyCountValues = (dailyCount?.groups ?? [])
|
|
2366
|
+
.map((g) => Number(g.value))
|
|
2367
|
+
.filter((v) => Number.isFinite(v));
|
|
2368
|
+
const meanDailyCalls = dailyCountValues.length > 0
|
|
2369
|
+
? dailyCountValues.reduce((a, b) => a + b, 0) / dailyCountValues.length
|
|
2370
|
+
: 0;
|
|
2371
|
+
const p95Latency = percentiles?.p95 ?? null;
|
|
2372
|
+
const totalCalls = dailyCount?.total?.value ?? 0;
|
|
2373
|
+
const observedErrorRate = errorRate?.total?.value !== undefined && errorRate.total.value !== null
|
|
2374
|
+
? Number(errorRate.total.value)
|
|
2375
|
+
: null;
|
|
2376
|
+
const existingTypes = new Set((alerts?.alerts ?? [])
|
|
2377
|
+
.map((a) => a.alertType)
|
|
2378
|
+
.filter((t) => typeof t === "string"));
|
|
2379
|
+
const proposals = [];
|
|
2380
|
+
const skipped = [];
|
|
2381
|
+
// 1. cost_daily = 過去 lookback 平均の 2× で trigger (= 異常な 1 日 支出 を 検出)
|
|
2382
|
+
if (existingTypes.has("cost_daily")) {
|
|
2383
|
+
skipped.push({
|
|
2384
|
+
alertType: "cost_daily",
|
|
2385
|
+
reason: "既に同 type の alert が 設定済",
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
else if (meanDailyCost <= 0) {
|
|
2389
|
+
skipped.push({
|
|
2390
|
+
alertType: "cost_daily",
|
|
2391
|
+
reason: `lookback ${lookbackDays} 日の cost data が 不足 (mean=${meanDailyCost})`,
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
else {
|
|
2395
|
+
const threshold = Math.round(meanDailyCost * 2 * 100) / 100;
|
|
2396
|
+
proposals.push({
|
|
2397
|
+
name: `Daily cost > ${threshold} USD (baseline 2×)`,
|
|
2398
|
+
alertType: "cost_daily",
|
|
2399
|
+
thresholdValue: threshold,
|
|
2400
|
+
windowMinutes: 1440,
|
|
2401
|
+
reasoning: `過去 ${lookbackDays} 日の 平均 daily cost = $${meanDailyCost.toFixed(2)} / 観測 max = $${maxDailyCost.toFixed(2)}。 baseline 2× を 異常閾値 として 提案。`,
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
// 2. latency_p95 = 過去 p95 の 1.5× で trigger
|
|
2405
|
+
if (existingTypes.has("latency_p95")) {
|
|
2406
|
+
skipped.push({
|
|
2407
|
+
alertType: "latency_p95",
|
|
2408
|
+
reason: "既に同 type の alert が 設定済",
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
else if (p95Latency === null || p95Latency <= 0) {
|
|
2412
|
+
skipped.push({
|
|
2413
|
+
alertType: "latency_p95",
|
|
2414
|
+
reason: "lookback 期間に p95 latency data が 不足",
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
else {
|
|
2418
|
+
const threshold = Math.round(p95Latency * 1.5);
|
|
2419
|
+
proposals.push({
|
|
2420
|
+
name: `p95 latency > ${threshold} ms (baseline 1.5×)`,
|
|
2421
|
+
alertType: "latency_p95",
|
|
2422
|
+
thresholdValue: threshold,
|
|
2423
|
+
windowMinutes: 60,
|
|
2424
|
+
reasoning: `過去 ${lookbackDays} 日の p95 latency = ${Math.round(p95Latency)} ms。 baseline 1.5× を 劣化閾値 として 提案。`,
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
// 3. error_rate = 観測 error_rate の 3× もしくは最低 5% で trigger
|
|
2428
|
+
if (existingTypes.has("error_rate")) {
|
|
2429
|
+
skipped.push({
|
|
2430
|
+
alertType: "error_rate",
|
|
2431
|
+
reason: "既に同 type の alert が 設定済",
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
else if (totalCalls < 100) {
|
|
2435
|
+
skipped.push({
|
|
2436
|
+
alertType: "error_rate",
|
|
2437
|
+
reason: `lookback の total calls = ${totalCalls} で 統計的に baseline 不確定 (要 100+ calls)`,
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
else {
|
|
2441
|
+
const baseRate = observedErrorRate ?? 0;
|
|
2442
|
+
const threshold = Math.max(0.05, Math.round(baseRate * 3 * 1000) / 1000);
|
|
2443
|
+
proposals.push({
|
|
2444
|
+
name: `Error rate > ${(threshold * 100).toFixed(1)}% (baseline 3× or 5% min)`,
|
|
2445
|
+
alertType: "error_rate",
|
|
2446
|
+
thresholdValue: threshold,
|
|
2447
|
+
windowMinutes: 60,
|
|
2448
|
+
reasoning: `過去 ${lookbackDays} 日の error rate = ${((baseRate ?? 0) * 100).toFixed(2)}%。 baseline 3× と 最低 5% の 大きい方 を 異常閾値 として 提案。`,
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
// 4. anomaly_cost = 統計的異常検知 (= forecast モデル baseline、 type 別 1 件のみ)
|
|
2452
|
+
if (existingTypes.has("anomaly_cost")) {
|
|
2453
|
+
skipped.push({
|
|
2454
|
+
alertType: "anomaly_cost",
|
|
2455
|
+
reason: "既に同 type の alert が 設定済",
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
else if (meanDailyCalls < 50) {
|
|
2459
|
+
skipped.push({
|
|
2460
|
+
alertType: "anomaly_cost",
|
|
2461
|
+
reason: `1 日平均 calls = ${Math.round(meanDailyCalls)} で anomaly 検知の 統計強度 不足 (要 50+ daily calls)`,
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
proposals.push({
|
|
2466
|
+
name: `Cost anomaly (statistical z-score > 3σ)`,
|
|
2467
|
+
alertType: "anomaly_cost",
|
|
2468
|
+
thresholdValue: 3,
|
|
2469
|
+
windowMinutes: 60,
|
|
2470
|
+
reasoning: `過去 ${lookbackDays} 日で 1 日平均 ${Math.round(meanDailyCalls)} calls 観測、 anomaly forecast モデルが 有効。 z-score 3σ 超を 異常 として 提案。`,
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
return {
|
|
2474
|
+
content: [
|
|
2475
|
+
{
|
|
2476
|
+
type: "text",
|
|
2477
|
+
text: JSON.stringify({
|
|
2478
|
+
lookbackDays,
|
|
2479
|
+
baseline: {
|
|
2480
|
+
meanDailyCost: Math.round(meanDailyCost * 100) / 100,
|
|
2481
|
+
maxDailyCost: Math.round(maxDailyCost * 100) / 100,
|
|
2482
|
+
p95Latency: p95Latency !== null ? Math.round(p95Latency) : null,
|
|
2483
|
+
errorRate: observedErrorRate,
|
|
2484
|
+
dailyCalls: Math.round(meanDailyCalls),
|
|
2485
|
+
totalCalls,
|
|
2486
|
+
},
|
|
2487
|
+
proposals,
|
|
2488
|
+
skipped,
|
|
2489
|
+
}),
|
|
2490
|
+
},
|
|
2491
|
+
],
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2061
2494
|
case "get_account_health": {
|
|
2062
2495
|
// 2026-06-05 axis 4 Tier 1 = 自社 LLM infra 健康状態 サマリ。 既存
|
|
2063
2496
|
// 4 endpoint を 並列 fetch + 1 narrative 圧縮。 個別 fail は partial
|