根据新接口改了一版Codex额度统计油猴脚本

卡卡罗特 2026-08-04 14:05 1

最近抓codex额度统计页的时候,发现有接口重新能返回具体模型的token使用情况统计了

于是把佬友之前的油猴统计脚本重新改了一版

缺点是统计有延迟,当天的token使用数据可能要第二天才有返回(也可能是半天,具体延迟多久不太清楚),但是使用百分比是实时变化的

返回token数据的接口并没有返回详细时间,只返回的日期,所以如果一天跨了两个周期并且两个周期都有使用会导致计算金额偏高


直接把以下脚本复制到油猴里,然后打开https://chatgpt.com/codex/cloud/settings/analytics

就能查看统计了


// ==UserScript==
// @name Codex Quota Compass (GPT-5.6 Model Stats)
// @namespace http://tampermonkey.net/
// @version 3.4.0
// @description Team 按模型 Token 估算;个人账号按 credits 点数独立展示并以 25 点 = 1 美元换算
// @author Jun Zhao, Canxin, loongphy, OpenAI
// @match https://chatgpt.com/codex/cloud/settings/analytics*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==

(function () {
"use strict";

if (window.__codexQuotaCompassInjected) return;
window.__codexQuotaCompassInjected = true;

const CONFIG = {
TARGET_WINDOW_SECONDS: 7 * 24 * 60 * 60,
DAY_MS: 24 * 60 * 60 * 1000,
USE_UTC_DAY: true,
EPS: 1e-9,
PERSONAL_CREDITS_PER_USD: 25,

// Standard API USD per 1M tokens. Fast applies the documented Codex credit multiplier
// as an estimated USD-equivalent multiplier; this is not an API bill.
// Sources: https://developers.openai.com/api/docs/models/gpt-5.6-terra,
// https://developers.openai.com/api/docs/models/gpt-5.6-luna, and
// https://help.openai.com/en/articles/20001106-codex-rate-card
// codex-auto-review is intentionally absent because it is not a public API model ID.
MODEL_PRICE_PER_1M: {
"gpt-5.6-sol": {
uncachedInput: 5.0,
cachedInput: 0.5,
output: 30.0,
fastMultiplier: 2.5,
},
"gpt-5.6-terra": {
uncachedInput: 2.0,
cachedInput: 0.2,
output: 12.0,
fastMultiplier: 2.5,
},
"gpt-5.6-luna": {
uncachedInput: 0.2,
cachedInput: 0.02,
output: 1.2,
fastMultiplier: 2.5,
},
"gpt-5.5": {
uncachedInput: 5.0,
cachedInput: 0.5,
output: 30.0,
fastMultiplier: 2.5,
},
"gpt-5.4": {
uncachedInput: 2.5,
cachedInput: 0.25,
output: 15.0,
fastMultiplier: 2.0,
},
"gpt-5.4-mini": {
uncachedInput: 0.75,
cachedInput: 0.075,
output: 4.5,
fastMultiplier: 2.0,
},
},
};

const HTML_ESCAPE = {
"&": "&",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};

const escapeHtml = (value) =>
String(value ?? "").replace(/[&<>"']/g, (ch) => HTML_ESCAPE[ch]);

const n = (value) => {
if (typeof value === "string") value = value.replace(/,/g, "").trim();
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};

const clamp = (value, min = 0, max = 1) =>
Math.min(max, Math.max(min, n(value)));

const trimFixed = (value, digits = 2) => {
const str = n(value).toFixed(digits);
return str.replace(/\.?0+$/, "");
};

const firstFinite = (...values) => {
for (const value of values) {
const num = n(value);
if (Number.isFinite(num) && num > 0) return num;
}
return 0;
};

const asArray = (value) => (Array.isArray(value) ? value : []);

const fmtNum = (value) => {
const num = n(value);
const abs = Math.abs(num);
const sign = num < 0 ? "-" : "";
if (abs >= 1e12) return sign + trimFixed(abs / 1e12, 2) + "T";
if (abs >= 1e9) return sign + trimFixed(abs / 1e9, 2) + "B";
if (abs >= 1e6) return sign + trimFixed(abs / 1e6, 2) + "M";
if (abs >= 1e3) return sign + trimFixed(abs / 1e3, 2) + "K";
return num.toLocaleString();
};

const fmtUsd = (value) => `$ ${n(value).toFixed(2)}`;

const fmtCredits = (value) =>
n(value).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 4,
});

const creditsToUsd = (credits) =>
n(credits) / CONFIG.PERSONAL_CREDITS_PER_USD;

const formatCreditCalculation = (credits) =>
`${fmtCredits(credits)} 点 ÷ ${CONFIG.PERSONAL_CREDITS_PER_USD} 点/美元 = ${fmtUsd(
creditsToUsd(credits),
)}`;

const tokenParts = (obj = {}) => ({
uncachedInput: n(obj.uncached_text_input_tokens),
cachedInput: n(obj.cached_text_input_tokens),
output: n(obj.text_output_tokens),
});

const tokenTotal = (obj = {}) => {
const total = n(obj.text_total_tokens);
const parts = tokenParts(obj);
const derived = parts.uncachedInput + parts.cachedInput + parts.output;
return total > 0 ? total : derived;
};

const normalizeModelName = (value) =>
String(value || "UNKNOWN").trim().toLowerCase();

const normalizeSpeed = (value, fallback = "standard") =>
String(value || fallback).trim().toLowerCase();

const getModelPrice = (modelName, speed = "standard") => {
const base = CONFIG.MODEL_PRICE_PER_1M[normalizeModelName(modelName)];
if (!base) return null;
const multiplier =
String(speed || "standard").toLowerCase() === "fast"
? base.fastMultiplier || 1
: 1;
return {
uncachedInput: base.uncachedInput * multiplier,
cachedInput: base.cachedInput * multiplier,
output: base.output * multiplier,
};
};

const estimateModelUsd = (obj = {}) => {
const p = getModelPrice(
obj.model || obj.model_id || obj.model_name || obj.name || obj.id,
obj.speed,
);
if (!p) return null;
const parts = tokenParts(obj);

return (
(parts.uncachedInput / 1_000_000) * p.uncachedInput +
(parts.cachedInput / 1_000_000) * p.cachedInput +
(parts.output / 1_000_000) * p.output
);
};

const fmtCalcNumber = (value, digits = 6) => trimFixed(value, digits);

const fmtTokenMillions = (tokens) =>
`${fmtCalcNumber(n(tokens) / 1_000_000)}M`;

const fmtCalcUsd = (value) => `$${fmtCalcNumber(value)}`;

const formatCostLine = (label, tokens, pricePerMillion) => {
const subtotal = (n(tokens) / 1_000_000) * n(pricePerMillion);
return `${label} ${fmtTokenMillions(tokens)} × ${fmtCalcUsd(
pricePerMillion,
)} = ${fmtCalcUsd(subtotal)}`;
};

const formatModelCostCalculation = (row = {}) => {
if (row.hasUnknownPricing) {
return `${row.name || "该模型"} 没有公开价格,无法显示完整计算过程。`;
}

const speed = normalizeSpeed(row.speed);
const price = getModelPrice(row.name, speed);
if (!price) return "没有可计算的 Token 数据。";

const basePrice = CONFIG.MODEL_PRICE_PER_1M[normalizeModelName(row.name)];
const multiplier = speed === "fast" ? n(basePrice?.fastMultiplier) || 1 : 1;
return [
speed === "fast"
? `Fast(按 Standard 的 ${fmtCalcNumber(multiplier, 2)}× 估算)`
: "Standard",
formatCostLine("未缓存输入", row.uncachedInputTokens, price.uncachedInput),
formatCostLine("缓存输入", row.cachedInputTokens, price.cachedInput),
formatCostLine("输出", row.outputTokens, price.output),
`合计 = ${fmtCalcUsd(row.estimatedUsd)}`,
].join("\n");
};

const fmtEstimatedUsd = (value, hasUnknownPricing = false) =>
`${fmtUsd(value)}${hasUnknownPricing ? "*" : ""}`;

const pad2 = (value) => String(value).padStart(2, "0");

const dateKeyFromMs = (ms) => {
const d = new Date(ms);
if (!Number.isFinite(d.getTime())) return "";
if (CONFIG.USE_UTC_DAY) return d.toISOString().slice(0, 10);
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
};

const dayStartMs = (dateKey) => {
const [year, month, day] = String(dateKey || "")
.slice(0, 10)
.split("-")
.map(Number);
if (!year || !month || !day) return NaN;
return CONFIG.USE_UTC_DAY
? Date.UTC(year, month - 1, day)
: new Date(year, month - 1, day).getTime();
};

const formatCycleDateTime = (ms) => {
const d = new Date(ms);
if (!Number.isFinite(d.getTime())) return "未知";

return `${d.getFullYear()}/${pad2(d.getMonth() + 1)}/${pad2(
d.getDate(),
)} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
};

const formatPercentFromRatio = (ratio) => {
const pct = clamp(ratio) * 100;
return `${pct >= 10 ? pct.toFixed(1) : pct.toFixed(2)}%`;
};

async function getAccessToken() {
try {
const res = await fetch("/api/auth/session", {
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
if (!res.ok) return null;
const session = await res.json();
const token =
session?.accessToken ||
session?.access_token ||
session?.token ||
null;

if (!token) {
console.warn(
"[Codex Quota Compass] Current session did not include an access token.",
session,
);
}

return token;
} catch (error) {
console.warn(
"[Codex Quota Compass] Failed to fetch current session token.",
error,
);
return null;
}
}

async function apiGet(path, token) {
const res = await fetch(path, {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
credentials: "include",
cache: "no-store",
});

const text = await res.text();
let json = null;

if (text) {
try {
json = JSON.parse(text);
} catch {
// ignore parse error
}
}

if (!res.ok) {
const detail =
json?.detail ||
json?.message ||
(typeof json?.error === "string" ? json.error : json?.error?.message) ||
text.slice(0, 200);

const error = new Error(
`${res.status} ${res.statusText}${detail ? `: ${detail}` : ""}`,
);
error.status = res.status;
error.detail = detail;
throw error;
}

if (json === null) throw new Error("接口返回的不是 JSON。");
return json;
}

async function getModelTokenData(tokenParams, token) {
const query = tokenParams.toString();
const workspacePath =
`/backend-api/wham/usage/daily-workspace-user-token-usage-breakdown?${query}`;

try {
return {
accountMode: "workspace",
payload: await apiGet(workspacePath, token),
};
} catch (error) {
const noActiveWorkspace =
error?.status === 400 &&
/no active workspace/i.test(error?.detail || error?.message || "");
if (!noActiveWorkspace) throw error;

console.info(
"[Codex Quota Compass] 当前为个人账号,切换到个人 Token 用量接口。",
);
return {
accountMode: "personal",
payload: await apiGet(
`/backend-api/wham/usage/daily-token-usage-breakdown?${query}`,
token,
),
};
}
}

const getRateLimitObject = (usage = {}) =>
usage?.rate_limit ||
usage?.rateLimit ||
usage?.rate_limits ||
usage?.rateLimits ||
usage?.limits ||
{};

const looksLikeWindow = (value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
return [
"limit_window_seconds",
"window_seconds",
"duration_seconds",
"reset_at",
"resetAt",
"used_percent",
"usedPercent",
"used_ratio",
"remaining",
"limit",
"used",
].some((key) => Object.prototype.hasOwnProperty.call(value, key));
};

const rateLimitWindows = (rateLimit = {}) => {
const windows = [];

if (Array.isArray(rateLimit?.windows)) {
rateLimit.windows.forEach((value, index) => {
if (looksLikeWindow(value)) {
windows.push({
...value,
_window_name: value.name || value.window_name || `window_${index + 1}`,
});
}
});
}

Object.entries(rateLimit || {}).forEach(([key, value]) => {
if (looksLikeWindow(value)) {
windows.push({
...value,
_window_name: key,
});
}
});

return windows;
};

const getWindowDurationSeconds = (quotaWindow) =>
firstFinite(
quotaWindow?.limit_window_seconds,
quotaWindow?.window_seconds,
quotaWindow?.duration_seconds,
);

const pickQuotaWindow = (rateLimit = {}) => {
const windows = rateLimitWindows(rateLimit);
if (windows.length === 0 && looksLikeWindow(rateLimit)) {
return { ...rateLimit, _window_name: "rate_limit" };
}
if (windows.length === 0) return null;

const withDuration = windows.filter((w) => getWindowDurationSeconds(w) > 0);
if (withDuration.length > 0) {
return [...withDuration].sort(
(a, b) =>
Math.abs(getWindowDurationSeconds(a) - CONFIG.TARGET_WINDOW_SECONDS) -
Math.abs(getWindowDurationSeconds(b) - CONFIG.TARGET_WINDOW_SECONDS),
)[0];
}

if (rateLimit.secondary_window) {
return { ...rateLimit.secondary_window, _window_name: "secondary_window" };
}

return windows[0];
};

const toEpochMs = (secondsOrMsOrIso) => {
if (typeof secondsOrMsOrIso === "string") {
const trimmed = secondsOrMsOrIso.trim();
const numeric = Number(trimmed);
if (Number.isFinite(numeric) && numeric > 0) {
return numeric > 1e12 ? numeric : numeric * 1000;
}
const parsed = Date.parse(trimmed);
return Number.isFinite(parsed) ? parsed : NaN;
}

const value = n(secondsOrMsOrIso);
if (!value) return NaN;
return value > 1e12 ? value : value * 1000;
};

const cycleRangeFromWindow = (quotaWindow) => {
const resetMs = toEpochMs(
quotaWindow?.reset_at ||
quotaWindow?.resetAt ||
quotaWindow?.resets_at ||
quotaWindow?.end_at ||
quotaWindow?.window_end,
);

const durationMs = getWindowDurationSeconds(quotaWindow) * 1000;
const nowMs = Date.now();

if (Number.isFinite(resetMs) && durationMs > 0) {
return {
cycleStartMs: resetMs - durationMs,
cycleEndMs: resetMs,
};
}

return {
cycleStartMs: nowMs - CONFIG.TARGET_WINDOW_SECONDS * 1000,
cycleEndMs: nowMs + CONFIG.TARGET_WINDOW_SECONDS * 1000,
};
};

const usedRatioFromWindow = (quotaWindow) => {
if (!quotaWindow) return 0;

const rawRatio = firstFinite(
quotaWindow.used_ratio,
quotaWindow.usedRatio,
quotaWindow.fraction_used,
);
if (rawRatio > 0) return clamp(rawRatio);

const rawPercent = firstFinite(quotaWindow.used_percent, quotaWindow.usedPercent);
if (rawPercent > 0) return clamp(rawPercent / 100);

const limit = firstFinite(
quotaWindow.limit,
quotaWindow.credit_limit,
quotaWindow.credits_limit,
quotaWindow.max,
);

const used = firstFinite(
quotaWindow.used,
quotaWindow.used_credits,
quotaWindow.consumed,
quotaWindow.consumed_credits,
);

const remaining = firstFinite(
quotaWindow.remaining,
quotaWindow.remaining_credits,
quotaWindow.available,
quotaWindow.available_credits,
);

if (limit > 0 && used > 0) return clamp(used / limit);
if (limit > 0 && remaining > 0) return clamp((limit - remaining) / limit);

return 0;
};

const usedPercentTextFromWindow = (quotaWindow) => {
const rawPercent = firstFinite(quotaWindow?.used_percent, quotaWindow?.usedPercent);
if (rawPercent > 0) return `${trimFixed(rawPercent, 2)}%`;

const ratio = usedRatioFromWindow(quotaWindow);
return ratio > CONFIG.EPS ? formatPercentFromRatio(ratio) : "未知";
};

const extractDailyList = (payload) => {
if (Array.isArray(payload)) return payload;

const candidates = [
payload?.data,
payload?.items,
payload?.results,
payload?.daily,
payload?.daily_usage,
payload?.dailyWorkspaceUsageCounts,
payload?.daily_workspace_usage_counts,
payload?.workspace_usage_counts,
];

for (const candidate of candidates) {
if (Array.isArray(candidate)) return candidate;
}

return [];
};

const buildDailyUsageFromModelTokens = (tokenDailyList) => {
return asArray(tokenDailyList).map((row) => {
const dateKey = String(row?.date || "").slice(0, 10);
const tokenModels = asArray(row?.models);
const totals = {
uncached_text_input_tokens: 0,
cached_text_input_tokens: 0,
text_output_tokens: 0,
text_total_tokens: 0,
};
let estimatedUsd = 0;
let hasUnknownPricing = false;

tokenModels.forEach((item) => {
const parts = tokenParts(item);
const total = tokenTotal(item);
totals.uncached_text_input_tokens += parts.uncachedInput;
totals.cached_text_input_tokens += parts.cachedInput;
totals.text_output_tokens += parts.output;
totals.text_total_tokens += total;

if (total <= CONFIG.EPS) return;
const itemUsd = estimateModelUsd(item);
if (itemUsd === null) hasUnknownPricing = true;
else estimatedUsd += itemUsd;
});

return {
...row,
date: dateKey,
totals,
_tokenModels: tokenModels,
_estimatedUsd: estimatedUsd,
_hasUnknownPricing: hasUnknownPricing,
};
}).filter((row) => tokenTotal(row.totals) > CONFIG.EPS);
};

const buildDailyUsageFromCredits = (creditDailyList) => {
return asArray(creditDailyList)
.map((row) => {
const dateKey = String(row?.date || "").slice(0, 10);
const creditModels = asArray(row?.models).filter(
(item) => n(item?.credits) > CONFIG.EPS,
);
const credits = creditModels.reduce(
(sum, item) => sum + n(item?.credits),
0,
);

return {
...row,
date: dateKey,
_creditModels: creditModels,
_credits: credits,
_estimatedUsd: creditsToUsd(credits),
};
})
.filter((row) => row._credits > CONFIG.EPS);
};

const makeDailyRow = (item) => {
const dateKey = String(item?.date || "").slice(0, 10);
return {
...item,
totals: item?.totals || item || {},
_displayDate: dateKey,
_dateKey: dateKey,
_sortTs: dayStartMs(dateKey),
};
};

const getCurrentCycleDailyRows = (
dailyList,
cycleStartMs,
cycleEndMs,
nowMs = Date.now(),
) => {
const currentCycleList = [];

const cycleStartDayMs = dayStartMs(dateKeyFromMs(cycleStartMs));
const effectiveCycleEndMs = Math.min(cycleEndMs, nowMs);
const cycleEndDayMs = dayStartMs(dateKeyFromMs(effectiveCycleEndMs));

asArray(dailyList).forEach((item) => {
const dateKey = String(item?.date || "").slice(0, 10);
const bucketStart = dayStartMs(dateKey);
if (!Number.isFinite(bucketStart)) return;
if (bucketStart > nowMs) return;

const row = makeDailyRow(item);

if (bucketStart >= cycleStartDayMs && bucketStart <= cycleEndDayMs) {
currentCycleList.push(row);
}
});

return currentCycleList.sort((a, b) => a._sortTs - b._sortTs);
};

const getStats = (list) => {
return list.reduce(
(acc, row) => {
const totals = row.totals || {};
const parts = tokenParts(totals);

acc.tokens += tokenTotal(totals);
acc.uncachedInputTokens += parts.uncachedInput;
acc.cachedInputTokens += parts.cachedInput;
acc.outputTokens += parts.output;
acc.estimatedUsd += n(row._estimatedUsd);
acc.hasUnknownPricing ||= Boolean(row._hasUnknownPricing);

return acc;
},
{
tokens: 0,
uncachedInputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
estimatedUsd: 0,
hasUnknownPricing: false,
},
);
};

const getCreditStats = (list) => {
const credits = asArray(list).reduce(
(sum, row) => sum + n(row?._credits),
0,
);
return {
credits,
estimatedUsd: creditsToUsd(credits),
};
};

const getModelDisplayName = (item = {}) =>
normalizeModelName(
item.model || item.model_id || item.model_name || item.name || item.id,
);

const isGpt56Model = (modelName) =>
/^gpt[-_.]?5[.-]?6(?:$|[-_.])/.test(normalizeModelName(modelName));

const aggregateModelUsage = (list) => {
const modelTotals = new Map();

const ensureModel = (name, speed = "standard") => {
const normalizedName = normalizeModelName(name);
const normalizedSpeed = normalizeSpeed(speed);
const key = `${normalizedName}
最新回复 (5)
  • Anoyou 08-04 14:07
    1

    错误: 400 : No active workspace found for the current account.

  • 卡卡罗特 楼主 08-04 14:11
    2

    要不让codex排查一下问题?我两个team账号,一个月限一个周限都能看 ^-^

  • 灵议元识 08-04 15:07
    3

    是必须要安装完脚本后第二天才能统计吗,我这也是说日接口不可用

  • liansishen 08-04 15:46
    4

    加个书签,晚上回家了看看我那两个只有 60 多刀额度的号是咋回事

  • wangdakee 08-04 16:34
    5

    帮佬友改好了,原汤化原石


    // ==UserScript==
    // @name Codex Quota Compass (GPT-5.6 Model Stats)
    // @namespace http://tampermonkey.net/
    // @version 3.4.0
    // @description 兼容个人账号:展示真实每日 Token,并按模型/速度权重折算 Credits
    // @author Jun Zhao, Canxin, loongphy
    // @match https://chatgpt.com/codex/cloud/settings/analytics*
    // @grant GM_addStyle
    // @run-at document-idle
    // ==/UserScript==

    (function () {
    "use strict";

    if (window.__codexQuotaCompassInjected) return;
    window.__codexQuotaCompassInjected = true;

    const CONFIG = {
    TARGET_WINDOW_SECONDS: 7 * 24 * 60 * 60,
    DAY_MS: 24 * 60 * 60 * 1000,
    USE_UTC_DAY: true,
    EPS: 1e-9,
    USD_PER_CREDIT: 40 / 1000,
    };

    const HTML_ESCAPE = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
    };

    const escapeHtml = (value) =>
    String(value ?? "").replace(/[&<>"']/g, (ch) => HTML_ESCAPE[ch]);

    const n = (value) => {
    if (typeof value === "string") value = value.replace(/,/g, "").trim();
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : 0;
    };

    const clamp = (value, min = 0, max = 1) =>
    Math.min(max, Math.max(min, n(value)));

    const trimFixed = (value, digits = 2) => {
    const str = n(value).toFixed(digits);
    return str.replace(/\.?0+$/, "");
    };

    const firstFinite = (...values) => {
    for (const value of values) {
    const num = n(value);
    if (Number.isFinite(num) && num > 0) return num;
    }
    return 0;
    };

    const asArray = (value) => (Array.isArray(value) ? value : []);

    const fmtNum = (value) => {
    const num = n(value);
    const abs = Math.abs(num);
    const sign = num < 0 ? "-" : "";
    if (abs >= 1e12) return sign + trimFixed(abs / 1e12, 2) + "T";
    if (abs >= 1e9) return sign + trimFixed(abs / 1e9, 2) + "B";
    if (abs >= 1e6) return sign + trimFixed(abs / 1e6, 2) + "M";
    if (abs >= 1e3) return sign + trimFixed(abs / 1e3, 2) + "K";
    return num.toLocaleString();
    };

    const fmtUsd = (value) => `$ ${n(value).toFixed(2)}`;

    const tokenParts = (obj = {}) => ({
    uncachedInput: n(obj.uncached_text_input_tokens),
    cachedInput: n(obj.cached_text_input_tokens),
    output: n(obj.text_output_tokens),
    });

    const tokenTotal = (obj = {}) => {
    const total = n(obj.text_total_tokens);
    const parts = tokenParts(obj);
    const derived = parts.uncachedInput + parts.cachedInput + parts.output;
    return total > 0 ? total : derived;
    };

    const normalizeModelName = (value) =>
    String(value || "UNKNOWN").trim().toLowerCase();

    const normalizeSpeed = (value, fallback = "standard") =>
    String(value || fallback).trim().toLowerCase();

    const fmtCalcNumber = (value, digits = 6) => trimFixed(value, digits);

    const fmtCredits = (value) => trimFixed(value, 3);

    const fmtSharePercent = (ratio) =>
    `${fmtCalcNumber(clamp(ratio) * 100, 2)}%`;

    const formatModelCreditCalculation = (row = {}) => [
    `${row.name || "该模型"} · ${row.speed || "unknown"}`,
    "按每天的模型/速度相对权重,对当天真实 Credits 进行折算。",
    `${fmtCredits(row.estimatedCredits)} Credits × $${CONFIG.USD_PER_CREDIT.toFixed(
    2,
    )} = $${n(row.estimatedUsd).toFixed(2)}`,
    "该分配不代表模型级原始 Token。",
    ].join("\n");

    const fmtEstimatedUsd = (value) => fmtUsd(value);

    const pad2 = (value) => String(value).padStart(2, "0");

    const dateKeyFromMs = (ms) => {
    const d = new Date(ms);
    if (!Number.isFinite(d.getTime())) return "";
    if (CONFIG.USE_UTC_DAY) return d.toISOString().slice(0, 10);
    return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
    };

    const dayStartMs = (dateKey) => {
    const [year, month, day] = String(dateKey || "")
    .slice(0, 10)
    .split("-")
    .map(Number);
    if (!year || !month || !day) return NaN;
    return CONFIG.USE_UTC_DAY
    ? Date.UTC(year, month - 1, day)
    : new Date(year, month - 1, day).getTime();
    };

    const formatCycleDateTime = (ms) => {
    const d = new Date(ms);
    if (!Number.isFinite(d.getTime())) return "未知";

    return `${d.getFullYear()}/${pad2(d.getMonth() + 1)}/${pad2(
    d.getDate(),
    )} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
    };

    const formatPercentFromRatio = (ratio) => {
    const pct = clamp(ratio) * 100;
    return `${pct >= 10 ? pct.toFixed(1) : pct.toFixed(2)}%`;
    };

    async function getSessionAuth() {
    try {
    const res = await fetch("/api/auth/session", {
    credentials: "include",
    cache: "no-store",
    headers: {
    Accept: "application/json",
    "Cache-Control": "no-cache",
    Pragma: "no-cache",
    },
    });
    if (!res.ok) return null;
    const session = await res.json();
    const token =
    session?.accessToken ||
    session?.access_token ||
    session?.token ||
    null;
    const accountId =
    session?.account?.id ||
    session?.account_id ||
    session?.accountId ||
    null;

    if (!token || !accountId) {
    console.warn(
    "[Codex Quota Compass] Current session did not include the required token/account ID.",
    );
    return null;
    }

    return { token, accountId };
    } catch (error) {
    console.warn(
    "[Codex Quota Compass] Failed to fetch current session authentication.",
    error,
    );
    return null;
    }
    }

    async function apiGet(path, auth) {
    const res = await fetch(path, {
    headers: {
    Authorization: `Bearer ${auth.token}`,
    "ChatGPT-Account-Id": auth.accountId,
    Accept: "application/json",
    "Cache-Control": "no-cache",
    Pragma: "no-cache",
    },
    credentials: "include",
    cache: "no-store",
    });

    const text = await res.text();
    let json = null;

    if (text) {
    try {
    json = JSON.parse(text);
    } catch {
    // ignore parse error
    }
    }

    if (!res.ok) {
    const detail =
    json?.detail ||
    json?.message ||
    (typeof json?.error === "string" ? json.error : json?.error?.message) ||
    text.slice(0, 200);

    throw new Error(
    `${path}: ${res.status} ${res.statusText}${detail ? `: ${detail}` : ""}`,
    );
    }

    if (json === null) throw new Error("接口返回的不是 JSON。");
    return json;
    }

    const getRateLimitObject = (usage = {}) =>
    usage?.rate_limit ||
    usage?.rateLimit ||
    usage?.rate_limits ||
    usage?.rateLimits ||
    usage?.limits ||
    {};

    const looksLikeWindow = (value) => {
    if (!value || typeof value !== "object" || Array.isArray(value)) return false;
    return [
    "limit_window_seconds",
    "window_seconds",
    "duration_seconds",
    "reset_at",
    "resetAt",
    "used_percent",
    "usedPercent",
    "used_ratio",
    "remaining",
    "limit",
    "used",
    ].some((key) => Object.prototype.hasOwnProperty.call(value, key));
    };

    const rateLimitWindows = (rateLimit = {}) => {
    const windows = [];

    if (Array.isArray(rateLimit?.windows)) {
    rateLimit.windows.forEach((value, index) => {
    if (looksLikeWindow(value)) {
    windows.push({
    ...value,
    _window_name: value.name || value.window_name || `window_${index + 1}`,
    });
    }
    });
    }

    Object.entries(rateLimit || {}).forEach(([key, value]) => {
    if (looksLikeWindow(value)) {
    windows.push({
    ...value,
    _window_name: key,
    });
    }
    });

    return windows;
    };

    const getWindowDurationSeconds = (quotaWindow) =>
    firstFinite(
    quotaWindow?.limit_window_seconds,
    quotaWindow?.window_seconds,
    quotaWindow?.duration_seconds,
    );

    const pickQuotaWindow = (rateLimit = {}) => {
    const windows = rateLimitWindows(rateLimit);
    if (windows.length === 0 && looksLikeWindow(rateLimit)) {
    return { ...rateLimit, _window_name: "rate_limit" };
    }
    if (windows.length === 0) return null;

    const withDuration = windows.filter((w) => getWindowDurationSeconds(w) > 0);
    if (withDuration.length > 0) {
    return [...withDuration].sort(
    (a, b) =>
    Math.abs(getWindowDurationSeconds(a) - CONFIG.TARGET_WINDOW_SECONDS) -
    Math.abs(getWindowDurationSeconds(b) - CONFIG.TARGET_WINDOW_SECONDS),
    )[0];
    }

    if (rateLimit.secondary_window) {
    return { ...rateLimit.secondary_window, _window_name: "secondary_window" };
    }

    return windows[0];
    };

    const toEpochMs = (secondsOrMsOrIso) => {
    if (typeof secondsOrMsOrIso === "string") {
    const trimmed = secondsOrMsOrIso.trim();
    const numeric = Number(trimmed);
    if (Number.isFinite(numeric) && numeric > 0) {
    return numeric > 1e12 ? numeric : numeric * 1000;
    }
    const parsed = Date.parse(trimmed);
    return Number.isFinite(parsed) ? parsed : NaN;
    }

    const value = n(secondsOrMsOrIso);
    if (!value) return NaN;
    return value > 1e12 ? value : value * 1000;
    };

    const cycleRangeFromWindow = (quotaWindow) => {
    const resetMs = toEpochMs(
    quotaWindow?.reset_at ||
    quotaWindow?.resetAt ||
    quotaWindow?.resets_at ||
    quotaWindow?.end_at ||
    quotaWindow?.window_end,
    );

    const durationMs = getWindowDurationSeconds(quotaWindow) * 1000;
    const nowMs = Date.now();

    if (Number.isFinite(resetMs) && durationMs > 0) {
    return {
    cycleStartMs: resetMs - durationMs,
    cycleEndMs: resetMs,
    };
    }

    return {
    cycleStartMs: nowMs - CONFIG.TARGET_WINDOW_SECONDS * 1000,
    cycleEndMs: nowMs,
    };
    };

    const usedRatioFromWindow = (quotaWindow) => {
    if (!quotaWindow) return 0;

    const rawRatio = firstFinite(
    quotaWindow.used_ratio,
    quotaWindow.usedRatio,
    quotaWindow.fraction_used,
    );
    if (rawRatio > 0) return clamp(rawRatio);

    const rawPercent = firstFinite(quotaWindow.used_percent, quotaWindow.usedPercent);
    if (rawPercent > 0) return clamp(rawPercent / 100);

    const limit = firstFinite(
    quotaWindow.limit,
    quotaWindow.credit_limit,
    quotaWindow.credits_limit,
    quotaWindow.max,
    );

    const used = firstFinite(
    quotaWindow.used,
    quotaWindow.used_credits,
    quotaWindow.consumed,
    quotaWindow.consumed_credits,
    );

    const remaining = firstFinite(
    quotaWindow.remaining,
    quotaWindow.remaining_credits,
    quotaWindow.available,
    quotaWindow.available_credits,
    );

    if (limit > 0 && used > 0) return clamp(used / limit);
    if (limit > 0 && remaining > 0) return clamp((limit - remaining) / limit);

    return 0;
    };

    const usedPercentTextFromWindow = (quotaWindow) => {
    const rawPercent = firstFinite(quotaWindow?.used_percent, quotaWindow?.usedPercent);
    if (rawPercent > 0) return `${trimFixed(rawPercent, 2)}%`;

    const ratio = usedRatioFromWindow(quotaWindow);
    return ratio > CONFIG.EPS ? formatPercentFromRatio(ratio) : "未知";
    };

    const extractDailyList = (payload) => {
    if (Array.isArray(payload)) return payload;

    const candidates = [
    payload?.data,
    payload?.items,
    payload?.results,
    payload?.daily,
    payload?.daily_usage,
    payload?.dailyWorkspaceUsageCounts,
    payload?.daily_workspace_usage_counts,
    payload?.workspace_usage_counts,
    ];

    for (const candidate of candidates) {
    if (Array.isArray(candidate)) return candidate;
    }

    return [];
    };

    const buildDailyUsageFromLegacy = (dailyList) =>
    asArray(dailyList)
    .map((row) => {
    const dateKey = String(row?.date || "").slice(0, 10);
    const totals = row?.totals || {};
    const credits = n(totals.credits);

    return {
    ...row,
    date: dateKey,
    totals,
    _credits: credits,
    _estimatedUsd: credits * CONFIG.USD_PER_CREDIT,
    };
    })
    .filter(
    (row) =>
    row.date &&
    (tokenTotal(row.totals) > CONFIG.EPS ||
    row._credits > CONFIG.EPS ||
    n(row.totals?.turns) > 0),
    );

    const makeDailyRow = (item) => {
    const dateKey = String(item?.date || "").slice(0, 10);
    return {
    ...item,
    totals: item?.totals || item || {},
    _displayDate: dateKey,
    _dateKey: dateKey,
    _sortTs: dayStartMs(dateKey),
    };
    };

    const getCurrentCycleDailyRows = (
    dailyList,
    cycleStartMs,
    cycleEndMs,
    nowMs = Date.now(),
    ) => {
    const currentCycleList = [];

    const cycleStartDayMs = dayStartMs(dateKeyFromMs(cycleStartMs));
    const effectiveCycleEndMs = Math.min(cycleEndMs, nowMs);
    const cycleEndDayMs = dayStartMs(dateKeyFromMs(effectiveCycleEndMs));

    asArray(dailyList).forEach((item) => {
    const dateKey = String(item?.date || "").slice(0, 10);
    const bucketStart = dayStartMs(dateKey);
    if (!Number.isFinite(bucketStart)) return;
    if (bucketStart > nowMs) return;

    const row = makeDailyRow(item);

    if (bucketStart >= cycleStartDayMs && bucketStart <= cycleEndDayMs) {
    currentCycleList.push(row);
    }
    });

    return currentCycleList.sort((a, b) => a._sortTs - b._sortTs);
    };

    const getStats = (list) => {
    return list.reduce(
    (acc, row) => {
    const totals = row.totals || {};
    const parts = tokenParts(totals);

    acc.tokens += tokenTotal(totals);
    acc.uncachedInputTokens += parts.uncachedInput;
    acc.cachedInputTokens += parts.cachedInput;
    acc.outputTokens += parts.output;
    acc.credits += n(totals.credits);
    acc.turns += n(totals.turns);
    acc.estimatedUsd += n(row._estimatedUsd);

    return acc;
    },
    {
    tokens: 0,
    uncachedInputTokens: 0,
    cachedInputTokens: 0,
    outputTokens: 0,
    credits: 0,
    turns: 0,
    estimatedUsd: 0,
    },
    );
    };

    const getModelDisplayName = (item = {}) =>
    normalizeModelName(
    item.model || item.model_id || item.model_name || item.name || item.id,
    );

    const isGpt56Model = (modelName) =>
    /^gpt[-_.]?5[.-]?6(?:$|[-_.])/.test(normalizeModelName(modelName));

    const aggregateModelUsage = (modelDistributionList, dailyUsageList) => {
    const modelTotals = new Map();
    const dailyCredits = new Map(
    asArray(dailyUsageList).map((row) => [
    String(row?.date || row?._dateKey || "").slice(0, 10),
    n(row?.totals?.credits),
    ]),
    );
    const datesWithModelWeights = new Set();

    const ensureModel = (name, speed = "standard") => {
    const normalizedName = normalizeModelName(name);
    const normalizedSpeed = normalizeSpeed(speed);
    const key = `${normalizedName}
* 帖子来源Linux.do
返回