diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 787c7c9a25..c2af21341b 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -394,6 +394,14 @@ CLI Command Code. Le catalogue, propre au compte, provient du point de terminais après la connexion. Les requêtes de chat utilisent la clé Bearer configurée. Créez des clés dans [Command Code Studio](https://commandcode.ai/studio/). +**Quota Command Code.** Le tableau de bord et `ocx account refresh` sondent les fenêtres +`/alpha/billing/credits` de Command Code (5 heures et hebdomadaire) sur l'hôte canonique +`https://api.commandcode.ai`. Le préréglage OAuth (`command-code`) utilise le jeton porteur du compte +enregistré ; le préréglage à clé d'API fournisseur (`commandcode`) utilise la clé active configurée. Une +URL de base modifiée ressemblant à l'original n'est jamais sondée. Les crédits mensuels, achetés et +gratuits restants sont affichés sous forme de fenêtre en USD lorsque Command Code signale également les +dépenses de la période. + **Découverte SambaNova Cloud.** Le préréglage lit la liste publique `/v1/models` de SambaNova Cloud depuis l'hôte API fixe, préserve les identifiants natifs du fournisseur et limite la découverte à 128 KiB et 128 lignes brutes. Le catalogue n'étant pas authentifié, le parcours de connexion de la CLI signale que la clé ne diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index e40bcd4f47..12c3d80e40 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -367,6 +367,13 @@ account-scoped and comes from the authenticated discovery endpoint after login. configured Bearer key. Create keys at [Command Code Studio](https://commandcode.ai/studio/). +**Command Code quota.** The dashboard and `ocx account refresh` probe Command Code's +`/alpha/billing/credits` windows (5-hour and weekly) on the canonical +`https://api.commandcode.ai` host. The OAuth preset (`command-code`) uses the stored +account bearer; the Provider-API key preset (`commandcode`) uses the active configured +key. A user-edited lookalike base URL is never probed. Remaining monthly, purchased, and +free credits are shown as a USD window when Command Code also reports period spend. + **SambaNova Cloud discovery.** The preset reads SambaNova Cloud's public `/v1/models` list from the fixed API host, preserves provider-native ids, and caps discovery at 128 KiB and 128 raw rows. Because the catalog is unauthenticated, the CLI login flow reports the key as unverifiable instead of treating diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 98e1965d0d..0dd37bfe1b 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -275,6 +275,8 @@ CLI 資格情報の取り込みも可能)。モデルカタログはアカウン discovery エンドポイントから取得します。チャットリクエストは設定済みの bearer キーを使います。 キーは [Command Code Studio](https://commandcode.ai/studio/) で作成します。 +**Command Code の quota:** ダッシュボードと `ocx account refresh` は、正規ホスト `https://api.commandcode.ai` 上の `/alpha/billing/credits` ウィンドウ(5時間と週次)を照会します。OAuth プリセット (`command-code`) は保存済みアカウント bearer を使い、Provider-API キープリセット (`commandcode`) は設定済みの有効キーを使います。ユーザーが編集した類似ホストは照会しません。期間支出が返る場合は、残りの monthly / purchased / free credits を USD ウィンドウとして表示します。 + **SambaNova Cloud の discovery:** preset は固定 API ホスト上の SambaNova Cloud の公開 `/v1/models` 一覧を読み、 プロバイダー固有の ID を保持し、discovery を 128 KiB と raw 128 行に制限します。カタログは認証不要のため、 CLI の login flow は公開レスポンスをキーの有効性の証拠にせず、キーを検証不能として報告します。chat リクエストは diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 9926dc46de..6246ccd61d 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -273,6 +273,8 @@ CLI 사용자는 `~/.commandcode/auth.json`의 로컬 CLI 자격 증명을 가 계정 단위이며 로그인 후 인증된 discovery 엔드포인트에서 가져옵니다. 채팅 요청은 설정된 bearer 키를 사용합니다. 키는 [Command Code Studio](https://commandcode.ai/studio/)에서 생성합니다. +**Command Code 할당량:** 대시보드와 `ocx account refresh`는 정규 호스트 `https://api.commandcode.ai`에서 `/alpha/billing/credits` 창(5시간 및 주간)을 조회합니다. OAuth 프리셋(`command-code`)은 저장된 계정 bearer를 사용하고, Provider-API 키 프리셋(`commandcode`)은 현재 설정된 활성 키를 사용합니다. 사용자가 바꾼 유사 base URL은 조회하지 않습니다. Command Code가 기간 사용량을 함께 반환하면 남은 monthly / purchased / free credits가 USD 창으로 표시됩니다. + **SambaNova Cloud 검색:** 프리셋은 고정 API 호스트의 SambaNova Cloud 공개 `/v1/models` 목록을 읽고, 프로바이더 네이티브 ID를 보존하며 discovery를 128 KiB와 raw 행 128개로 제한합니다. 카탈로그에는 인증이 필요하지 않으므로 CLI 로그인 흐름은 공개 응답을 키 유효성의 증거로 사용하지 않고 키를 검증할 수 없는 것으로 보고합니다. chat 요청은 diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index b6d640cc01..68c7e2a2c3 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -298,6 +298,8 @@ Service token Nscale создаётся в [Nscale Console](https://console.nsca аутентифицированного discovery endpoint после входа. Запросы чата используют настроенный bearer-ключ. Ключи создаются в [Command Code Studio](https://commandcode.ai/studio/). +**Квота Command Code.** Дашборд и `ocx account refresh` опрашивают окна `/alpha/billing/credits` (5 часов и неделя) на каноническом хосте `https://api.commandcode.ai`. OAuth-пресет (`command-code`) использует сохранённый bearer аккаунта; пресет Provider-API ключа (`commandcode`) — активный настроенный ключ. Пользовательски изменённый похожий base URL не опрашивается. Если Command Code также сообщает расход за период, оставшиеся monthly / purchased / free credits показываются как USD-окно. + **Discovery для SambaNova Cloud.** Пресет читает общедоступный список SambaNova Cloud `/v1/models` на фиксированном API-хосте, сохраняет нативные id провайдера и ограничивает discovery размером 128 KiB и 128 исходными строками. Каталог не требует аутентификации, поэтому процедура входа CLI сообщает, что diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index c5e036ab31..d48d1aa4cf 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -435,6 +435,8 @@ doğrulamalı keşif uç noktasından gelir. Sohbet istekleri yapılandırılmı anahtarını kullanır. [Command Code Studio](https://commandcode.ai/studio/) üzerinden anahtarlar oluşturun. +**Command Code kotası.** Pano ve `ocx account refresh`, kanonik `https://api.commandcode.ai` ana bilgisayarında `/alpha/billing/credits` pencerelerini (5 saat ve haftalık) sorgular. OAuth önayarı (`command-code`) kayıtlı hesap bearer'ını kullanır; Provider-API anahtar önayarı (`commandcode`) etkin yapılandırılmış anahtarı kullanır. Kullanıcının değiştirdiği benzer bir temel URL asla sorgulanmaz. Command Code dönem harcamasını da bildirirse kalan monthly / purchased / free credits USD penceresi olarak gösterilir. + **SambaNova Cloud keşfi.** Önayar, sabit API ana bilgisayarından SambaNova Cloud'un genel `/v1/models` listesini okur, sağlayıcı yerel kimliklerini korur ve keşfi 128 KiB ve 128 ham satırla sınırlar. Katalog kimlik doğrulamasız @@ -662,4 +664,3 @@ Canlı araştırmaya sahip sağlayıcılar: OpenAI/Codex, Anthropic, xAI, Cursor Kimi, Google Antigravity, OpenRouter, DeepSeek, ClinePass, Z.AI, MiniMax, Moonshot, Venice, Synthetic, DeepInfra, Neuralwatt ve a6api destekli herhangi bir özel sağlayıcı. - diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 2d2afefded..27000d121c 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -259,6 +259,8 @@ inference key 可从 [Vultr Console](https://my.vultr.com) 的订阅概览复制 `~/.commandcode/auth.json` 导入本地 CLI 凭据);模型目录按账户隔离,并在登录后从经过认证的发现 端点获取。聊天请求使用已配置的 bearer 密钥。密钥可在 [Command Code Studio](https://commandcode.ai/studio/) 创建。 +**Command Code 配额:**仪表盘和 `ocx account refresh` 会在规范主机 `https://api.commandcode.ai` 上探测 `/alpha/billing/credits` 窗口(5 小时和每周)。OAuth 预设 (`command-code`) 使用已保存的账户 bearer;Provider-API 密钥预设 (`commandcode`) 使用当前配置的有效密钥。用户改写后的仿冒 base URL 不会被探测。当 Command Code 同时返回周期消耗时,剩余的 monthly / purchased / free credits 会显示为 USD 窗口。 + **SambaNova Cloud 发现:**该预设从固定 API 主机读取 SambaNova Cloud 的公开 `/v1/models` 列表,保留提供商原生 模型 id,并将发现限制为 128 KiB 和 128 条原始记录。该目录无需鉴权,因此 CLI 登录流程不会把公开响应 当作密钥有效性的证明,而会将密钥报告为无法验证。chat 请求仍使用已配置的 Bearer 密钥;由于 SambaNova diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index 1c536f56b2..ab396da61a 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -339,6 +339,8 @@ Hyperbolic 另外的 image、audio 與 GPU endpoint 不在範圍內。可在 endpoint 取得。Chat request 使用設定的 Bearer key。可在 [Command Code Studio](https://commandcode.ai/studio/) 建立 key。 +**Command Code 配額。** 儀表板與 `ocx account refresh` 會在正規主機 `https://api.commandcode.ai` 探測 `/alpha/billing/credits` 視窗(5 小時與每週)。OAuth preset (`command-code`) 使用已儲存的帳號 bearer;Provider-API key preset (`commandcode`) 使用目前設定的有效 key。使用者改寫過的仿冒 base URL 不會被探測。當 Command Code 同時回報週期消耗時,剩餘的 monthly / purchased / free credits 會顯示為 USD 視窗。 + **SambaNova Cloud 探索。** preset 從固定 API host 讀取 SambaNova Cloud 公開的 `/v1/models` 列表,保留 provider-native id,並把 discovery 限制在 128 KiB/128 個 raw row。因 catalog 不需要認證,CLI login 流程會把 key 回報為 unverifiable,而不會把公開 response 當成有效 key 的證明。Chat request 仍使用 diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 06ee12e489..29a57c452a 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -38,6 +38,11 @@ const REQUEST_TIMEOUT_MS = 8_000; export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024; const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"; const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`; +const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai"; +const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`; +const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`; +const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`; +const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`; const A6API_BASE_URL = "https://api.a6api.com"; const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1"; const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`; @@ -1510,6 +1515,12 @@ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean { return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL; } +function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + // OAuth preset points at the API root; the Provider-API preset at /provider/v1. + return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`; +} + /** Prefer the nested `data` shell when the outer object is only an envelope. */ function unwrapKimiQuotaPayload(value: unknown): Record | null { const body = asRecord(value); @@ -1631,6 +1642,145 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom return quota ? report(provider, "kimi:usages", quota) : null; } +/** + * Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits, + * normalized to a percent with an optional reset timestamp. + */ +function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null { + const row = asRecord(value); + if (!row) return null; + const cap = toFiniteNumber(row.cap); + const used = toFiniteNumber(row.used); + if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null; + const percent = normalizePercent((used / cap) * 100); + if (percent === undefined) return null; + const resetAt = quotaResetAt(row); + return { percent, ...(resetAt !== undefined ? { resetAt } : {}) }; +} + +/** Soft-fail GET returning a parsed record, or null when unavailable. */ +async function fetchCommandCodeJson(url: string, bearer: string): Promise | null> { + try { + const response = await fetch(url, { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) return null; + return asRecord(await readQuotaJson(response)); + } catch { + return null; + } +} + +/** + * Soft-fail period spend (used) against the remaining credit pools → creditsUsd. + * Period scoping: `since=` keeps spend aligned with the + * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt. + */ +async function fetchCommandCodeSpend( + bearer: string, + credits: Record | null, + orgQuery: string, +): Promise { + if (!credits) return undefined; + const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer); + const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody; + const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : ""; + // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle + // remaining pools produces a wrong percent. Omit creditsUsd until a period exists. + if (!periodStart) return undefined; + const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`; + const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd); + const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer); + const summary = asRecord(summaryBody?.data) ?? summaryBody; + const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits); + if (used === undefined || used < 0) return undefined; + const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits] + .map(value => toFiniteNumber(value)) + .filter((value): value is number => value !== undefined); + // Field presence is what separates a real balance from absent data: an exhausted + // all-zero account still reports remaining=0, while no remaining-credit field at + // all means there is nothing to meter. + if (pools.length === 0) return undefined; + const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0); + const limit = used + remaining; + const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0); + // Purchased credits roll over past the subscription period end, so an expiry is + // only truthful when the aggregate contains no non-expiring purchased pool. + const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0; + return percent === undefined + ? undefined + : { + used, + limit, + remaining, + percent, + ...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}), + }; +} + +/** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */ +async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise { + if (config.authMode === "oauth") { + try { + return await getValidAccessToken("command-code"); + } catch { + return null; + } + } + // ACTIVE key only: a quota bar for a different account than the one routing + // requests is a wrong meter, not a helpful one. + return resolveEnvValue(config.apiKey)?.trim() || null; +} + +/** + * Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's + * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft + * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd. + */ +async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise { + // Never release credentials to a user-edited or lookalike provider host. + if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null; + const bearer = await resolveCommandCodeQuotaBearer(config); + if (!bearer) return null; + const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer); + const whoami = asRecord(whoamiBody?.data) ?? whoamiBody; + const org = asRecord(whoami?.org); + const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null; + const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : ""; + const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const raw = asRecord(await readQuotaJson(response)); + const body = asRecord(raw?.data) ?? raw; + const credits = asRecord(body?.credits); + const limits = asRecord(body?.windowLimits); + if (!credits && !limits) return null; + const fiveHour = parseCommandCodeWindow(limits?.fiveHour); + const weekly = parseCommandCodeWindow(limits?.weekly); + const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery); + return report(provider, "command-code:credits", { + ...(fiveHour ? { + fiveHourPercent: fiveHour.percent, + ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}), + } : {}), + ...(weekly ? { + weeklyPercent: weekly.percent, + ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}), + } : {}), + ...(creditsUsd ? { creditsUsd } : {}), + updatedAt: Date.now(), + }); +} + /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */ async function fetchCursorQuota(provider: string): Promise { let accessToken: string; @@ -1919,6 +2069,15 @@ async function maybeFetchProviderQuota( if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) { return fetchKimiQuota(name, provider); } + // OAuth account login or Provider-API key only; forward/local modes carry no + // credential of ours on the canonical host. + if (provider.authMode === "oauth" && name === "command-code") { + return fetchCommandCodeQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "commandcode" + && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) { + return fetchCommandCodeQuota(name, provider); + } if ((provider.authMode ?? "key") === "key" && name === "opencode-go") { return fetchOpenCodeGoQuota(name, provider); } diff --git a/tests/command-code-quota.test.ts b/tests/command-code-quota.test.ts new file mode 100644 index 0000000000..e840bb8dd3 --- /dev/null +++ b/tests/command-code-quota.test.ts @@ -0,0 +1,381 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { saveCredential } from "../src/oauth/store"; +import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../src/providers/quota"; +import type { OcxConfig } from "../src/types"; + +const originalFetch = globalThis.fetch; +const previousOpencodexHome = process.env.OPENCODEX_HOME; +let opencodexHome: string; + +function commandCodeConfig(baseUrl = "https://api.commandcode.ai/provider/v1"): OcxConfig { + return { + defaultProvider: "commandcode", + providers: { + commandcode: { + adapter: "openai-chat", + authMode: "key", + baseUrl, + apiKey: "commandcode-secret", + }, + }, + } as OcxConfig; +} + +function commandCodeOAuthConfig(baseUrl = "https://api.commandcode.ai"): OcxConfig { + return { + defaultProvider: "command-code", + providers: { + "command-code": { + adapter: "command-code", + authMode: "oauth", + baseUrl, + }, + }, + } as OcxConfig; +} + +beforeEach(() => { + opencodexHome = mkdtempSync(join(tmpdir(), "ocx-command-code-quota-")); + process.env.OPENCODEX_HOME = opencodexHome; + clearProviderQuotaCache(); +}); + +afterEach(() => { + globalThis.fetch = originalFetch; + clearProviderQuotaCache(); + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + rmSync(opencodexHome, { recursive: true, force: true }); +}); + +describe("Command Code provider quota", () => { + test("maps billing credits windows plus subscription-scoped spend into quota", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const headers = init?.headers as Record | undefined; + seen.push({ + url: String(input), + authorization: headers?.Authorization, + redirect: init?.redirect, + }); + const url = String(input); + const body = url.includes("/alpha/whoami") + ? { org: { id: "team-org-7" } } + : url.includes("/alpha/billing/subscriptions") + ? { data: { planId: "individual-pro", currentPeriodStart: "2026-08-01T00:00:00.000Z", currentPeriodEnd: "2026-09-01T00:00:00.000Z" } } + : url.includes("/alpha/usage/summary") + ? { totalCost: 15 } + : { + credits: { monthlyCredits: 20, purchasedCredits: 5, freeCredits: 1 }, + windowLimits: { + fiveHour: { cap: 100, used: 40, resetAt: "2026-08-15T20:00:00.000Z" }, + weekly: { cap: 500, used: 25, resetAt: "2026-08-18T00:00:00.000Z" }, + exceeded: null, + limited: false, + }, + }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.provider).toBe("commandcode"); + expect(result.reports[0]?.source).toBe("command-code:credits"); + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 40, + fiveHourResetAt: Date.parse("2026-08-15T20:00:00.000Z"), + weeklyPercent: 5, + weeklyResetAt: Date.parse("2026-08-18T00:00:00.000Z"), + creditsUsd: { + used: 15, + limit: 41, + remaining: 26, + percent: 36.585365853658534, + }, + updatedAt: expect.any(Number), + }); + expect(seen).toEqual([{ + url: "https://api.commandcode.ai/alpha/whoami", + authorization: "Bearer commandcode-secret", + redirect: "error", + }, { + url: "https://api.commandcode.ai/alpha/billing/credits?orgId=team-org-7", + authorization: "Bearer commandcode-secret", + redirect: "error", + }, { + url: "https://api.commandcode.ai/alpha/billing/subscriptions?orgId=team-org-7", + authorization: "Bearer commandcode-secret", + redirect: "error", + }, { + url: "https://api.commandcode.ai/alpha/usage/summary?orgId=team-org-7&since=2026-08-01T00%3A00%3A00.000Z", + authorization: "Bearer commandcode-secret", + redirect: "error", + }]); + expect(JSON.stringify(result)).not.toContain("commandcode-secret"); + }); + + test("keeps rolling windows when whoami and usage summary fail", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + seen.push(url); + if (url.includes("/alpha/whoami")) return new Response("down", { status: 500 }); + if (url.includes("/alpha/billing/subscriptions")) { + return new Response(JSON.stringify({ + data: { currentPeriodStart: "2026-08-01T00:00:00.000Z" }, + }), { status: 200, headers: { "content-type": "application/json" } }); + } + if (url.includes("/alpha/usage/summary")) return new Response("down", { status: 500 }); + return new Response(JSON.stringify({ + credits: { monthlyCredits: 20 }, + windowLimits: { fiveHour: { cap: 100, used: 40 } }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 40, + updatedAt: expect.any(Number), + }); + expect(seen[1]).toBe("https://api.commandcode.ai/alpha/billing/credits"); + expect(seen.some(url => url.includes("/alpha/usage/summary"))).toBe(true); + }); + + test("omits creditsUsd when the billing period cannot be established", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + seen.push(url); + if (url.includes("/alpha/whoami")) return new Response("{}", { status: 200 }); + if (url.includes("/alpha/billing/subscriptions")) return new Response("down", { status: 500 }); + if (url.includes("/alpha/usage/summary")) { + return new Response(JSON.stringify({ totalCost: 999 }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response(JSON.stringify({ + credits: { monthlyCredits: 20, purchasedCredits: 5, freeCredits: 1 }, + windowLimits: { fiveHour: { cap: 100, used: 40 }, weekly: { cap: 500, used: 25 } }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 40, + weeklyPercent: 5, + updatedAt: expect.any(Number), + }); + expect(result.reports[0]?.quota?.creditsUsd).toBeUndefined(); + expect(seen.some(url => url.includes("/alpha/usage/summary"))).toBe(false); + }); + + test("unwraps a data envelope on credits and spend payloads", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + const body = url.includes("/alpha/whoami") + ? { data: { org: { id: "team-org-7" } } } + : url.includes("/alpha/billing/subscriptions") + ? { data: { currentPeriodStart: "2026-08-01T00:00:00.000Z", currentPeriodEnd: "2026-09-01T00:00:00.000Z" } } + : url.includes("/alpha/usage/summary") + ? { data: { totalCost: 10 } } + : { + data: { + credits: { monthlyCredits: 20 }, + windowLimits: { fiveHour: { cap: 100, used: 25 } }, + }, + }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 25, + creditsUsd: { + used: 10, + limit: 30, + remaining: 20, + percent: 33.33333333333333, + expiresAt: Date.parse("2026-09-01T00:00:00.000Z"), + }, + updatedAt: expect.any(Number), + }); + }); + + test("treats a terminal 401 as invalid", async () => { + let rejected = false; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/alpha/whoami")) return new Response("{}", { status: 200 }); + if (rejected) return new Response("unauthorized", { status: 401 }); + return new Response(JSON.stringify({ + windowLimits: { fiveHour: { cap: 100, used: 40 } }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const valid = await fetchProviderQuotaReports(commandCodeConfig(), true); + rejected = true; + const invalid = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(valid.reports).toHaveLength(1); + expect(invalid.reports).toEqual([]); + }); + + test("does not probe quota for forward or local Command Code auth", async () => { + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + return new Response("{}", { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports({ + defaultProvider: "commandcode", + providers: { + commandcode: { + adapter: "openai-chat", + authMode: "forward", + baseUrl: "https://api.commandcode.ai/provider/v1", + apiKey: "commandcode-secret", + }, + }, + } as OcxConfig, true); + + expect(fetchCalls).toBe(0); + expect(result.reports).toEqual([]); + }); + + test("does not probe quota for local Command Code auth", async () => { + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + return new Response("{}", { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports({ + defaultProvider: "command-code", + providers: { + "command-code": { + adapter: "command-code", + authMode: "local", + baseUrl: "https://api.commandcode.ai", + }, + }, + } as OcxConfig, true); + + expect(fetchCalls).toBe(0); + expect(result.reports).toEqual([]); + }); + + test("a fully exhausted account still reports a zero-remaining credit window", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + const body = url.includes("/alpha/whoami") + ? {} + : url.includes("/alpha/billing/subscriptions") + ? { data: { currentPeriodStart: "2026-08-01T00:00:00.000Z", currentPeriodEnd: "2026-09-01T00:00:00.000Z" } } + : url.includes("/alpha/usage/summary") + ? { totalCost: 12 } + : { + credits: { monthlyCredits: 0, purchasedCredits: 0, freeCredits: 0 }, + windowLimits: { fiveHour: { cap: 100, used: 40 } }, + }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota).toEqual({ + fiveHourPercent: 40, + creditsUsd: { + used: 12, + limit: 12, + remaining: 0, + percent: 100, + expiresAt: Date.parse("2026-09-01T00:00:00.000Z"), + }, + updatedAt: expect.any(Number), + }); + }); + + test("a mixed balance with roll-over purchased credits carries no subscription expiry", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + const body = url.includes("/alpha/whoami") + ? {} + : url.includes("/alpha/billing/subscriptions") + ? { data: { currentPeriodStart: "2026-08-01T00:00:00.000Z", currentPeriodEnd: "2026-08-31T00:00:00.000Z" } } + : url.includes("/alpha/usage/summary") + ? { totalCost: 4 } + : { + credits: { monthlyCredits: 0, purchasedCredits: 10, freeCredits: 0 }, + windowLimits: { fiveHour: { cap: 100, used: 10 } }, + }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeConfig(), true); + + expect(result.reports[0]?.quota?.creditsUsd).toEqual({ + used: 4, + limit: 14, + remaining: 10, + percent: 28.57142857142857, + }); + }); + + test("OAuth Command Code probes the canonical API root with the stored bearer", async () => { + await saveCredential("command-code", { + access: "command-code-access", + refresh: "command-code-access", + expires: Date.now() + 3600_000, + }); + const seen: Array<{ url: string; authorization?: string }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const headers = init?.headers as Record | undefined; + seen.push({ url: String(input), authorization: headers?.Authorization }); + const url = String(input); + if (url.includes("/alpha/whoami") || url.includes("/alpha/billing/subscriptions") || url.includes("/alpha/usage/summary")) { + return new Response("down", { status: 500 }); + } + return new Response(JSON.stringify({ + windowLimits: { weekly: { cap: 200, used: 50 } }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(commandCodeOAuthConfig(), true); + + expect(result.reports[0]?.provider).toBe("command-code"); + expect(result.reports[0]?.quota).toEqual({ + weeklyPercent: 25, + updatedAt: expect.any(Number), + }); + expect(seen[0]).toEqual({ + url: "https://api.commandcode.ai/alpha/whoami", + authorization: "Bearer command-code-access", + }); + expect(JSON.stringify(result)).not.toContain("command-code-access"); + }); + + test("does not probe quota for a noncanonical Command Code destination", async () => { + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + return new Response("{}", { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + commandCodeConfig("https://example.invalid/provider/v1"), + true, + ); + + expect(fetchCalls).toBe(0); + expect(result.reports).toEqual([]); + }); +});