Kosten je Einheit aufrechnen und Abweichungen melden (#699)
- IUsageRepository.GetDailyByAgentAsync: Tagessummen je Agent, Abo-Tokens getrennt - UnitCosts: je Einheit samt Untereinheiten heute/7/30 Tage, Monat, Vormonat, Durchschnittstag (28 Tage, ab 7 Tagen Verlauf), Maximum = Summe der Agentengrenzen - Meldungen: Abweichung vom eigenen Verlauf, Naehe zur Grenzsumme, Monatsschwellen der Instanz; CostWatch alle 15 min, je Einheit/Tag einmal (Tabelle CostAlerts), als Aufgabe an den Menschen; schaltet nichts ab - API /usage/units, /usage/cost-alerts; Zeitreihe gruppiert nach Einheit - WebUI: Karte "Kosten je Einheit" mit Meldungen in der Verbrauchsansicht; i18n en/de - Einstellungen instance.json costWatch - Tests: UnitCostsTests (8) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7eb211fd06
commit
da34b215f7
@@ -671,6 +671,22 @@
|
||||
"usage.cost": "Kosten",
|
||||
"usage.unknownPrice": "(Preis unbekannt)",
|
||||
"usage.notAvailable": "Verbrauchsdaten nicht verfügbar",
|
||||
"usage.units.title": "Kosten je Einheit",
|
||||
"usage.units.hint": "Jede Einheit mit allem, was darunter liegt. Das Maximum ist die Summe der Tagesgrenzen ihrer Agenten — Grenzen bleiben am Agenten. Wichtig ist die Veränderung: Ein Tag weit über dem eigenen Durchschnitt der Einheit wird markiert, auch wenn nichts überschritten ist.",
|
||||
"usage.units.unit": "Einheit",
|
||||
"usage.units.today": "Heute",
|
||||
"usage.units.average": "Durchschnittstag",
|
||||
"usage.units.max": "Max. je Tag",
|
||||
"usage.units.last7": "7 Tage",
|
||||
"usage.units.thisMonth": "Dieser Monat",
|
||||
"usage.units.previousMonth": "Vormonat",
|
||||
"usage.units.quota": "Kontingent (30 T.)",
|
||||
"usage.units.instance": "Instanz",
|
||||
"usage.units.topLevel": "Oberste Ebene",
|
||||
"usage.units.alerts": "Meldungen der letzten 7 Tage",
|
||||
"usage.units.kind.Deviation": "Ungewöhnlicher Tag",
|
||||
"usage.units.kind.NearLimit": "Nahe an den Grenzen",
|
||||
"usage.units.kind.MonthlyThreshold": "Monatssumme überschritten",
|
||||
"usage.forecastTitle": "Kostenprognose",
|
||||
"usage.forecastDay": "Prognose je Tag",
|
||||
"usage.forecastMonth": "Prognose je Monat (30 Tage)",
|
||||
@@ -783,6 +799,7 @@
|
||||
"timeline.metric.runs": "Läufe",
|
||||
"timeline.groupBy": "Nach",
|
||||
"timeline.groupBy.agent": "Agent",
|
||||
"timeline.groupBy.unit": "Einheit",
|
||||
"timeline.groupBy.model": "Modell",
|
||||
"timeline.groupBy.source": "Ereignis",
|
||||
"timeline.groupBy.budget": "Budget",
|
||||
|
||||
@@ -671,6 +671,22 @@
|
||||
"usage.cost": "Cost",
|
||||
"usage.unknownPrice": "(price unknown)",
|
||||
"usage.notAvailable": "Usage data not available",
|
||||
"usage.units.title": "Costs by unit",
|
||||
"usage.units.hint": "Each unit with everything below it. The maximum is the sum of its agents' daily limits — limits stay at the agents. What matters is the change: a day far above the unit's own average is marked even when nothing is exceeded.",
|
||||
"usage.units.unit": "Unit",
|
||||
"usage.units.today": "Today",
|
||||
"usage.units.average": "Average day",
|
||||
"usage.units.max": "Max per day",
|
||||
"usage.units.last7": "7 days",
|
||||
"usage.units.thisMonth": "This month",
|
||||
"usage.units.previousMonth": "Last month",
|
||||
"usage.units.quota": "Quota (30 d)",
|
||||
"usage.units.instance": "Instance",
|
||||
"usage.units.topLevel": "Top level",
|
||||
"usage.units.alerts": "Alerts of the last 7 days",
|
||||
"usage.units.kind.Deviation": "Unusual day",
|
||||
"usage.units.kind.NearLimit": "Near the limits",
|
||||
"usage.units.kind.MonthlyThreshold": "Monthly total crossed",
|
||||
"usage.forecastTitle": "Cost Forecast",
|
||||
"usage.forecastDay": "Forecast per day",
|
||||
"usage.forecastMonth": "Forecast per month (30 days)",
|
||||
@@ -783,6 +799,7 @@
|
||||
"timeline.metric.runs": "Runs",
|
||||
"timeline.groupBy": "By",
|
||||
"timeline.groupBy.agent": "Agent",
|
||||
"timeline.groupBy.unit": "Unit",
|
||||
"timeline.groupBy.model": "Model",
|
||||
"timeline.groupBy.source": "Event",
|
||||
"timeline.groupBy.budget": "Budget",
|
||||
|
||||
@@ -91,7 +91,7 @@ function buildControls(filters, bucket, groupBy, metric) {
|
||||
|
||||
bar.append(
|
||||
group("metric", ["cost", "tokens", "runs"], metric),
|
||||
group("groupBy", ["agent", "model", "source", "budget"], groupBy),
|
||||
group("groupBy", ["agent", "unit", "model", "source", "budget"], groupBy),
|
||||
group("bucket", ["hour", "day"], bucket));
|
||||
return bar;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ async function ladeVerbrauch() {
|
||||
);
|
||||
|
||||
try {
|
||||
const [laeufe, prognose] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte()]);
|
||||
const [laeufe, prognose, einheiten] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte(), erstelleEinheitenKarte()]);
|
||||
container.replaceChildren();
|
||||
if (einheiten) container.append(einheiten);
|
||||
if (prognose) container.append(prognose);
|
||||
|
||||
if (!laeufe || laeufe.length === 0) {
|
||||
@@ -164,6 +165,59 @@ async function ladeVerbrauch() {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Costs by unit (#699) ───
|
||||
|
||||
/**
|
||||
* What each unit costs — itself and everything below it — against the sum of its agents'
|
||||
* limits and against its own course. The alert matters more than the limit: a unit far
|
||||
* above its average has exceeded nothing and is exactly the one to look at.
|
||||
*/
|
||||
async function erstelleEinheitenKarte() {
|
||||
let zeilen, warnungen;
|
||||
try {
|
||||
[zeilen, warnungen] = await Promise.all([api("/usage/units"), api("/usage/cost-alerts?days=7")]);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(zeilen) || zeilen.length === 0) return null;
|
||||
|
||||
const usd = (v) => (v == null ? "—" : `${Number(v).toFixed(2)} $`);
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle einheiten-kosten" });
|
||||
tabelle.append(erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...["usage.units.unit", "usage.units.today", "usage.units.average", "usage.units.max", "usage.units.last7", "usage.units.thisMonth", "usage.units.previousMonth", "usage.units.quota"]
|
||||
.map((k) => erstelleElement("th", { klasse: k === "usage.units.unit" ? "" : "zahl", text: t(k) })))));
|
||||
|
||||
const koerper = erstelleElement("tbody");
|
||||
for (const z of zeilen) {
|
||||
const name = z.unitId == null ? t("usage.units.instance") : z.unitId === "" ? t("usage.units.topLevel") : z.name;
|
||||
const auffaellig = z.averageDay != null && z.today >= 1 && z.today >= 2 * z.averageDay;
|
||||
const nahe = z.shareOfMaxToday != null && z.shareOfMaxToday >= 0.8;
|
||||
const anteil = z.shareOfMaxToday == null ? "" : ` (${Math.round(z.shareOfMaxToday * 100)} %)`;
|
||||
koerper.append(erstelleElement("tr", { klasse: auffaellig || nahe ? "zeile-warnung" : undefined },
|
||||
erstelleElement("td", { stil: `padding-left: ${14 + z.depth * 16}px`, text: name, title: z.path || "" }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.today) + anteil }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.averageDay) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.maxPerDay) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.last7Days) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.thisMonth) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: usd(z.previousMonth) }),
|
||||
// #712: a subscription costs 0 $ but uses quota — shown as tokens, not as "free".
|
||||
erstelleElement("td", { klasse: "zahl", text: z.subscriptionTokens30Days ? formatiereTokens(z.subscriptionTokens30Days) : "—" })));
|
||||
}
|
||||
tabelle.append(koerper);
|
||||
|
||||
const inhalt = [erstelleElement("p", { klasse: "gedaempft text-klein", text: t("usage.units.hint") }), erstelleElement("div", { klasse: "tabelle-scroll" }, tabelle)];
|
||||
if (Array.isArray(warnungen) && warnungen.length > 0) {
|
||||
inhalt.push(erstelleElement("h3", { klasse: "fall-verlauf-titel", text: t("usage.units.alerts") }));
|
||||
for (const w of warnungen) {
|
||||
inhalt.push(erstelleElement("div", { klasse: "hinweis-box warnung" },
|
||||
erstelleElement("strong", { text: `${formatiereDatumZeit(w.raisedAt)} · ${t(`usage.units.kind.${w.kind}`)}` }),
|
||||
erstelleElement("div", { text: w.message })));
|
||||
}
|
||||
}
|
||||
return erstelleKarte(t("usage.units.title"), inhalt);
|
||||
}
|
||||
|
||||
// ─── Kostenprognose (#595) ───
|
||||
|
||||
function getGrundlageText(basis, sample) {
|
||||
|
||||
Reference in New Issue
Block a user