feat(commandcode): quota usage dashboard, CLI-parity request, and connect timeout fixes
- Add CommandCode usage handler mirroring the official CLI /usage
(whoami → credits/subscriptions → summary) with 5h/weekly/monthly
quota rows, registered in services/usage.js and registry config
- Parse commandcode quota rows in ProviderLimits (remainingPercentage + $ unit)
- Match official CLI request shape: x-command-code-version 1.10.0,
User-Agent cli, and config.environment '${platform}-${arch}, Node.js ${version}'
- Fix connect timeout unit confusion: both profile and provider pages
now use ms with a 1s minimum guard (prevents 60ms footgun)
- Fix fetchT0 ReferenceError in base.js error path and log fetch
diagnostics only on upstream failure
- Quota Tracker defaults to the Active account filter
- Ignore .commandcode/ CLI local state
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -86,3 +86,6 @@ graphify-out/*
|
|||||||
.codegraph/
|
.codegraph/
|
||||||
.PR/
|
.PR/
|
||||||
.next-analyze/*
|
.next-analyze/*
|
||||||
|
|
||||||
|
# CommandCode CLI local state (auth/taste/projects)
|
||||||
|
.commandcode/
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ export class BaseExecutor {
|
|||||||
const timeoutMs = await resolveProviderTimeoutMs(this.provider, this.config?.timeoutMs, FETCH_CONNECT_TIMEOUT_MS);
|
const timeoutMs = await resolveProviderTimeoutMs(this.provider, this.config?.timeoutMs, FETCH_CONNECT_TIMEOUT_MS);
|
||||||
const connectTimer = setTimeout(() => connectCtrl.abort(new Error("fetch connect timeout")), timeoutMs);
|
const connectTimer = setTimeout(() => connectCtrl.abort(new Error("fetch connect timeout")), timeoutMs);
|
||||||
const mergedSignal = signal ? AbortSignal.any([signal, connectCtrl.signal]) : connectCtrl.signal;
|
const mergedSignal = signal ? AbortSignal.any([signal, connectCtrl.signal]) : connectCtrl.signal;
|
||||||
|
let fetchT0 = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bodyStr = JSON.stringify(transformedBody);
|
const bodyStr = JSON.stringify(transformedBody);
|
||||||
const fetchT0 = Date.now();
|
fetchT0 = Date.now();
|
||||||
dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${timeoutMs}ms`);
|
dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${timeoutMs}ms`);
|
||||||
const response = await proxyAwareFetch(url, {
|
const response = await proxyAwareFetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -165,6 +166,11 @@ export class BaseExecutor {
|
|||||||
clearTimeout(connectTimer);
|
clearTimeout(connectTimer);
|
||||||
lastError = error;
|
lastError = error;
|
||||||
const isConnectTimeout = connectCtrl.signal.aborted && error.name === "AbortError";
|
const isConnectTimeout = connectCtrl.signal.aborted && error.name === "AbortError";
|
||||||
|
// Error diagnostic — only logs on actual upstream failure. Distinguishes
|
||||||
|
// undici connect timeout (UND_ERR_CONNECT_TIMEOUT), DNS (ENOTFOUND),
|
||||||
|
// refused (ECONNREFUSED) vs our own connectCtrl abort (AbortError).
|
||||||
|
const cause = error?.cause || {};
|
||||||
|
console.log(`[FETCH-DIAG] ${this.provider} fetch error | name=${error.name} | code=${error.code ?? cause?.code ?? "none"} | msg=${String(error.message).slice(0, 120)} | connectTimeout=${timeoutMs}ms | elapsed=${Date.now() - fetchT0}ms`);
|
||||||
dbg("FETCH", `${this.provider.toUpperCase()} ✖ ${error.name}: ${error.message}${isConnectTimeout ? " (connect timeout)" : ""}`);
|
dbg("FETCH", `${this.provider.toUpperCase()} ✖ ${error.name}: ${error.message}${isConnectTimeout ? " (connect timeout)" : ""}`);
|
||||||
// Connect timeout is internal — convert to retryable network error, don't propagate AbortError
|
// Connect timeout is internal — convert to retryable network error, don't propagate AbortError
|
||||||
if (error.name === "AbortError" && !isConnectTimeout) throw error;
|
if (error.name === "AbortError" && !isConnectTimeout) throw error;
|
||||||
|
|||||||
@@ -23,9 +23,24 @@ export default {
|
|||||||
format: "commandcode",
|
format: "commandcode",
|
||||||
forceStream: true,
|
forceStream: true,
|
||||||
headers: {
|
headers: {
|
||||||
"x-command-code-version": "0.25.7",
|
"x-command-code-version": "1.10.0",
|
||||||
"x-cli-environment": "cli",
|
"x-cli-environment": "cli",
|
||||||
|
"User-Agent": "cli",
|
||||||
},
|
},
|
||||||
|
// Quota/billing endpoints (same alpha API the official CLI /usage calls).
|
||||||
|
// whoami resolves orgId; credits+subscription+usage/summary then report the
|
||||||
|
// 5-hour/weekly windows, plan, and period credits. See services/usage/commandcode.js.
|
||||||
|
usage: {
|
||||||
|
baseUrl: "https://api.commandcode.ai",
|
||||||
|
whoamiUrl: "/alpha/whoami",
|
||||||
|
creditsUrl: "/alpha/billing/credits",
|
||||||
|
subscriptionsUrl: "/alpha/billing/subscriptions",
|
||||||
|
summaryUrl: "/alpha/usage/summary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
usage: true,
|
||||||
|
usageApikey: true,
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { getXaiUsage } from "./usage/xai.js";
|
|||||||
import { getGrokCliUsage } from "./usage/grok-cli.js";
|
import { getGrokCliUsage } from "./usage/grok-cli.js";
|
||||||
import { getKimiUsage } from "./usage/kimi.js";
|
import { getKimiUsage } from "./usage/kimi.js";
|
||||||
import { getDeepseekUsage } from "./usage/deepseek.js";
|
import { getDeepseekUsage } from "./usage/deepseek.js";
|
||||||
|
import { getCommandCodeUsage } from "./usage/commandcode.js";
|
||||||
import {
|
import {
|
||||||
getQwenUsage,
|
getQwenUsage,
|
||||||
getIflowUsage,
|
getIflowUsage,
|
||||||
@@ -51,6 +52,7 @@ const USAGE_HANDLERS = {
|
|||||||
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||||
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
|
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
|
||||||
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
||||||
|
commandcode: (c) => getCommandCodeUsage(c.apiKey, c.proxyOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getUsageForProvider(connection, proxyOptions = null) {
|
export async function getUsageForProvider(connection, proxyOptions = null) {
|
||||||
|
|||||||
203
open-sse/services/usage/commandcode.js
Normal file
203
open-sse/services/usage/commandcode.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* CommandCode usage handler
|
||||||
|
*
|
||||||
|
* Mirrors the official command-code CLI /usage command: it calls the alpha API
|
||||||
|
* to surface the 5-hour + weekly usage windows, the subscription plan, and the
|
||||||
|
* credits consumed in the current billing period.
|
||||||
|
*
|
||||||
|
* GET /alpha/whoami → org.id (org-scoped billing; null for personal)
|
||||||
|
* GET /alpha/billing/credits → { credits: { monthlyCredits, purchasedCredits,
|
||||||
|
* freeCredits }, windowLimits: { fiveHour, weekly } }
|
||||||
|
* GET /alpha/billing/subscriptions → { data: { planId, currentPeriodStart, ... } }
|
||||||
|
* GET /alpha/usage/summary?since= → period token/cost totals
|
||||||
|
*
|
||||||
|
* The CLI fetches whoami first (for orgId), then credits + subscription in
|
||||||
|
* parallel, then the summary with since = currentPeriodStart. We keep the same
|
||||||
|
* order/dependencies: window limits live on credits, and the plan period start
|
||||||
|
* determines the summary window.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||||
|
import { U, parseResetTime } from "./shared.js";
|
||||||
|
|
||||||
|
const USAGE = U("commandcode");
|
||||||
|
const BASE = USAGE.baseUrl || "https://api.commandcode.ai";
|
||||||
|
const WHOAMI_URL = BASE + (USAGE.whoamiUrl || "/alpha/whoami");
|
||||||
|
const CREDITS_URL = BASE + (USAGE.creditsUrl || "/alpha/billing/credits");
|
||||||
|
const SUBSCRIPTIONS_URL =
|
||||||
|
BASE + (USAGE.subscriptionsUrl || "/alpha/billing/subscriptions");
|
||||||
|
const SUMMARY_URL = BASE + (USAGE.summaryUrl || "/alpha/usage/summary");
|
||||||
|
|
||||||
|
function buildHeaders(token) {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a normalized quota row. `unit` is "$" — the API reports currency credits. */
|
||||||
|
function makeQuota({ used, total, resetAt, unlimited = false, unit = "$" }) {
|
||||||
|
const safeTotal = Math.max(0, Number(total) || 0);
|
||||||
|
const safeUsed = Math.max(0, Number(used) || 0);
|
||||||
|
if (unlimited || safeTotal === 0) {
|
||||||
|
return {
|
||||||
|
used: safeUsed,
|
||||||
|
total: 0,
|
||||||
|
remainingPercentage: unlimited ? 100 : 0,
|
||||||
|
resetAt: resetAt || null,
|
||||||
|
unit,
|
||||||
|
unlimited: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const remaining = Math.max(0, safeTotal - safeUsed);
|
||||||
|
const remainingPercentage = (remaining / safeTotal) * 100;
|
||||||
|
return {
|
||||||
|
used: safeUsed,
|
||||||
|
total: safeTotal,
|
||||||
|
remainingPercentage,
|
||||||
|
resetAt: resetAt || null,
|
||||||
|
unit,
|
||||||
|
unlimited: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} apiKey - commandcode API key (user_...)
|
||||||
|
* @param {object|null} proxyOptions
|
||||||
|
*/
|
||||||
|
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
||||||
|
if (!apiKey) {
|
||||||
|
return { message: "CommandCode credential not available." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = buildHeaders(apiKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// whoami resolves the org id (billing is org-scoped; null for personal).
|
||||||
|
const whoamiRes = await proxyAwareFetch(
|
||||||
|
WHOAMI_URL,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
);
|
||||||
|
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
||||||
|
return { message: "CommandCode credential invalid or expired." };
|
||||||
|
}
|
||||||
|
if (!whoamiRes.ok) {
|
||||||
|
return { message: `CommandCode whoami API error (${whoamiRes.status}).` };
|
||||||
|
}
|
||||||
|
const whoami = await whoamiRes.json().catch(() => null);
|
||||||
|
const orgId = whoami?.org?.id ?? null;
|
||||||
|
|
||||||
|
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
||||||
|
|
||||||
|
const [creditsRes, subsRes] = await Promise.all([
|
||||||
|
proxyAwareFetch(
|
||||||
|
CREDITS_URL + orgQuery,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
),
|
||||||
|
proxyAwareFetch(
|
||||||
|
SUBSCRIPTIONS_URL + orgQuery,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
creditsRes.status === 401 ||
|
||||||
|
creditsRes.status === 403 ||
|
||||||
|
subsRes.status === 401 ||
|
||||||
|
subsRes.status === 403
|
||||||
|
) {
|
||||||
|
return { message: "CommandCode credential invalid or expired." };
|
||||||
|
}
|
||||||
|
if (!creditsRes.ok) {
|
||||||
|
return {
|
||||||
|
message: `CommandCode credits API error (${creditsRes.status}).`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const credits = await creditsRes.json().catch(() => null);
|
||||||
|
const subs = await subsRes.json().catch(() => null);
|
||||||
|
|
||||||
|
const subData = subs?.data;
|
||||||
|
const planId = subData?.planId ?? null;
|
||||||
|
const periodStart = subData?.currentPeriodStart ?? null;
|
||||||
|
|
||||||
|
// Summary needs `since`; the CLI falls back to first-of-month when the
|
||||||
|
// subscription period start is unavailable.
|
||||||
|
const since = periodStart || firstOfMonth();
|
||||||
|
const summaryRes = await proxyAwareFetch(
|
||||||
|
`${SUMMARY_URL}?since=${encodeURIComponent(since)}`,
|
||||||
|
{ method: "GET", headers },
|
||||||
|
proxyOptions,
|
||||||
|
);
|
||||||
|
const summary = summaryRes.ok
|
||||||
|
? await summaryRes.json().catch(() => null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const quotas = {};
|
||||||
|
const windowLimits = credits?.windowLimits || {};
|
||||||
|
|
||||||
|
const fiveHour = windowLimits.fiveHour;
|
||||||
|
if (fiveHour && Number(fiveHour.cap) > 0) {
|
||||||
|
quotas["5-hour window"] = makeQuota({
|
||||||
|
used: fiveHour.used,
|
||||||
|
total: fiveHour.cap,
|
||||||
|
resetAt: parseResetTime(fiveHour.resetAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekly = windowLimits.weekly;
|
||||||
|
if (weekly && Number(weekly.cap) > 0) {
|
||||||
|
quotas["Weekly window"] = makeQuota({
|
||||||
|
used: weekly.used,
|
||||||
|
total: weekly.cap,
|
||||||
|
resetAt: parseResetTime(weekly.resetAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monthly credits consumed this billing period (from summary when present,
|
||||||
|
// else the credits object's monthlyCredits as a fallback).
|
||||||
|
const monthlyUsed =
|
||||||
|
typeof summary?.totalCredits === "number"
|
||||||
|
? summary.totalCredits
|
||||||
|
: typeof credits?.credits?.monthlyCredits === "number"
|
||||||
|
? credits.credits.monthlyCredits
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const monthlyTotal =
|
||||||
|
typeof credits?.credits?.monthlyCredits === "number"
|
||||||
|
? credits.credits.monthlyCredits
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (monthlyTotal > 0 || monthlyUsed > 0) {
|
||||||
|
quotas["Monthly credits"] = makeQuota({
|
||||||
|
used: monthlyUsed,
|
||||||
|
total: monthlyTotal,
|
||||||
|
resetAt: periodStart ? undefined : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(quotas).length === 0) {
|
||||||
|
return {
|
||||||
|
plan: planId || "CommandCode",
|
||||||
|
message: "CommandCode connected, but no quota was reported.",
|
||||||
|
quotas: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan: planId || "CommandCode",
|
||||||
|
quotas,
|
||||||
|
periodBasis: summary?.periodBasis || "billing-period",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { message: `CommandCode usage error: ${error.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstOfMonth() {
|
||||||
|
const now = new Date();
|
||||||
|
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
||||||
|
}
|
||||||
@@ -191,13 +191,17 @@ export function openaiToCommandCodeRequest(
|
|||||||
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
// environment format mirrors the official command-code CLI (getEnvironmentInfo):
|
||||||
|
// `${platform}-${arch}, Node.js ${version}` (e.g. "darwin-arm64, Node.js v24.16.0").
|
||||||
|
const environment = `${process.platform}-${process.arch}, Node.js ${process.version}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId: randomUUID(),
|
threadId: randomUUID(),
|
||||||
memory: "",
|
memory: "",
|
||||||
config: {
|
config: {
|
||||||
workingDir: process.cwd(),
|
workingDir: process.cwd(),
|
||||||
date: today,
|
date: today,
|
||||||
environment: process.platform,
|
environment,
|
||||||
structure: [],
|
structure: [],
|
||||||
isGitRepo: false,
|
isGitRepo: false,
|
||||||
currentBranch: "",
|
currentBranch: "",
|
||||||
|
|||||||
@@ -288,9 +288,11 @@ export default function ProfilePage() {
|
|||||||
const handleGlobalTimeoutChange = async (e) => {
|
const handleGlobalTimeoutChange = async (e) => {
|
||||||
const raw = e.target.value.replace(/[^0-9]/g, "");
|
const raw = e.target.value.replace(/[^0-9]/g, "");
|
||||||
const numTimeout = parseInt(raw, 10);
|
const numTimeout = parseInt(raw, 10);
|
||||||
|
// Enforce a sane minimum (1s) so a stray "60" never becomes a 60ms
|
||||||
|
// connect timeout — same guard as the per-provider timeout input.
|
||||||
const patchValue =
|
const patchValue =
|
||||||
raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0
|
raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0
|
||||||
? numTimeout
|
? Math.max(1000, numTimeout)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ export default function ProviderDetailPage() {
|
|||||||
const thinkingCfg =
|
const thinkingCfg =
|
||||||
(settingsData.providerThinking || {})[providerId] || {};
|
(settingsData.providerThinking || {})[providerId] || {};
|
||||||
setThinkingMode(thinkingCfg.mode || "auto");
|
setThinkingMode(thinkingCfg.mode || "auto");
|
||||||
// Load per-provider connect timeout
|
// Load per-provider connect timeout (ms)
|
||||||
const timeoutCfg =
|
const timeoutCfg =
|
||||||
(settingsData.providerTimeouts || {})[providerId] || {};
|
(settingsData.providerTimeouts || {})[providerId] || {};
|
||||||
setProviderTimeout(
|
setProviderTimeout(
|
||||||
@@ -577,15 +577,13 @@ export default function ProviderDetailPage() {
|
|||||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||||
const current = settingsData.providerTimeouts || {};
|
const current = settingsData.providerTimeouts || {};
|
||||||
const updated = { ...current };
|
const updated = { ...current };
|
||||||
if (!ms || ms === "") {
|
const timeoutMs = parseInt(ms, 10);
|
||||||
delete updated[providerId];
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
||||||
|
// Stored in ms; enforce a sane minimum (1s) so a stray "60" means
|
||||||
|
// 60 SECONDS-worth of protection — never a 60ms connect timeout.
|
||||||
|
updated[providerId] = { timeoutMs: Math.max(1000, timeoutMs) };
|
||||||
} else {
|
} else {
|
||||||
const timeoutMs = parseInt(ms, 10);
|
delete updated[providerId];
|
||||||
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
||||||
updated[providerId] = { timeoutMs };
|
|
||||||
} else {
|
|
||||||
delete updated[providerId];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await fetch("/api/settings", {
|
await fetch("/api/settings", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export default function ProviderLimits() {
|
|||||||
const [proxyPools, setProxyPools] = useState([]);
|
const [proxyPools, setProxyPools] = useState([]);
|
||||||
const [providerFilter, setProviderFilter] = useState("all");
|
const [providerFilter, setProviderFilter] = useState("all");
|
||||||
const [providerOptions, setProviderOptions] = useState([]);
|
const [providerOptions, setProviderOptions] = useState([]);
|
||||||
const [accountFilter, setAccountFilter] = useState("all");
|
const [accountFilter, setAccountFilter] = useState("active");
|
||||||
const [quotaSortMode, setQuotaSortMode] = useState("default");
|
const [quotaSortMode, setQuotaSortMode] = useState("default");
|
||||||
const [quotaVisibility, setQuotaVisibility] = useState({});
|
const [quotaVisibility, setQuotaVisibility] = useState({});
|
||||||
const [expiringFirst, setExpiringFirst] = useState(false);
|
const [expiringFirst, setExpiringFirst] = useState(false);
|
||||||
|
|||||||
@@ -564,6 +564,24 @@ export function parseQuotaData(provider, data) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "commandcode":
|
||||||
|
// CommandCode reports currency credits (5-hour/weekly windows + monthly
|
||||||
|
// credits) with used/total in dollars. Forward remainingPercentage (the
|
||||||
|
// UI would otherwise render "$0.05" balances as "0%") and unit "$".
|
||||||
|
if (data.quotas) {
|
||||||
|
Object.entries(data.quotas).forEach(([name, quota]) => {
|
||||||
|
normalizedQuotas.push({
|
||||||
|
name,
|
||||||
|
used: quota.used || 0,
|
||||||
|
total: quota.total || 0,
|
||||||
|
unit: quota.unit || "$",
|
||||||
|
resetAt: quota.resetAt || null,
|
||||||
|
remainingPercentage: quota.remainingPercentage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Generic fallback for unknown providers
|
// Generic fallback for unknown providers
|
||||||
if (data.quotas) {
|
if (data.quotas) {
|
||||||
|
|||||||
218
tests/unit/commandcode-usage.test.js
Normal file
218
tests/unit/commandcode-usage.test.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||||
|
proxyAwareFetch: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
|
||||||
|
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||||
|
import {
|
||||||
|
USAGE_SUPPORTED_PROVIDERS,
|
||||||
|
USAGE_APIKEY_PROVIDERS,
|
||||||
|
} from "../../src/shared/constants/providers.js";
|
||||||
|
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||||
|
|
||||||
|
const BASE = "https://api.commandcode.ai";
|
||||||
|
const WHOAMI_URL = `${BASE}/alpha/whoami`;
|
||||||
|
const CREDITS_URL = `${BASE}/alpha/billing/credits`;
|
||||||
|
const SUBS_URL = `${BASE}/alpha/billing/subscriptions`;
|
||||||
|
const SUMMARY_URL = `${BASE}/alpha/usage/summary`;
|
||||||
|
|
||||||
|
function jsonResponse(body, status = 200) {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const WHOAMI = { success: true, user: { id: "u1" }, org: null };
|
||||||
|
const CREDITS = {
|
||||||
|
credits: {
|
||||||
|
belowThreshold: false,
|
||||||
|
creditThreshold: 0,
|
||||||
|
monthlyCredits: 9.9,
|
||||||
|
purchasedCredits: 0,
|
||||||
|
freeCredits: 0,
|
||||||
|
},
|
||||||
|
windowLimits: {
|
||||||
|
limited: true,
|
||||||
|
exceeded: null,
|
||||||
|
fiveHour: { used: 0.05, cap: 3, exceeded: false, resetAt: 1785812386064 },
|
||||||
|
weekly: { used: 0.1, cap: 6, exceeded: false, resetAt: 1786379982640 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const SUBS = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: "sub_1",
|
||||||
|
status: "active",
|
||||||
|
orgId: null,
|
||||||
|
planId: "individual-go",
|
||||||
|
currentPeriodStart: "2026-08-03T16:38:16.000Z",
|
||||||
|
currentPeriodEnd: "2026-09-03T16:38:16.000Z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const SUMMARY = {
|
||||||
|
totalCount: 61,
|
||||||
|
totalCost: 0.1,
|
||||||
|
totalCredits: 0.1,
|
||||||
|
totalMonthlyCredits: 0.1,
|
||||||
|
periodBasis: "billing-period",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("commandcode registry usage flags", () => {
|
||||||
|
it("is listed for apikey quota dashboard", () => {
|
||||||
|
expect(USAGE_SUPPORTED_PROVIDERS).toContain("commandcode");
|
||||||
|
expect(USAGE_APIKEY_PROVIDERS).toContain("commandcode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUsageForProvider(commandcode)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches whoami → credits+subs → summary and maps windows + credits", async () => {
|
||||||
|
proxyAwareFetch
|
||||||
|
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_cc_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.message).toBeUndefined();
|
||||||
|
expect(usage.plan).toBe("individual-go");
|
||||||
|
expect(usage.periodBasis).toBe("billing-period");
|
||||||
|
|
||||||
|
expect(proxyAwareFetch).toHaveBeenCalledTimes(4);
|
||||||
|
const [whoamiUrl, whoamiOpts] = proxyAwareFetch.mock.calls[0];
|
||||||
|
expect(whoamiUrl).toBe(WHOAMI_URL);
|
||||||
|
expect(whoamiOpts.headers.Authorization).toBe("Bearer user_cc_test");
|
||||||
|
|
||||||
|
// No org → credits/subscriptions called without orgId query
|
||||||
|
const creditsCall = proxyAwareFetch.mock.calls[1][0];
|
||||||
|
expect(creditsCall).toBe(CREDITS_URL);
|
||||||
|
|
||||||
|
// Summary uses currentPeriodStart as `since`
|
||||||
|
const summaryCall = proxyAwareFetch.mock.calls[3][0];
|
||||||
|
expect(summaryCall).toBe(
|
||||||
|
`${SUMMARY_URL}?since=${encodeURIComponent("2026-08-03T16:38:16.000Z")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(usage.quotas["5-hour window"]).toMatchObject({
|
||||||
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
resetAt: new Date(1785812386064).toISOString(),
|
||||||
|
});
|
||||||
|
expect(usage.quotas["Weekly window"]).toMatchObject({
|
||||||
|
used: 0.1,
|
||||||
|
total: 6,
|
||||||
|
resetAt: new Date(1786379982640).toISOString(),
|
||||||
|
});
|
||||||
|
expect(usage.quotas["Monthly credits"]).toMatchObject({
|
||||||
|
used: 0.1,
|
||||||
|
total: 9.9,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds orgId query when whoami returns an org", async () => {
|
||||||
|
proxyAwareFetch
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
jsonResponse({ success: true, org: { id: "org_1" } }),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUBS))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
|
await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_cc_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(proxyAwareFetch.mock.calls[1][0]).toBe(`${CREDITS_URL}?orgId=org_1`);
|
||||||
|
expect(proxyAwareFetch.mock.calls[2][0]).toBe(`${SUBS_URL}?orgId=org_1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to first-of-month since when subscription has no period start", async () => {
|
||||||
|
proxyAwareFetch
|
||||||
|
.mockResolvedValueOnce(jsonResponse(WHOAMI))
|
||||||
|
.mockResolvedValueOnce(jsonResponse(CREDITS))
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
jsonResponse({ success: true, data: { planId: "individual-go" } }),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce(jsonResponse(SUMMARY));
|
||||||
|
|
||||||
|
await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "user_cc_test",
|
||||||
|
});
|
||||||
|
|
||||||
|
const since = new URL(proxyAwareFetch.mock.calls[3][0]).searchParams.get(
|
||||||
|
"since",
|
||||||
|
);
|
||||||
|
// firstOfMonth() is local-time based; assert the local date is the 1st.
|
||||||
|
const localDate = new Date(since);
|
||||||
|
expect(localDate.getDate()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns message on missing key / 401 / non-ok whoami", async () => {
|
||||||
|
const missing = await getUsageForProvider({ provider: "commandcode" });
|
||||||
|
expect(missing.message).toMatch(/credential/i);
|
||||||
|
expect(proxyAwareFetch).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "no" }, 401));
|
||||||
|
const auth = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "bad",
|
||||||
|
});
|
||||||
|
expect(auth.message).toMatch(/invalid|expired/i);
|
||||||
|
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "x" }, 500));
|
||||||
|
const err = await getUsageForProvider({
|
||||||
|
provider: "commandcode",
|
||||||
|
apiKey: "bad",
|
||||||
|
});
|
||||||
|
expect(err.message).toMatch(/whoami/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseQuotaData(commandcode)", () => {
|
||||||
|
it("forwards remainingPercentage + unit for window/credit rows", () => {
|
||||||
|
const rows = parseQuotaData("commandcode", {
|
||||||
|
plan: "individual-go",
|
||||||
|
quotas: {
|
||||||
|
"5-hour window": {
|
||||||
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
remainingPercentage: 98.33,
|
||||||
|
resetAt: "2026-08-03T22:59:46.064Z",
|
||||||
|
unit: "$",
|
||||||
|
},
|
||||||
|
"Monthly credits": {
|
||||||
|
used: 0.1,
|
||||||
|
total: 9.9,
|
||||||
|
remainingPercentage: 98.99,
|
||||||
|
resetAt: null,
|
||||||
|
unit: "$",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
name: "5-hour window",
|
||||||
|
used: 0.05,
|
||||||
|
total: 3,
|
||||||
|
remainingPercentage: 98.33,
|
||||||
|
unit: "$",
|
||||||
|
});
|
||||||
|
expect(rows[1]).toMatchObject({
|
||||||
|
name: "Monthly credits",
|
||||||
|
used: 0.1,
|
||||||
|
total: 9.9,
|
||||||
|
remainingPercentage: 98.99,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user