#608: Ein wegen erschoepften Budgets abgelehnter Lauf hinterlaesst jetzt einen Beleg (Status BudgetExceeded) an Aufgabe und Verlauf; die Log-Warnung kommt einmal je Agent, Grenze und Tag. Die Kostenprognose zaehlt Ablehnungen nicht als Lauf und warnt, wenn die geplanten Aufgaben eines Agenten sein Tagesbudget uebersteigen (API + Verbrauchsansicht). #607: Passt schon der erste Treffer nicht in den Abruf, wird er gekuerzt (Anfang und Ende bleiben) statt ganz zu fehlen. remember weist auf gleichnamige Eintraege ohne Schluessel und auf Eintraege nahe der Abrufgrenze hin. Version 0.1.9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
11 KiB
JavaScript
289 lines
11 KiB
JavaScript
// ClawdDotNet — Token-Verbrauch & Analyse
|
||
//
|
||
// Diagramme (CSS/DOM-Balken ohne externe Bibliothek), Statistiken und Läufetabelle.
|
||
//
|
||
// Feldnamen wie UsageDto (#591): costUsd, costIsKnown, occurredAt, status. Eine
|
||
// Gesamtsumme liefert die API nicht — sie ist Prompt + Completion. Vorher las die Ansicht
|
||
// totalTokens, cost und timestamp und zeigte überall 0 bzw. "—".
|
||
|
||
import { api } from "../modules/api.js";
|
||
import {
|
||
erstelleElement,
|
||
erstelleKarte,
|
||
formatiereDatumZeit,
|
||
formatiereTokens,
|
||
formatiereKosten
|
||
} from "../modules/dom.js";
|
||
|
||
export async function rendereVerbrauch() {
|
||
const container = document.getElementById("ansicht-inhalt");
|
||
if (!container) return;
|
||
container.replaceChildren();
|
||
|
||
const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" });
|
||
layout.append(erstelleElement("h1", { text: "Token-Verbrauch & Analyse" }));
|
||
|
||
const inhaltBereich = erstelleElement("div", { id: "verbrauch-inhalt-bereich" });
|
||
layout.append(inhaltBereich);
|
||
container.append(layout);
|
||
|
||
await ladeVerbrauch();
|
||
}
|
||
|
||
function gesamtTokens(lauf) {
|
||
return (lauf.promptTokens || 0) + (lauf.completionTokens || 0);
|
||
}
|
||
|
||
async function ladeVerbrauch() {
|
||
const container = document.getElementById("verbrauch-inhalt-bereich");
|
||
if (!container) return;
|
||
|
||
container.replaceChildren(
|
||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verbrauchsdaten…" })
|
||
);
|
||
|
||
try {
|
||
const [laeufe, prognose] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte()]);
|
||
container.replaceChildren();
|
||
if (prognose) container.append(prognose);
|
||
|
||
if (!laeufe || laeufe.length === 0) {
|
||
container.append(
|
||
erstelleKarte("Keine Daten", [
|
||
erstelleElement("p", {
|
||
klasse: "gedaempft",
|
||
text: "Noch keine Token-Verbrauchsdaten aufgezeichnet."
|
||
})
|
||
])
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Statistiken berechnen
|
||
let summeTokens = 0;
|
||
let summePrompt = 0;
|
||
let summeCompletion = 0;
|
||
let summeKosten = 0;
|
||
let unbekannteKosten = 0;
|
||
const nachAgent = new Map();
|
||
const nachModell = new Map();
|
||
|
||
for (const l of laeufe) {
|
||
const tokens = gesamtTokens(l);
|
||
summeTokens += tokens;
|
||
summePrompt += l.promptTokens || 0;
|
||
summeCompletion += l.completionTokens || 0;
|
||
summeKosten += Number(l.costUsd || 0);
|
||
if (!l.costIsKnown) unbekannteKosten++;
|
||
|
||
const agentKey = l.agentId || "Unbekannt";
|
||
nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + tokens);
|
||
|
||
const modellKey = l.model || "Unbekannt";
|
||
nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + tokens);
|
||
}
|
||
|
||
// 1. Kacheln (Statistiken)
|
||
const kachelGitter = erstelleElement("div", { klasse: "kachel-gitter" });
|
||
|
||
kachelGitter.append(
|
||
erstelleStatKachel(`Gesamtkosten (${laeufe.length} Läufe)`, formatiereKosten(summeKosten)),
|
||
erstelleStatKachel("Tokens gesamt", formatiereTokens(summeTokens)),
|
||
erstelleStatKachel("Eingabe-Tokens (Prompt)", formatiereTokens(summePrompt)),
|
||
erstelleStatKachel("Ausgabe-Tokens (Completion)", formatiereTokens(summeCompletion))
|
||
);
|
||
|
||
const hinweise = [];
|
||
if (unbekannteKosten > 0) {
|
||
hinweise.push(
|
||
erstelleElement("p", {
|
||
klasse: "gedaempft text-klein",
|
||
text: `${unbekannteKosten} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.`
|
||
})
|
||
);
|
||
}
|
||
|
||
// 2. Balkendiagramme (rein CSS/DOM)
|
||
const diagrammGitter = erstelleElement("div", { klasse: "diagramm-gitter" });
|
||
|
||
const agentBalken = erstelleBalkenDiagramm("Tokens nach Agent", nachAgent, summeTokens);
|
||
const modellBalken = erstelleBalkenDiagramm("Tokens nach Modell", nachModell, summeTokens);
|
||
diagrammGitter.append(agentBalken, modellBalken);
|
||
|
||
// 3. Läufe-Tabelle
|
||
const tabelleKarte = erstelleElement("div", { klasse: "karte" });
|
||
tabelleKarte.append(erstelleElement("h2", { text: "Letzte Agenten-Läufe" }));
|
||
|
||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||
const thead = erstelleElement("thead");
|
||
thead.append(
|
||
erstelleElement("tr", {},
|
||
erstelleElement("th", { text: "Zeitpunkt" }),
|
||
erstelleElement("th", { text: "Agent" }),
|
||
erstelleElement("th", { text: "Modell" }),
|
||
erstelleElement("th", { text: "Status" }),
|
||
erstelleElement("th", { text: "Prompt", stil: "text-align: right;" }),
|
||
erstelleElement("th", { text: "Completion", stil: "text-align: right;" }),
|
||
erstelleElement("th", { text: "Gesamt", stil: "text-align: right;" }),
|
||
erstelleElement("th", { text: "Kosten", stil: "text-align: right;" })
|
||
)
|
||
);
|
||
|
||
const tbody = erstelleElement("tbody");
|
||
|
||
for (const lauf of laeufe) {
|
||
const kosten = lauf.costIsKnown
|
||
? formatiereKosten(lauf.costUsd)
|
||
: `${formatiereKosten(lauf.costUsd)} (Preis unbekannt)`;
|
||
|
||
const tr = erstelleElement("tr");
|
||
tr.append(
|
||
erstelleElement("td", { text: formatiereDatumZeit(lauf.occurredAt) }),
|
||
erstelleElement("td", {}, erstelleElement("strong", { text: lauf.agentId })),
|
||
erstelleElement("td", { klasse: "gedaempft text-klein", text: lauf.model }),
|
||
erstelleElement("td", { klasse: "text-klein", text: lauf.status || "—" }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.promptTokens) }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.completionTokens) }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(gesamtTokens(lauf)) }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: kosten })
|
||
);
|
||
tbody.append(tr);
|
||
}
|
||
|
||
tabelle.append(thead, tbody);
|
||
tabelleKarte.append(tabelle);
|
||
|
||
container.append(kachelGitter, ...hinweise, diagrammGitter, tabelleKarte);
|
||
} catch (err) {
|
||
container.replaceChildren(
|
||
erstelleKarte("Verbrauchsdaten nicht verfügbar", [
|
||
erstelleElement("p", { klasse: "gedaempft", text: err.message })
|
||
])
|
||
);
|
||
}
|
||
}
|
||
|
||
// ─── Kostenprognose (#595) ───
|
||
|
||
const GRUNDLAGE_TEXT = {
|
||
Laeufe: (n) => `Ø letzte ${n} Läufe`,
|
||
Agent: (n) => `Ø des Agenten (${n} Läufe)`,
|
||
Schaetzung: () => "Schätzung – noch kein Lauf",
|
||
Unbekannt: () => "keine Grundlage"
|
||
};
|
||
|
||
async function erstellePrognoseKarte() {
|
||
let p;
|
||
try {
|
||
p = await api("/usage/forecast");
|
||
} catch {
|
||
return null; // ohne Taskboard keine Prognose - die Verbrauchsansicht bleibt trotzdem
|
||
}
|
||
if (!p) return null;
|
||
|
||
const kacheln = erstelleElement("div", { klasse: "kachel-gitter" });
|
||
kacheln.append(
|
||
erstelleStatKachel("Prognose je Tag", formatiereKosten(p.totalPerDay)),
|
||
erstelleStatKachel("Prognose je Monat (30 Tage)", formatiereKosten(p.totalPerMonth)),
|
||
erstelleStatKachel("davon Aufgaben je Tag", formatiereKosten(p.tasksPerDay)),
|
||
erstelleStatKachel("sonstige Läufe je Tag (Ø 7 Tage)", formatiereKosten(p.otherPerDay))
|
||
);
|
||
|
||
const hinweise = [];
|
||
for (const w of p.budgetWarnings || []) {
|
||
hinweise.push(erstelleElement("div", {
|
||
klasse: "hinweis-box warnung",
|
||
text: `${w.agentId}: Die geplanten Aufgaben kosten laut Prognose ${formatiereKosten(w.costPerDay)} am Tag, `
|
||
+ `das Tagesbudget erlaubt ${formatiereKosten(w.budgetPerDay)}. Die späten Läufe eines Tages werden abgelehnt.`
|
||
}));
|
||
}
|
||
if (p.withoutBasis > 0) {
|
||
hinweise.push(erstelleElement("p", {
|
||
klasse: "gedaempft text-klein",
|
||
text: `${p.withoutBasis} Aufgabe(n) ohne Grundlage (kein Lauf, kein Modellpreis) – nicht in der Summe enthalten.`
|
||
}));
|
||
}
|
||
if (Number(p.oneOffOpen) > 0) {
|
||
hinweise.push(erstelleElement("p", {
|
||
klasse: "gedaempft text-klein",
|
||
text: `Offene einmalige Aufgaben kosten zusätzlich etwa ${formatiereKosten(p.oneOffOpen)}.`
|
||
}));
|
||
}
|
||
hinweise.push(erstelleElement("p", {
|
||
klasse: "gedaempft text-klein",
|
||
text: "Grundlage je Aufgabe: Mittelwert ihrer letzten Läufe, sonst der des Agenten, sonst eine Schätzung aus Promptlänge und Listenpreis (eher zu hoch)."
|
||
}));
|
||
|
||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||
tabelle.append(
|
||
erstelleElement("thead", {},
|
||
erstelleElement("tr", {},
|
||
erstelleElement("th", { text: "Aufgabe" }),
|
||
erstelleElement("th", { text: "Agent" }),
|
||
erstelleElement("th", { text: "Läufe/Tag", stil: "text-align: right;" }),
|
||
erstelleElement("th", { text: "je Lauf", stil: "text-align: right;" }),
|
||
erstelleElement("th", { text: "Grundlage" }),
|
||
erstelleElement("th", { text: "je Tag", stil: "text-align: right;" })
|
||
)
|
||
)
|
||
);
|
||
|
||
const tbody = erstelleElement("tbody");
|
||
for (const a of p.tasks || []) {
|
||
const grundlage = (GRUNDLAGE_TEXT[a.basis] || (() => a.basis))(a.sample);
|
||
tbody.append(
|
||
erstelleElement("tr", {},
|
||
erstelleElement("td", { text: a.oneOffOpen ? `${a.title} (einmalig)` : a.title }),
|
||
erstelleElement("td", { klasse: "text-klein", text: a.agentId || "—" }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: String(a.runsPerDay) }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: a.costPerRun == null ? "—" : formatiereKosten(a.costPerRun) }),
|
||
erstelleElement("td", { klasse: "gedaempft text-klein", text: grundlage }),
|
||
erstelleElement("td", { stil: "text-align: right;", text: formatiereKosten(a.costPerDay) })
|
||
)
|
||
);
|
||
}
|
||
tabelle.append(tbody);
|
||
|
||
return erstelleKarte("Kostenprognose", [kacheln, ...hinweise, tabelle]);
|
||
}
|
||
|
||
function erstelleStatKachel(titel, wert) {
|
||
const kachel = erstelleElement("div", { klasse: "stat-kachel karte" });
|
||
kachel.append(
|
||
erstelleElement("div", { klasse: "stat-titel gedaempft", text: titel }),
|
||
erstelleElement("div", { klasse: "stat-wert", text: wert })
|
||
);
|
||
return kachel;
|
||
}
|
||
|
||
function erstelleBalkenDiagramm(titel, datenMap, gesamt) {
|
||
const karte = erstelleKarte(titel);
|
||
const container = erstelleElement("div", { klasse: "balken-liste" });
|
||
|
||
const sortiert = [...datenMap.entries()].sort((a, b) => b[1] - a[1]);
|
||
|
||
for (const [name, wert] of sortiert) {
|
||
const prozent = gesamt > 0 ? ((wert / gesamt) * 100).toFixed(1) : "0";
|
||
|
||
const zeile = erstelleElement("div", { klasse: "balken-zeile" });
|
||
const meta = erstelleElement("div", { klasse: "balken-meta" });
|
||
meta.append(
|
||
erstelleElement("span", { klasse: "balken-name", text: name }),
|
||
erstelleElement("span", { klasse: "balken-wert gedaempft", text: `${formatiereTokens(wert)} (${prozent}%)` })
|
||
);
|
||
|
||
const spur = erstelleElement("div", { klasse: "balken-spur" });
|
||
const fuellung = erstelleElement("div", {
|
||
klasse: "balken-fuellung",
|
||
stil: `width: ${Math.max(1, Number(prozent))}%;`
|
||
});
|
||
spur.append(fuellung);
|
||
|
||
zeile.append(meta, spur);
|
||
container.append(zeile);
|
||
}
|
||
|
||
karte.append(container);
|
||
return karte;
|
||
}
|