chore: format usage
This commit is contained in:
550
usage/claude.ts
550
usage/claude.ts
@@ -1,13 +1,13 @@
|
|||||||
import type {
|
import type {
|
||||||
CredentialRankingStrategy,
|
CredentialRankingStrategy,
|
||||||
UsageAmount,
|
UsageAmount,
|
||||||
UsageFetchContext,
|
UsageFetchContext,
|
||||||
UsageFetchParams,
|
UsageFetchParams,
|
||||||
UsageLimit,
|
UsageLimit,
|
||||||
UsageProvider,
|
UsageProvider,
|
||||||
UsageReport,
|
UsageReport,
|
||||||
UsageStatus,
|
UsageStatus,
|
||||||
UsageWindow,
|
UsageWindow,
|
||||||
} from "../usage";
|
} from "../usage";
|
||||||
import { isRecord, toNumber } from "../utils";
|
import { isRecord, toNumber } from "../utils";
|
||||||
|
|
||||||
@@ -18,320 +18,358 @@ const MAX_RETRIES = 3;
|
|||||||
const BASE_RETRY_DELAY_MS = 500;
|
const BASE_RETRY_DELAY_MS = 500;
|
||||||
|
|
||||||
const CLAUDE_HEADERS = {
|
const CLAUDE_HEADERS = {
|
||||||
accept: "application/json, text/plain, */*",
|
accept: "application/json, text/plain, */*",
|
||||||
"accept-encoding": "gzip, compress, deflate, br",
|
"accept-encoding": "gzip, compress, deflate, br",
|
||||||
"anthropic-beta":
|
"anthropic-beta":
|
||||||
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"user-agent": "claude-cli/2.1.63 (external, cli)",
|
"user-agent": "claude-cli/2.1.63 (external, cli)",
|
||||||
connection: "keep-alive",
|
connection: "keep-alive",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function normalizeClaudeBaseUrl(baseUrl?: string): string {
|
function normalizeClaudeBaseUrl(baseUrl?: string): string {
|
||||||
if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
|
if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
|
||||||
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
||||||
const lower = trimmed.toLowerCase();
|
const lower = trimmed.toLowerCase();
|
||||||
if (lower.endsWith("/api/oauth")) return trimmed;
|
if (lower.endsWith("/api/oauth")) return trimmed;
|
||||||
let url: URL;
|
let url: URL;
|
||||||
try {
|
try {
|
||||||
url = new URL(trimmed);
|
url = new URL(trimmed);
|
||||||
} catch {
|
} catch {
|
||||||
return DEFAULT_ENDPOINT;
|
return DEFAULT_ENDPOINT;
|
||||||
}
|
}
|
||||||
let path = url.pathname.replace(/\/+$/, "");
|
let path = url.pathname.replace(/\/+$/, "");
|
||||||
if (path === "/") path = "";
|
if (path === "/") path = "";
|
||||||
if (path.toLowerCase().endsWith("/v1")) {
|
if (path.toLowerCase().endsWith("/v1")) {
|
||||||
path = path.slice(0, -3);
|
path = path.slice(0, -3);
|
||||||
}
|
}
|
||||||
if (!path) return `${url.origin}/api/oauth`;
|
if (!path) return `${url.origin}/api/oauth`;
|
||||||
return `${url.origin}${path}/api/oauth`;
|
return `${url.origin}${path}/api/oauth`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaudeUsageBucket {
|
interface ClaudeUsageBucket {
|
||||||
utilization?: number;
|
utilization?: number;
|
||||||
resets_at?: string;
|
resets_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedUsageBucket {
|
interface ParsedUsageBucket {
|
||||||
utilization?: number;
|
utilization?: number;
|
||||||
resetsAt?: number;
|
resetsAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaudeUsageResponse {
|
interface ClaudeUsageResponse {
|
||||||
five_hour?: ClaudeUsageBucket | null;
|
five_hour?: ClaudeUsageBucket | null;
|
||||||
seven_day?: ClaudeUsageBucket | null;
|
seven_day?: ClaudeUsageBucket | null;
|
||||||
seven_day_opus?: ClaudeUsageBucket | null;
|
seven_day_opus?: ClaudeUsageBucket | null;
|
||||||
seven_day_sonnet?: ClaudeUsageBucket | null;
|
seven_day_sonnet?: ClaudeUsageBucket | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClaudeUsagePayload = {
|
type ClaudeUsagePayload = {
|
||||||
payload: ClaudeUsageResponse;
|
payload: ClaudeUsageResponse;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseIsoTime(value: string | undefined): number | undefined {
|
function parseIsoTime(value: string | undefined): number | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const parsed = Date.parse(value);
|
const parsed = Date.parse(value);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
|
function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
|
||||||
if (!isRecord(bucket)) return undefined;
|
if (!isRecord(bucket)) return undefined;
|
||||||
const utilization = toNumber(bucket.utilization);
|
const utilization = toNumber(bucket.utilization);
|
||||||
const resetsAt = parseIsoTime(typeof bucket.resets_at === "string" ? bucket.resets_at : undefined);
|
const resetsAt = parseIsoTime(
|
||||||
if (utilization === undefined && resetsAt === undefined) {
|
typeof bucket.resets_at === "string" ? bucket.resets_at : undefined,
|
||||||
return undefined;
|
);
|
||||||
}
|
if (utilization === undefined && resetsAt === undefined) {
|
||||||
return { utilization, resetsAt };
|
return undefined;
|
||||||
|
}
|
||||||
|
return { utilization, resetsAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPayloadString(payload: Record<string, unknown>, key: string): string | undefined {
|
function getPayloadString(
|
||||||
const value = payload[key];
|
payload: Record<string, unknown>,
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
key: string,
|
||||||
|
): string | undefined {
|
||||||
|
const value = payload[key];
|
||||||
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractUsageIdentity(payload: ClaudeUsageResponse, orgId?: string): { accountId?: string; email?: string } {
|
function extractUsageIdentity(
|
||||||
if (!isRecord(payload)) return { accountId: orgId };
|
payload: ClaudeUsageResponse,
|
||||||
const accountId =
|
orgId?: string,
|
||||||
getPayloadString(payload, "account_id") ??
|
): { accountId?: string; email?: string } {
|
||||||
getPayloadString(payload, "accountId") ??
|
if (!isRecord(payload)) return { accountId: orgId };
|
||||||
getPayloadString(payload, "user_id") ??
|
const accountId =
|
||||||
getPayloadString(payload, "userId") ??
|
getPayloadString(payload, "account_id") ??
|
||||||
getPayloadString(payload, "org_id") ??
|
getPayloadString(payload, "accountId") ??
|
||||||
getPayloadString(payload, "orgId") ??
|
getPayloadString(payload, "user_id") ??
|
||||||
orgId;
|
getPayloadString(payload, "userId") ??
|
||||||
const email =
|
getPayloadString(payload, "org_id") ??
|
||||||
getPayloadString(payload, "email") ??
|
getPayloadString(payload, "orgId") ??
|
||||||
getPayloadString(payload, "user_email") ??
|
orgId;
|
||||||
getPayloadString(payload, "userEmail");
|
const email =
|
||||||
return { accountId, email };
|
getPayloadString(payload, "email") ??
|
||||||
|
getPayloadString(payload, "user_email") ??
|
||||||
|
getPayloadString(payload, "userEmail");
|
||||||
|
return { accountId, email };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasUsageData(payload: ClaudeUsageResponse): boolean {
|
function hasUsageData(payload: ClaudeUsageResponse): boolean {
|
||||||
return Boolean(payload.five_hour || payload.seven_day || payload.seven_day_opus || payload.seven_day_sonnet);
|
return Boolean(
|
||||||
|
payload.five_hour ||
|
||||||
|
payload.seven_day ||
|
||||||
|
payload.seven_day_opus ||
|
||||||
|
payload.seven_day_sonnet,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUsagePayload(
|
async function fetchUsagePayload(
|
||||||
url: string,
|
url: string,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
ctx: UsageFetchContext,
|
ctx: UsageFetchContext,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<ClaudeUsagePayload | null> {
|
): Promise<ClaudeUsagePayload | null> {
|
||||||
let lastPayload: ClaudeUsageResponse | null = null;
|
let lastPayload: ClaudeUsageResponse | null = null;
|
||||||
let lastOrgId: string | undefined;
|
let lastOrgId: string | undefined;
|
||||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
try {
|
try {
|
||||||
const response = await ctx.fetch(url, { headers, signal });
|
const response = await ctx.fetch(url, { headers, signal });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
ctx.logger?.warn("Claude usage fetch failed", { status: response.status, statusText: response.statusText });
|
ctx.logger?.warn("Claude usage fetch failed", {
|
||||||
return null;
|
status: response.status,
|
||||||
}
|
statusText: response.statusText,
|
||||||
const payload = (await response.json()) as ClaudeUsageResponse;
|
});
|
||||||
lastPayload = payload;
|
return null;
|
||||||
const orgId = response.headers.get("anthropic-organization-id")?.trim() || undefined;
|
}
|
||||||
lastOrgId = orgId ?? lastOrgId;
|
const payload = (await response.json()) as ClaudeUsageResponse;
|
||||||
if (payload && isRecord(payload) && hasUsageData(payload)) {
|
lastPayload = payload;
|
||||||
return { payload, orgId };
|
const orgId =
|
||||||
}
|
response.headers.get("anthropic-organization-id")?.trim() || undefined;
|
||||||
} catch (error) {
|
lastOrgId = orgId ?? lastOrgId;
|
||||||
ctx.logger?.warn("Claude usage fetch error", { error: String(error) });
|
if (payload && isRecord(payload) && hasUsageData(payload)) {
|
||||||
return null;
|
return { payload, orgId };
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger?.warn("Claude usage fetch error", { error: String(error) });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt < MAX_RETRIES - 1) {
|
if (attempt < MAX_RETRIES - 1) {
|
||||||
await new Promise(resolve => setTimeout(resolve, BASE_RETRY_DELAY_MS * 2 ** attempt));
|
await new Promise((resolve) =>
|
||||||
}
|
setTimeout(resolve, BASE_RETRY_DELAY_MS * 2 ** attempt),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return lastPayload ? { payload: lastPayload, orgId: lastOrgId } : null;
|
return lastPayload ? { payload: lastPayload, orgId: lastOrgId } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaudeProfile {
|
interface ClaudeProfile {
|
||||||
account?: {
|
account?: {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProfile(
|
async function fetchProfile(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
ctx: UsageFetchContext,
|
ctx: UsageFetchContext,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<ClaudeProfile | null> {
|
): Promise<ClaudeProfile | null> {
|
||||||
const url = `${baseUrl}/profile`;
|
const url = `${baseUrl}/profile`;
|
||||||
try {
|
try {
|
||||||
const response = await ctx.fetch(url, { headers, signal });
|
const response = await ctx.fetch(url, { headers, signal });
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
return (await response.json()) as ClaudeProfile;
|
return (await response.json()) as ClaudeProfile;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveEmail(
|
async function resolveEmail(
|
||||||
params: UsageFetchParams,
|
params: UsageFetchParams,
|
||||||
ctx: UsageFetchContext,
|
ctx: UsageFetchContext,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (params.credential.email) return params.credential.email;
|
if (params.credential.email) return params.credential.email;
|
||||||
|
|
||||||
const profile = await fetchProfile(baseUrl, headers, ctx, params.signal);
|
const profile = await fetchProfile(baseUrl, headers, ctx, params.signal);
|
||||||
return profile?.account?.email;
|
return profile?.account?.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageAmount(utilization: number | undefined): UsageAmount | undefined {
|
function buildUsageAmount(
|
||||||
if (utilization === undefined) return undefined;
|
utilization: number | undefined,
|
||||||
const clamped = Math.min(Math.max(utilization, 0), 100);
|
): UsageAmount | undefined {
|
||||||
const usedFraction = clamped / 100;
|
if (utilization === undefined) return undefined;
|
||||||
return {
|
const clamped = Math.min(Math.max(utilization, 0), 100);
|
||||||
used: clamped,
|
const usedFraction = clamped / 100;
|
||||||
limit: 100,
|
return {
|
||||||
remaining: Math.max(0, 100 - clamped),
|
used: clamped,
|
||||||
usedFraction,
|
limit: 100,
|
||||||
remainingFraction: Math.max(0, 1 - usedFraction),
|
remaining: Math.max(0, 100 - clamped),
|
||||||
unit: "percent",
|
usedFraction,
|
||||||
};
|
remainingFraction: Math.max(0, 1 - usedFraction),
|
||||||
|
unit: "percent",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
|
function buildUsageStatus(
|
||||||
if (usedFraction === undefined) return undefined;
|
usedFraction: number | undefined,
|
||||||
if (usedFraction >= 1) return "exhausted";
|
): UsageStatus | undefined {
|
||||||
if (usedFraction >= 0.9) return "warning";
|
if (usedFraction === undefined) return undefined;
|
||||||
return "ok";
|
if (usedFraction >= 1) return "exhausted";
|
||||||
|
if (usedFraction >= 0.9) return "warning";
|
||||||
|
return "ok";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageLimit(args: {
|
function buildUsageLimit(args: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
windowId: string;
|
windowId: string;
|
||||||
windowLabel: string;
|
windowLabel: string;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
bucket: ParsedUsageBucket | undefined;
|
bucket: ParsedUsageBucket | undefined;
|
||||||
provider: "anthropic";
|
provider: "anthropic";
|
||||||
tier?: string;
|
tier?: string;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
}): UsageLimit | null {
|
}): UsageLimit | null {
|
||||||
if (!args.bucket) return null;
|
if (!args.bucket) return null;
|
||||||
const amount = buildUsageAmount(args.bucket.utilization);
|
const amount = buildUsageAmount(args.bucket.utilization);
|
||||||
if (!amount) return null;
|
if (!amount) return null;
|
||||||
const window: UsageWindow = {
|
const window: UsageWindow = {
|
||||||
id: args.windowId,
|
id: args.windowId,
|
||||||
label: args.windowLabel,
|
label: args.windowLabel,
|
||||||
durationMs: args.durationMs,
|
durationMs: args.durationMs,
|
||||||
...(args.bucket.resetsAt !== undefined ? { resetsAt: args.bucket.resetsAt } : {}),
|
...(args.bucket.resetsAt !== undefined
|
||||||
};
|
? { resetsAt: args.bucket.resetsAt }
|
||||||
return {
|
: {}),
|
||||||
id: args.id,
|
};
|
||||||
label: args.label,
|
return {
|
||||||
scope: {
|
id: args.id,
|
||||||
provider: args.provider,
|
label: args.label,
|
||||||
windowId: args.windowId,
|
scope: {
|
||||||
tier: args.tier,
|
provider: args.provider,
|
||||||
shared: args.shared,
|
windowId: args.windowId,
|
||||||
},
|
tier: args.tier,
|
||||||
window,
|
shared: args.shared,
|
||||||
amount,
|
},
|
||||||
status: buildUsageStatus(amount.usedFraction),
|
window,
|
||||||
};
|
amount,
|
||||||
|
status: buildUsageStatus(amount.usedFraction),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
async function fetchClaudeUsage(
|
||||||
if (params.provider !== "anthropic") return null;
|
params: UsageFetchParams,
|
||||||
const credential = params.credential;
|
ctx: UsageFetchContext,
|
||||||
if (credential.type !== "oauth" || !credential.accessToken) return null;
|
): Promise<UsageReport | null> {
|
||||||
|
if (params.provider !== "anthropic") return null;
|
||||||
|
const credential = params.credential;
|
||||||
|
if (credential.type !== "oauth" || !credential.accessToken) return null;
|
||||||
|
|
||||||
const baseUrl = normalizeClaudeBaseUrl(params.baseUrl);
|
const baseUrl = normalizeClaudeBaseUrl(params.baseUrl);
|
||||||
const url = `${baseUrl}/usage`;
|
const url = `${baseUrl}/usage`;
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...CLAUDE_HEADERS,
|
...CLAUDE_HEADERS,
|
||||||
authorization: `Bearer ${credential.accessToken}`,
|
authorization: `Bearer ${credential.accessToken}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const payloadResult = await fetchUsagePayload(url, headers, ctx, params.signal);
|
const payloadResult = await fetchUsagePayload(
|
||||||
if (!payloadResult || !isRecord(payloadResult.payload)) return null;
|
url,
|
||||||
const { payload, orgId } = payloadResult;
|
headers,
|
||||||
|
ctx,
|
||||||
|
params.signal,
|
||||||
|
);
|
||||||
|
if (!payloadResult || !isRecord(payloadResult.payload)) return null;
|
||||||
|
const { payload, orgId } = payloadResult;
|
||||||
|
|
||||||
const fiveHour = parseBucket(payload.five_hour);
|
const fiveHour = parseBucket(payload.five_hour);
|
||||||
const sevenDay = parseBucket(payload.seven_day);
|
const sevenDay = parseBucket(payload.seven_day);
|
||||||
const sevenDayOpus = parseBucket(payload.seven_day_opus);
|
const sevenDayOpus = parseBucket(payload.seven_day_opus);
|
||||||
const sevenDaySonnet = parseBucket(payload.seven_day_sonnet);
|
const sevenDaySonnet = parseBucket(payload.seven_day_sonnet);
|
||||||
|
|
||||||
const limits = [
|
const limits = [
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
id: "anthropic:5h",
|
id: "anthropic:5h",
|
||||||
label: "Claude 5 Hour",
|
label: "Claude 5 Hour",
|
||||||
windowId: "5h",
|
windowId: "5h",
|
||||||
windowLabel: "5 Hour",
|
windowLabel: "5 Hour",
|
||||||
durationMs: FIVE_HOURS_MS,
|
durationMs: FIVE_HOURS_MS,
|
||||||
bucket: fiveHour,
|
bucket: fiveHour,
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
id: "anthropic:7d",
|
id: "anthropic:7d",
|
||||||
label: "Claude 7 Day",
|
label: "Claude 7 Day",
|
||||||
windowId: "7d",
|
windowId: "7d",
|
||||||
windowLabel: "7 Day",
|
windowLabel: "7 Day",
|
||||||
durationMs: SEVEN_DAYS_MS,
|
durationMs: SEVEN_DAYS_MS,
|
||||||
bucket: sevenDay,
|
bucket: sevenDay,
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
id: "anthropic:7d:opus",
|
id: "anthropic:7d:opus",
|
||||||
label: "Claude 7 Day (Opus)",
|
label: "Claude 7 Day (Opus)",
|
||||||
windowId: "7d",
|
windowId: "7d",
|
||||||
windowLabel: "7 Day",
|
windowLabel: "7 Day",
|
||||||
durationMs: SEVEN_DAYS_MS,
|
durationMs: SEVEN_DAYS_MS,
|
||||||
bucket: sevenDayOpus,
|
bucket: sevenDayOpus,
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
tier: "opus",
|
tier: "opus",
|
||||||
}),
|
}),
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
id: "anthropic:7d:sonnet",
|
id: "anthropic:7d:sonnet",
|
||||||
label: "Claude 7 Day (Sonnet)",
|
label: "Claude 7 Day (Sonnet)",
|
||||||
windowId: "7d",
|
windowId: "7d",
|
||||||
windowLabel: "7 Day",
|
windowLabel: "7 Day",
|
||||||
durationMs: SEVEN_DAYS_MS,
|
durationMs: SEVEN_DAYS_MS,
|
||||||
bucket: sevenDaySonnet,
|
bucket: sevenDaySonnet,
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
tier: "sonnet",
|
tier: "sonnet",
|
||||||
}),
|
}),
|
||||||
].filter((limit): limit is UsageLimit => limit !== null);
|
].filter((limit): limit is UsageLimit => limit !== null);
|
||||||
|
|
||||||
if (limits.length === 0) return null;
|
if (limits.length === 0) return null;
|
||||||
const identity = extractUsageIdentity(payload, orgId);
|
const identity = extractUsageIdentity(payload, orgId);
|
||||||
const accountId = identity.accountId ?? credential.accountId;
|
const accountId = identity.accountId ?? credential.accountId;
|
||||||
const email = identity.email ?? (await resolveEmail(params, ctx, baseUrl, headers));
|
const email =
|
||||||
|
identity.email ?? (await resolveEmail(params, ctx, baseUrl, headers));
|
||||||
|
|
||||||
const report: UsageReport = {
|
const report: UsageReport = {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
fetchedAt: Date.now(),
|
fetchedAt: Date.now(),
|
||||||
limits,
|
limits,
|
||||||
metadata: {
|
metadata: {
|
||||||
accountId,
|
accountId,
|
||||||
email,
|
email,
|
||||||
endpoint: url,
|
endpoint: url,
|
||||||
},
|
},
|
||||||
raw: payload,
|
raw: payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const claudeUsageProvider: UsageProvider = {
|
export const claudeUsageProvider: UsageProvider = {
|
||||||
id: "anthropic",
|
id: "anthropic",
|
||||||
fetchUsage: fetchClaudeUsage,
|
fetchUsage: fetchClaudeUsage,
|
||||||
supports: params => params.provider === "anthropic" && params.credential.type === "oauth",
|
supports: (params) =>
|
||||||
|
params.provider === "anthropic" && params.credential.type === "oauth",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const claudeRankingStrategy: CredentialRankingStrategy = {
|
export const claudeRankingStrategy: CredentialRankingStrategy = {
|
||||||
findWindowLimits(report) {
|
findWindowLimits(report) {
|
||||||
const primary = report.limits.find(l => l.id === "anthropic:5h");
|
const primary = report.limits.find((l) => l.id === "anthropic:5h");
|
||||||
const secondary = report.limits.find(l => l.id === "anthropic:7d");
|
const secondary = report.limits.find((l) => l.id === "anthropic:7d");
|
||||||
return { primary, secondary };
|
return { primary, secondary };
|
||||||
},
|
},
|
||||||
windowDefaults: { primaryMs: 5 * 60 * 60 * 1000, secondaryMs: 7 * 24 * 60 * 60 * 1000 },
|
windowDefaults: {
|
||||||
|
primaryMs: 5 * 60 * 60 * 1000,
|
||||||
|
secondaryMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
import { CODEX_BASE_URL } from "../providers/openai-codex/constants";
|
import { CODEX_BASE_URL } from "../providers/openai-codex/constants";
|
||||||
import type {
|
import type {
|
||||||
CredentialRankingStrategy,
|
CredentialRankingStrategy,
|
||||||
UsageAmount,
|
UsageAmount,
|
||||||
UsageFetchContext,
|
UsageFetchContext,
|
||||||
UsageFetchParams,
|
UsageFetchParams,
|
||||||
UsageLimit,
|
UsageLimit,
|
||||||
UsageProvider,
|
UsageProvider,
|
||||||
UsageReport,
|
UsageReport,
|
||||||
UsageWindow,
|
UsageWindow,
|
||||||
} from "../usage";
|
} from "../usage";
|
||||||
import { isRecord } from "../utils";
|
import { isRecord } from "../utils";
|
||||||
import { toNumber } from "./shared";
|
import { toNumber } from "./shared";
|
||||||
@@ -18,360 +18,415 @@ const JWT_AUTH_CLAIM = "https://api.openai.com/auth";
|
|||||||
const JWT_PROFILE_CLAIM = "https://api.openai.com/profile";
|
const JWT_PROFILE_CLAIM = "https://api.openai.com/profile";
|
||||||
|
|
||||||
interface CodexUsageWindowPayload {
|
interface CodexUsageWindowPayload {
|
||||||
used_percent?: number;
|
used_percent?: number;
|
||||||
limit_window_seconds?: number;
|
limit_window_seconds?: number;
|
||||||
reset_after_seconds?: number;
|
reset_after_seconds?: number;
|
||||||
reset_at?: number;
|
reset_at?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodexUsageRateLimitPayload {
|
interface CodexUsageRateLimitPayload {
|
||||||
allowed?: boolean;
|
allowed?: boolean;
|
||||||
limit_reached?: boolean;
|
limit_reached?: boolean;
|
||||||
primary_window?: CodexUsageWindowPayload | null;
|
primary_window?: CodexUsageWindowPayload | null;
|
||||||
secondary_window?: CodexUsageWindowPayload | null;
|
secondary_window?: CodexUsageWindowPayload | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodexUsagePayload {
|
interface CodexUsagePayload {
|
||||||
plan_type?: string;
|
plan_type?: string;
|
||||||
rate_limit?: CodexUsageRateLimitPayload | null;
|
rate_limit?: CodexUsageRateLimitPayload | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedUsageWindow {
|
interface ParsedUsageWindow {
|
||||||
usedPercent?: number;
|
usedPercent?: number;
|
||||||
limitWindowSeconds?: number;
|
limitWindowSeconds?: number;
|
||||||
resetAfterSeconds?: number;
|
resetAfterSeconds?: number;
|
||||||
resetAt?: number;
|
resetAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedUsage {
|
interface ParsedUsage {
|
||||||
planType?: string;
|
planType?: string;
|
||||||
allowed?: boolean;
|
allowed?: boolean;
|
||||||
limitReached?: boolean;
|
limitReached?: boolean;
|
||||||
primary?: ParsedUsageWindow;
|
primary?: ParsedUsageWindow;
|
||||||
secondary?: ParsedUsageWindow;
|
secondary?: ParsedUsageWindow;
|
||||||
raw: CodexUsagePayload;
|
raw: CodexUsagePayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JwtPayload {
|
interface JwtPayload {
|
||||||
[JWT_AUTH_CLAIM]?: {
|
[JWT_AUTH_CLAIM]?: {
|
||||||
chatgpt_account_id?: string;
|
chatgpt_account_id?: string;
|
||||||
};
|
};
|
||||||
[JWT_PROFILE_CLAIM]?: {
|
[JWT_PROFILE_CLAIM]?: {
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const toBoolean = (value: unknown): boolean | undefined => {
|
const toBoolean = (value: unknown): boolean | undefined => {
|
||||||
if (typeof value === "boolean") return value;
|
if (typeof value === "boolean") return value;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function base64UrlDecode(input: string): string {
|
function base64UrlDecode(input: string): string {
|
||||||
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
const padLen = (4 - (base64.length % 4)) % 4;
|
const padLen = (4 - (base64.length % 4)) % 4;
|
||||||
const padded = base64 + "=".repeat(padLen);
|
const padded = base64 + "=".repeat(padLen);
|
||||||
return Buffer.from(padded, "base64").toString("utf8");
|
return Buffer.from(padded, "base64").toString("utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJwt(token: string): JwtPayload | null {
|
function parseJwt(token: string): JwtPayload | null {
|
||||||
const parts = token.split(".");
|
const parts = token.split(".");
|
||||||
if (parts.length !== 3) return null;
|
if (parts.length !== 3) return null;
|
||||||
try {
|
try {
|
||||||
const payloadJson = base64UrlDecode(parts[1]);
|
const payloadJson = base64UrlDecode(parts[1]);
|
||||||
return JSON.parse(payloadJson) as JwtPayload;
|
return JSON.parse(payloadJson) as JwtPayload;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeEmail(email: string | undefined): string | undefined {
|
function normalizeEmail(email: string | undefined): string | undefined {
|
||||||
if (!email) return undefined;
|
if (!email) return undefined;
|
||||||
const normalized = email.trim().toLowerCase();
|
const normalized = email.trim().toLowerCase();
|
||||||
return normalized || undefined;
|
return normalized || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAccountId(token: string | undefined): string | undefined {
|
function extractAccountId(token: string | undefined): string | undefined {
|
||||||
if (!token) return undefined;
|
if (!token) return undefined;
|
||||||
const payload = parseJwt(token);
|
const payload = parseJwt(token);
|
||||||
return payload?.[JWT_AUTH_CLAIM]?.chatgpt_account_id ?? undefined;
|
return payload?.[JWT_AUTH_CLAIM]?.chatgpt_account_id ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractEmail(token: string | undefined): string | undefined {
|
function extractEmail(token: string | undefined): string | undefined {
|
||||||
if (!token) return undefined;
|
if (!token) return undefined;
|
||||||
const payload = parseJwt(token);
|
const payload = parseJwt(token);
|
||||||
return normalizeEmail(payload?.[JWT_PROFILE_CLAIM]?.email);
|
return normalizeEmail(payload?.[JWT_PROFILE_CLAIM]?.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUsageWindow(payload: unknown): ParsedUsageWindow | undefined {
|
function parseUsageWindow(payload: unknown): ParsedUsageWindow | undefined {
|
||||||
if (!isRecord(payload)) return undefined;
|
if (!isRecord(payload)) return undefined;
|
||||||
const usedPercent = toNumber(payload.used_percent);
|
const usedPercent = toNumber(payload.used_percent);
|
||||||
const limitWindowSeconds = toNumber(payload.limit_window_seconds);
|
const limitWindowSeconds = toNumber(payload.limit_window_seconds);
|
||||||
const resetAfterSeconds = toNumber(payload.reset_after_seconds);
|
const resetAfterSeconds = toNumber(payload.reset_after_seconds);
|
||||||
const resetAt = toNumber(payload.reset_at);
|
const resetAt = toNumber(payload.reset_at);
|
||||||
if (
|
if (
|
||||||
usedPercent === undefined &&
|
usedPercent === undefined &&
|
||||||
limitWindowSeconds === undefined &&
|
limitWindowSeconds === undefined &&
|
||||||
resetAfterSeconds === undefined &&
|
resetAfterSeconds === undefined &&
|
||||||
resetAt === undefined
|
resetAt === undefined
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
usedPercent,
|
usedPercent,
|
||||||
limitWindowSeconds,
|
limitWindowSeconds,
|
||||||
resetAfterSeconds,
|
resetAfterSeconds,
|
||||||
resetAt,
|
resetAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUsagePayload(payload: unknown): ParsedUsage | null {
|
function parseUsagePayload(payload: unknown): ParsedUsage | null {
|
||||||
if (!isRecord(payload)) return null;
|
if (!isRecord(payload)) return null;
|
||||||
const planType = typeof payload.plan_type === "string" ? payload.plan_type : undefined;
|
const planType =
|
||||||
const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : undefined;
|
typeof payload.plan_type === "string" ? payload.plan_type : undefined;
|
||||||
if (!rateLimit) return null;
|
const rateLimit = isRecord(payload.rate_limit)
|
||||||
const parsed: ParsedUsage = {
|
? payload.rate_limit
|
||||||
planType,
|
: undefined;
|
||||||
allowed: toBoolean(rateLimit.allowed),
|
if (!rateLimit) return null;
|
||||||
limitReached: toBoolean(rateLimit.limit_reached),
|
const parsed: ParsedUsage = {
|
||||||
primary: parseUsageWindow(rateLimit.primary_window),
|
planType,
|
||||||
secondary: parseUsageWindow(rateLimit.secondary_window),
|
allowed: toBoolean(rateLimit.allowed),
|
||||||
raw: payload as CodexUsagePayload,
|
limitReached: toBoolean(rateLimit.limit_reached),
|
||||||
};
|
primary: parseUsageWindow(rateLimit.primary_window),
|
||||||
if (!parsed.primary && !parsed.secondary && parsed.allowed === undefined && parsed.limitReached === undefined) {
|
secondary: parseUsageWindow(rateLimit.secondary_window),
|
||||||
return null;
|
raw: payload as CodexUsagePayload,
|
||||||
}
|
};
|
||||||
return parsed;
|
if (
|
||||||
|
!parsed.primary &&
|
||||||
|
!parsed.secondary &&
|
||||||
|
parsed.allowed === undefined &&
|
||||||
|
parsed.limitReached === undefined
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCodexBaseUrl(baseUrl?: string): string {
|
function normalizeCodexBaseUrl(baseUrl?: string): string {
|
||||||
const fallback = CODEX_BASE_URL;
|
const fallback = CODEX_BASE_URL;
|
||||||
const trimmed = baseUrl?.trim() ? baseUrl.trim() : fallback;
|
const trimmed = baseUrl?.trim() ? baseUrl.trim() : fallback;
|
||||||
const base = trimmed.replace(/\/+$/, "");
|
const base = trimmed.replace(/\/+$/, "");
|
||||||
const lower = base.toLowerCase();
|
const lower = base.toLowerCase();
|
||||||
if (
|
if (
|
||||||
(lower.startsWith("https://chatgpt.com") || lower.startsWith("https://chat.openai.com")) &&
|
(lower.startsWith("https://chatgpt.com") ||
|
||||||
!lower.includes("/backend-api")
|
lower.startsWith("https://chat.openai.com")) &&
|
||||||
) {
|
!lower.includes("/backend-api")
|
||||||
return `${base}/backend-api`;
|
) {
|
||||||
}
|
return `${base}/backend-api`;
|
||||||
return base;
|
}
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCodexUsageUrl(baseUrl: string): string {
|
function buildCodexUsageUrl(baseUrl: string): string {
|
||||||
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
||||||
return `${normalized}${CODEX_USAGE_PATH}`;
|
return `${normalized}${CODEX_USAGE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatWindowLabel(value: number, unit: "hour" | "day"): string {
|
function formatWindowLabel(value: number, unit: "hour" | "day"): string {
|
||||||
const rounded = Math.round(value);
|
const rounded = Math.round(value);
|
||||||
const suffix = rounded === 1 ? unit : `${unit}s`;
|
const suffix = rounded === 1 ? unit : `${unit}s`;
|
||||||
return `${rounded} ${suffix}`;
|
return `${rounded} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWindowLabel(seconds: number): { id: string; label: string } {
|
function buildWindowLabel(seconds: number): { id: string; label: string } {
|
||||||
const daySeconds = 86_400;
|
const daySeconds = 86_400;
|
||||||
if (seconds >= daySeconds) {
|
if (seconds >= daySeconds) {
|
||||||
const days = Math.round(seconds / daySeconds);
|
const days = Math.round(seconds / daySeconds);
|
||||||
return { id: `${days}d`, label: formatWindowLabel(days, "day") };
|
return { id: `${days}d`, label: formatWindowLabel(days, "day") };
|
||||||
}
|
}
|
||||||
const hours = Math.max(1, Math.round(seconds / 3600));
|
const hours = Math.max(1, Math.round(seconds / 3600));
|
||||||
return { id: `${hours}h`, label: formatWindowLabel(hours, "hour") };
|
return { id: `${hours}h`, label: formatWindowLabel(hours, "hour") };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveResetTime(window: ParsedUsageWindow, nowMs: number): number | undefined {
|
function resolveResetTime(
|
||||||
const resetAt = window.resetAt;
|
window: ParsedUsageWindow,
|
||||||
if (resetAt !== undefined) {
|
nowMs: number,
|
||||||
const resetAtMs = resetAt > 1_000_000_000_000 ? resetAt : resetAt * 1000;
|
): number | undefined {
|
||||||
if (Number.isFinite(resetAtMs)) return resetAtMs;
|
const resetAt = window.resetAt;
|
||||||
}
|
if (resetAt !== undefined) {
|
||||||
if (window.resetAfterSeconds !== undefined) {
|
const resetAtMs = resetAt > 1_000_000_000_000 ? resetAt : resetAt * 1000;
|
||||||
return nowMs + window.resetAfterSeconds * 1000;
|
if (Number.isFinite(resetAtMs)) return resetAtMs;
|
||||||
}
|
}
|
||||||
return undefined;
|
if (window.resetAfterSeconds !== undefined) {
|
||||||
|
return nowMs + window.resetAfterSeconds * 1000;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageWindow(window: ParsedUsageWindow, key: string, nowMs: number): UsageWindow {
|
function buildUsageWindow(
|
||||||
const resetsAt = resolveResetTime(window, nowMs);
|
window: ParsedUsageWindow,
|
||||||
if (window.limitWindowSeconds !== undefined) {
|
key: string,
|
||||||
const { id, label } = buildWindowLabel(window.limitWindowSeconds);
|
nowMs: number,
|
||||||
const durationMs = window.limitWindowSeconds * 1000;
|
): UsageWindow {
|
||||||
return { id, label, durationMs, ...(resetsAt !== undefined ? { resetsAt } : {}) };
|
const resetsAt = resolveResetTime(window, nowMs);
|
||||||
}
|
if (window.limitWindowSeconds !== undefined) {
|
||||||
const fallbackLabel = key === "primary" ? "Primary window" : "Secondary window";
|
const { id, label } = buildWindowLabel(window.limitWindowSeconds);
|
||||||
return { id: key, label: fallbackLabel, ...(resetsAt !== undefined ? { resetsAt } : {}) };
|
const durationMs = window.limitWindowSeconds * 1000;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
durationMs,
|
||||||
|
...(resetsAt !== undefined ? { resetsAt } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fallbackLabel =
|
||||||
|
key === "primary" ? "Primary window" : "Secondary window";
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
label: fallbackLabel,
|
||||||
|
...(resetsAt !== undefined ? { resetsAt } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
|
function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
|
||||||
const usedPercent = window.usedPercent;
|
const usedPercent = window.usedPercent;
|
||||||
if (usedPercent === undefined) {
|
if (usedPercent === undefined) {
|
||||||
return { unit: "percent" };
|
return { unit: "percent" };
|
||||||
}
|
}
|
||||||
const clamped = Math.min(Math.max(usedPercent, 0), 100);
|
const clamped = Math.min(Math.max(usedPercent, 0), 100);
|
||||||
const usedFraction = clamped / 100;
|
const usedFraction = clamped / 100;
|
||||||
return {
|
return {
|
||||||
used: clamped,
|
used: clamped,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
remaining: Math.max(0, 100 - clamped),
|
remaining: Math.max(0, 100 - clamped),
|
||||||
usedFraction,
|
usedFraction,
|
||||||
remainingFraction: Math.max(0, 1 - usedFraction),
|
remainingFraction: Math.max(0, 1 - usedFraction),
|
||||||
unit: "percent",
|
unit: "percent",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageStatus(usedFraction?: number, limitReached?: boolean): UsageLimit["status"] {
|
function buildUsageStatus(
|
||||||
if (limitReached) return "exhausted";
|
usedFraction?: number,
|
||||||
if (usedFraction === undefined) return "unknown";
|
limitReached?: boolean,
|
||||||
if (usedFraction >= 1) return "exhausted";
|
): UsageLimit["status"] {
|
||||||
if (usedFraction >= 0.9) return "warning";
|
if (limitReached) return "exhausted";
|
||||||
return "ok";
|
if (usedFraction === undefined) return "unknown";
|
||||||
|
if (usedFraction >= 1) return "exhausted";
|
||||||
|
if (usedFraction >= 0.9) return "warning";
|
||||||
|
return "ok";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageLimit(args: {
|
function buildUsageLimit(args: {
|
||||||
key: "primary" | "secondary";
|
key: "primary" | "secondary";
|
||||||
window: ParsedUsageWindow;
|
window: ParsedUsageWindow;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
planType?: string;
|
planType?: string;
|
||||||
limitReached?: boolean;
|
limitReached?: boolean;
|
||||||
nowMs: number;
|
nowMs: number;
|
||||||
}): UsageLimit {
|
}): UsageLimit {
|
||||||
const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
|
const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
|
||||||
const amount = buildUsageAmount(args.window);
|
const amount = buildUsageAmount(args.window);
|
||||||
return {
|
return {
|
||||||
id: `openai-codex:${args.key}`,
|
id: `openai-codex:${args.key}`,
|
||||||
label: usageWindow.label,
|
label: usageWindow.label,
|
||||||
scope: {
|
scope: {
|
||||||
provider: "openai-codex",
|
provider: "openai-codex",
|
||||||
accountId: args.accountId,
|
accountId: args.accountId,
|
||||||
tier: args.planType,
|
tier: args.planType,
|
||||||
windowId: usageWindow.id,
|
windowId: usageWindow.id,
|
||||||
shared: true,
|
shared: true,
|
||||||
},
|
},
|
||||||
window: usageWindow,
|
window: usageWindow,
|
||||||
amount,
|
amount,
|
||||||
status: buildUsageStatus(amount.usedFraction, args.limitReached),
|
status: buildUsageStatus(amount.usedFraction, args.limitReached),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openaiCodexUsageProvider: UsageProvider = {
|
export const openaiCodexUsageProvider: UsageProvider = {
|
||||||
id: "openai-codex",
|
id: "openai-codex",
|
||||||
supports(params: UsageFetchParams): boolean {
|
supports(params: UsageFetchParams): boolean {
|
||||||
return params.provider === "openai-codex" && params.credential.type === "oauth";
|
return (
|
||||||
},
|
params.provider === "openai-codex" && params.credential.type === "oauth"
|
||||||
async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
);
|
||||||
if (params.provider !== "openai-codex") return null;
|
},
|
||||||
const { credential } = params;
|
async fetchUsage(
|
||||||
if (credential.type !== "oauth") return null;
|
params: UsageFetchParams,
|
||||||
|
ctx: UsageFetchContext,
|
||||||
|
): Promise<UsageReport | null> {
|
||||||
|
if (params.provider !== "openai-codex") return null;
|
||||||
|
const { credential } = params;
|
||||||
|
if (credential.type !== "oauth") return null;
|
||||||
|
|
||||||
const accessToken = credential.accessToken;
|
const accessToken = credential.accessToken;
|
||||||
if (!accessToken) return null;
|
if (!accessToken) return null;
|
||||||
|
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) {
|
if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) {
|
||||||
ctx.logger?.warn("Codex usage token expired", { provider: params.provider });
|
ctx.logger?.warn("Codex usage token expired", {
|
||||||
return null;
|
provider: params.provider,
|
||||||
}
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = normalizeCodexBaseUrl(params.baseUrl);
|
const baseUrl = normalizeCodexBaseUrl(params.baseUrl);
|
||||||
const accountId = credential.accountId ?? extractAccountId(accessToken);
|
const accountId = credential.accountId ?? extractAccountId(accessToken);
|
||||||
const email = normalizeEmail(credential.email ?? extractEmail(accessToken));
|
const email = normalizeEmail(credential.email ?? extractEmail(accessToken));
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
||||||
};
|
};
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
headers["ChatGPT-Account-Id"] = accountId;
|
headers["ChatGPT-Account-Id"] = accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildCodexUsageUrl(baseUrl);
|
const url = buildCodexUsageUrl(baseUrl);
|
||||||
let payload: unknown;
|
let payload: unknown;
|
||||||
try {
|
try {
|
||||||
const response = await ctx.fetch(url, { headers, signal: params.signal });
|
const response = await ctx.fetch(url, { headers, signal: params.signal });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
ctx.logger?.warn("Codex usage request failed", { status: response.status, provider: params.provider });
|
ctx.logger?.warn("Codex usage request failed", {
|
||||||
return null;
|
status: response.status,
|
||||||
}
|
provider: params.provider,
|
||||||
payload = await response.json();
|
});
|
||||||
} catch (error) {
|
return null;
|
||||||
ctx.logger?.warn("Codex usage request error", { provider: params.provider, error: String(error) });
|
}
|
||||||
return null;
|
payload = await response.json();
|
||||||
}
|
} catch (error) {
|
||||||
|
ctx.logger?.warn("Codex usage request error", {
|
||||||
|
provider: params.provider,
|
||||||
|
error: String(error),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = parseUsagePayload(payload);
|
const parsed = parseUsagePayload(payload);
|
||||||
const planType =
|
const planType =
|
||||||
parsed?.planType ??
|
parsed?.planType ??
|
||||||
(isRecord(payload) && typeof payload.plan_type === "string" ? payload.plan_type : undefined);
|
(isRecord(payload) && typeof payload.plan_type === "string"
|
||||||
|
? payload.plan_type
|
||||||
|
: undefined);
|
||||||
|
|
||||||
const limits: UsageLimit[] = [];
|
const limits: UsageLimit[] = [];
|
||||||
if (parsed?.primary) {
|
if (parsed?.primary) {
|
||||||
limits.push(
|
limits.push(
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
key: "primary",
|
key: "primary",
|
||||||
window: parsed.primary,
|
window: parsed.primary,
|
||||||
accountId,
|
accountId,
|
||||||
planType,
|
planType,
|
||||||
limitReached: parsed.limitReached,
|
limitReached: parsed.limitReached,
|
||||||
nowMs,
|
nowMs,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (parsed?.secondary) {
|
if (parsed?.secondary) {
|
||||||
limits.push(
|
limits.push(
|
||||||
buildUsageLimit({
|
buildUsageLimit({
|
||||||
key: "secondary",
|
key: "secondary",
|
||||||
window: parsed.secondary,
|
window: parsed.secondary,
|
||||||
accountId,
|
accountId,
|
||||||
planType,
|
planType,
|
||||||
limitReached: parsed.limitReached,
|
limitReached: parsed.limitReached,
|
||||||
nowMs,
|
nowMs,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const report: UsageReport = {
|
const report: UsageReport = {
|
||||||
provider: "openai-codex",
|
provider: "openai-codex",
|
||||||
fetchedAt: nowMs,
|
fetchedAt: nowMs,
|
||||||
limits,
|
limits,
|
||||||
metadata: {
|
metadata: {
|
||||||
planType,
|
planType,
|
||||||
allowed: parsed?.allowed,
|
allowed: parsed?.allowed,
|
||||||
limitReached: parsed?.limitReached,
|
limitReached: parsed?.limitReached,
|
||||||
email,
|
email,
|
||||||
accountId,
|
accountId,
|
||||||
},
|
},
|
||||||
raw: parsed?.raw ?? payload,
|
raw: parsed?.raw ?? payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export const codexRankingStrategy: CredentialRankingStrategy = {
|
export const codexRankingStrategy: CredentialRankingStrategy = {
|
||||||
findWindowLimits(report) {
|
findWindowLimits(report) {
|
||||||
const findLimit = (key: "primary" | "secondary"): UsageLimit | undefined => {
|
const findLimit = (
|
||||||
const direct = report.limits.find(l => l.id === `openai-codex:${key}`);
|
key: "primary" | "secondary",
|
||||||
if (direct) return direct;
|
): UsageLimit | undefined => {
|
||||||
const byId = report.limits.find(l => l.id.toLowerCase().includes(key));
|
const direct = report.limits.find((l) => l.id === `openai-codex:${key}`);
|
||||||
if (byId) return byId;
|
if (direct) return direct;
|
||||||
const windowId = key === "secondary" ? "7d" : "1h";
|
const byId = report.limits.find((l) => l.id.toLowerCase().includes(key));
|
||||||
return report.limits.find(l => l.scope.windowId?.toLowerCase() === windowId);
|
if (byId) return byId;
|
||||||
};
|
const windowId = key === "secondary" ? "7d" : "1h";
|
||||||
return { primary: findLimit("primary"), secondary: findLimit("secondary") };
|
return report.limits.find(
|
||||||
},
|
(l) => l.scope.windowId?.toLowerCase() === windowId,
|
||||||
windowDefaults: { primaryMs: 60 * 60 * 1000, secondaryMs: 7 * 24 * 60 * 60 * 1000 },
|
);
|
||||||
hasPriorityBoost(primary) {
|
};
|
||||||
if (!primary) return false;
|
return { primary: findLimit("primary"), secondary: findLimit("secondary") };
|
||||||
const windowId = primary.scope.windowId?.toLowerCase();
|
},
|
||||||
const durationMs = primary.window?.durationMs;
|
windowDefaults: {
|
||||||
const isFiveHourWindow =
|
primaryMs: 60 * 60 * 1000,
|
||||||
windowId === "5h" ||
|
secondaryMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
(typeof durationMs === "number" &&
|
},
|
||||||
Number.isFinite(durationMs) &&
|
hasPriorityBoost(primary) {
|
||||||
Math.abs(durationMs - FIVE_HOUR_MS) <= 60_000);
|
if (!primary) return false;
|
||||||
if (!isFiveHourWindow) return false;
|
const windowId = primary.scope.windowId?.toLowerCase();
|
||||||
const usedFraction = primary.amount.usedFraction;
|
const durationMs = primary.window?.durationMs;
|
||||||
return typeof usedFraction === "number" && Number.isFinite(usedFraction) && usedFraction === 0;
|
const isFiveHourWindow =
|
||||||
},
|
windowId === "5h" ||
|
||||||
|
(typeof durationMs === "number" &&
|
||||||
|
Number.isFinite(durationMs) &&
|
||||||
|
Math.abs(durationMs - FIVE_HOUR_MS) <= 60_000);
|
||||||
|
if (!isFiveHourWindow) return false;
|
||||||
|
const usedFraction = primary.amount.usedFraction;
|
||||||
|
return (
|
||||||
|
typeof usedFraction === "number" &&
|
||||||
|
Number.isFinite(usedFraction) &&
|
||||||
|
usedFraction === 0
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export const toNumber = (value: unknown): number | undefined => {
|
export const toNumber = (value: unknown): number | undefined => {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
const parsed = Number(trimmed);
|
const parsed = Number(trimmed);
|
||||||
if (Number.isFinite(parsed)) return parsed;
|
if (Number.isFinite(parsed)) return parsed;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
411
usage/zai.ts
411
usage/zai.ts
@@ -1,12 +1,12 @@
|
|||||||
import type {
|
import type {
|
||||||
UsageAmount,
|
UsageAmount,
|
||||||
UsageFetchContext,
|
UsageFetchContext,
|
||||||
UsageFetchParams,
|
UsageFetchParams,
|
||||||
UsageLimit,
|
UsageLimit,
|
||||||
UsageProvider,
|
UsageProvider,
|
||||||
UsageReport,
|
UsageReport,
|
||||||
UsageStatus,
|
UsageStatus,
|
||||||
UsageWindow,
|
UsageWindow,
|
||||||
} from "../usage";
|
} from "../usage";
|
||||||
import { isRecord, toNumber } from "../utils";
|
import { isRecord, toNumber } from "../utils";
|
||||||
|
|
||||||
@@ -16,232 +16,247 @@ const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
|
|||||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
function normalizeZaiBaseUrl(baseUrl?: string): string {
|
function normalizeZaiBaseUrl(baseUrl?: string): string {
|
||||||
if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
|
if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
|
||||||
try {
|
try {
|
||||||
return new URL(baseUrl.trim()).origin;
|
return new URL(baseUrl.trim()).origin;
|
||||||
} catch {
|
} catch {
|
||||||
return DEFAULT_ENDPOINT;
|
return DEFAULT_ENDPOINT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ZaiUsageLimitItem {
|
interface ZaiUsageLimitItem {
|
||||||
type?: string;
|
type?: string;
|
||||||
usage?: number;
|
usage?: number;
|
||||||
currentValue?: number;
|
currentValue?: number;
|
||||||
percentage?: number;
|
percentage?: number;
|
||||||
remaining?: number;
|
remaining?: number;
|
||||||
nextResetTime?: number;
|
nextResetTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ZaiQuotaPayload {
|
interface ZaiQuotaPayload {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
code?: number;
|
code?: number;
|
||||||
msg?: string;
|
msg?: string;
|
||||||
data?: {
|
data?: {
|
||||||
limits?: ZaiUsageLimitItem[];
|
limits?: ZaiUsageLimitItem[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMillis(value: unknown): number | undefined {
|
function parseMillis(value: unknown): number | undefined {
|
||||||
const parsed = toNumber(value);
|
const parsed = toNumber(value);
|
||||||
if (parsed === undefined) return undefined;
|
if (parsed === undefined) return undefined;
|
||||||
return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
|
return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
|
function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
|
||||||
if (!isRecord(value)) return null;
|
if (!isRecord(value)) return null;
|
||||||
const type = typeof value.type === "string" ? value.type : undefined;
|
const type = typeof value.type === "string" ? value.type : undefined;
|
||||||
if (!type) return null;
|
if (!type) return null;
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
usage: toNumber(value.usage),
|
usage: toNumber(value.usage),
|
||||||
currentValue: toNumber(value.currentValue),
|
currentValue: toNumber(value.currentValue),
|
||||||
percentage: toNumber(value.percentage),
|
percentage: toNumber(value.percentage),
|
||||||
remaining: toNumber(value.remaining),
|
remaining: toNumber(value.remaining),
|
||||||
nextResetTime: parseMillis(value.nextResetTime),
|
nextResetTime: parseMillis(value.nextResetTime),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUsageAmount(args: {
|
function buildUsageAmount(args: {
|
||||||
used: number | undefined;
|
used: number | undefined;
|
||||||
limit: number | undefined;
|
limit: number | undefined;
|
||||||
remaining: number | undefined;
|
remaining: number | undefined;
|
||||||
unit: UsageAmount["unit"];
|
unit: UsageAmount["unit"];
|
||||||
percentage?: number;
|
percentage?: number;
|
||||||
}): UsageAmount {
|
}): UsageAmount {
|
||||||
const usedFraction =
|
const usedFraction =
|
||||||
args.percentage !== undefined
|
args.percentage !== undefined
|
||||||
? Math.min(Math.max(args.percentage / 100, 0), 1)
|
? Math.min(Math.max(args.percentage / 100, 0), 1)
|
||||||
: args.used !== undefined && args.limit !== undefined && args.limit > 0
|
: args.used !== undefined && args.limit !== undefined && args.limit > 0
|
||||||
? Math.min(args.used / args.limit, 1)
|
? Math.min(args.used / args.limit, 1)
|
||||||
: undefined;
|
: undefined;
|
||||||
const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
const remainingFraction =
|
||||||
return {
|
usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
||||||
used: args.used,
|
return {
|
||||||
limit: args.limit,
|
used: args.used,
|
||||||
remaining: args.remaining,
|
limit: args.limit,
|
||||||
usedFraction,
|
remaining: args.remaining,
|
||||||
remainingFraction,
|
usedFraction,
|
||||||
unit: args.unit,
|
remainingFraction,
|
||||||
};
|
unit: args.unit,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
|
function getUsageStatus(
|
||||||
if (usedFraction === undefined) return undefined;
|
usedFraction: number | undefined,
|
||||||
if (usedFraction >= 1) return "exhausted";
|
): UsageStatus | undefined {
|
||||||
if (usedFraction >= 0.9) return "warning";
|
if (usedFraction === undefined) return undefined;
|
||||||
return "ok";
|
if (usedFraction >= 1) return "exhausted";
|
||||||
|
if (usedFraction >= 0.9) return "warning";
|
||||||
|
return "ok";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: Date): string {
|
function formatDate(value: Date): string {
|
||||||
const pad = (input: number) => String(input).padStart(2, "0");
|
const pad = (input: number) => String(input).padStart(2, "0");
|
||||||
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}+${pad(value.getHours())}:${pad(
|
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}+${pad(value.getHours())}:${pad(
|
||||||
value.getMinutes(),
|
value.getMinutes(),
|
||||||
)}:${pad(value.getSeconds())}`;
|
)}:${pad(value.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildModelUsageUrl(baseUrl: string, now: Date): string {
|
function buildModelUsageUrl(baseUrl: string, now: Date): string {
|
||||||
const start = new Date(now.getTime() - SEVEN_DAYS_MS);
|
const start = new Date(now.getTime() - SEVEN_DAYS_MS);
|
||||||
const startTime = formatDate(start);
|
const startTime = formatDate(start);
|
||||||
const endTime = formatDate(now);
|
const endTime = formatDate(now);
|
||||||
return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
|
return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
async function fetchZaiUsage(
|
||||||
if (params.provider !== "zai") return null;
|
params: UsageFetchParams,
|
||||||
const credential = params.credential;
|
ctx: UsageFetchContext,
|
||||||
if (credential.type !== "api_key" || !credential.apiKey) return null;
|
): Promise<UsageReport | null> {
|
||||||
|
if (params.provider !== "zai") return null;
|
||||||
|
const credential = params.credential;
|
||||||
|
if (credential.type !== "api_key" || !credential.apiKey) return null;
|
||||||
|
|
||||||
const baseUrl = normalizeZaiBaseUrl(params.baseUrl);
|
const baseUrl = normalizeZaiBaseUrl(params.baseUrl);
|
||||||
const url = `${baseUrl}${QUOTA_PATH}`;
|
const url = `${baseUrl}${QUOTA_PATH}`;
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: credential.apiKey,
|
Authorization: credential.apiKey,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
"User-Agent": "OpenCode-Status-Plugin/1.0",
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload: ZaiQuotaPayload | null = null;
|
let payload: ZaiQuotaPayload | null = null;
|
||||||
try {
|
try {
|
||||||
const response = await ctx.fetch(url, {
|
const response = await ctx.fetch(url, {
|
||||||
headers,
|
headers,
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
ctx.logger?.warn("ZAI usage fetch failed", { status: response.status, statusText: response.statusText });
|
ctx.logger?.warn("ZAI usage fetch failed", {
|
||||||
return null;
|
status: response.status,
|
||||||
}
|
statusText: response.statusText,
|
||||||
payload = (await response.json()) as ZaiQuotaPayload;
|
});
|
||||||
} catch (error) {
|
return null;
|
||||||
ctx.logger?.warn("ZAI usage fetch error", { error: String(error) });
|
}
|
||||||
return null;
|
payload = (await response.json()) as ZaiQuotaPayload;
|
||||||
}
|
} catch (error) {
|
||||||
|
ctx.logger?.warn("ZAI usage fetch error", { error: String(error) });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
if (payload.success !== true) {
|
if (payload.success !== true) {
|
||||||
ctx.logger?.warn("ZAI usage response invalid", { code: payload.code, message: payload.msg });
|
ctx.logger?.warn("ZAI usage response invalid", {
|
||||||
return null;
|
code: payload.code,
|
||||||
}
|
message: payload.msg,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const limitsPayload = Array.isArray(payload.data?.limits) ? payload.data?.limits : [];
|
const limitsPayload = Array.isArray(payload.data?.limits)
|
||||||
const limits: UsageLimit[] = [];
|
? payload.data?.limits
|
||||||
|
: [];
|
||||||
|
const limits: UsageLimit[] = [];
|
||||||
|
|
||||||
for (const rawLimit of limitsPayload) {
|
for (const rawLimit of limitsPayload) {
|
||||||
const parsed = parseLimitItem(rawLimit);
|
const parsed = parseLimitItem(rawLimit);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
if (parsed.type === "TOKENS_LIMIT") {
|
if (parsed.type === "TOKENS_LIMIT") {
|
||||||
const amount = buildUsageAmount({
|
const amount = buildUsageAmount({
|
||||||
used: parsed.currentValue,
|
used: parsed.currentValue,
|
||||||
limit: parsed.usage,
|
limit: parsed.usage,
|
||||||
remaining: parsed.remaining,
|
remaining: parsed.remaining,
|
||||||
percentage: parsed.percentage,
|
percentage: parsed.percentage,
|
||||||
unit: "tokens",
|
unit: "tokens",
|
||||||
});
|
});
|
||||||
const window: UsageWindow = {
|
const window: UsageWindow = {
|
||||||
id: "quota",
|
id: "quota",
|
||||||
label: "Quota",
|
label: "Quota",
|
||||||
durationMs: SEVEN_DAYS_MS,
|
durationMs: SEVEN_DAYS_MS,
|
||||||
resetsAt: parsed.nextResetTime,
|
resetsAt: parsed.nextResetTime,
|
||||||
};
|
};
|
||||||
limits.push({
|
limits.push({
|
||||||
id: "zai:tokens",
|
id: "zai:tokens",
|
||||||
label: "ZAI Token Quota",
|
label: "ZAI Token Quota",
|
||||||
scope: {
|
scope: {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
windowId: window?.id ?? "quota",
|
windowId: window?.id ?? "quota",
|
||||||
shared: true,
|
shared: true,
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
amount,
|
amount,
|
||||||
status: getUsageStatus(amount.usedFraction),
|
status: getUsageStatus(amount.usedFraction),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (parsed.type === "TIME_LIMIT") {
|
if (parsed.type === "TIME_LIMIT") {
|
||||||
const window: UsageWindow = {
|
const window: UsageWindow = {
|
||||||
id: "quota",
|
id: "quota",
|
||||||
label: "Quota",
|
label: "Quota",
|
||||||
durationMs: SEVEN_DAYS_MS,
|
durationMs: SEVEN_DAYS_MS,
|
||||||
resetsAt: parsed.nextResetTime,
|
resetsAt: parsed.nextResetTime,
|
||||||
};
|
};
|
||||||
const amount = buildUsageAmount({
|
const amount = buildUsageAmount({
|
||||||
used: parsed.currentValue,
|
used: parsed.currentValue,
|
||||||
limit: parsed.usage,
|
limit: parsed.usage,
|
||||||
remaining: parsed.remaining,
|
remaining: parsed.remaining,
|
||||||
percentage: parsed.percentage,
|
percentage: parsed.percentage,
|
||||||
unit: "requests",
|
unit: "requests",
|
||||||
});
|
});
|
||||||
limits.push({
|
limits.push({
|
||||||
id: "zai:requests",
|
id: "zai:requests",
|
||||||
label: "ZAI Request Quota",
|
label: "ZAI Request Quota",
|
||||||
scope: {
|
scope: {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
windowId: "quota",
|
windowId: "quota",
|
||||||
shared: true,
|
shared: true,
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
amount,
|
amount,
|
||||||
status: getUsageStatus(amount.usedFraction),
|
status: getUsageStatus(amount.usedFraction),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limits.length === 0) return null;
|
if (limits.length === 0) return null;
|
||||||
|
|
||||||
const report: UsageReport = {
|
const report: UsageReport = {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
fetchedAt: Date.now(),
|
fetchedAt: Date.now(),
|
||||||
limits,
|
limits,
|
||||||
metadata: {
|
metadata: {
|
||||||
endpoint: url,
|
endpoint: url,
|
||||||
accountId: credential.accountId,
|
accountId: credential.accountId,
|
||||||
email: credential.email,
|
email: credential.email,
|
||||||
},
|
},
|
||||||
raw: payload,
|
raw: payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modelUsageUrl = buildModelUsageUrl(baseUrl, new Date());
|
const modelUsageUrl = buildModelUsageUrl(baseUrl, new Date());
|
||||||
try {
|
try {
|
||||||
const response = await ctx.fetch(modelUsageUrl, {
|
const response = await ctx.fetch(modelUsageUrl, {
|
||||||
headers,
|
headers,
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const modelUsagePayload = (await response.json()) as unknown;
|
const modelUsagePayload = (await response.json()) as unknown;
|
||||||
if (isRecord(modelUsagePayload)) {
|
if (isRecord(modelUsagePayload)) {
|
||||||
report.metadata = {
|
report.metadata = {
|
||||||
...report.metadata,
|
...report.metadata,
|
||||||
modelUsage: modelUsagePayload,
|
modelUsage: modelUsagePayload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger?.debug("ZAI model usage fetch failed", { error: String(error) });
|
ctx.logger?.debug("ZAI model usage fetch failed", { error: String(error) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zaiUsageProvider: UsageProvider = {
|
export const zaiUsageProvider: UsageProvider = {
|
||||||
id: "zai",
|
id: "zai",
|
||||||
fetchUsage: fetchZaiUsage,
|
fetchUsage: fetchZaiUsage,
|
||||||
supports: params => params.provider === "zai" && params.credential.type === "api_key",
|
supports: (params) =>
|
||||||
|
params.provider === "zai" && params.credential.type === "api_key",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user