fix(quota): harden dashboard integration
This commit is contained in:
@@ -129,9 +129,9 @@ While pi is running, use these provider commands without restarting:
|
||||
|
||||
- `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active.
|
||||
- `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning.
|
||||
- `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, month-to-date cost/requests/tokens, the API key name, and the 5-hour and weekly usage windows.
|
||||
- `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, available usage totals, the API key name, and the 5-hour and weekly usage windows.
|
||||
|
||||
The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If command cannot reach those endpoints or they change, the command reports a readable error instead of failing. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
|
||||
The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
|
||||
|
||||
Set `COMMANDCODE_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header.
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import { join } from "node:path"
|
||||
import { getConfiguredApiKey } from "./src/api-key.ts"
|
||||
import { createStreamCommandCode } from "./src/core.ts"
|
||||
import { calculateCommandCodeCost } from "./src/cost.ts"
|
||||
import { pickCommandCodeApiKey } from "./src/converters.ts"
|
||||
import {
|
||||
apiForModelId,
|
||||
baseUrlForModel,
|
||||
@@ -33,20 +32,10 @@ import {
|
||||
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
||||
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
||||
import { registerCommandCodeQuota } from "./src/quota-command.ts"
|
||||
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
||||
import { fetchCommandCodeQuota, formatQuota, redactValue } from "./src/quota.ts"
|
||||
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
||||
|
||||
const COMMAND_CODE_PROVIDER_ID = "commandcode"
|
||||
|
||||
async function resolveCommandCodeApiKey(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
||||
const registryKey = await ctx.modelRegistry?.getApiKeyForProvider?.(COMMAND_CODE_PROVIDER_ID)
|
||||
// Mirror src/core.ts: OMP may surface an unresolved placeholder; fall back
|
||||
// to the env/auth-file resolver so we never send a literal placeholder as a
|
||||
// Bearer token (which caused a 401 on /alpha/whoami).
|
||||
return pickCommandCodeApiKey(registryKey, getConfiguredApiKey())
|
||||
}
|
||||
|
||||
function commandCodeHeaders(): Record<string, string> | undefined {
|
||||
if (process.env.COMMANDCODE_ZDR === "1") {
|
||||
return { "x-cmd-zdr": "1" }
|
||||
@@ -136,41 +125,9 @@ export default async function (pi: ExtensionAPI) {
|
||||
return normalized ? { message: normalized.message } : undefined
|
||||
})
|
||||
|
||||
pi.registerCommand("commandcode-quota", {
|
||||
description: "Show Command Code account usage and quota",
|
||||
handler: async (_args, ctx) => {
|
||||
await ctx.waitForIdle?.()
|
||||
|
||||
// Resolve the key in a host-agnostic way so the command also works on
|
||||
// OMP (which passes an unresolved "$COMMANDCODE_API_KEY" placeholder
|
||||
// through the registry): filter placeholders and fall back to the
|
||||
// env/auth-file resolver, mirroring src/core.ts.
|
||||
const apiKey = await resolveCommandCodeApiKey(ctx)
|
||||
if (!apiKey) {
|
||||
ctx.ui.notify(
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set the COMMANDCODE_API_KEY env var.",
|
||||
"warning",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await fetchCommandCodeQuota({
|
||||
apiKey,
|
||||
// Alpha endpoints live under the legacy base (no /provider/v1),
|
||||
// same as the fallback generate transport.
|
||||
baseUrl: legacyApiBase(apiBase),
|
||||
// Respect the user's zero-data-retention preference on usage/account
|
||||
// calls too, matching the provider stream path.
|
||||
extraHeaders: commandCodeHeaders(),
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
ctx.ui.notify(redactValue(result.error.message), "error")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ui.notify(formatQuota(result.quota), "info")
|
||||
},
|
||||
registerCommandCodeQuota(pi, {
|
||||
apiBase: legacyApiBase(apiBase),
|
||||
headers: commandCodeHeaders(),
|
||||
})
|
||||
|
||||
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
||||
|
||||
+3
-3
@@ -29,14 +29,14 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
||||
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
||||
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
||||
"pi:isolated": "node scripts/pi-isolated.mjs",
|
||||
"pi:authenticated": "node scripts/pi-authenticated.mjs",
|
||||
"test:quota": "tsx tests/test-quota.ts",
|
||||
"test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
|
||||
"test:quota": "tsx tests/test-quota.ts && tsx tests/test-quota-command.ts",
|
||||
"test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
|
||||
"test:api-key": "tsx tests/test-api-key.ts",
|
||||
"test:models": "tsx tests/test-models.ts",
|
||||
"test:runtime": "tsx tests/test-runtime.ts",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { getConfiguredApiKey } from "./api-key.ts"
|
||||
import { pickCommandCodeApiKey } from "./converters.ts"
|
||||
import { fetchCommandCodeQuota, redactValue } from "./quota.ts"
|
||||
import { formatQuota } from "./quota-format.ts"
|
||||
|
||||
export interface QuotaCommandContext {
|
||||
waitForIdle?: () => Promise<void>
|
||||
modelRegistry?: {
|
||||
getApiKeyForProvider?: (provider: string) => Promise<string | undefined>
|
||||
}
|
||||
ui: {
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void
|
||||
}
|
||||
}
|
||||
|
||||
interface QuotaCommandApi {
|
||||
registerCommand(
|
||||
name: string,
|
||||
options: {
|
||||
description: string
|
||||
handler: (args: string, ctx: QuotaCommandContext) => Promise<void>
|
||||
},
|
||||
): void
|
||||
}
|
||||
|
||||
interface RegisterQuotaCommandOptions {
|
||||
apiBase: string
|
||||
headers?: Record<string, string>
|
||||
getConfiguredKey?: () => string | undefined
|
||||
fetchQuota?: typeof fetchCommandCodeQuota
|
||||
}
|
||||
|
||||
export function registerCommandCodeQuota(
|
||||
pi: QuotaCommandApi,
|
||||
options: RegisterQuotaCommandOptions,
|
||||
): void {
|
||||
const getConfiguredKey = options.getConfiguredKey ?? getConfiguredApiKey
|
||||
const fetchQuota = options.fetchQuota ?? fetchCommandCodeQuota
|
||||
|
||||
pi.registerCommand("commandcode-quota", {
|
||||
description: "Show Command Code account usage and quota",
|
||||
handler: async (_args, ctx) => {
|
||||
await ctx.waitForIdle?.()
|
||||
const registryKey = await ctx.modelRegistry?.getApiKeyForProvider?.("commandcode")
|
||||
const apiKey = pickCommandCodeApiKey(registryKey, getConfiguredKey())
|
||||
if (!apiKey) {
|
||||
ctx.ui.notify(
|
||||
"Command Code quota requires an API key. Run /login and select Command Code, or set COMMANDCODE_API_KEY.",
|
||||
"warning",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await fetchQuota({
|
||||
apiKey,
|
||||
baseUrl: options.apiBase,
|
||||
extraHeaders: options.headers,
|
||||
})
|
||||
if (!result.ok) {
|
||||
ctx.ui.notify(redactValue(result.error.message), "error")
|
||||
return
|
||||
}
|
||||
ctx.ui.notify(formatQuota(result.quota), "info")
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type {
|
||||
CommandCodeCredits,
|
||||
CommandCodeQuota,
|
||||
CommandCodeSubscription,
|
||||
CommandCodeWindowLimit,
|
||||
} from "./quota-types.ts"
|
||||
|
||||
export function formatWindowLimits(
|
||||
limits: readonly CommandCodeWindowLimit[],
|
||||
now: () => number = Date.now,
|
||||
): string[] {
|
||||
const labels: Record<CommandCodeWindowLimit["window"], string> = {
|
||||
fiveHour: "5-hour",
|
||||
weekly: "Weekly",
|
||||
}
|
||||
|
||||
return limits.map((limit) => {
|
||||
const used = limit.used.toFixed(2)
|
||||
const cap = limit.cap.toFixed(2)
|
||||
const percent = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0
|
||||
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`
|
||||
return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`
|
||||
})
|
||||
}
|
||||
|
||||
function formatResetClock(resetAtSeconds: number, now: () => number): string {
|
||||
const date = new Date(resetAtSeconds * 1000)
|
||||
if (Number.isNaN(date.getTime())) return "unknown"
|
||||
const diffMs = date.getTime() - now()
|
||||
if (diffMs <= 0) return "soon"
|
||||
const minutes = Math.ceil(diffMs / 60_000)
|
||||
if (minutes < 60) return `in ${minutes}m`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainingMinutes = minutes % 60
|
||||
if (hours < 24) {
|
||||
return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`
|
||||
}
|
||||
const days = Math.floor(hours / 24)
|
||||
return days === 1 ? "in 1 day" : `in ${days} days`
|
||||
}
|
||||
|
||||
function creditsDetail(credits: CommandCodeCredits | null): string | undefined {
|
||||
if (!credits) return undefined
|
||||
const parts = [
|
||||
`monthly $${credits.monthlyCredits.toFixed(2)}`,
|
||||
`purchased $${credits.purchasedCredits.toFixed(2)}`,
|
||||
]
|
||||
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`)
|
||||
return `Sources: ${parts.join(" / ")}`
|
||||
}
|
||||
|
||||
function subscriptionLine(subscription: CommandCodeSubscription): string {
|
||||
const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim()
|
||||
const status = subscription.status ? ` (${subscription.status})` : ""
|
||||
return `Plan: ${plan}${status}`
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`
|
||||
return String(tokens)
|
||||
}
|
||||
|
||||
export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string {
|
||||
const lines: string[] = []
|
||||
const remaining = quota.credits?.remainingCredits ?? 0
|
||||
const spent = quota.summary?.totalCost ?? 0
|
||||
const pool = remaining + spent
|
||||
|
||||
if (quota.credits || quota.summary) {
|
||||
lines.push("Credits")
|
||||
lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`)
|
||||
lines.push(` Used: $${spent.toFixed(2)}`)
|
||||
lines.push(` ${pool > 0 ? Math.round((spent / pool) * 100) : 0}% used`)
|
||||
}
|
||||
|
||||
const detail = creditsDetail(quota.credits)
|
||||
if (detail) lines.push(detail)
|
||||
if (quota.subscription) lines.push(subscriptionLine(quota.subscription))
|
||||
|
||||
if (quota.summary) {
|
||||
lines.push("")
|
||||
lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage")
|
||||
lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`)
|
||||
lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`)
|
||||
if (quota.summary.totalTokens !== undefined) {
|
||||
lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("Account")
|
||||
lines.push(` ${quota.account.keyName ?? quota.account.login}`)
|
||||
|
||||
const limits = quota.credits?.windowLimits ?? []
|
||||
if (limits.length > 0) {
|
||||
lines.push("")
|
||||
lines.push("Usage windows:")
|
||||
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`))
|
||||
}
|
||||
|
||||
if ((quota.unavailable?.length ?? 0) > 0) {
|
||||
lines.push("")
|
||||
lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`)
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("Full detail: https://commandcode.ai/usage")
|
||||
return lines.join("\n")
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export interface CommandCodeWindowLimit {
|
||||
window: "fiveHour" | "weekly"
|
||||
used: number
|
||||
cap: number
|
||||
resetAt: number | null
|
||||
}
|
||||
|
||||
export interface CommandCodeCredits {
|
||||
monthlyCredits: number
|
||||
purchasedCredits: number
|
||||
freeCredits: number
|
||||
remainingCredits: number
|
||||
windowLimits: CommandCodeWindowLimit[]
|
||||
}
|
||||
|
||||
export interface CommandCodeSubscription {
|
||||
planId: string | null
|
||||
status: string | null
|
||||
currentPeriodStart: string | null
|
||||
currentPeriodEnd: string | null
|
||||
}
|
||||
|
||||
export interface CommandCodeUsageSummary {
|
||||
totalCost: number
|
||||
totalCount: number
|
||||
totalTokens?: number
|
||||
}
|
||||
|
||||
export type CommandCodeQuotaSection = "credits" | "subscription" | "usage"
|
||||
|
||||
export interface CommandCodeQuota {
|
||||
account: {
|
||||
login: string
|
||||
orgId: string | null
|
||||
keyName?: string
|
||||
}
|
||||
credits: CommandCodeCredits | null
|
||||
subscription: CommandCodeSubscription | null
|
||||
summary: CommandCodeUsageSummary | null
|
||||
unavailable?: readonly CommandCodeQuotaSection[]
|
||||
}
|
||||
|
||||
export type CommandCodeQuotaErrorKind = "config" | "http" | "network" | "timeout"
|
||||
|
||||
export type CommandCodeQuotaResult =
|
||||
| { ok: true; quota: CommandCodeQuota }
|
||||
| { ok: false; error: { message: string; kind: CommandCodeQuotaErrorKind } }
|
||||
+176
-454
@@ -1,83 +1,34 @@
|
||||
/**
|
||||
* Command Code usage/quota fetch layer for the `/commandcode-quota` command.
|
||||
*
|
||||
* Command Code exposes account usage through a set of authenticated alpha
|
||||
* endpoints (the same ones the `cmd` CLI `/usage` command uses):
|
||||
*
|
||||
* - `/alpha/whoami` -> resolved account + optional org id
|
||||
* - `/alpha/billing/credits` -> monthly/purchased/free credits + window limits
|
||||
* - `/alpha/billing/subscriptions`-> plan id, status, billing period
|
||||
* - `/alpha/usage/summary` -> period totals (cost, request count, optional tokens)
|
||||
*
|
||||
* These endpoints are not part of the documented public Provider API
|
||||
* (`/provider/v1/*`) but are shipped with every `command-code` CLI release and
|
||||
* authenticate with the same API key the provider already uses. Fetches are
|
||||
* wrapped defensively so the quota command degrades to a readable error rather
|
||||
* than surfacing raw transport details.
|
||||
*/
|
||||
|
||||
import { redactCommandCodeErrorText } from "./overflow.ts"
|
||||
import type {
|
||||
CommandCodeCredits,
|
||||
CommandCodeQuotaResult,
|
||||
CommandCodeQuotaSection,
|
||||
CommandCodeSubscription,
|
||||
CommandCodeUsageSummary,
|
||||
CommandCodeWindowLimit,
|
||||
} from "./quota-types.ts"
|
||||
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
|
||||
export const QUOTA_TIMEOUT_MS = 15_000
|
||||
|
||||
/**
|
||||
* A single rolling usage window (the 5-hour or weekly cap on a plan's monthly
|
||||
* credits). Values are measured in credit value, not request count.
|
||||
*/
|
||||
export interface CommandCodeWindowLimit {
|
||||
window: "fiveHour" | "weekly"
|
||||
used: number
|
||||
cap: number
|
||||
/** Unix epoch seconds when this window resets, normalized from seconds or ms. */
|
||||
resetAt: number | null
|
||||
interface FetchOptions {
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
fetchImpl?: typeof fetch
|
||||
timeoutMs?: number
|
||||
extraHeaders?: Record<string, string>
|
||||
}
|
||||
|
||||
/** Credits exposed by the `/alpha/billing/credits` endpoint. */
|
||||
export interface CommandCodeCredits {
|
||||
monthlyCredits: number
|
||||
purchasedCredits: number
|
||||
freeCredits: number
|
||||
remainingCredits: number
|
||||
windowLimits: CommandCodeWindowLimit[]
|
||||
interface HttpErrorShape {
|
||||
__httpError: true
|
||||
message: string
|
||||
status: number
|
||||
body: string
|
||||
}
|
||||
|
||||
/** Subscription/plan info exposed by `/alpha/billing/subscriptions`. */
|
||||
export interface CommandCodeSubscription {
|
||||
planId: string | null
|
||||
status: string | null
|
||||
currentPeriodStart: string | null
|
||||
currentPeriodEnd: string | null
|
||||
}
|
||||
|
||||
/** Period totals exposed by `/alpha/usage/summary`. */
|
||||
export interface CommandCodeUsageSummary {
|
||||
totalCost: number
|
||||
totalCount: number
|
||||
/** Optional aggregate token count; only shown when the endpoint reports it. */
|
||||
totalTokens?: number
|
||||
}
|
||||
|
||||
/** Fully normalized quota snapshot for display. */
|
||||
export interface CommandCodeQuota {
|
||||
account: {
|
||||
login: string
|
||||
orgId: string | null
|
||||
/** Optional API key / account display name; falls back to login. */
|
||||
keyName?: string
|
||||
}
|
||||
credits: CommandCodeCredits | null
|
||||
subscription: CommandCodeSubscription | null
|
||||
summary: CommandCodeUsageSummary | null
|
||||
}
|
||||
|
||||
export type CommandCodeQuotaResult =
|
||||
| { ok: true; quota: CommandCodeQuota }
|
||||
| { ok: false; error: { message: string; kind: "config" | "http" | "network" | "timeout" } }
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
interface QuotaErrorShape {
|
||||
__quotaError: true
|
||||
kind: "timeout" | "network"
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -85,442 +36,275 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined
|
||||
return value >= 0 ? value : undefined
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
interface FetchOptions {
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
fetchImpl?: typeof fetch
|
||||
timeoutMs?: number
|
||||
/** Extra HTTP headers merged after Content-Type/Authorization (e.g. ZDR). */
|
||||
extraHeaders?: Record<string, string>
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
||||
const search = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null && value !== "") search.set(key, value)
|
||||
function normalizeResetAt(value: unknown): number | null {
|
||||
let timestamp: number | undefined
|
||||
if (typeof value === "number" && Number.isFinite(value)) timestamp = value
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
const trimmed = value.trim()
|
||||
timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
|
||||
}
|
||||
const query = search.toString()
|
||||
return `${path}${query ? `?${query}` : ""}`
|
||||
if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
|
||||
return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
|
||||
}
|
||||
|
||||
/** Extract the WindowLimit array from the top-level `windowLimits` object. */
|
||||
export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
|
||||
if (!isRecord(value)) return []
|
||||
const limits: CommandCodeWindowLimit[] = []
|
||||
|
||||
for (const [window, entry] of [
|
||||
["fiveHour", value.fiveHour],
|
||||
["weekly", value.weekly],
|
||||
] as const) {
|
||||
if (!isRecord(entry)) continue
|
||||
const used = numberValue(entry.used) ?? 0
|
||||
const cap = numberValue(entry.cap) ?? 0
|
||||
if (cap <= 0 && used <= 0) continue
|
||||
limits.push({
|
||||
window,
|
||||
used,
|
||||
cap,
|
||||
resetAt: normalizeResetAt(entry.resetAt),
|
||||
})
|
||||
const used = numberValue(entry.used)
|
||||
const cap = numberValue(entry.cap)
|
||||
if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
|
||||
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
|
||||
}
|
||||
|
||||
return limits
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a `resetAt` value to epoch seconds. Accepts seconds (10-digit),
|
||||
* milliseconds (13-digit, live API), a numeric string, or an ISO timestamp
|
||||
* string, then converts ms -> s consistently. Invalid/negative values -> null.
|
||||
*/
|
||||
function normalizeResetAt(value: unknown): number | null {
|
||||
let num: number | undefined
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
num = value
|
||||
} else if (typeof value === "string" && value.length > 0) {
|
||||
const trimmed = value.trim()
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
num = Number(trimmed)
|
||||
} else {
|
||||
const parsed = Date.parse(trimmed)
|
||||
if (!Number.isNaN(parsed)) num = Math.round(parsed / 1000)
|
||||
}
|
||||
}
|
||||
if (num === undefined || num < 0) return null
|
||||
return num >= 1e12 ? Math.round(num / 1000) : num
|
||||
}
|
||||
|
||||
function parseCredits(value: unknown): CommandCodeCredits | null {
|
||||
const credits = isRecord(value) ? value.credits : undefined
|
||||
if (!isRecord(credits)) return null
|
||||
|
||||
// `windowLimits` is a top-level sibling of `credits` in the
|
||||
// `/alpha/billing/credits` response, not nested inside it.
|
||||
const windowLimits = isRecord(value) ? value.windowLimits : undefined
|
||||
|
||||
const monthlyCredits = numberValue(credits.monthlyCredits) ?? 0
|
||||
const purchasedCredits = numberValue(credits.purchasedCredits) ?? 0
|
||||
const freeCredits = numberValue(credits.freeCredits) ?? 0
|
||||
|
||||
if (!isRecord(value) || !isRecord(value.credits)) return null
|
||||
const credits = value.credits
|
||||
const monthlyCredits = numberValue(credits.monthlyCredits)
|
||||
const purchasedCredits = numberValue(credits.purchasedCredits)
|
||||
const freeCredits = numberValue(credits.freeCredits)
|
||||
if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
|
||||
return null
|
||||
}
|
||||
const monthly = monthlyCredits ?? 0
|
||||
const purchased = purchasedCredits ?? 0
|
||||
const free = freeCredits ?? 0
|
||||
return {
|
||||
monthlyCredits,
|
||||
purchasedCredits,
|
||||
freeCredits,
|
||||
remainingCredits: monthlyCredits + purchasedCredits + freeCredits,
|
||||
windowLimits: windowLimitsFromCredits(windowLimits),
|
||||
monthlyCredits: monthly,
|
||||
purchasedCredits: purchased,
|
||||
freeCredits: free,
|
||||
remainingCredits: monthly + purchased + free,
|
||||
windowLimits: windowLimitsFromCredits(value.windowLimits),
|
||||
}
|
||||
}
|
||||
|
||||
function parseSubscription(value: unknown): CommandCodeSubscription | null {
|
||||
const data = isRecord(value) ? value.data : undefined
|
||||
if (!isRecord(data)) return null
|
||||
|
||||
if (!isRecord(value) || !isRecord(value.data)) return null
|
||||
const data = value.data
|
||||
const planId = stringValue(data.planId)
|
||||
const status = stringValue(data.status)
|
||||
const currentPeriodStart = stringValue(data.currentPeriodStart)
|
||||
const currentPeriodEnd = stringValue(data.currentPeriodEnd)
|
||||
if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
|
||||
return {
|
||||
planId: stringValue(data.planId) ?? null,
|
||||
status: stringValue(data.status) ?? null,
|
||||
currentPeriodStart: stringValue(data.currentPeriodStart) ?? null,
|
||||
currentPeriodEnd: stringValue(data.currentPeriodEnd) ?? null,
|
||||
planId: planId ?? null,
|
||||
status: status ?? null,
|
||||
currentPeriodStart: currentPeriodStart ?? null,
|
||||
currentPeriodEnd: currentPeriodEnd ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function parseSummary(value: unknown): CommandCodeUsageSummary | null {
|
||||
if (!isRecord(value)) return null
|
||||
const totalCost = numberValue(value.totalCost)
|
||||
const totalCount = numberValue(value.totalCount)
|
||||
if (totalCost === undefined || totalCount === undefined) return null
|
||||
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
|
||||
return {
|
||||
totalCost: numberValue(value.totalCost) ?? 0,
|
||||
totalCount: numberValue(value.totalCount) ?? 0,
|
||||
...(totalTokens === undefined ? {} : { totalTokens }),
|
||||
}
|
||||
return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
|
||||
}
|
||||
|
||||
function parseWhoami(value: unknown): {
|
||||
login: string
|
||||
orgId: string | null
|
||||
keyName?: string
|
||||
} {
|
||||
const org = isRecord(value) ? value.org : undefined
|
||||
const user = isRecord(value) ? value.user : undefined
|
||||
} | null {
|
||||
if (!isRecord(value)) return null
|
||||
const org = isRecord(value.org) ? value.org : undefined
|
||||
const user = isRecord(value.user) ? value.user : undefined
|
||||
const login =
|
||||
(org ? stringValue(org.login) : undefined) ??
|
||||
(user ? (stringValue(user.userName) ?? stringValue(user.name)) : undefined)
|
||||
if (!login) return null
|
||||
const orgId = org ? stringValue(org.id) : undefined
|
||||
const keyName = user ? (stringValue(user.keyName) ?? stringValue(user.displayName)) : undefined
|
||||
return { login, orgId: orgId ?? null, ...(keyName ? { keyName } : {}) }
|
||||
}
|
||||
|
||||
const orgLogin = isRecord(org) ? stringValue(org.login) : undefined
|
||||
const orgId = isRecord(org) ? stringValue(org.id) : undefined
|
||||
const userLogin =
|
||||
(isRecord(user) ? stringValue(user.userName) : undefined) ??
|
||||
(isRecord(user) ? stringValue(user.name) : undefined)
|
||||
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
||||
const search = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) search.set(key, value)
|
||||
}
|
||||
const query = search.toString()
|
||||
return `${path}${query ? `?${query}` : ""}`
|
||||
}
|
||||
|
||||
const keyName =
|
||||
stringValue(isRecord(user) ? user.keyName : undefined) ??
|
||||
stringValue(isRecord(user) ? user.displayName : undefined)
|
||||
function isHttpError(value: unknown): value is HttpErrorShape {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.__httpError === true &&
|
||||
typeof value.message === "string" &&
|
||||
typeof value.status === "number" &&
|
||||
typeof value.body === "string"
|
||||
)
|
||||
}
|
||||
|
||||
function isQuotaError(value: unknown): value is QuotaErrorShape {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.__quotaError === true &&
|
||||
(value.kind === "timeout" || value.kind === "network")
|
||||
)
|
||||
}
|
||||
|
||||
function isBlockingHttpError(error: HttpErrorShape): boolean {
|
||||
return error.status === 401 || error.status === 403
|
||||
}
|
||||
|
||||
function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
|
||||
const detail = error.body.trim().slice(0, 200)
|
||||
return {
|
||||
login: orgLogin ?? userLogin ?? "Unknown account",
|
||||
orgId: orgId ?? null,
|
||||
...(keyName === undefined ? {} : { keyName }),
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "http",
|
||||
message: redactValue(
|
||||
`${context} request failed (${error.status}): ${detail || error.message}`,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the `windowLimits` into a human-readable, header-safe line list that
|
||||
* the formatting layer appends. Split out so the pure shape is independently
|
||||
* testable.
|
||||
*/
|
||||
export function formatWindowLimits(
|
||||
limits: readonly CommandCodeWindowLimit[],
|
||||
now: () => number = Date.now,
|
||||
): string[] {
|
||||
const labels: Record<CommandCodeWindowLimit["window"], string> = {
|
||||
fiveHour: "5-hour",
|
||||
weekly: "Weekly",
|
||||
}
|
||||
class QuotaTimeoutError extends Error {}
|
||||
|
||||
return limits.map((limit) => {
|
||||
const label = labels[limit.window] ?? limit.window
|
||||
const used = limit.used.toFixed(2)
|
||||
const cap = limit.cap.toFixed(2)
|
||||
const pct = limit.cap > 0 ? Math.round((limit.used / limit.cap) * 100) : 0
|
||||
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`
|
||||
return `${label}: ${used} / ${cap} credits (${pct}% used)${reset}`
|
||||
})
|
||||
}
|
||||
|
||||
function formatResetClock(resetAtSeconds: number, now: () => number = Date.now): string {
|
||||
const date = new Date(resetAtSeconds * 1000)
|
||||
if (Number.isNaN(date.getTime())) return "unknown"
|
||||
const nowMs = now()
|
||||
const diffMs = date.getTime() - nowMs
|
||||
if (diffMs <= 0) return "soon"
|
||||
const minutes = Math.ceil(diffMs / 60_000)
|
||||
if (minutes < 60) return `in ${minutes}m`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const rem = minutes % 60
|
||||
if (hours < 24) return rem > 0 ? `in ${hours}h ${rem}m` : `in ${hours}h`
|
||||
const days = Math.floor(hours / 24)
|
||||
return days === 1 ? "in 1 day" : `in ${days} days`
|
||||
}
|
||||
|
||||
/** Derived credits view for the Remaining/Used layout. */
|
||||
interface CreditView {
|
||||
/** Credits remaining (monthly + purchased + free). */
|
||||
remaining: number
|
||||
/** Dollars spent this period (totalCost). */
|
||||
spent: number
|
||||
/** Total pool used as the percentage denominator: remaining + spent. */
|
||||
pool: number
|
||||
/** Percent of the pool used, 0-100. */
|
||||
usedPercent: number
|
||||
hasCreditsInfo: boolean
|
||||
}
|
||||
|
||||
function creditView(quota: CommandCodeQuota): CreditView {
|
||||
const credits = quota.credits
|
||||
const remaining = credits ? credits.remainingCredits : 0
|
||||
const spent = quota.summary?.totalCost ?? 0
|
||||
const pool = remaining + spent
|
||||
const hasCreditsInfo = Boolean(credits) || spent > 0
|
||||
return {
|
||||
remaining,
|
||||
spent,
|
||||
pool,
|
||||
usedPercent: hasCreditsInfo ? Math.round((pool > 0 ? spent / pool : 0) * 100) : 0,
|
||||
hasCreditsInfo,
|
||||
}
|
||||
}
|
||||
|
||||
function creditDetailLine(credits: CommandCodeCredits | null): string {
|
||||
if (!credits) return ""
|
||||
const parts = [`monthly $${credits.monthlyCredits.toFixed(2)}`]
|
||||
parts.push(`purchased $${credits.purchasedCredits.toFixed(2)}`)
|
||||
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`)
|
||||
return `Sources: ${parts.join(" / ")}`
|
||||
}
|
||||
|
||||
function subscriptionLine(subscription: CommandCodeSubscription): string {
|
||||
const rank = subscription.planId ?? "Unknown"
|
||||
const plan = rank.replace(/[_-]+/g, " ").trim()
|
||||
const status = subscription.status ? ` (${subscription.status})` : ""
|
||||
return `Plan: ${plan}${status}`
|
||||
}
|
||||
|
||||
function accountName(account: CommandCodeQuota["account"]): string {
|
||||
return account.keyName ?? account.login
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a normalized quota snapshot as clean, aligned, dashboard-style text
|
||||
* suitable for `ui.notify`. Pure so it can be unit tested without a runtime.
|
||||
*/
|
||||
export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.now): string {
|
||||
const lines: string[] = []
|
||||
|
||||
const credit = creditView(quota)
|
||||
if (credit.hasCreditsInfo) {
|
||||
lines.push("")
|
||||
lines.push("Credits")
|
||||
lines.push(padValue(`Remaining: $${credit.remaining.toFixed(2)} of $${credit.pool.toFixed(2)}`))
|
||||
lines.push(padValue(`Used: $${credit.spent.toFixed(2)}`))
|
||||
lines.push(` ${credit.usedPercent}% used`)
|
||||
}
|
||||
|
||||
const detail = creditDetailLine(quota.credits)
|
||||
if (detail) lines.push(detail)
|
||||
|
||||
if (quota.subscription) lines.push(subscriptionLine(quota.subscription))
|
||||
|
||||
if (quota.summary) {
|
||||
lines.push("")
|
||||
lines.push("Usage (this month)")
|
||||
lines.push(padValue(`Cost: $${quota.summary.totalCost.toFixed(2)}`))
|
||||
lines.push(padValue(`Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`))
|
||||
if (quota.summary.totalTokens && quota.summary.totalTokens > 0) {
|
||||
lines.push(padValue(`Tokens: ${formatTokens(quota.summary.totalTokens)}`))
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("Username")
|
||||
lines.push(padValue(accountName(quota.account)))
|
||||
|
||||
const limits = quota.credits?.windowLimits ?? []
|
||||
if (limits.length > 0) {
|
||||
lines.push("")
|
||||
lines.push("Usage windows:")
|
||||
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`))
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push(`Full detail: https://commandcode.ai/usage`)
|
||||
|
||||
// Trim leading/trailing blank lines so sections stay cleanly separated.
|
||||
while (lines.length > 0 && lines[0].length === 0) lines.shift()
|
||||
while (lines.length > 0 && lines[lines.length - 1].length === 0) lines.pop()
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function padValue(value: string): string {
|
||||
return ` ${value}`
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1_000_000_000) return `${(tokens / 1_000_000_000).toFixed(1)}B`
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`
|
||||
return String(tokens)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current account quota from Command Code.
|
||||
*
|
||||
* Resolution chain: whoami -> org id -> credits + subscription (parallel) ->
|
||||
* summary (needs the billing period start). Any individual endpoint failing
|
||||
* degrades gracefully: the remaining data is still reported, and a hard
|
||||
* failure (auth/config, network) is surfaced as a typed error.
|
||||
*/
|
||||
export async function fetchCommandCodeQuota(
|
||||
options: FetchOptions,
|
||||
): Promise<CommandCodeQuotaResult> {
|
||||
if (!options.apiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: "No Command Code API key found", kind: "config" },
|
||||
}
|
||||
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
|
||||
}
|
||||
|
||||
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
|
||||
|
||||
const overallController = new AbortController()
|
||||
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${options.apiKey}`,
|
||||
...options.extraHeaders,
|
||||
}
|
||||
|
||||
// One overall deadline shared across the sequential phases (whoami -> billing
|
||||
// -> summary) so a slow or blackholed dependency cannot compound per-request
|
||||
// timeouts into a ~45s stall; the command reports within QUOTA_TIMEOUT_MS.
|
||||
const overallController = new AbortController()
|
||||
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
|
||||
|
||||
const request = async (path: string): Promise<unknown> => {
|
||||
// The overall deadline may already have fired (e.g. a prior phase consumed
|
||||
// the budget) — AbortSignal does not replay past abort events to listeners
|
||||
// added afterward, so check synchronously instead of relying on the listener.
|
||||
if (overallController.signal.aborted) {
|
||||
throw new QuotaTimeoutError()
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
const onOverallAbort = () => controller.abort()
|
||||
overallController.signal.addEventListener("abort", onOverallAbort)
|
||||
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||
try {
|
||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
signal: overallController.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
message = "Command Code rejected the API key (401/403)"
|
||||
}
|
||||
return {
|
||||
__httpError: true,
|
||||
message,
|
||||
message:
|
||||
response.status === 401 || response.status === 403
|
||||
? "Command Code rejected the API key"
|
||||
: response.statusText,
|
||||
status: response.status,
|
||||
body: await response.text().catch(() => ""),
|
||||
}
|
||||
} satisfies HttpErrorShape
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
throw new QuotaTimeoutError()
|
||||
}
|
||||
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
overallController.signal.removeEventListener("abort", onOverallAbort)
|
||||
}
|
||||
}
|
||||
|
||||
/** Optional-endpoint wrapper: never throws. Transport/timeout/parse failures
|
||||
* become a sentinel so the rest of the dashboard still renders, matching the
|
||||
* existing graceful degradation for HTTP 5xx responses. */
|
||||
const safeRequest = async (path: string): Promise<unknown> => {
|
||||
try {
|
||||
return await request(path)
|
||||
} catch (error) {
|
||||
return {
|
||||
__quotaError: true,
|
||||
message: errorMessage(error),
|
||||
kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
|
||||
}
|
||||
} satisfies QuotaErrorShape
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const whoami = await request("/alpha/whoami")
|
||||
if (isHttpError(whoami)) return httpFailure(whoami, "whoami")
|
||||
const account = parseWhoami(whoami)
|
||||
const whoamiRaw = await request("/alpha/whoami")
|
||||
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
|
||||
const account = parseWhoami(whoamiRaw)
|
||||
if (!account) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { kind: "http", message: "Command Code returned an unrecognized account response" },
|
||||
}
|
||||
}
|
||||
|
||||
const orgId = account.orgId ?? undefined
|
||||
const creditsPath = buildUrl("/alpha/billing/credits", { orgId })
|
||||
const subPath = buildUrl("/alpha/billing/subscriptions", { orgId })
|
||||
|
||||
const [creditsRaw, subRaw] = await Promise.all([safeRequest(creditsPath), safeRequest(subPath)])
|
||||
|
||||
// Hard auth/permission failures abort; everything else (including thrown
|
||||
// network/timeout/parse failures) degrades to a null section.
|
||||
if (isHttpError(creditsRaw) && isBlockingQuotaHttpError(creditsRaw)) {
|
||||
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
||||
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
||||
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
|
||||
])
|
||||
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
||||
return httpFailure(creditsRaw, "credits")
|
||||
}
|
||||
if (isHttpError(subRaw) && isBlockingQuotaHttpError(subRaw)) {
|
||||
return httpFailure(subRaw, "subscription")
|
||||
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
||||
return httpFailure(subscriptionRaw, "subscription")
|
||||
}
|
||||
|
||||
const unavailable: CommandCodeQuotaSection[] = []
|
||||
const credits =
|
||||
creditsRaw && !isHttpError(creditsRaw) && !isQuotaError(creditsRaw)
|
||||
? parseCredits(creditsRaw)
|
||||
: null
|
||||
isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
|
||||
if (!credits) unavailable.push("credits")
|
||||
const subscription =
|
||||
subRaw && !isHttpError(subRaw) && !isQuotaError(subRaw) ? parseSubscription(subRaw) : null
|
||||
isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw)
|
||||
? null
|
||||
: parseSubscription(subscriptionRaw)
|
||||
if (!subscription) unavailable.push("subscription")
|
||||
|
||||
const since = subscription?.currentPeriodStart ?? undefined
|
||||
const summaryPath = buildUrl("/alpha/usage/summary", { orgId, since })
|
||||
const summaryRaw = await safeRequest(summaryPath)
|
||||
if (isHttpError(summaryRaw) && isBlockingQuotaHttpError(summaryRaw)) {
|
||||
const summaryRaw = await safeRequest(
|
||||
buildUrl("/alpha/usage/summary", {
|
||||
orgId,
|
||||
since: subscription?.currentPeriodStart ?? undefined,
|
||||
}),
|
||||
)
|
||||
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
||||
return httpFailure(summaryRaw, "summary")
|
||||
}
|
||||
const summary =
|
||||
summaryRaw && !isHttpError(summaryRaw) && !isQuotaError(summaryRaw)
|
||||
? parseSummary(summaryRaw)
|
||||
: null
|
||||
isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
|
||||
if (!summary) unavailable.push("usage")
|
||||
|
||||
if (credits === null && subscription === null && summary === null) {
|
||||
if (overallController.signal.aborted) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: "Command Code quota request timed out", kind: "timeout" },
|
||||
}
|
||||
}
|
||||
if (!credits && !subscription && !summary) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: "Command Code returned no usage data for the account",
|
||||
kind: "http",
|
||||
kind: overallController.signal.aborted ? "timeout" : "http",
|
||||
message: overallController.signal.aborted
|
||||
? "Command Code quota request timed out"
|
||||
: "Command Code returned no recognized usage data for the account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
quota: { account, credits, subscription, summary },
|
||||
quota: {
|
||||
account,
|
||||
credits,
|
||||
subscription,
|
||||
summary,
|
||||
...(unavailable.length > 0 ? { unavailable } : {}),
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
||||
@@ -541,69 +325,7 @@ export async function fetchCommandCodeQuota(
|
||||
}
|
||||
}
|
||||
|
||||
class QuotaTimeoutError extends Error {
|
||||
constructor() {
|
||||
super("Command Code quota request timed out")
|
||||
this.name = "QuotaTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
interface HttpErrorShape {
|
||||
__httpError: true
|
||||
message: string
|
||||
status: number
|
||||
body: string
|
||||
}
|
||||
|
||||
function isHttpError(value: unknown): value is HttpErrorShape {
|
||||
if (!isRecord(value) || value.__httpError !== true) return false
|
||||
return (
|
||||
typeof value.status === "number" &&
|
||||
typeof value.message === "string" &&
|
||||
typeof value.body === "string"
|
||||
)
|
||||
}
|
||||
|
||||
/** Sentinel produced by safeRequest for thrown transport/timeout/parse failures. */
|
||||
interface QuotaErrorShape {
|
||||
__quotaError: true
|
||||
message: string
|
||||
kind: "timeout" | "network"
|
||||
}
|
||||
|
||||
function isQuotaError(value: unknown): value is QuotaErrorShape {
|
||||
if (!isRecord(value) || value.__quotaError !== true) return false
|
||||
return typeof value.message === "string" && (value.kind === "timeout" || value.kind === "network")
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard-failure HTTP statuses for the quota dashboard: authentication and
|
||||
* permission failures. Rate limiting (429) is deliberately NOT included — it
|
||||
* is a transient dependency condition that should degrade like other non-auth
|
||||
* endpoint failures, not abort the whole command.
|
||||
*/
|
||||
function isBlockingQuotaHttpError(error: HttpErrorShape): boolean {
|
||||
return error.status === 401 || error.status === 403
|
||||
}
|
||||
|
||||
function httpFailure(error: HttpErrorShape, context: string): CommandCodeQuotaResult {
|
||||
const detail = error.body.trim().slice(0, 200)
|
||||
const message = detail
|
||||
? `${context} request failed (${error.status}): ${detail}`
|
||||
: `${context} request failed (${error.status}): ${error.message}`
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: redactValue(message), kind: "http" },
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort scrub of values that look like tokens/secrets from a message. */
|
||||
export function redactValue(value: string): string {
|
||||
// Reuse the broader Command Code redaction (Bearer, credential key-value
|
||||
// fields, user_/cc_ tokens, query-string secrets, standalone keys) so quota
|
||||
// errors get the same protection as stream errors. Additionally catch
|
||||
// JSON-quoted credential fields ({"apiKey":"..."}) that the upstream pattern
|
||||
// requires to be adjacent to `=`/`:`.
|
||||
return redactCommandCodeErrorText(value)
|
||||
.replace(
|
||||
/("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
|
||||
import { registerCommandCodeQuota, type QuotaCommandContext } from "../src/quota-command.ts"
|
||||
import type { CommandCodeQuotaResult } from "../src/quota-types.ts"
|
||||
|
||||
class CommandApiDouble {
|
||||
handler?: (args: string, ctx: QuotaCommandContext) => Promise<void>
|
||||
|
||||
registerCommand(
|
||||
name: string,
|
||||
options: {
|
||||
description: string
|
||||
handler: (args: string, ctx: QuotaCommandContext) => Promise<void>
|
||||
},
|
||||
): void {
|
||||
assert.equal(name, "commandcode-quota")
|
||||
assert.match(options.description, /usage and quota/)
|
||||
this.handler = options.handler
|
||||
}
|
||||
}
|
||||
|
||||
function context(registryKey: string | undefined) {
|
||||
const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = []
|
||||
let waited = false
|
||||
const value = {
|
||||
async waitForIdle() {
|
||||
waited = true
|
||||
},
|
||||
modelRegistry: {
|
||||
async getApiKeyForProvider(provider: string) {
|
||||
assert.equal(provider, "commandcode")
|
||||
return registryKey
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
notify(message: string, type?: "info" | "warning" | "error") {
|
||||
notifications.push({ message, type })
|
||||
},
|
||||
},
|
||||
} satisfies QuotaCommandContext
|
||||
return { value, notifications, waited: () => waited }
|
||||
}
|
||||
|
||||
const quotaResult: CommandCodeQuotaResult = {
|
||||
ok: true,
|
||||
quota: {
|
||||
account: { login: "alice", orgId: null },
|
||||
credits: null,
|
||||
subscription: null,
|
||||
summary: { totalCost: 1, totalCount: 2 },
|
||||
},
|
||||
}
|
||||
|
||||
describe("commandcode-quota command", () => {
|
||||
it("registers the command and resolves OMP placeholders through the fallback key", async () => {
|
||||
const pi = new CommandApiDouble()
|
||||
let requestKey = ""
|
||||
let requestBase = ""
|
||||
registerCommandCodeQuota(pi, {
|
||||
apiBase: "https://api.commandcode.ai",
|
||||
getConfiguredKey: () => "fallback-key",
|
||||
fetchQuota: async (options) => {
|
||||
requestKey = options.apiKey
|
||||
requestBase = options.baseUrl ?? ""
|
||||
return quotaResult
|
||||
},
|
||||
})
|
||||
|
||||
assert.ok(pi.handler)
|
||||
const ctx = context("$COMMANDCODE_API_KEY")
|
||||
await pi.handler("", ctx.value)
|
||||
assert.equal(ctx.waited(), true)
|
||||
assert.equal(requestKey, "fallback-key")
|
||||
assert.equal(requestBase, "https://api.commandcode.ai")
|
||||
assert.equal(ctx.notifications.at(-1)?.type, "info")
|
||||
assert.match(ctx.notifications.at(-1)?.message ?? "", /Requests: 2/)
|
||||
})
|
||||
|
||||
it("warns without calling the endpoint when no API key is available", async () => {
|
||||
const pi = new CommandApiDouble()
|
||||
let called = false
|
||||
registerCommandCodeQuota(pi, {
|
||||
apiBase: "https://api.commandcode.ai",
|
||||
getConfiguredKey: () => undefined,
|
||||
fetchQuota: async () => {
|
||||
called = true
|
||||
return quotaResult
|
||||
},
|
||||
})
|
||||
|
||||
assert.ok(pi.handler)
|
||||
const ctx = context(undefined)
|
||||
await pi.handler("", ctx.value)
|
||||
assert.equal(called, false)
|
||||
assert.equal(ctx.notifications.at(-1)?.type, "warning")
|
||||
assert.match(ctx.notifications.at(-1)?.message ?? "", /requires an API key/)
|
||||
})
|
||||
|
||||
it("redacts endpoint failures before notifying the host", async () => {
|
||||
const pi = new CommandApiDouble()
|
||||
registerCommandCodeQuota(pi, {
|
||||
apiBase: "https://api.commandcode.ai",
|
||||
getConfiguredKey: () => "real-key",
|
||||
fetchQuota: async () => ({
|
||||
ok: false,
|
||||
error: { kind: "http", message: "api_key=supersecretvalue123456 failed" },
|
||||
}),
|
||||
})
|
||||
|
||||
assert.ok(pi.handler)
|
||||
const ctx = context("real-key")
|
||||
await pi.handler("", ctx.value)
|
||||
assert.equal(ctx.notifications.at(-1)?.type, "error")
|
||||
assert.doesNotMatch(ctx.notifications.at(-1)?.message ?? "", /supersecret/)
|
||||
})
|
||||
})
|
||||
+30
-8
@@ -8,15 +8,18 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
|
||||
import { formatQuota, formatWindowLimits } from "../src/quota-format.ts"
|
||||
import {
|
||||
DEFAULT_API_BASE,
|
||||
fetchCommandCodeQuota,
|
||||
formatQuota,
|
||||
formatWindowLimits,
|
||||
redactValue,
|
||||
windowLimitsFromCredits,
|
||||
} from "../src/quota.ts"
|
||||
import type { CommandCodeQuota, CommandCodeCredits, CommandCodeWindowLimit } from "../src/quota.ts"
|
||||
import type {
|
||||
CommandCodeCredits,
|
||||
CommandCodeQuota,
|
||||
CommandCodeWindowLimit,
|
||||
} from "../src/quota-types.ts"
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
@@ -71,7 +74,7 @@ describe("Command Code quota", () => {
|
||||
assert.equal(limits[1]?.resetAt, 1_700_000_000)
|
||||
})
|
||||
|
||||
it("renders a zero request count instead of dropping the Requests line", () => {
|
||||
it("renders valid zero usage without claiming an unknown billing period", () => {
|
||||
const quota: CommandCodeQuota = {
|
||||
account: { login: "alice", orgId: null },
|
||||
credits: null,
|
||||
@@ -79,6 +82,8 @@ describe("Command Code quota", () => {
|
||||
summary: { totalCost: 0, totalCount: 0 },
|
||||
}
|
||||
const output = formatQuota(quota, () => 1_700_000_000_000)
|
||||
assert.match(output, /Usage\n/)
|
||||
assert.doesNotMatch(output, /billing period/)
|
||||
assert.match(output, /Requests: 0/)
|
||||
})
|
||||
|
||||
@@ -158,6 +163,20 @@ describe("Command Code quota", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("rejects unrecognized successful endpoint schemas instead of displaying zero usage", async () => {
|
||||
const fetchImpl = async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = String(input)
|
||||
if (url.includes("whoami")) return jsonResponse({ user: { userName: "alice" }, org: null })
|
||||
return jsonResponse({ changed: "schema" })
|
||||
}
|
||||
|
||||
const result = await fetchCommandCodeQuota({ apiKey: "cc_test_key", fetchImpl })
|
||||
assert.equal(result.ok, false)
|
||||
if (result.ok) return
|
||||
assert.equal(result.error.kind, "http")
|
||||
assert.match(result.error.message, /no recognized usage data/i)
|
||||
})
|
||||
|
||||
it("degrades gracefully when individual billing endpoints fail", async () => {
|
||||
const fetchImpl = async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = String(input)
|
||||
@@ -174,6 +193,8 @@ describe("Command Code quota", () => {
|
||||
if (!result.ok) return
|
||||
assert.equal(result.quota.credits, null)
|
||||
assert.equal(result.quota.summary?.totalCost, 3.0)
|
||||
assert.deepEqual(result.quota.unavailable, ["credits", "subscription"])
|
||||
assert.match(formatQuota(result.quota), /Unavailable: credits, subscription/)
|
||||
// Optional aggregate tokens are parsed when the summary reports them.
|
||||
assert.equal(result.quota.summary?.totalTokens, undefined)
|
||||
})
|
||||
@@ -194,6 +215,7 @@ describe("Command Code quota", () => {
|
||||
assert.equal(result.quota.credits, null)
|
||||
assert.equal(result.quota.subscription?.planId, "pro")
|
||||
assert.equal(result.quota.summary?.totalCost, 3.0)
|
||||
assert.deepEqual(result.quota.unavailable, ["credits"])
|
||||
})
|
||||
|
||||
it("fails the command when the summary endpoint rejects auth/permission", async () => {
|
||||
@@ -299,8 +321,8 @@ describe("Command Code quota", () => {
|
||||
subscription: {
|
||||
planId: "pro",
|
||||
status: "active",
|
||||
currentPeriodStart: "",
|
||||
currentPeriodEnd: "",
|
||||
currentPeriodStart: "2026-01-01T00:00:00Z",
|
||||
currentPeriodEnd: "2026-02-01T00:00:00Z",
|
||||
},
|
||||
summary: { totalCost: 12.34, totalCount: 1500 },
|
||||
}
|
||||
@@ -312,10 +334,10 @@ describe("Command Code quota", () => {
|
||||
assert.match(output, /Used: \$12\.34/)
|
||||
assert.match(output, /Sources: monthly \$40\.00 \/ purchased \$10\.00 \/ free \$5\.00/)
|
||||
assert.match(output, /Plan: pro \(active\)/)
|
||||
assert.match(output, /Usage \(this month\)/)
|
||||
assert.match(output, /Usage \(billing period\)/)
|
||||
assert.match(output, /Cost: \$12\.34/)
|
||||
assert.match(output, /Requests: 1,500/)
|
||||
assert.match(output, /Username/)
|
||||
assert.match(output, /Account/)
|
||||
assert.match(output, /alice-inc/)
|
||||
assert.match(output, /5-hour: 8\.00 \/ 16\.00 credits/)
|
||||
assert.match(output, /Weekly: 20\.00 \/ 40\.00 credits/)
|
||||
|
||||
Reference in New Issue
Block a user