@hachej/boring-governance 0.1.81 → 0.1.83

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.
@@ -1,5 +1,7 @@
1
1
  import { LoadCompanyAdminStatus, RenderCompanyAdminContent, CompanyAdminLabels } from '@hachej/boring-core/front';
2
2
  import * as react from 'react';
3
+ import { G as GovernanceUsageSummary } from '../usageContract-BNyGSg5T.js';
4
+ export { a as GovernanceUsageEntry } from '../usageContract-BNyGSg5T.js';
3
5
 
4
6
  interface GovernancePolicyStatus {
5
7
  state: 'disabled' | 'active' | 'invalid';
@@ -43,6 +45,27 @@ declare function GovernanceAdminView({ status }: {
43
45
  status: GovernanceMeResponse;
44
46
  }): react.JSX.Element;
45
47
 
48
+ interface GovernanceUsageMetersProps {
49
+ /** Route to fetch the usage summary from. Defaults to the governance plugin route. */
50
+ endpoint?: string;
51
+ /** Override the fetch implementation (tests / non-browser hosts). */
52
+ fetchImpl?: typeof fetch;
53
+ /** Fully override data loading; takes precedence over `endpoint`/`fetchImpl`. */
54
+ fetchUsageSummary?: () => Promise<GovernanceUsageSummary>;
55
+ /** Section heading. */
56
+ title?: string;
57
+ /** Supporting copy under the heading. */
58
+ description?: string;
59
+ className?: string;
60
+ }
61
+ /**
62
+ * Vertical list of per-model usage meters (plus an aggregate "All models" row
63
+ * when an aggregate cap is configured). Self-fetches from the governance
64
+ * usage-summary route by default; renders nothing when governance is disabled so
65
+ * a settings host can hide the section entirely.
66
+ */
67
+ declare function GovernanceUsageMeters({ endpoint, fetchImpl, fetchUsageSummary, title, description, className, }: GovernanceUsageMetersProps): react.JSX.Element | null;
68
+
46
69
  interface CreateGovernanceCompanyAdminOptions {
47
70
  fetchImpl?: typeof fetch;
48
71
  }
@@ -58,4 +81,4 @@ type GovernanceFrontPlugin = ((api: unknown) => void) & {
58
81
  };
59
82
  declare const governanceFrontPlugin: GovernanceFrontPlugin;
60
83
 
61
- export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
84
+ export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, GovernanceUsageMeters, type GovernanceUsageMetersProps, GovernanceUsageSummary, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
@@ -102,8 +102,109 @@ function GovernanceAdminView({ status }) {
102
102
  ] }) });
103
103
  }
104
104
 
105
+ // src/front/GovernanceUsageMeters.tsx
106
+ import * as React from "react";
107
+ import { Button as Button2, EmptyState, ErrorState, LoadingState, Meter } from "@hachej/boring-ui-kit";
108
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
109
+ var DEFAULT_ENDPOINT = "/api/v1/governance/usage-summary";
110
+ var MICROS_PER_EUR = 1e6;
111
+ function formatEur(micros) {
112
+ return new Intl.NumberFormat(void 0, { style: "currency", currency: "EUR", maximumFractionDigits: 2 }).format(micros / MICROS_PER_EUR);
113
+ }
114
+ function formatReset(resetsAt) {
115
+ const date = new Date(resetsAt);
116
+ if (Number.isNaN(date.getTime())) return null;
117
+ return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric", timeZone: "UTC" }).format(date);
118
+ }
119
+ function toneForPct(pctUsed) {
120
+ if (pctUsed === null) return "default";
121
+ if (pctUsed >= 90) return "danger";
122
+ if (pctUsed >= 75) return "warning";
123
+ return "default";
124
+ }
125
+ function UsageMeterRow({ entry }) {
126
+ const reset = formatReset(entry.resetsAt);
127
+ const hasCap = entry.budgetMicros !== null;
128
+ const consumedMicros = entry.usedMicros + entry.heldMicros;
129
+ return /* @__PURE__ */ jsxs2("div", { "data-slot": "governance-usage-row", className: "flex flex-col gap-1.5 py-3", children: [
130
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-baseline justify-between gap-3", children: [
131
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-sm font-medium text-foreground", children: entry.label }),
132
+ /* @__PURE__ */ jsx2("span", { className: "shrink-0 text-xs font-medium tabular-nums text-muted-foreground", children: entry.pctUsed === null ? "No cap" : `${entry.pctUsed}% used` })
133
+ ] }),
134
+ hasCap ? /* @__PURE__ */ jsx2(Meter, { value: entry.pctUsed ?? 0, tone: toneForPct(entry.pctUsed), label: `${entry.label} usage` }) : null,
135
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-baseline justify-between gap-3 text-xs text-muted-foreground", children: [
136
+ /* @__PURE__ */ jsx2("span", { children: reset ? `Resets ${reset}` : "Monthly reset" }),
137
+ /* @__PURE__ */ jsx2("span", { className: "tabular-nums", children: hasCap ? `${formatEur(consumedMicros)} of ${formatEur(entry.budgetMicros)}` : formatEur(consumedMicros) })
138
+ ] })
139
+ ] });
140
+ }
141
+ function GovernanceUsageMeters({
142
+ endpoint = DEFAULT_ENDPOINT,
143
+ fetchImpl,
144
+ fetchUsageSummary,
145
+ title = "Model usage",
146
+ description = "Your consumption this period against the caps set by your organization.",
147
+ className
148
+ }) {
149
+ const [state, setState] = React.useState({ status: "loading" });
150
+ const load = React.useCallback(async (signal) => {
151
+ setState({ status: "loading" });
152
+ try {
153
+ const summary2 = fetchUsageSummary ? await fetchUsageSummary() : await fetchSummary(endpoint, fetchImpl, signal);
154
+ if (signal?.aborted) return;
155
+ setState({ status: "ready", summary: summary2 });
156
+ } catch (error) {
157
+ if (signal?.aborted) return;
158
+ setState({ status: "error", message: error instanceof Error ? error.message : "Failed to load usage" });
159
+ }
160
+ }, [endpoint, fetchImpl, fetchUsageSummary]);
161
+ React.useEffect(() => {
162
+ const controller = new AbortController();
163
+ void load(controller.signal);
164
+ return () => controller.abort();
165
+ }, [load]);
166
+ if (state.status === "loading") {
167
+ return /* @__PURE__ */ jsxs2("section", { "data-slot": "governance-usage-meters", className, children: [
168
+ /* @__PURE__ */ jsx2(UsageHeader, { title, description }),
169
+ /* @__PURE__ */ jsx2(LoadingState, { label: "Loading usage\u2026" })
170
+ ] });
171
+ }
172
+ if (state.status === "error") {
173
+ return /* @__PURE__ */ jsxs2("section", { "data-slot": "governance-usage-meters", className, children: [
174
+ /* @__PURE__ */ jsx2(UsageHeader, { title, description }),
175
+ /* @__PURE__ */ jsx2(
176
+ ErrorState,
177
+ {
178
+ title: "Couldn\u2019t load usage",
179
+ description: state.message,
180
+ actions: /* @__PURE__ */ jsx2(Button2, { variant: "outline", size: "sm", onClick: () => void load(), children: "Retry" })
181
+ }
182
+ )
183
+ ] });
184
+ }
185
+ const { summary } = state;
186
+ if (!summary.enabled) return null;
187
+ const rows = summary.aggregate ? [summary.aggregate, ...summary.models] : summary.models;
188
+ return /* @__PURE__ */ jsxs2("section", { "data-slot": "governance-usage-meters", className, children: [
189
+ /* @__PURE__ */ jsx2(UsageHeader, { title, description }),
190
+ rows.length === 0 ? /* @__PURE__ */ jsx2(EmptyState, { title: "No usage caps configured", description: "Your organization has not set per-model usage limits yet." }) : /* @__PURE__ */ jsx2("div", { className: "divide-y divide-border rounded-xl border border-border bg-card px-4", children: rows.map((entry) => /* @__PURE__ */ jsx2(UsageMeterRow, { entry }, `${entry.provider}\0${entry.id}`)) })
191
+ ] });
192
+ }
193
+ function UsageHeader({ title, description }) {
194
+ return /* @__PURE__ */ jsxs2("header", { className: "mb-3", children: [
195
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold text-foreground", children: title }),
196
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-muted-foreground", children: description })
197
+ ] });
198
+ }
199
+ async function fetchSummary(endpoint, fetchImpl, signal) {
200
+ const doFetch = fetchImpl ?? fetch;
201
+ const response = await doFetch(endpoint, { credentials: "include", signal });
202
+ if (!response.ok) throw new Error(`Usage summary failed: HTTP ${response.status}`);
203
+ return await response.json();
204
+ }
205
+
105
206
  // src/front/index.tsx
106
- import { jsx as jsx2 } from "react/jsx-runtime";
207
+ import { jsx as jsx3 } from "react/jsx-runtime";
107
208
  function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
108
209
  const loadStatus = async () => {
109
210
  const response = await fetchImpl("/api/v1/governance/me", { credentials: "include" });
@@ -120,7 +221,7 @@ function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
120
221
  details: body
121
222
  };
122
223
  };
123
- const renderContent = (status) => /* @__PURE__ */ jsx2(GovernanceAdminView, { status: status.details });
224
+ const renderContent = (status) => /* @__PURE__ */ jsx3(GovernanceAdminView, { status: status.details });
124
225
  const labels = {
125
226
  menuLabel: "Company Admin",
126
227
  pageTitle: "Company Admin",
@@ -134,6 +235,7 @@ Object.defineProperty(governanceFrontPlugin, "pluginLabel", { value: "Governance
134
235
  var front_default = governanceFrontPlugin;
135
236
  export {
136
237
  GovernanceAdminView,
238
+ GovernanceUsageMeters,
137
239
  createGovernanceCompanyAdmin,
138
240
  front_default as default
139
241
  };
@@ -2,6 +2,8 @@ import * as _hachej_boring_agent_server from '@hachej/boring-agent/server';
2
2
  import { AgentMeteringSink, RegisterAgentRoutesOptions } from '@hachej/boring-agent/server';
3
3
  import { CoreConfig } from '@hachej/boring-core/shared';
4
4
  import { CoreWorkspaceAgentServerPlugin } from '@hachej/boring-core/app/server';
5
+ import { G as GovernanceUsageSummary } from '../usageContract-BNyGSg5T.js';
6
+ export { a as GovernanceUsageEntry } from '../usageContract-BNyGSg5T.js';
5
7
  import { PostgresBudgetReservationStore } from '@hachej/boring-core/server';
6
8
  import { FastifyRequest } from 'fastify';
7
9
 
@@ -74,6 +76,27 @@ interface LoadGovernancePolicyOptions {
74
76
  }
75
77
  declare function loadGovernancePolicy({ env, nodeEnv, config, }?: LoadGovernancePolicyOptions): Promise<GovernanceLoadResult>;
76
78
 
79
+ /**
80
+ * Minimal read-only view over the budget reservation store used to compute the
81
+ * usage summary. `PostgresBudgetReservationStore.getSpendSnapshot` satisfies
82
+ * this shape structurally, so the summary reuses the exact same used/held
83
+ * accounting the admission path applies during a reservation.
84
+ */
85
+ interface GovernanceUsageSpendReader {
86
+ getSpendSnapshot(query: {
87
+ scope: 'user';
88
+ userId: string;
89
+ } | {
90
+ scope: 'model';
91
+ userId: string;
92
+ provider: string;
93
+ model: string;
94
+ }): Promise<{
95
+ usedMicros: number;
96
+ heldMicros: number;
97
+ periodEnd: Date;
98
+ }>;
99
+ }
77
100
  type GovernancePolicyErrorCode = 'disabled' | 'invalid' | 'denied' | 'not_allowed';
78
101
  type CompanyContextAccess = 'none' | 'readonly' | 'readwrite';
79
102
  declare class GovernancePolicyError extends Error {
@@ -123,6 +146,13 @@ declare class GovernanceService {
123
146
  companyContextRules(user: GovernanceUserLike): string[];
124
147
  companyContextAccessForUser(user: GovernanceUserLike | null | undefined): CompanyContextAccess;
125
148
  companyContextWorkspaceId(): string | null;
149
+ /**
150
+ * Per-model consumed/remaining usage for the caller, plus an aggregate row
151
+ * when an aggregate (per-user) cap is configured. `used`/`held` are read from
152
+ * the same store accounting the admission path uses; `resetsAt` is the store's
153
+ * period boundary (never hardcoded).
154
+ */
155
+ getUsageSummary(user: GovernanceUserLike | null | undefined, reader: GovernanceUsageSpendReader): Promise<GovernanceUsageSummary>;
126
156
  me(user: GovernanceUserLike | null | undefined): GovernanceMeResponse;
127
157
  }
128
158
 
@@ -176,6 +206,8 @@ interface CreateGovernanceResult {
176
206
  }
177
207
  declare function createGovernance(config: CoreConfig): Promise<CreateGovernanceResult>;
178
208
  declare function createGovernanceModelFilter(service: GovernanceService): RegisterAgentRoutesOptions['filterModels'];
179
- declare function createGovernanceServerPlugin(service: GovernanceService): CoreWorkspaceAgentServerPlugin;
209
+ declare function createGovernanceServerPlugin(service: GovernanceService, options?: {
210
+ getUsageReader?: () => GovernanceUsageSpendReader | undefined;
211
+ }): CoreWorkspaceAgentServerPlugin;
180
212
 
181
- export { type BuildGovernanceOptions, type CompanyContextRootResolver, type CreateGovernanceFilesystemBindingsOptions, type CreateGovernanceResult, GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV, GOVERNANCE_POLICY_PATH_ENV, type GovernanceFilesystemBindingContext, type GovernanceLoadResult, type GovernanceMeResponse, type GovernanceModelGrant, type GovernancePolicy, GovernancePolicyError, type GovernancePolicyErrorCode, type GovernancePolicyStatus, GovernanceService, type GovernanceUserLike, type GovernanceUserPolicy, type ServedModelLike, type TenantRole, buildGovernanceService, createDefaultCompanyContextRootResolver, createGovernance, createGovernanceFilesystemBindings, createGovernanceMeteringSink, createGovernanceModelFilter, createGovernanceServerPlugin, loadGovernancePolicy, normalizePolicyEmail, validateGovernancePolicy };
213
+ export { type BuildGovernanceOptions, type CompanyContextRootResolver, type CreateGovernanceFilesystemBindingsOptions, type CreateGovernanceResult, GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV, GOVERNANCE_POLICY_PATH_ENV, type GovernanceFilesystemBindingContext, type GovernanceLoadResult, type GovernanceMeResponse, type GovernanceModelGrant, type GovernancePolicy, GovernancePolicyError, type GovernancePolicyErrorCode, type GovernancePolicyStatus, GovernanceService, type GovernanceUsageSpendReader, GovernanceUsageSummary, type GovernanceUserLike, type GovernanceUserPolicy, type ServedModelLike, type TenantRole, buildGovernanceService, createDefaultCompanyContextRootResolver, createGovernance, createGovernanceFilesystemBindings, createGovernanceMeteringSink, createGovernanceModelFilter, createGovernanceServerPlugin, loadGovernancePolicy, normalizePolicyEmail, validateGovernancePolicy };
@@ -1,5 +1,6 @@
1
1
  // src/server/index.ts
2
2
  import path3 from "path";
3
+ import { PostgresBudgetReservationStore as PostgresBudgetReservationStore2 } from "@hachej/boring-core/server";
3
4
 
4
5
  // src/server/loadPolicy.ts
5
6
  import { readFile, stat } from "fs/promises";
@@ -379,6 +380,42 @@ var GovernanceService = class {
379
380
  companyContextWorkspaceId() {
380
381
  return this.enabledPolicy()?.tenant.companyContextWorkspaceId ?? null;
381
382
  }
383
+ /**
384
+ * Per-model consumed/remaining usage for the caller, plus an aggregate row
385
+ * when an aggregate (per-user) cap is configured. `used`/`held` are read from
386
+ * the same store accounting the admission path uses; `resetsAt` is the store's
387
+ * period boundary (never hardcoded).
388
+ */
389
+ async getUsageSummary(user, reader) {
390
+ if (!this.loaded.enabled) return { enabled: false, currency: "EUR", models: [], aggregate: null };
391
+ const userPolicy = this.userPolicy(user);
392
+ const userId = user?.id;
393
+ if (!userPolicy || !userId) return { enabled: true, currency: "EUR", models: [], aggregate: null };
394
+ const models = [];
395
+ for (const grant of userPolicy.models) {
396
+ const snapshot = await reader.getSpendSnapshot({ scope: "model", userId, provider: grant.provider, model: grant.id });
397
+ models.push(buildUsageEntry({
398
+ provider: grant.provider,
399
+ id: grant.id,
400
+ label: grant.id,
401
+ budgetMicros: grant.monthlyBudgetMicros,
402
+ snapshot
403
+ }));
404
+ }
405
+ let aggregate = null;
406
+ const aggregateBudgetMicros = userPolicy.budgets.monthlyMicros;
407
+ if (aggregateBudgetMicros !== null) {
408
+ const snapshot = await reader.getSpendSnapshot({ scope: "user", userId });
409
+ aggregate = buildUsageEntry({
410
+ provider: "",
411
+ id: "__all__",
412
+ label: "All models",
413
+ budgetMicros: aggregateBudgetMicros,
414
+ snapshot
415
+ });
416
+ }
417
+ return { enabled: true, currency: "EUR", models, aggregate };
418
+ }
382
419
  me(user) {
383
420
  const role = this.roleForUser(user);
384
421
  const admin = role === "admin";
@@ -414,16 +451,43 @@ var GovernanceService = class {
414
451
  };
415
452
  }
416
453
  };
454
+ function buildUsageEntry(args) {
455
+ const { provider, id, label, budgetMicros, snapshot } = args;
456
+ const usedMicros = Math.max(0, snapshot.usedMicros);
457
+ const heldMicros = Math.max(0, snapshot.heldMicros);
458
+ const consumedMicros = usedMicros + heldMicros;
459
+ const base = { provider, id, label, usedMicros, heldMicros, resetsAt: snapshot.periodEnd.toISOString() };
460
+ if (budgetMicros === null) {
461
+ return { ...base, budgetMicros: null, remainingMicros: null, pctUsed: null };
462
+ }
463
+ if (budgetMicros <= 0) {
464
+ return { ...base, budgetMicros, remainingMicros: 0, pctUsed: 100 };
465
+ }
466
+ return {
467
+ ...base,
468
+ budgetMicros,
469
+ remainingMicros: Math.max(0, budgetMicros - consumedMicros),
470
+ pctUsed: Math.min(100, Math.round(consumedMicros / budgetMicros * 100))
471
+ };
472
+ }
417
473
  function createGovernanceService(result) {
418
474
  return new GovernanceService(result);
419
475
  }
420
476
 
421
477
  // src/server/routes.ts
422
- function governanceRoutes(service) {
478
+ function governanceRoutes(service, options = {}) {
423
479
  return async (app) => {
424
480
  app.get("/api/v1/governance/me", async (request) => {
425
481
  return service.me(request.user ?? null);
426
482
  });
483
+ app.get("/api/v1/governance/usage-summary", async (request) => {
484
+ const user = request.user ?? null;
485
+ const reader = options.getUsageReader?.();
486
+ if (!user || !reader) {
487
+ return { enabled: service.isEnabled(), currency: "EUR", models: [], aggregate: null };
488
+ }
489
+ return service.getUsageSummary({ id: user.id, email: user.email, emailVerified: user.emailVerified }, reader);
490
+ });
427
491
  };
428
492
  }
429
493
 
@@ -543,6 +607,7 @@ async function reconcileCompanyContextWorkspace(app, service) {
543
607
  import { ErrorCode } from "@hachej/boring-agent/shared";
544
608
  import { ModelBudgetExceededError, PostgresBudgetReservationStore, UserBudgetExceededError } from "@hachej/boring-core/server";
545
609
  var DEFAULT_HOLD_TTL_SECONDS = 60 * 60;
610
+ var GOVERNANCE_ELIGIBLE_LEGACY_SOURCES = ["pi-chat", "pi-chat-fallback", "pi-chat-expired"];
546
611
  function authRequiredError() {
547
612
  return Object.assign(new Error("authentication required"), { statusCode: 401, code: ErrorCode.enum.UNAUTHORIZED });
548
613
  }
@@ -561,7 +626,7 @@ function runKey(input) {
561
626
  }
562
627
  function createGovernanceMeteringSink(options) {
563
628
  let store;
564
- const getStore = () => store ??= new PostgresBudgetReservationStore(options.getDb(), { eligibleLegacySources: ["pi-chat", "pi-chat-fallback", "pi-chat-expired"] });
629
+ const getStore = () => store ??= new PostgresBudgetReservationStore(options.getDb(), { eligibleLegacySources: GOVERNANCE_ELIGIBLE_LEGACY_SOURCES });
565
630
  const admissionsByRun = /* @__PURE__ */ new Map();
566
631
  const holdTtlSeconds = options.holdTtlSeconds ?? DEFAULT_HOLD_TTL_SECONDS;
567
632
  async function reserveGovernance(input) {
@@ -1233,12 +1298,17 @@ function defaultAdminMutationsAllowed(options) {
1233
1298
  }
1234
1299
  async function createGovernance(config) {
1235
1300
  const service = await buildGovernanceService({ config });
1301
+ let usageDb;
1302
+ const getUsageReader = () => usageDb ? new PostgresBudgetReservationStore2(usageDb(), { eligibleLegacySources: GOVERNANCE_ELIGIBLE_LEGACY_SOURCES }) : void 0;
1236
1303
  return {
1237
1304
  service,
1238
1305
  status: service.policyStatus(),
1239
- serverPlugin: createGovernanceServerPlugin(service),
1306
+ serverPlugin: createGovernanceServerPlugin(service, { getUsageReader }),
1240
1307
  filterModels: createGovernanceModelFilter(service),
1241
- createMeteringSink: (delegate, getDb) => createGovernanceMeteringSink({ service, delegate, getDb }),
1308
+ createMeteringSink: (delegate, getDb) => {
1309
+ usageDb = getDb;
1310
+ return createGovernanceMeteringSink({ service, delegate, getDb });
1311
+ },
1242
1312
  getFilesystemBindings: (options = {}) => createGovernanceFilesystemBindings(service, {
1243
1313
  ...options,
1244
1314
  resolveCompanyContextRoot: options.resolveCompanyContextRoot ?? createDefaultCompanyContextRootResolver(),
@@ -1263,12 +1333,12 @@ function createGovernanceModelFilter(service) {
1263
1333
  return { models: allowedModels, defaultModel: allowedDefault };
1264
1334
  };
1265
1335
  }
1266
- function createGovernanceServerPlugin(service) {
1336
+ function createGovernanceServerPlugin(service, options = {}) {
1267
1337
  return {
1268
1338
  id: "full-app-governance",
1269
1339
  label: "Full-app governance",
1270
1340
  routes: async (app) => {
1271
- await app.register(governanceRoutes(service));
1341
+ await app.register(governanceRoutes(service, { getUsageReader: options.getUsageReader }));
1272
1342
  app.addHook("onReady", async () => {
1273
1343
  await reconcileCompanyContextWorkspace(app, service);
1274
1344
  });
@@ -0,0 +1,32 @@
1
+ interface GovernanceUsageEntry {
2
+ /** Provider id (empty string for the synthetic aggregate row). */
3
+ provider: string;
4
+ /** Model id, or "__all__" for the aggregate ("All models") row. */
5
+ id: string;
6
+ /** Human-facing label. */
7
+ label: string;
8
+ /** Settled spend in micros for the current period. */
9
+ usedMicros: number;
10
+ /** In-flight held spend in micros for the current period. */
11
+ heldMicros: number;
12
+ /** Configured cap in micros, or null when no cap is configured. */
13
+ budgetMicros: number | null;
14
+ /** Remaining micros (max(0, budget - used - held)), or null when no cap. */
15
+ remainingMicros: number | null;
16
+ /** Percentage of the cap consumed (0..100), or null when no cap. */
17
+ pctUsed: number | null;
18
+ /** ISO timestamp of the budget-period reset boundary. */
19
+ resetsAt: string;
20
+ }
21
+ interface GovernanceUsageSummary {
22
+ /** Whether governance is enabled for this host. */
23
+ enabled: boolean;
24
+ /** Currency of the underlying budgets. */
25
+ currency: 'EUR';
26
+ /** Per-model usage rows for the caller's allowed models. */
27
+ models: GovernanceUsageEntry[];
28
+ /** Aggregate ("All models") row when an aggregate cap is configured, else null. */
29
+ aggregate: GovernanceUsageEntry | null;
30
+ }
31
+
32
+ export type { GovernanceUsageSummary as G, GovernanceUsageEntry as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-governance",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -42,10 +42,10 @@
42
42
  "peerDependencies": {
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
- "@hachej/boring-agent": "0.1.81",
46
- "@hachej/boring-bash": "0.1.81",
47
- "@hachej/boring-core": "0.1.81",
48
- "@hachej/boring-ui-kit": "0.1.81"
45
+ "@hachej/boring-agent": "0.1.83",
46
+ "@hachej/boring-bash": "0.1.83",
47
+ "@hachej/boring-core": "0.1.83",
48
+ "@hachej/boring-ui-kit": "0.1.83"
49
49
  },
50
50
  "dependencies": {
51
51
  "proper-lockfile": "^4.1.2",
@@ -67,10 +67,10 @@
67
67
  "tsx": "^4.20.6",
68
68
  "typescript": "~6.0.3",
69
69
  "vitest": "^4.1.9",
70
- "@hachej/boring-agent": "0.1.81",
71
- "@hachej/boring-ui-kit": "0.1.81",
72
- "@hachej/boring-core": "0.1.81",
73
- "@hachej/boring-bash": "0.1.81"
70
+ "@hachej/boring-agent": "0.1.83",
71
+ "@hachej/boring-bash": "0.1.83",
72
+ "@hachej/boring-core": "0.1.83",
73
+ "@hachej/boring-ui-kit": "0.1.83"
74
74
  },
75
75
  "scripts": {
76
76
  "build": "tsup",