Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions docs/examples/api-key-quota-extractor-compatible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env node
/**
* Claude Code Hub getMyQuota extractor for ccswitch-style quota checks.
*
* Direct usage:
* node docs/examples/api-key-quota-extractor-compatible.js https://cch.example.com sk-your-api-key
*
* ccswitch template usage:
* Import or paste the exported `ccswitchTemplate` object.
*
* The request is:
* POST /api/actions/my-usage/getMyQuota
* Authorization: Bearer <apiKey>
* Content-Type: application/json
* Body: {}
*/

function toNumber(value, fallback = null) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}

function toBoolean(value, fallback = false) {
return typeof value === "boolean" ? value : fallback;
}

function toStringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}

function pickWindow(quotaWindows, name) {
if (!quotaWindows || typeof quotaWindows !== "object") {
return {};
}
const value = quotaWindows[name];
return value && typeof value === "object" ? value : {};
}

function normalizeQuotaResponse(response) {
const data =
response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};

const quotaWindows =
data.quotaWindows && typeof data.quotaWindows === "object" ? data.quotaWindows : {};
const fiveHour = pickWindow(quotaWindows, "fiveHour");
const daily = pickWindow(quotaWindows, "daily");
const weekly = pickWindow(quotaWindows, "weekly");
const monthly = pickWindow(quotaWindows, "monthly");
const total = pickWindow(quotaWindows, "total");

const keyEnabled = toBoolean(data.keyIsEnabled, true);
const userEnabled = toBoolean(data.userIsEnabled, true);
const remaining = toNumber(data.remaining, toNumber(total.remainingUsd, null));
const todayRemaining = toNumber(
data.todayRemainingUsd,
toNumber(daily.remainingUsd, toNumber(data.remainingDailyUsd, null))
);

return {
ok: response && response.ok === true,
isValid: response && response.ok === true && keyEnabled && userEnabled,
invalidMessage: response && response.ok === true ? undefined : "Quota request failed",

planName: "Claude Code Hub Usage",
unit: typeof data.unit === "string" ? data.unit : "USD",

keyName: toStringOrNull(data.keyName),
userName: toStringOrNull(data.userName),
providerGroup: toStringOrNull(data.providerGroup),
keyIsEnabled: keyEnabled,
userIsEnabled: userEnabled,

remaining,
todayRemaining,
todayUsed: toNumber(data.todayUsedUsd, toNumber(daily.usedUsd, 0)),
todayUsedPercent: toNumber(data.todayUsedPercent, toNumber(daily.usedPercent, null)),
todayRemainingPercent: toNumber(
data.todayRemainingPercent,
toNumber(daily.remainingPercent, null)
),
remainingPercent: toNumber(data.remainingPercent, toNumber(total.remainingPercent, null)),

remaining5h: toNumber(fiveHour.remainingUsd, toNumber(data.remaining5hUsd, null)),
remainingDaily: toNumber(daily.remainingUsd, toNumber(data.remainingDailyUsd, null)),
remainingWeekly: toNumber(weekly.remainingUsd, toNumber(data.remainingWeeklyUsd, null)),
remainingMonthly: toNumber(monthly.remainingUsd, toNumber(data.remainingMonthlyUsd, null)),
remainingTotal: toNumber(total.remainingUsd, toNumber(data.remainingTotalUsd, null)),

total: toNumber(total.limitUsd, toNumber(data.limitTotalUsd, null)),
used: toNumber(total.usedUsd, toNumber(data.usedTotalUsd, 0)),
rpmLimit: toNumber(data.rpmLimit, null),
concurrentSessions: toNumber(data.concurrentSessions, 0),
concurrentSessionsLimit: toNumber(data.concurrentSessionsLimit, null),
expiresAt: toStringOrNull(data.expiresAt),
resetMode: toStringOrNull(data.resetMode),
resetTime: toStringOrNull(data.resetTime),

quotaWindows: {
fiveHour,
daily,
weekly,
monthly,
total,
},

balance: remaining,
dailyBalance: todayRemaining,
weeklyBalance: toNumber(weekly.remainingUsd, toNumber(data.remainingWeeklyUsd, null)),
monthlyBalance: toNumber(monthly.remainingUsd, toNumber(data.remainingMonthlyUsd, null)),
extra: [
`5h=${toNumber(fiveHour.remainingUsd, "unlimited")}`,
`daily=${todayRemaining ?? "unlimited"}`,
`weekly=${toNumber(weekly.remainingUsd, "unlimited")}`,
`monthly=${toNumber(monthly.remainingUsd, "unlimited")}`,
`total=${toNumber(total.remainingUsd, remaining ?? "unlimited")}`,
].join(" "),
};
}

const ccswitchTemplate = {
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
Authorization: "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0",
},
body: "{}",
},
extractor: normalizeQuotaResponse,
};

async function fetchQuota(baseUrl, apiKey) {
const response = await fetch(new URL("/api/actions/my-usage/getMyQuota", baseUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0",
},
body: "{}",
});

const text = await response.text();
let payload;
try {
payload = JSON.parse(text);
} catch {
throw new Error(`Quota API did not return JSON: HTTP ${response.status}`);
}

if (!response.ok || payload.ok !== true) {
throw new Error(payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`);
}

return normalizeQuotaResponse(payload);
}

async function main() {
const [, , baseUrl, apiKey] = process.argv;
if (!baseUrl || !apiKey) {
process.stderr.write(
"Usage: node docs/examples/api-key-quota-extractor-compatible.js <baseUrl> <apiKey>\n"
);
process.exitCode = 1;
return;
}

const quota = await fetchQuota(baseUrl, apiKey);
process.stdout.write(`${JSON.stringify(quota, null, 2)}\n`);
}

if (require.main === module) {
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});
}

module.exports = {
ccswitchTemplate,
fetchQuota,
normalizeQuotaResponse,
};
73 changes: 73 additions & 0 deletions docs/usage-only-quota-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Usage-Only Quota Extractor

This note documents the minimal compatibility path for external quota checks.
It does not add a new endpoint or a new authentication surface.

## Endpoint

Call the existing action route with the API key as a Bearer token:

```bash
curl -sS "$CCH_BASE_URL/api/actions/my-usage/getMyQuota" \
-H "Authorization: Bearer $CCH_API_KEY" \
-H "Content-Type: application/json" \
-X POST \
--data '{}'
```

The route is `POST /api/actions/my-usage/getMyQuota`. The request body is `{}`.
It uses the existing `allowReadOnlyAccess` path, so read-only keys can query
their own usage data without gaining access to admin-only actions.

## Response Fields

The response shape is the standard action wrapper:

- `ok`: true when the action succeeds.
- `data`: quota payload for the current key and user.

Useful compatibility fields under `data` include:

- `remaining`: the most restrictive remaining USD amount across configured quota windows, or `null` when unlimited.
- `todayRemainingUsd`: remaining USD amount for the daily window.
- `todayUsedUsd`: used USD amount for the daily window.
- `todayRemainingPercent`: remaining percentage for the daily window.
- `remainingPercent`: the most restrictive remaining percentage across configured quota windows.
- `quotaWindows`: structured `fiveHour`, `daily`, `weekly`, `monthly`, and `total` quota windows.
- `remaining5hUsd`, `remainingDailyUsd`, `remainingWeeklyUsd`, `remainingMonthlyUsd`, `remainingTotalUsd`: flat remaining aliases.
- `rpmLimit`, `concurrentSessions`, `concurrentSessionsLimit`: rate and session limits.
- `keyName`, `userName`, `providerGroup`, `keyIsEnabled`, `userIsEnabled`: key and user metadata.

Each `quotaWindows.*` entry contains:

- `period`
- `limitUsd`
- `usedUsd`
- `remainingUsd`
- `usedPercent`
- `remainingPercent`
- `isUnlimited`
- `isExhausted`

## Example Script

Use `docs/examples/api-key-quota-extractor-compatible.js` as either a direct
Node.js checker or as a ccswitch-style template source.

Direct check:

```bash
node docs/examples/api-key-quota-extractor-compatible.js "$CCH_BASE_URL" "$CCH_API_KEY"
```

The normalized output includes ccswitch-friendly fields such as `remaining`,
`todayRemaining`, `quotaWindows`, `balance`, `dailyBalance`, `weeklyBalance`,
and `monthlyBalance`.

## PR Note

This is a compatibility extension for clients that already consume usage data.
It documents and normalizes the existing `getMyQuota` response fields. It does
not introduce a public quota endpoint, does not accept API keys in request
bodies, and does not bypass the existing `allowReadOnlyAccess` authorization
gate.
Loading
Loading