Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/src/usage.ts

Snapshot at 34e3175

Containing directory

3,872 bytes · typescriptView raw
1import type { Brand, ProjectState, Review, Usage, UsageSources } from "./contracts.ts";
2
3export const totalTokens = (usage: Usage) => usage.inputTokens + usage.outputTokens;
4export function tokenNumber(value: number): string {
5  if (value < 100000) return value.toLocaleString("en-US");
6  return `${Number((value / (value >= 1000000 ? 1000000 : 1000)).toFixed(1))}${value >= 1000000 ? "M" : "k"}`;
7}
8export const usageValues = (sources?: UsageSources) =>
9  [sources?.reported, sources?.metered].filter((value): value is Usage => !!value);
10export function usageTooltip(usage: Usage, agent: string, brand: Brand): string {
11  return usage.source === "metered"
12    ? `Metered by AI Gateway for ${agent}.`
13    : `Reported by ${agent} through ${brand.name}. ${brand.name} can't verify it.`;
14}
15/** The current entrant's budget controls promotion; a review's older snapshot never overrides it. */
16export function usagePromotionCaption(review: Review, state: ProjectState): string {
17  const race = state.races?.find((r) => r.packet === review.packet);
18  if (!race || race.tokenBudget == null) return "";
19  const entrant = race.entrants.find(
20    (e) => e.agent === review.agent && e.repoName === review.repoName,
21  );
22  if (!entrant) return "";
23  if (entrant.budget === "over") {
24    const source = entrant.usage?.metered ? "metered" : "reported";
25    return `Over its token budget (${source}). It can't win this race.`;
26  }
27  if (entrant.budget === "unchecked" || !usageValues(entrant.usage).length)
28    return "Report usage first: this race has a token budget.";
29  if (entrant.budget === undefined) return "Budget status unavailable. Refresh before promoting.";
30  return "";
31}
32/** Latest source per binding, not sum of live claims and their review snapshots. */
33export function agentUsageToday(
34  state: ProjectState,
35  agent: string,
36  now = Date.now(),
37): UsageSources {
38  const latest = new Map<string, Usage>();
39  const add = (packet: string, repo: string, sources?: UsageSources) => {
40    for (const value of usageValues(sources)) {
41      const at = Date.parse(value.at);
42      if (!Number.isFinite(at) || at > now) continue;
43      const key = JSON.stringify([packet, repo, value.source]);
44      if (!latest.has(key) || Date.parse(latest.get(key)!.at) < at) latest.set(key, value);
45    }
46  };
47  for (const claim of state.claims.filter((c) => c.active && c.agent === agent))
48    add(claim.packet, claim.repoName, claim.usage);
49  for (const race of state.races ?? [])
50    for (const entrant of race.entrants.filter((e) => e.agent === agent))
51      add(race.packet, entrant.repoName, entrant.usage);
52  for (const review of state.reviews.filter((r) => r.agent === agent))
53    add(review.packet, review.repoName, review.usageAtReview);
54  const start = new Date(now);
55  start.setHours(0, 0, 0, 0);
56  const result: UsageSources = { reported: null, metered: null };
57  for (const value of latest.values()) {
58    if (Date.parse(value.at) < start.getTime()) continue;
59    const sum = result[value.source];
60    result[value.source] = {
61      inputTokens: (sum?.inputTokens ?? 0) + value.inputTokens,
62      outputTokens: (sum?.outputTokens ?? 0) + value.outputTokens,
63      source: value.source,
64      at: !sum || sum.at < value.at ? value.at : sum.at,
65    };
66  }
67  return result;
68}
69/** Graph lanes are binding-specific; historical promotions keep the review snapshot. */
70export function laneUsage(
71  state: ProjectState,
72  lane: { packet: string; repoName: string; review?: Review; promoted?: unknown },
73): UsageSources | undefined {
74  if (lane.promoted) return lane.review?.usageAtReview;
75  return (
76    state.races
77      ?.find((r) => r.packet === lane.packet)
78      ?.entrants.find((e) => e.repoName === lane.repoName)?.usage ??
79    state.claims.find((c) => c.active && c.packet === lane.packet && c.repoName === lane.repoName)
80      ?.usage ??
81    lane.review?.usageAtReview
82  );
83}
84