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-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-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.
|
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 { getConfiguredApiKey } from "./src/api-key.ts"
|
||||||
import { createStreamCommandCode } from "./src/core.ts"
|
import { createStreamCommandCode } from "./src/core.ts"
|
||||||
import { calculateCommandCodeCost } from "./src/cost.ts"
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
||||||
import { pickCommandCodeApiKey } from "./src/converters.ts"
|
|
||||||
import {
|
import {
|
||||||
apiForModelId,
|
apiForModelId,
|
||||||
baseUrlForModel,
|
baseUrlForModel,
|
||||||
@@ -33,20 +32,10 @@ import {
|
|||||||
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||||
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
||||||
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.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 { createCommandCodeRuntime } from "./src/runtime.ts"
|
||||||
import { fetchCommandCodeQuota, formatQuota, redactValue } from "./src/quota.ts"
|
|
||||||
import { createCommandCodeTransportRouter } from "./src/transport.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 {
|
function commandCodeHeaders(): Record<string, string> | undefined {
|
||||||
if (process.env.COMMANDCODE_ZDR === "1") {
|
if (process.env.COMMANDCODE_ZDR === "1") {
|
||||||
return { "x-cmd-zdr": "1" }
|
return { "x-cmd-zdr": "1" }
|
||||||
@@ -136,41 +125,9 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
return normalized ? { message: normalized.message } : undefined
|
return normalized ? { message: normalized.message } : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
pi.registerCommand("commandcode-quota", {
|
registerCommandCodeQuota(pi, {
|
||||||
description: "Show Command Code account usage and quota",
|
apiBase: legacyApiBase(apiBase),
|
||||||
handler: async (_args, ctx) => {
|
headers: commandCodeHeaders(),
|
||||||
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")
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
||||||
|
|||||||
+3
-3
@@ -29,14 +29,14 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"typecheck": "tsc --noEmit",
|
||||||
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
||||||
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
||||||
"pi:isolated": "node scripts/pi-isolated.mjs",
|
"pi:isolated": "node scripts/pi-isolated.mjs",
|
||||||
"pi:authenticated": "node scripts/pi-authenticated.mjs",
|
"pi:authenticated": "node scripts/pi-authenticated.mjs",
|
||||||
"test:quota": "tsx tests/test-quota.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-retry.ts && tsx tests/test-transport.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:api-key": "tsx tests/test-api-key.ts",
|
||||||
"test:models": "tsx tests/test-models.ts",
|
"test:models": "tsx tests/test-models.ts",
|
||||||
"test:runtime": "tsx tests/test-runtime.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 { 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 DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||||
|
|
||||||
export const QUOTA_TIMEOUT_MS = 15_000
|
export const QUOTA_TIMEOUT_MS = 15_000
|
||||||
|
|
||||||
/**
|
interface FetchOptions {
|
||||||
* A single rolling usage window (the 5-hour or weekly cap on a plan's monthly
|
apiKey: string
|
||||||
* credits). Values are measured in credit value, not request count.
|
baseUrl?: string
|
||||||
*/
|
fetchImpl?: typeof fetch
|
||||||
export interface CommandCodeWindowLimit {
|
timeoutMs?: number
|
||||||
window: "fiveHour" | "weekly"
|
extraHeaders?: Record<string, string>
|
||||||
used: number
|
|
||||||
cap: number
|
|
||||||
/** Unix epoch seconds when this window resets, normalized from seconds or ms. */
|
|
||||||
resetAt: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Credits exposed by the `/alpha/billing/credits` endpoint. */
|
interface HttpErrorShape {
|
||||||
export interface CommandCodeCredits {
|
__httpError: true
|
||||||
monthlyCredits: number
|
message: string
|
||||||
purchasedCredits: number
|
status: number
|
||||||
freeCredits: number
|
body: string
|
||||||
remainingCredits: number
|
|
||||||
windowLimits: CommandCodeWindowLimit[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Subscription/plan info exposed by `/alpha/billing/subscriptions`. */
|
interface QuotaErrorShape {
|
||||||
export interface CommandCodeSubscription {
|
__quotaError: true
|
||||||
planId: string | null
|
kind: "timeout" | "network"
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
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 {
|
function numberValue(value: unknown): number | undefined {
|
||||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
|
||||||
return value >= 0 ? value : undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringValue(value: unknown): string | undefined {
|
function stringValue(value: unknown): string | undefined {
|
||||||
return typeof value === "string" && value.length > 0 ? value : undefined
|
return typeof value === "string" && value.length > 0 ? value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FetchOptions {
|
function errorMessage(error: unknown): string {
|
||||||
apiKey: string
|
return error instanceof Error ? error.message : String(error)
|
||||||
baseUrl?: string
|
|
||||||
fetchImpl?: typeof fetch
|
|
||||||
timeoutMs?: number
|
|
||||||
/** Extra HTTP headers merged after Content-Type/Authorization (e.g. ZDR). */
|
|
||||||
extraHeaders?: Record<string, string>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
function normalizeResetAt(value: unknown): number | null {
|
||||||
const search = new URLSearchParams()
|
let timestamp: number | undefined
|
||||||
for (const [key, value] of Object.entries(params)) {
|
if (typeof value === "number" && Number.isFinite(value)) timestamp = value
|
||||||
if (value !== undefined && value !== null && value !== "") search.set(key, 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()
|
if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return null
|
||||||
return `${path}${query ? `?${query}` : ""}`
|
return timestamp >= 1e12 ? Math.round(timestamp / 1000) : timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extract the WindowLimit array from the top-level `windowLimits` object. */
|
|
||||||
export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
|
export function windowLimitsFromCredits(value: unknown): CommandCodeWindowLimit[] {
|
||||||
if (!isRecord(value)) return []
|
if (!isRecord(value)) return []
|
||||||
const limits: CommandCodeWindowLimit[] = []
|
const limits: CommandCodeWindowLimit[] = []
|
||||||
|
|
||||||
for (const [window, entry] of [
|
for (const [window, entry] of [
|
||||||
["fiveHour", value.fiveHour],
|
["fiveHour", value.fiveHour],
|
||||||
["weekly", value.weekly],
|
["weekly", value.weekly],
|
||||||
] as const) {
|
] as const) {
|
||||||
if (!isRecord(entry)) continue
|
if (!isRecord(entry)) continue
|
||||||
const used = numberValue(entry.used) ?? 0
|
const used = numberValue(entry.used)
|
||||||
const cap = numberValue(entry.cap) ?? 0
|
const cap = numberValue(entry.cap)
|
||||||
if (cap <= 0 && used <= 0) continue
|
if (used === undefined || cap === undefined || (used === 0 && cap === 0)) continue
|
||||||
limits.push({
|
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) })
|
||||||
window,
|
|
||||||
used,
|
|
||||||
cap,
|
|
||||||
resetAt: normalizeResetAt(entry.resetAt),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return limits
|
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 {
|
function parseCredits(value: unknown): CommandCodeCredits | null {
|
||||||
const credits = isRecord(value) ? value.credits : undefined
|
if (!isRecord(value) || !isRecord(value.credits)) return null
|
||||||
if (!isRecord(credits)) return null
|
const credits = value.credits
|
||||||
|
const monthlyCredits = numberValue(credits.monthlyCredits)
|
||||||
// `windowLimits` is a top-level sibling of `credits` in the
|
const purchasedCredits = numberValue(credits.purchasedCredits)
|
||||||
// `/alpha/billing/credits` response, not nested inside it.
|
const freeCredits = numberValue(credits.freeCredits)
|
||||||
const windowLimits = isRecord(value) ? value.windowLimits : undefined
|
if (monthlyCredits === undefined && purchasedCredits === undefined && freeCredits === undefined) {
|
||||||
|
return null
|
||||||
const monthlyCredits = numberValue(credits.monthlyCredits) ?? 0
|
}
|
||||||
const purchasedCredits = numberValue(credits.purchasedCredits) ?? 0
|
const monthly = monthlyCredits ?? 0
|
||||||
const freeCredits = numberValue(credits.freeCredits) ?? 0
|
const purchased = purchasedCredits ?? 0
|
||||||
|
const free = freeCredits ?? 0
|
||||||
return {
|
return {
|
||||||
monthlyCredits,
|
monthlyCredits: monthly,
|
||||||
purchasedCredits,
|
purchasedCredits: purchased,
|
||||||
freeCredits,
|
freeCredits: free,
|
||||||
remainingCredits: monthlyCredits + purchasedCredits + freeCredits,
|
remainingCredits: monthly + purchased + free,
|
||||||
windowLimits: windowLimitsFromCredits(windowLimits),
|
windowLimits: windowLimitsFromCredits(value.windowLimits),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSubscription(value: unknown): CommandCodeSubscription | null {
|
function parseSubscription(value: unknown): CommandCodeSubscription | null {
|
||||||
const data = isRecord(value) ? value.data : undefined
|
if (!isRecord(value) || !isRecord(value.data)) return null
|
||||||
if (!isRecord(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 {
|
return {
|
||||||
planId: stringValue(data.planId) ?? null,
|
planId: planId ?? null,
|
||||||
status: stringValue(data.status) ?? null,
|
status: status ?? null,
|
||||||
currentPeriodStart: stringValue(data.currentPeriodStart) ?? null,
|
currentPeriodStart: currentPeriodStart ?? null,
|
||||||
currentPeriodEnd: stringValue(data.currentPeriodEnd) ?? null,
|
currentPeriodEnd: currentPeriodEnd ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSummary(value: unknown): CommandCodeUsageSummary | null {
|
function parseSummary(value: unknown): CommandCodeUsageSummary | null {
|
||||||
if (!isRecord(value)) return 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)
|
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens)
|
||||||
return {
|
return { totalCost, totalCount, ...(totalTokens === undefined ? {} : { totalTokens }) }
|
||||||
totalCost: numberValue(value.totalCost) ?? 0,
|
|
||||||
totalCount: numberValue(value.totalCount) ?? 0,
|
|
||||||
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseWhoami(value: unknown): {
|
function parseWhoami(value: unknown): {
|
||||||
login: string
|
login: string
|
||||||
orgId: string | null
|
orgId: string | null
|
||||||
keyName?: string
|
keyName?: string
|
||||||
} {
|
} | null {
|
||||||
const org = isRecord(value) ? value.org : undefined
|
if (!isRecord(value)) return null
|
||||||
const user = isRecord(value) ? value.user : undefined
|
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
|
function buildUrl(path: string, params: Record<string, string | undefined>): string {
|
||||||
const orgId = isRecord(org) ? stringValue(org.id) : undefined
|
const search = new URLSearchParams()
|
||||||
const userLogin =
|
for (const [key, value] of Object.entries(params)) {
|
||||||
(isRecord(user) ? stringValue(user.userName) : undefined) ??
|
if (value) search.set(key, value)
|
||||||
(isRecord(user) ? stringValue(user.name) : undefined)
|
}
|
||||||
|
const query = search.toString()
|
||||||
|
return `${path}${query ? `?${query}` : ""}`
|
||||||
|
}
|
||||||
|
|
||||||
const keyName =
|
function isHttpError(value: unknown): value is HttpErrorShape {
|
||||||
stringValue(isRecord(user) ? user.keyName : undefined) ??
|
return (
|
||||||
stringValue(isRecord(user) ? user.displayName : undefined)
|
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 {
|
return {
|
||||||
login: orgLogin ?? userLogin ?? "Unknown account",
|
ok: false,
|
||||||
orgId: orgId ?? null,
|
error: {
|
||||||
...(keyName === undefined ? {} : { keyName }),
|
kind: "http",
|
||||||
|
message: redactValue(
|
||||||
|
`${context} request failed (${error.status}): ${detail || error.message}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class QuotaTimeoutError extends Error {}
|
||||||
* 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",
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
export async function fetchCommandCodeQuota(
|
||||||
options: FetchOptions,
|
options: FetchOptions,
|
||||||
): Promise<CommandCodeQuotaResult> {
|
): Promise<CommandCodeQuotaResult> {
|
||||||
if (!options.apiKey) {
|
if (!options.apiKey) {
|
||||||
return {
|
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } }
|
||||||
ok: false,
|
|
||||||
error: { message: "No Command Code API key found", kind: "config" },
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
|
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE
|
||||||
const fetchImpl = options.fetchImpl ?? fetch
|
const fetchImpl = options.fetchImpl ?? fetch
|
||||||
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
|
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS
|
||||||
|
const overallController = new AbortController()
|
||||||
|
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs)
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
accept: "application/json",
|
||||||
Authorization: `Bearer ${options.apiKey}`,
|
Authorization: `Bearer ${options.apiKey}`,
|
||||||
...options.extraHeaders,
|
...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> => {
|
const request = async (path: string): Promise<unknown> => {
|
||||||
// The overall deadline may already have fired (e.g. a prior phase consumed
|
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||||
// 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)
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers,
|
headers,
|
||||||
signal: controller.signal,
|
signal: overallController.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let message = response.statusText
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
message = "Command Code rejected the API key (401/403)"
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
__httpError: true,
|
__httpError: true,
|
||||||
message,
|
message:
|
||||||
|
response.status === 401 || response.status === 403
|
||||||
|
? "Command Code rejected the API key"
|
||||||
|
: response.statusText,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
body: await response.text().catch(() => ""),
|
body: await response.text().catch(() => ""),
|
||||||
}
|
} satisfies HttpErrorShape
|
||||||
}
|
}
|
||||||
return await response.json()
|
return await response.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (controller.signal.aborted) {
|
if (overallController.signal.aborted) throw new QuotaTimeoutError()
|
||||||
throw new QuotaTimeoutError()
|
|
||||||
}
|
|
||||||
throw error
|
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> => {
|
const safeRequest = async (path: string): Promise<unknown> => {
|
||||||
try {
|
try {
|
||||||
return await request(path)
|
return await request(path)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
__quotaError: true,
|
__quotaError: true,
|
||||||
message: errorMessage(error),
|
|
||||||
kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
|
kind: error instanceof QuotaTimeoutError ? "timeout" : "network",
|
||||||
}
|
} satisfies QuotaErrorShape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const whoami = await request("/alpha/whoami")
|
const whoamiRaw = await request("/alpha/whoami")
|
||||||
if (isHttpError(whoami)) return httpFailure(whoami, "whoami")
|
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami")
|
||||||
const account = parseWhoami(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 orgId = account.orgId ?? undefined
|
||||||
const creditsPath = buildUrl("/alpha/billing/credits", { orgId })
|
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
||||||
const subPath = buildUrl("/alpha/billing/subscriptions", { orgId })
|
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
||||||
|
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId })),
|
||||||
const [creditsRaw, subRaw] = await Promise.all([safeRequest(creditsPath), safeRequest(subPath)])
|
])
|
||||||
|
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
||||||
// Hard auth/permission failures abort; everything else (including thrown
|
|
||||||
// network/timeout/parse failures) degrades to a null section.
|
|
||||||
if (isHttpError(creditsRaw) && isBlockingQuotaHttpError(creditsRaw)) {
|
|
||||||
return httpFailure(creditsRaw, "credits")
|
return httpFailure(creditsRaw, "credits")
|
||||||
}
|
}
|
||||||
if (isHttpError(subRaw) && isBlockingQuotaHttpError(subRaw)) {
|
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
||||||
return httpFailure(subRaw, "subscription")
|
return httpFailure(subscriptionRaw, "subscription")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unavailable: CommandCodeQuotaSection[] = []
|
||||||
const credits =
|
const credits =
|
||||||
creditsRaw && !isHttpError(creditsRaw) && !isQuotaError(creditsRaw)
|
isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw)
|
||||||
? parseCredits(creditsRaw)
|
if (!credits) unavailable.push("credits")
|
||||||
: null
|
|
||||||
const subscription =
|
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 summaryRaw = await safeRequest(
|
||||||
const summaryPath = buildUrl("/alpha/usage/summary", { orgId, since })
|
buildUrl("/alpha/usage/summary", {
|
||||||
const summaryRaw = await safeRequest(summaryPath)
|
orgId,
|
||||||
if (isHttpError(summaryRaw) && isBlockingQuotaHttpError(summaryRaw)) {
|
since: subscription?.currentPeriodStart ?? undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
||||||
return httpFailure(summaryRaw, "summary")
|
return httpFailure(summaryRaw, "summary")
|
||||||
}
|
}
|
||||||
const summary =
|
const summary =
|
||||||
summaryRaw && !isHttpError(summaryRaw) && !isQuotaError(summaryRaw)
|
isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw)
|
||||||
? parseSummary(summaryRaw)
|
if (!summary) unavailable.push("usage")
|
||||||
: null
|
|
||||||
|
|
||||||
if (credits === null && subscription === null && summary === null) {
|
if (!credits && !subscription && !summary) {
|
||||||
if (overallController.signal.aborted) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: { message: "Command Code quota request timed out", kind: "timeout" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: {
|
error: {
|
||||||
message: "Command Code returned no usage data for the account",
|
kind: overallController.signal.aborted ? "timeout" : "http",
|
||||||
kind: "http",
|
message: overallController.signal.aborted
|
||||||
|
? "Command Code quota request timed out"
|
||||||
|
: "Command Code returned no recognized usage data for the account",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
quota: { account, credits, subscription, summary },
|
quota: {
|
||||||
|
account,
|
||||||
|
credits,
|
||||||
|
subscription,
|
||||||
|
summary,
|
||||||
|
...(unavailable.length > 0 ? { unavailable } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
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 {
|
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)
|
return redactCommandCodeErrorText(value)
|
||||||
.replace(
|
.replace(
|
||||||
/("\s*(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*"\s*:\s*")([^"]{8,})/gi,
|
/("\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 assert from "node:assert/strict"
|
||||||
import { describe, it } from "node:test"
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import { formatQuota, formatWindowLimits } from "../src/quota-format.ts"
|
||||||
import {
|
import {
|
||||||
DEFAULT_API_BASE,
|
DEFAULT_API_BASE,
|
||||||
fetchCommandCodeQuota,
|
fetchCommandCodeQuota,
|
||||||
formatQuota,
|
|
||||||
formatWindowLimits,
|
|
||||||
redactValue,
|
redactValue,
|
||||||
windowLimitsFromCredits,
|
windowLimitsFromCredits,
|
||||||
} from "../src/quota.ts"
|
} 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 {
|
function jsonResponse(body: unknown, status = 200): Response {
|
||||||
return new Response(JSON.stringify(body), {
|
return new Response(JSON.stringify(body), {
|
||||||
@@ -71,7 +74,7 @@ describe("Command Code quota", () => {
|
|||||||
assert.equal(limits[1]?.resetAt, 1_700_000_000)
|
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 = {
|
const quota: CommandCodeQuota = {
|
||||||
account: { login: "alice", orgId: null },
|
account: { login: "alice", orgId: null },
|
||||||
credits: null,
|
credits: null,
|
||||||
@@ -79,6 +82,8 @@ describe("Command Code quota", () => {
|
|||||||
summary: { totalCost: 0, totalCount: 0 },
|
summary: { totalCost: 0, totalCount: 0 },
|
||||||
}
|
}
|
||||||
const output = formatQuota(quota, () => 1_700_000_000_000)
|
const output = formatQuota(quota, () => 1_700_000_000_000)
|
||||||
|
assert.match(output, /Usage\n/)
|
||||||
|
assert.doesNotMatch(output, /billing period/)
|
||||||
assert.match(output, /Requests: 0/)
|
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 () => {
|
it("degrades gracefully when individual billing endpoints fail", async () => {
|
||||||
const fetchImpl = async (input: RequestInfo | URL): Promise<Response> => {
|
const fetchImpl = async (input: RequestInfo | URL): Promise<Response> => {
|
||||||
const url = String(input)
|
const url = String(input)
|
||||||
@@ -174,6 +193,8 @@ describe("Command Code quota", () => {
|
|||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
assert.equal(result.quota.credits, null)
|
assert.equal(result.quota.credits, null)
|
||||||
assert.equal(result.quota.summary?.totalCost, 3.0)
|
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.
|
// Optional aggregate tokens are parsed when the summary reports them.
|
||||||
assert.equal(result.quota.summary?.totalTokens, undefined)
|
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.credits, null)
|
||||||
assert.equal(result.quota.subscription?.planId, "pro")
|
assert.equal(result.quota.subscription?.planId, "pro")
|
||||||
assert.equal(result.quota.summary?.totalCost, 3.0)
|
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 () => {
|
it("fails the command when the summary endpoint rejects auth/permission", async () => {
|
||||||
@@ -299,8 +321,8 @@ describe("Command Code quota", () => {
|
|||||||
subscription: {
|
subscription: {
|
||||||
planId: "pro",
|
planId: "pro",
|
||||||
status: "active",
|
status: "active",
|
||||||
currentPeriodStart: "",
|
currentPeriodStart: "2026-01-01T00:00:00Z",
|
||||||
currentPeriodEnd: "",
|
currentPeriodEnd: "2026-02-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
summary: { totalCost: 12.34, totalCount: 1500 },
|
summary: { totalCost: 12.34, totalCount: 1500 },
|
||||||
}
|
}
|
||||||
@@ -312,10 +334,10 @@ describe("Command Code quota", () => {
|
|||||||
assert.match(output, /Used: \$12\.34/)
|
assert.match(output, /Used: \$12\.34/)
|
||||||
assert.match(output, /Sources: monthly \$40\.00 \/ purchased \$10\.00 \/ free \$5\.00/)
|
assert.match(output, /Sources: monthly \$40\.00 \/ purchased \$10\.00 \/ free \$5\.00/)
|
||||||
assert.match(output, /Plan: pro \(active\)/)
|
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, /Cost: \$12\.34/)
|
||||||
assert.match(output, /Requests: 1,500/)
|
assert.match(output, /Requests: 1,500/)
|
||||||
assert.match(output, /Username/)
|
assert.match(output, /Account/)
|
||||||
assert.match(output, /alice-inc/)
|
assert.match(output, /alice-inc/)
|
||||||
assert.match(output, /5-hour: 8\.00 \/ 16\.00 credits/)
|
assert.match(output, /5-hour: 8\.00 \/ 16\.00 credits/)
|
||||||
assert.match(output, /Weekly: 20\.00 \/ 40\.00 credits/)
|
assert.match(output, /Weekly: 20\.00 \/ 40\.00 credits/)
|
||||||
|
|||||||
Reference in New Issue
Block a user