feat(ai): resolve an AI usage level per record and publish it as ai.usage

Three levels (frugal, standard, full) with an operator ceiling and default
and a per-account choice within them, resolved in the capability layer for
the record and published on GET /api/auth/me as ai.usage: the level in
effect, the stored choice, the bounds, why (user, default, admin_cap), the
three options with whether each is allowed and an estimate of tokens per
day scaled from the record's last seven full days of coach_usage, and the
measured mean. A delegate sees the level, never the owner's spend.

PUT /api/auth/me/ai-usage stores the account's own choice or null for the
default, refusing a level above the ceiling with 422 ai.usage.aboveCeiling
and meta.maxLevel. The new supplementLabel capability needs standard or
above; below it the capability is unavailable with the new usage_level
reason, refused with 403 ai.usage.levelTooLow and meta.requiredLevel.

Refs #1217
This commit is contained in:
Marc-André Bombeck committed 2026-10-15 22:49:01 +02:00
1 parent e573f28ae8
commit 6f673a7b84
22 files changed
+1342 -84

No files matched your search

+20
View File
@@ -9,6 +9,7 @@ import type {
} from "@/lib/ai/capabilities/types";
import { PROVIDER_DEFAULT_TIMEOUT_MS } from "@/lib/ai/effective-timeout";
import type { CoachReasoningState } from "@/lib/ai/reasoning/resolve";
import type { AiUsageState } from "@/lib/ai/usage-level";
/**
* The web reader for the `ai` block on `GET /api/auth/me`.
@@ -85,6 +86,25 @@ function useCoachReasoningInner(): CoachReasoningState | null {
return user?.coachReasoning ?? null;
}
/**
* v1.43 (#1217) — the record's AI usage level (`ai.usage` on
* `GET /api/auth/me`): the level in effect, the person's choice, the
* operator's bounds, the three options with their estimates. `null` while it
* is not known or on a server that does not publish it; the setting is then
* not shown. Render from it; never compute an estimate or an allowance here.
*/
export function useAiUsage(): AiUsageState | null {
const hasClient = useQueryClientMounted();
if (!hasClient) return null;
// eslint-disable-next-line react-hooks/rules-of-hooks
return useAiUsageInner();
}
function useAiUsageInner(): AiUsageState | null {
const { user } = useAuth();
return user?.ai?.usage ?? null;
}
/**
* The server's answer for one capability, or `null` while there is none yet
* (`/me` loading, no signed-in account, no query client). For the few places