Claude quota only worked when the user had signed into Anthropic through OpenCode. Credentials are now discovered from Claude Code itself first: the macOS Keychain entry, then the Linux/WSL credentials file (honouring CLAUDE_CONFIG_DIR), then OpenCode auth.json, then CLAUDE_CODE_OAUTH_TOKEN. All sources stay read-only and the OAuth token is never refreshed: Anthropic allows one live refresh token per client_id, so refreshing here would sign the user out of Claude Code. Credentials are re-read per request instead, and an expired token reports that Claude Code needs a sign-in rather than a bare 401. Usage is now read from the limits[] array, so model-scoped weekly limits work again after Anthropic stopped populating seven_day_sonnet/seven_day_opus, and new limit kinds no longer need a code change. Adds extra-usage spend and the plan name, and holds the last good values through Anthropic's 429s with a cooldown and an account-keyed cache.
121 lines
4.1 KiB
JavaScript
121 lines
4.1 KiB
JavaScript
/**
|
|
* Shapes Anthropic's OAuth usage payload into quota windows.
|
|
*
|
|
* The payload carries the same limit twice: a legacy set of named fields
|
|
* (`five_hour`, `seven_day`, ...) and a newer `limits` array. Only the array
|
|
* reports model-scoped limits, and Anthropic ships new limit kinds there under
|
|
* rotating internal code names, so limits are read from the array by `kind` and
|
|
* fall back to the legacy fields when the array is missing.
|
|
*
|
|
* @module quota/providers/claude/transforms
|
|
*/
|
|
|
|
import { asObject, asNonEmptyString, toNumber, toTimestamp, toUsageWindow, formatMoney } from '../../utils/index.js';
|
|
|
|
const SESSION_WINDOW = '5h';
|
|
const WEEKLY_WINDOW = '7d';
|
|
const EXTRA_USAGE_WINDOW = 'extra_usage';
|
|
|
|
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
|
|
/** Money in Anthropic's minor-unit form, e.g. `{ amount_minor: 10000, exponent: 2 }`. */
|
|
const toAmount = (value) => {
|
|
const money = asObject(value);
|
|
const minor = toNumber(money?.amount_minor);
|
|
if (minor === null) return null;
|
|
const exponent = toNumber(money?.exponent) ?? 2;
|
|
return minor / 10 ** exponent;
|
|
};
|
|
|
|
const formatSpendLabel = (used, limit, currency) => {
|
|
const usedLabel = formatMoney(used);
|
|
if (usedLabel === null) return null;
|
|
const prefix = currency === 'USD' || !currency ? '$' : `${currency} `;
|
|
const limitLabel = formatMoney(limit);
|
|
return limitLabel === null ? `${prefix}${usedLabel}` : `${prefix}${usedLabel} / ${prefix}${limitLabel}`;
|
|
};
|
|
|
|
const addWindow = (target, key, { percent, resetAt, valueLabel }) => {
|
|
if (percent === null && !valueLabel) return;
|
|
target[key] = toUsageWindow({ usedPercent: percent, windowSeconds: null, resetAt, valueLabel });
|
|
};
|
|
|
|
const applyLimitsArray = (limits, windows, models) => {
|
|
for (const entry of limits) {
|
|
const limit = asObject(entry);
|
|
if (!limit) continue;
|
|
const percent = toNumber(limit.percent);
|
|
const resetAt = toTimestamp(limit.resets_at);
|
|
const modelName = asNonEmptyString(asObject(asObject(limit.scope)?.model)?.display_name);
|
|
|
|
if (limit.kind === 'session') {
|
|
addWindow(windows, SESSION_WINDOW, { percent, resetAt });
|
|
continue;
|
|
}
|
|
if (limit.kind === 'weekly_all') {
|
|
addWindow(windows, WEEKLY_WINDOW, { percent, resetAt });
|
|
continue;
|
|
}
|
|
if (limit.kind === 'weekly_scoped' && modelName) {
|
|
const modelWindows = {};
|
|
addWindow(modelWindows, WEEKLY_WINDOW, { percent, resetAt });
|
|
if (Object.keys(modelWindows).length > 0) models[modelName] = { windows: modelWindows };
|
|
}
|
|
}
|
|
};
|
|
|
|
const applyLegacyFields = (payload, windows) => {
|
|
const fiveHour = asObject(payload.five_hour);
|
|
const sevenDay = asObject(payload.seven_day);
|
|
if (fiveHour) {
|
|
addWindow(windows, SESSION_WINDOW, {
|
|
percent: toNumber(fiveHour.utilization),
|
|
resetAt: toTimestamp(fiveHour.resets_at)
|
|
});
|
|
}
|
|
if (sevenDay) {
|
|
addWindow(windows, WEEKLY_WINDOW, {
|
|
percent: toNumber(sevenDay.utilization),
|
|
resetAt: toTimestamp(sevenDay.resets_at)
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Extra usage is the paid overflow beyond plan limits. It is only meaningful
|
|
* once the account has it enabled; a disabled block would render as a permanent
|
|
* empty bar.
|
|
*/
|
|
const applyExtraUsage = (payload, windows) => {
|
|
const spend = asObject(payload.spend);
|
|
if (!spend || spend.enabled !== true) return;
|
|
const used = toAmount(spend.used);
|
|
const limit = toAmount(spend.limit);
|
|
addWindow(windows, EXTRA_USAGE_WINDOW, {
|
|
percent: toNumber(spend.percent),
|
|
resetAt: null,
|
|
valueLabel: formatSpendLabel(used, limit, asNonEmptyString(asObject(spend.used)?.currency))
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {unknown} rawPayload Parsed JSON body of the OAuth usage endpoint.
|
|
* @returns {{ windows: Record<string, object>, models: Record<string, object> }}
|
|
*/
|
|
export const toClaudeUsage = (rawPayload) => {
|
|
const payload = asObject(rawPayload) ?? {};
|
|
const windows = {};
|
|
const models = {};
|
|
|
|
const limits = asArray(payload.limits);
|
|
if (limits.length > 0) {
|
|
applyLimitsArray(limits, windows, models);
|
|
} else {
|
|
applyLegacyFields(payload, windows);
|
|
}
|
|
|
|
applyExtraUsage(payload, windows);
|
|
|
|
return { windows, models };
|
|
};
|