@hachej/boring-governance 0.1.80 → 0.1.82
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/front/index.d.ts +24 -1
- package/dist/front/index.js +104 -2
- package/dist/server/index.d.ts +34 -2
- package/dist/server/index.js +76 -6
- package/dist/usageContract-BNyGSg5T.d.ts +32 -0
- package/package.json +9 -9
package/dist/front/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/front/index.js
CHANGED
|
@@ -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
|
|
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__ */
|
|
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
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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:
|
|
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) =>
|
|
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.
|
|
3
|
+
"version": "0.1.82",
|
|
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.
|
|
46
|
-
"@hachej/boring-bash": "0.1.
|
|
47
|
-
"@hachej/boring-
|
|
48
|
-
"@hachej/boring-
|
|
45
|
+
"@hachej/boring-agent": "0.1.82",
|
|
46
|
+
"@hachej/boring-bash": "0.1.82",
|
|
47
|
+
"@hachej/boring-ui-kit": "0.1.82",
|
|
48
|
+
"@hachej/boring-core": "0.1.82"
|
|
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-
|
|
71
|
-
"@hachej/boring-
|
|
72
|
-
"@hachej/boring-core": "0.1.
|
|
73
|
-
"@hachej/boring-
|
|
70
|
+
"@hachej/boring-ui-kit": "0.1.82",
|
|
71
|
+
"@hachej/boring-agent": "0.1.82",
|
|
72
|
+
"@hachej/boring-core": "0.1.82",
|
|
73
|
+
"@hachej/boring-bash": "0.1.82"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"build": "tsup",
|