feat(api): structured ?format=json for the self-service usage endpoint (#11190)

* feat(api): structured ?format=json for the self-service usage endpoint

GET /api/usage/om-usage already let any key read its own usage — personal
daily/weekly USD limits and the provider quota snapshot — but only as
text/plain, which a UI cannot parse safely. OmniCopilot issue #8 asks exactly
for this surface.

Adds ?format=json, returning the ApiKeyUsageLimitStatus + UsageSnapshot the
text is rendered from. Text and JSON share the same collectors
(collectUsageSnapshots, getApiKeyUsageLimitStatus), so the two can never
disagree about a number. The response is a discriminated union: a key without
allowUsageCommand (403) or an invalid key (401) returns
{ allowed:false, error:{message} }, distinct from allowed:true with empty
sections — the state a panel must render as "nothing learned yet", not a
refusal. Text form unchanged; without ?format the contract is untouched.

The endpoint was previously missing from API_REFERENCE.md; it now has a
section documenting both forms, the allowUsageCommand gate, and the
self-service auth model (caller's own key, not requireManagementAuth).

Regression guards in tests/unit/usage-command-json-format.test.ts (4 tests:
json shape, text default preserved, structured 403, sanitized 401 with no
stack trace). Existing internal-usage-command suite still 12/12.

* chore(changelog): correct the fragment to the real PR number (#11190)

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-22 22:23:17 -03:00
committed by GitHub
parent 62ab93d789
commit eb9fa33ee7
4 changed files with 257 additions and 5 deletions

View File

@@ -13,7 +13,7 @@ const TEXT_PLAIN_HEADERS = { "Content-Type": "text/plain; charset=utf-8" } as co
type JsonRecord = Record<string, unknown>;
interface UsageCommandApiKeyMetadata {
export interface UsageCommandApiKeyMetadata {
id: string;
name?: string;
allowedConnections?: string[] | null;
@@ -31,7 +31,7 @@ interface ProviderConnectionLike {
quotaWindowThresholds?: Record<string, number> | null;
}
interface UsageSnapshot {
export interface UsageSnapshot {
connectionId: string;
provider: string;
plan: unknown;
@@ -39,7 +39,7 @@ interface UsageSnapshot {
quotaWindowThresholds?: Record<string, number> | null;
}
interface UsageCommandSelection {
export interface UsageCommandSelection {
preferredProvider?: string | null;
preferredConnectionId?: string | null;
}
@@ -258,7 +258,7 @@ function snapshotFromConnection(
};
}
async function collectUsageSnapshots(
export async function collectUsageSnapshots(
metadata: UsageCommandApiKeyMetadata,
deps: RequiredDeps
): Promise<UsageSnapshot[]> {
@@ -525,6 +525,52 @@ function appendQuotaBlock(
lines.push(`⏱ reset in ${formatResetIn(getResetAt(match?.quota ?? null), now)}`);
}
/**
* Structured form of the usage command — what {@link buildUsageCommandText}
* renders as text, exposed as data for API consumers (the OmniCopilot panel
* asks for it via `?format=json`). Text and JSON share the exact same
* collectors, so the two can never disagree about a number.
*
* The key design constraint is the 403 case: a key without `allowUsageCommand`
* must reach the client as a *structured* reason, not a bare text error — a
* caller rendering a usage panel has to be able to tell "the server does not
* know your limits yet" apart from "this key may not ask".
*/
/** Discriminated so the caller never reads a data field off a refusal:
* `allowed:false` carries only `error`; `allowed:true` carries the data. */
export type UsageCommandJson =
| { allowed: false; error: { message: string } }
| {
allowed: true;
/** Present only when the key opted into per-key usage limits. */
personal: unknown | null;
/** The selected provider snapshot, or null when nothing is cached. */
provider: UsageSnapshot | null;
};
export async function buildUsageCommandJson(
metadata: UsageCommandApiKeyMetadata,
deps: InternalUsageCommandDeps = {},
selection: UsageCommandSelection = {}
): Promise<UsageCommandJson> {
const resolvedDeps = await normalizeDeps(deps);
const personal =
metadata.usageLimitEnabled === true
? await resolvedDeps.getApiKeyUsageLimitStatus(
{
...metadata,
preferredProvider: selection.preferredProvider ?? metadata.preferredProvider ?? null,
},
{ now: resolvedDeps.now }
)
: null;
const provider = selectUsageSnapshot(
await collectUsageSnapshots(metadata, resolvedDeps),
selection
);
return { allowed: true, personal, provider };
}
export async function buildUsageCommandText(
metadata: UsageCommandApiKeyMetadata,
deps: InternalUsageCommandDeps = {},
@@ -588,6 +634,17 @@ function inferHttpUsageCommandSelection(request: Request): UsageCommandSelection
}
}
/** `?format=json` (or `?format=JSON`) — anything else falls back to the text
* form, which is the historical contract of this endpoint. */
function wantsUsageCommandJson(request: Request): boolean {
try {
const format = new URL(request.url, "http://localhost").searchParams.get("format");
return format !== null && format.trim().toLowerCase() === "json";
} catch {
return false;
}
}
function createPlainUsageCommandResponse(text: string, status = 200): Response {
return new Response(text, { status, headers: TEXT_PLAIN_HEADERS });
}
@@ -764,22 +821,45 @@ export async function handleInternalUsageCommandHttpRequest(
): Promise<Response> {
try {
const resolvedDeps = await normalizeDeps(deps);
const json = wantsUsageCommandJson(request);
const apiKey = extractUsageCommandApiKey(request);
if (!apiKey || !(await resolvedDeps.isValidApiKey(apiKey))) {
if (json) {
return Response.json(
{ allowed: false, error: { message: USAGE_COMMAND_AUTH_REQUIRED_MESSAGE } } satisfies UsageCommandJson,
{ status: 401 }
);
}
return createPlainUsageCommandResponse(USAGE_COMMAND_AUTH_REQUIRED_MESSAGE, 401);
}
const metadata = await resolvedDeps.getApiKeyMetadata(apiKey);
if (!metadata?.id) {
if (json) {
return Response.json(
{ allowed: false, error: { message: USAGE_COMMAND_AUTH_REQUIRED_MESSAGE } } satisfies UsageCommandJson,
{ status: 401 }
);
}
return createPlainUsageCommandResponse(USAGE_COMMAND_AUTH_REQUIRED_MESSAGE, 401);
}
if (metadata.allowUsageCommand !== true) {
if (json) {
return Response.json(
{ allowed: false, error: { message: USAGE_COMMAND_DISABLED_MESSAGE } } satisfies UsageCommandJson,
{ status: 403 }
);
}
return createPlainUsageCommandResponse(USAGE_COMMAND_DISABLED_MESSAGE, 403);
}
const selection = inferHttpUsageCommandSelection(request);
if (json) {
return Response.json(await buildUsageCommandJson(metadata, resolvedDeps, selection));
}
return createPlainUsageCommandResponse(
await buildUsageCommandText(metadata, resolvedDeps, inferHttpUsageCommandSelection(request))
await buildUsageCommandText(metadata, resolvedDeps, selection)
);
} catch (err) {
const body = buildErrorBody(500, err instanceof Error ? err.message : String(err));