mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
* 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>
869 lines
28 KiB
TypeScript
869 lines
28 KiB
TypeScript
import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits";
|
|
import {
|
|
buildApiKeyUsageLimitPercentText,
|
|
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>;
|
|
|
|
export interface UsageCommandApiKeyMetadata {
|
|
id: string;
|
|
name?: string;
|
|
allowedConnections?: string[] | null;
|
|
preferredProvider?: string | null;
|
|
allowUsageCommand?: boolean;
|
|
usageLimitEnabled?: boolean;
|
|
dailyUsageLimitUsd?: number | null;
|
|
weeklyUsageLimitUsd?: number | null;
|
|
}
|
|
|
|
interface ProviderConnectionLike {
|
|
id: string;
|
|
provider: string;
|
|
isActive?: boolean;
|
|
quotaWindowThresholds?: Record<string, number> | null;
|
|
}
|
|
|
|
export interface UsageSnapshot {
|
|
connectionId: string;
|
|
provider: string;
|
|
plan: unknown;
|
|
quotas: JsonRecord;
|
|
quotaWindowThresholds?: Record<string, number> | null;
|
|
}
|
|
|
|
export interface UsageCommandSelection {
|
|
preferredProvider?: string | null;
|
|
preferredConnectionId?: string | null;
|
|
}
|
|
|
|
interface UsageCommandQuotaPolicy {
|
|
defaultThresholdPercent: number;
|
|
providerWindowDefaults: Record<string, Record<string, number>>;
|
|
}
|
|
|
|
export interface InternalUsageCommandDeps {
|
|
now?: () => number;
|
|
isValidApiKey?: (apiKey: string) => Promise<boolean>;
|
|
getApiKeyMetadata?: (apiKey: string) => Promise<UsageCommandApiKeyMetadata | null>;
|
|
getProviderConnectionById?: (connectionId: string) => Promise<unknown>;
|
|
getProviderConnections?: (filter?: JsonRecord) => Promise<unknown[]>;
|
|
getProviderLimitsCache?: (connectionId: string) => ProviderLimitsCacheEntry | null;
|
|
getAllProviderLimitsCache?: () => Record<string, ProviderLimitsCacheEntry>;
|
|
getApiKeyUsageLimitStatus?: (
|
|
metadata: UsageCommandApiKeyMetadata,
|
|
deps?: { now?: () => number }
|
|
) => Promise<ApiKeyUsageLimitStatus>;
|
|
getQuotaPolicy?: () => Promise<UsageCommandQuotaPolicy>;
|
|
}
|
|
|
|
type RequiredDeps = Required<InternalUsageCommandDeps>;
|
|
|
|
async function normalizeDeps(deps: InternalUsageCommandDeps = {}): Promise<RequiredDeps> {
|
|
const auth = deps.isValidApiKey ? null : await import("@/sse/services/auth");
|
|
const apiKeys = deps.getApiKeyMetadata ? null : await import("@/lib/db/apiKeys");
|
|
const providers =
|
|
deps.getProviderConnectionById && deps.getProviderConnections
|
|
? null
|
|
: await import("@/lib/db/providers");
|
|
const providerLimits =
|
|
deps.getProviderLimitsCache && deps.getAllProviderLimitsCache
|
|
? null
|
|
: await import("@/lib/db/providerLimits");
|
|
const usageLimits = deps.getApiKeyUsageLimitStatus
|
|
? null
|
|
: await import("@/lib/usage/apiKeyUsageLimits");
|
|
|
|
return {
|
|
now: deps.now ?? Date.now,
|
|
isValidApiKey: deps.isValidApiKey ?? auth!.isValidApiKey,
|
|
getApiKeyMetadata: deps.getApiKeyMetadata ?? apiKeys!.getApiKeyMetadata,
|
|
getProviderConnectionById:
|
|
deps.getProviderConnectionById ?? providers!.getProviderConnectionById,
|
|
getProviderConnections: deps.getProviderConnections ?? providers!.getProviderConnections,
|
|
getProviderLimitsCache: deps.getProviderLimitsCache ?? providerLimits!.getProviderLimitsCache,
|
|
getAllProviderLimitsCache:
|
|
deps.getAllProviderLimitsCache ?? providerLimits!.getAllProviderLimitsCache,
|
|
getApiKeyUsageLimitStatus:
|
|
deps.getApiKeyUsageLimitStatus ?? usageLimits!.getApiKeyUsageLimitStatus,
|
|
getQuotaPolicy: deps.getQuotaPolicy ?? getDefaultUsageCommandQuotaPolicy,
|
|
};
|
|
}
|
|
|
|
async function getDefaultUsageCommandQuotaPolicy(): Promise<UsageCommandQuotaPolicy> {
|
|
const [{ getCachedSettings }, { resolveResilienceSettings }] = await Promise.all([
|
|
import("@/lib/localDb"),
|
|
import("@/lib/resilience/settings"),
|
|
]);
|
|
const resilience = resolveResilienceSettings(await getCachedSettings());
|
|
return {
|
|
defaultThresholdPercent: resilience.quotaPreflight.defaultThresholdPercent,
|
|
providerWindowDefaults: resilience.quotaPreflight.providerWindowDefaults,
|
|
};
|
|
}
|
|
|
|
function isRecord(value: unknown): value is JsonRecord {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function readHeader(request: Request, name: string): string | null {
|
|
return request.headers.get(name) || request.headers.get(name.toLowerCase());
|
|
}
|
|
|
|
function readPathScopedToken(request: Request): string | null {
|
|
try {
|
|
const url = new URL(request.url, "http://localhost");
|
|
const segments = url.pathname
|
|
.split("/")
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean);
|
|
|
|
if (segments[0] === "vscode" && segments[1]) {
|
|
return decodeURIComponent(segments[1]).trim() || null;
|
|
}
|
|
|
|
if (segments[0] === "api" && segments[1] === "v1" && segments[2] === "vscode") {
|
|
const tokenIndex = segments[3] === "raw" || segments[3] === "combos" ? 4 : 3;
|
|
if (segments[tokenIndex]) return decodeURIComponent(segments[tokenIndex]).trim() || null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function extractUsageCommandApiKey(request: Request): string | null {
|
|
const authHeader = readHeader(request, "Authorization");
|
|
if (authHeader) {
|
|
const trimmed = authHeader.trim();
|
|
if (trimmed.toLowerCase().startsWith("bearer ")) return trimmed.slice(7).trim() || null;
|
|
}
|
|
|
|
if (readHeader(request, "anthropic-version")) {
|
|
const xApiKey = readHeader(request, "x-api-key");
|
|
if (xApiKey?.trim()) return xApiKey.trim();
|
|
}
|
|
|
|
return readPathScopedToken(request);
|
|
}
|
|
|
|
function toNumber(value: unknown, fallback = Number.NaN): number {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim()) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function textFromContent(content: unknown): string | null {
|
|
if (typeof content === "string") return content;
|
|
|
|
if (Array.isArray(content)) {
|
|
const parts: string[] = [];
|
|
for (const part of content) {
|
|
if (typeof part === "string") {
|
|
parts.push(part);
|
|
continue;
|
|
}
|
|
if (!isRecord(part)) continue;
|
|
const text = part.text ?? part.content;
|
|
if (typeof text === "string") parts.push(text);
|
|
}
|
|
return parts.length > 0 ? parts.join("") : null;
|
|
}
|
|
|
|
if (isRecord(content)) {
|
|
const text = content.text ?? content.content;
|
|
return typeof text === "string" ? text : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function extractLastRoleText(items: unknown, role: string): string | null {
|
|
if (!Array.isArray(items)) return null;
|
|
|
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
const item = items[i];
|
|
if (!isRecord(item) || item.role !== role) continue;
|
|
return textFromContent(item.content);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function extractLastUserText(body: unknown): string | null {
|
|
if (!isRecord(body)) return null;
|
|
|
|
const messagesText = extractLastRoleText(body.messages, "user");
|
|
if (messagesText !== null) return messagesText;
|
|
|
|
if (typeof body.input === "string") return body.input;
|
|
|
|
const inputText = extractLastRoleText(body.input, "user");
|
|
if (inputText !== null) return inputText;
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isInternalUsageCommand(text: string | null | undefined): boolean {
|
|
return typeof text === "string" && text.trim() === INTERNAL_USAGE_COMMAND;
|
|
}
|
|
|
|
function readThresholdMap(value: unknown): Record<string, number> | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
const out: Record<string, number> = {};
|
|
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
const numeric = Number(raw);
|
|
if (key && Number.isFinite(numeric) && numeric >= 0 && numeric <= 100) {
|
|
out[key] = numeric;
|
|
}
|
|
}
|
|
return Object.keys(out).length > 0 ? out : null;
|
|
}
|
|
|
|
function connectionFromValue(value: unknown): ProviderConnectionLike | null {
|
|
if (!isRecord(value)) return null;
|
|
const id = typeof value.id === "string" ? value.id : "";
|
|
const provider = typeof value.provider === "string" ? value.provider : "";
|
|
if (!id || !provider || value.isActive === false) return null;
|
|
return {
|
|
id,
|
|
provider,
|
|
isActive: value.isActive === true,
|
|
quotaWindowThresholds: readThresholdMap(value.quotaWindowThresholds),
|
|
};
|
|
}
|
|
|
|
function snapshotFromConnection(
|
|
connection: ProviderConnectionLike,
|
|
cache: ProviderLimitsCacheEntry | null
|
|
): UsageSnapshot | null {
|
|
if (!cache || !isRecord(cache.quotas) || Object.keys(cache.quotas).length === 0) return null;
|
|
return {
|
|
connectionId: connection.id,
|
|
provider: connection.provider,
|
|
plan: cache.plan,
|
|
quotas: cache.quotas,
|
|
quotaWindowThresholds: connection.quotaWindowThresholds ?? null,
|
|
};
|
|
}
|
|
|
|
export async function collectUsageSnapshots(
|
|
metadata: UsageCommandApiKeyMetadata,
|
|
deps: RequiredDeps
|
|
): Promise<UsageSnapshot[]> {
|
|
const allowedConnections = Array.isArray(metadata.allowedConnections)
|
|
? metadata.allowedConnections.filter((id) => typeof id === "string" && id.trim())
|
|
: [];
|
|
|
|
if (allowedConnections.length > 0) {
|
|
const snapshots: UsageSnapshot[] = [];
|
|
for (const connectionId of allowedConnections) {
|
|
const connection = connectionFromValue(await deps.getProviderConnectionById(connectionId));
|
|
if (!connection) continue;
|
|
const snapshot = snapshotFromConnection(
|
|
connection,
|
|
deps.getProviderLimitsCache(connection.id)
|
|
);
|
|
if (snapshot) snapshots.push(snapshot);
|
|
}
|
|
return snapshots;
|
|
}
|
|
|
|
const caches = deps.getAllProviderLimitsCache();
|
|
const connections = await deps.getProviderConnections({ isActive: true });
|
|
const snapshots: UsageSnapshot[] = [];
|
|
for (const rawConnection of connections) {
|
|
const connection = connectionFromValue(rawConnection);
|
|
if (!connection) continue;
|
|
const snapshot = snapshotFromConnection(connection, caches[connection.id] ?? null);
|
|
if (snapshot) snapshots.push(snapshot);
|
|
}
|
|
return snapshots;
|
|
}
|
|
|
|
function normalizeQuotaKey(key: string): string {
|
|
return key
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
interface QuotaMatch {
|
|
key: string;
|
|
quota: JsonRecord;
|
|
}
|
|
|
|
function findQuota(
|
|
quotas: JsonRecord,
|
|
kind: "session" | "weekly" | "weekly-sonnet"
|
|
): QuotaMatch | null {
|
|
const entries = Object.entries(quotas).filter(([, value]) => isRecord(value));
|
|
|
|
for (const [key, value] of entries) {
|
|
const normalized = normalizeQuotaKey(key);
|
|
if (kind === "session" && (normalized.includes("session") || normalized.includes("5h"))) {
|
|
return { key, quota: value as JsonRecord };
|
|
}
|
|
if (
|
|
kind === "weekly-sonnet" &&
|
|
normalized.includes("weekly") &&
|
|
normalized.includes("sonnet")
|
|
) {
|
|
return { key, quota: value as JsonRecord };
|
|
}
|
|
if (
|
|
kind === "weekly" &&
|
|
(normalized === "weekly" || normalized.includes("weekly") || normalized.includes("7d")) &&
|
|
!normalized.includes("sonnet")
|
|
) {
|
|
return { key, quota: value as JsonRecord };
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getQuotaUsedPercent(quota: JsonRecord | null): number | null {
|
|
if (!quota) return null;
|
|
|
|
const usedPercentage = toNumber(quota.usedPercentage);
|
|
if (Number.isFinite(usedPercentage)) return Math.max(0, Math.min(100, usedPercentage));
|
|
|
|
const used = toNumber(quota.used);
|
|
const total = toNumber(quota.total);
|
|
if (Number.isFinite(used) && Number.isFinite(total) && total > 0) {
|
|
return Math.max(0, Math.min(100, (used / total) * 100));
|
|
}
|
|
|
|
if (Number.isFinite(used) && used >= 0 && used <= 100) {
|
|
return used;
|
|
}
|
|
|
|
const remainingPercentage = toNumber(quota.remainingPercentage);
|
|
if (Number.isFinite(remainingPercentage)) {
|
|
return Math.max(0, Math.min(100, 100 - remainingPercentage));
|
|
}
|
|
|
|
const remaining = toNumber(quota.remaining);
|
|
if (Number.isFinite(remaining) && remaining >= 0 && remaining <= 100) {
|
|
return Math.max(0, Math.min(100, 100 - remaining));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getResetAt(quota: JsonRecord | null): string | null {
|
|
if (!quota) return null;
|
|
return typeof quota.resetAt === "string" && quota.resetAt.trim() ? quota.resetAt : null;
|
|
}
|
|
|
|
function formatLeftPercent(percent: number | null): string {
|
|
if (percent === null || !Number.isFinite(percent)) return "Unavailable";
|
|
return `${Math.round(Math.max(0, Math.min(100, percent)))}% left`;
|
|
}
|
|
|
|
export function formatResetIn(resetAt: string | null, now = Date.now()): string {
|
|
if (!resetAt) return "unknown";
|
|
const resetMs = Date.parse(resetAt);
|
|
if (!Number.isFinite(resetMs)) return "unknown";
|
|
|
|
const deltaMs = resetMs - now;
|
|
if (deltaMs <= 0) return "now";
|
|
|
|
const minuteMs = 60_000;
|
|
const totalMinutes = Math.max(1, Math.ceil(deltaMs / minuteMs));
|
|
const dayMinutes = 24 * 60;
|
|
const days = Math.floor(totalMinutes / dayMinutes);
|
|
const hours = Math.floor((totalMinutes % dayMinutes) / 60);
|
|
const minutes = totalMinutes % 60;
|
|
|
|
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
return `${minutes}m`;
|
|
}
|
|
|
|
function formatPlan(plan: unknown): string {
|
|
if (typeof plan === "string" && plan.trim()) return plan.trim();
|
|
if (typeof plan === "number" && Number.isFinite(plan)) return String(plan);
|
|
return "Unavailable";
|
|
}
|
|
|
|
function snapshotScore(snapshot: UsageSnapshot): number {
|
|
let score = snapshot.provider === "claude" ? 100 : 0;
|
|
if (findQuota(snapshot.quotas, "session")) score += 10;
|
|
if (findQuota(snapshot.quotas, "weekly")) score += 10;
|
|
if (findQuota(snapshot.quotas, "weekly-sonnet")) score += 10;
|
|
if (formatPlan(snapshot.plan) !== "Unavailable") score += 1;
|
|
return score;
|
|
}
|
|
|
|
function selectBestUsageSnapshot(snapshots: UsageSnapshot[]): UsageSnapshot | null {
|
|
let selected: UsageSnapshot | null = null;
|
|
let bestScore = -1;
|
|
for (const snapshot of snapshots) {
|
|
const score = snapshotScore(snapshot);
|
|
if (score > bestScore) {
|
|
selected = snapshot;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
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 quotaWindowLookupNames(provider: string, windowName: string): string[] {
|
|
const names = [windowName];
|
|
const lower = windowName.toLowerCase();
|
|
if (lower !== windowName) names.push(lower);
|
|
|
|
const normalized = normalizeQuotaKey(windowName);
|
|
if (normalized.includes("session") || normalized.includes("5h")) {
|
|
names.push("session", "session (5h)");
|
|
}
|
|
if (normalized.includes("weekly") || normalized.includes("7d")) {
|
|
if (normalized.includes("sonnet")) {
|
|
names.push("weekly sonnet", "weekly sonnet (7d)");
|
|
} else {
|
|
names.push("weekly", "weekly (7d)");
|
|
}
|
|
}
|
|
if (provider === "codex" && (normalized.includes("monthly") || normalized.includes("30d"))) {
|
|
names.push("monthly");
|
|
}
|
|
|
|
return [...new Set(names)];
|
|
}
|
|
|
|
function resolveQuotaCutoffPercent(
|
|
snapshot: UsageSnapshot,
|
|
windowName: string,
|
|
policy: UsageCommandQuotaPolicy
|
|
): number {
|
|
const provider = normalizeProviderId(snapshot.provider) ?? snapshot.provider;
|
|
const providerDefaults =
|
|
policy.providerWindowDefaults[snapshot.provider] ||
|
|
policy.providerWindowDefaults[provider] ||
|
|
{};
|
|
const overrides = snapshot.quotaWindowThresholds ?? {};
|
|
|
|
for (const lookupName of quotaWindowLookupNames(provider, windowName)) {
|
|
const override = overrides[lookupName];
|
|
if (typeof override === "number") return override;
|
|
const providerDefault = providerDefaults[lookupName];
|
|
if (typeof providerDefault === "number") return providerDefault;
|
|
}
|
|
|
|
return policy.defaultThresholdPercent;
|
|
}
|
|
|
|
function effectiveRemainingPercent(
|
|
realRemaining: number | null,
|
|
cutoffPercent: number
|
|
): number | null {
|
|
if (realRemaining === null || !Number.isFinite(realRemaining)) return null;
|
|
const remaining = Math.max(0, Math.min(100, realRemaining));
|
|
const cutoff = Math.max(0, Math.min(99, cutoffPercent));
|
|
if (remaining <= cutoff) return 0;
|
|
return ((remaining - cutoff) / (100 - cutoff)) * 100;
|
|
}
|
|
|
|
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,
|
|
match: QuotaMatch | null,
|
|
snapshot: UsageSnapshot,
|
|
policy: UsageCommandQuotaPolicy,
|
|
now: number
|
|
) {
|
|
lines.push(label);
|
|
const usedPercent = getQuotaUsedPercent(match?.quota ?? null);
|
|
const realRemaining =
|
|
usedPercent === null || !Number.isFinite(usedPercent)
|
|
? null
|
|
: 100 - Math.max(0, Math.min(100, usedPercent));
|
|
const cutoff = match ? resolveQuotaCutoffPercent(snapshot, match.key, policy) : 0;
|
|
lines.push(formatLeftPercent(effectiveRemainingPercent(realRemaining, cutoff)));
|
|
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 = {},
|
|
selection: UsageCommandSelection = {}
|
|
): Promise<string> {
|
|
const resolvedDeps = await normalizeDeps(deps);
|
|
const sections: string[] = [];
|
|
if (metadata.usageLimitEnabled === true) {
|
|
const usageMetadata: UsageCommandApiKeyMetadata = {
|
|
...metadata,
|
|
preferredProvider: selection.preferredProvider ?? metadata.preferredProvider ?? null,
|
|
};
|
|
const status = await resolvedDeps.getApiKeyUsageLimitStatus(usageMetadata, {
|
|
now: resolvedDeps.now,
|
|
});
|
|
const now = resolvedDeps.now();
|
|
sections.push(["Personal quota", buildApiKeyUsageLimitPercentText(status, now)].join("\n"));
|
|
}
|
|
|
|
const snapshot = selectUsageSnapshot(
|
|
await collectUsageSnapshots(metadata, resolvedDeps),
|
|
selection
|
|
);
|
|
|
|
if (!snapshot) {
|
|
sections.push(["Provider quota", "No cached usage data available."].join("\n"));
|
|
return sections.join("\n\n");
|
|
}
|
|
|
|
const now = resolvedDeps.now();
|
|
const policy = await resolvedDeps.getQuotaPolicy();
|
|
const lines = ["Provider quota"];
|
|
appendQuotaBlock(lines, "Session", findQuota(snapshot.quotas, "session"), snapshot, policy, now);
|
|
lines.push("");
|
|
appendQuotaBlock(lines, "Weekly", findQuota(snapshot.quotas, "weekly"), snapshot, policy, now);
|
|
sections.push(lines.join("\n"));
|
|
return sections.join("\n\n");
|
|
}
|
|
|
|
function getResponseModel(body: unknown): string {
|
|
return isRecord(body) && typeof body.model === "string" && body.model.trim()
|
|
? body.model
|
|
: 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,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** `?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 });
|
|
}
|
|
|
|
function isAnthropicRequest(request: Request): boolean {
|
|
if (request.headers.has("anthropic-version")) return true;
|
|
try {
|
|
return new URL(request.url).pathname.endsWith("/v1/messages");
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function textEncoderStream(payload: string): ReadableStream<Uint8Array> {
|
|
const encoder = new TextEncoder();
|
|
return new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(payload));
|
|
controller.close();
|
|
},
|
|
});
|
|
}
|
|
|
|
function createOpenAITextResponse(text: string, body: unknown): Response {
|
|
const created = Math.floor(Date.now() / 1000);
|
|
const model = getResponseModel(body);
|
|
const payload = {
|
|
id: `chatcmpl_usage_${created}`,
|
|
object: "chat.completion",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: text },
|
|
finish_reason: "stop",
|
|
},
|
|
],
|
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
};
|
|
return Response.json(payload);
|
|
}
|
|
|
|
function createOpenAIStreamResponse(text: string, body: unknown): Response {
|
|
const created = Math.floor(Date.now() / 1000);
|
|
const model = getResponseModel(body);
|
|
const id = `chatcmpl_usage_${created}`;
|
|
const first = {
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }],
|
|
};
|
|
const second = {
|
|
id,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
};
|
|
return new Response(
|
|
textEncoderStream(
|
|
`data: ${JSON.stringify(first)}\n\ndata: ${JSON.stringify(second)}\n\ndata: [DONE]\n\n`
|
|
),
|
|
{ headers: { "Content-Type": "text/event-stream; charset=utf-8" } }
|
|
);
|
|
}
|
|
|
|
function createAnthropicTextResponse(text: string, body: unknown): Response {
|
|
const payload = {
|
|
id: `msg_usage_${Date.now()}`,
|
|
type: "message",
|
|
role: "assistant",
|
|
model: getResponseModel(body),
|
|
content: [{ type: "text", text }],
|
|
stop_reason: "end_turn",
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
};
|
|
return Response.json(payload);
|
|
}
|
|
|
|
function createAnthropicStreamResponse(text: string, body: unknown): Response {
|
|
const id = `msg_usage_${Date.now()}`;
|
|
const model = getResponseModel(body);
|
|
const events = [
|
|
[
|
|
"message_start",
|
|
{
|
|
type: "message_start",
|
|
message: {
|
|
id,
|
|
type: "message",
|
|
role: "assistant",
|
|
model,
|
|
content: [],
|
|
stop_reason: null,
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
},
|
|
},
|
|
],
|
|
[
|
|
"content_block_start",
|
|
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
|
|
],
|
|
[
|
|
"content_block_delta",
|
|
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text } },
|
|
],
|
|
["content_block_stop", { type: "content_block_stop", index: 0 }],
|
|
[
|
|
"message_delta",
|
|
{
|
|
type: "message_delta",
|
|
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
usage: { output_tokens: 0 },
|
|
},
|
|
],
|
|
["message_stop", { type: "message_stop" }],
|
|
] as const;
|
|
const payload = events
|
|
.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
.join("");
|
|
return new Response(textEncoderStream(payload), {
|
|
headers: { "Content-Type": "text/event-stream; charset=utf-8" },
|
|
});
|
|
}
|
|
|
|
export function createLocalTextResponse(request: Request, body: unknown, text: string): Response {
|
|
const stream = isRecord(body) && body.stream === true;
|
|
if (isAnthropicRequest(request)) {
|
|
return stream
|
|
? createAnthropicStreamResponse(text, body)
|
|
: createAnthropicTextResponse(text, body);
|
|
}
|
|
return stream ? createOpenAIStreamResponse(text, body) : createOpenAITextResponse(text, body);
|
|
}
|
|
|
|
export async function handleInternalUsageCommand(
|
|
request: Request,
|
|
body: unknown,
|
|
deps: InternalUsageCommandDeps = {}
|
|
): Promise<Response | null> {
|
|
const lastUserText = extractLastUserText(body);
|
|
if (!isInternalUsageCommand(lastUserText)) return null;
|
|
|
|
const resolvedDeps = await normalizeDeps(deps);
|
|
const apiKey = extractUsageCommandApiKey(request);
|
|
if (!apiKey || !(await resolvedDeps.isValidApiKey(apiKey))) {
|
|
return createLocalTextResponse(request, body, USAGE_COMMAND_AUTH_REQUIRED_MESSAGE);
|
|
}
|
|
|
|
const metadata = await resolvedDeps.getApiKeyMetadata(apiKey);
|
|
if (!metadata?.id) {
|
|
return createLocalTextResponse(request, body, USAGE_COMMAND_AUTH_REQUIRED_MESSAGE);
|
|
}
|
|
|
|
if (metadata.allowUsageCommand !== true) {
|
|
return createLocalTextResponse(request, body, USAGE_COMMAND_DISABLED_MESSAGE);
|
|
}
|
|
|
|
return createLocalTextResponse(
|
|
request,
|
|
body,
|
|
await buildUsageCommandText(metadata, resolvedDeps)
|
|
);
|
|
}
|
|
|
|
export async function handleInternalUsageCommandHttpRequest(
|
|
request: Request,
|
|
deps: InternalUsageCommandDeps = {}
|
|
): 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, selection)
|
|
);
|
|
} catch (err) {
|
|
const body = buildErrorBody(500, err instanceof Error ? err.message : String(err));
|
|
return Response.json(body, { status: 500 });
|
|
}
|
|
}
|