Files
jschoubben e8c3ea8979 model-usage: the vendor-neutral usage store (ADR 0054)
The home ADR 0050 left open for a usage reading. mesh-control is a CLI and
cannot consume events, so the store that keeps the current usage picture is a
MODULE — the audit-logger's sibling: it consumes `module.*.usage.*` and upserts
each reading into its own provisioned postgres store, latest per
(licence, consumer, period, metric), in the clear. One vendor-neutral table
holds BOTH grains; they differ only in `consumer` (the holding module for the
licence grain, the session for the finer one). The consumer creates its table
on startup and, as a restart-until-ready service, self-heals rather than
gating the apply on a run-once that must reach a provider over the overlay.

The vendor->row normalisation moves into the adapter, as ADR 0054 requires:
anthropic-manager (licence grain, utilization%) and anthropic-consumer (session
grain, token/cost) now emit already-normalised { rows: UsageRow[], raw } on
their existing keys, so the store stays vendor-blind.

Proven end to end by the mesh-lab model-usage bed (green): a usage event
emitted into the mesh is upserted at both grains, latest-per-key, in the clear.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-07 04:07:26 +02:00

33 lines
1.2 KiB
TypeScript

// model-usage's tools — its own read-only query surface over the usage store (novox/hq ADR 0039:
// the tools and the client they call live in the module). The store is opened from the environment;
// when it cannot be (no DATABASE_URL), the module contributes no tools rather than failing the whole
// tool runtime — the postgres precedent.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { UsageStore } from "../store.js";
export function getUsageTools(store: UsageStore): ToolDefinition[] {
return [
{
name: "model_usage_current",
description:
"The latest model-usage reading per (licence, consumer, period, metric), across both grains, optionally restricted to one licence.",
input: {
licence: { type: "string", description: "restrict to one licence (optional)" },
},
run: async (args) => {
const licence = args.licence ? String(args.licence) : undefined;
return { usage: await store.current(licence) };
},
},
];
}
registerModuleTools("model-usage", (env) => {
try {
return getUsageTools(UsageStore.fromEnv(env));
} catch {
return [];
}
});