fix: restore om-usage HTTP endpoint (#5859)

Integrated into release/v3.8.43
This commit is contained in:
WITALO ROCHA
2026-07-01 22:03:17 -03:00
committed by GitHub
parent 9358eeada9
commit cfb2db8261
6 changed files with 296 additions and 3 deletions

View File

@@ -0,0 +1,16 @@
import { handleCorsOptions } from "@/shared/utils/cors";
import { handleInternalUsageCommandHttpRequest } from "@/lib/usage/internalUsageCommand";
export async function OPTIONS() {
return handleCorsOptions();
}
/**
* GET /api/usage/om-usage
*
* Terminal-friendly equivalent of @@om-usage. Authenticates with the same
* OmniRoute API key used by Claude Code/Codex and requires allowUsageCommand.
*/
export async function GET(request: Request) {
return handleInternalUsageCommandHttpRequest(request);
}

View File

@@ -3,11 +3,13 @@ import {
buildApiKeyUsageLimitText,
type ApiKeyUsageLimitStatus,
} from "@/lib/usage/apiKeyUsageLimits";
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
export const INTERNAL_USAGE_COMMAND = "@@om-usage";
export const USAGE_COMMAND_DISABLED_MESSAGE = "Usage command is disabled for this API key.";
const USAGE_COMMAND_AUTH_REQUIRED_MESSAGE = "Usage command requires an authenticated API key.";
const LOCAL_USAGE_MODEL = "omniroute/local-usage";
const TEXT_PLAIN_HEADERS = { "Content-Type": "text/plain; charset=utf-8" } as const;
type JsonRecord = Record<string, unknown>;
@@ -34,6 +36,11 @@ interface UsageSnapshot {
quotas: JsonRecord;
}
interface UsageCommandSelection {
preferredProvider?: string | null;
preferredConnectionId?: string | null;
}
export interface InternalUsageCommandDeps {
now?: () => number;
isValidApiKey?: (apiKey: string) => Promise<boolean>;
@@ -350,7 +357,7 @@ function snapshotScore(snapshot: UsageSnapshot): number {
return score;
}
function selectUsageSnapshot(snapshots: UsageSnapshot[]): UsageSnapshot | null {
function selectBestUsageSnapshot(snapshots: UsageSnapshot[]): UsageSnapshot | null {
let selected: UsageSnapshot | null = null;
let bestScore = -1;
for (const snapshot of snapshots) {
@@ -363,6 +370,35 @@ function selectUsageSnapshot(snapshots: UsageSnapshot[]): UsageSnapshot | null {
return selected;
}
function normalizeProviderId(provider: string | null | undefined): string | null {
const normalized = provider?.trim().toLowerCase().replace(/_/g, "-");
if (!normalized) return null;
if (normalized === "cc" || normalized === "claude-code" || normalized === "claudecode") {
return "claude";
}
return normalized;
}
function selectUsageSnapshot(
snapshots: UsageSnapshot[],
selection: UsageCommandSelection = {}
): UsageSnapshot | null {
const preferredConnectionId = selection.preferredConnectionId?.trim();
if (preferredConnectionId) {
const snapshot = snapshots.find((entry) => entry.connectionId === preferredConnectionId);
if (snapshot) return snapshot;
}
const preferredProvider = normalizeProviderId(selection.preferredProvider);
if (preferredProvider) {
return selectBestUsageSnapshot(
snapshots.filter((entry) => normalizeProviderId(entry.provider) === preferredProvider)
);
}
return selectBestUsageSnapshot(snapshots);
}
function appendQuotaBlock(lines: string[], label: string, quota: JsonRecord | null, now: number) {
lines.push(label);
lines.push(formatPercent(getQuotaUsedPercent(quota)));
@@ -371,7 +407,8 @@ function appendQuotaBlock(lines: string[], label: string, quota: JsonRecord | nu
export async function buildUsageCommandText(
metadata: UsageCommandApiKeyMetadata,
deps: InternalUsageCommandDeps = {}
deps: InternalUsageCommandDeps = {},
selection: UsageCommandSelection = {}
): Promise<string> {
const resolvedDeps = await normalizeDeps(deps);
if (metadata.usageLimitEnabled === true) {
@@ -381,7 +418,10 @@ export async function buildUsageCommandText(
);
}
const snapshot = selectUsageSnapshot(await collectUsageSnapshots(metadata, resolvedDeps));
const snapshot = selectUsageSnapshot(
await collectUsageSnapshots(metadata, resolvedDeps),
selection
);
if (!snapshot) {
return ["Plan", "Unavailable", "", "Usage", "No cached usage data available."].join("\n");
@@ -403,6 +443,28 @@ function getResponseModel(body: unknown): string {
: LOCAL_USAGE_MODEL;
}
function inferHttpUsageCommandSelection(request: Request): UsageCommandSelection {
try {
const url = new URL(request.url, "http://localhost");
return {
preferredConnectionId:
url.searchParams.get("connectionId")?.trim() ||
readHeader(request, "x-omniroute-connection")?.trim() ||
null,
preferredProvider: url.searchParams.get("provider")?.trim() || null,
};
} catch {
return {
preferredConnectionId: readHeader(request, "x-omniroute-connection")?.trim() || null,
preferredProvider: null,
};
}
}
function createPlainUsageCommandResponse(text: string, status = 200): Response {
return new Response(text, { status, headers: TEXT_PLAIN_HEADERS });
}
function isAnthropicRequest(request: Request): boolean {
if (request.headers.has("anthropic-version")) return true;
try {
@@ -568,3 +630,32 @@ export async function handleInternalUsageCommand(
await buildUsageCommandText(metadata, resolvedDeps)
);
}
export async function handleInternalUsageCommandHttpRequest(
request: Request,
deps: InternalUsageCommandDeps = {}
): Promise<Response> {
try {
const resolvedDeps = await normalizeDeps(deps);
const apiKey = extractUsageCommandApiKey(request);
if (!apiKey || !(await resolvedDeps.isValidApiKey(apiKey))) {
return createPlainUsageCommandResponse(USAGE_COMMAND_AUTH_REQUIRED_MESSAGE, 401);
}
const metadata = await resolvedDeps.getApiKeyMetadata(apiKey);
if (!metadata?.id) {
return createPlainUsageCommandResponse(USAGE_COMMAND_AUTH_REQUIRED_MESSAGE, 401);
}
if (metadata.allowUsageCommand !== true) {
return createPlainUsageCommandResponse(USAGE_COMMAND_DISABLED_MESSAGE, 403);
}
return createPlainUsageCommandResponse(
await buildUsageCommandText(metadata, resolvedDeps, inferHttpUsageCommandSelection(request))
);
} catch (err) {
const body = buildErrorBody(500, err instanceof Error ? err.message : String(err));
return Response.json(body, { status: 500 });
}
}

View File

@@ -14,6 +14,10 @@ const PUBLIC_API_ROUTE_PREFIXES = [
// access token. The handler enforces its own password check + lockout — there
// is no token yet at this point, so it cannot require management auth.
"/api/cli/connect",
// Terminal-friendly @@om-usage equivalent for CLI clients (Claude Code/Codex).
// The handler enforces its own auth via extractUsageCommandApiKey/isValidApiKey
// and the allowUsageCommand flag — it must not be gated by management auth.
"/api/usage/om-usage",
];
const PUBLIC_READONLY_API_ROUTE_PREFIXES = [

View File

@@ -176,6 +176,13 @@ const cases: Case[] = [
{ name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" },
{ name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" },
{
name: "/api/usage/om-usage is PUBLIC (handler enforces its own API key auth)",
path: "/api/usage/om-usage",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "Unknown top-level path defaults MANAGEMENT (fail-closed)",
path: "/totally-unknown",

View File

@@ -5,6 +5,7 @@ import {
buildUsageCommandText,
extractLastUserText,
handleInternalUsageCommand,
handleInternalUsageCommandHttpRequest,
isInternalUsageCommand,
} from "../../src/lib/usage/internalUsageCommand.ts";
@@ -175,6 +176,174 @@ test("buildUsageCommandText formats API key USD limits when fair usage is enable
);
});
test("handleInternalUsageCommandHttpRequest returns terminal text for an allowed API key", async () => {
const response = await handleInternalUsageCommandHttpRequest(
new Request("http://localhost/api/usage/om-usage?provider=claude", {
headers: { Authorization: "Bearer sk-allowed" },
}),
{
now: () => NOW,
isValidApiKey: async (apiKey) => apiKey === "sk-allowed",
getApiKeyMetadata: async () => ({
id: "key-allowed",
name: "Claude terminal",
allowedConnections: ["conn-codex", "conn-claude"],
allowUsageCommand: true,
}),
getProviderConnectionById: async (connectionId) => ({
id: connectionId,
provider: connectionId === "conn-claude" ? "claude" : "codex",
isActive: true,
}),
getProviderConnections: async () => [],
getProviderLimitsCache: (connectionId) =>
connectionId === "conn-claude"
? {
plan: "Claude Max",
quotas: {
"session (5h)": {
used: 74,
total: 100,
remaining: 26,
resetAt: new Date(NOW + 2 * 60 * 60_000).toISOString(),
},
"weekly (7d)": {
used: 25,
total: 100,
remaining: 75,
resetAt: new Date(NOW + 6 * 24 * 60 * 60_000).toISOString(),
},
},
message: null,
fetchedAt: new Date(NOW).toISOString(),
}
: {
plan: "Codex Pro",
quotas: {
weekly: {
used: 9,
total: 100,
remaining: 91,
resetAt: new Date(NOW + 5 * 24 * 60 * 60_000).toISOString(),
},
},
message: null,
fetchedAt: new Date(NOW).toISOString(),
},
getAllProviderLimitsCache: () => ({}),
getApiKeyUsageLimitStatus: async () => {
throw new Error("usage limit lookup must not run for provider quota output");
},
}
);
assert.equal(response.status, 200);
assert.equal(response.headers.get("content-type"), "text/plain; charset=utf-8");
assert.equal(
await response.text(),
[
"Plan",
"Claude Max",
"",
"Usage",
"Session (5hr)",
"74%",
"Resets in 2h",
"",
"Weekly (7 day)",
"25%",
"Resets in 6d",
"",
"Weekly Sonnet",
"Unavailable",
"Resets in unknown",
].join("\n")
);
});
test("handleInternalUsageCommandHttpRequest sanitizes internal errors and never leaks stack traces", async () => {
const response = await handleInternalUsageCommandHttpRequest(
new Request("http://localhost/api/usage/om-usage", {
headers: { Authorization: "Bearer sk-boom" },
}),
{
isValidApiKey: async () => {
throw new Error(
`boom at /home/diegosouzapw/dev/proxys/OmniRoute/src/lib/usage/internalUsageCommand.ts:1:1`
);
},
getApiKeyMetadata: async () => {
throw new Error("metadata lookup must not run when auth check throws");
},
getProviderConnectionById: async () => null,
getProviderConnections: async () => [],
getProviderLimitsCache: () => null,
getAllProviderLimitsCache: () => ({}),
getApiKeyUsageLimitStatus: async () => {
throw new Error("usage limit lookup must not run when auth check throws");
},
}
);
assert.equal(response.status, 500);
assert.equal(response.headers.get("content-type"), "application/json");
const body = await response.json();
assert.equal(typeof body.error.message, "string");
assert.equal(body.error.message.includes("at /"), false);
assert.equal(body.error.message.includes("internalUsageCommand.ts"), false);
});
test("handleInternalUsageCommandHttpRequest rejects invalid API keys as plain text", async () => {
const response = await handleInternalUsageCommandHttpRequest(
new Request("http://localhost/api/usage/om-usage", {
headers: { Authorization: "Bearer sk-invalid" },
}),
{
isValidApiKey: async () => false,
getApiKeyMetadata: async () => {
throw new Error("metadata lookup must not run for invalid keys");
},
getProviderConnectionById: async () => null,
getProviderConnections: async () => [],
getProviderLimitsCache: () => null,
getAllProviderLimitsCache: () => ({}),
getApiKeyUsageLimitStatus: async () => {
throw new Error("usage limit lookup must not run for invalid keys");
},
}
);
assert.equal(response.status, 401);
assert.equal(response.headers.get("content-type"), "text/plain; charset=utf-8");
assert.equal(await response.text(), "Usage command requires an authenticated API key.");
});
test("handleInternalUsageCommandHttpRequest rejects API keys without usage command access", async () => {
const response = await handleInternalUsageCommandHttpRequest(
new Request("http://localhost/api/usage/om-usage", {
headers: { Authorization: "Bearer sk-disabled" },
}),
{
isValidApiKey: async () => true,
getApiKeyMetadata: async () => ({
id: "key-disabled",
allowUsageCommand: false,
}),
getProviderConnectionById: async () => null,
getProviderConnections: async () => [],
getProviderLimitsCache: () => null,
getAllProviderLimitsCache: () => ({}),
getApiKeyUsageLimitStatus: async () => {
throw new Error("usage limit lookup must not run for disabled keys");
},
}
);
assert.equal(response.status, 403);
assert.equal(response.headers.get("content-type"), "text/plain; charset=utf-8");
assert.equal(await response.text(), "Usage command is disabled for this API key.");
});
test("handleInternalUsageCommand returns disabled response locally without provider routing", async () => {
const response = await handleInternalUsageCommand(
new Request("http://localhost/v1/chat/completions", {

View File

@@ -25,3 +25,9 @@ test("isPublicApiRoute rejects non-public management routes", () => {
assert.equal(isPublicApiRoute("/api/settings"), false);
assert.equal(isPublicApiRoute("/api/providers"), false);
});
test("isPublicApiRoute allows /api/usage/om-usage (handler enforces its own API key auth)", () => {
assert.equal(isPublicApiRoute("/api/usage/om-usage"), true);
assert.equal(isPublicApiRoute("/api/usage/om-usage", "GET"), true);
assert.equal(isPublicApiRoute("/api/usage/om-usage", "OPTIONS"), true);
});